Frida操作大全

Frida Labs全解

Frida labs

参数解释

1
2
3
4
5
-U 指定USB设备
-f 指定app包名,通过spawn方式启动(就是重新启动的意思)
-P 指定APP的pid
--no-pause 不暂停
-l 加载hook脚本

0x1 Hook修改被调用的方法的逻辑,返回值,传入参数

使用随机数,对输入进行判断,如果输入数的满足一个表达式,就会打印出flag

image

于是这里就有两种思路了。第一种是 hook get_random 函数,让该函数的返回值变成一个固定的值,从而计算表达式的值

第二种思路是 hook check 函数,直接将我们自己定义的值传入进去

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 hook() {

var MainActivity = Java.use("com.ad2001.frida0x1.MainActivity"); //使用 Frida 的 Java.use() 方法获取目标类 MainActivity 的引用。
MainActivity.get_random.implementation = function () { //implementation 在 Frida 中是一个关键属性,用于覆盖原始 Java 方法的实现
return 0;
}
}


function hook2() {
var MainActivity = Java.use("com.ad2001.frida0x1.MainActivity");
MainActivity.check.overload('int', 'int').implementation = function (a, b) { //overload用于指定参数,由于Java的方法可以重载,所以需要指定
console.log("Origin i and i2 = ", a, b); //console.log打印
return this.check(3, b); //我们自己将 i 的值定为 3 传入进去
}
}

function main() {

Java.perform(function () {
hook2();
})
}

setImmediate(main);

注入

1
frida -U -f com.ad2001.frida0x1 -l hook.js

-U -f 缺一不可

为什么

缺少 -f

1
2
3
4
5
6
7
8
9
10
11
12
13
C:\Users\29660\Desktop\Frida\Frida-Labs-main\Frida-Labs-main\Frida 0x1>frida -U com.ad2001.frida0x1 -l hook.js
____
/ _ | Frida 16.4.10 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to Android Emulator 5554 (id=emulator-5554)
Failed to spawn: unable to find process with name 'com.ad2001.frida0x1'

Failed to spawn: unable to find process with name ‘com.ad2001.frida0x1’表示没有Frida 找不到名为 com.ad2001.frida0x1 的进程,详见lab02

缺少-U

1
2
3
4
5
6
7
8
9
10
11
12
13
C:\Users\29660\Desktop\Frida\Frida-Labs-main\Frida-Labs-main\Frida 0x1>frida -f com.ad2001.frida0x1 -l hook.js
____
/ _ | Frida 16.4.10 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to Local System (id=local)
Failed to spawn: unable to find executable at 'com.ad2001.frida0x1'

说明 Frida 没有连接到 Android 模拟器,而是连接到了你本地电脑(Windows),所以它以为是要在本地运行一个叫 com.ad2001.frida0x1 的可执行文件。

0x2 Hook调用静态的未被调用的方法

可以看到这里定义了一个 get_flag 方法,但是并没有在主函数中被调用

image

使用 frida 直接调用未被调用的静态方法

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
setImmediate(function () {

Java.perform(function () {

var a = Java.use("com.ad2001.frida0x2.MainActivity")
a.get_flag(4919);

})

})



function hook() {
var MainActivity = Java.use("com.ad2001.frida0x2.MainActivity");
MainActivity.get_flag(4919);
}

function main() {
Java.perform(function () {
hook();
})
}

setImmediate(main);

hook的时候使用如下命令有可能会hook不上

1
frida -U -f com.ad2001.frida0x2 -l hook.js

原因是setImmediate(main);是程序启动后立即注入,可能会导致程序还没完全启动就注入了,所以hook不上

解决办法是使用

1
frida -U 'frida 0x2' -l hook.js

那么,为什么将frida 0x2写成包名com.ad2001.frida0x2就不行呢

1
frida -U com.ad2001.frida0x2 -l hook.js

image

这个命令尝试的是:根据包名注入,默认是 spawn(启动)模式

实际上这条命令是尝试 spawn(启动)包名对应的 app,但没写 -f,它又不是 attach 模式,就会失败。

或者将hook代码中的

setImmediate(main);改成

setTimeout(main,1000);延迟启动

