Objection笔记
2025-05-16 11:30:33 # Tools

一、确认要hook的apk

我使用的环境是真机环境,使用USB连接手机。安装的Objection的版本为1.11.0。这里注意,真机的frida-server的版本要和你电脑的frida版本一致,我是用的是15.2.2frida

使用frida-ps -U找到自己要查看的包名,可能是com.x2n.xxx的形式也可能是中文的名字。

image-20250321235259345

找到自己的包,然后使用objection -g com.x2n.xxx explore进入到REPL模式。这里com.x2n.xxx为中文也可,中文时我使用""进行包裹。我使用的1.11.0objection支持中文,其它版本我不确定。

image-20250321235804595

二、常用的命令

进入到Objection REPL界面[1]中,当不知道命令时通过按空格就会提示可用的命令。在出现提示后通过上下选择键及回车键便可以输入 命令。

image-20250322000716481

下面以手机自带的com.android.phone为例,开始介绍正式的使用命令。

1、help命令

当不知道当前命令的效果是什么时,在当前命令前加上help(比如help env)再回车之后就会出现当前命令的解释信息。

image-20250323165012672

2、jobs命令

作业系统很好用,用于查看和管理当前所执行 Hook的任务,建议一定要掌握,可以同时运行多项Hook作业。

3、内存漫游相关指令

Objection可以快速便捷地打印出内存中的各种类的相关信息,这对App快速定位有着无可比拟的优势,下 面介绍几个常用命令。

(1)列出内存中的所有类

命令android hooking list classes

image-20250323173058671

一共有22110个类。

(2)在内存中所有已加载的类中搜索包含特定关键词的类

android hooking search classes display

这里搜索一下包含display关键词的类。

image-20250323173440288

(3)从内存中搜索所有包含关键词key的方法。

android hooking search methods <key>

从上文中可以发现,内存中已加载的类高达22110个。它们的方法是类的个数的数倍,整个过程会相当耗时。

这里展示搜索包含display关键词的方法。

android hooking search methods display

image-20250323174515328

(4)查看类的所有方法

android hooking list class_methods <className>

随意找到一个类com.android.internal.telephony.DisplayInfoControl,结果如下:

image-20250323181548491

三、获取四大组件相关内容

Android基础中的基础就是Android的四大组件,即活动 (Activity) 、服务(Service) 、广播接收器(Broadcast Receiver)以及内容提供者(Content Provider)。

Activity可以理解为界面,一个Activity就是一个界面;Service 相当于Windows上的一个后台进程;Broadcast Receiver用于响应来自其他应用程序或者系统的广播消息;Content Provider用于进程间的交互,通常通过请求从一个应用程序向其他应用程序提供数据。

第二部分介绍都是最基础的一些Java类相关的内容。在Android中,四大组件的相关内容是非常值得关注的,Objection在这方面也提供了支持,下面介绍一下。

1、列出进程所有的activity

android hooking list activities

image-20250323182721262

2、列出进程所有的service

android hooking list services

结果如下:

image-20250323182756596

需要列出其他两个组件的信息时,只要将对应的地方更换为receiversproviders即可,这里不再演示。

四、Hook相关命令

作为Frida的核心功能,Hook总是不能绕过的。同样地,Objection作为Frida优秀的开发工具,Hook相关的 命令是一定要实现的。事实上,Objection在这方面的表现确实令人称赞。

1、对指定的方法进行Hook

android hooking watch class_method <methodName>

这里选择对JavaFile类的构造函数进行Hook,结果如下:

android hooking watch class_method java.io.File.$init --dump-args --dump-backtrace --dump-return

image-20250323192008745

在上述命令中 , 我们加上了--dump-args--dumpbacktrace--dump-return三个参数,分别用于打印函数的参数、 调用栈以及返回值。这三个参数对逆向分析的帮助是非常大的:有些函数的明文和密文非常有可能放在参数和返回值中,而打印调用栈可以让分析者快速进行调用链的溯源。

