[AOH 008]realworld前端混淆破解-WebAssembly逆向

渗透技巧 2年前 (2021) admin
1,178 0 0

一、前言

想要实现在某站点自动化且高效率的执行高危动作,需要完成两个步骤:

1.从接口拉取密钥2.SDK函数使用密钥,进行散列签名即可执行一次高危动作

关键在于获取密钥,然而糟糕的是接口返回的数据进行了加密混淆,本文记录破解其加密算法过程。

这儿的高危动作指对于敏感数据的读写动作

二、在合适的地方断点

首先,梳理清楚网络动作,即这个过程需要通过Burpsuite找到关键的拉取密钥的接口,下面即为定位到的关键接口:

[AOH 008]realworld前端混淆破解-WebAssembly逆向

图片可以看到返回的数据做了加密混淆,这个弄清楚了,接下来就有几种方案可以选择实现进一步的分析:

1.关键词断点法2.动作断点法3.异常断点法

2.1 关键词断点法

可以通过接口的URL, 全局查找,然后下断点。

优点:

常规思路,容易理解

缺点:

在webpack逐渐流行的前端环境下,常量和逻辑分离,导致断点不在执行流断点位置特别多断点位置一般在逻辑最外层,需要非常多次的跟进,这个过程很痛苦且容易出错

推荐指数:

2星

2.2 动作断点法

利用浏览器自带的断点捕获功能,由于接口是http请求动作,可以下XHR断点。

优点:

在调用栈回溯父函数容易梳理逻辑一般跳出几次即可走进关键代码逻辑

缺点:

有些请求无法捕获对于有定时请求动作的页面,断点会被频繁触发,需要耐心定位到哪个是目标接口的请求动作针对请求做了多层封装的情况,调试次数较多

推荐指数:

3.5星

2.3 异常断点法

猜测构造某种会让程序抛错的数据,进而在抛错位置进行断点。

优点:

基本不需要怎么回溯,对于没有做异常多层封装的场景,基本断点位置即业务主逻辑

缺点:

程序不一定抛错,可能做了异常处理需要构造能导致抛错的数据

推荐指数:

4星

2.4 通过异常断点法断在关键逻辑

由于对于这个业务执行过程比较了解,可以断定 关键请求接口返回的内容肯定需要被解密才能使用,而解密对于数据有完整性的要求,通过抓响应包,将内容改成123456, 此时在浏览器控制台回显如下报错:

[AOH 008]realworld前端混淆破解-WebAssembly逆向

可以推断在66354这行应该是解密的一环,对数据类型做判断,由于篡改了数据导致这儿不正确。

因此可以在66354这行下一个断点,然后再次执行请求动作:

[AOH 008]realworld前端混淆破解-WebAssembly逆向

此时,调用堆栈出来了,我们回溯一下,比如回溯到l.encrypt:

[AOH 008]realworld前端混淆破解-WebAssembly逆向

在其下面,既有一个decrypt,我们进行断点,基本上此时我们已经到达关键的解密逻辑位置。

三、解密逻辑分析

根据上面,此刻断点在 r(u(s(m))) ,且m即我们需要解密的数据:

[AOH 008]realworld前端混淆破解-WebAssembly逆向

3.1 s函数

packString: function(t) {    var e = o.lengthBytesUTF8(t)    , r = o._createBuffer(e)    , s = o.getValue(r + p, "i32*");    return A(t, o.HEAPU8, s, s + e),    r}    function A(o, t, e, r) {    var s = t.subarray(e, r);    if (typeof o != "string") {        s.fill(0),            console.error(new Error("Invalid input: input not a string, cannot code"));        return    }    if (g)        if (g.encodeInto)            g.encodeInto(o, s);        else {            var u = g.encode(o);            s.set(u)        }    else if (typeof f != "undefined") {        var h = f.from(o, "utf-8");        h.copy(s)    } else        for (var u = I.encode(o), v = 0; v < s.length; v++)            s[v] = u.charCodeAt(v)}

看起来很复杂,动态调试来看,非常简单,即将字符串t,依次取值转为整数,保存在一个内存地址上保存在数组中

该函数参数为字符串,返回结果为整数,该整数对应了在内存保存数据的地址。

3.2 u函数

decrypt: function(t) {    return o._vigenereDecrypt(t)}
(func $vigenereDecrypt (;13;) (export "vigenereDecrypt") (param $var0 i32) (result i32)    (local $var1 i32) (local $var2 i32) (local $var3 i32) (local $var4 i32) (local $var5 i32)...)

参数t为整数,对应需要解密数据的内存地址

$vigenereDecrypt 可能是C或者rust编译而来,在浏览器调试下,只能看到是wasm 的形式

3.2.1 维吉尼亚算法

函数名vigenereDecrypt其实给了我们很大的提示,可以简单了解下维吉尼亚算法,对于后续的解密流程会更加得心应手。

简单举例如下:

有明文: ABCDEFGHIJK

有密钥: KEY

加密过程:

1.首先密钥需要对齐明文, 即变为KEYKEYKEYKE2.依次取明文和密钥一位,如第一组,A,K ;第二组 B,E3.代入二元一次方程, 或密码表, 算出密文

常见攻击思路:

1.唯密文攻击:词频攻击2.已知明文攻击:对于上面加密的第三步,如果密码表是线性的,那么通过密文和明文是非常容易推导出密钥的

3.3 r函数

s函数的反向操作,即将内存中的每个整数转换为字符并拼接为字符串。

四、WebAssembly代码分析

