Frida逆向与协议分析

 起因是第七期分享会,有师傅推荐这本书,正好自己在frida脚本上很欠缺,所以买来看看。重点是实操,和C++反汇编不一样,别懒!

0x00 安卓逆向环境搭建

htop:动态查看当前活跃的,占用高的进程。

jnettop:查看下载速度与对应的 IP。

 解锁OEM时会出现waiting for any device的情况,解决办法。刷机过程见P9。adb devices只有进入系统时才能看到,fastboot devices只有进入bootloader时才能看到。

 Kali NetHunter刷机:针对移动设备的安卓渗透测试平台。它主要修改安卓内核的内容,这些修改对日常使用无影响。最后使用链接下载SuperSU-2.82。对于用NetHunter刷完机无法被虚拟机发现的情况,看链接

0x01 Frida Hook基础与快速定位

基础

frida基础

 两种操作模式:(1)CLI模式:通过命令行将js脚本注入到进程中。(2)RPC模式:通过py脚本间接完成js脚本的注入。本章重点关注CLI模式。

 两种操作App的方式:(1)spawn(调用)模式:将启动App的权利交给Frida来控制,即使目标App已启动,在使用Frida对程序进行注入时,还是会重新启动App并注入。frida -f就会调用spawn模式。(2)attach(附加)模式:在目标App已启动的前提下,利用ptrace原理注入程序进而完成Hook操作,默认以attach模式注入。

 注:ida attach调试程序时,无法以frida attach模式注入;但是当先进行frida attach模式注入后,可以使用ida attach调试(先后问题)。

 对于在App启动时执行的方法(so库的.init_array.init_proc),使用spawn方式hook。而对于频繁运行的函数,可以使用attach方式hook。

 frida连接手机的两种模式:(1)usb数据线:使用adb链接,加入-U参数即可。(2)网络模式:adb链接,frida-server在运行时使用-l监听ip与端口,主机上的frida通过-H参数指定手机的ip和端口。

frida hook基础

 xposed使用java代码编写hook模块后,需要重启后才能使hook代码运行起来。而frida只需在手机上运行frida-server,然后在主机上将js脚本注入即可。frida注入成功后,即使脚本被修改,hook效果也能及时生效。

 针对显示->主动显示的钩取如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// hello.js
function hook(){
Java.perform(function(){
// perform setting class
var settings = Java.use("com.android.settings.DisplaySettings");
// get class function
var getMetricsCategory_func = settings.getMetricsCategory.overload();
getMetricsCategory_func.implementation = function(){
// this --> setting class
// perform origin func
var result = this.getMetricsCategory();
console.log("getMetricsCategory called, result => ", result);
return result;
}
})
}
function main(){
hook();
}
setImmediate(main)

 之后运行:frida -U com.android.settings -l hello.js

 注:

(1)所有针对java层函数的hook脚本必须处于Java.perform的包装中。

(2)Java.use获取指定类的handle后,使用类似于调用类静态方法的方式获得对应函数(上述代码中的settings.getMetricsCategory)。如果函数存在多个重载,还需要添加.overload(<signature>)获取指定函数。

 如下所示,subString函数有两个重载:String substring(int)String substring(int, int)。我们只想要钩取substring(int),如下所示:

1
2
3
4
5
6
7
8
9
10
11
function hookSubString(){
Java.perform(function(){
var String = Java.use('java.lang.String')
var subString_int_func = String.substring.overload('int')
subString_int_func.implementation = function(index){
var result = this.substring(index)
console.log("substring called",'index =>',index,',result =>',result)
return result
}
})
}

objection基础

 frida提供了API供用户自定义使用,在此基础上可以实现很多具体功能。而objection则是将各种常用功能整合并可在命令行使用的利器。在安卓中使用objection,可以快速完成内存、类与模块搜索、方法hook、打印参数等。

 以movetv.apk为例,通过jadx查看AndroidManifest.xml,获取到包名为com.cz.babySister。安装到手机上,并启动frida-server,之后运行:

1
objection -g com.cz.babySister explore

 之后,就可以对进程进行操作:

(1)打印内存:可打印出内存中已加载类的相关信息,从而快速定位app中的关键类。

1
android hooking list classes // 列出进程加载过的类
1
android hooking search classes <pattern> // 列出加载的类名中包含<pattern>格式的类

image-20230926151600680

1
android hooking list class_methods <class_name> // 获取指定类中所有非构造函数的方法名

image-20230926152118857

(2)Hook函数:

 objection可以hook一个类中所有函数。

1
android hooking watch class <class_name> // 钩取某个类中所有非构造函数

image-20230926152440471

1
2
3
4
5
// hook 指定函数
// classMethod 为方法名
// option 支持的参数有:--dump-args(函数执行时打印参数内容),--dump-return(打印返回值),--dump-backtrace(打印调用栈)
// overload 表示具体是哪个函数(函数可能有重载),如果没指定那么默认为所有重载的函数
android hooking watch class_method <classMethod> <overload> <option>

 下图就默认hook了所有setForegroundColor重载函数:

image-20230926153419471

 如果要指定钩取参数为int数组的setForegroundColor函数,那么就写为:

1
android hooking watch class_method com.cz.babySister.view.Loading.setForegroundColor "[I" --dump-args --dump-return --dump-backtrace 

(3)jobs:hook任务管理,android heap:用于操作内存中类的实例。这些略。

Hook快速定位

 逆向分析时遵循hook->主动调用->RPC,这是什么?即,协议分析的流程为:(1)用hook的方式确定关键业务逻辑的位置;(2)通过主动调用实现关键业务逻辑的调用;(3)通过RPC远程过程调用(python+js)的方式进行关键业务逻辑的批量调用。frida将hook工作成功地从xposed模块每次编译都需要重启的循环中解放出来

关键类定位:基于trace的枚举

 DDMS(Dalvik Debug Monitor Server)是早期android studio中用于调试和监视 Android 设备和模拟器的工具。其中method trace功能可以获得一段时间内app执行的函数记录,但是,它有以下缺点:(1)要求app的debuggable为true。(2)会记录所有函数,包括系统函数。

 而frida无需处于debuggable状态,且支持跟踪指定类中的函数,支持跟踪特定类中的所有函数。在此节中,不直接编写frida脚本来进行trace,而是使用frida封装的可用于进行trace定位的工具。

objection trace

 以movetv.apk为例,首先搜索包含com.cz.babySister的类:

1
android hooking search classes com.cz.babySister

 将输出保存为文件hook,并在每一行开头加入android hooking watch class,表示钩取所有与com.cz.babySister.xxx类相关的函数,如下所示:

image-20230926155725845

 通过如下命令,使用objection对所有com.cz.babySister.xxx类相关的函数进行钩取:

1
objection -g com.cz.babySister explore -c hook.txt

 此时再触发我们所关心的业务逻辑,就能快速筛选出业务逻辑所在类的范围。

 注1:search classes是先获取所有已加载类,之后再筛选。因此,它可能无法涵盖应用中所有的类。因此,在hook之

前,要尽可能多的使用app,以求加载足够多的类。objection中trace的核心源码见P37。

 注2:如果app加壳,那么最好选择attach模式进行trace/hook。否则在应用还未启动时,真实的类还没有被释放。此时会报ClassNotFoundException错误,这是因为app的ClassLoader在运行时的切换问题所导致的。

ZenTracer

 具体见P39。

 总结一下,frida相比于DDMS,能够依据使用者的想法对执行函数进行跟踪,同时无需样本处于debuggable状态。但是,由于frida本身不太稳定,因此被测试的程序会经常崩溃。