1
2
3
4
5
6
7
8
9
10
11
12
13
function hook() {
var MainActivity = Java.use("com.ad2001.frida0x2.MainActivity");
MainActivity.get_flag(4919);
}

function main() {
Java.perform(function () {
hook();
})
}

// setImmediate(main);
setTimeout(main, 1000);

0x3 更改类中的静态变量

hook code的值

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function hook() {

var a = Java.use("com.ad2001.frida0x3.Checker") //code的值在Checker类里
a.code.value = 512; //使用这一句修改静态变量的值
}

function main() {

Java.perform(function () {
hook();
})
}

setImmediate(main);

思考:

a是什么

a.code.value具体是什么意思

以下内容来自GPT

在这段 Frida 脚本中:

1
var a = Java.use("com.ad2001.frida0x3.Checker")

a 是什么?

👉 a 是一个 Frida 创建的 Java 类代理对象(Java class wrapper) ,你可以把它当成是一个“控制器”或者“代理”,用来访问和操作 Java 中的类 com.ad2001.frida0x3.Checker 的静态字段、静态方法、实例方法,甚至可以 hook 它的方法。


类比一下:

在 Java 代码中你可能会这样用一个类:

1
Checker.code = 512;

在 Frida 中,为了达到类似的效果,我们写:

1
2
var a = Java.use("com.ad2001.frida0x3.Checker")
a.code.value = 512;

更具体地说:

  • Java.use("...") 会返回一个 Frida 的 Java 类接口(wrapper)

  • 这个对象不是类的一个实例,而是一个可以操作 类本身(静态成员)或创建实例 的对象。

  • 如果你需要创建类的实例,你可以这样写:

    1
    var instance = a.$new();  // 等于 Java 里的 new Checker()

所以总结一下:

名称 解释
a Frida 中的类包装对象,表示Checker
a.code.value 表示访问并修改类中code这个静态字段的值
a.$new() (可选)表示创建这个类的一个新实例

0x4 调用非MainActivity,非静态方法

MainActivity里没有任何东西

image

定义了一个Check类,里面有get_flag方法

image

想调用这个get_flag方法,必须实例化一个check类对象,在frida中可以用以下脚本实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hook() {

var c = Java.use("com.ad2001.frida0x4.Check");
var c_check = c.$new(); //实例化check类对象 相当于Java里写 c c_check = new c();

var flag = c_check.get_flag(1337);
console.log(flag);
}

function main() {

Java.perform(function () {

hook();
})
}

setImmediate(main);

运行结果如下

image

0x5 调用MainActivity中的非静态方法

在 0x2 中,我们使用frida调用了静态方法

image

但在本次,这是一个非静态方法

image

Java中静态方法可以直接调用,而非静态方法必须实例化一个对象之后才能调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Hello {

// 静态方法
public static void sayStatic() {
System.out.println("我是静态方法");
}

// 非静态方法
public void sayInstance() {
System.out.println("我是非静态方法");
}

public static void main(String[] args) {
// 调用静态方法:不需要创建对象
Hello.sayStatic();

// 调用非静态方法:必须先创建对象
Hello h = new Hello();
h.sayInstance();
}
}

我们直接用 0x2 的脚本hook一下

1
2
3
4
5
6
7
8
9
10
11
12
function hook() {
var MainActivity = Java.use("com.ad2001.frida0x5.MainActivity");
MainActivity.flag(1337);
}

function main() {
Java.perform(function () {
hook();
})
}

setImmediate(main);

哦豁,报错了

image

以下解释来自SWDD师傅

直接使用Frida创建MainActivity或任何Android组件可能会很棘手,因为Android的生命周期和线程规则。Android组件,如Activity子类,依赖于应用程序上下文进行正确运行。在Frida中,您可能缺少必要的上下文。Android UI组件通常需要具有关联Looper的特定线程。如果涉及UI任务,请确保在具有活动Looper的主线程上执行。活动是较大的Android应用程序生命周期的一部分。创建MainActivity的实例可能需要应用处于特定状态,并且通过Frida管理整个生命周期可能并不直接。总之,为MainActivity创建实例并不是一个好主意。

