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$.gethttps://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だと思った.

参考文献