Frida 练习——Frida-labs

这个项目可以从看 frida 相关视频过渡到实战,没有 ctf 这么难,感觉不看 frida 相关视频直接打这个练习也行。

项目地址: https://github.com/DERE-ad2001/Frida-Labs/tree/main


frida_0x1

从应用的界面看这是要你输入一个数字

01

查看源代码,很容易的看出这是要你找到这个随机数 i

02

下面的声明的 obj 是要在 app 界面中输入的数字

然后把存有随机数 i 与输入的数字 obj 传到 check 函数中进行检查

image-20250108143145970

在 check 函数中发现输入的 obj 也就是 check 函数声明的 i2

判断条件是(i*2)+4==i2

所以我们要输入的 obj 应该是随机数 i*2+4

这里有两个解法:


解法一:hook get_random

这里我们可以直接 hook 掉 get_random 来查看 get_random 生成的随机数

也可以直接返回一个特定值然后按照计算式输入,但是随机数是在程序初始化生成的,所以要在程序初始化时进行注入。

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

查看 get_random exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function hookTest1() {
var utils = Java.use("com.ad2001.frida0x1.MainActivity");
utils.get_random.implementation = function () {
var ret = this.get_random();
console.log("get_random: ", ret);
return ret;
};
}

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

image-20250108144455672

05

返回特定数字 exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
function hookTest1() {
var utils = Java.use("com.ad2001.frida0x1.MainActivity");
utils.get_random.implementation = function () {
return 0;
};
}

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

然后输入 4 即可进入下一个界面


解法二:hook check

check 函数是在程序初始化之后才运行的,所以不必在初始化的时候进行 hook

1
frida -U "Frida 0x1" -l ./hook.js

这样 hook 之后随便输入一串数字之后就可以进到下一个页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function hookTest1() {
var utils = Java.use("com.ad2001.frida0x1.MainActivity");
utils.check.overload("int", "int").implementation = function (a, b) {
this.check(a, a * 2 + 4);
return;
};
}

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

frida_0x2

感觉这个题没有上一个难,hook 静态方法。

打开软件,发现只有一个“HOOK ME!”

02

查看源码

01

发现 main 函数下面有一个 get_flag 函数

这串代码是把下面的经过 base64 加密过的 aes 密文进行解密

而且这是个静态函数,有静态标识符,直接按照静态函数 hook 模板 hook 就行

exp:

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

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

frida_0x3

这题也是 hook 一个静态方法,但是这次的静态方法不在 Mainactivity

而是在 Checker 函数中

先打开这个 App 界面,如下图,点击完 click me 会提示 try again

01

查看源码

02

发现判断条件是 Checker.code == 512

跟进 Checker.code

03

发现只是个静态函数,并且有个 increase 函数每次加 2,所以我们只需要用让这个 increase 函数循环 256 次就能得到 flag

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义一个名为hookTest1的函数
function hookTest1() {
Java.perform(function () {
var ClassName = Java.use("com.ad2001.frida0x3.Checker");
for (var i = 0; i < 256; i++) {
ClassName.increase();
}
});
}

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

frida_0x4

这道题是要 hook 一个没有被在 MainActivity 调用的类中的普通方法、

查看 MainActivity 发现其中并没有任何与 flag 有关的类或者方法

image-20250228103655675

查看 Check 类,发现加密在 Check 类中的 get_flag 方法

image-20250228103930255

但是 MainActivity 并没有调用过 Check,

所以我们要先创建实例,然后再进行调用 hook

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function hookTest1() {
Java.perform(function () {
var a = Java.use("com.ad2001.frida0x4.Check");
var check = a.$new(); //手动创建实例
var flag = check.get_flag(1337); //调用获得返回值flag
console.log("flag: " + flag);
});
}

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

frida_0x5

这个题是要 hook 在 MainActivity 中的普通方法

因为是在 MainActivity 之中所以就没有必要手动进行实例化

image-20250228112841728

所以我们只需要获得这个实例就可以了

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//hook非静态方法
function hookTest2() {
var ret = null;
Java.perform(function () {
Java.choose("com.ad2001.frida0x5.MainActivity",{ //要hook的类
onMatch:function(instance){
ret=instance.flag(1337); //要hook的方法
},
onComplete:function(){
// console.log("result: " + ret);
}
});
})
}

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

frida -U -F -l .\hook.js

image-20250228113019144


frida_0x6

这道是 hook 一个在 MainActivity 中的普通方法

