Open10

CTF

semiexpsemiexp

CTF をやり始めたので、解いた問題の記録を書いていく

semiexpsemiexp

ding-dong-ting-ping (CakeCTF 2023)

AlpacaHack で解いた。

問題

サーバーに対して次の操作ができる

  1. register: {PREFIX}|user={username}|{timestamp} を暗号化したものを教えてくれる。username は base64 で渡すので、任意のバイト列を渡すことができる。PREFIX は不明。usernameroot が入っていたらだめ
  2. login: register で作ったものを渡すと、username を返してくれる。これが root ならフラグがもらえる。解いてるときは気にしていなかったが、decode() しているので UTF-8 じゃないバイト列になってしまうとエラーらしい

暗号化は 16 byte block の AES-CBC みたいなものだが、通常の CBC では前の block の暗号文と現在の block の平文の XOR をとるのに対し、この問題では前の block の暗号文 の MD5 と現在の block の平文の XOR をとる。

解法

{PREFIX}|user=root|(なにか) を暗号化したものが得られれば勝ち。

block 境界がわからないと何もできないので、まず PREFIX の長さを求めることを考える。
usernameaaaabaaaaaaa... みたいなものを b の位置を変えて渡して、どのブロックまで暗号文が一致しているかを観察することで、{PREFIX}|user= 部分の長さがわかる。今回の場合これが 23 であるとわかる。特に、PREFIX の長さは 17 である。

block 境界がわかったので、username に適当に長いバイト列を渡して、適当な block を見ると、その block の暗号文は AES(md5(その前の block の暗号文) xor (その block の平文)) となる。なので、その block の平文を細工することで、任意の 16 byte block に対して AES(block) を直接求めることができる。

{PREFIX}|user=root|(なにか) を暗号化することを考える。これは次のようにブロックに分割できる:

  1. PREFIX の先頭 16 文字
  2. ?PREFIX の末尾の文字として、?|user=root|...
  3. (なにか) の後ろのほう

1 を暗号化した結果はわかっているので、2 の ? が分かれば先ほどの手続きにより全体を暗号化した結果も求められることになる。? は 1 バイトで、あり得るものは高々 256 通りしかないので、適当に長い username に対して ? を全通り試して先ほどの手続きで正しい暗号化結果になるものを選べばよい。

semiexpsemiexp

janken vs yoshiking 2 (CakeCTF 2023)

AlpacaHack で解いた。

問題

素数 p と、mod p でのランダムな 5 \times 5 行列 M が与えられる。適当な整数 r に対して M^r \pmod p が与えられるので、r \bmod 3 を決定せよ。

解法

