安卓逆向入门-Frida入门

环境准备

1
2
3
4
5
6
7
8
9
10
pip install frida frida-tools objection frida-dexdump jnitrace#objection需要安装比frida晚的版本 有必要时自己指定一下版本号 frida应为14.2.18及之前版本
frida --version #根据版本去Github下载相应frida-server
adb push ./frida-server-16.3.3-android-x86_64 /data/local/tmp
adb shell
su
cd /data/local/tmp
mv ./frida-server-16.3.3-android-x86_64 ./frida-server
chmod 777 ./frida-server
./frida-server #运行不了则尝试setenforce 0关闭SELinux
frida-ps -U #另开一个终端

对于Objection 1.11.0的打开\Lib\site-packages\objection\commands\frida_commands.py,注释掉第38行('Script Filename', frida_env['filename']),

下载https://github.com/hluwa/Wallbreaker ,随便找个地方放着。

若为Objection 1.11.0,可打开Python\Lib\site-packages\objection\agent.js,修改第18753行如下,这样使用命令android hooking watch class后可对类中所有函数进行Hook,包括改之前不支持的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// start a new job container
const job = {
identifier: jobs_1.jobs.identifier(),
implementations: [],
type: `watch-class for: ${clazz}`,
};
uniqueMethods.concat(["$init"]).forEach((method) => { //这里
clazzInstance[method].overloads.forEach((m) => {
// get the argument types for this overload
const calleeArgTypes = m.argumentTypes.map((arg) => arg.className);
send(`Hooking ${color_1.colors.green(clazz)}.${color_1.colors.greenBright(method)}(${color_1.colors.red(calleeArgTypes.join(", "))})`);
// replace the implementation of this method
// tslint:disable-next-line:only-arrow-functions
m.implementation = function () {
send(color_1.colors.blackBright(`[${job.identifier}] `) +
`Called ${color_1.colors.green(clazz)}.${color_1.colors.greenBright(m.methodName)}(${color_1.colors.red(calleeArgTypes.join(", "))})`);
// actually run the intended method
return m.apply(this, arguments);
};
// record this implementation override for the job
job.implementations.push(m);
});
});

入门

例如工程:

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
package com.example.myapplication;
import android.content.DialogInterface;
import android.os.Bundle;
import android.widget.Button;
import android.view.View;
import android.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button=findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View view){
AlertDialog.Builder alterDialog=new AlertDialog.Builder(MainActivity.this);
alterDialog.setTitle("");
alterDialog.setMessage(getString());
alterDialog.setPositiveButton("确定",new DialogInterface.OnClickListener(){
@Override
public void onClick(DialogInterface dialog,int which){}
});
alterDialog.setNegativeButton("取消",new DialogInterface.OnClickListener(){
@Override
public void onClick(DialogInterface dialog,int which){}
});
alterDialog.show();
}
});
}
private String getString(){
return "正常输出";
}
}

布局文件:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="click"/>
</LinearLayout>

此时Hook“正常输出”改为“应用已被Hook”。

1
2
3
4
5
6
7
8
9
10
11
12
13
setImmediate(function(){
Java.perform(function(){
send("starting script");
var Activity=Java.use("com.example.myapplication.MainActivity");
Activity.getString.overload().implementation=function(){
var result=this.getString();
send("getString="+result);
var newResult="应用已被Hook!";
send(newResult);
return newResult;
};
});
});

先启动APP,再用运行方法:

1
frida -U -l ./frida_hook.js -f com.example.myapplication

还可以把JavaScript写成Python脚本:

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
import frida,sys
device=frida.get_usb_device()
session=device.attach("My Application")
front_app=device.get_frontmost_application()
print("正在运行的应用为:",front_app)
src="""
setImmediate(function(){
Java.perform(function(){
send("starting script");
var Activity=Java.use("com.example.myapplication.MainActivity");
Activity.getString.overload().implementation=function(){
var result=this.getString();
send("getString="+result);
var newResult="应用已被Hook!";
send(newResult);
return newResult;
};
});
});
"""
def on_message(message,data):
if message["type"]=="send":
print("[+]{}".format(message["payload"]))
else:
print("[-]{}".format(message))
script=session.create_script(src)
script.on("message",on_message)
script.load()
sys.stdin.read()

语法

常用命令:

1
2
3
4
5
6
7
8
9
10
11
frida-ps -U #查询USB连接设备上进程信息
frida-ps -Ua #显示USB连接设备上活跃的进程信息
frida-ps -Uai #显示USB连接是被上安装的应用信息
frida-ls-devices #列出附加设备
frida-discover -n <进程名> #发现程序内部函数
frida-discover -p <进程PID>
frida-trace -i "recv*" -i "send*" <进程名> #跟踪进程中recv*和send*的API调用
frida-trace -m "Objc" <进程名> #应用程序中跟踪Objc方法的调用
frida-trace -U -f <进程名> -I "call" #打开引用程序并跟踪call函数调用
frida-trace -U -i "Java_*" <进程名> #指定应用中跟踪所有JNI函数调用
frida-kill -D <设备ID> <进程ID>

实战

例如Hook foo.so中的strncmp函数:

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
import frida,sys
def on_message(message,data):
if message['type']=='send':
print("[*]{0}".format(message['payload']))
else:
print(message)
jscode="""
Java.perform(function(){
console.log("[*]Hooking calls to System.exit");
const exitClass=Java.use("java.lang.System");
exitClass.exit.implementation=function(){
console.log("[*]System.exit called");
}
var strncmp=undefined;
var imports=Module.enumerateImportsSync("libfoo.so");
for(var i=0;i<imports.length;i++){
if(imports[i].name=="strncmp"){
strncmp=imports[i].address;
break;
}
}
Interceptor.attach(strncmp,{
onEnter:function(args){
if(Memory.readUtf8String(args[0],23)=="01234567890123456789012"){
console.log("[*]Secret string at"+args[1]+":"+Memory.readUtf8String(args[1],23));
}
},
});
console.log("[*]Intercepting strncmp");
});
"""
process=frida.get_usb_device().attach("owasp.mstg.uncrackable2")
script=process.create_script(jscode)
script.on('message',on_message)
script.load()
sys.stdin.read()

通过以下命令获取当前显示页面的Activity所在类:

1
adb shell dumpsys activity | grep "mResume"

基础语法

框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function main(){
console.log("Script loaded successfully")
Java.perform(function(){
console.log("Inside java perform function")
//Frida主代码
var MainActivity=Java.use("com.roysue.demo02.MainActivity")
console.lo("Java.Use.Successfully!") //定位类成功
MainActivity.fun.implementation=function(x,y){
console.log("x=>",x,"y=>",y)
var ret_value=this.fun(x,y);
return ret_value;
}
})
}
setImmediate(main) //立即执行main

当fun函数有重载时,可以在第7行指定函数签名,如:

1
2
MainActivity.fun.overload('int','int').implementation
MainActivity.fun.overload('java.lang.String').implementation

Java类的函数分为类函数和实例方法。类函数用static修饰,和对应类绑定,若类函数被public修饰,则外部可直接通过类调用。实例方法没有static,需要创建对应类的实例再通过该实例调用,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//类函数
var MainActivity=Java.use('com.roysue.demo02.MainActivity')
MainActivity.staticSecret()

//实例方法
Java.choose('com.roysue.demo02.MainActivity',{
onMatch:function(instance){
console.log('instance found,total value=',instance.total.value)
instance.secret()
},
onComplete:function(){
console.log('search Complete')
}
})

例如不打算立即执行,可以将main函数改为其他函数名,分别写成函数后在Javascript代码最后进行导出:

1
2
3
4
rpc.exports={
callsecretfunc:CallSecretFunc, //将CallSecretFunc导出为callsecretfunc函数 导出函数名不能有大写字母或下划线
gettotalvalue:getTotalValue
};

然后用RPC模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import frida,sys
def on_message(message,data):
if message['type']=='send':
print("[*]{0}".format(message['payload']))
else:
print(message)
device=frida.get_usb_device()
process=device.attach('com.roysue.demo02')
with open('4.js') as f:
jscode=f.read()
script=process.create_script(jscode)
script.on('message',on_message)
script.load()
#...
script.exports.callsecretfunc()
script.exports.gettotalvalue()

RPC常用方法

获取设备:

1
2
3
import frida
device=frida.get_usb_device()
device=frida.get_device_manager().add_remote_device("192.168.50.96:6666")

注入进程:

1
2
3
4
5
6
7
8
import time,frida
#spawn模式
pid=device.spawn(["com.android.settings"])
device.resume(pid)
time.sleep(1) #等待进程被完全唤醒
session=device.attach(pid)
#attach模式
session=device.attach("com.android.settings")

注入脚本:

1
2
3
4
5
6
7
8
9
10
11
12
#直接注入
import frida
script=session.create_script("""
setImmediate(Java.perform(function(){
console.log("hello frida");
}))
""")
script.load() #将脚本加载进进程空间
#文件注入
with open("hook.js") as f:
script=session.create_script(f.read())
script.load()

JavaScript脚本向Python导出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import frida,time
device=frida.get_usb_device()
pid=device.spawn(["com.android.settings"])
device.resume(pid)
time.sleep(1)
session=device.attach(pid)
script=session.create_script("""
rpc.exports={
hello:function(){
return 'hello';
},
failPlease:function(){
return 'oops';
}
};
""")
script.load()
api=script.exports
print("api.hello()=>",api.hello())
print("api.fail_please()=>",api.fail_please()) #大写字母被替换为“_小写字母”