但是这个普通方法中的参数是一个未被实例化的类中的两个参数

image-20250228150218173

image-20250228150233351

要 hook 这个方法的话只需要调用方法之前将 Check 类进行实例化以及对里面的变量进行赋值就可以了

exp:

这里有个注意点,因为 check.num1 它只是一个 frida 代理的一个字段对象,直接进行赋值是赋不了的,

要给其所代理的进行赋值是要用 check.num1.value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hookTest2() {
var ret = null;
Java.perform(function () {
Java.choose("com.ad2001.frida0x6.MainActivity", {
//要hook的类
onMatch: function (instance) {
var checker = Java.use("com.ad2001.frida0x6.Checker");
var check = checker.$new();
check.num1.value = 1234;
check.num2.value = 4321;
instance.get_flag(check); //要hook的方法
},
onComplete: function () {
// console.log("result: " + ret);
},
});
});
}

image-20250228152204483


frida_0x7

这题和上面 0x6 差不多,

先是查看 MainActivity,,发现 Check 已经被实例化了

image-20250228154942647

但是这里可以有两种解法

解一:

和上体一样直接当作普通类进行 hook,但是这里有构造方法

需要按照构造方法的传参进行 hook

image-20250228154723182

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hookTest2() {
var ret = null;
Java.perform(function () {
Java.choose("com.ad2001.frida0x7.MainActivity",{ //要hook的类
onMatch: function (instance) {
var checker = Java.use("com.ad2001.frida0x7.Checker");
var a = 513;
var b = 513;
var check = checker.$new(a,b);
//var check = checker.$new(513,513);直接进行赋值也行
instance.flag(check); //要hook的方法
},
onComplete:function(){
// console.log("result: " + ret);
}
});
})
}
frida -U -F -l .\hook.js

解二:hook 构造方法


因为 Check 类中声明了构造方法,所以我们可以直接 hook 构造方法

1
2
3
4
5
6
7
function hookTest3(){
var utils = Java.use("com.ad2001.frida0x7.Checker");
utils.$init.overload('int','int').implementation = function(a,b){
this.$init(513,513);
}
}
frida -U -f com.ad2001.frida0x7 -l .\frida0x7.js

这里的 overload(‘int’,’int’)可以省略,但是为了重构的准确性还是加上为好

function(a,b),这里对应着原来的参数个数

image-20250228161832614


frida_0x8

这道题是要 hook *静态注册*的 JNI native 层的函数

前置了解:

https://www.52pojie.cn/thread-1840174-1-1.html

要先了解一下导入表,导出表,以及 so 的基址的获取方法

打印导入表

1
2
3
4
5
6
7
8
9
10
var imports = Module.enumerateImports("libfrida0x8.so");
console.log("导入表:\n", JSON.stringify(imports, null, 2));
//这两种效果相同
// console.log("导入表");
// for (var i = 0; i < imports.length; i++){
// // if(imports[i].name == "vip"){
// console.log(JSON.stringify(imports[i], null, 2)); //通过JSON.stringify打印object数据
// // console.log(imports[i].address);
// //}
// }

打印导出表

1
2
3
4
5
6
7
var exports = Module.enumerateExports("libfrida0x8.so");
console.log("导出表:\n", JSON.stringify(exports, null, 2));
//这两种效果相同
// console.log("导出表");
// for (var i = 0; i < exports.length; i++){
// console.log(JSON.stringify(exports[i], null, 2));
// }

打印.so 基址

1
2
var baseAddr = Module.getBaseAddress("libfrida0x8.so");
console.log("Base Address of libfrida0x8.so:", baseAddr);

寻找打印指定 so 中 export 库中的函数地址

这里有两个 api 方法

Module.findExportByName(lib,exportName)

从 lib 中寻找指定函数的地址,若不知道 lib 名称可以传 NULL,找不到函数会抛出异常

Module.getExportByName(lib,exportName)

功能与 getExportByName 相同,找不到返回 null

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
//getExportByName
//从lib中寻找指定函数的地址,若不知道lib名称可以传NULL,找不到会抛出异常
var cmpstr_addr = Module.getExportByName(
"libfrida0x8.so",
"Java_com_ad2001_frida0x8_MainActivity_cmpstr"
);
var strcmp_addr = Module.getExportByName("libc.so", "strcmp");
var strcmp_null_addr = Module.getExportByName(null, "strcmp");
console.log("\ncmpstr_addr:", cmpstr_addr);
console.log("strcmp_addr:", strcmp_addr);
console.log("strcmp_null_addr:", strcmp_null_addr);

