PicoCTF 2019 やってみた, Light な Writeup

- 5分野から、問題を1つ選んで書いた
  - Binary Exploitationは、ない・・
  - 自分の解いた中で、一番高かったやつ、脳汁でたやつ
- 人生2回目のCTF、楽しかった

問題（要ログイン）
URL: https://2019game.picoctf.com/problems

## [General Skills] mus1c, 1_wanna_b3_a_r0ck5tar

歌詞「lyrics.txt」が提供されます。Hintは、「rockstarをマスターできるかい？」
「ロックをマスターする」じゃないのか？とか思いながら検索を進めたとこ、
[Rockstar](https://github.com/RockstarLang/rockstar)というプログラミング言語があり、そのソースコードだということがわかりました。

以下、解法（実行方法）、要``Node.js``です。

```bash:mus1cの解
git clone https://github.com/RockstarLang/rockstar
cd rockstar/satriani

# ちなみに、rockstarのソースコードの拡張子は「.rock」
node rockstar lyrics.txt
114
114
114
111
99
107
110
114
110
48
49
49
51
114
```
出力された数値をASCIIにマップすると、``rrrocknrn0113r``
→　フラグは``picoCTF{rrrocknrn0113r}``

次問の1_wanna_b3_a_r0ck5tarもrockstarですが、ユーザ入力を受付け、なおかつ正しい入力結果じゃないと条件分岐して何も出力しません。
なので、入力待ちと、条件分岐部分を削ってしまいます。
`Listen to the ~`, `If the ~`, `Else ~`が該当箇所なので削除して実行すると以下の通り。

```bash:1_wanna_b3_a_r0ck5tar
node rockstar lyrics2.rock
66
79
78
74
79
86
73
```
出力された数値をASCIIにマップすると、``BONJOVI``
→　フラグは``picoCTF{BONJOVI}``


## [Forensics] WhitePages

真っ白のテキストファイル「whitepage.txt」が与えられます。
`hexdump`でダンプしてみると、`e2 80 83`と`20`がたくさん出てきます。
`0x20`=SPACE、`0xe2 0x80 0x83`=EM SPACE ですので、2種の空白文字が羅列されていることになります。

2種類なのでバイナリーかな？とあたりをつけて解いてみます。
実際は以下の感じで解きました。

- SPACE=1, EM SPACE=0 として変換し、Hex表記にする。
- URLエンコードとみなし、デコードする。

以下、作成したコードです。（ここでは、EM SPACEを'|'に置換してます）

```python:
a = '|||| | ||||| || |||| || |   |||||  | || |  |||  |  |    | ||||  | | | ||| |||  ||||| | ||||| | ||||| || |||| || | | ||  | ||| | | ||| | || |||||| | ||||| | | | | |||| || ||  ||| || || | ||||  || |||||| | || || ||| | | ||||  | ||    | | || || ||| ||| | ||  || ||||||| ||  ||| |||||| |||| || ||||| | ||||  | || |  | |||   | | || || ||    | | | | | ||   || ||| |||| |||||| | || || ||| | | | ||||| ||    | | || || | | |||||| | ||||| || |||| || ||  | | ||  ||||||  ||||||  |||||| |||||| |||  ||  |    |   || ||  ||| ||  || | |   ||  || |||||| ||||| |   |  ||  || | || |  |||| |||||| | |||||  | || |   | |||   | |||   ||  |  ||| ||   | | |   || ||  ||   |  | ||||| |  |||| |||||| | ||||| ||||| || |||||||  ||| ||  | | ||  || |||  ||| ||  ||  |||| | ||||| || |||| || |   |||||  | || |  |||  |  |    | ||||  | | | ||| |||  ||    |  |  |   ||  |    |   | ||| |     |  |||| |  |  |||  |  ||| |     |   ||  |   |||||  |||| |  |||  |  || | |   ||  | |     |  |||| |   || ||  || | | |     |  |||  |   || ||  || | |  |||| |   | |||  || | |  || ||| |     |  || | |   ||| |   | | |  |||| |  |  ||| |     ||  ||| ||  |   ||   ||||  || ||||  |   ||  || |||  ||||||  || |||  | | ||  || ||  |||| |  ||  |||  ||| |  |||| |  ||  |||  || |||   || ||  ||  ||  |  |||   || |  || | ||  ||| ||  | | ||  | |||  || | |  |||  |  |||| ||  || |||  ||  |  |||| ||   || ||  | | |     | |||| | ||||| || |||| || '
b = ''.join(['0' if i == '|' else '1' for i in a])
i = int(b, 2)
h = '' if len(hex(i)) % 2 == 0 else '0' + hex(i)[2:]
u = '%' + '%'.join([h[i:i+2] for i in range(0, len(h), 2)])

import urllib
urllib.parse.unquote(u)
# 実行結果
		picoCTF

		SEE PUBLIC RECORDS & BACKGROUND REPORT
		5000 Forbes Ave, Pittsburgh, PA 15213
		picoCTF{not_all_spaces_are_created_equal_178d720252af1af29369e154eca23a95}
```

上のRockstarを先に解いていたから、最初[Whitespace](https://ja.wikipedia.org/wiki/Whitespace)プログラミングの問題かと思いました。

## [Reverse Engineering] vault-door-8

Javaのコードが渡されます。8問目は、改行なし悪魔のワンライナーコードだったのでonline java beautifierにかけましたが、難読化（or デコンパイル後のもの？）されていました。
しかも、`scramble`メソッド以下で、ビット単位で値をシャッフルしてて非常にめんどくさいコードになっています。

しかし、扱っている範囲は1 byte分でしかないので、scramble以下を流用し、0~255と変換後の値とのLookup Tableを作成することで、簡単にしてしまいます。

```python:
def scramble(c):
    c = switchBits(c, 1, 2)
    c = switchBits(c, 0, 3)
    c = switchBits(c, 5, 6)
    c = switchBits(c, 4, 7)
    c = switchBits(c, 0, 1)
    c = switchBits(c, 3, 4)
    c = switchBits(c, 2, 5)
    c = switchBits(c, 6, 7)
    return c
    
def switchBits(c, p1, p2):
    mask1 = 1 << p1
    mask2 = 1 << p2
    bit1 = c & mask1
    bit2 = c & mask2
    rest = c & ~(mask1 | mask2)
    shift = p2 - p1
    return (bit1 << shift) | (bit2 >> shift) | rest

lookup = {scramble(i): chr(i) for i in range(256)}
''.join([lookup[i] for i in [0xF4,0xC0,0x97,0xF0,0x77,0x97,0xC0,0xE4,0xF0,0x77,0xA4,0xD0,0xC5,0x77,0xF4,0x86,0xD0,0xA5,0x45,0x96,0x27,0xB5,0x77,0xF1,0xC1,0xF0,0x94,0xC1,0xA5,0xC1,0xC2,0xA4]])

# 実行結果
's0m3_m0r3_b1t_sh1fTiNg_743a4f48b'
```

## [Cryptography] AES-ABC

データ部が暗号化されたPPM形式の画像と、その暗号化に利用したコードが与えられます。

暗号化は、AES ECSで暗号化後、先頭にIV追加、順々にブロックを混ぜていくことをしています。
Hintには、AES ECSが抜け目みたいなことが書いてあります。画像だし、よくみるTuxの[あれ](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation)かな・・・

ブロックの混ぜ合わせ部分を戻せれば、解にたどり着きそうなのでそこだけ頑張ります。

ブロックが先頭から $block[0], block[1], ..., block[n]$ と並んでいるとします。（$block[0]$は、IVにあたります。）ブロックを混ぜ合わせる式は、

$block[i] = ( block[i] + block[i-1] ) \bmod 256^{16}, \qquad i=1, 2, \cdots, n$

なので、復号化時に $block[i-1]$ が $block[i]$ より大ならば、暗号化時に $\bmod$ されていたことがわかります。よって、

```math
block[i] = \begin{cases}
  256^{16} - block[i - 1] + block[i] & (block[i - 1] > block[i]) \\
  block[i] - block[i - 1] & (otherwise)
\end{cases}, \qquad i=n, n-1, \cdots, 1
```

で混ぜ合わす前の値が得られます。暗号化時とは、処理順が逆方向なことに注意。
以下コード。

```python:AES-ABC混ぜ合わせのデコード
bsize = 16
maxval = 256 ** bsize

with open('body.enc.ppm', 'rb') as f:
    data = f.read()

# header抽出
headers = []
for _ in range(3):
    i = data.index(b'\n')
    tmp, data = data[:i + 1], data[i + 1:]
    headers.append(tmp)

# abcモードを戻す
blocks = [data[i * bsize:(i + 1) * bsize] for i in range(len(data) // bsize)]
blocks_i = [int.from_bytes(b, 'big') for b in blocks]
blocks_i_inv = list(reversed(blocks_i))

blocks_i_inv = [(maxval - a + c) if a > c else (c - a) for a, c in zip(blocks_i_inv[1:], blocks_i_inv)]

iv = blocks_i[0]
blocks_i = list(reversed(blocks_i_inv))
blocks = [i.to_bytes(bsize, 'big') for i in blocks_i]

with open('result.ppm', 'wb') as f:
    f.write(b''.join(headers))
    f.write(b''.join(blocks))
```

戻したPPMはこんな感じ。
![result-aes-abc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/450728/24f8b857-019d-39a9-5047-20cc9ae03cf4.png)


## [Web Exploitation] Java Script Kiddie

Hintに従いサイトのJSを見るに、あるスペース区切りの数列をバイトとして読み込み、デコードしてPNG画像として読み込もうとしています。

デコード内容を見るに、データを16-byte区切りにし、行列（m行16列）として見立て、$i$列目にあたるバイトを、ある数値分だけ列方向にシフトさせるエンコードをしているようです。
先頭16-byteが、PNGのファイルシグネチャ、IHDRチャンク（先頭8-byte）になるシフト量の組合せを探索して、解候補の画像を全出力させて解きました。

```python
arr = [59,120,172,124,140,0,73,158,164,109,61,140,73,175,14,206,200,239,223,243,254,10,26,254,255,34,202,16,0,44,235,218,137,252,155,0,207,0,1,59,78,222,89,190,154,245,147,0,0,80,254,71,96,73,68,69,0,122,90,201,98,251,10,82,164,0,188,114,20,144,88,10,1,231,163,66,110,73,0,108,48,0,0,2,231,194,75,114,84,234,105,13,61,130,68,164,128,16,78,191,74,94,160,65,165,27,174,0,157,72,95,55,36,163,1,1,206,46,28,186,208,68,0,237,164,192,110,209,226,10,0,97,0,164,22,36,248,0,0,143,13,77,60,108,191,61,133,252,13,81,146,110,119,0,156,237,84,174,163,249,80,241,244,86,0,243,159,156,83,120,160,42,191,210,126,63,2,143,33,59,133,242,187,235,1,28,133,231,122,187,222,52,142,26,107,146,26,75,171,52,199,155,79,26,56,119,173,125,191,42,86,159,70,250,48,173,236,22,121,54,144,250,6,62,190,136,243,13,84,120,193,108,210,222,176,249,83,172,106,116,250,145,73,212,241,19,63,241,83,11,117,56,207,28,223,190,249,55,88,72,11,233,243,215,90,227,157,177,249,15,255,2,172,110,63,157,243,19,44,136,153,249,245,245,212,156,1,157,116,7,134,103,94,66,83,154,122,67,210,78,125,255,219,56,241,188,8,8,90,252,155,177,99,32,75,136,98,96,72,114,73,19,250,32,8,92,90,20,13,247,164,137,195,35,221,34,39,195,172,211,201,104,160,35,241,77,5,199,99,254,121,82,19,233,46,102,192,100,106,248,191,217,223,182,252,125,247,72,222,100,39,70,127,33,84,189,134,156,167,106,36,76,13,98,249,250,119,149,205,249,149,145,144,31,135,158,153,152,45,199,118,29,102,226,102,61,31,115,150,33,162,147,168,76,123,238,83,255,235,144,215,121,124,195,174,82,78,20,245,253,173,90,129,115,32,234,239,125,164,234,99,113,77,130,151,31,40,97,199,114,248,62,165,223,146,207,203,160,181,92,147,88,237,183,214,249,158,221,45,131,102,207,231,33,87,99,179,229,10,127,21,0,255,145,214,191,218,30,187,243,137,231,154,167,136,120,227,16,234,65,223,143,210,83,168,172,144,55,151,217,35,211,253,252,219,252,175,240,171,177,82,120,83,199,123,239,243,203,179,123,249,190,241,134,122,230,32,223,225,169,55,254,14,8,53,17,132,157,126,175,215,139,49,247,207,142,251,17,31,87,249,117,190,89,195,42,237,213,163,127,21,209,113,157,95,187,130,110,50,29,207,6,195,147,61,181,57,223,190,236,251,219,235,183,111,56,39,85,30,127,143,83,166,253,127,191,126,51,59,114,174,178,38,127,183,14,103,204,156,227,43,66,47,126,124,255,34,247,206,109,137,146,7,122,219,249,32,245,73,31,126,110,174,40,140,32,72,7,254,184,103,234,45,253,39,227,201,44,127,173,179,255,177,78,32,190,121,251,242,126,255,78,229,63,141,159,234,254,249,131,68,239,199,206,241,63,255,107,62,190,104,126,53,59,191,23,242,194,103,205,96,132,143,84]

import numpy as np
mat = np.array([arr[i*16:(i+1)*16] for i in range(len(arr) // 16)])

png = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
       0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52]

buff = []
for i, v in enumerate(png):
    buff.append([j for j, _ in filter(lambda x: x[1] == v, enumerate(mat[:, i]))])

def func(i, stack):
    if i == 16:
        name = '-'.join(map(str, stack))
        tmp = []
        for k, v in enumerate(stack):
            tmp.append(np.roll(mat[:, k], -v))
        tmp = b''.join(map(lambda x: int(x).to_bytes(1, 'big'), np.array(tmp).T.flatten()))
        with open('js-%s.png' % name, 'wb') as f:
            f.write(tmp)
    else:
        for j in buff[i]:
            stack.append(j)
            func(i + 1, stack)
            stack.pop(-1)

func(0, [])
```
QRコードの画像が出力されるので、スマホで読み取ってフラグ`picoCTF{4c182733af80dd49cc12d13be80d5893}`ゲット。

![2-3-6-3-9-1-1-4-3-8-7-5-0-6-5-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/450728/bf81a673-c930-0862-86f5-c22a81c71852.png)
これ