三种注入分离响应函数、进程崩溃响应函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import sys,frida,time
def on_detached():
print("on_detached")
def on_detached_with_reason(reason):
print("on_detached_with_reason:",reason)
def on_detached_with_varargs(*args):
print("on_detached_with_varargs:",args)
def on_process_crashed(crash):
print("on_process_crashed\t crash:",crash)
device=frida.get_usb_device()
pid=device.spawn(["com.android.settings"])
device.resume(pid)
time.sleep(1)
device.on('process-crashed',on_process_crashed)
session=device.attach(pid)
print("attached")
session.on("detached",on_detached)
session.on('detached',on_detached_with_reason)
session.on('detached',on_detached_with_varargs)
sys.stdin.read()

Objection初探

Objection有仨部分。第一部分为Objection重打包相关组件,将Frida运行所需frida-gadget.so重打包进App,完成Frida的无root调试。第二部分为Objection本身,和frida-gadget.so交互,运行并分析Frida的Hook。第三部分为Objection从TypeScript项目编译来的agent.js,后者在App运行中插入Frida运行库,支持Objection各功能。

打开“设置”,用Objection注入“设置”应用,并用explore命令进入REPL模式:

1
2
3
#先启动frida-server
adb shell dumpsys activity activity top
objection -g com.android.settings explore

REPL模式常用命令有:

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
help env #展示env命令帮助
jobs list #查看作业相关信息
frida #查看Frida相关信息
android hooking list classes #列出内存中所有类 实用但输出量大 去找历史记录C:\Users\Administrator\.objection
android hooking search classes XXX #内存中搜索包含XXX关键词的类
android hooking search methods XXX #内存中搜索包含XXX关键词的方法
android hooking list class_methods XXX.XXX.XXX.XXX #列出某类所有方法
android hooking list activities #列出当前注入的进程的所有activity
android hooking list services #列出进程所有service 还可改为receivers和providers
android hooking list receivers
android intent launch_activity <Activity类> #启动指定Activity组件
android intent launch_service <Service类>
android hooking watch class <类名>
android hooking watch class_method 方法名 --dump-args --dump-backtrace --dump-return #对指定方法Hook 方法名如java.io.File.$init
android hooking set return_value <类名> false #设置返回值 只支持bool型
android hooking generate simple <类名> #生成Frida的Hook代码
search instances search instances <类名> #搜索指定类的实例 获取实例ID
android heap search instances 类名 #搜索某类的实例
android heap execute 哈希码 方法名 #调用无参数实例方法
android heap evaluate 哈希码 #针对哈希码编写脚本 调用带参实例方法
android sslpinning disable
memory list modules #枚举当前进程模块
memory list exports <库名> #查看指定模块导出函数
memroy list exports <库名> --json result.json
memory search --string --offsets-only
android root disable #对抗root检测
android root simulate #模拟root环境
android ui screenshot <文件名.png>
android shell_exec <命令>

上述命令android hooking list classes比较实用但输出量大,运行后去找历史记录C:\Users\Administrator.objection\objection.log。Visual Studio Code上用Alt+Shift+鼠标进行块状选中,可编写-c选项REPL批处理脚本。

Hook实战例如:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
com.android.settings on (samsung: 9) [usb] # android hooking watch class_method java.io.File.$init --dump-args --dump-b
acktrace --dump-return
(agent) Attempting to watch class java.io.File and method $init.
(agent) Hooking java.io.File.$init(java.io.File, java.lang.String)
(agent) Hooking java.io.File.$init(java.lang.String)
(agent) Hooking java.io.File.$init(java.lang.String, int)
(agent) Hooking java.io.File.$init(java.lang.String, java.io.File)
(agent) Hooking java.io.File.$init(java.lang.String, java.lang.String)
(agent) Hooking java.io.File.$init(java.net.URI)
(agent) Registering job 456383. Type: watch-method for: java.io.File.$init
com.android.settings on (samsung: 9) [usb] # jobs list
Job ID Hooks Type
------ ----- ------------------------------------
456383 6 watch-method for: java.io.File.$init
com.android.settings on (samsung: 9) [usb] #
// 这里随便点进“设置”一个选项
(agent) [456383] Called java.io.File.File(java.lang.String)
(agent) [456383] Backtrace:
java.io.File.<init>(Native Method)
android.content.res.XResources.isFirstLoad(XResources.java:120)
de.robv.android.xposed.XposedInit.cloneToXResources(XposedInit.java:282)
de.robv.android.xposed.XposedInit.access$000(XposedInit.java:60)
de.robv.android.xposed.XposedInit$2.afterHookedMethod(XposedInit.java:145)
de.robv.android.xposed.XC_MethodHook.callAfterHookedMethod(XC_MethodHook.java:68)
EdHooker_.hook(Unknown Source:175)
android.app.ResourcesManager.createBaseActivityResources(ResourcesManager.java:733)
android.app.ContextImpl.createActivityContext(ContextImpl.java:2384)
android.app.ActivityThread.createBaseContextForActivity(ActivityThread.java:3043)
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2872)
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3093)
android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1823)
android.os.Handler.dispatchMessage(Handler.java:106)
android.os.Looper.loop(Looper.java:193)
android.app.ActivityThread.main(ActivityThread.java:6840)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:860)

(agent) [456383] Arguments java.io.File.File(/system/priv-app/Settings/Settings.apk)
(agent) [456383] Return Value: (none)
com.android.settings on (samsung: 9) [usb] # jobs kill 456383
com.android.settings on (samsung: 9) [usb] # jobs list
Job ID Hooks Type
------ ----- ----
com.android.settings on (samsung: 9) [usb] # android hooking watch class java.io.File
(agent) Hooking java.io.File.createTempFile(java.lang.String, java.lang.String)
(agent) Hooking java.io.File.createTempFile(java.lang.String, java.lang.String, java.io.File)
(agent) Hooking java.io.File.listRoots()
(agent) Hooking java.io.File.readObject(java.io.ObjectInputStream)
(agent) Hooking java.io.File.slashify(java.lang.String, boolean)
(agent) Hooking java.io.File.writeObject(java.io.ObjectOutputStream)
(agent) Hooking java.io.File.canExecute()
(agent) Hooking java.io.File.canRead()
(agent) Hooking java.io.File.canWrite()
(agent) Hooking java.io.File.compareTo(java.io.File)
(agent) Hooking java.io.File.compareTo(java.lang.Object)
(agent) Hooking java.io.File.createNewFile()
(agent) Hooking java.io.File.delete()
(agent) Hooking java.io.File.deleteOnExit()
(agent) Hooking java.io.File.equals(java.lang.Object)
(agent) Hooking java.io.File.exists()
(agent) Hooking java.io.File.getAbsoluteFile()
(agent) Hooking java.io.File.getAbsolutePath()
(agent) Hooking java.io.File.getCanonicalFile()
(agent) Hooking java.io.File.getCanonicalPath()
(agent) Hooking java.io.File.getFreeSpace()
(agent) Hooking java.io.File.getName()
(agent) Hooking java.io.File.getParent()
(agent) Hooking java.io.File.getParentFile()
(agent) Hooking java.io.File.getPath()
(agent) Hooking java.io.File.getPrefixLength()
(agent) Hooking java.io.File.getTotalSpace()
(agent) Hooking java.io.File.getUsableSpace()
(agent) Hooking java.io.File.hashCode()
(agent) Hooking java.io.File.isAbsolute()
(agent) Hooking java.io.File.isDirectory()
(agent) Hooking java.io.File.isFile()
(agent) Hooking java.io.File.isHidden()
(agent) Hooking java.io.File.isInvalid()
(agent) Hooking java.io.File.lastModified()
(agent) Hooking java.io.File.length()
(agent) Hooking java.io.File.list()
(agent) Hooking java.io.File.list(java.io.FilenameFilter)
(agent) Hooking java.io.File.listFiles()
(agent) Hooking java.io.File.listFiles(java.io.FileFilter)
(agent) Hooking java.io.File.listFiles(java.io.FilenameFilter)
(agent) Hooking java.io.File.mkdir()
(agent) Hooking java.io.File.mkdirs()
(agent) Hooking java.io.File.renameTo(java.io.File)
(agent) Hooking java.io.File.setExecutable(boolean)
(agent) Hooking java.io.File.setExecutable(boolean, boolean)
(agent) Hooking java.io.File.setLastModified(long)
(agent) Hooking java.io.File.setReadOnly()
(agent) Hooking java.io.File.setReadable(boolean)
(agent) Hooking java.io.File.setReadable(boolean, boolean)
(agent) Hooking java.io.File.setWritable(boolean)
(agent) Hooking java.io.File.setWritable(boolean, boolean)
(agent) Hooking java.io.File.toPath()
(agent) Hooking java.io.File.toString()
(agent) Hooking java.io.File.toURI()
(agent) Hooking java.io.File.toURL()
(agent) Registering job 388472. Type: watch-class for: java.io.File
com.android.settings on (samsung: 9) [usb] # jobs list
Job ID Hooks Type
------ ----- -----------------------------
388472 56 watch-class for: java.io.File
// 这里随便点进“设置”一个选项
(agent) [388472] Called java.io.File.exists()
(agent) [388472] Called java.io.File.getPath()
(agent) [388472] Called java.io.File.equals(java.lang.Object)
(agent) [388472] Called java.io.File.compareTo(java.io.File)
(agent) [388472] Called java.io.File.getPath()
(agent) [388472] Called java.io.File.getPath()
(agent) [388472] Called java.io.File.equals(java.lang.Object)
(agent) [388472] Called java.io.File.compareTo(java.io.File)
(agent) [388472] Called java.io.File.getPath()
(agent) [388472] Called java.io.File.getPath()
(agent) [388472] Called java.io.File.lastModified()