通俗点来说就是,

用Frida直接创建 MainActivity 或其他 Android 组件(比如 Activity)是很麻烦的,甚至是不可行的。

原因是这样的:

  1. Activity 需要上下文(Context)才能正常工作:就像你造房子要有地基一样,Activity 这种组件运行的时候,需要依赖应用程序提供的运行环境(就是 Context)。在 Frida 里,你是“旁观者”,而不是“房子的主人”,所以没法轻松提供这个环境。
  2. UI 相关的代码必须在主线程(带 Looper 的线程)里跑:Android 要求所有跟界面有关的操作都得在“主线程”里运行,这个线程有个叫 Looper 的消息循环系统。在 Frida 里,如果你随便在哪个线程里跑 UI 代码,程序很可能直接崩掉。
  3. Activity 的生命周期很复杂:系统会自动帮 Activity 调用一系列方法,比如 onCreate、onStart、onResume 等。这一整套流程是系统控制的。如果你自己用 Frida 去“强行”创建一个 Activity 实例,系统其实并不知道它存在,所以它无法正常工作。

那么应该怎么办呢

SWDD说

当Android应用程序启动时,系统会创建MainActivity的一个实例(或AndroidManifest.xml文件中指定的启动器活动)。创建MainActivity实例是Android应用程序生命周期的一部分。因此,我们可以使用frida获取MainActivity的实例,然后调用flag()方法来获取我们的标志。

让我们看看GPT怎么说


📱 正常来说,Android 应用是怎么启动的?

  1. 当你点击某个 App 图标时,
  2. Android 系统就会去看这个 App 的 AndroidManifest.xml 文件,
  3. 找到标注为「启动入口」的 Activity(比如 MainActivity),
  4. 然后系统会自动创建这个 Activity 的实例,并调用它的一系列生命周期方法(比如 onCreate()onResume() 等)。

🧠 那这段话的核心意思就是:

你自己不用 new 一个 MainActivity,系统已经帮你创建好了!

所以与其自己创建一个可能不完整的实例(会有很多问题),不如直接去“拿到”系统已经创建的那个实例,然后调用你想要的函数,比如 flag()

✅ 总结一下,通俗点说就是:

“Android 系统会自动帮你创建 MainActivity 的实例,所以你不需要自己造一个。用 Frida 可以直接找到这个已经存在的实例,然后调用你想要的方法(比如 flag())来拿 flag。”

有这样一个模板

1
2
3
4
5
6
7
8
Java.performNow(function() {
Java.choose('<包名>.<类名>', {
onMatch: function(instance) {
// 待办事项
},
onComplete: function() {}
});
});
  • onMatch

    • onMatch回调函数在Java.choose操作期间找到指定类的每个实例时执行。
    • 这个回调函数接收当前实例作为它的参数。
    • 您可以在onMatch回调中定义自定义操作,以在每个实例上执行。
    • function(instance) {}instance参数表示目标类的每个匹配实例。您可以使用任何其他名称。参数 instance 就是这个 MainActivity 的对象,你可以在这里调用它的函数,比如 instance.flag()
  • onComplete

    • onComplete回调在Java.choose操作完成后执行操作或清理任务。此块是可选的,如果您在搜索完成后不需要执行任何特定操作,则可以选择将其留空。

尝试使用该模板寻找MainActivity实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function hook() {

Java.choose('com.ad2001.frida0x5.MainActivity', {

onMatch: function (instance) {
console.log("Sucess found!!!");
},
onComplete: function () {
console.log("end");
}
});
}

function main() {
Java.perform(function () {
hook();
})
}

setImmediate(main);

image

在这里还发现个问题,当使用如下命令启动frida时,只会打印一个 end ,hook不上,需要重新修改一下脚本(例如注释掉某一行再加上)

1
frida -U -f com.ad2001.frida0x5 -l lab05.js

image

原因其实跟 0x2 一样,由于脚本是立即注入,app还来不及创建MainActivity实例,自然就没找到

解决办法也跟 0x2 一样,这里就不再赘述

0x6 MainActivity中的非静态方法且参数通过非MainActivity非静态方法传递

