注入框架Frida的安装配置与基本使用

1 背景

Frida是一款跨系统、跨平台的开源进程注入框架,官方网站是https://www.frida.re。Frida支持Windows、GNU/Linux、macOS、IOS、Android等平台的进程注入,功能强大。使用Frida提供的Shell环境,可以查看目标进程内存中的任何内容、调用进程中加载的函数、注入一个外部的DEX文件或so动态库等。

2 Frida的安装与配置

2.1 计算机端安装

安装Frida分为计算机端安装和Android设备端安装两部分。计算机端基本配置信息为Windows 10、Python 3.8.7,用pip安装Frida的命令为pip install frida-tools,如图1所示。

图1 pip安装Frida

2.2 Android设备端安装

在Android设备端安装Frida的步骤也很简单,只需将frida-server复制到设备中。frida-server的可执行文件可以在https://github.com/frida/frida/releases页面找到。

比如,针对Aarch64架构的frida-server最新版本的文件名为frida-server-15.2.2-android-arm64.xz,要想知道Android设备CPU的架构类型,可以通过命令adb shell getprop ro.product.cpu.abi查看。

图2 查看Android设备CPU架构类型

如图2所示,我的Android Studio模拟器CPU架构类型是armeabi-v7a,因此,我需要下载的frida-server文件名是frida-server-15.2.2-android-arm.xz,解压后并push到/data/local/tmp目录下,然后赋予可执行权限,如图3所示。

图3 将frida-server复制到Android设备端

【注】此处我使用AndroidStudio模拟器进行操作,因为可以创建CPU架构与真机一致的armeabi-v7a,其他模拟器都没有这种效果,虽然慢的要命┭┮﹏┭┮,但多等一会也能运行起来。

至此,Frida的安装过程就结束了,在计算机端执行命令frida-ls-devices,可查看当前连接到计算机的设备,如图4所示。

图4 查看当前连接到计算机的设备

在设备上启动frida-server,然后另开一个终端执行命令frida-ps -U,可查看设备上的进程信息,如图5&6所示。

图5 启动frida-server

图6 查看设备上的进程信息

3 Java层执行注入与Hook

下面主要从以下几个方面来介绍:

  • 如何修改Java层的函数参数和返回值
  • 如何拦截Native层的函数参数和返回值

3.1 Crackme0201

用Frida可向运行在Android设备上的进程注入一小段代码或一个程序。以Crackme0201为例,首先在设备上启动目标程序(如 Crackme0201)(注意:一定要先打开目标程序,再下一步操作),然后运行frida-server。此时可用frida-ps -U命令查看其进程信息,如图6所示。

执行命令frida -U –no-pause -f com.ihui.crackme0201,用Frida对com.ihui.crackme0201进程进行注入,如图7所示。

图7 注入进程Crackme0201

若注入成功,能看到图7箭头处显示的Shell环境。在其中可执行Frida API提供的方法,查看进程的详细信息。按Tab会以菜单的方式显示当前执行环境支持的方法和命令。图8是执行Process中的方法,看到的Process的一些信息。

图8 查看进程的详细信息

执行Module中的一些方法,可查看模块信息。如libc.so的导入函数表和导出函数,及libc.so的内存区域中哪些内存区是可执行的:

Module.findBaseAddress(“libc.so”)
Module.enumerateImportsSync(“libc.so”)
Module.enumerateExportsSync(“libc.so”)
Module.enumerateRangesSync(“libc.so”,”–x”)

现在的目标不仅是查看进程空间的内容,还要修改checkSN(),让其在传入任意参数时返回true,要执行此操作,需要使用与Java相关的方法。先执行命令Java.available,判断当前进程是否支持执行Java方法,如图9所示。

图9 判断当前进程是否支持执行Java方法

输出为true,表示可执行Java.perform()Java.use()等方法。为修改checkSN()的实现,要先用Java.use()获取MainActivity类,再修改其checkSN的implementation属性。整段JavaScript代码如下:

1
2
3
4
5
6
7
8
Java.perform(function() {
var clz = Java.use("com.ihui.crackme0201.MainActivity");
clz.checkSN.implementation = function(s1, s2) {
console.log("[*] checkSN() called");
return true;
};
console.log("[*] checkSN handler modified");
})

【注意】在Frida的Shell环境里,无法一次性复制粘贴所有内容,所有代码要在一行中输入。故上述js代码要按图10进行输入。

图10 输入脚本注册成功

但这样一行输入效率太低,Frida提供外部输入功能:先将要注入的代码写好,启动Frida程序时传入,重新整理上述代码

1
2
3
4
5
6
7
8
9
10
11
12
13
setImmediate(function() {
console.log("[*] Starting script");
Java.perform(function() {
var clz = Java.use("com.ihui.crackme0201.MainActivity");
clz.checkSN.implementation = function(s1, s2) {
console.log("[*] checkSN() called");
console.log("[*] s1: " + s1);
console.log("[*] s2: " + s2);
return true;
};
console.log("[*] checkSN handler modified");
});
});

【注意】变量clz一定要声明,原书未声明,导致报错,但奇怪的是前面的命令中clz未声明,却执行成功。

图11 指定JavaScript脚本注册成功

上述代码同样修改了checkSN()的返回值,还输出了两个参数,在Frida的Shell中按Ctrl+D退出,同样提示注册成功,而Frida的Shell中输出了在目标程序中输入的两个参数,如图11所示。

除了JavaScript,Frida还支持多种语言的API绑定,如Python。可在Python的交互环境中执行import frida命令,然后用Frida提供的所有Python API驱动Frida工作(注意:若出现frida.ServerNotRunningError: unable to connect to remote frida-server错误,可尝试端口转发adb forward tcp:27042 tcp:27042)。

如图12所示,可以看到,破解效果与前面介绍的相同。自动化脚本crackme0201hooker.py如下所示:

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
import frida
import codecs
import sys

jscode= """
setImmediate(function() {
console.log("[*] Starting script");
Java.perform(function() {
var clz = Java.use("com.ihui.crackme0201.MainActivity");
clz.checkSN.implementation = function(s1, s2) {
console.log("[*] checkSN() called");
console.log("[*] s1: " + s1);
console.log("[*] s2: " + s2);
return true;
};
console.log("[*] onClick handler modified");
});
});
"""

device = frida.get_usb_device()
session = device.attach("Crackme0201")
script = session.create_script(jscode)
script.load()
sys.stdin.read()
session.detach()

图12 Python自动化脚本注册成功

【注意】这里需要对原书中的脚本稍加修改,不然会报错。

  • 变量clz一定要声明,原书未声明,导致报错;
  • 把含有print语句的一行代码删除;
  • session = device.attach(“com.droider.crackme0201”)修改为session = device.attach(“Crackme0201”)

3.2 FridaApp

使用Jadx打开FridaApp.apk文件后,进行常规分析,发现MainActivity中有5个按钮,点击后分别对应相应的事件响应。

我们先在AS模拟器上打开APP,不然会报错提示找不到这个程序进程。

3.2.1 Hook类的构造方法

这里Hook类Money的构造方法,在Hook之前点击HOOK构造方法的按钮后,会显示RMB: 100的字符串,Hook之后,会显示注入的美元: 10000的字符串,如图13所示。

图13 Hook构造方法

构造方法的固定写法是$init,具体的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
# HookConstructionMethod.py

import frida, sys

jscode = """
Java.perform(function () {
var money = Java.use('com.qianyu.fridaapp.Money');
money.$init.implementation = function (a, b) {
console.log("Hook Start...");
send(arguments[0]);
send(arguments[1]);
send("Success!");
return this.$init(10000, "美元");
}
});
"""

def message(message, data):
if message["type"] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)

process = frida.get_remote_device().attach('FridaApp')
script= process.create_script(jscode)
script.on("message", message)
script.load()
sys.stdin.read()

3.2.2 Hook类的普通方法

