picoCTF 2019: Java Script Kiddie
問題
問題文
The image link appears broken... https://2019shell1.picoctf.com/problem/59857 or http://2019shell1.picoctf.com:59857
解答例
指針
- 頑張って JavaScript のコードを読む
- pngファイルの signature の値からkeyの候補を狭める
解説
与えられたサイトのソースコードを読むと, 以下のような JavaScript のコードが <script>
タグ内に書かれていることが分かる.
var bytes = []; $.get("bytes", function(resp) { bytes = Array.from(resp.split(" "), x => Number(x)); }); function assemble_png(u_in){ var LEN = 16; var key = "0000000000000000"; var shifter; if(u_in.length == LEN){ key = u_in; } var result = []; for(var i = 0; i < LEN; i++){ shifter = key.charCodeAt(i) - 48; for(var j = 0; j < (bytes.length / LEN); j ++){ result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i] } } while(result[result.length-1] == 0){ result = result.slice(0,result.length-1); } document.getElementById("Area").src = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(result))); return false; }
assemble_png()
関数は以下のように, form タグの onsubmit
属性の値として指定されており, "submit" が押されたとき実行される.
<form action="#" onsubmit="assemble_png(document.getElementById('user_in').value)"> <input type="text" id="user_in"> <input type="submit" value="Submit"> </form>
JavaScript のコードを先頭から読んでいく.
jQuery の $.get
で https://2019shell1.picoctf.com/problem/59857/bytes の値をとってきて,
bytes
という配列に格納している.
var bytes = []; $.get("bytes", function(resp) { bytes = Array.from(resp.split(" "), x => Number(x)); });
assemble_png
関数を読んでいく.
以下のように, user の入力である引数 u_in
の長さが, 16 であるとき, 変数 key
の値を更新している.
var LEN = 16; var key = "0000000000000000"; if(u_in.length == LEN){ key = u_in; }
正しい入力を与えることで, 正しい key
の値を設定すればよいと推測できる.
以下の部分で, result という配列に bytes の値をある規則に従って代入してる. ここで, shifter という変数は key の各文字を数値に直した値となる. Python3 の以下のコードと等価である.
shifter = int(key[i])
ASCII コード表を見れば, 0
が 48 に対応していることが確認できる.
var result = []; for(var i = 0; i < LEN; i++){ shifter = key.charCodeAt(i) - 48; for(var j = 0; j < (bytes.length / LEN); j ++){ result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i] } }
key はおそらく数字を表す文字列であると推測する. (初期値が 0000...00
なので)
このとき, key の候補は各文字について10通りで16文字あるので 1016 通りとなるので, 単純な全探索はできない.
JavaScript の以下の部分を見ると, 配列変数 result の値をバイナリデータとして読み込み, base64 エンコードして png ファイルとして表示している.
document.getElementById("Area").src = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(result)));
pngファイルは以下の8バイトの signature から始まる.
89 50 4E 47 0D 0A 1A 0A
また, pngファイルは signature のあとに IHDR
という chunk があり, 以下の8バイトのデータが続くらしい.
00 00 00 0D 49 48 44 52
これにより変数 result の最初の 16 個の値が確定するので key の候補を絞り込むことができる.
Python3 により, i 番目の key の候補を出力するプログラムを作成する.
result の最初の 16 個は, 以下の JavaScript のコードの j = 0 のときなので,
var result = []; for(var i = 0; i < LEN; i++){ shifter = key.charCodeAt(i) - 48; for(var j = 0; j < (bytes.length / LEN); j ++){ result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i] } }
result[i]
が bytes[str(key)*LEN + i]
と等しいかを見ればよい.
もし等しければ, その時の key の値は解の候補となる.
#!/usr/bin/env python3 import requests r = requests.get('https://2019shell1.picoctf.com/problem/59857/bytes') _bytes = [ int(x) for x in r.text.split() ] # png header result = [ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, ] for i in range(16): print("{:02d}".format(i) + "th possible key is: ", end = "") for key in range(10): if result[i] == _bytes[key*16+i]: print("{:x}".format(key), end = " ") print()
- 実行結果
00th possible key is: 4 01th possible key is: 5 02th possible key is: 3 4 03th possible key is: 9 04th possible key is: 6 05th possible key is: 1 06th possible key is: 8 07th possible key is: 5 08th possible key is: 1 2 09th possible key is: 5 6 7 10th possible key is: 0 1 11th possible key is: 1 12th possible key is: 2 13th possible key is: 4 14th possible key is: 9 15th possible key is: 5
2, 8, 10 文字目で候補が2通りあり, 9文字目は3通り, 合計で 24通りの候補がある.
この程度なら全て試すことができる.
先程のコードを少し改変し, keyとなる16文字の文字列の候補を全て出力するプログラムを作成する.
#!/usr/bin/env python3 import requests r = requests.get('https://2019shell1.picoctf.com/problem/59857/bytes') _bytes = [ int(x) for x in r.text.split() ] # png header result = [ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, ] dp = [ [] for _ in range(20) ] dp[0].append("") for i in range(16): for key in range(10): if result[i] == _bytes[key*16+i]: for d in dp[i]: dp[i+1].append(d + str(key)) for d in dp[16]: print(d)
- 実行結果
4539618515012495 4549618515012495 4539618525012495 4549618525012495 4539618516012495 4549618516012495 4539618526012495 4549618526012495 4539618517012495 4549618517012495 4539618527012495 4549618527012495 4539618515112495 4549618515112495 4539618525112495 4549618525112495 4539618516112495 4549618516112495 4539618526112495 4549618526112495 4539618517112495 4549618517112495 4539618527112495 4549618527112495
これを上から順に, 問題のサイトの Form に入力してボタンを押していくと, 8番目であたりを引いた.
4539618515012495 => ng 4549618515012495 => ng 4539618525012495 => ng 4549618525012495 => ng 4539618516012495 => ng 4549618516012495 => ng 4539618526012495 => ng 4549618526012495 => ok! 4539618517012495 ...
4549618526012495
を入力すると, QRコードの画像が表示され, それを読み取ると flag が得られた.
flag: picoCTF{cfbdafe5a65de4f32cce2e81e8c14a39}
感想
Web問として出題されているが, ほぼPPCだと思った.