Hello World em Shellcode

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.

"Shellcode" é o nome que se dá a um trecho de código destinado a ser injetado, e em seguida executado, dentro do espaço de memória de um programa vulnerável a partir de uma falha que permita ao atacante obter controle sobre o fluxo de execução do mesmo. O objetivo dos primeiros "shellcodes" era abrir um "shell" (chamar o /bin/sh ou algo que o valha). Hoje em dia, há shellcodes que fazem muito mais que isso – alguns criam túneis reversos, outros têm até interface gráfica, a ponto que até o termo "shellcode" deixou de fazer sentido. Por isso, alguns autores modernos chamam apenas de "payload" (carga) do exploit. Nesses artigos, continuarei chamando apenas de "shellcode".

Idealmente, o shellcode precisa ser bastante independente de: frameworks, maquinas virtuais, interpretadores e etc. Sendo assim, o shellcode precisa ser escrito usando apenas componentes básicos do sistema, como registradores, instruções nativas do processador e chamadas do sistema operacional (syscalls).

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:

  • nasm (Compilador assembly)
  • ld (Linker)
  • objdump (Visualizador de arquivos Object)
  • gcc (Compilador C)

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.

Para chamarmos diretamente, precisamos realizar os seguintes passos:

  • Colocar no registrador EAX o valor da syscall desejada
  • Colocar nos demais registradores (EBX, ECX, EDX, ESI, EDI, EPB) os argumentos para a syscall
  • Executar a instrução int 0x80.

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:

Na tabela acima podemos encontrar as informações necessárias para interagirmos com as syscalls diretamente.

Agora que conhecemos o que precisamos fazer, vamos à prática:

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.

A seguir temos um código em assembly que irá executar a syscall exit:

	Section	.text
	
		global _start

	_start:

		mov ebx, 0x0
		mov eax, 0x1
		int 0x80

Aqui salvamos estas linhas de código acima no arquivo chamado exit_v1.asm, agora vamos compilar e linkar:

$ nasm -f elf exit_v1.asm
$ ld -o exit_v1 exit_v1.o

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:

$ objdump -d exit_v1

exit_v1:     file format elf32-i386

Disassembly of section .text:

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:

unsigned char shellcode[] =

"\xbb\x00\x00\x00\x00"
"\xb8\x01\x00\x00\x00"
"\xcd\x80";

int main(void)
{
        int (*f)() =  (int(*)())shellcode;
        f();
}

Examinemos em particular o trecho de código apresentado abaixo:

int (*f)()=(int(*)())shellcode;

Esta linha de código especifica a declaração da variável f e a define como um ponteiro para a posição de memória em que nosso shellcode está. Esta definição pode parecer confusa para alguns devido à sintaxe repleta de parênteses que a linguagem C exige para representar ponteiros para funções. Feito isso, basta chamar a função f do mesmo jeito que se chama qualquer outra função da linguagem C, usando o operador (). É isso que faz a linha seguinte, que contém apenas f();.

Este pequeno programa pode ser compilado com o seguinte comando:

$ gcc -o teste_shellcoders teste_shellcoders.c

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:

$ 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.

O código ficará assim:

unsigned char shellcode[] =

"\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:

$ gcc -o teste_shellcoders teste_shellcoders.c
$ strace ./teste_shellcoders

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

Agora que já conseguimos transformar nosso código em assembly em uma string de Object Codes representados em hexadecimal, precisaremos fazer algumas modificações para poder chamá-lo de um shellcode.

Frequentemente, os shellcodes precisam ser injetados em um buffer que pode ser controlado pelo usuário (atacante). Em alguns casos, esse buffer é um array de char e, assim sendo, alguns códigos presentes em nosso shellcode podem ter um significado especial nesse contexto, tornando o shellcode inefetivo. Revisitando a nossa string de Object Codes podemos perceber que ela contém vários códigos "\x00", e este código em particular é interpretado como término de uma string. São exatamente estes códigos que precisamos eliminar.

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.

Uma possível modificação em nosso código é apresentada a seguir:

Section .text
     
        global _start

_start:

        xor ebx, ebx    ;em vez de mov ebx, 0x0
        xor eax, eax    ;zerando o registrador pra evitar lixo de memoria
        mov al, 0x1     ;em vez de mov eax, 0x1
        int 0x80

