一、前言
想要实现在某站点自动化且高效率的执行高危动作,需要完成两个步骤:
1.从接口拉取密钥2.SDK函数使用密钥,进行散列签名即可执行一次高危动作
关键在于获取密钥,然而糟糕的是接口返回的数据进行了加密混淆,本文记录破解其加密算法过程。
这儿的高危动作指对于敏感数据的读写动作
二、在合适的地方断点
首先,梳理清楚网络动作,即这个过程需要通过Burpsuite找到关键的拉取密钥的接口,下面即为定位到的关键接口:
图片可以看到返回的数据做了加密混淆,这个弄清楚了,接下来就有几种方案可以选择实现进一步的分析:
1.关键词断点法2.动作断点法3.异常断点法
2.1 关键词断点法
可以通过接口的URL, 全局查找,然后下断点。
优点:
•常规思路,容易理解
缺点:
•在webpack逐渐流行的前端环境下,常量和逻辑分离,导致断点不在执行流•断点位置特别多•断点位置一般在逻辑最外层,需要非常多次的跟进,这个过程很痛苦且容易出错
推荐指数:
•2星
2.2 动作断点法
利用浏览器自带的断点捕获功能,由于接口是http请求动作,可以下XHR断点。
优点:
•在调用栈回溯父函数容易梳理逻辑•一般跳出几次即可走进关键代码逻辑
缺点:
•有些请求无法捕获•对于有定时请求动作的页面,断点会被频繁触发,需要耐心定位到哪个是目标接口的请求动作•针对请求做了多层封装的情况,调试次数较多
推荐指数:
•3.5星
2.3 异常断点法
猜测构造某种会让程序抛错的数据,进而在抛错位置进行断点。
优点:
•基本不需要怎么回溯,对于没有做异常多层封装的场景,基本断点位置即业务主逻辑
缺点:
•程序不一定抛错,可能做了异常处理•需要构造能导致抛错的数据
推荐指数:
•4星
2.4 通过异常断点法断在关键逻辑
由于对于这个业务执行过程比较了解,可以断定 关键请求接口返回的内容肯定需要被解密才能使用,而解密对于数据有完整性的要求,通过抓响应包,将内容改成123456, 此时在浏览器控制台回显如下报错:
可以推断在66354这行应该是解密的一环,对数据类型做判断,由于篡改了数据导致这儿不正确。
因此可以在66354这行下一个断点,然后再次执行请求动作:
此时,调用堆栈出来了,我们回溯一下,比如回溯到l.encrypt:
在其下面,既有一个decrypt,我们进行断点,基本上此时我们已经到达关键的解密逻辑位置。
三、解密逻辑分析
根据上面,此刻断点在 r(u(s(m))) ,且m即我们需要解密的数据:
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上右键,选择 内存检查器 面板
4.2 依次读取待解密数据
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
$var0 先被赋值了数据的内存地址, 然后进行一系列偏移操作
stack = $var0
然后将$var0对应地址上存放的值取到stack上 , 这儿读取的密文位命名为c :
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:
_ = _ + 32
else:
_ = _ + 127
p = _
基本上,我们把整个解密逻辑分析完成,我们看下密钥在内存中是啥样的(1104 -> 1104+512) :
五、编写解密算法
5.1 密钥提取
5.1.1 方式1 内存导出
var uint8View = new Uint8Array(memories["$env.memory"]["buffer"].slice(1104,1104 + 512));uint8View.toString();
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逆向