Skip to content

[JLU CTF/2020] babywasm WriteUp

Published: at 23:55

这篇文章记录的是吉林大学 2020 CTF 校赛的 babywasm 题解。用到的工具有 Chrome Developer Toolwabt

ToC

记录基本偏移信息

上来先观察 data 段。

我们发现了一些有趣的东西。首先是 flag 必备的 Spirit{},然后是弹出对话框中的文本,中间夹杂了一些不明所以的 ASCII 字符。

我们把这些东西的偏移都记录下来。从前面的 i32 const 1048576 可以知道,基础偏移是 1048576a995 的偏移是 1048613Spirit 的偏移是 1048713

目前我们还不知道这些东西到底有什么用,带着准备好的偏移数据,我们进入正式的分析环节。

入口

首先我们需要定位目标函数。通过 JavaScript,我们知道最终调用的函数是 greet,于是就在 wasm 里寻找 greet

在分析过程中,最需要注意的就是访存指令。想要生成 flag 就一定需要访问内存。并且由于我们 greet 传入的参数是字符串,因此获得这个字符串本身也需要经过内存。于是我们需要重点关注的就是访存指令:i32.load

greet 的源码如下:

我们在 i32.load 处打断点,观察执行前后栈的情况。首先是 0x06cde 行:

执行完这一行后的栈中存储的是 1114120,和 var0 的内容一致,即输入字符串的地址。

然后看 0x06ce5 行,这行执行完后栈中存储的是 4,和 var1 的内容一致,即输入字符串的长度。

虽然上面的步骤并没有发现有用的信息,但却是必不可少的。因此这里没有省略这些失败的尝试。

而接下来就是有用的了。后面跟着的就是一个 call 9,我们不妨直接跳过:

我们发现,直接弹出了 alert,说明主要的内容就在这个函数内。call 9 之后的内容都不需要深究了

进入 func9

进入 func9 之后我们依然是在 i32.load 相关的地方打断点。在 0x048cf 处,我们有了发现。

执行完这一行之后,栈中的内容为 1114184,查看内存:

再尝试变换为 ASCII 码:

试着找到字符串末尾:

就得到了这样一个 64 位的字符串:

IF 线:如果你会使用搜索引擎

其实这时候你就可以拿这个去搜了,可以得到这样的结果:

https://hashtoolkit.com/reverse-md5-hash/f7e0b956540676a129760a3eae309294
https://hashtoolkit.com/reverse-md5-hash/f7e0b956540676a129760a3eae309294

于是你会发现中间那一团 ASCII 码字符其实是 64+36 位的。你迅速猜测 64 位代表 sha25636 位代表 UUID。但当你试图提交的时候,却发现答案错误。

于是,你又回来了。

发挥作用的偏移

与此同时,在 0x048d9,我们看到了一个熟悉的数字:

这不就是 a995 的偏移吗!而在下面,我们发现了 1048713:Spirit 的偏移。

如果你看下去,会发现到了 0x049ef 就是我们熟悉的 alert 了:

因此关键的部分就在中间这一段:

关键逻辑分析

我们把这部分代码复制出来:

1
block $label3 (result i32)
2
block $label2
3
block $label0
4
block $label1
5
local.get $var2
6
i32.load offset=28
7
i32.const 64
8
i32.eq
9
if
10
local.get $var2
11
i32.load offset=24
12
local.tee $var0
13
i32.const 1048613
14
i32.eq
15
br_if $label0 ;; if $var0 == 1048613 -> goto $label0
16
local.get $var0
17
call $func54 ;; $func54($var0)
18
local.get $var2
19
i32.const 192
20
i32.add
21
call $func70 ;; $func70($var2 + 192)
22
br_if $label1 ;; return != 0 -> goto $label1
23
br $label2
24
end
25
local.get $var2
26
i32.const 192
27
i32.add
28
call $func70
29
end $label1
30
local.get $var2 ;; if br_if $label_1
31
i32.const 32
32
i32.add
33
br $label3 ;; force exit
34
end $label0
35
local.get $var2
36
i32.const 192
37
i32.add
38
call $func70
39
end $label2
40
local.get $var2 ;; if br $label2
41
i32.const 336
42
i32.add
43
call $func50
44
local.get $var2
45
i32.const 32
46
i32.add
47
call $func70
48
local.get $var2
49
i32.const 40
50
i32.add
51
local.get $var2
52
i32.const 344
53
i32.add
54
i32.load
55
i32.store
56
local.get $var2
57
local.get $var2
58
i64.load offset=336
59
i64.store offset=32
60
local.get $var2
61
i32.const 32
62
i32.add
63
end $label3

经过观察,我们发现:如果我们执行了 br_if $label1,那么直接就会跳出这一块的执行,因此我们尝试阻止其运行。暴力一点,我们直接把 br_if 注释掉:

1
local.get $var2
2
i32.const 192
3
i32.add
4
call $func70 ;; $func70($var2 + 192)
5
;; br_if $label1 ;; return != 0 -> goto $label1
6
br $label2

修改 WASM

修改的步骤需要将 WASM 转换为 wat,再回编译成 WASM。用到的工具分别是 wasm2watwat2wasm

对于执行,你可以选择将整个站点都下载到本地,也可以选择使用 ChromeOverride 功能。

结果

在你修改完成时,一切就都结束了——

这就是真正的 flag 了。这段 WASM 的实际源码如下:

1
mod utils;
2
3
use wasm_bindgen::prelude::*;
4
use sha2::{Sha256, Digest};
5
use hex::encode;
6
7
#[cfg(feature = "wee_alloc")]
8
#[global_allocator]
9
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
10
11
#[wasm_bindgen]
12
extern {
13
fn alert(s: &str);
14
}
15
16
#[wasm_bindgen]
17
pub fn greet(hint: &str) {
18
let mut content = String::from("do_not_submit-where_is_the_real_flag?");
19
// hint = 'you-can-never-know-what-it-is/www'
20
if hash_match(hint, "a9553e6a285a1f8e24167204d1ebb273cbe9254c6d4ad681dc1a2644e929da43") {
21
// 6351789a-2107-4796-9f51-ba7b8cb9d92e
22
content = rot13("6351789n-2107-4796-9s51-on7o8po9q92r");
23
}
24
25
let prefix = "Spirit{";
26
let postfix = "}";
27
let result = format!("{}{}{}", prefix, content, postfix);
28
29
alert(&result);
30
}
31
32
fn hash_match(s: &str, exp: &str) -> bool {
33
let mut hasher = Sha256::new();
34
hasher.update(s);
35
let result = hasher.finalize();
36
let hash = hex::encode(result);
37
return &hash == exp;
38
}
39
40
fn rot13(text: &str) -> String {
41
text.chars().map(|c| {
42
match c {
43
'A'...'M' | 'a'...'m' => ((c as u8) + 13) as char,
44
'N'...'Z' | 'n'...'z' => ((c as u8) - 13) as char,
45
_ => c
46
}
47
}).collect()
48
}

也就是对 flag 进行了简单的 rot13 操作。是不是签到题(逃