Ao escrever um valor em uma região de memória (pilha) devemos nos preocupar em escrever de forma correta respeitando a convenção little endian ou big endian.
Uma das formas de colocar valores na pilha é através da instrução assembly push
. Ao utilizar o push devemos nos preocupar em inverter todos os bytes do valor que queremos escrever na memória e lembrar de respeitar a LIFO (Last In First Out), ou seja, também colocar os valores que queremos do final para o início.
Logo , se precisamos colocar a string /bin/sh na pilha, devemos lembrar que:
- A memória contém uma sequência de bytes. Para a CPU não existe o conceito de string, etc. O que ela reconhece são bytes. Uma string para um programa de alto nível nada mais é que uma seqência de bytes que termina com 0x00 (que conhecemos como NULL BYTE!)
- O push, em 32 bits ( 32 bits / 8 bits = 4 bytes), coloca blocos de 4 em 4 bytes. Se você usa um push com menos de 4 bytes o assembler vai tratar de completar com NULL BYTES (0x00) o valor. Seria como se tívemos um campo que obrigatoriamente devemos colocar 4 números. Se colocarmos 9 o programa completará com zeros para ficar 0009. Logo um
push 0x01
, se tornará um 0x00 00 00 01, formando assim 4 bytes. - Ao fazer um PUSH , o registrador ESP é decrementado de 4 bytes e os 4 bytes (arquitetura 32bits) passados são colocados no endereço de memória que o registrador ESP aponta no momento.
Vamos observar na prática :
O código abaixo tem a finalidade de chamar a syscall execve passando para ela a string /bin/sh , que é o caminho do binário que ela deve executar.
[BITS 32]
; nasm -f elf32 empilha-by-push.asm -o empilha-by-push.o
; ld -m elf_i386 empilha-by-push.o -o empilha-by-push
; gdb -q empilha-by-push
section .text
global _start
_start:
xor eax,eax
push eax ; Insere NULL BYTES na pilha para indicar o fim da string
; Insere na pilha /bin//sh , escrevendo de forma invertida
; em blocos de 4 bytes
; python -c "print '/bin//sh'.encode('hex')" => 2f62696e2f2f7368
push 0x68732f2f ; hs//
push 0x6e69622f ; nib/
; construcao dos parametros da execve
mov ebx,esp ; 1o param - endereco da string do comando a ser executado
mov ecx,eax ; 2o param - 0 = sem argumentos
mov al,0xb ; valor da syscall (0xb = 11 = execve)
int 0x80 ; chama a syscall
Vamos focar nos push's. Bem a string (sequencia de bytes) que queremos colocar na memória é /bin//sh, em hexa : 2f 62 69 6e 2f 2f 73 68.
observação: colocamos duas barras após o bin e antes do sh para que possamos ter 8 bytes , facilitando assim o push. Lembra que o push coloca blocos de 4 bytes ?
Observe que para escrever na memória devemos inverter os bytes e depois, agrupados em 4 bytes, colocar de forma de trás para frente, o que ficaria:
2f 62 69 6e 2f 2f 73 68 → 68 73 2f 2f + 6e 69 62 2f
Montando os pushs então, colocando primeiro do último para o primeiro bloco, fica :
push 0x68732f2f ; hs//
push 0x6e69622f ; nib/
Isso faz sentido mesmo ? Vamos ver no debug como isso se comporta.
Bem no início da execução temos em (1) as instruções e em (2) e (3) o endereço onde o ESP está apontando, ou seja, o topo do pilha. Observe e compare ao final. O endereço é o 0xffffd730.
Na tela abaixo, depois de executado os push's temos:
1 → Endereço do registrador ESP, que aponta para o topo da pilha (Observe que o endereço é o 0xffffd724 ou seja 12 bytes a menos que o inicial 0xffffd730)
O endereço do ESP foi descolado 12 bytes "para trás", devido os pushs.
Pode haver uma certa confusão de como 0x...24 para 0x...30 tem 12 bytes e não apenas 6 bytes, afinal, 30 - 24 = 6, correto ? Ai que está, precisamos lembrar que estamos tratando do sistema de numeração hexadecimal ( que possui 16 símbolos - 0 1 2 3 4 5 6 7 8 9 A B C D E F ) mas temos como hábito usar o sistema decimal (que possui 10 símbolos - 0 1 2 3 4 5 6 7 8 9 ).
Vejamos, usando nosso amigo python:
python -c "print(0xffffd730 - 0xffffd724)" → 12
python -c "print(0x30 - 0x24)" → 12
Mas como assim ?! Basta contar, lembrando que a contagem não deve ser "vinta quatro", "vinte cinco", "vinte seis" ... "vinte nove", mas sim "dois quatro" , "dois cinco" , "dois seis" , ... , "dois nove" , "dois , letra a" , "dois , letra b" ... Percebem a diferença ? Veja:
24 - 25 - 26 - 27 - 28 - 29 - 2A - 2B - 2C - 2D - 2E - 2F - 30 = 12 bytes de diferença entre o 24 (dois quatro ) e o 30 (três zero)
2 → As instruç!oes que foram executadas ( os push's )
3 → A Pilha depois da execução dos push's
4 → Uma outra forma de ver o que tem no endereço de memória apontado pelo ESP, ou seja, uma outra forma de visualizar o conteúdo da pilha.
O que observamos é:
Fizemos um push eax, que é zero, e foram colocados 4 bytes 0x00. Observe na imagem (4) que eles estão no final, na linha debaixo. Primeiros a entrar ficam embaixo da pilha. (LIFO!)
O próximo push é push 0x68732f2f ; hs//
, ou seja, o final da string, de trás pra frente. Agora olha que interessante nós passamos 68 73 2f 2f , no push. Mas observe como eles foram colocados na memória (4) na forma 2f 2f 73 68 ( / / s h ), ou seja, já de forma "desinvertida"
O próximo push é o push 0x6e69622f ; nib/
. Mais uma vez eles foram colocados na memória (4) na forma 2f 62 69 6e ( / b i n ), ou seja, já de forma "desinvertiva"
Ao final, o ESP aponta para a região da memória que tem o primeiro caracter da string de forma correta , que é o / (0x2f)
O que acontece é que quando você escreve em blocos de bytes na memória a "CPU" já trata de desinverter os blocos. Por isso que , se você tentar fazer um push sem inverter os bytes, eles serão colocados na memória de forma invertida.
Observe também que o PUSH, como colocado acima, fez com que o ESP se deslocasse para endereços menores. Foi de 0xFFFFD730 para 0xFFFFD724
E se for de byte em byte ?!
Agora, isso também é verdade se escrevermos de byte em byte na pilha , ao invés de usar blocos de 4 bytes com as instruções push ? Vamos verificar.
Vamos ao código que terá a mesma finalidade do código anterior mas ao invés de usar PUSH vamos usar mov para colocar a string byte a byte na pilha.
[BITS 32]
section .text
global _start
_start:
xor eax,eax ; zera o eax que vai ser usado para
; definir o valor da syscall
; Colocando a string na pilha byte a byte em forma direto
mov byte [esp],0x2f ; /
mov byte [esp+1],0x62 ; b
mov byte [esp+2],0x69 ; i
mov byte [esp+3],0x6e ; n
mov byte [esp+4],0x2f ; /
mov byte [esp+5],0x73 ; s
mov byte [esp+6],0x68 ; h
mov byte [esp+7],0x00 ; NULL BYTE
; construcao dos parametros da execve
mov ebx,esp ; 1o param - endereco da string do comando
; a ser executado
mov ecx,eax ; segundo parametro - 0 = sem argumentos
mov al,0xb ; valor da syscall (0xb = 11 = execve)
int 0x80 ; chama a syscall
Na imagem abaixo temos
1 → instruções responsáveis por escrever byte a byte na pilha
2 → endereço apontado para o topo da pilha antes de começar a escrita ( 0xFFFFD730 ). Observe este valor de ESP do mesmo modo como fizemos quando observamos o código anterior que usava PUSH's
3 → valores encontrados no endereço (pilha) apontado pelo ESP
Vamos executar o código passo a passo e verificar calmamente a escrita dos valores em memória. Observe o ESP e o conteúdo da pilha!
Executaremos inscrução por instrução usando o comando ni
(next instruction do gdb) e você deve observar na janela lateral o que está ocorrendo com o registrador ESP e o que está ocorrendo com a stack (pilha).
Usaremos ainda o comando x/8b $esp
, para pedir que o gdb nos mostre 8 bytes a partir do endereço apontado pelo registrador ESP.
Na primeira execução nada mudou no ESP nem na memória, afinal, apenas foi executado um xor eax,eax
, ou seja, zeramos o eax.
A partir da segunda instrução você verá que o conteúdo da pilha começa a ser modificado mas o ESP permanece inalterado, afinal, não estamos "mexendo com ele" apenas usando-o para apontar o local da memória onde queremos escrever.
Observe ainda que o GEF tenta já interpretar os valores que estamos colocando na pilha e já começa a mostrar o /bin ... só que em determinado momento (após os 4 bytes iniciais) ele "se perde" e depois se acha quando inserimos o NULL BYTE, fechando assim a string.
Observe que, desta forma, eu não preciso me preocupar em inverter os bytes , me preocupar com o LIFO, etc. Eu simplesmente mando escrever o byte que eu quero , na posição de memória que eu quero, e a CPU coloca lá. Não tem o que inverter, correto ? Afinal, estamos tratando de um byte apenas.
Ao final, como podem observar, teremos o ESP também apontando para o início da nossa string desejada. Não mudou e apenas estamos escrevendo byte a byte a partir dele para frente!
Veja, esse código não deve ser utilizado assim, pois a escrita do ESP para regiões posteriores pode e vai sobrescrever dados na pilha. Se não fosse uma POC, deveríamos ter reservado espaço na pilha para escrever nossos dados, como é esperado. Isso pode ser feito , subtraindo a quantidade de bytes desejados do ESP, fazendo ele se movimentar "pra trás", ou seja, para endereços menores ( como acontece com o push) para então poder escrever os dados. O código ficaria:
[BITS 32]
section .text
global _start
_start:
xor eax,eax ; zera o eax que vai ser usado
; para definir o valor da syscall
sub esp,8 ; separando 8 bytes na pilha <------
mov byte [esp],0x2f ; /
mov byte [esp+1],0x62 ; b
mov byte [esp+2],0x69 ; i
mov byte [esp+3],0x6e ; n
mov byte [esp+4],0x2f ; /
mov byte [esp+5],0x73 ; s
mov byte [esp+6],0x68 ; h
mov byte [esp+7],0x00 ; NULL BYTE
; construcao dos parametros da execve
mov ebx,esp ; 1o param - endereco da string do comando
mov ecx,eax ; 2o param - 0 = sem argumentos
mov al,0xb ; valor da syscall (0xb = 11 = execve)
int 0x80 ; chama a syscall
Vamos executar e ver o ESP voltando 8 bytes e depois escrever os bytes a partir deste novo ESP, evitando assim de sobrescrever dados essencias da pilha.
Observe que o ESP "subiu" abrindo espaço na pilha para escrita dos valores evitando assim sobrescrever possíveis dados essenciais.
A idéia aqui é de estudar e melhorar o entendimento. Obviamente o segundo processo de escrever byte a byte toda a string desejada, apesar de nos livrar da preocupação de "pensar invertido", consome muito mais opcodes e possivelmente ciclos de CPU. Em um processo de exploração , por exemplo, onde dependendo cada byte conta, não dá para abrir mão dos push's e eles estão ai para serem usados.
Em tempo, fica uma dica de um script criado pelo grande Polverari (https://blog.polverari.com.br/) que facilita demaisss na hora de montar os push's "invertidos"
> echo -ne "/bin//sh" | rev | xxd -p | fold -w8 | awk '{print "push 0x"$1}'
push 0x68732f2f
push 0x6e69622f
É isso, até a próxima.