这篇文章记录的是吉林大学 2020 CTF 校赛的 babywasm
题解。用到的工具有 Chrome Developer Tool
和 wabt
。
ToC
记录基本偏移信息
上来先观察 data
段。
我们发现了一些有趣的东西。首先是 flag
必备的 Spirit{}
,然后是弹出对话框中的文本,中间夹杂了一些不明所以的 ASCII
字符。
我们把这些东西的偏移都记录下来。从前面的 i32 const 1048576
可以知道,基础偏移是 1048576
,a995
的偏移是 1048613
,Spirit
的偏移是 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://static.mmf.moe/wp/2020/10/image-44.png)
于是你会发现中间那一团 ASCII
码字符其实是 64+36 位的。你迅速猜测 64
位代表 sha256
,36
位代表 UUID
。但当你试图提交的时候,却发现答案错误。
于是,你又回来了。
发挥作用的偏移
与此同时,在 0x048d9
,我们看到了一个熟悉的数字:
这不就是 a995
的偏移吗!而在下面,我们发现了 1048713:Spirit
的偏移。
如果你看下去,会发现到了 0x049ef
就是我们熟悉的 alert
了:
因此关键的部分就在中间这一段:
关键逻辑分析
我们把这部分代码复制出来:
1block $label3 (result i32)2 block $label23 block $label04 block $label15 local.get $var26 i32.load offset=287 i32.const 648 i32.eq9 if10 local.get $var211 i32.load offset=2412 local.tee $var013 i32.const 104861314 i32.eq15 br_if $label0 ;; if $var0 == 1048613 -> goto $label016 local.get $var017 call $func54 ;; $func54($var0)18 local.get $var219 i32.const 19220 i32.add21 call $func70 ;; $func70($var2 + 192)22 br_if $label1 ;; return != 0 -> goto $label123 br $label224 end25 local.get $var226 i32.const 19227 i32.add28 call $func7029 end $label130 local.get $var2 ;; if br_if $label_131 i32.const 3232 i32.add33 br $label3 ;; force exit34 end $label035 local.get $var236 i32.const 19237 i32.add38 call $func7039 end $label240 local.get $var2 ;; if br $label241 i32.const 33642 i32.add43 call $func5044 local.get $var245 i32.const 3246 i32.add47 call $func7048 local.get $var249 i32.const 4050 i32.add51 local.get $var252 i32.const 34453 i32.add54 i32.load55 i32.store56 local.get $var257 local.get $var258 i64.load offset=33659 i64.store offset=3260 local.get $var261 i32.const 3262 i32.add63end $label3
经过观察,我们发现:如果我们执行了 br_if $label1
,那么直接就会跳出这一块的执行,因此我们尝试阻止其运行。暴力一点,我们直接把 br_if
注释掉:
1 local.get $var22 i32.const 1923 i32.add4 call $func70 ;; $func70($var2 + 192)5 ;; br_if $label1 ;; return != 0 -> goto $label16 br $label2
修改 WASM
修改的步骤需要将 WASM
转换为 wat
,再回编译成 WASM
。用到的工具分别是 wasm2wat
和 wat2wasm
。
对于执行,你可以选择将整个站点都下载到本地,也可以选择使用 Chrome
的 Override
功能。
结果
在你修改完成时,一切就都结束了——
这就是真正的 flag
了。这段 WASM
的实际源码如下:
1mod utils;2
3use wasm_bindgen::prelude::*;4use sha2::{Sha256, Digest};5use hex::encode;6
7#[cfg(feature = "wee_alloc")]8#[global_allocator]9static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;10
11#[wasm_bindgen]12extern {13 fn alert(s: &str);14}15
16#[wasm_bindgen]17pub 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-ba7b8cb9d92e22 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
32fn 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
40fn 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 _ => c46 }47 }).collect()48}
也就是对 flag
进行了简单的 rot13
操作。