主动调用实战例如:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
com.android.settings on (samsung: 9) [usb] # android heap search instances java.io.File
Class instance enumeration complete for java.io.File
Hashcode Class toString()
----------- ------------ --------------------------------------------------------------
133688275 java.io.File /system/priv-app/Settings/Settings.apk
47718804 java.io.File /proc
137635794 java.io.File /sys/fs/bpf/traffic_uid_stats_map
-1320040561 java.io.File /proc/net/xt_qtaguid/iface_stat_all
-1320030053 java.io.File /proc/net/xt_qtaguid/iface_stat_fmt
-1877617375 java.io.File /proc/net/xt_qtaguid/stats
2048048455 java.io.File /sys/board_properties/soc/msv
-54300376 java.io.File /data/misc/user/0
-48471265 java.io.File /data/misc/user/0/cacerts-added
2041518079 java.io.File /data/misc/user/0/cacerts-removed
1664757827 java.io.File /system/lib64
-895673764 java.io.File /system/vendor/lib64
114250896 java.io.File /data/user_de/0/com.android.settings/code_cache
958541197 java.io.File /data/user/0/com.android.settings
-1227050545 java.io.File /data/user_de/0/com.android.settings
-1227050545 java.io.File /data/user_de/0/com.android.settings
1664757827 java.io.File /system/lib64
-895673764 java.io.File /system/vendor/lib64
1645413532 java.io.File /data/user_de/0/com.android.settings/files/condition_state.xml
1234366 java.io.File /
-1782362368 java.io.File /data/user_de/0/com.android.settings/cache
1664757827 java.io.File /system/lib64
-895673764 java.io.File /system/vendor/lib64
-895673764 java.io.File /system/vendor/lib64
1664757827 java.io.File /system/lib64
2097462291 java.io.File /system/priv-app/Settings/lib/x86_64
1664757827 java.io.File /system/lib64
-895673764 java.io.File /system/vendor/lib64
-1714625846 java.io.File /system/framework/eddexmaker.jar
1190786266 java.io.File /system/framework/eddalvikdx.jar
-594014569 java.io.File /system/framework/edxp.jar
1664757827 java.io.File /system/lib64
-895673764 java.io.File /system/vendor/lib64
1664757827 java.io.File /system/lib64
-895673764 java.io.File /system/vendor/lib64
1664757827 java.io.File /system/lib64
-895673764 java.io.File /system/vendor/lib64
1664757827 java.io.File /system/lib64
-895673764 java.io.File /system/vendor/lib64
1664757827 java.io.File /system/lib64
-895673764 java.io.File /system/vendor/lib64
-1782362368 java.io.File /data/user_de/0/com.android.settings/cache
-1779601385 java.io.File /data/user_de/0/com.android.settings/files
-1782362368 java.io.File /data/user_de/0/com.android.settings/cache
1433727619 java.io.File /system/framework/org.apache.http.legacy.boot.jar
133688275 java.io.File /system/priv-app/Settings/Settings.apk
-1975059403 java.io.File /data/misc/keychain/cacerts-added
-882893419 java.io.File /data/misc/keychain/cacerts-removed
1173056351 java.io.File /system/etc/security/cacerts
-853728249 java.io.File /system/etc/security/ct_known_logs
-2013037938 java.io.File /data/misc/keychain/trusted_ct_logs/current
47683016 java.io.File /data
-1978853580 java.io.File /mnt/expand
-2123135345 java.io.File /system
-1575638275 java.io.File /storage
1671863261 java.io.File /data/cache
384824 java.io.File /odm
384857 java.io.File /oem
-533391 java.io.File /product
-2056170586 java.io.File /vendor
-1119854388 java.io.File /cache/recovery/block.map
-702818824 java.io.File /system/etc/security/otacerts.zip
-1577688767 java.io.File /cache/recovery/last_install
1902719031 java.io.File /cache/recovery/log
405131488 java.io.File /cache/recovery
301269365 java.io.File /cache/recovery/uncrypt_file
1310302411 java.io.File /cache/recovery/uncrypt_status
1664757827 java.io.File /system/lib64
-895673764 java.io.File /system/vendor/lib64
-895673764 java.io.File /system/vendor/lib64
1664757827 java.io.File /system/lib64
1234367 java.io.File .
com.android.settings on (samsung: 9) [usb] # android heap execute 133688275 getPath
Found 2 handles, this is probably a bug, please report it!
Handle 133688275 is to class
java.io.File
Executing method: getPath()
/system/priv-app/Settings/Settings.apk
com.android.settings on (samsung: 9) [usb] # android heap evaluate 133688275
(The hashcode at `133688275` will be available as the `clazz` variable.)
console.log('File is canWrite? =>',clazz.canWrite()) //clazz为该File类
clazz.setWritable(false)
console.log('File is canWrite? =>',clazz.canWrite())
JavaScript capture complete. Evaluating...
Found 2 handles, this is probably a bug, please report it!
Handle 133688275 is to class
java.io.File
File is canWrite? => false
File is canWrite? => false

SSL Pinning称为证书绑定。该方式不仅校验服务器证书是否是系统中可信凭证,通信过程中甚至连系统内置证书都不信任,而只信任指定证书。一旦发现服务器证书为非指定证书即停止通信,导致抓包软件安装到系统信任凭据中也无法生效。

Objection有自带SSL Pinning Bypass功能:

1
android sslpinning disable

此外还有ZenTrace工具:https://github.com/hluwa/ZenTracer 。使用Match RegEx可添加类的过滤,例如“M:com.cz.babySister”表示Hook,“B:com.cz.babySister”表示不过滤。Start后,ZenTracer对前台证显示的应用所有符合匹配规则的目标类进行Hook,上方显示参数和返回值信息。还可导出为JSON。

逆向工作思路

找到当前正在运行的Activity:

1
adb shell dumpsys activity top

objection注入:

1
objection -g com.example.junior explore

找出所有Activity:

1
android hooking list activities

找到目标Activity直接启动:

1
android intent launch_activity com.example.junior.CalculatorActivity

列出该类中方法:

1
android hooking list class_methods com.example.junior.CalculatorActivity

对目标方法Hook:

1
android hooking watch class_method com.example.junior.CalculatorActivity.caculate --dump-args --dump-backtrace --dump-return

用以下Frida脚本和Objection输出内容相同,但Frida和Objection不能同时Hook同一个方法,得先在Objection里删除该Job。

1
2
3
4
5
6
7
8
9
10
11
12
13
function main(){
Java.perform(function(){
var Arith=Java.use('com.example.junior.util.Arith')
Arith.sub.overload('java.lang.String','java.lang.String').implementation=function(str1,str2){
var JavaString=Java.use('java.lang.String')
var result=this.sub(str,JavaString.$new('123')) //看好目标函数签名
console.log('str,str2,result=>',str,str2,result)
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
return result
}
})
}
setImmediate(main)

主动调用方法:

1
2
3
4
5
6
7
8
9
function CallSub(a,b){
var Arith=Java.use('com.example.junior.util.Arith')
var JavaString=Java.use('java.lang.String')
var result=Arith.sub(JavaString.$new(a),JavaString.$new(b))
console.log(a,"-",b,"=",result)
}
rpc.exports={
sub:CallSub
};

编写利用脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import frida,sys
def on_message(message,data):
if message['type']=='send':
print("[*]{0}".format(message['payload']))
else:
print(message)
device=frida.get_usb_device()
process=device.attach('com.example.junior')
with open('call.js') as f:
jscode=f.read()
script=process.create_script(jscode)
script.on('message',on_message)
script.load()
for i in range(20,30):
for j in range(0,10):
script.exports.sub(str(i),str(j))

想做到规模化就应使用Frida的网络模式,服务端运行为:

1
2
3
./frida-server -l 0.0.0.0:8888 #监听手机8888端口
netstat --pantul | grep frida
ifconfig #例如本机为192.168.xxx.xxx

进行远程注入:

1
objection -N -h 192.168.xxx.xxx -p 8888 -g com.xxx.xxx explore

利用脚本中改为:

1
device=frida.get_device_manager().add_remote_device('192.168.xxx.xxx:8888')

常用插件

Wallbreaker是Objection的一个插件,下载后在Objection的REPL模式下:

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
com.android.settings on (samsung: 9) [usb] # plugin load D:\Wallbreaker
Loaded plugin: wallbreaker
com.android.settings on (samsung: 9) [usb] # plugin wallbreaker objectsearch java.io.File #跟objection heap search功能一样
[0x2e46]: /proc
...
com.android.settings on (samsung: 9) [usb] # plugin wallbreaker objectdump 0x2e46 #将某实例具体内容打印出来 后面为实例码

package java.io

class File {

/* static fields */
static boolean $assertionsDisabled; => false
...

/* instance fields 重点在这里 观察该实例各成员的值*/
Path filePath; => null
...

/* constructor methods */
java.io.File(File, String);
...

/* static methods */
static File createTempFile(String, String);
...

/* instance methods */
void readObject(ObjectInputStream);
...
}

定位弹窗使用的类常见有android.app.AlertDialog、android.app.Dialog、android.widget.PopupWindow等,优先用这几个尝试objectsearch。

设置完Hook后,如果需要在App一运行后就挂载某Hook,可以实现:

1
objection -g com.xxx.xxx explore -s "android hooking watch class xxx.xxx.xxx"

此外,-c命令为App一运行就要跑的REPL命令脚本,如:

1
objection -g com.xxx.xxx explore -c "*.txt"

FRIDA-DEXDump为Objection的一个脱壳插件,暴力搜索内存中符合dex格式的数据完成dump。在CLI模式下可以:

1
2
frida-dexdump -FU #dump前台应用
frida-dexdump -U -f com.xxx.xxx #spawn一个App来脱

作为Objection插件使用:

1
2
plugin load D:\DEXDump
plugin dexdump dump

脱壳后会提示dump出来的.dex文件存储路径,按照文件从小到大分别改名为classes.dex、classes2.dex、classes3.dex、classes4.dex等,放到并替换原classes.dex目录下。在Jadx搜索找到extends Application的代码,反编译其目录下AndroidManifest.xml,把Application标签内android:name改为完整类名后,即可用apktool重新打包。

项目https://github.com/WooyunDota/DroidSSLUnpinning/blob/master/ObjectionUnpinningPlus/hooks.js 补充了一些Objection没有的SSL Pinning Bypass方式。

