Hello World em Shellcode
Hello World em Shellcode
blog.tempest.com.br/leandro-oliveira/hello-world-shellcode.html
Quando eu vi pela primeira vez um exploit para uma vulnerabilidade de buffer overflow,
fiquei intrigado com um monte de números incompreensíveis em hexadecimal, os
chamados "shellcodes", que, segundo me contaram, são a parte crucial dos exploits, pois
são eles que nos dão os meios de controlar as máquinas-vítimas. Nesta série de posts,
vou contar o que aprendi sobre como esses códigos são criados. No post de hoje, vou
explicar o bê-a-bá e chegaremos até um "shellcode" que funciona, mas apenas imprime
um "hello world". No próximo post chegaremos a criar um shellcode que realmente abre um
shell.
Boa parte dos shellcodes são gerados através da extração dos Object Codes (linguagem
nativa dos processadores) de um código escrito em assembly, e são representados através
de uma cadeia de valores em hexadecimal, para serem mais facilmente manipulados e
injetados nos programas alvo.
O nosso exemplo será construído no sistema operacional debian lenny 32bits. Usaremos
os seguintes programas:
Usando as Syscalls
O maior objetivo de um shellcode é fazer com que um programa vulnerável funcione como
uma porta de acesso ao Sistema Operacional hospedeiro. E a maneira mais fácil de
interagir com o S.O., é através de suas chamadas de sistema (syscalls).
Para utilizarmos as syscalls no Linux, nós podemos fazer chamadas indiretas através de
funções da libc, ou chama-las diretamente através do assembly.
1/9
Para chamarmos diretamente, precisamos realizar os seguintes passos:
As informações do número da syscall e dos argumentos que elas esperam podem ser
obtidas através dos manuais das syscalls, ou simplesmente decompilando programas que
as utilizam através da libc. Entretanto, para fins didáticos, neste post eu montei uma
tabela contendo as informações necessárias para chamarmos as syscall que
precisaremos. A tabela abaixo mostra como chamamos as syscalls exit e write:
Primeiro vamos começar com uma syscall mais simples, a exit. Conforme a tabela acima,
a exit é a syscall de número 1, sendo assim precisaremos colocar isto no registrador
EAX, e ela espera como argumento um inteiro referente ao código de retorno, e isso
precisa estar no registrador EBX.
Section .text
global _start
_start:
Aqui salvamos estas linhas de código acima no arquivo chamado exit_v1.asm, agora
vamos compilar e linkar:
Feitos os passos acima, teremos um arquivo ELF chamado exit_v1, e agora podemos
extrair os Object Codes deste executável. Vamos usar o objdump para isto:
2/9
$ objdump -d exit_v1
08048060 <_start>:
8048060: bb 00 00 00 00 mov $0x0,%ebx
8048065: b8 01 00 00 00 mov $0x1,%eax
804806a: cd 80 int $0x80
Na coluna do meio temos os Object Codes do nosso código em assembly. Agora o que
precisamos fazer é transformá-los em uma string para ser usada dentro de um programa
em C (o "exploit"), o que ficará assim:
"\xbb\x00\x00\x00\x00"
"\xb8\x01\x00\x00\x00"
"\xcd\x80"
Feito isso, vamos testar. Tudo o que precisamos é de um código em C capaz de executar
a nossa string. O código a seguir faz exatamente isso:
"\xbb\x00\x00\x00\x00"
"\xb8\x01\x00\x00\x00"
"\xcd\x80";
int main(void)
{
int (*f)() = (int(*)())shellcode;
f();
}
int (*f)()=(int(*)())shellcode;
Como o que esperamos do nosso shellcode é que ele simplesmente termine o programa,
se o executarmos da forma "normal" (escrevendo apenas ./teste_shellcoders no
shell de comandos) não veremos nada. Por essa razão, vamos executá-lo por intermédio
do programa strace:
3/9
$ strace ./teste_shellcoders
A última linha do output deste comando nos mostra que o nosso shellcode funcionou
perfeitamente:
Para termos certeza disto, vamos mudar o código de retorno da syscall, de 0 por 1.
Na primeira linha do nosso shellcode, vamos substituir o segundo Object Code, onde está
\x00, vamos colocar \x01.
"\xbb\x01\x00\x00\x00"
"\xb8\x01\x00\x00\x00"
"\xcd\x80";
int main(void)
{
int (*f)() = (int(*)())shellcode;
f();
}
Para testar a mudança, o programa deve ser recompilado e executado novamente com o
auxílio do strace:
4/9
Como previsto, a última linha da saída do strace mudou e agora temos como código de
retorno o número 1. Com isso, temos certeza de que o código de retorno adveio como
resultado da execução do nosso "shellcode" e não de alguma outra chamada do sistema
operacional.
Shellcode Injetável
Existem várias maneiras de fazer isso. Para quem tem familiaridade com assembly isto
pode ser mais trivial. No nosso caso, vamos tentar simplesmente substituir as instruções
que geram estes valores com outras que produzam resultado equivalente.
Section .text
global _start
_start:
08048060 <_start>:
8048060: 31 db xor %ebx,%ebx
8048062: 31 c0 xor %eax,%eax
8048064: b0 01 mov $0x1,%al
8048066: cd 80 int $0x80
Traduzindo os Object Codes acima em uma string teremos o nosso primeiro shellcode.
Para testar, usaremos o nosso programinha teste_shellcoders.c:
"\x31\xdb"
"\x31\xc0"
"\xb0\x01"
"\xcd\x80";
int main(void)
{
int (*f)() = (int(*)())shellcode;
f();
}
Compilando e testando:
6/9
Como podemos perceber o código acima não gerou nenhum Object Code 00, está
significativamente menor e ainda continua funcionando.
Syscall Write
O próximo passo é utilizar a syscall write. Como podemos observar na tabela anterior o
número desta syscall é 04 (colocaremos 04 em EAX), e ela espera os seguintes
parâmetros:
Para escrevermos esta string na memória, utilizaremos a tabela ASCII, a qual irá nos
informar o valor de cada caractere em hexadecimal. A tabela abaixo contém apenas os
caracteres da nossa string a ser impressa e os seus respectivos valores em hexadecimal:
7/9
Section .text
global _start
_start:
08048060 <_start>:
8048060: 31 c0 xor %eax,%eax
8048062: b0 04 mov $0x4,%al
8048064: 31 db xor %ebx,%ebx
8048066: 53 push %ebx
8048067: 68 72 6c 64 0a push $0xa646c72
804806c: 68 6f 20 77 6f push $0x6f77206f
8048071: 68 68 65 6c 6c push $0x6c6c6568
8048076: 89 e1 mov %esp,%ecx
8048078: 31 d2 xor %edx,%edx
804807a: b3 01 mov $0x1,%bl
804807c: b2 0c mov $0xc,%dl
804807e: cd 80 int $0x80
8048080: b0 01 mov $0x1,%al
8048082: cd 80 int $0x80
8/9
unsigned char shellcode[] =
"\x31\xc0\xb0\x04\x31\xdb\x53\x68\x72\x6c\x64\x0a"
"\x68\x6f\x20\x77\x6f\x68\x68\x65\x6c\x6c\x89\xe1"
"\x31\xd2\xb3\x01\xb2\x0c\xcd\x80\xb0\x01\xcd\x80";
int main(void)
{
int (*f)() = (int(*)())shellcode;
f();
}
9/9