MainActivity中的非静态方法 get_flag ,并且参数是通过 Checker 类对象 A 来传递的

image

Checker类如下

image

所以这题就是0x5 0x4 0x3的结合,但是在 0x3 中,变量为静态变量,所以需要这里需要创建一个类的对象来对变量进行更改

image

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
function hook() {

Java.choose("com.ad2001.frida0x6.MainActivity", {

onMatch: function (MainActivity) {

var c = Java.use("com.ad2001.frida0x6.Checker")
var A = c.$new();

A.num1.value = 1234;
A.num2.value = 4321;

console.log("success");
MainActivity.get_flag(A);
},
onComplete: function () {
console.log("end");
}
});

}

function main() {

Java.perform(function () {
hook();
})
}

// setImmediate(main);
setTimeout(main, 1000);

0x7 Hook构造函数

构造函数是 Java 类中的一种特殊方法,它的作用是:在创建对象的时候用来初始化对象的属性

先举个例子来了解一下森莫是Java中的构造函数

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
public class Dog {
String name;
int age;

// 构造函数
public Dog(String n, int a) {
name = n;
age = a;
}

// 普通方法
public void bark() {
System.out.println(name + " 汪汪叫!");
}

public void showInfo() {
System.out.println("这只狗叫 " + name + ",它 " + age + " 岁了。");
}

public static void main(String[] args) {
Dog d = new Dog("小黑", 3); // 调用构造函数,初始化对象 Dog(第一个)👉 表示变量 d 的 类型,也就是类名,说明 d 是一个 Dog 类型的对象。
//new Dog("小黑", 3)(后面这个)👉 是 调用构造函数,创建一个新的 Dog 对象,并传入两个参数。
d.bark(); // 调用普通方法
d.showInfo(); // 调用普通方法
}
}

来看本题,调用了flag方法,并且通过Checker构造函数对 num1 和 num2 赋值为 123 和 321

image

image

在Frida中使用 $init 关键字来 hook 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function hook() {

var Checker = Java.use("com.ad2001.frida0x7.Checker");
Checker.$init.implementation = function (a, b) {

// Checker.$init 是这个类的 构造函数(constructor)。
// .implementation = function (a, b) 表示你要“劫持”这个构造函数,用你自己的实现来替代原来的。
// a, b 就是原本传给构造函数的两个参数。

console.log("Origin num", a, b);
this.$init(666, 666); //this.$init(...) 表示“用原本的方式继续初始化对象,但用我给的参数”
console.log("success");
}
}

function main() {
Java.perform(function () {
hook();
})
}
setImmediate(main);

image

写到这我有点好奇如果使用 0x1 的方法直接去 hook 构造函数会怎样

来吧,实践出真知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function hook() {
var Ch = Java.use("com.ad2001.frida0x7.Checker");
Ch.Checker.overload('int', 'int').implementation = function (a, b) {
console.log("Origin num", a, b); //console.log打印
return this.Checker(666, 666);
}
}

function main() {
Java.perform(function () {
hook();
})
}
setImmediate(main);

哈哈,报错了,cannot read property ‘overload’ of undefined 说明试图 hook 的 Java 方法并没有找到

image

然后我又好奇,如果使用 0x3 的方法直接改变 num1 和 num2 会怎样

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function hook() {

var Checker = Java.use("com.ad2001.frida0x7.Checker");
Checker.$init.implementation = function (a, b) {

// Checker.$init 是这个类的 构造函数(constructor)。
// .implementation = function (a, b) 表示你要“劫持”这个构造函数,用你自己的实现来替代原来的。
// a, b 就是原本传给构造函数的两个参数。

console.log("Origin num", a, b);
// this.$init(666, 666); //this.$init(...) 表示“用原本的方式继续初始化对象,但用我给的参数”
Checker.num1.value = 666;
Checker.num2.value = 666;
console.log("success");
}
}

function main() {
Java.perform(function () {
hook();
})
}
setImmediate(main);

哈哈,果然又报错了 Error: Cannot access an instance field without an instance 表示试图访问 Checker.num1,但这是 实例字段(成员变量) ,你必须通过一个具体对象(也就是 this)来访问它