关键类定位:基于内存的枚举

 当逆向者通过分析发现某些类可能是关键类时,可以通过对此关键类进行hook去验证分析的结果。相比于xposed,frida不仅不用重启,还支持对内存进程的漫游功能,即:能够通过Java.choose()在目标进程的java堆中寻找和修改已存在的java对象实例。这使得我们不仅能够对未执行的函数设置hook,还能对已经创建的实例进行操作

 以OkHttp中的hook抓包进行介绍。OkHttp的核心是拦截器。一个完整的网络请求被拆分为多个步骤,每个步骤都由拦截器完成。拦截器由okhttp3.OkHttpClient类中的List成员_interceptors数组管理,此数组包含Client中的所有拦截器。那么,如果写一个打印日志的LogInterceptor拦截器,并添加到_interceptors数组中,就可以完成对所有数据包的抓取。

 如何做?通过Java.choose从内存中找到okhttp3.OkHttpClient的实例对象,并修改_interceptors数组内容,使得此数据包含我们刚才写的LogInterceptor拦截器,从而完成数据的抓包。

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
// hookOkHttp3.js
function searchClient(){
Java.perform(function(){
var gson2 = Java.use("com.google.gson.Gson");

// load CurlInterceptor dex
Java.openClassFile("/data/local/tmp/muok2curl.dex").load();
console.log("loading dex successful");
const curlInterceptor = Java.use("com.moczul.ok2curl.CurlInterceptor");
const loggable = Java.use("com.moczul.ok2curl.logger.Loggable");
var Log = Java.use("android.util.Log");
var TAG = "okhttpGETcurl";

// register MyLogClass
var MyLogClass = Java.registerClass({
name: "okhttp3.MyLogClass",
implements: [loggable],
methods: {
log: function(MyMessage){
Log.v(TAG, MyMessage);
}
}
})
const mylog = MyLogClass.$new();
// get origin CurlInterceptor object
var curlInter = curlInterceptor.$new(mylog);

// load logging-interceptor.dex
Java.openClassFile("/data/local/tmp/okhttplogging.dex").load();
var MyInterceptor = Java.use("com.r0ysue.learnokhttp.okhttp3Logging");
var MyInterceptorObj = MyInterceptor.$new();
Java.choose("okhttp3.OkHttpClient", {
onMatch:function(instance){
console.log("1. find instance", instance)
console.log("2. instance.interceptors():", instance.interceptors().$className);
console.log("3. instance._interceptors:", instance._interceptors().value.$className);
console.log("5. interceptors:", Java.use("java.util.Arrays").toString(instance.interceptors().toArray()));
var newInter = Java.use("java.util.ArrayList").$new();
newInter.addAll(instance.interceptors());
console.log("6. interceptors:", Java.use("java.util.Arrays").toString(newInter.toArray()));
console.log("7. interceptors:", newInter.$className);
newInter.add(MyInterceptorObj);
newInter.add(curlInter);
instance._interceptors.value = newInter;
}, onComplete:function(){
console.log("Search complete");
}
})
})
}

setImmediate(searchClient)

 上述代码中,myok2curl.dex负责具体拦截器的实现,okhttplogging.dex负责具体拦截器的实现。这样做的目的是为了避免将Java翻译成js的复杂工作。Java.openClassFile("xx").load()用于导入dex文件,在内存中注册一个类,当然也可以使用Java.registerClass完成。

 注:objection的插件WallBreaker,可以快速定位类中所包含的属性与函数,也可以直接通过对象的句柄获取所在类中所有属性的值。其基本原理就是使用Java.choose对内存中对象进行搜索,然后通过对对象的句柄进行反射,从而获得相应成员与函数。

 frida也可以在native层进行hook。以脱壳为例,hexdump工具的核心原理是在目标进程的内存空间中遍历搜索包含DEX文件特征的数据,匹配成功则将dex dump出来,它主要使用了frida的Memory.scanSync函数。hexdump仅仅能够解决一代整体加固,对于二代抽取加固,有工具frida_fart。其通过hook app运行过程中的native函数,即LoadMethod函数(此函数处于加载与执行dex文件的ART虚拟机中),利用此函数的参数,可以获得加载的java函数所在的dex文件与函数代码。

