安卓逆向入门-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
36
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()

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

逆向工作思路

找到当前正在运行的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
2
3
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!")
}
})
})
}

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
function hook_native(){
var addr = Module.getExportByName("libnative-lib.so", "Java_com_roysue_r0so_MainActivity_stringFromJNI"); //找到函数首地址 第一个参数为null则从所有模块中搜索
Interceptor.attach(addr,{ //要Hook的函数地址
onEnter:function(args){ //调用前产生的回调
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("=================")
}
})
}
function main(){
hook_native()
}
setImmediate(main)

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还可对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);
}
})
}

安全加固

ProGuard

build.gradle中添加:

1
2
3
4
5
6
buildTypes{
release{
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-optimize.txt'),'proguard-rules.pro'
}
}

签名校验

上文说PMS返回值可能被替换,这里直接绕过系统API,直接读取data/app/包名/base.apk中签名文件,获取原始签名信息。系统PackageParser服务对APK进行解析并获取文件信息,但该系统不对外暴露,需要用反射调用:

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//MainActivity
package com.verify.signature;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import com.verify.signature.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Context context = getApplicationContext();
Log.i("Current sign ===>", Tools.getAppSignature(context));
Log.i("Reflect sign ===>", Tools.getReflectSignature(context));
Log.i("Native sign ===>", Tools.getSignByJni(context));
binding.fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Tools.getSignatureFileCrc(context);
if (!Tools.checkSignature(context)){
new AlertDialog.Builder(MainActivity.this).setTitle("安全提醒")//设置对话框标题
.setMessage("当前使用的客户端为非官方版本,存在极大的安全隐患。为了您的财产安全请在官方渠道下载重新安装!")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
System.exit(0);
}
}).show();
}
}
});
}
}