//程序中没有这个函数,会抛出异常,显示报错
var Closure_addr = Module.getExportByName(null, "Closure");
console.log("Closure_addr:", Closure_addr);

//findExportByName
//功能与getExportByName相同,找不到返回null
var strcmp_ad = Module.findExportByName("libc.so", "strcmp");
var strcmp_null_ad = Module.findExportByName(null, "strcmp");
console.log("\ncmpstr_ad:", strcmp_ad);
console.log("strcmp_null_ad:", strcmp_null_ad);

//程序中没有这个函数,但是返回的是null,所以会打印null然后继续向下正常运行
var Closure_ad = Module.findExportByName("libfrida0x8.so", "Closure");
console.log("Closure_addr:", Closure_ad);

解题过程:

看 MainActivity 发现有一个 native 层的函数 cmpstr

并且 flag 的判断条件也是这个函数

所以我们要在函数被调用前进行 hook

image-20250302142540583

用 ida 查看 frida0x8.so

找到 Java_com_ad2001_frida0x8_MainActivity_cmpstr

这个命名规则和 java 层的包名是一样的,只要把包名的.换成_就可以了

image-20250302142811969

根据伪代码,我们可以清晰的看到我们实际上要 hook 的是 cmpstr 中的 strcmp 函数,

因此这道题的 hook 目标就是 strcmp

其中参数 s1 是我们输入的字符串,s2 是 flag

我们可以查看先看.so 的导入表然后再获取 strcmp 的地址进行 hook

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function hookTest() {
Java.perform(function () {
var imp = Module.enumerateImports("libfrida0x8.so");
console.log("导入表:\n", JSON.stringify(imp, null, 2));

var strcmp_add = Module.getExportByName("libc.so", "strcmp");
console.log("strcmp_addr:", strcmp_add);
var out_put;
Interceptor.attach(strcmp_add, {
onEnter: function (args) {
//作用于目标函数被调用前,通常用于获取/修改函数参数
var arg0 = Memory.readUtf8String(args[0]);
var arg1 = Memory.readUtf8String(args[1]);
out_put = arg1;
if (arg0.includes("tttt")) {
// console.log("args0:", arg0);
console.log("args0:", arg1);
}
},
});
});
}

frida 执行之后需要在 app 输入框中输入包含 tttt 的文本

然后点击提交,这样 s2 包含的 flag 就会打印在终端之中

image-20250302143829271

image-20250302143815191


frida_0x9

和 0x8 一样也是要 hook native 层的函数,

但是这个题的 hook 点是在函数执行后

image-20250303192558431

查看.so 中的函数发现只是的那会一个 1

image-20250303193951284

那么我们只要使得函数结束后返回 1337 就可以了

exp:

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 hookTest() {
Java.perform(function () {
var imp = Module.enumerateImports("liba0x9.so");
console.log("导入表:\n", JSON.stringify(imp, null, 2));

var expr = Module.enumerateExports("liba0x9.so");
console.log("导出表:\n", JSON.stringify(expr, null, 2));

var check_1flag_add = Module.getExportByName(
"liba0x9.so",
"Java_com_ad2001_a0x9_MainActivity_check_1flag"
);
console.log("check_1flag_addr:", check_1flag_add);
// var out_put;
Interceptor.attach(check_1flag_add, {
onEnter: function (args) {},
onLeave: function (retval) {
//触发时机在目标函数返回后,用于获取/修改返回值
retval.replace(1337); //修改返回值为1337
// console.log("retval",retval.toInt32());显示返回值
},
});
});
}

frida_0xA

这一关也是 hook 一个静态注册的 native 层

但是和上一关不一样的是这关的获得 flag 的函数没有被 java 层的 MainActivity 调用

需要在 frida 之中进行 native 层的声明实例化。

我们发现了声明了 native 层的 stringFromJNI 方法

image-20250304200942788

查看 native 层的 stringFromJNI 函数

image-20250304201657728

没啥有用的,看下函数导出表,搜了一下 flag,发现一个 get_flag 的函数

image-20250304202902519

跟进查看,发现是一个 flag 的自解密程序

最后在日志中进行打印(__android_log_print)

这里要先知道一下__android_log_print 这个函数,

它是 Android NDK 提供的日志函数,它的作用类似 printf,但输出到 adb logcat

ida 中这句具体函数中的各参数的意思是