image

改成这样或许会比初代脚本好理解一些,跟 0x1 也更像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function hook() {

var Checker = Java.use("com.ad2001.frida0x7.Checker");
Checker.$init.overload('int', 'int').implementation = function (a, b) {
console.log("Origin num:", a, b);

// 调用原始构造函数初始化对象
this.$init(a, b);

// 修改成员变量
this.num1.value = 666;
this.num2.value = 666;

console.log("Success: num1 and num2 changed to 666");
};
}

function main() {
Java.perform(function () {
hook();
})
}
setImmediate(main);

0x8 Hook Native层中调用的函数并且读取传入的参数

从本章开始进入 Native层

模板如下

1
2
3
4
5
6
7
8
9
10
Interceptor.attach(targetAddress, {
onEnter: function (args) {
console.log('Entering ' + functionName);
// Modify or log arguments if needed
},
onLeave: function (retval) {
console.log('Leaving ' + functionName);
// Modify or log return value if needed
}
});

它的作用是:

当程序运行到某个特定函数(地址为 targetAddress)时:

  • 函数刚进入时(onEnter) :打印一句话,说“我进来了”;
  • 函数执行完返回时(onLeave) :再打印一句“我出去了”。

每行的意思

1
Interceptor.attach(targetAddress, {

用 Frida 的 Interceptor.attach拦截(hook)某个函数地址。这个地址是你想观察或修改的函数位置(targetAddress)。

1
2
3
4
onEnter: function (args) {
console.log('Entering ' + functionName);
// Modify or log arguments if needed
},

当程序刚进入这个函数时,会执行 onEnter 这个回调。
你可以:

  • 打印日志(像现在这样打印“正在进入函数”);

  • 查看和修改传入参数(args 是参数列表)。

1
2
3
4
onLeave: function (retval) {
console.log('Leaving ' + functionName);
// Modify or log return value if needed
}

当函数执行完准备返回时,会执行 onLeave
你可以:

  • 打印返回值;
  • 修改返回值(retval 是返回值)。

需要获取targetAddress我们可以使用如下API

  1. Module.enumerateExports()
    通过调用 Module.enumerateExports(),我们可以获取到导出函数的名称、地址以及其他相关信息。这些信息对于进行函数挂钩、函数跟踪或者调用其他函数都非常有用。
  2. Module.getExportByName()
    当我们知道要查找的导出项的名称但不知道其地址时,可以使用 Module.getExportByName()。通过提供导出项的名称作为参数,这个函数会返回与该名称对应的导出项的地址。
  3. Module.findExportByName()
    这与 Module.getExportByName() 是一样的。唯一的区别在于,如果未找到导出项,Module.getExportByName() 会引发异常,而 Module.findExportByName() 如果未找到导出项则返回 null
  4. Module.getBaseAddress()
    通过调用 Module.getBaseAddress() 函数,我们可以获取指定模块的基址地址,然后可以基于这个基址地址进行偏移计算,以定位模块内部的特定函数、变量或者数据结构
  5. Module.enumerateImports()
    通过调用 Module.enumerateImports() 函数,我们可以获取到指定模块导入的外部函数或变量的名称、地址以及其他相关信息。

使用 Module.enumerateImports(“libfrida0x8.so”) 查看导入表

image

使用Module.findExportByName(“libc.so”,”strcmp”);来获取strcmp的地址

image

来看这题

加载了一个 frida0x8

image

so文件里有一个 strcmp 函数,第二个参数 s2 就是经过处理后的正确的 flag ,所以我们需要把strcmp函数的第二个参数hook出来

image

我先按照我自己的想法写了一个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function hook() {

var target = Module.findExportByName("libc.so", "strcmp"); //查找strcmp的地址
console.log("strcmp addr is :", target.toString(16));

Interceptor.attach(target, {
onEnter: function (args) {
console.log(Memory.readUtf8String(args[1])); //打印strcmp的第二个参数
},

onLeave: function (retval) {

}
})
}


function main() {
Java.perform(function () {
hook();
})
}
setImmediate(main);

输出如下

image

这里其实是因为,我以为我hook的是特定的某一个strcmp,实际上这个 hook 是生效于所有调用 libc.so!strcmp 的地方,无论是 native 调用还是通过 JNI 被 Java 调用的 native 方法,都会被拦截

所以导致了这种情况

所以要加一个判断,当strcmp的第一个参数包含某个数(例如666)时,就打印第二个参数的结果,使我们的目标更加“精确”

修改脚本如下

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
function hook() {

var target = Module.findExportByName("libc.so", "strcmp"); //查找strcmp的地址
console.log("strcmp addr is :", target.toString(16));

Interceptor.attach(target, {
onEnter: function (args) { //args只是我对参数起的名字,可以改成任何别的

var input = Memory.readUtf8String(args[0]);

if (input.includes("666")) {
console.log(Memory.readUtf8String(args[1])); //打印strcmp的第二个参数
}

},

onLeave: function (retval) {

}
})
}


function main() {
Java.perform(function () {
hook();
})
}
setImmediate(main);

在 Frida 中:

  • Memory 是 Frida 的一个全局对象,提供了访问目标进程内存的方法。

  • 它可以用来:

    • 读取内存(如 Memory.readUtf8String(ptr)Memory.readByteArray(ptr, size)
    • 写入内存(如 Memory.writeUtf8String(ptr, "hello")

你用的 Memory.readUtf8String() 是它最常用的函数之一,专门处理 C 字符串(以 null 结尾的 UTF-8 字符串)。

image

0x9 Hook Native层函数的返回值

check_flag的值如果是1337就打印flag

image

native层中的该函数的返回值是1

image

修改该函数的返回值即可

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 hook() {

var target = Module.enumerateExports("liba0x9.so")[0]["address"];

console.log("addr is :", target);

Interceptor.attach(target, {
onEnter: function (args) {
},

onLeave: function (retval) {

console.log("Origin retval is :", retval);
retval.replace(1337);
}
})
}


function main() {
Java.perform(function () {
hook();
})
}
setImmediate(main);

思考:

这一句

1
var target = Module.enumerateExports("liba0x9.so")[0]["address"];

在 0x8 中使用的是

1
var target = Module.findExportByName("libc.so", "strcmp");

我的猜想是,在 0x8 中,我们使用

Module.enumerateImports(“libfrida0x8.so”)查看导入表,而在该题目中,由于hook的是自己的函数,所以需要使用Module.enumerateExports(“liba0x9.so”)查看导出表

  • 导入表(Imports)
    是这个库 调用别人的函数,通常是系统函数,比如 libc.so 里的 mallocprintfstrlen 等。
  • 导出表(Exports)
    是这个库 暴露给别人用的函数,很多时候是你自己写的,比如 Java_com_example_myapp_NativeMethod 这种 native 接口,或者一些插件调用的入口函数。

查看导出表

image

尝试改成跟 0x8 中差不多的写法

1
var target = Module.findExportByName("liba0x9.so", "Java_com_ad2001_a0x9_MainActivity_check_1flag");

好吧这样也可以,实践出真知了

image

还是解释一下这一句吧

1
var target = Module.enumerateExports("liba0x9.so")[0]["address"];
  • Module.enumerateExports("liba0x9.so")
    这会列出 liba0x9.so 这个 native 库里导出的所有函数(也就是那些对外开放可以调用的函数),返回一个包含信息的数组,比如:

    1
    2
    3
    4
    5
    [
    {name: "check_flag", address: ptr("0x12345678"), type: "function"},
    {name: "init", address: ptr("0x12345690"), type: "function"},
    ...
    ]
  • [0]["address"]
    表示取这个导出函数列表里的第一个函数的地址,也就是 check_flag 的地址(假设第一个刚好是它)。这一行把这个地址赋值给 check_flag 变量。[0]表示索引

0xA Hook Native层未被调用的方法

从本章开始使用模拟器会闪退,这里使用的环境是Redmi K60 安卓14 已ROOT

可以使用如下模板

1
2
3
var native_adr = new NativePointer(<address_of_the_native_function>);
const native_function = new NativeFunction(native_adr, '<return type>', ['argument_data_type']);
native_function(<arguments>);

让我们来逐行解释一下

1
var native_adr = new NativePointer(<address_of_the_native_function>);

这一句的意思是,我们知道一个原生函数在内存中的地址(比如 0x12345678),把它封装成一个 NativePointer 类型的对象,好让 Frida 能识别这个地址。<address_of_the_native_function> 是手动填进去的地址,通常是通过逆向分析(IDA、Frida hook 等)找到的函数地址。

1
const native_function = new NativeFunction(native_adr, '<return type>', ['argument_data_type']);

这一行的意思是,把这个地址(native_adr)对应的函数转换成 JavaScript 能调用的函数。

  • <return type> 表示这个函数的返回值类型,比如 'int''void''pointer' 等。
  • ['argument_data_type'] 表示这个函数的参数类型列表,比如 ['int', 'pointer']

所以这一行的作用是:用 Frida 的 NativeFunction 包装原生函数,让你可以像普通 JS 函数那样去调用它。

1
native_function(<arguments>);

最后直接调用这个原生函数,传入你需要的参数(这些参数要和上面声明的参数类型匹配)

现在来看看例题

Java层并不能看到什么有用的,只是加载了stringFromJNI()

image

我们来看看native层,stringFromJNI也没什么有用的信息,但是有一个get_flag函数可以打印flag

image

image

于是思路就是,利用上面的模板 hook 出get_flag函数

模仿 0x9 中使用 Module.enumerateExports(“libfrida0xa.so”) 查看导出表,发现导出表非常之长,直接使用var target = Module.enumerateExports(“liba0x9.so”)[0][“address”];进行索引会有点麻烦,所以我们换一种方法

image

由于ALSR(地址随机化)的问题,所以我们先使用Module.findBaseAddress("libfrida0xa.so");查看一下基地址

image

可以构造如下循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var base_addr = Module.findBaseAddress("libfrida0xa.so");
var exports = Module.enumerateExports("libfrida0xa.so");

var target = null;

for (var i = 0; exports[i] != null; i++){

if (exports[i]["name"] == "_Z8get_flagii") {

console.log("function get_flag : ", exports[i]["address"]);
console.log((exports[i]["address"] - base_addr).toString(16));

target = exports[i]["address"];

}

}

这里我们找到基地址后,遍历导出表,如果在导出表中找到了 get_flag 函数,就打印出 get_flag 函数的绝对地址

同时由于我们知道了绝对地址和基地址,于是我们就可以顺便把偏移地址也打印出来

偏移 = 函数地址 - 模块基地址

这样我们就找到了我们需要 hook 的目标函数的地址

这里还有个问题,为什么 get_flag 要写成 “_Z8get_flagii” 呢

这其实是C++ 的 name mangling(名字改编 / 名字重整)问题

什么是 Name Mangling?

C++ 中,函数名会被编译器自动改写成一串带有额外信息的名字,称为 mangled name(重整名字)

这是因为 C++ 支持函数重载(同名函数,不同参数) ,而汇编语言、链接器、操作系统等底层东西并不支持这一特性。

举个例子:

1
2
int add(int a, int b);
float add(float a, float b);

这两个函数在 C++ 中是合法的,因为它们参数不同。但如果你不做 name mangling,它们在汇编/符号表里都叫 add,系统就不知道你到底想调用哪个。

所以编译器会把它们改名:

1
2
int add(int, int)       --> _Z3addii
float add(float, float) --> _Z3addff

这就是 name mangling。

由于我们主要研究frida 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
function hook() {

var base_addr = Module.findBaseAddress("libfrida0xa.so");
var exports = Module.enumerateExports("libfrida0xa.so");

var target = null;

for (var i = 0; exports[i] != null; i++){

if (exports[i]["name"] == "_Z8get_flagii") {

console.log("function get_flag : ", exports[i]["address"]);
console.log("base addr is:", (exports[i]["address"] - base_addr).toString(16));

target = exports[i]["address"];

}

}

var get_flag_ptr = new NativePointer(target);
const My_get_flag = new NativeFunction(get_flag_ptr, 'char', ['int', 'int']);


var flag = My_get_flag(1, 2);
console.log(flag);


}

function main() {
Java.perform(function () {
hook();
})
}

setImmediate(main)

image

由于打印出的是函数的返回值,而且so文件中使用的函数是__android_log_print,所以flag在logcat中可以看到

image

思考:能否直接在frida面板中打印出flag?

有的兄弟,有的

还记得lab0x8的内容吗,我们可以Hook __android_log_print这个函数,直接截获 flag

image

flag的内容是__android_log_print的第四个参数,所以我们直接读取它的第四个参数然后打印出来就行

完整hook并打印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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function hook() {

var base_addr = Module.findBaseAddress("libfrida0xa.so");
var exports = Module.enumerateExports("libfrida0xa.so");

var target = null;

for (var i = 0; exports[i] != null; i++){

if (exports[i]["name"] == "_Z8get_flagii") {

console.log("function get_flag : ", exports[i]["address"]);
console.log("base addr is:", (exports[i]["address"] - base_addr).toString(16));

target = exports[i]["address"];

}

}

var get_flag_ptr = new NativePointer(target);
const My_get_flag = new NativeFunction(get_flag_ptr, 'char', ['int', 'int']);

var print_addr = Module.findExportByName(null, "__android_log_print"); //hook __android_log_print函数
Interceptor.attach(print_addr, {

onEnter: function (args) {

var flagPtr = args[3];
var flag = flagPtr.readCString();
console.log(flag); //读取并打印flag内容
},

onLeave: function (retval) {

}
});

var flag = My_get_flag(1, 2);
console.log(flag);


}

function main() {
Java.perform(function () {
hook();
})
}

setImmediate(main)

0xB 更改Native层的汇编指令

先来看x86下的模板

1
2
3
4
5
6
7
8
9
10
var writer = new X86Writer(opcodeaddr);
Memory.protect(opcodeaddr, 0x1000, "rwx");
try {

writer.flush();

} finally {

writer.dispose();
}

我们来解释一下

1
var writer = new X86Writer(opcodeaddr);
  • 创建了一个写指令的“写手”对象,叫 writer
  • X86Writer 是 Frida 提供的一个类,用来向指定内存地址写入 x86 汇编指令。
  • opcodeaddr 是你想修改的地址,比如某个函数开头或者中间的指令地址。
1
emory.protect(opcodeaddr, 0x1000, "rwx");
  • 把从 opcodeaddr 开始的一段内存(大小为 0x1000 字节,也就是一页)设置为可读(r)、可写(w)、可执行(x)。
  • 默认情况下,程序的代码区域一般是只读的;要想修改它,就得先把它“解锁”。
1
try { writer.flush(); }

try块中我们可以插入要修改/添加的x86指令。X86Writer实例提供了各种方法来插入各种x86指令。

writer.flush();

  • 插入指令后,调用flush方法将更改应用到内存中。这确保修改后的指令被写入内存位置。
1
finally { writer.dispose(); }

最后无论是否出错,都调用 dispose() 来释放资源。否则可能会造成内存泄漏

对于x86而言,我们可以查阅如下文档

JavaScript API | Frida • A world-class dynamic instrumentation toolkit

image

对于arm64而言,我们可以查阅如下文档

JavaScript API | Frida • A world-class dynamic instrumentation toolkit

image

接下来让我们看看例题,使用的架构为arm64架构

Java层依旧是什么都没有

image

native层中的MainActivity伪代码界面什么都没有,很明显这不正常

image

看看控制流,发现这里构成了一个永假跳转指令,导致我们的IDA反编译出错

image

那么,我们直接nop掉这个B.NE即可

IDA查看偏移地址是在0x15248处

image

开始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
function hook() {

var base_addr = Module.getBaseAddress("libfrida0xb.so");

console.log("Base address : ", base_addr);
var BNE_addr = base_addr.add(0x15248);

Memory.protect(base_addr, 0x1000, "rwx");

var writer = new Arm64Writer(BNE_addr);

try {
writer.putNop();
writer.flush();
console.log("Success!!");
} finally {
writer.dispose();
}
}

function main() {
Java.perform(function () {
hook();
})
}

setTimeout(main, 1000);

image

image

同样是需要到logcat中查看flag

image