抓包入门

网络通信相关框架有HttpURLConnection、okhttp、okhttp3等,其中okhttp和okhttp3完全不同。HttpURLConnection的原生库底层使用okhttp。

HttpURLConnection

一个基本的HttpURLConnection如下:

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
import android.os.Bundle;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(new Runnable() {
@Override
public void run() {
while (true){
try {
URL url = new URL("https://www.baidu.com");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET"); //请求方法
connection.setRequestProperty("token","r0ysue666"); //请求参数
connection.setConnectTimeout(8000); //连接超时时间
connection.setReadTimeout(8000); //接收超时时间
connection.connect(); // 开始连接
InputStream in = connection.getInputStream(); //获取服务器返回的输入流
//if(in.available() > 0){
// 每次写入1024字节
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
StringBuffer sb = new StringBuffer();
while ((in.read(buffer)) != -1) {
sb.append(new String(buffer));
}
Log.d("r0ysue666", sb.toString());
connection.disconnect(); //关闭连接
// }
} catch (IOException e) {
e.printStackTrace();
}
try {
Thread.sleep(10*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}

并在AndroidManifest.xml中加入网络权限:

1
<uses-permission android:name="android.permission.INTERNET"/>

第一步先Hook整个URL类的构造函数:

1
android hooking watch class_method java.net.URL.$init --dump-args --dump-backtrace --dump-return

自吐脚本为:

1
2
3
4
5
6
7
8
9
10
11
function main(){
Java.perform(function(){
var URL=Java.use('java.net.URL')
URL.$init.overload('java.lang.String').implementation=function(urlset){
console.log('url=>',urlstr)
var result=this.$init(urlstr)
return result
}
})
}
setImmediate(main)

下一步还可以Hook整个HttpURLConnection类所有函数:

1
2
android hooking watch class java.net.HttpURLConnection
android hooking watch class_method java.net.HttpURLConnection.$init --dump-args --dump--backtrace --dump-return

用WallBreaker搜索HttpURLConnection类实例,发现该类其实是个抽象类,但运行时是抽象类的具体实现类在工作。此时关注上述源码中构造该类的openConnection,对后者进行Hook:

1
2
3
4
5
6
7
8
9
10
11
function main(){
Java.perform(function(){
var URL=Java.use('java.net.URL')
URL.openConnection.overload().implementation=function(){
var result=this.openConnection()
console.log('openConnection() returnType=>',result.$className)
return result
}
})
}
setImmediate(main)

返回值找到具体实现类为com.android.okhttp.internal.huc.HttpURLConnectionImpl类,尝试:

1
android hooking watch class com.android.okhttp.internal.huc.HttpURLConnectionImpl

Hook发现程序中每个函数都被调用到了,最终请求参数自吐脚本如下:

1
2
3
4
5
6
7
8
9
10
11
function main(){
Java.perform(function(){
var HttpURLConnectionImpl=Java.use('com.android.okhttp.internal.huc.HttpURLConnectionImpl')
HttpURLConnectionImpl.setRequestProperty.implementation=function(key,value){
var result=this.setRequestProperty(key,value)
console.log('setRequestProperty=>',key,':',value)
return result
}
})
}
setImmediate(main)

okhttp3

在app/build.gradle中添加okhttp3的引用并Sync:

1
2
3
4
5
dependencies {
...
// 增加对Okhttp3的依赖
implementation("com.squareup.okhttp3:okhttp:3.12.0")
}

其中主界面如下:

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
package com.r0ysue.okhttp3demo;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
private static String TAG = "r0ysue666";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 定位发送请求按钮
Button btn = findViewById(R.id.mybtn);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 访问百度首页
String requestUrl = "https://www.baidu.com/";
example myexample = new example(); //example类自己实现
try {
myexample.run(requestUrl);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}

对于example类实现:

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
package com.r0ysue.okhttp3demo;
import android.util.Log;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class example {
// TAG即为日志打印时的标签
private static final String TAG = "r0ysue666";
// 新建一个Okhttp客户端
// OkHttpClient client = new OkHttpClient();
// 新建一个拦截器
OkHttpClient client = new OkHttpClient.Builder()
// .addNetworkInterceptor(new LoggingInterceptor()) 添加拦截器 下面会讲
// .readTimeout(5, TimeUnit.SECONDS) 读超时
// .writeTimeout(5, TimeUnit.SECONDS) 写超时
// .connectTimeout(15, TimeUnit.SECONDS) 连接超时
// .retryOnConnectionFailure(true) 是否自动重连
.build();
void run(String url) throws IOException {
// 构造request
Request request = new Request.Builder()
.url(url)
.header("token","r0ysue") //参数
.build();
// 发起异步请求
client.newCall(request).enqueue( //发起网络请求
new Callback() {
@Override
public void onFailure(Call call, IOException e) {
call.cancel();
}
@Override
public void onResponse(Call call, Response response) throws IOException { //处理每次请求结果
//打印输出
Log.d(TAG, response.body().string());
}
}
);
}
}

如果单纯Hook上述newCall,可能存在Call后没有发出实际请求的情况,该函数实际调用了RealCall.newRealCall。RealCall对象是okhttp3.Call接口的唯一实现,表示一个等待执行的请求且只能被执行以此,到这一步请求仍可被取消。只有Hook了executeenqueue才能真正保证每个从okhttp出去的请求都能被Hook到。

1
2
3
4
5
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket){
RealCall call=new RealCall(client,originalRequest,forWebSocket);
call.eventListener=client.eventListenerFactory().create(call);
return call;
}

okhttp3通过拦截器Interceptor完成监控管理、重写和重试请求。每个网络请求和接收都必须经过okhttp3本身存在的五大拦截器。拦截器可对request修改,数据返回时对response做出修改。拦截器机制实际上是一个链条,最上层拦截器首先向下传递一个request,请求下层拦截器返回一个response。传递到最后一个拦截器,它对该request进行处理并返回resposne。

1
2
3
4
5
6
7
8
9
10
11
12
13
Response getResponseWithInterceptorChain() throws IOException {
List<Interceptor> interceptors = new ArrayList<>(); //空拦截器容器
interceptors.addAll(client.interceptors()); //用户自定义应用拦截器
interceptors.add(retryAndFollowUpInterceptor); //重定向拦截器 用于取消、失败重试、重定向
interceptors.add(new BridgeInterceptor(client.cookieJar())); //桥接拦截器 把用户请求转为HTTP请求
interceptors.add(new CacheInterceptor(client.internalCache())); //缓存拦截器 用于读写缓存、根据策略决定是否使用
interceptors.add(new ConnectInterceptor(client)); //连接拦截器 建立连接
if (!forWebSocket) //非WebSocket请求 则添加用户自定义网络拦截器
interceptors.addAll(client.networkInterceptors());
interceptors.add(new CallServerInterceptor(forWebSocket)); //发起请求拦截器
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0, originalRequest, this, eventListener, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}

这里演示新建一个拦截器类,打印URL和请求headers:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package com.r0ysue.okhttp3demo;
import android.util.Log;
import java.io.EOFException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;
import okhttp3.Connection;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.internal.http.HttpHeaders;
import okio.Buffer;
import okio.BufferedSource;
import okio.GzipSource;
public class LoggingInterceptor implements Interceptor {
private static final String TAG = "okhttpGET";
private static final Charset UTF8 = Charset.forName("UTF-8");
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RequestBody requestBody = request.body();
boolean hasRequestBody = requestBody != null;
Connection connection = chain.connection();
String requestStartMessage = "--> " + request.method() + ' ' + request.url();
Log.e(TAG, requestStartMessage);
if (hasRequestBody) {
if (requestBody.contentType() != null)
Log.e(TAG, "Content-Type: " + requestBody.contentType());
if (requestBody.contentLength() != -1)
Log.e(TAG, "Content-Length: " + requestBody.contentLength());
}
Headers headers = request.headers();
for (int i = 0, count = headers.size(); i < count; i++) {
String name = headers.name(i);
// Skip headers from the request body as they are explicitly logged above.
if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name))
Log.e(TAG, name + ": " + headers.value(i));
}
if (!hasRequestBody)
Log.e(TAG, "--> END " + request.method());
else if (bodyHasUnknownEncoding(request.headers()))
Log.e(TAG, "--> END " + request.method() + " (encoded body omitted)");
else {
Buffer buffer = new Buffer();
requestBody.writeTo(buffer);
Charset charset = UTF8;
MediaType contentType = requestBody.contentType();
if (contentType != null)
charset = contentType.charset(UTF8);
Log.e(TAG, "");
if (isPlaintext(buffer)) {
Log.e(TAG, buffer.readString(charset));
Log.e(TAG, "--> END " + request.method() + " (" + requestBody.contentLength() + "-byte body)");
} else
Log.e(TAG, "--> END " + request.method() + " (binary " + requestBody.contentLength() + "-byte body omitted)");
}
long startNs = System.nanoTime();
Response response;
try {
response = chain.proceed(request);
} catch (Exception e) {
Log.e(TAG, "<-- HTTP FAILED: " + e);
throw e;
}
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
ResponseBody responseBody = response.body();
long contentLength = responseBody.contentLength();
String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length";
Log.e(TAG, "<-- " + response.code() + (response.message().isEmpty() ? "" : ' ' + response.message()) + ' ' + response.request().url() + " (" + tookMs + "ms" + (", " + bodySize + " body:" + "") + ')');
Headers myheaders = response.headers();
for (int i = 0, count = myheaders.size(); i < count; i++)
Log.e(TAG, myheaders.name(i) + ": " + myheaders.value(i));
if (!HttpHeaders.hasBody(response))
Log.e(TAG, "<-- END HTTP");
else if (bodyHasUnknownEncoding(response.headers()))
Log.e(TAG, "<-- END HTTP (encoded body omitted)");
else {
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE); // Buffer the entire body.
Buffer buffer = source.buffer();
Long gzippedLength = null;
if ("gzip".equalsIgnoreCase(myheaders.get("Content-Encoding"))) {
gzippedLength = buffer.size();
GzipSource gzippedResponseBody = null;
try {
gzippedResponseBody = new GzipSource(buffer.clone());
buffer = new Buffer();
buffer.writeAll(gzippedResponseBody);
} finally {
if (gzippedResponseBody != null)
gzippedResponseBody.close();
}
}
Charset charset = UTF8;
MediaType contentType = responseBody.contentType();
if (contentType != null)
charset = contentType.charset(UTF8);
if (!isPlaintext(buffer)) {
Log.e(TAG, "");
Log.e(TAG, "<-- END HTTP (binary " + buffer.size() + "-byte body omitted)");
return response;
}
if (contentLength != 0) {
Log.e(TAG, "");
Log.e(TAG, buffer.clone().readString(charset));
}
if (gzippedLength != null)
Log.e(TAG, "<-- END HTTP (" + buffer.size() + "-byte, " + gzippedLength + "-gzipped-byte body)");
else
Log.e(TAG, "<-- END HTTP (" + buffer.size() + "-byte body)");
}
return response;
}
static boolean isPlaintext(Buffer buffer) {
try {
Buffer prefix = new Buffer();
long byteCount = buffer.size() < 64 ? buffer.size() : 64;
buffer.copyTo(prefix, 0, byteCount);
for (int i = 0; i < 16; i++) {
if (prefix.exhausted())
break;
int codePoint = prefix.readUtf8CodePoint();
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint))
return false;
}
return true;
} catch (EOFException e) {
return false; // Truncated UTF-8 sequence.
}
}
private boolean bodyHasUnknownEncoding(Headers myheaders) {
String contentEncoding = myheaders.get("Content-Encoding");
return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity") && !contentEncoding.equalsIgnoreCase("gzip");
}
}