0x02 Frida脚本开发之主动调用与RPC入门

 之前的内容讲了如何定位到算法的关键函数,之后我们就需要反复调用此函数,以进行调试,了解此函数真正的程序逻辑。但是如果按部就班的进行调用,函数传入的参数往往是变化的。因此,就需要主动调用,即:目标函数的参数是可控的。远程过程调用(RPC)(就是python脚本的frida)也是与主动调用一起使用的一种技术。

 之前说过Frida调试的3板斧:(1)Hook定位关键逻辑;(2)主动调用构造参数进行利用;(3)RPC导出结果进行规模化利用。Frida实际上是基于Python和JS的进程级Hook框架,其中JS承担Hook函数的工作,Python相当于提供给外界的绑定接口,使用者可通过Python将JS脚本注入到进程中。其中,官方提供了frida-python来进行远程过程调用。

python实现frida注入的基本知识

获取设备

1
2
3
4
5
6
7
8
import frida

# 获取 usb 链接设备
device = frida.get_usb_device()
print(device)
# 获取网络设备
device = frida.get_device_manager().add_remote_device('192.168.75.1:6666')
print(device)

image-20231008100259050

 获取到设备后,实现进程注入同样有spawn(重新启动app)与attach(附到已启动的app)。如下所示:

注入进程-两种方式

1
2
3
4
5
6
7
8
9
10
11
12
import time
import frida
# spawn 注入进程,注意此时 spawn 的参数是 list 类型
pid = device.spawn(["com.android.settings"])
# 唤醒进程
device.resume(pid)
# 等待进程被完全唤起
time.sleep(1)
session = device.attach(pid)

# attach 注入进程
session = device.attach("com.android.settings")

 成功注入进程后,之后要进行hook脚本的注入,实际上就是将js脚本作为字节流通过frida提供的api加载到相应进程的session中。

hook脚本注入

方式1:js字符串

1
2
3
4
5
6
7
8
9
10
# 读取 hook 脚本内容
script = session.create_script("""

setImmediate(Java.perform(function(){
console.log("hello python frida");
}))

""")
# 将脚本加载到进程空间中
script.load()

方式2:js文件

1
2
3
with open("hook.js") as f:
script = session.create_script(f.read())
script.load()

rpc学习

学习rpc实际上是学习js脚本与python进行交互的方式。rpc脚本示例如下,功能主要是打开settings,并调用两个函数:

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
# 导入 print 函数
from __future__ import print_function
from frida.core import Session

import frida
import 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()
# 获取 rpc 导出函数
api = script.exports
# 执行导出函数
print("api.hello() => ", api.hello())
print("api.fail_please() => ", api.fail_please())

 调用结果如下:

image-20231008141217217

 注意js脚本中带大写字母的导出函数变成了小写加下划线的形式,例如failPlease变为fail_please。可以这么说,rpc介绍了在python中主动调用js中函数的方式。下面再来介绍js主动向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
from __future__ import print_function

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