普通方法包括了静态方法、私有方法、公开方法等,这个操作和上面的构造方法其实很类似。这里Hook类Utils的普通方法,在Hook之前点击HOOK普通方法的按钮后,会显示7000的字符串,Hook之后,会显示注入的300的字符串,如图14所示。

图14 Hook普通方法

具体的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
# HookGeneralMethod.py

import frida, sys

jscode = """
Java.perform(function () {
var utils = Java.use('com.qianyu.fridaapp.Utils');
utils.getCalc.implementation = function (a, b) {
console.log("Hook Start...");
send(arguments[0]);
send(arguments[1]);
send("Success!");
console.log(this.getCalc(arguments[0], arguments[1]));
return this.getCalc(100, 200);
}
});
"""

def message(message, data):
if message["type"] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)

process = frida.get_remote_device().attach('FridaApp')
script= process.create_script(jscode)
script.on("message", message)
script.load()
sys.stdin.read()

3.2.3 Hook类的重载方法

重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。这里Hook类Utils的重载方法test(int),在Hook之前点击HOOK重载方法的按钮后,会显示迁羽老师:31928的字符串,Hook之后,会显示注入的qianyu的字符串,如图15所示。

图15 Hook重载方法

具体的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
# HookOverloadingMethod.py

import frida, sys

jscode = """
Java.perform(function () {
var utils = Java.use('com.qianyu.fridaapp.Utils');
utils.test.overload("int").implementation = function (a) {
console.log("Hook Start...");
send(arguments[0]);
send("Success!");
return "qianyu";
}
});
"""

def message(message, data):
if message["type"] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)

process = frida.get_remote_device().attach('FridaApp')
script= process.create_script(jscode)
script.on("message", message)
script.load()
sys.stdin.read()

3.2.4 Hook构造对象参数

这里Hook类Utils和Money的对象参数,在Hook之前点击HOOK构造对象参数的按钮后,会显示迁羽老师QQ:3192850648的字符串,Hook之后,会显示注入的迁羽老师: 800的字符串,如图16所示。

图16 Hook构造对象参数

具体的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
# HookConstructionObjectParameters.py

import frida, sys

jscode = """
Java.perform(function () {
var utils = Java.use('com.qianyu.fridaapp.Utils');
var money = Java.use('com.qianyu.fridaapp.Money');
utils.test.overload().implementation = function () {
send("Hook Start...");
var mon = money.$new(2000,'港币');
send(mon.getInfo());
return this.test(800);
}
});
"""

def message(message, data):
if message["type"] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)

process = frida.get_remote_device().attach('FridaApp')
script= process.create_script(jscode)
script.on("message", message)
script.load()
sys.stdin.read()

【注】构造一个对象其实很简单直接固定写法$new即可,然后有了对象也可以直接调用其对应的方法即可。

3.2.5 Hook构造对象属性

这里Hook类Utils、Money和Class的对象属性,在Hook之前点击HOOK构造对象属性的按钮后,会显示迁羽老师QQ:3192850648的字符串,Hook之后,会显示注入的迁羽老师: 10010的字符串,如图17所示。

图17 Hook对象属性

修改对象的字段值需要用反射去进行操作,具体的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
# HookConstructionObjectProperties.py

import frida, sys

jscode = """
Java.perform(function () {
var utils = Java.use('com.qianyu.fridaapp.Utils');
var money = Java.use('com.qianyu.fridaapp.Money');
var clazz = Java.use('java.lang.Class');
utils.test.overload().implementation = function () {
send("Hook Start...");
var mon = money.$new(200,'港币');
send(mon.getInfo());
var numid= Java.cast(mon.getClass(),clazz).getDeclaredField('num');
numid.setAccessible(true);
send(numid.get(mon));
numid.setInt(mon, 1000);
send(mon.getInfo());
return this.test(10010);
}
});
"""

def message(message, data):
if message["type"] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)

process = frida.get_remote_device().attach('FridaApp')
script= process.create_script(jscode)
script.on("message", message)
script.load()
sys.stdin.read()