将上述工程编译为.apk文件,解压出classes.dex,改名为okhttp3loggin.dex放到/data/local/tmp下,用Firda脚本将该拦截器添加到原有的拦截器链条中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hook_okhttp3() {
// 1. frida Hook java层的代码必须包裹在Java.perform中,Java.perform会将Hook Java相关API准备就绪。
Java.perform(function () {
Java.openClassFile("/data/local/tmp/okhttp3logging.dex").load();
var MyInterceptor = Java.use("com.r0ysue.okhttp3demo.LoggingInterceptor");
var MyInterceptorObj = MyInterceptor.$new();
var Builder = Java.use("okhttp3.OkHttpClient$Builder");
console.log(Builder);
Builder.build.implementation = function () {
this.networkInterceptors().add(MyInterceptorObj);
return this.build();
};
console.log("hook_okhttp3...");
});
}
function main() {
hook_okhttp3();
}
setImmediate(main)

若用attach模式,Hook时机偏后,会带来大量干扰信息, 甚至导致App崩溃。这里用spawn模式,命令为:

1
frida -U -f com.xxx.xxx -l hookInterceptor.js --no-pause

okhttp3的证书绑定方法为:

1
client=new OkHttpClient.Builder().certificatePinner(new CertificatePinner.Builder().add("test.com","sha512/14cf3JCaO4V...").build()).build();

Socket

无论有多少三方框架,三方框架是否被混淆,都不可避免地经过系统Socket相关类。Socket由系统完成,相关类一定不会被混淆。先搜索Socket全部类:

1
android hooking search classes socket

找历史记录,每行前面加上android hooking watch class,重新Spawn:

1
objection -g com.xxx.xxx explore -c xxx.txt

根据Android源码/xref/libcore/ojluni/src/main/java/java/net/SocketOutputStream.java,发现java.net.AbstractPlainSocketImpl.acquireFDsocketWrite调用,第一个参数为网络传输的数据内容。获取request的脚本为:

1
2
3
4
5
6
7
Java.use('java.net.SocketOutputStream').socketWrite.overload('[B','int','int').implementation=function(b,off,len){
var result=this.socketWrite(b,off,len);
console.log('socketWrite result,b,off,len=>',result,b,off,len);
var ByteString=Java.use("com.android.okhttp.okio.ByteString");
console.log('contents:=>',ByteString.of(b).hex());
return result;
};

先写一个函数,将Java层的byte数组打印出相应字节的dexdump:

1
2
3
4
5
6
function jhexdump(array){
var ptr=Memory.alloc(array.length);
for(var i=0;i<array.length;i++)
Memory.writeS8(ptr.add(i),array[i]);
console.log(hexdump(ptr,{offset:0,length:array.length,header:false,ansi:false}));
}

