抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

概述

在app逆向中常用frida作为hook框架对目标程序进程进行调试等操作,虽然关于frida的教程有不少,但似乎没有一篇从安装配置到实际使用的教程,于是便厚着脸皮写这一篇。

frida GitHub地址

安装frida

1.安装Python

官方要求Python3版本且为Python3.7以上,建议直接安装Python3.7.0
Python 3.7.0下载

2.安装frida的python模块

如果已经将Python相关目录添加到了PATH中,则执行pip install frida,否则的话需要执行python -m pip install frida

同上方法,安装frida-tools模块:pip install frida-tools或者python -m pip install frida-tools

完成安装之后,打开cmd输入frida,查看是否成功执行

firda_command

3.在被调试的机器上安装frida-server

被调试的机器必须root

前往frida的官方github release页面下载对应版本的frida-server
frida release,下载时注意选择与被调试机器架构相同的版本(执行adb shell getprop ro.product.cpu.abi查询)

注意要下载的文件名为frida-server-xxxxxx(版本)-android-架构.xz

下载解压之后,使用命令adb push frida-server在你电脑解压出来的位置 /data/local/tmp将frida-server传输到目标机器中

接下来执行adb shell打开目标机器的shell,执行如下命令给frida-server赋予执行权限并启动frida-server

1
2
3
4
sudo su
cd /data/loca/tmp
chmod +x frida-server
./frida-server

完成以上步骤之后,不要关闭adb shell的窗口,在电脑上继续执行如下命令进行端口转发

1
2
adb forward tcp:27042 tcp:27042
adb forward tcp:27043 tcp:27043

全部完成之后,在电脑上执行frida-ps -U,正常情况下此时应该会列出当前被调试的机器上的进程列表。

frida-ps

当看到这一步的时候,说明frida-server已经成功安装并启动

下次使用时可以通过如下bat脚本快速启动frida-server并配置端口转发(将其中adb目录替换实际情况)

1
2
3
4
5
"D:\Program Files\Nox64\bin\nox_adb" shell "nohup /data/local/tmp/frida-server &"
"D:\Program Files\Nox64\bin\nox_adb" forward tcp:27042 tcp:27042
"D:\Program Files\Nox64\bin\nox_adb" forward tcp:27043 tcp:27043
frida-ps -U
pause

使用frida

1.编写一个注入脚本

frida使用的脚本语言为Javascript,提供的api参考如下文档 frida javascript api

新建一个js脚本文件,输入如下内容

1
2
3
4
5
6
7
8
9
function test(){
console.log(Java.classFactory.loader); //输出目前的classloader
}
function main(){
Java.perform(function(){ //参考frida JavaScript api文档 Java.perform会在class loader加载完毕之后执行回调函数,而Java.perfornNow则不论class loader是否加载好都立刻执行回调函数
test();
});
}
setImmediate(main); //为了防止要执行的函数过慢导致脚本超时

通过如下命令,将拉起目标进程并挂起,将脚本注入进去并恢复目标进程,用这种方法可以避免部分进程的反调试导致无法注入。

frida -U -f com.taobao.taobao -l a.js –no-pause

该命令中拉起的是手机淘宝客户端,注入a.js文件,并且在注入之后自动恢复目标进程。如果不加–no-pause参数则需要之后输入%resume命令恢复目标进程。

正常情况下输出如下:

frida output

可以看到当前的class loader信息已经被打印出来了。

2.对目标类进行hook

使用 jdax 或其他程序对apk进行反编译并寻找要hook的类,在这里我们以hook淘宝的网络类以便展示其进行的所有由mtop类发送的https请求明文为例

首先jdax反编译目标apk,找到要hook的类,本文中使用的手机淘宝9.23.0版本中mtop网络类位于

mtopsdk.network.impl.b

jdax
查看目标类构造函数的签名,可以知道他需要传入两个参数,分别为mtopsdk.network.domain.Requestandroid.content.Context类型

修改我们之前的注入脚本,将test函数中console.log修改为hook内容,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function test(){
var ANetworkCallImpl = Java.use('mtopsdk.network.impl.b');
ANetworkCallImpl.$init.overload('mtopsdk.network.domain.Request', 'android.content.Context').implementation = function(){
console.log("\nANetworkCallImpl "+arguments[0])
var ret = this.$init.apply(this, arguments);
return ret
}
}
function main(){
Java.perform(function(){
test();
});
}
setImmediate(main);

network call
可以看到,所有mtop sdk发出的https请求明文在这里都可以看到了。

3.hook签名方法

通过某些不可描述的方法(百度与google),我们知道了对参数进行签名的关键函数在