另外需要注意的是,此时虽然只确定了Hook构造函数,但是默认会Hook 对应方法的所有重载 。 同时,在输出的最后一行显示Registering job 561713,这表示这个Hook被作为一个“作业”添加到Objection的作业系统中了,此时运行job list命令可以查看到这个“作业”的相关信息,如下图。可以发现这里的Job ID对应的是561713,同时Hooks对应的6正是Hook的函数的数量。

image-20250323192539602

当我在com.android.phone对应的"电话"中进行操作时,会发现java.io.File.File(java.io.File, java.lang.String)这一个函数被调用了。在Backtrace之后打印的调用栈中,可以清楚地看到这个构造函数的调用来源,如下图。

注意,调用栈的顺序是从下至上的,根据Arguments那一行会发现打开的文件路径是/data/user_de/0/com.android.phone/files,文件名为persist_atoms.pb。虽然Return Value后打印的返回值为none,表明这个函数没有返回值,但是也是真实地打印了返回值。当然,读者也可以Hook其他函数以打印返回值进行测试。

image-20250323193322876

测试结束后,可以根据"作业"的ID来删除"作业",取消对这些函数的Hook,最终执行结果如下:

jobs kill <id>

image-20250323195814075

2、对指定类中所有函数的Hook

除了可以直接Hook一个函数之外,Objection还可以通过执行命令实现对指定类classname中所有函数的Hook**(这里的所有函 数并不包括构造函数的Hook)**。

android hooking watch class <classname>

同样以java.io.File类为例,最终执行效果如下:

image-20250323201138858

一共Hook56个函数,输出结果如下:

image-20250323201237383

最终Hook的效果如下。当然,这里的调用顺序(自上而下)和之前的调用栈的打印是不同的。

image-20250323203121588

五、Objection的主动调用

主动调用:android heap相关命令。最后介绍Frida的一大特色——主动调用在Objection中的使用。

1、实例搜索(基于Java.choose实现)

基于最简单的Java.choose的实现,在Frida脚本中,对实例的搜索在Objection中是使用以下命令实现的:

android heap search instances <classname>

这里仍以java.io.File类为例,搜索到很多File的实例,并且打印出对应的HandletoString()的结果。

下图中显示的Hashcode十分重要,在之后的主动调用中都是以这个值作为实例的句柄来调用和执行函数。

image-20250323215320587

2、Objection中调用实例方法的两种方法

第一种:使用execute

android heap execute <Hashcode> <methodname>

这里的实例方法指的是没有参数的实例方法。下面演示一下使用Hashcode值为31598268所对应的实例来执行FilegetPath方法。

image-20250323221934874

使用execute执行带参函数会报错,如下图:

image-20250323222724278

第二种:使用evaluate

如果要执行带参数的函数,则需要先执行以下命令:

android heap evaluate <Hashcode>

在进入一个迷你编辑器环境后,输入想要执行的脚本内容,确认编辑完成,然后按Esc键退出编辑器,最后按回车键,即会开始执行这行脚本并输出结果。这里的脚本内容和在编辑器中直接编写的脚本内容是一样的(使用File类的canWrite()函数和setWritable()函数进 行测试)。

具体实现的代码如下:

1
2
3
console.log(clazz.canWrite())
clazz.setWritable(false)
console.log(clazz.canWrite())

在这个脚本中 , Objection 设定clazz用于代表Hashcode值为31598268所对应的实例,同时函数canWrite()用于返回这个实例所打 开的文件是否可写。setWritable()函数用于修改对应文件是否可写的属性。脚本的编辑页面和最终的执行效果如下图。其中的TrueFalse属于输出的结果。

image-20250323223947608

heap evaluate既可以执行有参函数,也可以执行无参函数,这里不再演示,留待读者自行研究。

六、Frida开发思想🌟

在介绍完FridaObjection后,将在这一节中提出一个在逆向过程中常用的工作思路,通常将之称为“Frida三板斧”。

1、定位:Objection辅助定位

