BROP - Blind Return Oriented Programming
Informações Básicas
O objetivo deste ataque é ser capaz de abusar de um ROP via um buffer overflow sem qualquer informação sobre o binário vulnerável. Este ataque é baseado no seguinte cenário:
Uma vulnerabilidade na pilha e conhecimento de como acioná-la.
Uma aplicação de servidor que reinicia após uma falha.
Ataque
1. Encontrar o offset vulnerável enviando um caractere a mais até que uma falha do servidor seja detectada
2. Brute-force canário para vazá-lo
3. Brute-force endereços RBP e RIP armazenados na pilha para vazá-los
Você pode encontrar mais informações sobre esses processos aqui (BF Forked & Threaded Stack Canaries) e aqui (BF Addresses in the Stack).
4. Encontrar o gadget de parada
Este gadget basicamente permite confirmar que algo interessante foi executado pelo gadget ROP porque a execução não falhou. Normalmente, este gadget será algo que para a execução e está posicionado no final da cadeia ROP ao procurar gadgets ROP para confirmar que um gadget ROP específico foi executado.
5. Encontrar o gadget BROP
Esta técnica usa o gadget ret2csu. E isso ocorre porque, se você acessar este gadget no meio de algumas instruções, você obtém gadgets para controlar rsi
e rdi
:
Esses seriam os gadgets:
pop rsi; pop r15; ret
pop rdi; ret
Note como com esses gadgets é possível controlar 2 argumentos de uma função a ser chamada.
Além disso, note que o gadget ret2csu tem uma assinatura muito única porque vai estar popando 6 registradores da pilha. Portanto, enviando uma cadeia como:
'A' * offset + canary + rbp + ADDR + 0xdead * 6 + STOP
Se o STOP for executado, isso basicamente significa que um endereço que está popando 6 registradores da pilha foi usado. Ou que o endereço usado também era um endereço STOP.
Para remover esta última opção, uma nova cadeia como a seguinte é executada e não deve executar o gadget STOP para confirmar que o anterior popou 6 registradores:
'A' * offset + canary + rbp + ADDR
Sabendo o endereço do gadget ret2csu, é possível inferir o endereço dos gadgets para controlar rsi
e rdi
.
6. Encontrar PLT
A tabela PLT pode ser pesquisada a partir de 0x400000 ou do endereço RIP vazado da pilha (se PIE estiver sendo usado). As entradas da tabela são separadas por 16B (0x10B), e quando uma função é chamada, o servidor não falha mesmo que os argumentos não estejam corretos. Além disso, verificar o endereço de uma entrada na PLT + 6B também não falha pois é o primeiro código executado.
Portanto, é possível encontrar a tabela PLT verificando os seguintes comportamentos:
'A' * offset + canary + rbp + ADDR + STOP
-> sem falha'A' * offset + canary + rbp + (ADDR + 0x6) + STOP
-> sem falha'A' * offset + canary + rbp + (ADDR + 0x10) + STOP
-> sem falha
7. Encontrando strcmp
A função strcmp
define o registrador rdx
para o comprimento da string sendo comparada. Note que rdx
é o terceiro argumento e precisamos que ele seja maior que 0 para depois usar write
para vazar o programa.
É possível encontrar a localização de strcmp
na PLT com base em seu comportamento usando o fato de que agora podemos controlar os 2 primeiros argumentos das funções:
strcmp(<endereço não lido>, <endereço não lido>) -> falha
strcmp(<endereço não lido>, <endereço lido>) -> falha
strcmp(<endereço lido>, <endereço não lido>) -> falha
strcmp(<endereço lido>, <endereço lido>) -> sem falha
É possível verificar isso chamando cada entrada da tabela PLT ou usando o caminho lento da PLT, que basicamente consiste em chamar uma entrada na tabela PLT + 0xb (que chama dlresolve
) seguido na pilha pelo número da entrada que se deseja sondar (começando em zero) para escanear todas as entradas PLT a partir da primeira:
strcmp(<endereço não lido>, <endereço lido>) -> falha
b'A' * offset + canary + rbp + (BROP + 0x9) + RIP + (BROP + 0x7) + p64(0x300) + p64(0x0) + (PLT + 0xb ) + p64(ENTRY) + STOP
-> Vai falharstrcmp(<endereço lido>, <endereço não lido>) -> falha
b'A' * offset + canary + rbp + (BROP + 0x9) + p64(0x300) + (BROP + 0x7) + RIP + p64(0x0) + (PLT + 0xb ) + p64(ENTRY) + STOP
strcmp(<endereço lido>, <endereço lido>) -> sem falha
b'A' * offset + canary + rbp + (BROP + 0x9) + RIP + (BROP + 0x7) + RIP + p64(0x0) + (PLT + 0xb ) + p64(ENTRY) + STOP
Lembre-se que:
BROP + 0x7 aponta para
pop RSI; pop R15; ret;
BROP + 0x9 aponta para
pop RDI; ret;
PLT + 0xb aponta para uma chamada a dl_resolve.
Tendo encontrado strcmp
, é possível definir rdx
para um valor maior que 0.
Note que geralmente rdx
já terá um valor maior que 0, então este passo pode não ser necessário.
8. Encontrando Write ou equivalente
Finalmente, é necessário um gadget que exfiltra dados para exfiltrar o binário. E neste momento é possível controlar 2 argumentos e definir rdx
maior que 0.
Existem 3 funções comuns que poderiam ser abusadas para isso:
puts(data)
dprintf(fd, data)
write(fd, data, len(data)
No entanto, o artigo original menciona apenas a write
, então vamos falar sobre isso:
O problema atual é que não sabemos onde a função write está dentro da PLT e não sabemos um número de fd para enviar os dados para nosso socket.
No entanto, sabemos onde está a tabela PLT e é possível encontrar write com base em seu comportamento. E podemos criar várias conexões com o servidor e usar um FD alto esperando que corresponda a algumas de nossas conexões.
Assinaturas de comportamento para encontrar essas funções:
'A' * offset + canary + rbp + (BROP + 0x9) + RIP + (BROP + 0x7) + p64(0) + p64(0) + (PLT + 0xb) + p64(ENTRY) + STOP
-> Se houver dados impressos, então puts foi encontrado'A' * offset + canary + rbp + (BROP + 0x9) + FD + (BROP + 0x7) + RIP + p64(0x0) + (PLT + 0xb) + p64(ENTRY) + STOP
-> Se houver dados impressos, então dprintf foi encontrado'A' * offset + canary + rbp + (BROP + 0x9) + RIP + (BROP + 0x7) + (RIP + 0x1) + p64(0x0) + (PLT + 0xb ) + p64(STRCMP ENTRY) + (BROP + 0x9) + FD + (BROP + 0x7) + RIP + p64(0x0) + (PLT + 0xb) + p64(ENTRY) + STOP
-> Se houver dados impressos, então write foi encontrado
Exploração Automática
Referências
Artigo original: https://www.scs.stanford.edu/brop/bittau-brop.pdf
Last updated