com.taobao.wireless.security.adapter.JNICLibrary.doCommandNative

函数声明如下

public static native Object doCommandNative(int arg0, Object[] arg1);

于是针对其编写hook脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function test(){
Java.use("com.taobao.wireless.security.adapter.JNICLibrary").doCommandNative.implementation = function(m,n){
var result = this.doCommandNative(m,n);
for (var j = 0; j < arguments.length; j++) {
console.log("arg[" + j + "]: " + arguments[j] + " => " + JSON.stringify(arguments[j]));
}
console.log("doCommandNative => ",m,n,result);
return result;
}
}

function main(){
Java.perform(function(){
test();
});
}

setImmediate(main);

执行该脚本,发现如下返回

frida docommandnative hook 1
报错找不到该类,nmd wsm.jpg

nmdwsm.jpg

4.定位class loader

仔细观察上面的报错,可以发现如下一条

zip file “/data/app/com.taobao.taobao-0XVGcUBeC_rav8IvTjFXZw==/base.apk”

即当前的class loader为系统默认class loader,而我们希望hook的目标类

com.taobao.wireless.security.adapter.JNICLibrary

是由程序启动加载时动态加载进来的,所以系统默认的class loader中没有该类,这种情况下我们可以使用Java.enumerateClassLoaders(callbacks) 来遍历目前所有的class loader来找到目标类所在的class loader。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function test(){
Java.enumerateClassLoaders({
onMatch: function (loader){
try{
if(loader.findClass("com.taobao.wireless.security.adapter.JNICLibrary")){
console.log("found com.taobao.wireless.security.adapter.JNICLibrary loader");
console.log(loader);
}
}catch(error){
}
},
onComplete: function(){
console.log("enumerateClassLoaders complete");
}
});
}
function main(){
Java.perform(function(){
setTimeout(() => { //这里我们需要延时一小会,否则由于我们程序是刚拉起的,如果不等待的话执行我们脚本的时候目标类可能还没有动态加载进来
test();
}, 1000); //根据你被调试的系统的反应速度调整这个值
});
}
setImmediate(main);

执行结果如下,可见我们要寻找的目标类在libsgmain.so之中

atlibsgmain.so

找到目标类所在的class loader之后,可以通过修改Java.classFactory.loader的值,手动指定当前使用的class loader为前一步找到的loader。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
var loaderSwitched = false;
function test(){
Java.enumerateClassLoaders({
onMatch: function (loader){
try{
if(loader.findClass("com.taobao.wireless.security.adapter.JNICLibrary")){
console.log("found com.taobao.wireless.security.adapter.JNICLibrary loader");
console.log(loader);
Java.classFactory.loader = loader;
loaderSwitched = true;
}
}catch(error){
}
},
onComplete: function(){
console.log("enumerateClassLoaders complete");
}
});
}
function starthook(){
if(!loaderSwitched){
console.log("loader not switched, return");
return;
}
Java.use("com.taobao.wireless.security.adapter.JNICLibrary").doCommandNative.implementation = function(m,n){
var result = this.doCommandNative(m,n);
for (var j = 0; j < arguments.length; j++) {
console.log("arg[" + j + "]: " + arguments[j] + " => " + JSON.stringify(arguments[j]));
}
console.log("doCommandNative => ",m,n,result);
return result;
}
}
function main(){
Java.perform(function(){
setTimeout(() => {
test(); //先执行寻找classloader
}, 1000);
setTimeout(() => {
starthook(); //等classloader寻找估计差不多了,再执行hook
}, 2000);
});
}
setImmediate(main);

执行该脚本,返回如下

docommandnative hooked
可以看到目标函数已经被成功hook并且打印出了输入和返回值,观察打印的内容我们发现,调用xsign签名算法的时候第一个参数为 70102
修改以上脚本使其只打印arg[0]为70102的部分,效果如下

70102

其传入和传出的参数,正是我们提供签名的内容和签名出来的x-sign。

使用frida-rpc

1.frida-rpc的配置

为了将我们发现的目标函数导出以便电脑其他程序调用,我们需要利用frida提供的frida-rpc框架

