最近在测试时,发现某个网页接口加了签名验证,并且代码做了混淆。同时还利用了wasm来生成签名,本文做一个记录。

寻找线索

首先搜索该签名的关键字,下断点,做调试,我们可以看到传入的参数。
同时我们分析签名格式,通过传入同样的数据来生成多个签名。其中比较明显的是,签名中有一个64位的字符串、一个32位的字符串、一个时间戳、一段base64编码的内容,以及一些不变的内容。 通过对比,我们发现base64部分的内容不变,但是解码是乱码,不确定内容,64位和32位以及时间戳每次不一样。

调试之后,发现生成签名的部分是在wasm里面的,因此我们需要分析wasm才可以知道。

根据格式,我们猜测64位的是sha256,32位的为md5。

调试

由于wasm看起来跟汇编差不多,直接不好分析。通过网络搜索,了解到jeb是支持decompile wasm的。于是下载jeb进行分析。

由于猜测64位为sha256,因此代码里面肯定有计算sha256的代码。于是网络搜索到一份sha256的c代码,以及wasm的代码。(后面看来,运气不错,刚好找到了一份几乎一样的c代码。) 在使用jeb之前,先使用了wabt的wasm2c工具,将网页中的wasm转换为c,不过这个代码看起来也不是很清晰。对比一下,jeb生成的代码更清晰一点,但是也有一些让人疑惑的显示。

由于直接分析wasm2c生成的c代码没有发现什么,并且代码非常长。(不过这也有一个好处,就是可以在整体上进行搜索)。转而使用jeb,对单个函数生成的代码来分析。

接着我们对比了sha256算法和wasm对应的wat代码。

我们在网络上找到的sha256的c代码中,可以看到几个常数,通过在代码中搜索这个几个常数,以及常数的上下关系,我们找到了init函数。通过对比逻辑,确实与sha256.c中的init函数是一致的。(不过这里由于没有细看,也给后面挖了坑。)

通过16711680和65280我们找到$sha256_transform函数。

接着再找init的调用点,对比函数的结构,我们找打了trans函数、update函数、final函数。在jeb中给函数重命名,这样看起来方便。

我们的想法是,看sha256函数生成的字符串是否是最终结果。
我们给update函数下断点,可以看到传入的数据的index(类似指针?)和长度。 通过下面的语句来查看数据,传入index和长度。

new TextDecoder('utf8').decode(memories.$memory.buffer.slice(5264000,5264000+800))

我们拿到了需要sha256的字符串,接着我们将这个字符串在另一个网页计算器中计算,生成一个sha256的hash,结果和最终hash不一样。

hash分析

结果我们断点了final函数,通过查看函数执行后,对应的hash的内容。

new DataView(memories.$memory.buffer.slice(5265000,5265000+100)).getUint32(0).toString(16)

我们发现函数生成的hash和最终结果的hash是一致的。那么,说明,之后并没有其他操作,就是这个函数的返回值,即sha256的hash。

那么,为什么计算的hash不一样呢?

第一个想法是,算法进行了修改。

接着,我们尝试去下载wasm的编译环境,再下载了一份sha256.c的代码进行编译,之后再decompile这份wasm,发现生成的这份代码缺少很多东西,并没有发现相应的sha256算法模块,因此没法进行对比。同样网络上的wasm对应的wat格式也和浏览器中的wat格式不一样,也不适合对比。

接下来就只能考虑调试了。这里要吐槽一下,wasm的指令比较简单,因此会出现很多看起来几个变量在来回旋转赋值的操作,分析起来也不是很友好。

接下来,我们想单步看一看,到底每一步是不是生成一致的。网络上找到一个可以单步展示sha256算法的页面,对比进行分析。

通过单步执行,与jeb中看到的的函数逻辑以及sha256.c代码的逻辑进行对比,看清楚关键的步骤。

发现对trans函数中的m数组赋值这一步没有问题,64步都是正确的,我们使用代码查看对应内存数据是一致的。 之后是给a、b、c、d、e、f、g、h进行赋值,然后查看了前面几个a、b、c都是对的,本来准备跳过的,结果查看发现h有一位不一致。 刚开始还以为从内存中取数据取错了,执行了几遍还是不一致的。 然后去查看wasm对应的init函数,一看,发现这个值确实是不一致的。这就是之前的那个坑,这个值被做了一个微小的改动。

验证

之后将sha256算法网站的js中的h值进行替换,重新计算hash,发现算出来的结果是一样的。因此可以初步得出结论,sha256算法这个h的值被修改掉了。

其他

关于64位

由于在代码中找md5的关键常数没有找到,后续在查看内存时,发现fingerprintjs的链接,然后部分数据的格式和fingerprint生成的数据很像,并且fp的id也是64位的,因此猜测这个64位的数据是对客户端的一个fingerprint。但是这个内容每次都会变化,不确定是有什么其他逻辑。但是这个值会由客户端传递给服务端。因此不影响hash的生成。

关于运气

找了一份javascript生成sha256的代码,发现里面根本没有写常数,是根据算法生成的。

同时找了其他几份sha256.c的代码,代码结构就不是4个函数这种,并且没有使用循环,因此,如果之前找到了其他版本的sha256.c,估计分析时间还要加长。

有用的命令

查看字符串

new TextDecoder('utf8').decode(memories.$memory.buffer.slice(5260000,5260000+100))

查看某个位置的字符串

new DataView(memories.$memory.buffer.slice(5260000,5260000+100)).getUint32(0)

16进制查看

new DataView(memories.$memory.buffer.slice(5260000,5260000+100)).getUint32(0).toString(16)

总结

整体看下来,签名中是包含时间戳、固定常量(盐?)、数据的hash,以及由这些数据生成的hash的。 时间戳可以防止重放,不过这个场景看起来不需要防止重放攻击,服务端看起来没有检查timestamp。 生成逻辑放到wasm里面,这样分析起来更麻烦一点。 关于修改sha256算法中的初始值,这一点在这里做签名问题不大。 如果是真的用来做hash算法,可能还需要分析修改之后,生成的hash的碰撞问题。 总的来说,学习了一些wasm知识,同时复习了一下sha256算法。

https://github.com/WebAssembly/wabt https://www.pnfsoftware.com/jeb/manual/webassembly/ https://emn178.github.io/online-tools/sha256.html https://gist.github.com/acidsound/716f669d322b702d1bbfefc5748a2b88 https://sha256algorithm.com/ https://emscripten.org/docs/compiling/WebAssembly.html https://geraintluff.github.io/sha256/