mod p での 5 \times 5 行列のうち可逆なものは、乗法に対して群をなすが、その位数は d = p^{10} (p^5 - 1)(p^4 - 1)(p^3 - 1)(p^2 - 1)(p - 1) である (cf. https://note.com/shiny_broom989/n/nb718170f755d) 。M はランダムかつ p は十分大きいので、ほとんどの場合 M は可逆である。

今回、d3 で 6 回割り切れる。ここで、M^{d/3^5} は(少なくとも)だいたいの場合 I となることが実験的に確かめられる。M^{d/3^6} = I となる事象は多分 1/3 くらいの確率でしか起きないので、そうなったらやりなおす。M^{d/3^6} \neq I ならば、M^{r \cdot d/3^6}I, M^{d/3^6}, M^{2\cdot d/3^6} の 3 通りの値しかとらず、このどれになるかは r \bmod 3 によって決まることから、r \bmod 3 を決定できた。

ただ、この解法だと M^{d/3^6} = I とならなくても不正解になる場合がある。このとき何が起きているのかはよくわからない。

semiexpsemiexp

AlpacaHack Round 3 (Crypto)

14 位。

qrime

問題

P(n)n より大きい最小の素数とする。n = pq, r は既知、p, q は素数、p = P(q)r + qP(r) のとき、n を素因数分解せよ。

解法

素数定理より、平均的に P(n) - n \sim O(\log n) なので、P(q) - q を全探索することができる。この値を決め打つと (r は既知なので) pq の二次方程式の形で書けて、q を決定できる。

Rainbow Sweet Alchemist

問題

以下の手順で 1024 ビットの素数を作る。

  1. 64 ビットの素数を 16 個作る。
  2. その積の 2 倍 + 1 が素数かを判定する。素数ならばそれを返し終了する。
  3. 素数でないならば、今持っている 16 個の素数のうち 1 個ランダムに選んで、それを新たに作った 64 ビット素数で置き換え、2 に戻る。

ただし、64 ビット素数を作る過程は deterministic である(i 回目に作った素数が何になるかは既知である)。16 個のうち 1 個選ぶ過程は non-deterministic である。

この過程を 2 回繰り返して素数 p, q を得る。n=pq を素因数分解せよ。

解法

実験すると、↑の過程で n を得るまでに、64 ビット素数を作る過程は 800 回程度しか実行されないと見積もれる。

p=2p_1 p_2 \dots p_{16} + 1, q=2q_1 q_2 \dots q_{16} + 1 (q_i は素数) と書けるので、
\phi(n) = (p-1)(q-1) = 4p_1 \dots p_{16} q_1 \dots q_{16} である。各 p_i, q_i は、800 回の素数生成過程で生成されるもののうちどれかである。

R = \{ r_1, \dots, r_m \} を素数の集合とするとき、2^{r_1 \dots r_m} \equiv 1 \pmod n\{ p_1, \dots, p_{16}, q_1, \dots, q_{16} \} \subseteq \{ r_1, \dots, r_m \} はほぼ同値である(これに違反する確率は十分低いので無視する)。よって、2^{r_1 \dots r_m} \equiv 1 \pmod n である限り R から素数を 1 個ずつ除いていくことで、\{ p_1, \dots, p_{16}, q_1, \dots, q_{16} \} を求めることができる。こうして得られた 32 個の素数のうち、生成された巡目が早いほうから 16 個が p、残る 16 個が q を得るために使われる。

A Dance of Add and Mul

問題

暗号文をビット列で m_1, \dots, m_b とする。

楕円曲線 E: BLS12-381 を考える。E(G_1, G_2) によって生成されるとする(どのような G_1, G_2 をとったかは既知)。
このとき、o_1, o_2G_1, G_2 の位数、x_0, x_1, \dots, x_m を乱数とする。

c_i を、m_1=0 のとき x_{i-1} \cdot G_1 + x_i \cdot G_2m_1=1 のとき x_{i-1} \cdot G_2 + x_i \cdot G_1 で定める。c_i が既知のとき、m を復元せよ。

思考過程

  • 楕円曲線わからん。なんかアーベル群になることだけ知ってる
  • o_1 \neq o_2 なら、o_i \cdot c_iG_1 成分消して (o_i x_i) G_2 がわかって、隣り合うもので比較すると 01 や 10 の並びが特定できるんじゃね?どっちにもならなかったら前のビットと同じということで
  • とりあえず SageMath をインストールしないと話にならん→インストール
  • 動かしてみたところ o_1 = o_2 が判明。困る
  • factor により、o_1 = 3 \cdot 11 \cdot 10177 \cdot 859267 \cdot 52437899 \cdot 52435875175126190479447740508185965837690552500527637822603658699938581184513 が判明する
  • o_1, o_23 の倍数っぽいし、a G_1 + b G_2 の形で書いたときに a, b3 の倍数になっているかは \frac{o_1}{3} 倍することで判定できない?
  • できなかった。↑は E\langle G_1 \rangle , \langle G_2 \rangle の直和で書けることを前提としているが、そうではないため
  • でも隣り合うものの差が G_1 ないし G_2 の整数倍かどうかって使いたいよな~
  • \langle c_i - c_{i+1} \rangle \subseteq \langle G_1 \rangle かどうかの判定ができればよいが、一般にそんなことができるとは思えない
  • (ライブラリのドキュメントを見て、そのような関数が存在しないかを探す)
  • 10177 っていうちょうどいい素因数があるし、\frac{o_1}{10177} 倍してから生成される群を見ればいいんじゃね?→正解

解法

まず m_1, m_2 についてのみ考える。

d = c_1 - c_2 とおく。m_1=0, m_2=1 のとき、d = c_1 - c_2 = (x_1 - x_3) G_1 である。
同様に、m_1=1, m_2=0 のとき、d = c_1 - c_2 = (x_1 - x_3) G_2 である。
これ以外のときは、dG_1 あるいは G_2 の整数倍とはならない。ゆえに、dG_1 あるいは G_2 の整数倍かを判定する問題を考える。

G_1 についてのみ考える (G_2 も同様)。これは \langle d \rangle \subseteq \langle G_1 \rangle かを判定する問題となる。x_1 - x_3G_2 成分が偶然 10177 の倍数となってしまう場合を無視すれば、\langle \frac{o_1}{10177} d \rangle \subseteq \langle \frac{o_1}{10177} G_1 \rangle の比較である。また G_1 成分が偶然 10177 の倍数となってしまう場合も無視すると、\langle \frac{o_1}{10177} d \rangle = \langle \frac{o_1}{10177} G_1 \rangle かどうかの判定に帰着される。両辺の群の位数は 10177 要素なので、生成系の定義に従って群の元を列挙するのでも十分高速に判定ができる。

↑の手順により、隣り合うビットが同じか異なるかどうか、また異なるときは 0→1 か 1→0 のいずれか、というのもわかるため、全体のビットを決定可能である。

Hat Trick

問題

mod がランダムな 64 ビット素数、base が乱数なローリングハッシュが 3 つある。与えられた乱数 3 つ組に対して、ローリングハッシュ 3 個の値がそれになるような文字列を 1 個求めよ。文字列は英小文字 54 文字以下でなければならない。

ローリングハッシュが与えられてから 3 分以内に 3 組の乱数に対して解答する必要がある。

思考過程

  • ローリングハッシュじゃん
  • 競プロ的な構築か?
  • 与えられる乱数が 192 bit くらいのエントロピーを持っていて、文字列のエントロピーが 253.8 bit くらいなので、そんなに余裕がない
  • (ちょっと考える)→1 つのローリングハッシュの都合を合わせると他のローリングハッシュの値が壊れるので、直接構築を与えるのは難しそう
  • CTF 典型?と思って調べる
  • こんな記事が出てきた https://qiita.com/kusano_k/items/5509bff6e426e5043591
  • LLL というアルゴリズムがあって、ナップサックを解くのに使えるらしい
  • SageMath なら簡単に LLL を使える
  • いろいろ試したら解けた

解法

文字列の長さを 54 に固定し、i 文字目を c_i とする。-12 \leq c_i \leq 13 として一般性を失わない。

i 番目のローリングハッシュの mod を p_i、base を b_i、目標の(乱数)値を t_i とすると、\sum_{j=0}^{53} {b_i}^j c_j \equiv r_i \pmod{p_i} なる c_j を求める問題となる。mod がちょっと邪魔なので、r_ip_i の乱数倍を足して、mod を除去する。

この上で、↓のような行列を考える。S は十分大きい定数とする。

\left(\begin{matrix} 1 & 0 & 0 & \cdots & (-{b_1}^0 \bmod p_1) \cdot S & (-{b_2}^0 \bmod p_2) \cdot S & (-{b_3}^0 \bmod p_3) \cdot S \\ 0 & 1 & 0 & \cdots & (-{b_1}^1 \bmod p_1) \cdot S & (-{b_2}^1 \bmod p_2) \cdot S & (-{b_3}^1 \bmod p_3) \cdot S \\ 0 & 0 & 1 & \cdots & (-{b_1}^2 \bmod p_1) \cdot S & (-{b_2}^2 \bmod p_2) \cdot S & (-{b_3}^2 \bmod p_3) \cdot S \\ \vdots & \vdots & \vdots & & \vdots & \vdots & \vdots \\ 0 & 0 & 0 & \cdots & r_1 S & r_2 S & r_3 S \end{matrix} \right)

これに LLL を適用すると、52 行目 (1-based) の左のほうに求める c_i が出てくる。-12 \leq c_i \leq 13 に違反しているかもしれないので、その場合は r_i の乱数を変えてやり直す。

LLL について

最初、得られる行列の各行が「元の行列の行ないし列の線形結合の係数」みたいなものを表しているのかと思って混乱した。実際には、行列に対して「行の整数倍の和として書けるベクトルからなるベクトル空間」を考えたとき、LLL はこのベクトル空間が一致するような行列であって、係数が小さいものを求めているのである。

特に、LLL の出力で得られる行列の各行は、元の行列の行の整数倍の和として書くことができる。

LLL で得られた行列の各行の左側 54 列の要素を非零とするには、元の行列での対応する行を選ぶほかない。このため、↑のような行列に対する LLL の結果の左側は、それぞれ c_j に対応しており、実際に \sum_{j=0}^{53} {b_i}^j c_j \equiv r_i \pmod{p_i} となっているならばナップサック問題の解として使えるのである。ただし、\sum_{j=0}^{53} {b_i}^j c_j = 0 なる解もたくさんあり、それらが 1~51 行目に入っているようである。なぜ求める解が 52 行目に入るのかはよくわからなかった。

semiexpsemiexp

IERAE CTF 2024

チーム poteti fan clubsemiexp です。enjoyable lattice, Free Your Mind, analog の 3 問を解きました

enjoyable lattice

問題

  • q: 32 ビットの素数, N = 32
  • R = \mathrm{GF}(q)[x] / (x^N + 1)
  • AR の元からなるランダム 2 \times 2 行列とする。s, eR の中の係数が小さいランダム 2 要素ベクトルとして、t = As + e とする。このとき s は秘密鍵、A, t は公開鍵とする
  • 暗号化のときは、平文のビット列を係数が 0/1 の R の元 m に対応させる。r, e_1 を (R の中の) 係数が小さいランダム 2 要素ベクトル、e_2 を係数が小さいランダム元として、u = A^\top r + e_1, v = t^\top r + e_2 + m \cdot \frac{q+1}{2} とする

公開鍵と暗号文から平文を復元せよ(与えられたコードには AES とか書いてあるが、それは本質ではない)

解法

秘密鍵がわかる場合の復号を考える。v - s^\top u \in R の係数は、m での対応する係数に応じて、だいたい 0 またはだいたい \frac{q+1}{2} のいずれかであるからこれで復号できる。

s および v - s^\top u 両方の係数を小さくするような s は LLL で求めることができる。ついでに e = t - As の係数も小さくしておくとよい。

Free Your Mind

問題

円分体からごにょごにょして作った体の元を a_0 + \omega a_1 + \omega^2 a_2 + \omega^3 a_3 + \omega^4 a_4 として書いて a_i を与えることができる。ただし a_i の絶対値は 2^{16} 以上でなければならない。そのノルムを RSA 暗号の e として使う。RSA 暗号で使う素数 (2048 ビット) もランダムに作られる。暗号化結果から平文を復元せよ

解法

-12215346
-43882763
10662968
55247289
20585228

これをサーバーに送りつけると e=1 になるので、返ってきた値を bytes に変換するとフラグが得られる。

↑この謎のパラメータは何かというと (\omega + 1)^{20} に対応していて、\omega + 1 のノルムが 1 であることと、ノルムが乗法的であることから、↑のパラメータでノルムが 1 になる。

思考過程

  • e がめちゃくちゃ小さければ直接 e 乗根をとることができるので、ノルムがめちゃくちゃ小さくなるような元を求めたい
  • 無理では?
  • 素数生成部分を攻撃することを考える
    • set_random_seed(int(str(alpha).encode().hex(), 16) * current_randstate().ZZ_seed())
    • random seed にめちゃくちゃ大きい値を入れる?
    • 2^{20000} とか入れてもだめっぽい
    • そもそも↑の seed の下位ビットを固定できるとしたら int(str(alpha).encode().hex(), 16) が 2 でたくさん割り切れる必要があるが、そのためには str(alpha) に大量の null 文字が必要なので、無理
  • そういえばノルムって乗法的だな
  • 非自明な(1 でない)値であってノルムが 1 になるものがあれば、それをたくさん掛けることでパラメータを大きくできそう
  • \omega + 1 のノルムが 1 であることが判明したので、それを 20 乗したらパラメータがデカくなった

analog

問題

stdin から入力を受け取って stdout にそれを変換した結果を吐くバイナリ、およびその出力が与えられるので、入力を復元する

解法

ghidra で逆コンパイルして気合で読むと、バイナリが次の 5 つの処理からなることがわかる

  1. 入力の各 byte の各 bit を 1 byte ずつにする
  2. 3 byte ずつ 1 byte (0~7) にする
  3. 各 byte を dict で別の byte (0~7) に変換する。全単射
  4. 各 byte について cos(x/8 * 2pi + pi/8), sin(x/8 * 2pi + pi/8) を float として出力先に 4 回ずつ書く
  5. それぞれの float 2 個組に対して独立に、乱数 a, b として cos(a * 2pi) * 0.4 * b, sin(a * 2pi) * 0.4 * b のノイズを加える

4, 5 をやる前の byte (というか octet) 列を復元できれば勝ち。各 byte ごとに独立にノイズを加えたものが 4 組あるので、平均をとることでノイズを小さくできる。あとはノイズがなかった場合に各バイトがどう変換されるかを考えると、ノイズがある場合でも最も近い点をとればよいことがわかる。

semiexpsemiexp

pwn 入門編

AlpacaHack Round1 echo

指定の関数 win を呼び出すという問題。

(size = abs(size)) > BUF_SIZEがあるので size は必ず 0 以上 BUF_SIZE 以下になるように見えるが、size == INT_MIN のときに限り abs(size) == INT_MIN で 0 未満となる。このとき get_databuf に確保されたスタック領域を大きく逸脱して書き込みを行ってしまう。

あとは buf のあるスタックフレームの後ろの方の return address をぶっ壊して win のアドレスにすればよい。ghidra などで解析すると、このスタックフレームは buf の先頭から saved rbp までが 0x110 byte であるとわかるから、saved rbp 含め 0x118 byte のダミー値 + win のアドレス + get_data を止めるための \n をデータとして与えると echo からの return 先が win になる

メモ

この 0x110 byte という値は、echo 関数の最初のほうに

PUSH RBP
MOV RBP,RSP
SUB RSP, 0x110

とあることから判断した。この一連の命令により、スタックは(アドレスの小さいほうから順に)

RSP             RBP
 |     0x110     | (saved RBP) | (old RSP) |

という状態になっている。(old RSP) はこの関数を呼び出すときの call 命令で保存されている。

semiexpsemiexp

AlpacaHack Round4 (Rev)

Simple Flag Checker

  • ghidra で逆コンパイルしたら、関数 updateが大変なことになっていたので気合で読むのを諦める
  • とりあえず angr を試してみる→終わらないしメモリを食いつぶして大変なことになった
  • チェック部分は 1 文字ずつ見ていて、不正解と判断された時点で特定のレジスタの値が変わるので、最初のほうから 1 文字ずつ決めていけば OK
  • 各文字のチェックはそれより前の文字に依存するので、aaaa....aaa とか bbbb....bbb とかを順次試すだけでは不十分で、1 文字ずつ決めていく必要がある
  • これを手動でやっていたらコンテストが終わるので、gdb script を書いて実行

Everything Silver

  • ghidra で逆コンパイルして読んでみたが、なんか fork してるし、ptrace とか出てきてよくわからない
  • 調べながら読んでみたところ、子プロセスは INT3(割り込み命令)が大量に並んだバイナリを実行し、割り込みがかかると親プロセスが適宜バイナリの内容を書き換えているということはわかった
  • が、子プロセスに gdb attach もできないし、どうやって解析したらいいのかまるでわからなかった
semiexpsemiexp

XorshiftStream (AlpacaHack Round5)

xorshift により生成される bit stream と XOR をとることで暗号化しているものを復号する。

まず、xorshift の各状態は、最初の state を s_0, s_1, \dots, s_{63} として、s_i のいくつかの xor として表現可能であることに注意する。

c = xss.encrypt(key.hex().encode() + strxor(key, FLAG))

key.hex()7c3f4d... のように ASCII 文字列である。特に、各バイトの LSB は 0 である。このことから、各バイトの LSB に相当する位置では xorshift の bitstream の値そのものが判明することがわかる。簡単な計算により flag の長さは 63 バイトであることがわかり、key.hex() 部分の長さは 126 バイトであるから、\{s_i\} に関する制約が 126 個得られることになる。これは \{s_i\} を復元するのに十分であり、実際 \mathrm{GF}(2) 上の連立方程式を解くことで \{s_i\} を得ることができる。これが得られれば key, flag の復元は容易。

以下のコードでは最初の _next 呼び出しの直後の state の内容を求めている。

def xor_bvec(seq1, seq2):
    assert len(seq1) == len(seq2)
    return [i ^^ j for i, j in zip(seq1, seq2)]

nbits = 64
state = []

for i in range(nbits):
    s = [1 if i == j else 0 for j in range(nbits)]
    state.append(s)

mat_all = []
for i in range(126 // 8):
    mat_all += state

    for shift in [13, -7, 17]:
        state2 = []
        for j in range(nbits):
            k = j - shift
            if 0 <= k < nbits:
                state2.append(xor_bvec(state[j], state[k]))
            else:
                state2.append(state[j])
        state = state2

encrypted = bytes.fromhex("(output.txt の内容)")

mat = []
expected = []
for i in range(7, len(mat_all), 8):
    mat.append(mat_all[i])
    expected.append(encrypted[i // 8] >> 7)

mat = matrix(GF(2), mat)
expected = vector(GF(2), expected)

res = mat.solve_right(expected)
print(res)

initial_state = 0
for i in range(64):
    initial_state |= int(res[i]) << i

print(initial_state)
semiexpsemiexp

NNNN (AlpacaHack Round5)

素数 p, q がある。d_0 = 0, d_1, d_2, d_3 に対して、p + d_i, q + d_i はいずれも素数となる。n_i=(p+d_i)(q+d_i) が与えられるので、それぞれ素因数分解したい。

n_i - n_0 = (p+q)d_i + {d_i}^2 である。ゆえに、

\frac{n_2-n_0}{n_1-n_0} = \frac{(p+q)d_2 + {d_2}^2}{(p+q)d_1 + {d_1}^2} = \frac{(p+q)d_2 + d_1 d_2 + {d_2}^2 - d_1 d_2}{(p+q)d_1 + {d_1}^2} = \frac{d_2}{d_1} + \frac{{d_2}^2 - d_1 d_2}{(p+q)d_1 + {d_1}^2}

であるが、p, q が 768 ビットである一方、d_i は 192 ビットと小さいことから、これはほぼ \frac{d_2}{d_1} であり、\frac{n_2-n_0}{n_1-n_0} の近似分数を求める過程で \frac{d_2}{d_1} が出てくることが期待できる。この近似分数は Stern-Brocot tree を使うと求めることができる。d_1, d_2 が直接得られるわけではないが、p+q = \frac{n_i-n_0}{d_i} - d_i より \frac{n_1-n_0}{d_1} - d_1 = \frac{n_2-n_0}{d_2} - d_2 であるから、適当な数を掛けて d_1, d_2 を復元できる。これにより p+q が得られ、pq = n_0 は既知であるから、p, q を復元できる。

from Crypto.Util.number import getPrime, isPrime, long_to_bytes

# ni, ci を output.txt からコピペ

x = n1 - n0
y = n2 - n0

def find_approx(a, b, c, d, p, q):
    while True:
        u = a + c
        v = b + d

        if v > 2**50 and (y - v * v) % v == 0:
            return u, v

        if u > 2**200:
            return

        if u * q < p * v:
            a, b = u, v
        else:
            c, d = u, v


d1, d2 = find_approx(0, 1, 1, 1, x, y)

assert (x - d1 * d1) % d1 == 0
assert (y - d2 * d2) % d2 == 0

import math
a = math.gcd(x // d1, y // d2)

for r in range(1, a + 1):
    if a % r != 0:
        continue
    if x // (d1 * r) - d1 * r == y // (d2 * r) - d2 * r:
        d1 *= r
        d2 *= r
        break

a = x // d1 - d1
b = n0


def sqrt(n):
    l = 0
    r = 2 ** 2000

    while l < r:
        mid = (l + r) // 2
        if mid * mid < n:
            l = mid + 1
        else:
            r = mid
    return l


det = sqrt(a * a - 4 * b)
p = (a - det) // 2
q = (a + det) // 2


def solve(n, c):
    d = (-(p + q) + sqrt((p + q) * (p + q) - 4 * (p * q - n))) // 2

    pi = p + d
    qi = q + d
    ei = pow(0x10001, -1, (pi - 1) * (qi - 1))

    m = pow(c, ei, pi * qi)
    return long_to_bytes(m)

ans = solve(n0, c0) + solve(n1, c1) + solve(n2, c2) + solve(n3, c3)
print(ans)
semiexpsemiexp

SECCON CTF 13 Quals

チーム ZK Lovers で参加しました。warmup しか解けなかった

reiwa_rot13

key (英小文字 10 文字) とその rot13 を RSA 暗号化したものが与えられるので key を復元する。

rot13 は ASCII 表現だと +13 か -13 のいずれかにしかならないので、各文字がどっちになるかを全探索することができる (1024 通り)。このパターンを決め打つと、C を定数として「key と (key + C) を RSA 暗号化したものが与えられるので key を復元する」という問題になる。これは Franklin-Reiter Related Message Attack で解くことができる。

Trillion Bank

送り先のユーザー名を指定して、自分の持っている金額を d 減らして送り先の金額を d 増やすということができる。ユーザー登録したとき金額は 10 である。この条件下で金額を 1 兆にする。

1000 億回ユーザー登録するなんてのは不可能なので金額の総和の保存則をユーザー登録以外で破る必要がある。送金部分のコードに注目すると、送り元から減らすときはユーザーを id で区別しているのに、送り先については name で区別していて怪しい。これを見ると、同じ name を持つ違うユーザーを作りたくなる(ただし直接これを行うことはできない)。

ここで、name は TEXT 型であるが、MySQL だと TEXT 型には 65535 文字しか入らない(これを超えると黙って切り落とされる)。これに注目すると、JavaScript 側では違う name なのに SQL 内では同じ name として扱われるユーザーを作ることができる。

具体的には、(longname) を 65535 文字の適当な文字列として、(longname) (longname)a (longname)b というユーザーを作る。(longname)a(longname)b は SQL 内では (longname) という name になるので、これらユーザーも (longname) に対する送金を受け取ることができる。そのため、(longname)a から (longname) に送金すると、自分の所持金を減らさず (longname)b の所持金を増やすことができる(a b を逆にしても同様)。a, b 交互に、自分の所持金すべてを送金する操作を繰り返すと、フィボナッチ数列のように所持金が増えていく。数十回の操作が必要になるが、十分に手動で対応可能である(多分本当は自動化したほうがいいんだけど、やり方がわからなかった)

ちなみにこの方法だと (longname) でユーザー登録したセッションは消してしまって構わなくて、2 つのセッションを持っていれば解ける。ブラウザを 1 種類しかインストールしていなくても、普通のウィンドウとプライベートウィンドウで 2 つの異なるセッションを持つことができるので、解ける

1linepyjail (解けなかった)

builtins, () 以外の () 禁止で flag.txt の中身を取得する。

https://book.hacktricks.xyz/jp/generic-methodologies-and-resources/python/bypass-python-sandboxes
https://github.com/salvatore-abello/python-ctf-cheatsheet/tree/main/pyjails#no-builtins-no-mro-single-exec

この辺を参考にいろいろ試していたが、__builtins__ を取得するところまでは行けるものの、そこから先がうまくいかない

  • breakpoint()→これを呼び出した時点で新しく何かを import しているらしく、__builtins__ がないことが効いてエラーになる
  • help()→pager が less ではなく cat になっていて、シェルを叩くなどができなくなっている