才过去一天,我又在研究 SC
的东西了(你的作业呢!!!!!!!!
嘛,我感觉我要死了,但是研究都研究了,时间也花了,没研究出什么不是很浪费,于是抱着这样的心态我开始接触 WASM
。估计大部分人初次接触 WebAssembly
应该都是以各种其他语言开始的吧,然而谁让我在逆向呢(悲)。
ToC
WebAssembly 文本格式
这部分的内容由于校赛需要被移到这里了:
下面就直接开始了(
开始
从二进制到文本文件
二进制显然是看不了的,这里我们就需要用到 wasm
转 wat
的工具了。我这里使用的是 [wasm2wat](https://webassembly.github.io/wabt/doc/wasm2wat.1.html)
。
输入命令:
1./wasm2wat ./resource_hash.wasm > resource_hash.wat
我们就得到了逆向所需要的基本文件。
JavaScript
层入口
一切都需要有个入口,而 WebAssembly
的逆向又需要分为 JS
层面和 WebAssembly 层面的。JS
层面的入口我选择了虾泥助手对应的函数,你也可以自行选择。
选择入口的目的是打断点。虽然 Chrome
的 WASM
调试还有些问题,但动态分析还是能很大程度上帮助我们了解函数执行的全貌(帮助我们更好地猜出实现)。
WASM
入口
WASM
的入口位于 f289
,也就是 (;289;)
。这个函数通过相对调用的方式调配了 decryptResource
和 encryptPath
两个函数的执行。真正的入口其实是:
decryptResource
:f304
encryptPath
:f302
这里就用到查表了,我们虽然不一定要完全弄清楚 Table,但使用还是要会的(
decryptResource
既然是调用的 f304
,那我们直接来看:
1 (func (;304;) (type 4) (param i32 i32 i32) (result i32) ;; decryptResource called2 (local i32)3 (;4 arg#0, arg#1, arg#2, local#35 ;)6 global.get 6 ;; global#67 local.set 3 ;; local#3 = global$68 ;; save current global$69 global.get 610 i32.const 1611 i32.add12 global.set 6 ;; global#6 += 10000b13 ;; (the following 4 lines describe the stack after i32.const 3)14 local.get 3 ;; local#0 // fn15 local.get 1 ;; local#2 // byteLength16 local.get 2 ;; local#1 // toDecrypt17 local.get 0 ;; local#3 // local#3(global$6 + 10000b)18 i32.const 319 i32.and ;; local$0 -> local$0 & 011b20 i32.const 45321 i32.add ;; p0 -> p0 & 011b + 111000101b22 call_indirect (type 2) ;; type2: (param i32 i32 i32) with no return23 ;; result: 454, jump to 59624 local.get 325 call 16526 local.set 027 local.get 328 call 3129 local.get 330 global.set 6 ;; restore global$631 local.get 0)
可以看到,这里仍然没有对数据作什么太多的操作,我们跟着进 f596
:
1(func (;596;) (type 2) (param i32(;local#0;) i32(;local#1 toDecrypt;) i32(;local#2 byteLength;)) ;; decryptResource, called by f3042 (local i32 (;local#3;))3 global.get 64 local.set 3 ;; save current global#65 global.get 66 i32.const 167 i32.add8 global.set 6 ;; global$6 += 16(4 bytes)9 local.get 310 local.get 111 i32.store ;; store local#1(toDecrypt) to [local#3]12 local.get 313 local.get 214 i32.store offset=4 ;; store local#2(byteLength) to [local#3+4]15 local.get 0 ;; current local#0(original global#6)16 local.get 317 i32.const 2151418 call 300 ;; call f300(local#3, 21514) ;; might be part of decrypt19 local.tee 0 ;; save _malloc addr to local#020 i32.load ;; load (second malloc addr)21 local.get 022 i32.load offset=4 ;; load byteLength23 call 299 ;; f299(local#0, second malloc addr, byteLength)24 local.get 025 i32.load26 call 33 ;; _free27 local.get 028 call 33 ;; _free29 local.get 3 ;; recover global#630 global.set 6)
这里就有点意思了,出现了一个有点意思的未知数字:21514。这里我们还不知道它是什么,去看 f300
:
1 ;; 前半2 (func (;300;) (type 1) (param i32 i32) (result i32) ;; called by decryptResource-23 (local i32 i32 i32 i32 i32 i32 i32) ;; 7 local variables4 i32.const 85 call 47 ;; f47 is _malloc, _malloc(8)6 local.tee 3 ;; tee_local: like set_local, but also returns the set value7 ;; save _malloc return value to local#3, also keep it in stack8 i32.const 09 i32.store ;; clear front 4 bytes of _malloced space10 local.get 311 i32.const 412 i32.add13 local.tee 6 ;; save mid addr of _malloced space to local#614 i32.const 015 i32.store ;; clear latter 4 bytes of _malloced space16 local.get 017 i32.load ;; load one byte from local#0(local#3 of the caller) ->18 ;; local#0: |------------|19 ;; -> | toDecrypt |20 ;; | byteLength |21 ;; |------------|22 i32.eqz23 if ;; label = @124 local.get 3 ;; return malloced space directly, which should be zero25 return26 end27 local.get 028 i32.load offset=4 ;; load byteLength29 local.set 4 ;; save byteLength to local#430 local.get 1 ;; 2151431 call 6032 local.set 7 ;; save return value to local#7 ;; strlen(HEADER)33 local.get 4 ;; get byteLength34 i32.const 135 i32.add36 local.tee 2 ;; save byteLength+1 to local#237 call 47 ;; _malloc(byteLength+1)38 local.tee 5 ;; save return value to local#539 i32.const 040 local.get 241 call 84 ;; f84(byteLength + 1, 0, __malloc_space__) // looks like memset // TODO42 drop ;; explictly pop value from stack, ignore the return value43 local.get 044 i32.load ;; load [local#0] ;; toDecrypt45 local.set 8 ;; local#8 = [local#0] ;; toDecrypt
这里出现了超级敏感的操作:分配内存,而且还做了两次,一次是分配了两个 i32
的大小,而另一次则是和我们的输入有关的,byteLength + 1
大小。然而到这里,当时的我还是一无所知,不过看下面:
1 ;; 中段2 i32.const 03 local.set 2 ;; local#2 = 04 i32.const 05 local.set 0 ;; local#0 = 06 loop ;; label = @1 ;; for (int local#0=0;local#0 != local#4;local#0++)7 local.get 08 local.get 49 i32.ne10 if ;; label = @2 ;; if local#0 != local#4 ;; byteLength;;--------------------------------------------------------------------------11 ;; for (int i=0;i != byteLength;i++)12 ;; local#0: i13 ;; local#1: 21514, addr of HEADER string14 ;; local#4: byteLength15 ;; local#5: addr of _malloc(byteLength + 1)16 ;; local#7: length of HEADER string17 ;; local#8: toDecrypt18 local.get 519 local.get 020 i32.add ;; local#5 + local#0(*) ;; malloc_addr + offset21 local.get 822 local.get 023 i32.add24 i32.load8_s ;; load a byte from [local#8+local#0](*) ;; toDecrypt[offset]25 local.get 1 ;; local#1(*) ;; 2151426 i32.const 027 local.get 228 local.get 229 local.get 730 i32.eq ;; local#2 == local#7 ? 0 : local#2 ;; local#2 = local2 % local#7 ;; j %= strlen(HEADER);31 select32 local.tee 2 ;; save to local#2 and keep in stack33 i32.add ;; result local#134 i32.load8_s ;; load [result + local#1] ;; HEADER[local#2]35 i32.xor ;; (local#8 + local#0) ^ [result + local#1] ;; toDecrypt[local#0] ^ HEADER[local#2]36 i32.store8 ;; store a byte to [local#5+local#0] ;; toDecrypt[offset] ;; malloced[i] = toecrypt[i] ^ HEADER[j];37 local.get 238 i32.const 139 i32.add40 local.set 2 ;; local#2++ ;; j++;41 local.get 0 ;; ----------------------------------------------------------------------------------------------------------42 i32.const 143 i32.add44 local.set 0 ;; local#0++45 br 1 (;@1;) ;; goto @146 end47 end
这里出现了一个循环和一个 xor
。这已经是很明显的特征了,然而当时的我并没有领悟,懵懵懂懂地看了过去,直到机缘巧合下返回来看……
我们看到,这个循环是对整个输入数组进行的,而循环的过程中对另一个量(实际上是数组,不过我们假装不知道)进行的循环的偏移访问,由此,我们可以推断:另一个量也是一个数组,并且存储的很有可能就是 XOR
的 key。
这时候就要问一个问题了:21514
究竟是什么?
数据段
仍然是 MDN 告诉我们:数据段允许字符串字节在实例化时被写在一个指定的偏移量。而且,它与原生的可执行格式中的数据(.data)段是类似的。而 21514
在执行过程中不断被当作地址使用,我们很容易推断:这是内存空间的一个地址。
于是我们来看 resource_hash.wasm
定义的 data
:
![纵向全貌](https://static.mmf.moe/wp/2020/04/image-19.png)
顺藤摸瓜,我们看到了熟悉的东西:
![眼熟吧(](https://static.mmf.moe/wp/2020/04/image-20.png)
这个 WL[
基本可以算是刻在 DNA 里的特征了,而且正好这个字符串开头就是对应的 21514
,于是此时我们大胆推断:call 60
得到的结果就是字符串的长度。
想要验证这个推断是否正确,可以使用静态分析或者动态分析,这里我直接用了 Chrome 的调试工具,最终证明推断是正确的。但在推断之前,我也静态分析了很长一段时间,因此很难说到底是谁的功劳,应该是二者都有吧,没有静态分析我就不会作出这样的假设,而没有动态分析,估计这时候我还没把第一个函数推出来不是(
XOR 解密
之后就是随手搓一个 XOR
的时间了,这里给出一个 xjb 写的 C
程序:
1#include2#define KEY_LEN 543
4char key[KEY_LEN] = "B'KYWL[DI\\vqUIyw_we_are_hiring_https://knocknote.co.jp";5
6int main() {7 FILE* fi = fopen("test.in", "rb");8 FILE* fo = fopen("test.out", "wb");9
10 char c;11 int offset = 0;12 do {13 c = fgetc(fi);14 fputc(c ^ key[offset], fo);15 offset = (offset + 1) % KEY_LEN;16 } while (!feof(fi));17
18 fclose(fi);19 fclose(fo);20}
然后随便下一个资源下来,我用的是 asset-map-aac011a61415a220560587aaf0177b2d98e8c7897d7697db4dd51b509162040d
,执行,binwalk
,然后我就傻了:
再一看 Dolphin
:
![连图标都变了](https://static.mmf.moe/wp/2020/04/image-21.png)
用 Ark
和 Kate
打开:
![好嘛,游戏结束了](https://static.mmf.moe/wp/2020/04/image-22.png)
![wtcl](https://static.mmf.moe/wp/2020/04/image-23.png)
于是 decryptResource
成果解密(我之前怎么就没想到是 gzip
的魔数呢
encryptPath
数据段
有了上一个函数的经验,这里我们首先看的就是 data
段:
果不其然,甚至和刚才那段是同一行,还在那段的前面,出现了我们想要的信息:picosha2
。于是我们推断:最终结果是某一个值的 sha256
结果。
(其实也没那么容易作出判断,这里我是和 picosha2
的源码作比对之后确认了 sha256
计算在最后才得出的推论,但今后其实可以大胆一点,毕竟推错了也没什么太大影响,但一旦对了就能节省不少时间(
f302
同样是从入口开始,这次是 f302
:
1 (func (;302;) (type 4) (param i32(;2;) i32(;path;) i32(;tail;)) (result i32)2 (local i32 i32)3 global.get 64 local.set 35 global.get 66 i32.const 487 i32.add8 global.set 6 ;; global#6 += 489 local.get 310 i32.const 1211 i32.add12 local.tee 4 ;; local#4 = global#6(original) + 1213 local.get 1 ;; path14 call 164 ;; f164(global#6_original + 12, path)15 local.get 316 local.get 217 call 164 ;; f164(global#6_original, tail)18 local.get 319 i32.const 2420 i32.add21 local.tee 1 ;; local#1 = global#6_original + 24, keep in stack* ;; +2422 local.get 4 ;; local#4* ;; after_path +1223 local.get 3 ;; global#6_original* ;; after_tail +024 local.get 025 i32.const 326 i32.and27 i32.const 45328 i32.add ;; 2 & 3 + 45329 call_indirect (type 2) ;; 455, jump to f26230 local.get 131 call 16532 local.set 033 local.get 1 ;; _frees34 call 3135 local.get 336 call 3137 local.get 438 call 3139 local.get 340 global.set 641 local.get 0)
可以看到,这里经过了两次 f164
,不知道干了什么,但这里我当时选择了先跳过,于是我们来看 f262
(并且后来证明 f164
并不大影响理解(
f262
不得不说的是,这部分的解密带有运气的色彩
1 (func (;262;) (type 2) (param i32 i32 i32)2 (local i32 i32 i32 i32)3 global.get 64 local.set 35 global.get 66 i32.const 327 i32.add8 global.set 6 ;; local#6 = global#6 + 329 local.get 210 i32.load8_s offset=1111 i32.const 012 i32.lt_s13 if ;; label = @114 local.get 215 i32.load16 local.set 217 end18 local.get 319 i32.const 2020 i32.add21 local.set 522 local.get 323 i64.const 024 i64.store25 local.get 226 i32.load8_s27 local.set 4 ;; save ascii of first character of tail to local#428 local.get 229 local.get 230 call 60 ;;strlen(tail)31 i32.const -132 i32.add ;; strlen - 133 i32.add ;; local#2 + strlen - 1 (tail)34 i32.load8_s ;; ascii of last character of tail35 local.set 6 ;; save to local#636 local.get 337 i32.const 838 i32.add39 local.tee 2 ;; local#2 = #global(ori) + 840 local.get 441 i32.store ;; [local#2] = local#4(first character)42 local.get 243 local.get 644 i32.store offset=4 ;; [local#2+4] = local#6(last character)45 local.get 3 ;; global_original46 i32.const 8 ;; 847 i32.const 21154 ;; %c%c48 local.get 2 ;; global_original + 8
这里最大的发现是它截取了 tail
(也就是第二个参数)的首尾字母,并且下面的 21154
的值恰好就是 %c%c
,于是猜测这两个字母是连续的。测试了一下随便更换 tail
的中间字母,发现也没有任何变化,基本上可以证实了:
![完 全 一 致](https://static.mmf.moe/wp/2020/04/image-24.png)
![函数本体](https://static.mmf.moe/wp/2020/04/image-25.png)
接下来就是这两个字母到底是怎么放的问题了,于是我又开始猜测了:是不是开头/结尾?果不其然,结果是开头:
![这是我万万没想到的](https://static.mmf.moe/wp/2020/04/Screenshot_20200416_065003.png)
![不过总算是结束了(](https://static.mmf.moe/wp/2020/04/image-26.png)
就此,整个 resource_hash.wasm
宣告解密完毕。
结语
这次的逆向过程可以说是我第一次认真对汇编级别的源码进行分析的过程。中间有试过用 wasm2c
转成 C
再用 gcc
编译再导入 IDA
,但效果并不尽如人意(还是我不大会 x86 汇编的锅)。相比之下,WASM
本身比 x86
的指令简单了 114514 倍,强行转过去不是自己给自己增加难度吗(
一晚上就这样过去了,不过总算是有结果了,也算是不辜负我这一晚上的爆肝吧(