device = frida.get_usb_device()
pid = device.spawn(["com.android.settings"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)

print("attached")
# 注入分离响应函数,当停止 frida-server 时会调用下面 3 个函数
session.on('detached', on_detached)
session.on('detached', on_detached_with_reason)
session.on('detached', on_detached_with_varargs)
sys.stdin.read()

 上述两个脚本是frida-python给出的两个简单的例子,js还可以用过send函数向python脚本发送数据。frida-python还包括:(1)child_gating.py:子进程注入;(2)bytecode.py:将脚本编译为字节码然后再加载脚本;(3)inject_library文件夹:手动向进程中注入一个动态库。等等。

 这对于之前的ZenTracer项目,其真实用于Hook的代码只有traceClass函数,剩下的部分都是js与python之间的数据传递,主要是:通过send将js中的数据传输到python用于接收数据的FridaReceive函数中。

 注:frida中session.on、script.on、device.on的区别?

  • session.on:这个方法用于在Frida会话(Session)对象上注册事件处理程序。Frida会话是与目标应用程序通信的主要接口。通过session.on方法,可以注册多种类型的事件处理程序,如detached(会话与目标应用程序断开连接时触发)和message(接收到来自目标应用程序的消息时触发)等。
  • script.on:这个方法用于在Frida脚本(Script)对象上注册事件处理程序。Frida脚本是在目标应用程序中执行的用户定义代码。通过script.on方法,可以注册多种类型的事件处理程序,如message(接收到来自目标应用程序的消息时触发)和destroyed(脚本被销毁时触发)等。
  • device.on:这个方法用于在Frida设备(Device)对象上注册事件处理程序。Frida设备是与目标设备或模拟器通信的接口。通过device.on方法,可以注册多种类型的事件处理程序,如spawned(目标应用程序被启动时触发)和output(设备的输出消息时触发)等。

frida java层主动调用与rpc

 以demoso1工程为例,此工程中存在两个native函数,在应用打开后被循环调用。其中method01是静态函数,对输入进行加密并返回,method02是成员函数,对密文解密并返回。其java层声明如下:

image-20231008151416556

 注:如果native函数是静态注册的,那么so层最终生成的函数是以Java_开头的导出函数。然而此静态注册方式不安全,因此用RegisterNatives()函数对上述两个函数进行了动态注册,以加强安全性。注册过程如下:

image-20231008152338641

 运行日志如下:

image-20231008232457673

 首先使用上一章介绍的objection工具来注入目标进程,以确定method01与method02的签名与调用情况。

1
2
3
4
5
6
// 使用 objection 注入到进程
objection -g com.example.demoso1 explore
// 获取 com.example.demoso1.MainActivity 中所有非构造函数的方法名
android hooking list class_methods com.example.demoso1.MainActivity
// 钩取 com.example.demoso1.MainActivity 中所有非构造函数
android hooking watch class com.example.demoso1.MainActivity

 函数签名如下:

image-20231008234926412

 调用情况如下:

image-20231008235130276

 可以看出,此程序主动调用了method01。之后,我们就要写hook脚本,从而对此函数进行主动调用。主动调用最重要的是参数的构造,主动调用其实就是构造和hook时使用的参数类型一致的参数。首先,编写invoke.js如下:

1
2
3
4
5
6
7
8
9
10
11
12
function hook(){
Java.perform(function(){
var MainActivity = Java.use("com.example.demoso1.MainActivity");
MainActivity.method01.implementation = function(str){
// hook active call
var result = this.method01(str);
console.log("str =>", str);
console.log("result =>", result);
return result;
}
})
}

 当注入并调用Hook函数时,结果如下所示:

image-20231009115622012

 重写一下invoke.js,目的是调用method01函数,并对xcl进行加密:

1
2
3
4
5
6
7
8
9
10
function invokeMethod01(){
Java.perform(function(){
var MainActivity = Java.use("com.example.demoso1.MainActivity");
var javaString = Java.use("java.lang.String");
var plaintext = "xcl";
var result = MainActivity.method01(javaString.$new(plaintext));
console.log("plaintext =>", plaintext);
console.log("result =>", result);
})
}

 结果如下所示:

image-20231009120311213

 刚才的method01是静态函数,这类函数的主动调用只需要获得对应类的句柄。而对于一般的函数method02来说,这样调用就会出错,因为此函数需要通过实例进行调用,而不是一个类句柄。而之前学过,Java.choose()能够在内存中获取实例,因此对method02的主动调用脚本如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function invokeMethod02(){
Java.perform(function(){
Java.choose("com.example.demoso1.MainActivity",{
onMatch:function(instance){
var javaString = Java.use("java.lang.String")
var ciphertext = "0646275a2d4a4e00a36c8b909a405f25"
// instance is com.example.demoso1.MainActivity's instance
var result = instance.method02(javaString.$new(ciphertext))
console.log("ciphertext =>", ciphertext)
// result = xcl
console.log("result =>", result)
},
onComplete(){}
})
})
}

 结果如下所示:

image-20231009121456713

 测试好主动调用后,下一步就是rpc(python脚本调用js中的函数)。此时需要将主动调用提供为外部接口,需要两步:

(1)将主动调用参数配置为js函数的参数并将主动调用的结果返回,以方便外部自定义参数进行主动调用,更改后的代码如下所示:

image-20231009122014774

image-20231009134401539

(2)将这两个主动调用的函数导出,最终export.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
// export.js
rpc.exports = {
method01:invokeMethod01,
method02:invokeMethod02
}

function invokeMethod01(plaintext){
var result;
Java.perform(function(){
var MainActivity = Java.use("com.example.demoso1.MainActivity");
var javaString = Java.use("java.lang.String");
result = MainActivity.method01(javaString.$new(plaintext));
console.log("plaintext =>", plaintext);
console.log("result =>", result);
})
return result;
}

function invokeMethod02(ciphertext){
var result;
Java.perform(function(){
Java.choose("com.example.demoso1.MainActivity",{
onMatch:function(instance){
var javaString = Java.use("java.lang.String")
// instance is com.example.demoso1.MainActivity's instance
result = instance.method02(javaString.$new(ciphertext))
console.log("ciphertext =>", ciphertext)
console.log("result =>", result)
},
onComplete(){}
})
})
return result
}

 到此,rpc中js部分完成,此时再写一个Python外部调用的脚本invoke.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
27
28
29
30
31
32
# invoke.py
import time
import frida
import json

# define error handler
def my_message_handler(message, payload):
print(message)
print(payload)

# connect to frida-server
device = frida.get_usb_device()

# run demo01
pid = device.spawn(["com.example.demoso1"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)

# load script
with open("export.js") as f:
script = session.create_script(f.read())

# call message handler
script.on("message", my_message_handler)
script.load()
# get export function list
api = script.exports

ciphertext = api.method01("xcl")
print("method01 => encode_result: " + ciphertext)
print("method02 => decode_result: " + api.method02(ciphertext))

 运行结果如下所示:

image-20231009135957027

 再丰富一下上述代码,使其能够通过浏览器访问批量调用:

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
# invoke_flask.py
from flask import Flask, request
import json
app = Flask(__name__)

import time
import frida
import json

# define error handler
def my_message_handler(message, payload):
print(message)
print(payload)

# connect to frida-server
device = frida.get_usb_device()

# run demo01
pid = device.spawn(["com.example.demoso1"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)

# load script
with open("export.js") as f:
script = session.create_script(f.read())

# call message handler
script.on("message", my_message_handler)
script.load()

# data encrypt
@app.route('/encrypt', methods=['POST'])
def encrypt_class():
data = request.get_data()
json_data = json.loads(data.decode('utf-8'))
postdata = json_data.get("data")
res = script.exports.method01(postdata)
return res

# data decrypt
@app.route('/decrypt', methods=['POST'])
def decrypt_class():
data = request.get_data()
json_data = json.loads(data.decode('utf-8'))
postdata = json_data.get("data")
res = script.exports.method02(postdata)
return res

if __name__ == "__main__":
app.run()

 启动之后,运行:

1
curl -X POST http://127.0.0.1:5000/encrypt -H "{Content-Type:application/json}" -d '{"data":"xcl"}'

 注:-X指定http请求的方法,-H指定http请求的头部,-d指数据体。

 运行结果如下:

image-20231009142820042

 如果进一步想要将rpc变成批量化集群调用,则可以部署到服务器上。但是需要考虑手机性能、frida版本的稳定性、网络状况等。使用siege压力测试工具进行测试:

1
siege -c10 r100 "http://127.0.0.1:5000/encrypt POST < encrypt.json"
1
2
// encrypt.json
{"data":"xcl"}

 注:-c10代表并发数为10,-r100代表重复次数为100。结果如下所示:

image-20231009150640971

 这个速率还可以。对于method02而言,由于其中有Java.choose(),且此操作非常耗时,因此会降低method02调用的速度。为什么此操作耗时?因为每次调用Java.choose都会在内存中重新搜索实例。

 我们改进如下:将对象始终保存在程序的外部全局变量中,这样每次调用method02的时候不至于重新寻找。

image-20231009151522330

 但是这利用了app的特殊性,即app不会退出(一直循环调用method01与method02),且调用的函数处于MainActivity中,即只要app不退出,MainActivity就会一直存在于内存中。那么,如果在其他app中,对象被系统进行垃圾回收了怎么办?这就需要下一节知识:Native层的主动调用。

frida native层函数的主动调用

 虽然method01与method02分别是static与成员函数,但是他们都是native层的。这样的话,我们可以直接在native层进行主动调用,而无需考虑对象是否被回收的问题。Ok,那我们要完成native层的主动调用。继续三板斧。

(1)找到java函数在native层对应的函数符号,并以此找到函数地址。此时分为两种情况:

 (a)静态注册的jni函数,其函数符号为Java_<类名>_<函数名>,例如stringFromJNI,此时hook脚本为:

1
2
3
4
5
6
7
8
9
10
11
function hookmethod(){
var method01 = Module.findExportByName('libnative-lib.so', 'Java_com_example_demoso1_MainActivity_stringFromJNI')
Interceptor.attach(method01, {
onEnter:function(args){
// do something
},
onLeave:function(retval){
// do something
}
})
}

 如果不确定函数的符号,可以通过Objection来查看导出的函数列表:

  • 查看此时运行的app名称:

image-20231009153432891

  • 使用 objection 注入到进程。
1
objection -g com.example.demoso1 explore
  • 列出libnative-lib.so模块中的导出函数。
1
2
// 列出so模块中的导出函数
memory list exports libnative-lib.so

image-20231009154612817

 (b)使用RegisterNatives来进行动态注册的函数,例如method01与method02,可以使用frida_hook_libart项目中的hook_RegisterNatives.js脚本获取动态注册后函数所在的地址(注意由于实验版本的frida版本低,所以将脚本中的let改为var)。

1
frida -U -f com.example.demoso1 -l hook_RegisterNatives.js --no-pause

image-20231009160118020

 可以发现method01的名称改为:_Z8method01P7_JNIEnvP7_jclassP8_jstring

(2)写Hook脚本,并改写为主动调用。可以写hook脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hook_native_method(method_name){
var m = Module.findExportByName('libnative-lib.so', method_name)
Interceptor.attach(m, {
onEnter:function(args){
// JNIEnv*
console.log("args[0] =>", args[0])
// jclass
console.log("args[1] =>", args[1])
// jni function args
console.log("args[2] =>", Java.vm.getEnv().getStringUtfChars(args[2], null).readCString())
},
onLeave:function(retval){
console.log("result =>", Java.vm.getEnv().getStringUtfChars(retval, null).readCString())
}
})
}
function hookmethod01(){
hook_native_method("_Z8method01P7_JNIEnvP7_jclassP8_jstring");
}

 结果如下:

image-20231009161635251

 实际上,书中使用frida_hook_libart项目时只给出了method01在libnative-lib.so中的偏移量offset,此时脚本改为:

1
2
3
4
5
6
7
8
9
10
function hook_native_method(addr){
Interceptor.attach(addr, {
...
})
}
function hookmethod01(){
var base = Module.findBaseAddress('libnative-lib.so');
var method01_addr = base.add(offset)
hook_native_method(method01_addr);
}

 注:为什么要用Java.vm.getEnv().getStringUtfChars?这是因为Java中的string参数到native层变成了Jstring对象

 上述脚本并没有涉及函数的主动调用,因为在脚本中并没有主动调用原来的函数。如果主动调用函数的话,应该是这样:

image-20231009163204785

 因此,将上述脚本修改如下,这样程序调用method01时,就会执行替换后的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hook_native_method(method_name){
var m = Module.findExportByName('libnative-lib.so', method_name)
// args is 3 pointer, ret is 1 pointer
var addr_func = new NativeFunction(m, 'pointer', ['pointer', 'pointer', 'pointer'])

Interceptor.replace(m, new NativeCallback(function(arg1, arg2, arg3){
// active call
var result = addr_func(arg1, arg2, arg3)
console.log("arg3 =>", Java.vm.getEnv().getStringUtfChars(arg3, null).readCString())
console.log("result =>", Java.vm.getEnv().getStringUtfChars(result, null).readCString())
return result
},
'pointer',
['pointer', 'pointer', 'pointer']
))
}
function hookmethod01(){
hook_native_method("_Z8method01P7_JNIEnvP7_jclassP8_jstring");
}

 可改写为针对method01的主动调用,这样就可以主动调用native层的method01(最终形态):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function hook_native_method(method_name, data){
var result = null
var m = Module.findExportByName('libnative-lib.so', method_name)
// args is 3 pointer, ret is 1 pointer
var addr_func = new NativeFunction(m, 'pointer', ['pointer', 'pointer', 'pointer'])

Java.perform(function(){
var env = Java.vm.getEnv()
console.log("data =>", data)
var jstring = env.newStringUtf(data)
// 要转为 jstring 才行
result = addr_func(env, ptr(1), jstring)
result = env.getStringUtfChars(result, null)
})
return result
}
function hookmethod01(){
var result = hook_native_method("_Z8method01P7_JNIEnvP7_jclassP8_jstring", "xcl")
console.log('result =>', result)
}

 注:(1)JNI函数(native层的函数),第一个参数一定是JNIEnv的指针;第二个参数为jclass或jobject,分别表示jni函数在Java层中是静态还是动态函数。由于method01中没有使用第二个参数,因此可以任意传递相同类型的数据,这里使用ptr(1)构造了一个指针。(2)由于函数主动调用时使用了Java.vm.getEnv这个API,因此需要包裹在java.perform()中。

 执行结果如下:

image-20231009170414179

(3)对native函数进行导出,并配置rpc,前面说过,因此不再赘述。

native函数调用的另类之法

 以method01为例,此法脱离特定apk加载模块并调用native的固有模式。

(1)将apk解压,并将method01所在模块libnative-lib.so导出到/data/app目录下(必须要放到这个目录下),并赋予权限。

image-20231009173346328

(2)编写主动调用脚本,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function invoke_func(addr,contents){
var result = null;
var func = new NativeFunction(addr,'pointer',['pointer','pointer','pointer']);
Java.perform(function(){
var env = Java.vm.getEnv();
console.log("content is ",contents)
var jstring = env.newStringUtf(contents)
result = func(env,ptr(1),jstring)
result = env.getStringUtfChars(result, null)
})
return result;
}

function invoke_method01(){
var method_name = "_Z8method01P7_JNIEnvP7_jclassP8_jstring"
var m = Module.findExportByName('/data/app/libnative-lib.so', method_name)
var result = invoke_func(m, 'xcl')
console.log("result =>", result.readCString())
}

注:frida-server仅在python脚本下需要手动启动,在命令行下会自动启动(有时也需要手动),objection使用时也需要手动启动frida-server。

 此时注入任何应用都会并调用invoke_method01,都会成功,如下所示(注入了com.android.settings):

image-20231009182516774

 注:真实的情况是,脱离app进行模块函数调用时会发生各种问题,例如签名校验,native层调用Java函数等,这个时候此方法就行不通了。

 总结:此章的作用就是非常便捷的模拟app中的某些函数,且仅需要知道函数的参数与返回值。

留言

© 2024 wd-z711

⬆︎TOP