3LL → **日志级别 ANDROID_LOG_DEBUG**(等同于 ANDROID_LOG_DEBUG = 3)。

"FLAG"日志 Tag,用于 logcat 过滤日志。

"Decrypted Flag: %s"格式化字符串,输出 "Decrypted Flag: v4"

v4要打印的字符串char* 类型)。

所以在 hook 完这个函数之后要在日志中过滤查找 flag

image-20250304203021266

但是直接查找这个函数名的地址时会发现找不到

image-20250304203606779

回到 ida 的汇编界面才发现这个函数的名字不是 get_flag

而是_Z8get_flagii

image-20250304203723164

这样就能找到 get_flag 的地址了

image-20250304203824647

然后需要进行实例化调用,之后进行 hook,最后在日志中查找

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function hookTest() {
Java.perform(function () {
var get_flag_add = Module.getExportByName(
"libfrida0xa.so",
"_Z8get_flagii"
);
console.log("get_flag_add_addr:", get_flag_add);
//创建NativePointer对象
var get_flag_ptr = new NativePointer(get_flag_add);
//创建NativeFunction对象,参数为NativePointer对象,返回类型,原函数参数
const get_flag = new NativeFunction(get_flag_ptr, "void", ["int", "int"]); //
get_flag(1, 2); //主动调用
console.log(get_flag(1, 2));
});
}

image-20250304205528101


frida_0xB

hook静态的so层,但是需要用Frida的硬编码工具修改硬编码

查看java层主类,发现调用了so层的getflag函数

image-20250317174821063

查看so层的getflag函数,发现并不能正常反编译

原来是 rax 这里有个比较,但是赋值和比较值不一样所以且恒为 0,所以 ida 会自动不编译

image-20250317175401133

把 jnz 改成 jz 之后就可以正常查看伪代码了,

伪代码就是一个简单异或

image-20250317175542094

综上这里有两个方法

方法一:修改硬编码,hookjnzjz

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
function hookTest7() {
Java.perform(function () {
var libc_base = Module.getBaseAddress("libfrida0xb.so"); //libc基地址
var jnz = libc_base.add(0x170ce); //JNZ所在偏移
console.log("libc_base : ", libc_base);
console.log("jnz : ", jnz);

Memory.protect(jnz, 0x1000, "rwx"); //赋予rwx权限
var writer = new X86Writer(jnz);

// 读取内存范围内的指令
var size = 0x30; // 读取的字节数
var instructionBytes = Memory.readByteArray(jnz, size);
console.log("instructionBytes :\n", instructionBytes);
// 解析并输出汇编指令
var instructions = Instruction.parse(jnz, instructionBytes);
console.log("instructions :", instructions);

try {
writer.putBytes([0x0f, 0x84]); //替换为Jz,0x0A为条件不满足则跳转
writer.flush(); // 刷新指令缓存
} finally {
writer.dispose(); // 释放writer对象
}

var instructionBytes = Memory.readByteArray(jnz, size);
console.log("new instructionBytes :\n", instructionBytes);
//解析并输出汇编指令
var instructions = Instruction.parse(jnz, instructionBytes);
console.log("new instructions :", instructions);
});
}

方法二:修改上面的比较,把长的数字改成短的,使得比较结果恒为 1

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
Java.perform(function () {
var libc_base = Module.getBaseAddress("libfrida0xb.so"); //libc基地址
var DEADBEEF = libc_base.add(0x170c0); //0xDEADBEEF所在偏移
console.log("libc_base : ", libc_base);
console.log("DEADBEEF : ", DEADBEEF);

Memory.protect(DEADBEEF, 0x1000, "rwx"); //赋予rwx权限
var writer = new X86Writer(DEADBEEF);

// 读取内存范围内的指令
var size = 0x30; // 读取的字节数
var instructionBytes = Memory.readByteArray(DEADBEEF, size);
console.log("instructionBytes :", instructionBytes);
// 解析并输出汇编指令
var instructions = Instruction.parse(DEADBEEF, instructionBytes);
console.log("instructions :", instructions);

try {
writer.putBytes([0xc7, 0x45, 0xdc, 0x39, 0x05, 0x00, 0x00]); //修改为0x539
writer.flush();
} finally {
writer.dispose();
}

var instructionBytes = Memory.readByteArray(DEADBEEF, size);
console.log("new instructionBytes :", instructionBytes);
// 解析并输出汇编指令
var instructions = Instruction.parse(DEADBEEF, instructionBytes);
console.log("new instructions :", instructions);
});

exp:

img