对于response,找java.net.SocketInputStream.read([B,int,int)函数,得到的是gzip压缩后的数据,解压即可:

1
2
3
4
5
6
7
Java.use('java.new.SocketInputStream').read.overload('[B','int','int').implementation=function(bytearray1,int1,int2){
var result=this.read(bytearray1,int1,int2);
console.log('read result,bytearray1,int1,int2=>',result,bytearray1,int1,int2);
var ByteString=Java.use("com.android.okhttp.okio.ByteString");
jhexdump(bytearray1);
return result;
};

上面是HTTP的抓包方法,现在为HTTPS抓包方法。此时关键函数为com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write([B,int,int)com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read([B,int,int)。它们第一个参数为明文数据。

无论是HTTP还是HTTPS,都可Hook函数java.net.InetSocketAddress.InetSocketAddress(java.net.InetAddress,int)。第一个参数转String为IP地址,第二个参数为端口号。

用下面命令翻一下类的结构:

1
2
plugin load D:/plugins/Wallbreaker
plugin wallbreaker classdump java.net.InetAddress

其中isSiteLocalAddress区分是本地地址还是远程地址,最终如下:

1
2
3
4
5
6
7
8
9
10
11
12
function hookAddress(){
Java.perform(function(){
Java.use('java.net.InetSocketAddress').$init.overload('java.net.InetAddress','int').implementation=function(addr,port){
var result=this.$init(addr,port);
if(addr.isSiteLocalAddress())
console.log('Local address=>',addr.toString(),',port is ',port);
else
console.log('Server address=>',addr.toString(),',port is ',port);
}
return result;
})
}

WebSocket

使用WebSocket时,服务器可主动向客户端推送信息,客户端也可主动向服务器发送信息,即全双工通信。WebSocket建立连接后能保持持久化连接,不需要一个request对应一个response。

XMPP

XMPP用于即时通信,基于XML。一个XML消息实体如下,message标签即为XML Stanza。Android中常用的基于XMPP协议的开发框架为Smack。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xmlversion='1.0'?>
<stream:stream
to='Receiver'
xmlns='jabber:client'
xmlns:stream='http_etherx_jabber_org/stream'
version='1.0'>
<message
from='Sender'
to='Receiver'
xml:lang='zh-cn'>
<body>xxx</body>
</message>
</stream:stream>

Protobuf

Protobuf时一种不依赖语言和平台类型、可扩展的用于序列化结构数据的机制,支持多种主流计算机语言,目前主要用proto2和proto3两大版本。Protobuf在直播、弹幕等实时性数据传输业务需求下,能够减少数据在传输过程中占用空间的大小,是JSON性能的100多倍。Protobuf用.proto文件定义数据格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
syntax="proto3" //版本
option java_multiple_files=true;
option java_package="com.xxx.xxx"; //生成的类放在什么Java包下
option java_outer_classname="HelloWorldProto";
option objc_class_prefix="HLW";
package helloworld; //声明包名
message HelloRequest{
string name=1; //1为唯一标识符
int32 sex=2;
}
message HelloReply{
string message=1;
}

Protobuf通常与gRPC框架配合使用,后者是个高性能、开源和通用的RPC框架,主要面向移动设计,提供多个语言版本,支持Android和Web。gRPC基于HTTP/2标准设计,具有双向流、流控、头部压缩、单TCP连接上的多复用请求等特性,移动设备上更省电、空间占用更少。

图片抓包

Android通常用BitmapFactory类中函数加载Bitmap对象,通过ImageView控件加载Bitmap对象类型的图片。BitmapFactory类提供的静态方法有decodeFiledecodeResourcedecodeStreamdecodeByteArray,分别从文件系统、资源、输入流和字节数组中加载Bitmap对象。图片读写非常耗时,下面脚本新建线程进行图片文件写入。

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
function guid(){
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,function(c){
var r=Math.random()*16|0,v=c=='x'?r:(r&0x3|0x8);
return v.toString(16);
});
}
function saveBitmap_3(){
Java.perform(function(){
var Runnable = Java.use("java.lang.Runnable");
var saveImg = Java.registerClass({ //新建一个Java类
name: "com.roysue.runnable", //新建类的类名
implements: [Runnable], //父类对象或接口 这里是java.lang.Runnable
fields: { //类中成员
bm: "android.graphics.Bitmap",
},
methods: { //类中函数
$init: [{
returnType: "void",
argumentTypes: ["android.graphics.Bitmap"],
implementation: function (bitmap) {
this.bm.value = bitmap;
}
}],
run: function () {
var path = "/sdcard/Download/tmp/" + guid() + ".jpg"
console.log("path=> ", path)
var file = Java.use("java.io.File").$new(path)
var fos = Java.use("java.io.FileOutputStream").$new(file);
this.bm.value.compress(Java.use("android.graphics.Bitmap$CompressFormat").JPEG.value, 100, fos)
console.log("success!")
fos.flush();
fos.close();
}
}
});
Java.use('android.graphics.BitmapFactory').decodeByteArray.overload('[B', 'int', 'int', 'android.graphics.BitmapFactory$Options').implementation = function(data,offset,length,opts){
var result = this.decodeByteArray(data,offset,length,opts)
var ByteString = Java.use("com.android.okhttp.okio.ByteString");
console.log("data is coming!")
var runable = saveImg.$new(result)
runable.run()
return result
}
})
}

下面这个脚本针对任意控件点触即输出所在类的脚本:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
var jclazz = null;
var jobj = null;
function getObjClassName(obj) {
if (!jclazz)
var jclazz = Java.use("java.lang.Class");
if (!jobj)
var jobj = Java.use("java.lang.Object");
return jclazz.getName.call(jobj.getClass.call(obj));
}
function watch(obj, mtdName) {
var listener_name = getObjClassName(obj);
var target = Java.use(listener_name);
if (!target || !mtdName in target)
return;
// send("[WatchEvent] hooking " + mtdName + ": " + listener_name);
target[mtdName].overloads.forEach(function (overload) {
overload.implementation = function () {
//send("[WatchEvent] " + mtdName + ": " + getObjClassName(this));
console.log("[WatchEvent] " + mtdName + ": " + getObjClassName(this))
return this[mtdName].apply(this, arguments);
};
})
}
function OnClickListener() {
Java.perform(function () {
//以spawn启动进程的模式来attach的话
Java.use("android.view.View").setOnClickListener.implementation = function (listener) {
if (listener != null)
watch(listener, 'onClick');
return this.setOnClickListener(listener);
};
//如果frida以attach的模式进行attch的话
Java.choose("android.view.View$ListenerInfo", {
onMatch: function (instance) {
instance = instance.mOnClickListener.value;
if (instance) {
console.log("mOnClickListener name is :" + getObjClassName(instance));
watch(instance, 'onClick');
}
},
onComplete: function () {
}
})
})
}
function OnTouchListener() {
Java.perform(function () {
//以spawn启动进程的模式来attach的话
Java.use("android.view.View").setOnTouchListener.implementation = function (listener) {
if (listener != null)
watch(listener, 'onTouch');
return this.setOnTouchListener(listener);
};
//如果frida以attach的模式进行attch的话
Java.choose("android.view.View$ListenerInfo", {
onMatch: function (instance) {
instance = instance.mOnTouchListener;
if (instance) {
console.log("mOnTouchListener name is :" + getObjClassName(instance));
watch(instance, 'onTouch');
}
},
onComplete: function () {
}
})
})
}
setImmediate(OnClickListener);
/**
* OnClickListener#onClick(View v)
* OnTouchListener#onTouch(View v, MotionEvent event)
* Activity onTouchEvent(MotionEvent event) Activity 触摸事件
* RecyclerView OnItemTouchListener#onTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) RecyclerView条目触摸事件
* OnFlingListener#onFling(int x, int y)

OnScrollChangeListener#onScrollChange(View v, int x,int y, int oldX, int oldY)

OnScrollListener#onScrollStateChanged(RecyclerView recyclerView, int newState)

OnScrollListener#onScrolled(RecyclerView recyclerView,int dx, int dy) onScroll RecyclerView 滑动事件
* ListView OnItemClickListener#onItemClick(AdapterView parent,View view, int position, long id) - onViewItemClick - ListView - 条目点击事件
* OnScrollListener#onScrollStateChanged(AbsListView view, int scrollState) 或
OnScrollChangeListener#onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) onScroll ListView 滑动事件
*/

例如修改某个类的实例变量值:

1
2
3
4
5
6
7
8
9
10
11
12
function hookVIP(){
Java.perform(function(){
Java.choose("com.ilulutv.fulao2.film.l",{
onMatch:function(ins){
console.log("found ins:=>",ins)
ins.q0.value = true;
},onComplete:function(){
console.log("search completed!")
}
})
})
}

Frida SO 入门

通过模块名获取Module如下,此外还可用findModuleByAddress通过地址获取Module.

1
2
3
var module=Process.findModuleByName("libxxx.so");
if(module!=null)
console.log(JSON.stringify(module));

Process常用方法:

1
2
3
4
5
6
7
8
9
10
console.log("pid:",Process.id);
console.log("arch:",Process.arch);
console.log("platform:",Process.platform);
console.log("pageSize:",Process.pageSize); //虚拟内存页大小
console.log("pointerSize:",Process.pointerSize); //指针大小 64位为8字节
console.log("CurrentThreadId:",Process.getCurrentThreadId());
var soAddr=Process.findModuleByName("libxxx.so").base;
console.log("soAddr:",soAddr);
var range=Process.findRangeByAddress(Process.findModuelByName("libxxx.so").base);
console.log("Range:",JSON.stringify(range));

枚举导入表:

1
2
3
4
5
6
7
8
9
10
var imports=Process.findModuleByName("libxxx.so").enumerateImports();
var sprintf_addr=null;
for(let i=0;i<imports.length;i++){
let _import=imports[i];
if(_import.name.indexOf("sprintf")!=-1){
sprintf_addr=_import.address;
break;
}
}
console.log("sprintf_addr:",sprintf_addr);

枚举导出表:

1
2
3
4
5
6
7
8
9
10
var exports=Process.findModuleByName("libxxx.so").enumerateExports();
var MD5Final_addr=null;
for(let i=0;i<exports.length;i++){
let _export=exports[i];
if(_export.name.indexOf("_Z8MD5FinalP7MD5_CTXPh")!=-1){
MD5Final_addr=_exports.address;
break;
}
}
console.log("MD5Final_addr:",MD5Final_addr);

枚举符号表:

1
2
3
4
5
6
7
8
var symbols=Process.findModuleByName("libxxx.so").enumerateSymbols();
var RegisterNatives_addr=null;
for(let i=0;i<symbols.length;i++){
var symbol=symbols[i];
if(symbol.name.indexOf("CheckJNI")==-1&&symbol.name.indexOf("RegisterNatives")!=-1) //不带有“CheckJNI”且包含“RegisterNatives”
RegisterNatives_addr=symbol.address;
}
console.log("RegisterNatives_addr:",RegisterNatives_addr);

获取JNIEnv*指针变量的内存地址:

1
2
3
4
var env=Java.vm.tryGetEnv();
console.log(hexdump(env.handle.readPointer)); //几种等价的写法
console.log(hexdump(Memory.readPointer(env)));
console.log(hexdump(ptr(env).readPointer()));

打印系统函数栈:

1
2
3
console.log(Thread.backtrace(this.context,Backtracer.ACCURATE)); //比较准确 有时不能用
console.log(Thread.backtrace(this.context,Backtracer.FUZZY)); //二进制文件都有效 结果可能不准
console.log(DebugSymbol.fromAddress(ptr(0x79ca9793a8)).toString()); //查对应地址的调试符号

调试符号类:

1
2
3
4
5
6
7
8
9
10
var debsym=DebugSymbol.fromName("strcat");
console.log("address:",debsym.address);
console.log("name:",debsym.name);
console.log("moduleName:",debsym.moduleName);
console.log("fileName:",debsym.fileName);
console.log("lineNumber:",debsym.lineNumber);
console.log("toString:",debsym.toString());
console.log("getFunctionByName:",DebugSymbol.getFunctionByName("strcat"));
console.log("findFunctionsNamed:",DebugSymbol.findFunctionsNamed("JNI_OnLoad"));
console.log("findFunctionMatching:",DebugSymbol.findFunctionsMatching("JNI_OnLoad"));

修改内存权限:

1
2
var soAddr=Module.findBaseAddress("libxxx.so");
Memory.protect(soAddr.add(0x3DED),16,'rwx')

内存读写:

1
2
3
4
5
var addr=Memory.alloc(8);
addr.writeByteArray(hexToBytes("0123456789abcdef"));
console.log(addr.readByteArray(8));
var addr=Memory.allocUtf8String("嗨嗨嗨");
console.log(addr.readByteArrary(16));

有时应用队SO文件加固,需要dump内存,这里将SO函数dump到文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function dump_so(so_name){
Java.perform(function(){
var module=Process.getModuleByName(so_name);
console.log("[nane]:",module.name);
console.log("[base]:",module.base);
console.log("[size]:",module.size);
console.log("[path]:",module.path);
var currentApplication=Java.use("android.app.ActivityThread").currentApplication();
var dir=currentApplication.getApplicationContext().getFilesDir().getPath();
var path=dir+"/"+module.name+"_"+module.base+"_"+module.size+".so";
var file=new File(path,"wb");
if(file){
Memory.protect(module.base,module.size,'rwx');
var buffer=module.base.readByteArray(module.size);
file.write(buffer);
file.flush();
file.close();
console.log("[dump]:",path);
}
});
}
dump_so("libxxx.so");

代码跟踪引擎Stalker:

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
//onReceive用法
var md5Addr=Module.getExportByName("libxxx.so","Java_com_xxx_ndk_NativeHelper_md5");
Interceptor.attach(md5Addr,{
onEnter:function(){
this.tid=Process.getCurrentThreadId(); //线程ID
Stalker.follow(this.tid,{
events:{ //生成跟踪事件传给onReceive和onCallSummary call函数调用、ret返回指令、exec所有指令、基本块block等
call:true,
},
onReceive(events){
var _events=Stalker.parse(events); //解析参数
for(var i=0;i<_events.length;i++){
var addr1=_events[i][1];
var module1=Process.findModuleByAddress(addr1);
if(module1&&module.name=="libxxx.so"){
var addr2=_events[i][2];
var module2=Process.findModuleByAddress(addr2);
console.log(module1.name,addr1.sub(module1.base),module2.name,addr2.sub(module2.base)); //发生call的模块和偏移、被调用的函数模块和偏移
}
}
},
});
},
onLeave:function(){
Stalker.unfollow(this.tid); //解除追踪
}
});

//onCallSummary
var md5Addr=Module.getExportByName("libxxx.so","Java_com_xxx_ndk_NativeHelper_md5");
Interceptor.attach(md5Addr,{
onEnter:function(){
this.tid=Process.getCurrentThreadId();
Stalker.follow(this.tid,{
events:{
call:true,
},
onCallSummary(summary){
for(const addr in summary){
var module=Process.findModuleByAddress(addr);
if(module&&module.name=="libxxx.so"){
const num=summary[addr];
console.log(module.name,ptr(addr).sub(module.base),num); //模块、地址、被调用次数
}
}
},
});
},
onLeave:function(){
Stalker.unfollow(this.tid);
}
});

Native Hook

在Java数据类型和JNI数据类型对应中,JNI数据类型为“j+Java数据类型小写”。例如Java下的String、Object对应JNI下的jstring、jobject。

在Objection下:

1
2
memory list modules #列出进程中模块、加载地址、文件大小、存储目录
memory list exports lib*.so #查看某模块所有导出符号

在编写Native程序时,函数声明前若不加extern "C"描述符,会被C++的名称粉碎机制而改变函数名,导致无法Java层无法找到相应native实现。例如可以恢复:

1
2
3
┌──(root㉿computer)-[~]
└─# c++filt _Z48Java_com_roysue_r0so_MainActivity_stringFromJNI2P7_JNIEnvP8_jobject
Java_com_roysue_r0so_MainActivity_stringFromJNI2(_JNIEnv*, _jobject*)

一个模板如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function hook_native(){
var addr = Module.getExportByName("libnative-lib.so", "Java_com_roysue_r0so_MainActivity_stringFromJNI"); //找到函数首地址 第一个参数为null则从所有模块中搜索
Interceptor.attach(addr,{ //要Hook的函数地址
onEnter:function(args){ //调用前产生的回调
//args[2]=ptr(100); //修改参数的两种方法
//this.context.x2=100;
console.log("jnienv pointer =>",args[0])
console.log("jobj pointer =>",args[1])
},onLeave:function(retval){ //函数返回值
console.log("retval is =>",Java.vm.getEnv().getStringUtfChars(retval, null).readCString()) //当前线程JNIEnv readCString从获得的C字符串指针获取内存中对应C字符串
console.log("=================")
//retval.replace(100); //修改返回值的两种方法
//this.context.x0=100;
}
})
}
function main(){
hook_native()
}
setImmediate(main)

此外对于字符串类型的参数赋值方法为:

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
function stringToBytes(str){
return hexToBytes(stringToHex(str));
}
function stringToHex(str){
return str.split("").map(function(c){
return ("0"+c.charCodeAt(0).toString(16)).slice(-2);
}).join("");
}
function hexToBytes(hex){
for(var bytes=[],c=0;c<hex.length;c+=2)
bytes.push(parseInt(hex.substr(c,2),16));
return bytes;
}
function hexToString(hexStr){
var hex=hexStr.toString();
var str='';
for(var i=0;i<hex.length;i+=2)
str+=String.fromCharCode(parseInt(hex.substr(i,2),16));
return str;
}
var MD5Update=Module.findExportByName("libxxx.so","_Z9MD5UpdateP7MD5_CTXPhj");
var strAddr=Module.findBaseAddress("libxxx.so").add(0x3CFD);
var newStr="xxx";
var newStrAddr=Memory.allocUtf8String(newStr);
Interceptor.attach(MD5Update,{
onEnter:function(args){
//内存中已有字符串
if(args[1].readCString()=="xxx"){
args[1]=strAddr;
console.log(hexdump(args[1]));
args[2]=ptr(strAddr.readCString().length);
console.log(args[2].toInt32());
}
//主动构造字符串
if(args[1].readCString()=="xxx"){
let newStr="xxx\0";
args[1].writeByteArray(stringToBytes(newStr));
console.log(hexdump(args[1]));
args[2]=ptr(newStr.length-1);
console.log(args[2].toInt32());
}
//内存中构造字符串
if(args[1].readCString()=="xxx"){
args[1]=newStrAddr;
console.log(hexdump(args[1]));
args[2]=ptr(newStr.length);
console.log(args[2].toInt32());
}
},
onLeave:function(retval){
}
})

Native层动态注册方法为:

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
#include <jni.h>
#include <string>
#include <android/log.h>
#define TAG "r0so2"
// 定义info信息
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)
// 定义debug信息
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
// 定义error信息
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__)
extern "C" JNIEXPORT jstring JNICALL Java_com_roysue_r0so_MainActivity_stringFromJNI(JNIEnv* env,jobject /* this */) {
std::string hello = "Hello from C++ r0ysue";
return env->NewStringUTF(hello.c_str());
}
JNIEXPORT jstring JNICALL stringFromJNI3(JNIEnv* env,jobject /*this*/) {
std::string hello = "Hello from C++ stringFromJNI3 r0ysue ";
return env->NewStringUTF(hello.c_str());
}
jint JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env;
vm->GetEnv((void**)&env,JNI_VERSION_1_6);
JNINativeMethod methods[] = {{"sI3","()Ljava/lang/String;",(void*)stringFromJNI3}};
env->RegisterNatives(env->FindClass("com/roysue/r0so/MainActivity"),methods,1);
return JNI_VERSION_1_6;
}

