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