-
最后还剩下一个图片请求返回的内容是加密的,本章把这个图片加密解一下
-
本篇做的是
web
的滑块,app跟web用的是同样的加密,用扣代码的方式把这个搞定
环境
- 滑块版本:
3.0.22
- 滑块地址: 点击
定位
-
上图可以看到后端返回的内容是一个加密的值,那解密完成后会是一个字符串,然后要转成json供后面使用,肯定要调用到
JSON.parse
这个函数 -
所以这里直接Hook
JSON.parse
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const originalJsonParse = JSON.parse;
// 保存原始的 JSON.parse 函数
// 创建一个新的 JSON.parse 函数
JSON.parse =
function
(text, reviver) {
try
{
const result = originalJsonParse(text, reviver);
// 调用原始的 JSON.parse 函数
debugger;
console.log(
"Parsed result:"
, result);
return
result;
}
catch
(error) {
console.error(
"JSON parsing error:"
, error);
throw
error;
}
};
-
从Console中执行后,刷新下图片,重新请求
-
刷新后,就会hook到好几个请求,很多明显不是请求相关的,一直放过就行,直到看见下面这个明显就是解密后的结果,我们可以跟一下调用栈
ts文件格式化
-
有些朋友hook以后可能跟我的页面不一样,文件是
.ts
的代码都在一行 无法格式化(如下图) -
这里需要设置一下
-
设置 ==> 然后找到
JavaScript source maps
去掉前面的对号 就可以了
流程分析
-
从hook处向上点一个堆栈 看到如下位置的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
case
10:
return
a = e.sent,
e.next = 13,
a.decryptData(atob(o.data));
case
13:
if
(c = e.sent,
s = {},
e.prev = 15,
s = JSON.parse(c.decData),
!Qo) {
e.next = 23;
break
}
return
Qo = !1,
e.next = 21,
_n(
"fetch_picture_start"
);
c.decData
就是最后解密出来的值c = e.sent
这句话的意思是 c就是上一个case
的返回值,所以我们要看谁跳到的case13
case10
中能看到有e.next = 13
说明他的下一个case
就是13 所以我们要看case10
的返回值a.decryptData(atob(o.data));
这是case10
的最后一句话,我们断点打过来打一下- 这里需要刷新一下图片,因为刚的断点位置已经过去了
- 还有我们前面的hook 也可以去掉了
JSON.parse = originalJsonParse;
执行一下 就可以
-
这里
o.data
就是服务端返回的加密的内容,然后做了一个atob
,通过a.decryptData(atob(o.data))
变成了正常的内容 -
跟进
a.decryptData
看一下解密的实现 -
这里把atob后的内容传进来,然后调用了
e
函数传进去 -
再跟进
e
看一下 -
这里是个异步直接单步跟,从
e.apply
进到下一个函数 -
这里我们分析了一下其他代码没啥核心的,直接把断点下到
switch
看一下执行流程 -
多点几次最后定位到
case17
中的 d 就是我们需要的内容 -
上面看到有一个
ccall
的调用,那加密的核心逻辑很有可能是在这里,我们跟进去看一下 -
进入之后,有一个
a.apply
的调用,看起来比较像核心位置,点击进去 -
调用了
Oe.h
方法,点进Oe.h
这里就是wasm
代码了
保存wasm文件
-
本节我们用扣代码的方式来把这个
wasm
搞定,下一节 会写 App的wasm
纯算 -
首先 我们先把当前打开的这个
wasm
文件保存下来 -
新起一个项目,建一个js文件,用来扣代码,然后把上面我们存下来的wasm文件和js放到同级
还原控制流
-
既然我们是从控制流进来的,我们先手动还原一下这个控制流看看都在干嘛
-
这块代码我们按照
case
执行顺序 改完后就是这样1
2
3
4
5
6
7
8
9
10
11
12
13
n = a(atob(r));
u = t.allocate(n, t.ALLOC_NORMAL);
s = i(4);
l = t.ccall(
"cmd_clientDecrypt"
,
"number"
, [
"number"
,
"number"
,
"string"
,
"number"
], [u, n.length,
null
, s.offset]);
f = t.getValue(s.offset,
"i32"
);
A = i(f);
t.ccall(
"cmd_clientDecrypt"
,
"number"
, [
"number"
,
"number"
,
"number"
,
"number"
], [u, n.length, A.offset, s.offset]);
f = t.getValue(s.offset,
"i32"
);
d = t.UTF8ArrayToString(A.data, 0, f);
t._free(u);
c(s);
c(A);
console.log(d)
-
最后拿到
d
也就能拿到解密后的结果了
找加载位置
-
wasm
如果没有基础的同学,去看星球里这篇文章,讲的很清晰 -
wasm
有同步和异步两种加载方式-
同步:
1
2
const module =
new
WebAssembly.Module(wasmBuffer);
const instance =
new
WebAssembly.Instance(module, importObject);
-
异步:
1
2
WebAssembly.instantiateStreaming (需要比较新浏览器)
WebAssembly.instantiate (兼容性更好)
-
-
所以,同步加载有一种方式,异步加载有两种方式,那我们直接搜索一下,把有加载的位置都下上断点,看看走了那一个
-
异步加载一
-
同步加载一
-
-
搜索完发现就这两处,直接都下上断点,重新运行
-
停在了这里 我们看一下
e
是一个和很长的数组,其实这里向上跟两个堆栈就能看到,这个e就是wasm的代码,也就是我们上面从网页上保存的那个.wasm
的文件 -
所以这里就是我们要找的加载位置
扣代码
- 这里加载都是在一起的,我们可以直接把这一块代码都扣出来
-
首先把全部的代码复制到一个可以折叠的编辑器里,我这里是
notepad++
-
折叠完后,直接搜索
WebAssembly.instantiate(e, t)
这一块加载代码 -
搜到之后 我们直接向上找他属于那一个代码块,把内容拿下来,不用函数包裹了
-
开始:
-
结束:
-
-
直接复制到我们的js文件
- 复制过来之后,586行会报个错,因为我们直接粘过来,他不是一个函数了,不需要返回,所以删掉这两行就可以了
-
处理一下,代码里的加载方式,让他读我们本地的文件,并且把调用方式用改成同步的,同步处理起来会简单些
- 这块代码我们直接改为同步加载
1
2
3
4
5
6
function
V(e, t, r) {
const fs = require(
"fs"
);
n =
new
WebAssembly.Module(fs.readFileSync(
"./00021e0a.wasm"
));
// 传入前面保存的wasm文件路径
b =
new
WebAssembly.Instance(n,t);
// 第一个参数是Model 第二个参数是 导入的对象
return
r(b);
}
-
然后加载代码搞好之后,我们把前面控制流的代码封成一个函数,放在底部
-
我们运行一下
z1
函数,然后会出现个报错-
原因是因为Oe是一个空对象,那我们看一下Oe的生成
-
就是这个t函数 我们看一下t的返回值
-
问题就出在这,我们修改一下 让他返回t的执行结果
1
2
3
X(0, z, e, (
function
(e) {
return
t(e)
}
-
改成上面的就可以了
-
-
报错
r
没有定义,这里r 就是服务端返回的加密的内容,我们去复制一个 -
直接去调用一下
- 复制结果,放到js里就可以
let r = 复制的结果
- 复制结果,放到js里就可以
-
报错
a
没有定义- 这里直接去把a扣一下
- 这里看到下一行有个
i
函数,我们后面也会用到就一起拿下来 - 还有一个
c
函数,也没有,我们直接点进去一起扣下来了
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
a =
function
(e) {
var
t = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0,
r = 0;
if
(0 == (r = t <= 0 ? e.length : t))
return
null
;
for
(
var
n =
new
Array(r), o = 0; o < r; o++)
if
(o > e.length)
n[o] = 0;
else
{
var
i = e.charCodeAt(o);
n[o] = i
}
return
n
},
i =
function
(e) {
if
(e <= 0)
return
null
;
var
r = t._malloc(e);
return
null
== r ?
null
: (t.HEAPU8.set(
new
Uint8Array(e), r), {
data: t.HEAPU8.subarray(r, r + e),
offset: r
})
}
c =
function
(e) {
e &&
"number"
==
typeof
e.offset ? t._free(e.offset) : console.log(
"free error "
, e)
}
- 这里直接去把a扣一下
-
报错
t
未定义- 我们在网站上看一下
t
是什么
-
t 有这么多的方法,我们随便搜一个
-
看到一段熟悉的代码,前面我们处理
return
错误的时候就是这个函数,所以这里的c
就是我们用到的t
- 我们在网站上看一下
-
再次运行,就成功解密了