Hook函数RegisterNativeshttps://github.com/lasting-yang/frida_hook_libart/blob/master/hook_RegisterNatives.js ,需要在Spawn方法使用,可获取动态注册时相应函数地址,并写脚本对0xf444进行Hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function hook_native3(){
var libnative_addr = Module.findBaseAddress('libnative-lib.so');
console.log("libnative_addr is => ",libnative_addr)
var stringfromJNI3 = libnative_addr.add(0xf444);
console.log("stringfromJNI3 address is =>",stringfromJNI3);
Interceptor.attach(stringfromJNI3,{
onEnter:function(args){
console.log("jnienv pointer =>",args[0])
console.log("jobj pointer =>",args[1])
// console.log("jstring pointer=>",Java.vm.getEnv().getStringUtfChars(args[2], null).readCString() )
},onLeave:function(retval){
console.log("retval is =>",Java.vm.getEnv().getStringUtfChars(retval, null).readCString())
console.log("=================")
}
})
}
function main(){
hook_native3()
}
setImmediate(main)

Native层主动调用如下:

1
2
3
4
5
6
7
8
9
10
setImmediate(function(){
var method01_addr=Module.findExportByName("libroysue.so","Java_com_roysue_easysol_MainActivity_method01");
console.log("method01 address is=>",method01_addr);
Java.perform(function(){
var jstring=Java.vm.getEnv().newStringUtf("roysue");
var method01=new NativeFunction(method01_addr,"pointer",["pointer","pointer","pointer"]); //第二个为返回值 后三个为参数类型 jstring也是指针
var result=method01(Java.vm.getEnv(),jstring,jstring);
console.log("Final result is=>",Java.vm.getEnv().getStringUtfChars(result,null).readCString());
})
})

对于Socket层的抓包,之前函数com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write在Native层的实现是boringssl模块中的SSL_write函数。查看boringssl模块的Android.bp编译配置文件可知最终编译生成模块有libcrypto.so和libssl.so,此时查看这俩模块导出函数:

1
2
memory list exports libcrypto.so --json libcrypto.so.json
memory list exports libssl.so --json libssl.so.json

吐出libssl.so中SSL_write的接收到的数据包信息脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function hook_ssl_write(){
var addr = Module.getExportByName("libssl.so", "SSL_write");
Interceptor.attach(addr,{
onEnter:function(args){
console.log("\n",hexdump(args[1],{length: args[2].toInt32()}))
},onLeave:function(retval){
// console.log("retval is =>",Java.vm.getEnv().getStringUtfChars(retval, null).readCString())
console.log("================== onLeave =================")
}
})
}
function main(){
console.log("Entering main")
hook_ssl_write()
}
setImmediate(main)

接下来trace一下libssl.so中所有符号函数:

1
frida-trace -UF -I libssl.so

Frida还可便利进程所有模块,和某模块所有导出符号,实现类似Objection的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function traceNativeExports(){
var modules=Process.enumerateModules();
for(var i=0;i<modules.length;i++){
var module=module[i];
if(module.name.indexOf("libssl.so")<0)
continue;
var exports=module.enumerateExports();
for(var j=0;j<exports.length;j++)
console.log("module name is=>",module.name,"symbol name is=>",exports[j].name,"address:"+exports[j].address,"offset=>",(exports[j].address.sub(module.base)));
}
}
function traceNativeSymbols(){
var modules=Process.enumerateModules();
for(var i=0;i<modules.length;i++){
var module=modules[i];
if(module.name.indexOf("libssl.so")<0)
continue;
var exports=module.enumerateSymbols();
for(var j=0;j<exports.length;j++)
console.log("module name is=>",module.name,"symbol name is=>",exports[j].name,"address:"+exports[j].address,"offset=>",(exports[j].address.sub(module.base)));
}
}

Frida还可以写文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function writeSomething(path, contents) {
var fopen_addr = Module.findExportByName("libc.so", "fopen");
var fputs_addr = Module.findExportByName("libc.so", "fputs");
var fclose_addr = Module.findExportByName("libc.so", "fclose");
var fopen = new NativeFunction(fopen_addr, "pointer", ["pointer", "pointer"])
var fputs = new NativeFunction(fputs_addr, "int", ["pointer", "pointer"])
var fclose = new NativeFunction(fclose_addr, "int", ["pointer"])
var fileName = Memory.allocUtf8String(path);
var mode = Memory.allocUtf8String("a+");
var fp = fopen(fileName, mode);
var contentHello = Memory.allocUtf8String(contents);
var ret = fputs(contentHello,fp)
fclose(fp);
}

