Google CTF 2018に参加しました。
相変わらずの難易度だったが、今回はBeginners Questがあったりしてカジュアル勢にも楽しめるコンテストになっていたと思う。
一応1問(JS SAFE 2.0)だけ解けたのでwrite upを書いておく。
問題
You stumbled upon someone's "JS Safe" on the web. It's a simple HTML file that can store secrets in the browser's localStorage. This means that you won't be able to extract any secret from it (the secrets are on the computer of the owner), but it looks like it was hand-crafted to work only with the password of the owner...
function x(х) {
ord = Function.prototype.call.bind(''.charCodeAt);
chr = String.fromCharCode;
str = String;
console.log(ord('х'), ord('x'));
function h(s) {
for (i = 0; i != s.length; i++) {
a = ((typeof a == 'undefined' ? 1 : a) + ord(str(s[i]))) % 65521; // 0xfff1
b = ((typeof b == 'undefined' ? 0 : b) + a) % 65521;
}
// bとaの上下8bitずつで文字列生成
return chr(b >> 8) + chr(b & 0xFF) + chr(a >> 8) + chr(a & 0xFF)
}
function c(a, b, c) {
for (i = 0; i != a.length; i++) c = (c || '') + chr(ord(str(a[i])) ^ ord(str(b[i % b.length])));
return c
}
// for (a = 0; a != 1000; a++) debugger; // 邪魔
x = h(str(x)); // 1.
console.log(x, x.length, '\n'); // 8bit 4文字
source = /Ӈ#7ùª9¨M¤�À.áÔ¥6¦¨¹.ÿÓÂ.Ö�£JºÓ¹WþÊ�mãÖÚG¤�¢dÈ9&òªћ#³1᧨/;
source.toString = function() {
return c(source, x)
};
console.log('source', source.source.length);
try {
// console.log('debug', source);
// with(source)return eval('eval(c(source,x))')
with(source) {
var s = c(source,x); // 2.
console.log('c', s);
var s2 = eval(s); // 3.
console.log('eval2', s2);
return s2;
}
} catch (e) {
console.error(e);
}
}
求めるべきフラッグはパスワード。
問題としてはjavascriptのコードを読み解くだけなのだが、色々と引っ掛けがあってだいぶ時間を溶かしてしまった。
解法
関数x
がtrueを返すような入力を求めればよい。関数x
がやっていることは
-
x
を関数h
で加工して32bitに(正確には8bit文字*4) - 上を数字と
source
文字列でxorを取って文字列の生成 - 生成した文字列を
eval
してreturn
この時、注意しないと行けないのは、関数x
の引数がх
(ギリシャ文字のカイ)であること。
なので、str(x)
は関数x
を文字列化したものになる。(javascriptは関数を文字列化出来るらしい)
なので2までは入力に関係なく毎回同じ処理になる。
関数h
を詳しくみる。
function h(s) {
for (i = 0; i != s.length; i++) {
a = ((typeof a == 'undefined' ? 1 : a) + ord(str(s[i]))) % 65521; // 0xfff1
b = ((typeof b == 'undefined' ? 0 : b) + a) % 65521;
}
// bとaの上下8bitずつで文字列生成
return chr(b >> 8) + chr(b & 0xFF) + chr(a >> 8) + chr(a & 0xFF)
}
注意しないといけないのは、a
とb
がグローバル変数であるということ。なのでa
の状態によって結果が変わってしまう。
さらに関数x
の途中でfor (a = 0; a != 1000; a++) debugger;
が挟まっているので、処理1を行う時はa=1000
になっている。
a=1000
でh(x)
を計算して、source文字列を関数c
で復元したら、以下の文字列が得られた。
х==c('¢×&Ê´cʯ¬$¶³´}ÍÈ´T©Ð8ͳÍ|Ô÷aÈÐÝ&¨þJ',h(х))//᧢
ここのх
はカイ。なので、上の処理が真になるようなカイ(=パスワード)を求めればよい。
h(х)
の結果は32bitなので総当りしてもよいのだが、入力х
が許されている文字が、[0-9a-zA-Z_@!?-]
しかないのことを利用する。
function c(a, b, c) {
for (var i = 0; i != a.length; i++) c = (c || '') + chr(ord(str(a[i])) ^ ord(str(b[i % b.length])));
return c;
}
関数c
はa[i]
とh(х)[i % 4]
をxorした結果を返すので、これが全て[0-9a-zA-Z_@!?-]
になるようなh(х)[i]
だけを残す。
これなら256 * len(a)
の探索で済む。
その結果2通りのh(х)
が見つかって、その片方でフラッグが求まった。
CTF{_N3x7-v3R51ON-h45-AnTI-4NTi-ant1-D3bUg_}
感想
問題としてはそこまで複雑ではない(というか専門的な知識を特に必要としない)
が、х
をカイと気づかなかったり、sourceに非unicode文字が混ざっているので下手にコピペを行うとsourceの文字列が変わってしまったりとハマりやすいところが多かったと思う。
(しかし今回はOCR is COOLにフラッグがなかったり、shall we play a gameで1,000,000回勝ったのにフラッグが文字化けしたりとツライ問題が多かった)
なんか最近miscみたいな問題しか解いていないので、暗号とかpwnとか解けるようになりたいね。