Um payload (shellcode) para Linux é criado usando as syscalls deste sistema. Eu queria criar um payload simples que apenas executasse o comando “ls -l”. Só isso. Como fazer isso? Entrei no site syscalls.kernelgrok.com e vi o que era necessário para executar o “sys_execve”. Os passos são:
- Salvar “0x0B” em EAX
- Salvar em EBX um endereço de memória que contenha uma string com o executável que eu desejo executar
- Salvar em ECX o endereço de um ponteiro de ponteiro (matriz multidimensional) que contenha os argumentos que serão passados para o executável. A documentação do Linux diz que este valor pode ser zero, mas se tiver argumentos a serem passados, ao final do array multidimensional é necessário ter um “ponteiro nulo”
- Salvar em EDX o endereço de um ponteiro de ponteiro (matriz multidimensional) que contenha possíveis variáveis de ambiente que o executável deverá usar. A documentação do Linux diz que este valor pode ser zero, mas se tiver argumentos a serem passados, ao final do array multidimensional é necessário ter um “ponteiro nulo”
Criando o payload em C primeiro
Para simplificar um pouco mais, criei um código em C que chame a função “execve” que executa a syscall que quero chamar. Em um compilador C online qualquer (digitei “C compiler online” sem as aspas no Google e entrei no primeiro resultado) testei o seguinte código seguindo a documentação da função execve ( http://man7.org/linux/man-pages/man2/execve.2.html ):
#include <stdio.h> int main(){ char sh[] = "/bin/sh"; char *args[] = {"/bin/sh", "-c", "ls -l"}; execve(sh, args,0); return 0; }
Este código deu um “warning” falando da declaração implícita da função. Esse “warning” me diz que esqueci de colocar a biblioteca que deveria ser usada. A documentação me diz que a declaração dessa função está em unistd.h. Ok. Adicionei essa biblioteca no meu código, ficando assim:
#include <stdio.h> #include <unistd.h> int main(){ char sh[] = "/bin/sh"; char *args[] = {"/bin/sh", "-c", "ls -l"}; execve(sh, args,0); return 0; }
Agora parece correto, mas ainda não estava retornando a lista de arquivos e pastas do diretório que estou… Por que? Li novamente a documentação e vi que “ao final do array multidimensional é necessário ter um ponteiro nulo”. Talvez seja isso. Adicionei um NULL ao final da matriz multidimensional de argumentos. Meu código ficou assim:
#include <stdio.h> #include <unistd.h> int main(){ char sh[] = "/bin/sh"; char *args[] = {"/bin/sh", "-c", "ls -l", NULL}; execve(sh, args,0); return 0; }
Aháááá! Agora deu certo! Pela descrição dos registradores para chamar a syscall, sei que EAX precisa ter “0x0b” (11 em hexadecimal), em EBX estará o endereço do primeiro argumento da função execve, em ECX estará o endereço da matriz multidimensional que enviei para o segundo argumento e em EDX terá zero.
Pronto. Tudo está pronto para criar o código em Assembly. Já entendi perfeitamente todo o necessário. Vamos nessa!
Passando o payload para Assembly
Para criar um payload/shellcode confiável, eu preciso empurrar as strings para a stack, pegar os endereços de cada string para enviar para os registradores, colocar 0x0B em EAX e criar uma interrupção 0x80 para executar a syscall. Tudo isso gerando o menor código de máquina possível. Ao fim de algum tempo tentando diminuir o código, consegui chegar a isso:
section .text global main global _start main: _start: mov eax, 0x0b ;Aqui copio 11 para EAX push 0x0000006c ;Empurro para a stack o valor ;hexadecimal de "l" na tabela ;ASCII. A stack funciona de ;trás para frente push 0x2d20736c ;Empurro para a stack o valor ;hexadecimal de "ls -" na tabela ;ASCII. A stack funciona de trás ;para frente mov edi, esp ;Em ESP tem o endereço de onde ;começa a string "ls -l" (ou ;seja, ESP tem um ponteiro). ;Copio o valor de ESP para EDI. ;Então em EDI tem um ponteiro ;para a string "ls -l" push 0x0000632d ;Empurro para a stack o valor ;hexadecimal de "-c" na tabela ;ASCII. A stack funciona de ;trás para frente mov edx, esp ;Em ESP tem o endereço de onde ;começa a string "-c" (ou ;seja, ESP tem um ponteiro). ;Copio o valor de ESP para EDX. ;Então em EDX tem um ponteiro ;para o argumento "-c" push 0x0068732f ;Empurro para a stack o valor ;hexadecimal de "/sh" na tabela ;ASCII. A stack funciona de trás ;para frente push 0x6e69622f ;Empurro para a stack o valor ;hexadecimal de "/bin" na ;tabela ASCII. A stack funciona de ;trás para frente mov ebx, esp ;Em ESP tem o endereço de onde ;começa a string "/bin/sh" (ou ;seja, ESP tem um ponteiro). Copio ;o valor de ESP para EBX. ;Então em EBX já tem o valor da ;string do meu executável. Aqui ;tá pronto push 0x00000000 ;Agora vou começar a preparar ;a stack para conter todos os ;meus argumentos. Aqui empurro ;zero para a stack porque ;"ao final do array multidimensional ;é necessário ter um ponteiro nulo" push edi ;Em EDI tem o ponteiro para ;"ls -l". Empurro ele para ;a stack push edx ;Em EDX tem o ponteiro para "-c". ;Empurro ele para a stack push ebx ;Em EBX tem o ponteiro para "/bin/sh". ;Empurro ele para a stack mov ecx, esp ;Em ESP tem um ponteiro para o ;array multidimensional. Copio esse ;ponteiro para ECX mov edx, 0 ;Copio zero para EDX porque não ;usarei variáveis de ambiente int 0x80 ;Gero a interrupção 0x80
Pronto! Salvei esse código como teste.asm. Para gerar o “código objeto” no Linux, usei o nasm com os seguintes comandos:
nasm -f elf32 teste.asm
Para linkar, usei:
ld -s -o teste teste.o
Executei ./teste no terminal e lá estava a listagem de arquivos e pastas do meu diretório atual! Deu certo =D
Meu código gerou o seguinte código de máquina:
“\xB8\x0B\x00\x00\x00\x6A\x6C\x68\x6C\x73\x20\x2D\x89\xE7\x68\x2D\x63\x00\x00\x89\xE2\x68\x2F\x73\x68\x00\x68\x2F\x62\x69\x6E\x89\xE3\x6A\x00\x57\x52\x53\x89\xE1\xBA\x00\x00\x00\x00\xCD\x80”
Consegui um shellcode de 47 bytes! Legal! Bem reduzido! Qual o tamanho que o metasploit gerou? 41 bytes… 6 bytes a menos… Como? Vamos analisar!
Analisando o payload do Metasploit
O payload do Metasploit linux/x86/exec que execute “ls -l” é este:
\x6A\x0B\x58\x99\x52\x66\x68\x2D\x63\x89\xE7\x68\x2F\x73\x68\x00\x68\x2F\x62\x69\x6E\x89\xE3\x52\xE8\x06\x00\x00\x00\x6C\x73\x20\x2D\x6C\x00\x57\x53\x89\xE1\xCD\x80.
Digitei “hex to assembly” no Google e entrei no primeiro site que apareceu ( https://defuse.ca/online-x86-assembler.htm ). Digitei este código hexadecimal e me retornou este código:
0: 6a 0b push 0xb
2: 58 pop eax
3: 99 cdq
4: 52 push edx
5: 66 68 2d 63 pushw 0x632d
9: 89 e7 mov edi,esp
b: 68 2f 73 68 00 push 0x68732f
10: 68 2f 62 69 6e push 0x6e69622f
15: 89 e3 mov ebx,esp
17: 52 push edx
18: e8 06 00 00 00 call 0x23
1d: 6c ins BYTE PTR es:[edi],dx
1e: 73 20 jae 0x40
20: 2d 6c 00 57 53 sub eax,0x5357006c
25: 89 e1 mov ecx,esp
27: cd 80 int 0x80
A primeira diferença que surtiu resultado é o “push 0x0b; pop eax”. O meu “mov eax, 0x0b” gerou 5 bytes (b8 0b 00 00 00), já o “push 0x0b; pop eax” gerou apenas 3 (6a 0b 58).
A segunda diferença que mais visível foi usar o “cdq”. Esta instrução significa “Convert Double to Quad”. Para isso, ela pega o bit de sinal de EAX e preenche EDX com ele. O bit de sinal de EAX é zero, então EDX será zerado com uma instrução de 1 byte apenas (CDQ vale 99), enquanto meu “mov edx, 0” ocupa 5 bytes (ba 00 00 00 00).
Com essas duas pequenas mudanças, o payload do Metasploit conseguiu diminuir 6 bytes em relação ao meu! =D
Bônus
Na última parte do payload, que é preparar a stack para conter os endereços onde estão as strings que serão passadas como argumentos para o executável, o Metasploit tem algo interessante:
- Ele empurra zero para a stack “push edx”
- Chama a instrução que está no endereço offset 0x23. Isso ficará no meio da linha com 20 na frente. Portanto o código irá para “57 53 89 e1 cd 80”. Portato a instrução “sub eax,0x5357006c” muda, já que o código literalmente pula dois bytes da instrução. Os bytes 57 e 53 equivalem respectivamente a “push edi” e “push ebx”. Uma instrução de “call” empurra para a stack o endereço do próximo byte porque quando a função for finalizada, o processador precisa saber para onde voltar. Portanto “call 0x23” empurra para a stack o endereço onde começa a instrução “ins BYTE PTR es:[edi],dx”. Mas espera, este call pula os bytes “6c 73 20 2d 6c 00” e coloca na stack o endereço de “retorno” para 6c. Analisando esses bytes na tabela ASCII, vejo que “6c 73 20 2d 6c 00” equivale a “ls -l”. Ou seja, Ele não empurrou “ls -l” na stack como eu fiz porque não precisava! Ele já estava no código! Assim o autor deste payload só precisou empurrar o endereço onde estava esta string e a instrução “call” resolveu isso com perfeição. Muito legal! =D
O que achou dessa análise? Me conta!
Abraços e até a próxima