然后再对所有函数追踪:

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 attach(name,address){
console.log("attaching ",name);
Interceptor.attach(address,{
onEnter:function(args){
console.log("Entering => " ,name)
},onLeave:function(retval){}
})
}
function traceNativeExport(){
var modules = Process.enumerateModules();
for(var i = 0;i<modules.length;i++){
var module = modules[i];
if(module.name.indexOf("libssl.so")<0)
continue;
var exports = module.enumerateExports();
console.log('module.addr',module.base);
for(var j = 0;j<exports.length;j++){
if(exports[j].type == "function")
attach(exports[j].name,exports[j].address)
var path = "/data/data/com.roysue.httpurlconnectiondemo/"+module.name+".txt"
writeSomething(path,"type: "+exports[j].type+" function name :"+exports[j].name+" address : "+exports[j].address+" offset => 0x"+(exports[j].address - (modules[i].base)).toString(16)+"\n")
}
}
}

Frida还可以反调试:

1
2
3
4
5
6
7
function replaceKill(){
var kill_addr=Module.findExportByName("libc.so","kill");
Interceptor.replace(kill_addr,new NativeCallback(function(arg0,arg1){
console.log("arg0=>",arg0);
console.log("arg1=>",arg1);
},'int',['int','int']))
}

Frida集成了Capstone,方法为:

1
2
3
4
5
6
7
8
9
10
11
function dis(adress,number){
for(var i=0;i<number;i++){
var ins=Instruction.parse(address);
console.log("address:"+address+"--dis:"+ins.toString());
address=ins.next;
}
}
setImmediate(function(){
var stringFromJniaddr=Module.findExportByName("libroysue.so","Java_com_roysue_easysol_MainActivity_stringFromJNI");
dis(stringFromJniaddr,10);
})

Frida还可指令级修改:

1
2
3
4
5
6
7
8
9
10
11
12
//修改单指令
var soAddr=Module.findBaseAddress("libxxx.so");
new Arm64Writer(soAddr.add(0x1AEC)).putNop();
console.log(Instruction.parse(soAddr.add(0x1AEC)).toString());

//修改函数指令
var codeAddr=Module.findBaseAddress("libxxx.so").add(0x1AF4);
Memory.patchCode(codeAddr,4,function(code){
var writer=new Arm64Writer(code,{pc:codeAddr});
writer.putBytes(hexToBytes("0001094B")); //sub w0, w8, w9
writer.flush();
});

Frida还可对SO动态库中函数地址进行Hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var base_address=Module.findBaseAddress("libdebug.so")
if(base_address){
var func_offset=0x1F17C;
var addr_func=base_address.add(func_offset);
var pfunc=new NativePointer(addr_func);
Interceptor.attach(pfunc,{
onEnter:function(args){
console.log("call func with args:"+args[0],args[1]);
},
onLeave:function(retval){
console.log("func return:",retval);
}
})
}

JNI函数静态注册可用Hook dlsym。静态注册时一开始没绑定so层函数。当Java层的native函数首次被调用,系统按规则构建出对应函数名,通过dlsym去每个so文件中找符号并绑定,之后便不再触发。

1
2
3
4
5
6
7
8
9
10
11
12
var dlsymAddr=Module.findExportsByName("libdl.so","dlsym");
Interceptor.attach(dlsymAddr,{
onEnter:function(args){
this.args1=args[1];
},
onLeave:function(retval){
var module=Process.findModuleByAddress(retval);
if(module==null)
return;
console.log(this.args1.readCString(),module.name,retval,retval.sub(module.base));
}
});

JNI动态注册时用Hook RegisterNatives即可,定义为:

1
2
3
4
5
6
jint (*RegisterNatives)(JNIEnv*,jclass,const JNINativeMethod*,jint);
typedef struct{
const char* name;
const char* signature;
void* fnPtr;
}JNINativeMethod;

RegisterNatives Hook如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var RegisterNativesAddr=null;
var _symbols=Process.findModuleByName("libart.so").enumerateSymbols();
for(var i=0;i<_symbols.length;i++){
var _symbol=_symbols[i];
if(_symbol.name.indexOf("CheckJNI")==-1&&_symbol.name.indexOf("RegisterNatives")!=-1)
RegisterNativesAddr=_symbols[i].address;
}
Interceptor.attach(RegisterNativesAddr,{
onEnter:function(args){
var env=Java.vm.tryGetEnv();
var className=env.getClassName(args[1]);
var methodCount=args[3].toInt32();
for(let i=0;i<methodCount;i++){
var methodName=args[2].add(Process.pointerSize*3*i).readPointer().readCString();
var signature=args[2].add(Process.pointerSize*3*i).add(Process.pointerSize).readPointer().readCString();
var fnPtr=args[2].add(Process.pointerSize*3*i).add(Process.pointerSize*2).readPointer();
var module=Process.findModuleByAddress(fnPtr);
console.log(className,methodName,signature,fnPtr,module.name,fnPtr.sub(module.base));
}
},
onLeave:function(retval){
}
});

jnitrace用法:

1
jnitrace -m attach -l libxxx.so com.xxx.xxx.android

有时若用Spawn方式注入,SO文件还未加载无法注入。用Attach模式时,SO文件可能已经加载且要Hook的函数已执行完毕。Hook dlopen方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function hook_func(){
var myInit=Module.findBaseAddress("libxxx.so").add(0x1DE8);
Interceptor.replace(myInit,new NativeCallback(function(){
console.log("success");
},"void",[]));
}
function hook_dlopen(){
var android_dlopen_ext=Module.findExportByName("libdl.so","android_dlopen_ext");
Interceptor.attach(android_dlopen_ext,{
onEnter:function(args){
var soPath=args[0].readCString();
if(soPath.indexOf("libxxx.so")!=-1)
this.hook=true;
},
onLeave:function(retval){
if(this.hook)
hook_func();
}
});
}
hook_dlopen();

dlopen执行后调用JNI_OnLoad,所以Hook JNI_OnLoad只需Hook dlopen,并在onLeave中处理即可。SO文件加载时,initinit_arraydlopen执行过程中调用,Hookinit_array时需要dlopen中再找个Hook点,必须在该函数执行前SO文件加载完毕,且SO文件中initinit_array尚未被调用,这里选择linker64.so中的call_constructors。Hookinit_array方法为:

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
function hook_dlopen(addr,soName,callback){
Interceptor.attach(addr,{
onEnter:function(args){
var soPath=args[0].readCString();
if(soPath.indexOf(soName)!=-1)
callback();
},
onLeave:function(retval){
}
});
}
function hook_initarray(){
var xxxAddr=Module.findBaseAddress("libxxx.so");
var func_addr=xxxAddr.add(0x1D14);
Interceptor.replace(func_addr,new NativeCallback(function(){
console.log("replaced");
},'void',[]));
Interceptor.detachAll();
}
function hook_call_constructors(){
var _symbols=Process.getModuleByName("linker64").enumerateSymbols();
var call_constructors_addr=null;
for(let i=0;i<_symbols.length;i++)
if(_symbol.name.indexOf("call_constructors")!=-1)
call_constructors_addr=_symbol.address;
Interceptor.attach(call_constructors_addr,{
onEnter:function(args){
hook_initarray();
},
onLeave:function(retval){
}
});
}
var android_dlopen_ext=Module.findExportByName("libdl.so","android_dlopen_ext");
hook_dlopen(android_dlopen_ext,"libxxx.so",hook_call_constructors);

Hook pthread_create,用于检查应用为哪些函数开启了线程。

1
2
3
4
5
6
7
8
9
10
11
var pthread_create_addr=Module.findExportByName("libc.so","pthread_create");
Interceptor.attach(pthread_create_addr,{
onEnter:function(args){
console.log(args[0],args[1],args[2],args[3]);
var Module=Process.findModuleByAddress(args[2]);
if(Module!=null)
console.log(Module.name,args[2].sub(Module.base));
},
onLeave:function(retval){
}
})

修改某块内存区域的访问权限后,当应用访问这块内存时,会触发Access-violation异常。此时设置异常处理回调函数,当异常触发时,回调函数可通过修改寄存器和内存让程序从异常中恢复,返回true则Frida立即恢复线程。这种方法只能修改内存页权限,想要监控指定字节还得用Unidbg。

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
function hook_dlopen(addr,soName,callback){
Interceptor.attach(addr,{
onEnter:function(args){
var soPath=args[0].readCString();
if(soPath.indexOf(soName)!=-1)
this.hook=true;
},
onLeave:function(retval){
if(this.hook)
callback();
}
});
}
function set_read_write_break(){
Process.setExceptionHandler(function(details){
console.log(JSON.stringify(details,null,2));
console.log("lr",DebugSymbol.fromAddress(details.context.lr));
console.log("pc",DebugSymbol.fromAddress(details.context.pc));
Memory.protect(details.memory.address,Process.pointerSize,'rwx');
console.log(Thread.backtrace(details.context,Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n')+'\n');
return true;
});
var addr=Module.findBaseAddress("libxxx.so").add(0x3DED);
Memory.protect(addr,8,'---')
}
var android_dlopen_ext=Module.findExportByName("libdl.so","android_dlopen_ext");
hook_dlopen(android_dlopen_ext,"libxxx.so",set_read_write_break);

一个对常用Hook方法的封装:

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
function printAddr(addr){
var module=Process.findRangeByAddress(addr);
if(module!=null)
return hexdump(addr)+"\n";
return ptr(addr)+"\n";
}
function hookAddr(funcAddr,paramsNum){
var module=Process.findModuleByAddress(funcAddr);
Interceptor.attach(funcAddr,{
onEnter:function(args){
this.logs=[];
this.params=[];
this.logs.push("call "+module.name+"!"+ptr(funcAddr).sub(module.base)+"\n");
for(let i=0;i<paramsNum;i++){
this.params..push(args[1]);
this.logs.push("this.args"+i+"onEnter:"+printAddr(args[i]));
}
},
onLeave:function(retval){
for(let i=0;i<paramsNum;i++)
this.logs.push("this.args"+i+"onLeave:"+printAddr(this.params[i]));
this.logs.push("retval onLeave:"+printAddr(retval)+"\n");
console.log(this.logs);
}
});
}
var soAddr=Module.findBaseAddress("libxxx.so");
hookAddr(soAddr.add(0x1ACC),5);