到这里我们就把所有可能遇到的情形Java层hook操作都介绍完了,主要包括以下几种常见情形:

  • Hook类的构造方法和普通方法,注意构造方法是固定写法$init即可,获取参数可以通过自定义参数名也可以直接用系统隐含的arguments变量获取即可。
  • 修改方法的参数和返回值,直接调用原始方法传入需要修改的参数值和直接修改返回值即可。
  • 构造对象使用固定写法$new即可。
  • 如果需要修改对象的字段值需要用反射去进行操作。
  • 堆栈信息打印直接调用Java的Exception类即可,通过adb logcat -s AndroidRuntime来过滤日志信息查看崩溃堆栈信息。

3.3 CTF_100

安装并打开CTF_100.apk之后,发现题目为爬到指定的楼层然后才能显示FLAG,如图18所示。

图18 查看程序

使用Jadx反编译APK查看运行逻辑,MainActivity中的has_gone_int表示已经爬的楼数,to_reach_int表示需要爬的楼数,get_flag(int i)表示最终返回FLAG的方法在native层实现,因此我们就可以通过修改has_gone_int和to_reach_int的值满足条件,然后调用get_flag(int i)返回FLAG信息,具体的代码如下所示。

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
import frida, sys

jscode = """
Java.perform(function () {
var MainActivity = Java.use('com.ctf.test.ctf_100.MainActivity');
MainActivity.Btn_up_onclick.implementation = function (a) {
send("Hook Start...");
this.has_gone_int = 1
this.to_reach_int = 1
var flag = this.get_flag(1)
send(flag);
send("Hook End...");
}
});
"""

def message(message, data):
if message["type"] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)

process = frida.get_remote_device().attach('CTF_100')
script= process.create_script(jscode)
script.on("message", message)
script.load()
sys.stdin.read()

最后,frida注入成功,获得了flag,如图19所示。

图19 获得flag

4 Native层执行注入与Hook

4.1 系统电话

Frida除了基于Shell的注入方式实现Hook,还提供frida-trace命令实现跟踪式Hook。如图20所示,执行命令frida-trace -U -i open com.android.phone,Hook本地手机的系统电话的open()函数,执行成功后,会在终端输出系统电话程序调用open()函数的日志信息。

图20 Hook系统电话程序的open()函数

可看到在handlers/libc.so目录下生成了open.js。按Ctrl+C结束Frida运行,js文件如下面所示。

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
/*
* Auto-generated by Frida. Please modify to match the signature of open.
* This stub is currently auto-generated from manpages when available.
*
* For full API reference, see: https://frida.re/docs/javascript-api/
*/

{
/**
* Called synchronously when about to call open.
*
* @this {object} - Object allowing you to store state for use in onLeave.
* @param {function} log - Call this function with a string to be presented to the user.
* @param {array} args - Function arguments represented as an array of NativePointer objects.
* For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8.
* It is also possible to modify arguments by assigning a NativePointer object to an element of this array.
* @param {object} state - Object allowing you to keep state across function calls.
* Only one JavaScript function will execute at a time, so do not worry about race-conditions.
* However, do not use this to store function arguments across onEnter/onLeave, but instead
* use "this" which is an object for keeping state local to an invocation.
*/
onEnter(log, args, state) {
log('open()');
},

/**
* Called synchronously when about to return from open.
*
* See onEnter for details.
*
* @this {object} - Object allowing you to access state stored in onEnter.
* @param {function} log - Call this function with a string to be presented to the user.
* @param {NativePointer} retval - Return value represented as a NativePointer object.
* @param {object} state - Object allowing you to keep state across function calls.
*/
onLeave(log, retval, state) {
}
}

将参数信息添加到输出的信息中,主要就是修改onEnter()函数,将open.js文件修改如下:

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
/*
* Auto-generated by Frida. Please modify to match the signature of open.
* This stub is currently auto-generated from manpages when available.
*
* For full API reference, see: https://frida.re/docs/javascript-api/
*/