经过前面的学习,我们发现Objection在逆向过程中最强大的功能其实是从海量的代码中快速定位关键的程序逻辑Frida 需要每次手动编写代码去Hook从静态分析到的函数,进而观察其参数和返回值是否与需求相符,Objection将常用的一些功能集成在一 起,使得逆向开发和分析人员在分析过程中不需要浪费精力在编写代码上。

下面以样例程序Junior.apk为例(样本来自于《 Android Studio开发实战:从零基础到App上线(第2版)》一书中的Junior样例,源代码在这里[2]apk文件在这里[3]

使用adb install -t junior.apk命令将Junior.apk安装并启动后,首先使用Objection遍历一下App的所有activity(活动), 如下图所示。

android hooking list activities

image-20250323231643853

在安装和遍历App的所有activity后,我们会发现整个App总共有 17个activity,为了方便讲解,这里选择分析的目标activity为计算器的相关活动com.example.junior.CalculatorActivity ,并尝试使用如下命令去启动这个活动。

1
2
3
4
com.example.junior on (google: 13) [usb] # android intent launch_activity com.example.junio
r.CalculatorActivity
(agent) Starting activity com.example.junior.CalculatorActivity...
(agent) Activity successfully asked to start.

观察手机页面,会发现activity被成功启动了,最终手机显示计算器的页面。

这里选取减法作为我们的分析目标,在计算器成功启动后,从源 码地址下载对应android2源码并直接查看这个activity的源码:切换 到 android2工程下 , 打开对应的junior/src/main/java/com/example/junior/CalculatorActivity.java文件,从这个文件中的onCreate()函数可以 看到整个活动注册了很多控件的点击事件。

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
// onCreate函数
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_calculator);
// 从布局文件中获取名叫tv_result的文本视图
tv_result = findViewById(R.id.tv_result);
// 设置tv_result内部文本的移动方式为滚动形式
tv_result.setMovementMethod(new ScrollingMovementMethod());
// 下面给每个按钮控件都注册了点击监听器
findViewById(R.id.btn_cancel).setOnClickListener(this); // “取消”按钮
findViewById(R.id.btn_divide).setOnClickListener(this); // “除法”按钮
findViewById(R.id.btn_multiply).setOnClickListener(this); // “乘法”按钮
findViewById(R.id.btn_clear).setOnClickListener(this); // “清除”按钮
findViewById(R.id.btn_seven).setOnClickListener(this); // 数字7
findViewById(R.id.btn_eight).setOnClickListener(this); // 数字8
findViewById(R.id.btn_nine).setOnClickListener(this); // 数字9
findViewById(R.id.btn_plus).setOnClickListener(this); // “加法”按钮
findViewById(R.id.btn_four).setOnClickListener(this); // 数字4
findViewById(R.id.btn_five).setOnClickListener(this); // 数字5
findViewById(R.id.btn_six).setOnClickListener(this); // 数字6
findViewById(R.id.btn_minus).setOnClickListener(this); // “减法”按钮
findViewById(R.id.btn_one).setOnClickListener(this); // 数字1
findViewById(R.id.btn_two).setOnClickListener(this); // 数字2
findViewById(R.id.btn_three).setOnClickListener(this); // 数字3
findViewById(R.id.btn_zero).setOnClickListener(this); // 数字0
findViewById(R.id.btn_dot).setOnClickListener(this); // “小数点”按钮
findViewById(R.id.btn_equal).setOnClickListener(this); // “等号”按钮
findViewById(R.id.ib_sqrt).setOnClickListener(this); // “开平方”按钮
}

随便测试这个计算器之后会发现,每次按“等号”按钮后计算结果都会被打印出来。根据这一现象,找到对应“等号”按钮的idbtn_equal,并根据这个id找到对应的点击响应函数onClick函数中属于“等号”按钮的源码部分,最终的源码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//onClick函数
@Override
public void onClick(View v) {
int resid = v.getId(); // 获得当前按钮的编号
...
} else if (resid == R.id.btn_equal) { // 点击了等号按钮
if (operator.length() == 0 || operator.equals("=")) {
Toast.makeText(this, "请输入运算符", Toast.LENGTH_SHORT).show();
return;
} else if (nextNum.length() <= 0) {
Toast.makeText(this, "请输入数字", Toast.LENGTH_SHORT).show();
return;
}
if (caculate()) { // 计算成功,则显示计算结果
operator = inputText;
showText = showText + "=" + result;
tv_result.setText(showText);
} else { // 计算失败,则直接返回
return;
}
}
...
}

