环境
- 手机
- 小米8
- 版本
- 33.4.3
准备
-
抓包失败
-
开了抓包以后APP直接是网络无连接了,并且抓到的请求也是不正常的,下面我们就开始定位
日志分析
-
可以从日志分析入手,看看有没有什么异常的报错
-
使用
adb logcat
命令查看-
- 先用 adb logcat -c 请一下日志
- 执行 adb logcat 命令
- 然后打开tiktok app
- 出现发生错误这个页面后停止 adb locat 命令
- 把文件保存到本地进行分析
-
定位(整个日志中,只有此处有发送网络请求而且失败,所以一眼定位这里)
-
日志里找到有一个请求URL的位置并且有一个
|Exception in CronetUrlRequest
像是网络错误的请求所以分析一下这个位置
jadx反编译
-
拿上面的这个
Exception in CronetUrlRequest
去jdax里进行搜索 -
上面搜索到三个位置,后两个是同一个函数,所以hook一下这两个地方,打印下堆栈
-
直接右键复制为Frida片段就可以了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java.perform(
function
() {
let g = Java.use(
"org.chromium.g"
);
g[
"LIZ"
].implementation =
function
(i, i2, str) {
console.log(`g.LIZ is called: i=${i}, i2=${i2}, str=${str}`);
let result =
this
[
"LIZ"
](i, i2, str);
console.log(`g.LIZ result=${result}`);
console.log(Java.use(
"android.util.Log"
).getStackTraceString(Java.use(
"java.lang.Throwable"
).$
new
()));
return
result;
};
let CronetUrlRequest = Java.use(
"com.ttnet.org.chromium.net.impl.CronetUrlRequest"
);
CronetUrlRequest[
"onError"
].implementation =
function
(i, i2, i3, str, j) {
console.log(`CronetUrlRequest.onError is called: i=${i}, i2=${i2}, i3=${i3}, str=${str}, j=${j}`);
this
[
"onError"
](i, i2, i3, str, j);
console.log(Java.use(
"android.util.Log"
).getStackTraceString(Java.use(
"java.lang.Throwable"
).$
new
()));
};
})
-
打印结果
-
上面图片可以看到所有的打印都是走的
CronetUrlRequest.onError
这个函数,他是一个Native层的方法 那下面来分析一下So层
定位So
-
直接把app文件 重命名一下
.apk
改为.zip
-
直接找到
tiktok-33-4-3.zip\lib\arm64-v8a
这里面有很多so文件,怎么定位到我们需要的so文件那 -
我这里使用grep进行搜索
grep的作用就是搜索到我们要定位的字符串在那个So文件里
grep的安装可以看这个
https://blog.youkuaiyun.com/qq_29752857/article/details/140169107
-
把.apk后缀改为.zip
-
压解到一个文件夹
-
cd 到解压文件夹的
\lib\arm64-v8a
目录 打开cmd -
执行命令
grep -r "CronetUrlRequest" *
-
显示在
libsscronet.so
这个So文件中
分析So
-
从
\lib\arm64-v8a
下面找到libsscronet.so
这个文件丢到ida64中 -
按Shift F12 字符串搜索
CronetUrlRequest
-
点击
com/ttnet/org/chromium/net/impl/CronetUrlRequest
-
进入
sub_190028+8↓o
然后按tab键 -
Hook下这个位置
190028
-
这里为了方便注释Hook的代码,拆分开写的
-
主函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hook_dlopen(module_name, fun) {
var android_dlopen_ext
=
Module.findExportByName(null,
"android_dlopen_ext"
);
if
(android_dlopen_ext) {
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
pass
},
onLeave: function (retval) {
passa
}
});
}
}
function main() {
hook_dlopen(
"libsscronet.so"
)
}
setImmediate(main)
- 注释
- Hook
android_dlopen_ext
监控.so
加载 - 判断是不是我们要hook的"libsscronet.so"文件
- Hook
- 注释
-
onEnter
1
2
3
4
5
6
7
8
9
onEnter:
function
(args) {
var
pathptr = args[0];
// 读取第一个参数,它是一个指向动态库路径的指针
if
(pathptr) {
// 确保指针有效,避免 `null` 访问错误
this
.path = pathptr.readCString();
// 将指针解析成字符串,得到动态库的路径
if
(
this
.path.indexOf(module_name) >= 0) {
// 判断路径是否包含目标库名
this
.canhook =
true
;
// 发现目标库,标记 `this.canhook = true`,表示可以 hook
}
}
}
- 注释
onEnter
在目标函数(android_dlopen_ext
)执行前触发- 如果路径是否包含目标库名,把canhook设置为True
- 注释
-
onLeave
1
2
3
4
5
6
7
8
9
10
11
12
13
14
onLeave:
function
(retval) {
if
(
this
.canhook) {
// 只有在 onEnter 里确认目标库被加载了,才会执行 hook 逻辑
let base_libsscronet = Module.findBaseAddress(
"libsscronet.so"
);
// 获取 libsscronet.so 的基地址
let sub_190028 = base_libsscronet.add(0x190028);
// 计算 sub_190028 函数的地址(基地址 + 偏移 0x190028)
console.log(
"hook"
, sub_190028);
// 打印目标函数的地址,便于调试
Interceptor.attach(sub_190028, {
// Hook 目标函数
onEnter(args) {
console.log(
"sub_190028 called, return address:"
, DebugSymbol.fromAddress(
this
.returnAddress));
// `this.returnAddress` 是调用 `sub_190028` 的上层函数的返回地址
// `DebugSymbol.fromAddress` 解析该地址,尝试获取调用 `sub_190028` 的函数名
}
});
}
}
-
注释
-
this.returnAddress
返回的是调用者的地址 -
DebugSymbol.fromAddress
是为了把地址转为函数名
-
-
-
-
打印结果
-
按G跳转到
0x18fae8
- 偏移量在V16上
-
点进
sub_18209C
这个函数.cc
文件是 C++ 源代码文件,它的作用与.cpp
文件相同,通常用于存放 C++ 代码
-
跟进
sub_182194
-
看到这块信息Hook一下
sub_20E814
可以看到第二个是一个String ,所以hook代码要修改一下把第二个参数String也打印
onLeave: function (retval) { if (this.canhook) { let base_libsscronet = Module.findBaseAddress("libsscronet.so"); let sub_20E814 = base_libsscronet.add(0x20E814); console.log("hook", sub_20E814); Interceptor.attach(sub_20E814, { onEnter(args) { console.log("sub_20E814 called, return address:",args[1].readCString()), DebugSymbol.fromAddress(this.returnAddress); } }); } }
-
输出结果
- 这个地方明显不是,app已经出现连接失败了,后面过了一会才有的打印,说明这个打印已经晚了,如果是我们想要的位置,他应该在检测中进行打印
- 上面的输出其实并没有什么有效信息
分析引用
-
那这样我们线索已经断了,先按X看一下这个函数都是有谁引用了 看看有没有线索
-
看了半天感觉也没啥有用的东西,那下面我们整理一下已经有信息,来猜测一下,后续怎么进行
-
那目前我们根据已经有信息,看到代码里有用
../../net/tt_net/ipc/ipc_channel_reader.cc"
这样的方式去写的 -
那可以猜测,我们真正需要Hook的那个函数也有可能是这样加载的,可以字符串搜索一下
../../
-
看到有几个目录
net
base
components
等,我们是为了解决抓包问题,那很有可能是在net目录下(network),那来搜索一下../../net
-
还是比较多的,这个时候可以有选择的看一下,跟网络请求相关的
quic
ssl
等 -
最后定位到是
../../net/socket/ssl_client_socket_impl.cc
这个文件
-
-
然后点进来看一下
-
hook下
20E7EC
-
看到有一个叫
HandleVerifyResult
比较像,我们跳进去看一下0x3174dc
- 如果打印不出
HandleVerifyResult
可以换个手机试下
- 如果打印不出
-
然后向上滑找到函数看一下是谁在引用, 因为怀疑这一段是 错误处理 所以先看一下谁调用他
-
这里有两个点那个都可以,最后都是一个位置,进来之后继续找到函数看一下引用
-
接着点进去,进去以后看到很多个函数都点开看一下,最后定位到
sub_4913E8()
上面 -
我们可以搜一下
SSL_CTX_set_custom_verify
这是什么意思 -
第三个参数是一个回调函数,这里他的返回值1表示成功,0表示失败,那我们手动把他Hook为0,让他不支持quic协议,实现降级
-
就能成功绕过校验了~!
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
function
hook_dlopen(module_name, fun) {
var
android_dlopen_ext = Module.findExportByName(
null
,
"android_dlopen_ext"
);
if
(android_dlopen_ext) {
Interceptor.attach(android_dlopen_ext, {
onEnter:
function
(args) {
var
pathptr = args[0];
if
(pathptr) {
this
.path = (pathptr).readCString();
if
(
this
.path.indexOf(module_name) >= 0) {
this
.canhook =
true
;
}
}
},
onLeave:
function
(retval) {
if
(
this
.canhook) {
let verifyadd = Module.getExportByName(
"libsscronet.so"
,
"SSL_CTX_set_custom_verify"
);
Interceptor.attach(verifyadd,{
onEnter(args){
Interceptor.attach(args[2],{
onLeave(retval){
retval.replace(0x0);
}
})
}
})
}
}
});
}
}
-
成功搞定
-
就可以抓包并且上TikTok了