{
/**
* Called synchronously when about to call open.
*
* @this {object} - Object allowing you to store state for use in onLeave.
* @param {function} log - Call this function with a string to be presented to the user.
* @param {array} args - Function arguments represented as an array of NativePointer objects.
* For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8.
* It is also possible to modify arguments by assigning a NativePointer object to an element of this array.
* @param {object} state - Object allowing you to keep state across function calls.
* Only one JavaScript function will execute at a time, so do not worry about race-conditions.
* However, do not use this to store function arguments across onEnter/onLeave, but instead
* use "this" which is an object for keeping state local to an invocation.
*/
onEnter: function (log, args, state) {
log("open(" + "pathname=" + Memory.readUtf8String(args[0])+ ", flags=" + args[1] + ")");
},

/**
* Called synchronously when about to return from open.
*
* See onEnter for details.
*
* @this {object} - Object allowing you to access state stored in onEnter.
* @param {function} log - Call this function with a string to be presented to the user.
* @param {NativePointer} retval - Return value represented as a NativePointer object.
* @param {object} state - Object allowing you to keep state across function calls.
*/
onLeave(log, retval, state) {
}
}

再次执行命令frida-trace -U -i open com.android.phone,即可看到参数信息,如图21所示。

图21 修改open.js的onEnter()函数后运行命令

4.2 jisuanqi1

首先,在AS创建的arm架构的模拟器中运行jisuanqi1.apk,如图22所示。

图22 计算器

使用Jadx分析APK文件后,发现计算器实现的加减乘除都是在native层实现,进而使用IDA打开APK文件解压后的libjisuanqi.so文件,JNI_OnLoad中有Java层函数与C层函数的对应关系,另外在Exports中找到了加减乘除的相关实现,如图23所示。

图23 加减乘数函数在so层的实现

由图23可知,addc()函数的地址为0x00000D0C,因此需要在对应的代码位置处修改,如下所示:

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
# Hooklibjisuanqi.py

import frida, sys

jscode = """
setImmediate(function () {
send("start");
//遍历模块找基址
Process.enumerateModules({
onMatch: function (exp) {
if (exp.name == "libjisuanqi.so") {
send('enumerateModules find');
send(exp.name + "|" + exp.base + "|" + exp.size + "|" + exp.path);
send(exp);
return 'stop';
}
},
onComplete: function () {
send('enumerateModules stop');
}
});

//hook导出函数
var exports = Module.enumerateExportsSync("libjisuanqi.so");
for(var i=0;i<exports.length;i++){
send("name:"+exports[i].name+" address:"+exports[i].address);
}

//通过模块名直接查找基址
var baseSOFile = Module.findBaseAddress("libjisuanqi.so");
Interceptor.attach(baseSOFile.add(0x00000D0C),{
onEnter: function(args) {
console.log(args[2]);
console.log(args[3]);
},
onLeave: function(retval){
retval.replace(10)
console.log(retval);
}
});
});
"""

def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)

process = frida.get_usb_device().attach('jisuanqi1')
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
sys.stdin.read()

最后,打印输入的两个数100和20的IEEE 754存储形式0x42c80000和0x41a00000,如图24所示。

图24 打印输入的两个数

Native层代码Hook操作总结如下:

  • Hook导出的函数直接用so文件名和函数名即可

  • Hook未导出的函数需要计算出函数在内存中的绝对地址,通过查看maps文件获取so的基地址+函数的相对地址即可,最后不要忘了+1操作(是Arm指令还是Thumb指令)。

5 总结

本文主要介绍了如何在Android系统中注入与Hook操作,并讲解了这些技术的原理,演示了如何通过Hook与注入JavaScript和Python代码的方法改变Crackme0201程序的行为,也演示了跟踪系统电话程序的Native方法来实现跟踪式Hook。

接下来,准备向第十一章《软件保护技术》进发。

6 参考文献

[1]丰生强. Android软件安全权威指南[M]. 电子工业出版社, 2019.
[2]https://blog.csdn.net/freeking101/article/details/106965168


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,可以邮件至 xingshuaikun@163.com。

×

喜欢就点赞,疼爱就打赏