引言 大家好,还是原来的爬虫练习平台 ,上篇文章讲到了 wasm 的逆向,本篇文章讲 wasm 的进阶,调用 wasm 中入参和出参都是字符串的情况。
spa15 spa15 地址:https://spa15.scrape.center/
spa15 说明:
电影数据网站,数据通过 Ajax 加载,数据接口参数加密且有时间限制,加密过程通过字符串型 WASM 实现,适合 WASM 逆向分析。
还是老样子,wasm 加密,我们直接看wasm 调用位置,传参变成了两个字符串参数。
其中一个参数是当前的路径,另一个参数是时间戳字符串。继续看,调用的方式好像变化了,变成了 ccall 调用,我们步入看看是不是真的调用了 wasm。
两个字符串参数被转成了整形传入了 wasm,并不是直接将字符串传入。
跟着走了一遍,整体逻辑是在将字符串参数传入 wasm 之前,先要将字符串参数转成 ASCII 码,然后写入到一个通过 wasm 分配的内存中,猜测可能是 wasm 不能直接支持字符串参数,所以最后传入 wasm 的其实是两个字符串写入 wasm 内存的地址值。wasm 执行完成后,将数据存储到内存中,返回地址,然后 JS 再读取它,将其转回正常的字符串。由于流程比较长,这里就不贴所有的截图了。
wasm 内存 Wasm 内存是一个连续的线性字节数组,用于模块与宿主环境(如浏览器、Node.js)之间的数据交互,内存是从 0 开始的线性地址空间,以字节为单位寻址,支持高效随机访问。内存按页分配,每页固定为 64KB (65,536 字节),初始内存大小通过 initial
参数定义(默认至少 1 页),运行时可按需扩展,最大不超过 maximum
参数限制(可选配置)。
从上面的 JS 代码中也可以看到,其实在 ccall 调用的 L
函数中,就是先对当前的字符串进行了编码,然后将编码后的数据通过 N
函数调用 M
函数写入了一个超大的字节数组中,也就是变量 n
,长度达到了 16777216
,它就是 wasm 的内存。在调用函数后, 通过读取函数返回的地址值,在指定位置读取结果。
wasm2c wasm 是一种低级的二进制指令格式,支持 C/C++、Rust、Go 等语言编译为 Wasm 模块,是一种类汇编语言,我们平时用到的 wasm 基本上都是从各种语言编译过来的。
一般对于 wasm 的逆向,有几种选择:
一种是通过 Python 或者 nodejs 或者语言实现的 wasm 运行时,将 JS 代码抠出来,使用 wasm 运行时来运行wasm 代码,缺点是需要运行时才能执行代码,好处是方便,通用性比较强。 或者使用 ida 等工具进行逆向分析,分析出详细的算法,缺点是耗时长,并且掉头发,通用性不强。 还有一种方案就是将 wasm 翻译成等价的 C 语言代码,直接执行翻译后的 C 语言代码,这样不需要运行时,并且通用性还不错。 这里我们选择第三种方案。需要用到的工具:https://github.com/WebAssembly/wabt ,大家需要下载前面这个项目的 release,根据自己的系统版本来下载。同时要下载 https://github.com/WebAssembly/wabt/tree/main/wasm2c 文件夹中的代码,后面会用到。
翻译成 C语言 首先我们将下载下来的 wasm 文件放到 wasm2c 可执行文件目录中,执行命令:./wasm2c Wasm.wasm -o wasm.c
,可以得到两个文件,wasm.c
和 wasm.h
,这两个文件就是 wasm 翻译好的 C 语言代码,查看 https://github.com/WebAssembly/wabt/blob/main/wasm2c/README.md 发现默认使用的是 C99
规范,下面我们来编译试试。
由于刚才翻译成的 C语言代码中只有 wasm 对应的代码,它是一个 lib
库,可以理解为是一个动态链接库(dll
或者 so
),它是没有 main
函数的,在 C语言中,没有 main
函数就无法直接执行。
为了能正常初始化 wasm 运行时代码,并且能验证 wasm 翻译成的 C 语言代码是否可以得到正确的结果,我们手动添加一个 main
函数,并且按照上图中 readme 的要求手动添加 wasm 初始化代码。例如添加一个 main.c
文件:
1 2 3 4 5 6 7 8 9 10 #include "wasm.h" #include <stdio.h> #include <stdlib.h> int main () { w2c_Wasm instance; wasm_rt_init(); wasm2c_Wasm_instantiate(&instance, NULL ); return 0 ; }
编译C语言 按照教程的要求初始化了wasm 运行时,并且导入了我们翻译后的 wasm 库的代码到 main.c
中。现在我们来编译一下试试,编译前别忘了将之前提到的 wasm 运行时代码放到和 main 文件相同的目录中,截止到现在,文件夹中应该有如下文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 ❯ tree . ├── main.c ├── wasm-rt-impl-tableops.inc ├── wasm-rt-impl.c ├── wasm-rt-impl.h ├── wasm-rt-mem-impl-helper.inc ├── wasm-rt-mem-impl.c ├── wasm-rt.h ├── wasm.c └── wasm.h 1 directory, 9 files
现在编译试下,我这里使用的是 MacOS 系统,clang 版本是 16.0:
1 2 3 4 5 6 ❯ gcc main.c wasm-rt-impl.c wasm-rt-mem-impl.c wasm.c Undefined symbols for architecture arm64: "_w2c_wasi__snapshot__preview1_proc_exit" , referenced from: _w2c_Wasm_f19 in wasm-aa7a7b.o ld: symbol(s) not found for architecture arm64 clang: error: linker command failed with exit code 1 (use -v to see invocation)
发现报错了,错误是缺少了一个符号引用。搜索了一下发现在 wasm.h
文件中,定义了函数,但是没有在 wasm.c
中找到具体的实现,既然没有实现,我们为了编译通过,先根据函数签名补充一个空的实现。
在 main
文件中开头位置添加以下代码:
1 void w2c_wasi__snapshot__preview1_proc_exit (struct w2c_wasi__snapshot__preview1*, u32) {};
重新编译,发现编译通过了,现在来补充 encrypt
函数调用代码,根据 wasm.c
文件中具体的实现代码和 JS 代码的部分逻辑,手动转换成对应的 C 语言代码,添加以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 char * a1 = "/api/movie" ;printf ("%s\n" , a1);char * a2 = "1738565280" ;printf ("%s\n" , a2);u32 str_len1 = strlen (a1) + 1 ; u32 ptr1 = w2c_Wasm_stackAlloc(&instance,str_len1); memcpy (w2c_Wasm_memory(&instance)->data + ptr1, a1, str_len1);u32 str_len2 = strlen (a2) + 1 ; u32 ptr2 = w2c_Wasm_stackAlloc(&instance,str_len2); memcpy (w2c_Wasm_memory(&instance)->data + ptr2, a2, str_len2);u32 out_ptr = w2c_Wasm_encrypt(&instance,ptr1, ptr2); char * out_str = (char *)malloc (128 );memcpy (out_str, w2c_Wasm_memory(&instance)->data + out_ptr, 128 );w2c_Wasm_stackRestore(&instance,ptr1); w2c_Wasm_stackRestore(&instance,ptr2); printf ("%s\n" ,out_str);free (out_str);
重新编译一下,通过了,生成了可执行文件:a.out
,执行一下看看:
1 2 3 4 ❯ ./a.out /api/movie 1738565280 MmNlMTgxM2ViNzI0NzdmMjIzODU4MzJkOGQ0NWNhMWE2MDc5YmMwMywxNzM4NTY1Mjgw
和网页上的结果对比一下,发现结果完全相同。
编译动态链接库 上面我们成功的将 wasm 翻译后的 C 语言代码编译成了可执行文件,并且验证了算法逻辑是正确的,为了便于后续给其他语言调用,最好将其编译成动态链接库。但是现在我们只有一个 main
函数,所有的逻辑都在 main
函数中,为了方便对外暴露接口,我们稍微修改代码。改完代码别忘了编译运行一下确认修改后的代码可以正常运行。
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 36 37 38 39 40 41 #include "wasm.h" #include <stdio.h> #include <stdlib.h> void w2c_wasi__snapshot__preview1_proc_exit (struct w2c_wasi__snapshot__preview1*, u32) {};w2c_Wasm instance; void init_wasm () { wasm_rt_init(); wasm2c_Wasm_instantiate(&instance, NULL ); } char * encrypt (const char * a1,const char * a2) { u32 str_len1 = strlen (a1) + 1 ; u32 ptr1 = w2c_Wasm_stackAlloc(&instance,str_len1); memcpy (w2c_Wasm_memory(&instance)->data + ptr1, a1, str_len1); u32 str_len2 = strlen (a2) + 1 ; u32 ptr2 = w2c_Wasm_stackAlloc(&instance,str_len2); memcpy (w2c_Wasm_memory(&instance)->data + ptr2, a2, str_len2); u32 out_ptr = w2c_Wasm_encrypt(&instance,ptr1, ptr2); char * out_str = (char *)malloc (128 ); memcpy (out_str, w2c_Wasm_memory(&instance)->data + out_ptr, 128 ); w2c_Wasm_stackRestore(&instance,ptr1); w2c_Wasm_stackRestore(&instance,ptr2); return out_str; } int main () { init_wasm(); char * a1 = "/api/movie" ; printf ("%s\n" , a1); char * a2 = "1738565280" ; printf ("%s\n" , a2); char * out_str=encrypt(a1,a2); printf ("%s\n" ,out_str); free (out_str); return 0 ; }
修改后,使用下面的命令编译为 so 文件:
1 ❯ gcc main.c wasm-rt-impl.c wasm-rt-mem-impl.c wasm.c -shared -fPIC -o wasm.so
编译后,查看一下是否有我们需要的导出函数 encrypt
和 init_wasm
,使用 nm
命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ❯ nm -gU wasm.so 0000000000002fe8 T _encrypt 0000000000028000 S _g_wasm_rt_jmp_buf 0000000000002fc4 T _init_wasm 0000000000028100 S _instance 0000000000003124 T _main 0000000000006418 T _w2c_Wasm_0x5F_indirect_function_table 0000000000006430 T _w2c_Wasm_0x5Finitialize 00000000000045bc T _w2c_Wasm_encrypt 00000000000045a4 T _w2c_Wasm_memory 0000000000006504 T _w2c_Wasm_stackAlloc 00000000000064b0 T _w2c_Wasm_stackRestore 0000000000006464 T _w2c_Wasm_stackSave ...后续内容省略
Python 调用 so so
文件确认没有问题后,可以使用 Python 或者其他语言来调用 so
计算我们需要的 token
了,我这里使用 Python,尝试以下代码调用 so
中的 encrypt
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import ctypesdll = ctypes.CDLL('./c99/wasm.so' ) dll.init_wasm() def encrypt (p1, p2 ): print (p1, p2) dll.encrypt.argtypes = [ctypes.c_char_p, ctypes.c_char_p] dll.encrypt.restype = ctypes.c_char_p a1 = ctypes.c_char_p(p1.encode('utf-8' )) a2 = ctypes.c_char_p(p2.encode('utf-8' )) result = dll.encrypt(a1, a2) return result.decode() if __name__ == '__main__' : print (encrypt('/api/movie' , '1738565280' ))
查看输出结果,符合预期,大功告成!
1 2 /api/movie 1738565280 MmNlMTgxM2ViNzI0NzdmMjIzODU4MzJkOGQ0NWNhMWE2MDc5YmMwMywxNzM4NTY1Mjgw
完整爬取逻辑 接下来补充其他代码,完善爬取逻辑:
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 import ctypesimport timeimport requestsimport urllib3urllib3.disable_warnings() dll = ctypes.CDLL('./c99/wasm.so' ) dll.init_wasm() def encrypt (p1, p2 ): print (p1, p2) dll.encrypt.argtypes = [ctypes.c_char_p, ctypes.c_char_p] dll.encrypt.restype = ctypes.c_char_p a1 = ctypes.c_char_p(p1.encode('utf-8' )) a2 = ctypes.c_char_p(p2.encode('utf-8' )) result = dll.encrypt(a1, a2) return result.decode() def get_content (): for offset in range (0 , 110 ): sign = encrypt('/api/movie' , str (int (time.time()))) url = f'https://spa15.scrape.center/api/movie/?limit=10&offset={offset} &token={sign} ' r = requests.get(url, verify=False ).text print (r) time.sleep(0.5 ) if __name__ == '__main__' : get_content()
可以正常爬取数据了,完成!
最终代码见:https://github.com/libra146/learnscrapy/tree/main/js/spa15
总结 wasm 转成 C语言后,调用还是比较方便的,而且不需要 wasm 运行时,性能也可能会高一些,就是编译过程比较繁琐,如果遇到了需要从 JS 中导入函数的 wasm(进行环境检测),这种方式可能会更加复杂,所以具体选择什么方式还是要看具体的情况。
还有 wasm2c 最后生成的 C 语言代码是 C99 规范的,它默认不允许调用未在当前文件定义的函数,明明初始化函数在运行时中定义过了,但是我这里调用就是会编译报错,这里就花了我不少的时间进行调试,实际上只要在编译的时候加上对应的 C 文件就可以了,还是对 C 太不熟悉了哈哈。
断断续续的编译的过程花了我好多时间,期间一直反复的看文档查资料,因为找不到更多其他的参考,只有一个短短的官方文档,加上 AI 的帮助,终于是搞出来了,最终的效果还是很不错的。
本文章首发于个人博客 LLLibra146’s blog
本文作者 :LLLibra146
更多文章请关注公众号 (LLLibra146):
版权声明 :本博客所有文章除特别声明外,均采用 © BY-NC-ND 许可协议。非商用转载请注明出处!严禁商业转载!
本文链接 :https://blog.d77.xyz/archives/d0897e99.html