onClick函数中可以发现最终真实的点击“等号”按钮后的主要代码在caculate()函数中。接下来就是验证我们想法的时候了:为了防止源码和真实运行代码不同,先使用以下命令验证是否存在caculate()函数。

1
2
3
4
5
6
7
8
com.example.junior on (google: 13) [usb] # android hooking list class_methods com.example.j
unior.CalculatorActivity
private boolean com.example.junior.CalculatorActivity.caculate()
private void com.example.junior.CalculatorActivity.clear(java.lang.String)
protected void com.example.junior.CalculatorActivity.onCreate(android.os.Bundle)
public void com.example.junior.CalculatorActivity.onClick(android.view.View)

Found 4 method(s)

上面的执行结果说明caculate()函数确实是存在的。

接下来就很明显了,使用如下命令Hook这个函数来确认在点击 “等号”按钮后这个函数被调用了。在Hook上后,任意输入一个表达式并点击“等号”按钮,会发现这个函数在点击“等号”按钮后被调用,Hook结果如下。

1
2
3
4
5
com.example.junior on (google: 13) [usb] # android hooking watch class_method com.example
.junior.CalculatorActivity.caculate --dump-args --dump-backtrace --dump-return
(agent) Attempting to watch class com.example.junior.CalculatorActivity and method caculate.
(agent) Hooking com.example.junior.CalculatorActivity.caculate()
(agent) Registering job 658110. Type: watch-method for: com.example.junior.CalculatorActivity.caculate

image-20250324145106214

查找代码,找到了其中的的caculate()函数。

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
// 开始加减乘除四则运算,计算成功则返回true,计算失败则返回false
private boolean caculate() {
if (operator.equals("+")) { // 当前是相加运算
result = String.valueOf(Arith.add(firstNum, nextNum));
} else if (operator.equals("-")) { // 当前是相减运算
result = String.valueOf(Arith.sub(firstNum, nextNum));
} else if (operator.equals("×")) { // 当前是相乘运算
result = String.valueOf(Arith.mul(firstNum, nextNum));
} else if (operator.equals("÷")) { // 当前是相除运算
if (Double.parseDouble(nextNum) == 0) { // 发现除数是0
// 除数为0,要弹窗提示用户
Toast.makeText(this, "除数不能为零", Toast.LENGTH_SHORT).show();
// 返回false表示运算失败
return false;
} else { // 除数非0,则进行正常的除法运算
result = String.valueOf(Arith.div(firstNum, nextNum));
}
}
// 把运算结果打印到日志中
Log.d(TAG, "result=" + result);
firstNum = result;
nextNum = "";
// 返回true表示运算成功
return true;
}

在这个函数中,对减法的处理是通过调用Arith类中的sub()函数来实现的。为了验证Arith类在内存中是真实存在的,我们通常使用以下Objection命令来获取一个应用在内存中的所有类。

# android hooking list classes

通常,在运行这行命令后会列出很多类,甚至会超过整个Terminal缓存空间,这时会出现一些类被缓存冲刷掉的情况,如果只是简单地在终端窗口里查找,那么不一定能找到。其实Objection本身有一个log文件,用于记录objection运行时产生的所有数据。这个日志数据存放在~/.objection目录下的objection.log文件中。

解决方法:在运行objection注入App之前,首先切换到~/.objection目录下,将之前的objection.log文件删除或者改名。如下所示:

image-20250324150259874

在删除这个log文件后重新注入App,并重新遍历应用在内存中的所有类,命令如下:

1
2
3
4
# 注入应用
objection -g junior explore
# 进入到objection的命令窗口,遍历内存中的所有类
android hooking list classes