接下来深入分析$vigenereDecrypt函数:

  (func $vigenereDecrypt (;13;) (export "vigenereDecrypt") (param $var0 i32) (result i32)    (local $var1 i32) (local $var2 i32) (local $var3 i32) (local $var4 i32) (local $var5 i32)    local.get $var0    i32.load offset=4    local.tee $var3    if      local.get $var0      i32.load offset=8      local.set $var4      loop $label0        local.get $var2        local.get $var4        i32.add        local.tee $var5        i32.load8_u        local.tee $var1        i32.const -32        i32.add        i32.const 255        i32.and        i32.const 94        i32.le_u        if          local.get $var1          local.get $var2          i32.const 512          i32.rem_s          i32.const 1104          i32.add          i32.load8_u          i32.sub          local.tee $var1          i32.const 127          i32.add          local.get $var1          i32.const 32          i32.add          local.tee $var1          local.get $var1          i32.const 24          i32.shl          i32.const 24          i32.shr_s          i32.const 32          i32.lt_s          select          local.set $var1        end        local.get $var5        local.get $var1        i32.store8        local.get $var2        i32.const 1        i32.add        local.tee $var2        local.get $var3        i32.ne        br_if $label0      end $label0    end    local.get $var0  )

关于指令的基本知识可以在参考下的链接进行查阅,这儿直接分析逻辑实现.

4.1 打开内存检查器

在$env.memory上右键,选择 内存检查器 面板

[AOH 008]realworld前端混淆破解-WebAssembly逆向

4.2 依次读取待解密数据

local.get $var0i32.load offset=4local.tee $var3if    local.get $var0    i32.load offset=8    local.set $var4    loop $label0    local.get $var2    local.get $var4    i32.add    local.tee $var5    i32.load8_u

$var0 先被赋值了数据的内存地址, 然后进行一系列偏移操作

stack = $var0

然后将$var0对应地址上存放的值取到stack上 , 这儿读取的密文位命名为c :

[AOH 008]realworld前端混淆破解-WebAssembly逆向

4.3 数据检查

    local.tee $var1    i32.const -32    i32.add    i32.const 255    i32.and    i32.const 94    i32.le_u    if

c为当前密文位

(c – 32 ) & 255 <= 94 若成立,进入到if分支

4.4 依次读取密钥

    if        local.get $var1        local.get $var2        i32.const 512        i32.rem_s        i32.const 1104        i32.add        i32.load8_u        i32.sub        local.tee $var1        i32.const 127        i32.add        local.get $var1        i32.const 32        i32.add        local.tee $var1        local.get $var1        i32.const 24        i32.shl        i32.const 24        i32.shr_s        i32.const 32        i32.lt_s        select        local.set $var1    end

$var2 从0开始自增,即每读取一位数,进入if分支,其自增1

$var2 = $var2 % 512, 说明密钥长度应该是512位,超过了又从头开始,这和前面分析的维吉尼亚算法重复密钥的逻辑吻合

接着从 1104 + $var2 内存地址上取值, 作为当前位的密钥, 这儿命名为 k

接下来的是一个选择分支, 命令p为明文,接下来用python描述c(密文) , k(密钥) , p(明文)三者的关系:

_ = (c-k)if _ + 127 >= 127:    _ = _ + 32else:    _ = _ + 127p  = _

基本上,我们把整个解密逻辑分析完成,我们看下密钥在内存中是啥样的(1104 -> 1104+512) :

[AOH 008]realworld前端混淆破解-WebAssembly逆向

五、编写解密算法

5.1 密钥提取

5.1.1 方式1 内存导出

var uint8View = new Uint8Array(memories["$env.memory"]["buffer"].slice(1104,1104 + 512));uint8View.toString();

[AOH 008]realworld前端混淆破解-WebAssembly逆向

5.1.2 方式2 已知明文攻击

因为最开始使用明文攻击方式反向求解密钥和真实密钥存在偏差,在修正偏差的过程中,更正了很多逻辑,所以还是非常有意义的

首先,在Burpsuite将返回数据,在Hex展示页中选中,Copy to file 保存为cipher_1

然后,在解密最外层拿到解密后的数据

# 密文with open("cipher_1","rb") as f:    z = f.read()cipher_list = []for one in z:    cipher_list.append(int(one))# 明文plaintext_list = []plaintext = '{XXXXXXXX}'# JS动态调试拿到解密的明文for one in plaintext:    plaintext_list.append(ord(one)) # 求解出密钥key_ = []for i in range(len(plaintext_list)):    _ = (cipher_list[i] - plaintext_list[i])    if _ + 127 >= 127:        _ = _ + 32    else:        _ = _ + 127    key_.append(_ % 127 )

5.2 解密算法

'''cipher_ = [int,int...]key_ = [int,int...]'''def decrpyt_pretty(cipher_,key_):    plaintext = ""    for i in range(len(cipher_)):        _ = (cipher_[i] - key_[i % len(key_)])        if _ + 127 >= 127: # 当然也可以写成 _ >= 0            _ = _ + 32        else:            _ = _ + 127        plaintext += chr(_)    return plaintext

至此,我们可以对响应数据做自动化解密。

六、小结

进一步学习了混淆场景下的JS调试

第一次尝试WebAssembly逆向,对于维吉尼亚这种古典密码的逆向也算踩过坑了,相信对于后面浏览器内核的逆向会带来帮助

最后的最后,不要相信前端的混淆加密能保护你的秘密, Will Be Hack!

七、参考

[1] 使用Chrome调试wasm https://developer.chrome.com/blog/wasm-debugging-2020/

[2] wasm相关指令介绍 https://openhome.cc/eGossip/WebAssembly/Number.html

[3] wasm相关指令介绍 https://juejin.cn/post/6844904077411745800#heading-4


原文始发于微信公众号(Art Of Hunting):[AOH 008]realworld前端混淆破解-WebAssembly逆向

版权声明:admin 发表于 2021年11月25日 上午2:29。
转载请注明:[AOH 008]realworld前端混淆破解-WebAssembly逆向 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...