Compilando, linkando e extraindo os Object Codes:

$ nasm -f elf exit_v3.asm
$ ld -o exit_v3 exit_v3.o
$ objdump -d exit_v3

exit_v3:     file format elf32-i386

Disassembly of section .text:

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:

unsigned char shellcode[] =

"\x31\xdb"
"\x31\xc0"
"\xb0\x01"
"\xcd\x80";

int main(void)
{
        int (*f)() =  (int(*)())shellcode;
        f();
}

Compilando e testando:

$ gcc -o teste_shellcoders teste_shellcoders.c
$ strace ./teste_shellcoders

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:

  • O número do file descriptor em EBX
  • Um ponteiro para a string a ser impressa em ECX
  • O tamanho da string a ser impressa em EDX.

Sendo assim, precisaremos:

  • Escrever a nossa string hello world na memória
  • Colocar o endereço dessa string em ECX
  • Definir o tamanho dessa string em EDX
  • Definir EBX como sendo o número 01 (que, no Linux, corresponde ao file descriptor STDOUT)

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:

Vamos agora escrever o nosso código em assembly que irá colocar as coisas nos seus devidos lugares, executar a syscall write e em seguida executar a syscall exit (para que o shellcode saia de maneira "limpa"), lembrando que os seus Object Codes não podem conter \x00.

Section         .text
     
        global _start

_start:
     
        xor     eax,eax         ;Zerando eax
        mov     al,0x4          ;Colocando na ultima parte de eax o n da syscall write
        xor     ebx,ebx         ;Zerando ebx
        push    ebx             ;Colocando o \0 no final da string
        push    0xa646c72       ;Escrevendo 'rld\n'
        push    0x6f77206f      ;Escrevendo 'o wo'  
        push    0x6c6c6568      ;Escrevendo 'hell'
        mov     ecx,esp         ;Colocando o ponteiro pra string em ecx
        xor     edx,edx         ;Zerando edx
        mov     bl,0x1          ;Colocando em ebx o numero do file descriptor stdout
        mov     dl,0xc          ;Colocando o tamanho da string (12)em edx 
        int     0x80            ;Chamando write
        mov     al,0x1          ;Colocando o numero da syscall exit em eax
        int     0x80            ;Chamando exit

Compilando, linkando e extraindo os Object Codes:

$ nasm -f elf hello_world.asm
$ ld -o hello_world hello_world.o
$ objdump -d hello_world

hello_world:     file format elf32-i386

Disassembly of section .text:

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

Em seguida, repassamos o código em C, o recompilamos e testamos sua execução:

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();
}

$ gcc -o teste_shellcoders teste_shellcoders.c
$ ./teste_shellcoders 
hello world
$

O exemplo utilizado neste post foi escolhido por oferecer uma maneira mais didática de tratar o assunto, de uma forma mais passo-a-passo. Neste primeiro passo, vimos apenas o básico para que nosso código seja executado. No próximo post faremos um shellcode "de verdade".

Comentários
Aceita-se formatação à la TWiki. HTML e scripts são filtrados. Máximo 15KiB.

 
Enviando... por favor aguarde...
Comentário enviado com suceso -- obrigado.
Ele aparecerá quando os moderadores o aprovarem.
Houve uma falha no envio do formulário!
Deixei uma nota para os admins verificarem o problema.
Perdoe-nos o transtorno. Por favor tente novamente mais tarde.
Tímido Visitante Anônimo | 2012-07-03 19:59:53 | permalink | topo

pow cara a muito tempo estava procurando um tutorial que abrangece esses assuntos vc esta de parabéns.

o futuro da internet | 2012-03-18 19:57:44 | permalink | topo

Muito mau explicado.

faltou dominio no assunto.

Danilo Clemente | 2011-10-13 08:49:48 | permalink | topo

Leandro, Muito bom o Artigo, Parabéns!

Você poderia ter comentado sobre Endianness por alto e que o objdump cuida disso automaticamente.

Keep up the good work! =]

Danilo Clemente

Marcos Álvares | 2011-04-29 20:02:01 | permalink | topo

Recentemente vi uns caras que também estão escrevendo bons artigos sobre shellcoding:

Vale a pena dar uma olhada. De repente sai dai a inspiração para o segundo post de shellcode. : ] []'s

Marcos Álvares | 2011-04-29 18:21:06 | permalink | topo

Leandro,

parabéns pelo excelente post! Objetivo, didático e MUITO bem escrito!

Uma sugestão é acrescentar algumas referências nesse artigo. As referências por onde tu estudasses e mais algumas para a turma que desejar se aprofundar no assunto.

Grande abraço.

Marcos Álvares

Ricardo Ulisses | 2011-04-16 15:59:22 | permalink | topo

Frank,

Apesar de ter lhe falado pessoalmente, estava em débito com você por ainda não ter deixado meus parabéns explícitos no blog.

Embora você tenha abordado um dos assuntos mais obscuros para a maioria dos técnicos em computação, conseguiu fazer isso com clareza e elegância. Na minha opinião, você foi muito feliz tanto na medida quanto na forma.

O uso da ferramenta strace para depurar a execução dos programas ajudou bastante a esclarecer os exemplos. Para quem não conhecia a ferramenta, fica a dica que pode ser útil para depurar as syscalls e sinais emitidos/recebidos por executáveis diversos.

Abraço, Ricardo.

Rodrigo Santos Silva | 2011-04-14 01:18:00 | permalink | topo

Post extremamente recomendado! Claro, correto e conciso (CCC). Assim como os demais leitores, estou aguardando os próximos passos!

Parabéns Leandro! (:

Leandro Oliveira | 2011-04-11 14:13:12 | permalink | topo

Pimps, Obrigado!

[]s

-Frank

Leandro Oliveira | 2011-04-07 15:17:56 | permalink | topo

Thanks, Julio.

[]s

-Frank

Leandro Oliveira | 2011-04-07 15:12:30 | permalink | topo

Grande Léo,

Obrigado! Assim que der, eu começo a escrever o próximo.

Sua sua sugestão para o RSS já está no Roadmap do blog.

[]s

-Frank

Leandro Oliveira | 2011-04-07 14:57:58 | permalink | topo

Aê, Heyder.

Valeu pelas sugestões! Realmente não entrei em detalhes sobre truques de assembly para substituir as instruções que geram os indesejáveis "\x00". Em algum dos próximos eu pretendo falar sobre otimizações, ai nesse eu devo explicar melhor isso. -Frank

Heyder Andrade | 2011-04-07 12:33:40 | permalink | topo

Muito bom Leandro,

Estou aguardando ansioso os próximos da série.

Só faltou falar o que era o "bl" e "dl" em:

mov bl,0x1 ;Colocando em ebx o numero do file descriptor stdout mov dl,0xc ;Colocando o tamanho da string (12)em edx

Como você fez em: mov al,0x4 ;Colocando na ultima parte de eax o número da syscall write

|%AL|últimos 8 bits de EAX| |%AX||últimos 16 bits de EAX| |%EAX|todos 32 bits de EAX|

Sugestão para o próximo:

ShellCode of Hello World on Windows.

[]s

Pimpão | 2011-04-07 10:13:29 | permalink | topo

Ótimo Post Leandro...

Parabéns pela didática!

Abraço.

- Pimps.

Frank | 2011-04-06 20:13:51 | permalink | topo

Mestre Kiko,

Obrigado! Nos próximos eu coloco mais referências.

-Frank

Leocadio Tiné | 2011-04-06 19:12:26 | permalink | topo

Muito bom, Leandro! Está de parabéns. Estou ansioso pela parte 2 :)

E uma sugestão ao administrador do site: o RSS feed truncado fica ruim de ler. Seria legal colocar os artigos completos no feed.

[]s

Julio Melo | 2011-04-06 18:42:13 | permalink | topo

Very well done, Leandro.

I can't wait to see the next chapter of your article.

Keep up the good work!

Cheers, mate.

Marco Carnut | 2011-04-06 15:52:18 | permalink | topo

Leandro,

Gostei da abordagem, muito didática! Se eu tivesse lido esse seu texto na época em que aprendi isso ha 1,5 décadas atrás, eu provavelmente teria sofrido bem menos!

Só achei que faltou umas referências para os textos clássicos, mas acho que você terá chance de retificar essa ausência na próxima parte.

-K.