在遍历完成后退出Objection注入模式以确保log文件刷新成功, 并重新通过cat命令查看这个objection.log文件,由于log文件过大,因此还需要配合grep命令过滤文本,从而通过观察结果是否有输出来判定内存中是否存在目标类Arith,如下图所示。

image-20250324151645694

在判定内存中确实存在Arith类后,我们进一步通过Objection命令判断Arith类是否存在sub()函数。如下图所示。

image-20250324152011009

在内存中确定这个函数存在后,便可以使用如下命令对这个函数进行Hook了。

1
android hooking watch class_method com.example.junior.util.Arith.sub --dump-args --dump-backtrace --dump-return

最终确认这个简单计算器的减法是通过sub(java.lang.String, java.lang.String)实现的。

image-20250324233129462

这里我使用了计算器的减法时,发现没能触发hook,这里hook失败了。

然后我使用同样的方法尝试了其它的App[4],成功hook,可能是我下载的junior来源有点问题。

1
android hooking watch class_method com.zj.wuaipojie.Demo.a --dump-args --dump-backtrace --dump-return

image-20250324234444822

2、利用:Frida脚本修改参数

前面我们确认了Arith类的函数sub(java.lang.String, java.lang.String)是最终计算器减法的真实执行函数。这里尝试使用frida的脚本去进行hook这个sub函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function main(){
Java.perform(function() {
var Arith = Java.use('com.example.junior.util.Arith');
// Hook Arith.sub() 方法
Arith.sub.overload('java.lang.String', 'java.lang.String').implementation = function(str1, str2) {
var result = this.sub(str1, str2);
console.log('str1,str2,result =>',str1,str2,result);
//打印Java调用栈
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
return result;
};
});
}
setImmediate(main)

仍然是hook失败的,进行减法操作无任何反应。

image-20250324235914989

我们知道,Frida脚本中Java函数的主动调用(区分静态函数和实例函数)。如果是静态函数,只需要获取类对象即可直接完成函数的主动调用;如果是实例函数,只需要优先获取到类的实例对象即可完成函数的主动调用。

主动调用的代码如下:

1
2
3
4
5
6
7
8
9
function main(){
Java.perform(function(){
var Arith = Java.use('com.example.junior.util.Arith')
var JavaString = Java.use('java.lang.String');
var reuslt = Arith.sub(JavaString.$new('123'),JavaString.$new('112'));
console.log(reuslt);
});
}
setImmediate(main)

成功调用。

image-20250325000255731

主动调用可以,但是被动调用就不行,这里可能是App的问题。

深入到Arith.java代码[5]中,发现sub函数的实现使用了BigDecimal函数。

image-20250325001453566

尝试去hook这个代码,发现成功hook,这里不清楚为什么。

image-20250325001629404

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
function main(){
Java.perform(function(){
var BigDecimal = Java.use("java.math.BigDecimal");
// 构造函数 hook(验证参数)
// BigDecimal.$init.overloads.forEach(function(overload){
// overload.implementation = function(){
// console.log("[Hooked BigDecimal constructor] called:", overload.toString());
// for(var i=0; i<arguments.length; i++){
// console.log(" arg["+i+"]: " + arguments[i]);
// }
// return overload.apply(this, arguments);
// };
// });
// subtract 方法 hook(捕捉核心运算)
BigDecimal.subtract.overload("java.math.BigDecimal").implementation = function(b2){
console.log("[Hooked BigDecimal.subtract] called");
console.log(" self:", this.toString());
console.log(" arg :", b2.toString());
var result = this.subtract(b2);
console.log(" result:", result.toString());
return result;
};
});
}
setImmediate(main);

参考


  1. 《安卓Frida逆向与抓包实战》 陈佳林/著 ↩︎

  2. https://github.com/aqi00/android2 ↩︎

  3. https://github.com/x2nn/android_security/blob/main/junior.apk ↩︎

  4. https://github.com/x2nn/android_security/blob/main/woaipojie.apk ↩︎

  5. https://github.com/aqi00/android2/blob/master/junior/src/main/java/com/example/junior/util/Arith.java#L3 ↩︎