GoogleCTF2018-beginners-js_safe_1

最近听群里师傅提起GoogleCTF,正好我没有做这个比赛,于是打算近期将题目复现一下。

题目环境地址:https://github.com/google/google-ctf


这一题是一道 JS 代码审计、调试的题目,代码很简单,只有一个HTML文件,主要的功能就是输入一个字符串,运行 JS 代码之后告诉你这个字符串是否正确,一般来说这个字符串就是 flag 了。

因为主要代码不多,所以我就直接贴上来了

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
26
27
28
29
30
31
32
33
34
35
async function x(password) {
// TODO: check if they can just use Google to get the password once they understand how this works.
var code = '<GENERATED CODE>'
var env = {
a: (x,y) => x[y],
b: (x,y) => Function.constructor.apply.apply(x, y),
c: (x,y) => x+y,
d: (x) => String.fromCharCode(x),
e: 0,
f: 1,
g: new TextEncoder().encode(password),
h: 0,
};
for (var i = 0; i < code.length; i += 4) {
var [lhs, fn, arg1, arg2] = code.substr(i, 4);
try {
env[lhs] = env[fn](env[arg1], env[arg2]);
} catch(e) {
env[lhs] = new env[fn](env[arg1], env[arg2]);
}
if (env[lhs] instanceof Promise) env[lhs] = await env[lhs];
}
return !env.h;
}
const alg = { name: 'AES-CBC', iv: Uint8Array.from([211,42,178,197,55,212,108,85,255,21,132,210,209,137,37,24])};
const secret = Uint8Array.from([26,151,171,117,143,168,228,24,197,212,192,15,242,175,113,59,102,57,120,172,50,64,201,73,39,92,100,64,172,223,46,189,65,120,223,15,34,96,132,7,53,63,227,157,15,37,126,106]);
async function open_safe() {
keyhole.disabled = true;
password = /^CTF{([0-9a-zA-Z_@!?-]+)}$/.exec(keyhole.value);
if (!password || !(await x(password[1]))) return document.body.className = 'denied';
document.body.className = 'granted';
const pwHash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password[1]));
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['decrypt']);
content.value = new TextDecoder("utf-8").decode(await crypto.subtle.decrypt(alg, key, secret))
}

其中code变量是generate.py生成的一个1000+长度的字符串,而keyhole.value即我们的输入字符串。

简单审计一下代码,首先我们要输入一个CTF{xxxxxxx}格式的字符串,而字符串中间的xxxxxx则会储存在env[g]中,然后 javascript 代码遍历code字符串,不停地给env[lhs]赋予新值,最后如果env[h]为0,就会返回”granted”,flag就是这个时候我们输入的那个字符串。

因为一共进行了几百次循环,所以为了减少工作量,我们可以找到env[g]首次出现的地方(因为前面的处理都跟我们的输入无关,是每次都相同的),然后从那里开始,这里我使用CTF{Twings}作为测试。

1
2
3
4
if(/g/.test(code.substr(i,4))){
console.log(i);
break;
}

一个简单的正则表达式,得到的 i 为876,于是:

1
2
3
if (i >= 876) {
console.log(i + ": env[" + lhs + "] = " + env[fn] + "(" + env[arg1] + ", " + env[arg2] + ")");
}

把处理的过程都打印出来看看都是些什么东西。我们可以看到 i=884 的时候对我们的输入做了一些处理:

1
884: env[ѹ] = (x,y) => Function.constructor.apply.apply(x, y)(function digest() { [native code] }, [object SubtleCrypto],sha-256,84,119,105,110,103,115)

后面那一串数字即我们输入的 Twings 字符串的ASCII值,看起来似乎是进行了SHA256加密,我们打印一下结果看一下:

1
2
[[Uint8Array]]: Uint8Array(32) [61, 220, 43, 215, 224, 93, 96, 98, 53, 26, 246, 255, 102, 104, 78, 67, 15, 189, 227, 8, 243, 191, 171, 129, 99, 53, 248, 87, 25, 151, 243, 173]
byteLength: 32

而生成结果中正好存放着它的SHA256哈希,看来这个猜想没有错,我们继续往下看。

我们可以看到,从 i=960 开始,代码会遍历这个哈希,并将它的各个字符跟一个固定字符串的各个字符轮流进行异或,再将异或结果与env[h]进行或运算,并将或运算的结果存入env[h]中。而env[h]的初始值为0,也就是说,如果我们要最后env[h]的值为0,我们就需要输入一个SHA256哈希值跟这个固定字符串相同的字符串,来让异或的结果一直为0。

我们先将这个字符串提取出来:

1
2
3
4
var flag = [];
if (lhs == "҂") {
flag.push(env[arg1][1]);
}

结果是:[230, 104, 96, 84, 111, 24, 205, 187, 205, 134, 179, 94, 24, 181, 37, 191, 252, 103, 247, 114, 198, 80, 206, 223, 227, 255, 122, 0, 38, 250, 29, 238],转化为十六进制即e66860546f18cdbbcd86b35e18b525bffc67f772c650cedfe3ff7a0026fa1dee,谷歌这个哈希即可得到正确的flag:CTF{Passw0rd!}