首先需要修改注入的脚本,将要导出的目标函数传递给rpc.exports

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
var loaderSwitched = false;
function test(){
Java.enumerateClassLoaders({
onMatch: function (loader){
try{
if(loader.findClass("com.taobao.wireless.security.adapter.JNICLibrary")){
console.log("found com.taobao.wireless.security.adapter.JNICLibrary loader");
console.log(loader);
Java.classFactory.loader = loader;
loaderSwitched = true;
}
}catch(error){
}
},
onComplete: function(){
console.log("enumerateClassLoaders complete");
}
});
}
function main(){
Java.perform(function(){
setTimeout(() => {
test();
}, 1000);
});
}
rpc.exports = {
sign: function(func, input, data){
if(loaderSwitched){
var Integer = Java.use("java.lang.Integer");
var Boolean = Java.use("java.lang.Boolean");
var String = Java.use("java.lang.String");
var argList = Java.array("Ljava.lang.Object;",
[
String.$new("21646297"),//app id,安卓固定为21646297
String.$new(input), //要签名的数据拼接之后
Boolean.$new(false),//固定false
Integer.$new(0), //固定为0
String.$new(func), //调用的mtop方法名
String.$new(data), //调用mtop方法传入的参数
null, //固定null
null, //固定null
null, //固定null
String.$new("r_1") //数字代表这是第几个请求,其实并不影响签名
]
);
var Map = Java.use('java.util.HashMap');
var ret = Java.use("com.taobao.wireless.security.adapter.JNICLibrary").doCommandNative(70102, argList); //调用70102方法签名x-sign
var args_map = Java.cast(ret, Map);
var jsonRet = {"x-sgext":args_map.get("x-sgext").toString(), "x-umt":args_map.get("x-umt").toString(), "x-mini-wua":args_map.get("x-mini-wua").toString(), "x-sign":args_map.get("x-sign").toString()}
return {"Success":true,"Data":jsonRet};
}else{
return {"Success":false,"Reason":"Not loaded"};
}
}
};
setImmediate(main);

同时我们还需要编写对应的Python脚本将上面的JavaScript脚本注入并用来调用导出的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
#coding=utf-8
import frida
from time import sleep

device = frida.get_device_manager().enumerate_devices()[-1] #列出设备并选择最后一个
pid = device.spawn(["com.taobao.taobao"]) #启动淘宝并暂停
session = device.attach(pid) #attach到进程
spt = open("F:\\a.js", encoding="utf-8").read()
script = session.create_script(spt)
script.load() #注入脚本
device.resume(pid) #恢复进程执行
sleep(2) #等待脚本找到class loader
print(script.exports.sign("test", "test", "test"))

Python执行该脚本,查看返回

python xsign
可以看到Python已经成功远程调用了我们导出的签名函数。

其他说明

1.关于导出的签名函数需要的三个参数具体内容

hook导出的签名函数
sign: function(func, input, data)

第一个参数func,值为所调用的mtop api名称,例如解析淘口令时api名称为mtop.taobao.sharepassword.querypassword

第二个参数input,值为请求header中参与签名的那部分(deviceId,utdid,ttid,extdata等)经过特定排序组合起来的值,其排序方法为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def params2signdata(params):
names = ["uid", "reqbiz-ext", "appKey", "data", "t", "api", "v", "sid", "ttid", "deviceId", "lat", "lng", "extdata", "x-features", "routerId", "placeId", "open-biz","mini-appkey", "req-appkey", "accessToken", "open-biz-data"]
signStr = ""
if "utdid" in params:
signStr += params["utdid"]
for name in names:
if name == "data":
signStr += "&" + hashlib.md5(params[name].encode("utf-8")).hexdigest()
continue
if name == "extdata" and name in params and params[name] == "":
continue
if name not in params or params[name] == None:
params[name] = ""
signStr += "&" + str(params[name])
return signStr

第三个参数data,值为类似 “pageId=xxx&pageName=xxx” 的标识目前所在页面的内容,其中pageId的值需要进行一次urlencode

2.淘宝的反调试

淘宝运行起来之后会定期通过读取 /proc/{PID}/status 文件检查自己是否被调试,虽然目前不对其进行处理也能正常运行和hook,但稳妥起见可以对这个读取行为进行hook让他无法通过这个检查到自己是否被调试,代码如下(这段代码来自 r0tracer项目):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var ByPassTracerPid = function () {
var fgetsPtr = Module.findExportByName("libc.so", "fgets");
var fgets = new NativeFunction(fgetsPtr, 'pointer', ['pointer', 'int', 'pointer']);
Interceptor.replace(fgetsPtr, new NativeCallback(function (buffer, size, fp) {
var retval = fgets(buffer, size, fp);
var bufstr = Memory.readUtf8String(buffer);
if (bufstr.indexOf("TracerPid:") > -1) {
Memory.writeUtf8String(buffer, "TracerPid:\t0");
console.log("tracerpid replaced: " + Memory.readUtf8String(buffer));
}
return retval;
}, 'pointer', ['pointer', 'int', 'pointer']));
};
setImmediate(ByPassTracerPid);

评论