//Tools
package com.verify.signature;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.Build;
import android.util.Log;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class Tools {
static{
try {
System.loadLibrary("test");
} catch (Exception e) {
e.printStackTrace();
}
}
public static native String getSignByJni(Context mContext);
public static boolean checkSignature(Context context){
String sign = "8b34e97425e0e682e3a73bd55830fc28ce34a4e8";
if (sign.equals(getReflectSignature(context)))
return true;
else
return false;
}
public static String getSignatureFileCrc(Context context){
try {
ZipFile zf;
String path= context.getPackageManager().getApplicationInfo(context.getPackageName(), 0).publicSourceDir;
zf = new ZipFile(path);
ZipEntry ze = zf.getEntry("META-INF/MANIFEST.MF");
String crcValue = String.valueOf(ze.getCrc());
Log.i("Test MANIFEST Crc ====>",crcValue);
return crcValue;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
public static String getAppSignature(Context context) {
try {
return sha1(getSignature(context));
} catch (Exception e) {}
return null;
}
public static byte[] getSignature(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signatures = packageInfo.signatures;
if (signatures != null) {
Log.i("Test ===>", signatures[0].toCharsString());
return signatures[0].toByteArray();
}
} catch (Exception e) {}
return null;
}
public static String getReflectSignature(Context context) {
try {
String path= context.getPackageManager().getApplicationInfo(context.getPackageName(), 0).publicSourceDir;
return sha1(getApkSignatureByReflect(path));
} catch (Exception e) {}
return null;
}
public static String sha1(byte[] bytes) {
try {
MessageDigest md = MessageDigest.getInstance("SHA1");
md.update(bytes);
byte[] b = md.digest();
int i;
StringBuilder sb = new StringBuilder();
for (byte value : b) {
i = value;
if (i < 0)
i += 256;
if (i < 16)
sb.append("0");
sb.append(Integer.toHexString(i));
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
public static byte[] getApkSignatureByReflect(String apkPath) {
String fullPackageParserPath = "android.content.pm.PackageParser";
try {
Class packageParserClass = Class.forName(fullPackageParserPath);
Constructor pkgParserConstructor = null;
Object pkgParserIns = null;
if (Build.VERSION.SDK_INT > 20){
pkgParserConstructor = packageParserClass.getConstructor();
pkgParserIns = pkgParserConstructor.newInstance();
Class[] args = {File.class, Integer.TYPE};;
Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", args);
Object[] valueArgs = new Object[2];
valueArgs[0] = new File(apkPath);
valueArgs[1] = PackageManager.GET_SIGNATURES;
Object parserPackage = parsePackageMethod.invoke(pkgParserIns, valueArgs);
if (Build.VERSION.SDK_INT>=28){
Class[] typeArgs = {parserPackage.getClass(),Boolean.TYPE};
Method collectCertificatesMethod = packageParserClass.getDeclaredMethod("collectCertificates", typeArgs);
Object[] valueArgs2 = {parserPackage, Build.VERSION.SDK_INT>28};
collectCertificatesMethod.invoke(pkgParserIns, valueArgs2);
Field mSignatures = null;
if(Build.VERSION.SDK_INT>=29)
mSignatures = parserPackage.getClass().getDeclaredField("mSigningDetails");
else
mSignatures = parserPackage.getClass().getDeclaredField("mSignatures");
mSignatures.setAccessible(true);
Object mSigningDetails = mSignatures.get(parserPackage);
Field infoField = mSigningDetails.getClass().getDeclaredField("signatures");
infoField.setAccessible(true);
Signature[] info = (Signature[]) infoField.get(mSigningDetails);
Log.i("ReflectSignature ===>", info[0].toCharsString());
return info[0].toByteArray();
}
Class[] typeArgs = {parserPackage.getClass(),Integer.TYPE};
Method collectCertificatesMethod = packageParserClass.getDeclaredMethod("collectCertificates", typeArgs);
Object[] valueArgs2 = {parserPackage, PackageManager.GET_SIGNATURES};
collectCertificatesMethod.invoke(pkgParserIns, valueArgs2);
// 应用程序信息包, 这个公开的, 不过有些函数, 变量没公开
Field packageInfoFld = parserPackage.getClass().getDeclaredField("mSignatures");
Signature[] info = (Signature[]) packageInfoFld.get(parserPackage);
Log.i("ReflectSignature ===>", info[0].toCharsString());
return info[0].toByteArray();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

JNI层:

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
//signature
#include <jni.h>
#include <string.h>
#include <android/log.h>
#include <malloc.h>
#define TAG "Test"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
const char *APP_SIGNATURE = "10645EA8A12BE7A2C04B1F81DF3B4D90";
void ByteToHexStr(const char *source, char *dest, int sourceLen) {
short i;
char highByte, lowByte;
for (i = 0; i < sourceLen; i++) {
highByte = source[i] >> 4;
lowByte = source[i] & 0x0f;
highByte += 0x30;
if (highByte > 0x39)
dest[i * 2] = highByte + 0x07;
else
dest[i * 2] = highByte;
lowByte += 0x30;
if (lowByte > 0x39)
dest[i * 2 + 1] = lowByte + 0x07;
else
dest[i * 2 + 1] = lowByte;
}
}
jstring sha1(JNIEnv *env, jbyteArray source) {
// MessageDigest类
jclass classMessageDigest = env->FindClass("java/security/MessageDigest");
// MessageDigest.getInstance()静态方法
jmethodID midGetInstance = env->GetStaticMethodID(classMessageDigest, "getInstance","(Ljava/lang/String;)Ljava/security/MessageDigest;");
// MessageDigest object
jobject objMessageDigest = env->CallStaticObjectMethod(classMessageDigest,midGetInstance,env->NewStringUTF("sha1"));
// update方法,这个函数的返回值是void,写V
jmethodID midUpdate = env->GetMethodID(classMessageDigest, "update", "([B)V");
env->CallVoidMethod(objMessageDigest, midUpdate, source);
// digest方法
jmethodID midDigest = env->GetMethodID(classMessageDigest, "digest", "()[B");
jbyteArray objArraySign = (jbyteArray) env->CallObjectMethod(objMessageDigest, midDigest);
jsize intArrayLength = env->GetArrayLength(objArraySign);
jbyte *byte_array_elements = env->GetByteArrayElements(objArraySign, NULL);
size_t length = (size_t) intArrayLength * 2 + 1;
char *char_result = (char *) malloc(length);
memset(char_result, 0, length);
// 将byte数组转换成16进制字符串,发现这里不用强转,jbyte和unsigned char应该字节数是一样的
ByteToHexStr((const char *) byte_array_elements, char_result, intArrayLength);
// 在末尾补\0
*(char_result + intArrayLength * 2) = '\0';
jstring stringResult = env->NewStringUTF(char_result);
// release
env->ReleaseByteArrayElements(objArraySign, byte_array_elements, JNI_ABORT);
// 释放指针使用free
free(char_result);
env->DeleteLocalRef(classMessageDigest);
env->DeleteLocalRef(objMessageDigest);
return stringResult;
}
static jobject getContext(JNIEnv *env) {
jobject application = NULL;
jclass activity_thread_clz = env->FindClass("android/app/ActivityThread");
if (activity_thread_clz != NULL) {
jmethodID currentApplication = env->GetStaticMethodID(activity_thread_clz, "currentApplication", "()Landroid/app/Application;");
if (currentApplication != NULL)
application = env->CallStaticObjectMethod(activity_thread_clz, currentApplication);
env->DeleteLocalRef(activity_thread_clz);
}
return application;
}
JNIEXPORT jstring JNICALL getSignByJni(JNIEnv *env, jobject thiz, jobject context) {
// 获得Context类
jclass cls = env->GetObjectClass(context);
// 得到getPackageManager方法的ID
jmethodID mid = env->GetMethodID(cls, "getPackageManager","()Landroid/content/pm/PackageManager;");
// 获得应用包的管理器
jobject pm = env->CallObjectMethod(context, mid);
// 得到getPackageName方法的ID
mid = env->GetMethodID(cls, "getPackageName", "()Ljava/lang/String;");
// 获得当前应用包名
jstring packageName = (jstring) env->CallObjectMethod(context, mid);
// 获得PackageManager类
cls = env->GetObjectClass(pm);
// 得到getPackageInfo方法的ID
mid = env->GetMethodID(cls, "getPackageInfo","(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
// 获得应用包的信息
jobject packageInfo = env->CallObjectMethod(pm, mid, packageName, 0x40); //GET_SIGNATURES = 64;
// 获得PackageInfo 类
cls = env->GetObjectClass(packageInfo);
// 获得签名数组属性的ID
jfieldID fid = env->GetFieldID(cls, "signatures", "[Landroid/content/pm/Signature;");
// 得到签名数组
jobjectArray signatures = (jobjectArray) env->GetObjectField(packageInfo, fid);
// 得到签名
jobject signature = env->GetObjectArrayElement(signatures, 0);
// 获得Signature类
cls = env->GetObjectClass(signature);
// 得到toCharsString方法的ID
mid = env->GetMethodID(cls, "toByteArray", "()[B");
// 返回当前应用签名信息
jbyteArray signatureByteArray = (jbyteArray) env->CallObjectMethod(signature, mid);
return sha1(env, signatureByteArray);
}

SO防护

对不想对外暴露的函数进行隐藏:

1
2
3
__attribute__((visibility("hidden"))) int test(int a,int b){
return a+b;
};

反调试

调试工具附加到目标进程时需要用ptrace系统调用操作,以建立其与目标程序的跟踪关系。正常情况下每个进程同一时间只能被附加一次,若应用程序启动后fork一个子进程抢先一步附加到自身进程,则调试器将无法正常附加,达到调试目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <time.h>
void check_debug_by_ptrace() {
int pid = fork();
if (pid == 0) {
int ppid = getppid();
if (ptrace(PTRACE_ATTACH, ppid, NULL, NULL) == 0) {
waitpid(ppid, NULL, 0);
ptrace(PTRACE_CONT, NULL, NULL);
}
}
}

也可直接用PTRACE_TRACEME参数,在进程启动时主动请求被监控跟踪:

1
2
3
void anti_debug_by_ptrace(){
ptrace(PTRACE_TRACEME,0,NULL,NULL);
}

当攻击者使用调试器进行动态分析时,需要设置断点将应用挂起,此时调试器发出SIGTRAP信号。可以在应用中主动添加SIGTRAP信号监听代码:

1
2
3
4
5
6
7
8
9
void signal_handle(int sig){ 
// 自定义信号处理函数
exit(0);
}
void check_debug_by_signal(){
// 设置SIGTRAP信号的处理函数为signal_handle
long ret = (long)signal(SIGTRAP, signal_handle);
raise(SIGTRAP); // 主动发送SIGTRAP信号
}

进程被附加后有些属性值被改变:

特征文件名 特征值
/proc/pid/status和/proc/pid/task/pid/status 进程status值中TracePid值非0时处于调试状态
/proc/pid/stat和/proc/pid/task/pid/stat 进程stat值中括号后跟随的第一个字母为t时处于调试状态
/proc/pid/wchan和/proc/pid/task/pid/wchan 进程wchan状态值为ptrace_stop时处于调试状态

提取特征值为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void check_debug_by_tracerpid() {
char debug_file[56]={0};
int state = 0;
int pid = getpid();
sprintf(debug_file, "/proc/%d/status", pid);
FILE* fp = fopen(debug_file, "r");
char line[1024]={0};
while(fgets(line, 1024, fp)){
if (strncmp(line, "TracerPid", 9) == 0){
state = atoi(&line[10]);
if (state != 0){
fclose(fp);
exit(0);
}
break;
}
memset(line, 0, 1024);
}
fclose(fp);
}

还可监测代码执行时间,可能出现单步调试现象:

1
2
3
4
5
6
7
8
9
10
void check_debug_by_time() {
time_t start_time;
time_t end_time;
time(&start_time);
// 此处添加需要监控的核心代码
time(&end_time);
// 时间差根据实际情况变动
if(end_time - start_time > 10)
exit(0);
}

进行调试时需要开启并监听指定端口,若设备开启这些端口则说明在和调试器通信,处于调试状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* IDA调试的默认端口23946,对应的十六进制值5D8A
* Frida默认会占用的两个端口27402、27403,对应的十六进制值6B0A、6B0B
*/
void check_debug_by_port() {
char buff[BUFF_LEN];
char line[BUFF_LEN];
const char* dir = "/proc/net/tcp";
FILE *fp = fopen(dir, "r");
while (fgets(buff, BUFF_LEN, fp) != NULL)
if (strstr(buff, "5D8A") != NULL || strstr(buff, "6B0A") != NULL || strstr(buff, "6B0B") != NULL) {
fclose(fp);
exit(0);
}
fclose(fp);
FILE *fd = popen("netstat -apn", "r");
while (fgets(line, sizeof(line), fd) != NULL)
if (strstr(line, "23946") != NULL || strstr(line, "27402") != NULL || strstr(line, "27403") != NULL){
fclose(fd);
exit(0);
}
pclose(fd);
}

为防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
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/ptrace.h>
/*
hook注入检测
返回值为0 说明没有异常库信息
正常返回异常库的个数。
根据type的值不同 检测hook框架和注入
*/
void check_inject()
{
char pFilePath[32];
char pLibInfo[256];
char *pLibPath = NULL;
char *savePtr = NULL;
int pid = getpid();
sprintf(pFilePath, "/proc/%d/maps", pid);
FILE *fp = fopen(pFilePath, "r");
while (fgets(pLibInfo, sizeof(pLibInfo), fp) != NULL)
{
strtok_r(pLibInfo, " \t", &savePtr); // 地址信息
strtok_r(NULL, " \t", &savePtr); // 权限信息
strtok_r(NULL, " \t", &savePtr); // 偏移信息
strtok_r(NULL, " \t", &savePtr); // 设备信息
strtok_r(NULL, " \t", &savePtr); // 节点信息
pLibPath = strtok_r(NULL, " \t", &savePtr); // 路径信息
if (pLibPath != NULL) // 判断是否有路径信息
{
if (check_block_list(pLibPath) == -1)
{
exit(0);
}
}
memset(pLibInfo, 0, 256);
}
fclose(fp);
}

完整性校验

对应用安装包中文件计算CRC值,与原始CRC值对比,其中CRC值应保存到服务器中。

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
//MainActivity
package com.test.integritycheck_android;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
Button btn = null;
Context mContext = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContext = getApplicationContext();
btn = findViewById(R.id.check);
btn.setOnClickListener(new MyClick());
}
class MyClick implements View.OnClickListener{
@Override
public void onClick(View view) {
new Thread(new Runnable() {
@Override
public void run() {
UtilTools.getFileCrc(mContext);
UtilTools.getAllFileCrc(mContext);
}
}).start();
}
}
}

//UtilTools
package com.test.integritycheck_android;
import android.content.Context;
import android.util.Log;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class UtilTools {
private static final String[] fileArray = {"META-INF/MANIFEST.MF","classes.dex","resources.arsc","AndroidManifest.xml"};
public static Map getFileCrc(Context context){
Map result = new HashMap();
try {
String path= context.getPackageManager().getApplicationInfo(context.getPackageName(), 0).publicSourceDir;
ZipFile zf = new ZipFile(path);
ZipEntry ze;
for(String item:fileArray){
ze = zf.getEntry(item);
if (ze != null){
String crcValue = String.valueOf(ze.getCrc());
result.put(item, crcValue);
Log.i(item+ " CRC ======>",crcValue);
}
}
return result;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
public static Map getAllFileCrc(Context context){
Map result = new HashMap();
try {
String path= context.getPackageManager().getApplicationInfo(context.getPackageName(), 0).publicSourceDir;
ZipFile zf = new ZipFile(path);
Enumeration<? extends ZipEntry> entries = zf.entries();
while (entries.hasMoreElements()) {
// get the zip entry
ZipEntry entry = entries.nextElement();
String crcValue = String.valueOf(entry.getCrc());
result.put(entry.getName(), crcValue);
Log.i(entry.getName()+ " CRC ======>",crcValue);
}
return result;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}