0% acharam este documento útil (0 voto)
7 visualizações

Hello World em Shellcode

Direitos autorais
© © All Rights Reserved
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
7 visualizações

Hello World em Shellcode

Direitos autorais
© © All Rights Reserved
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
Você está na página 1/ 9

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.

"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.
1/9
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:

2/9
$ 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:

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.

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

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

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
5/9
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

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:

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.

7/9
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:

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

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

9/9

Você também pode gostar