frida-labs WP

Frida Labs

同一道题有时候用 frida hook 了返回结果就能输出结果,但是如果输入成功了并不会返回正确值就要逆向,所有 ctf 出题的时候都只存储 flag 的密码,然后将输入值加密后再与其对比,这时候 frida 就没什么用
ctf 里面的安卓大多数是为了逆向,但是实际应用中,frida 大多用来绕过,因为并没有那么多需要逆向的地方

https://github.com/DERE-ad2001/Frida-Labs.git

0x1

第一关就是一个很典型的案例,flag 是被加密的,如果输入了正确的数值就自动解密将其输出获得 flag,此时可以直接解密获得到 flag,但从 frida 角度来看,可以直接固定 get_random 方法的返回值,直接获取 flag

固定 get_random 返回值为 1

1
2
3
4
5
6
7
Java.perform(function() {
var a = Java.use("com.ad2001.frida0x1.MainActivity")
a.get_random.implementation = function() {
console.log("get_random called")
return 1;
}
})

因为他的判断不是很难,(i * 2) + 4 == i2 所以只需要找一个满足这个关系的两个数就可以让直接输出 flag

1
2
3
4
5
6
Java.perform(function() {
var a = Java.use("com.ad2001.frida0x1.MainActivity");
a.check.overload('int', 'int').implementation = function(a, b) {
this.check(4, 12);
}
});

check 函数是一个 void 函数,没有返回值,不能直接获取其返回值,但他弹窗的时候又去调用了 setText 方法,直接 hook 其参数打印出 flag

1
2
3
4
5
6
7
8
9
Java.perform(function () {
var TextView = Java.use("android.widget.TextView");

TextView.setText.overload('java.lang.CharSequence').implementation = function (text) {
var content = text.toString();
console.log("[*] 检测到 TextView.setText() 被调用,内容是: " + content);
return this.setText(text);
};
});

两个脚本结合来用,就可以让他直接打印 flag 到命令行中

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

var MainActivity = Java.use("com.ad2001.frida0x1.MainActivity");

MainActivity.check.overload('int', 'int').implementation = function (a, b) {
console.log("[*] 拦截到 check(),原始参数为: " + a + ", " + b);
var result = this.check(4, 12);
};

var TextView = Java.use("android.widget.TextView");

TextView.setText.overload('java.lang.CharSequence').implementation = function (text) {
var content = text.toString();
console.log("[*] TextView 显示的内容是: " + content);

return this.setText(text);
};
});
1
frida -U -f com.ad2001.frida0x1 -l .\frida.js

0x2

这关比较特别该应用目前唯一在做的事情就是设置 TextView,并且定义一个 get_flag 函数

但是该函数没有一处调用

所以需要我们去主动调用该方法才能打印出 flag

1
2
3
4
Java.perform(function() {
var MainActivity = Java.use("com.ad2001.frida0x2.MainActivity");
MainActivity.get_flag(4919);
})

由于没有直接调用,所以直接注入 frida 脚本不能直接获取 flag,得在 app 环境下运行脚本才能调用成功

1
frida -U -f com.ad2001.frida0x2

前者不能直接使用脚本注入,是因为 frida 脚本早于 Activity 创建就已经注入了,TextView 还没初始化,调用 t1.setText() 会失败,无法显示内容,最好的方法就是在脚本中加入延时等 Activity 初始化后再注入

1
2
3
4
5
6
7
8
Java.perform(function() {

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

})
1
frida -U -f com.ad2001.frida0x2 -l .\0x2.js

可以直接 hook TextView 类的 setText 方法直接将 flag 打印到命令行中

1
2
3
4
5
6
7
8
9
10
11
12
13
Java.perform(function() {

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

var TextView = Java.use("android.widget.TextView")
TextView.setText.overload('java.lang.CharSequence').implementation = function (text) {
var content = text.toString();
console.log("[*] TextView 显示的内容是: " + content);
}
})

0x3

只有当 code 的值为 512 的时候才能输出 flag

但是 checker 类中的 increase 方法并没有被调用,所以 code 是不可能为 512 的

直接将 code 的值修改为 512

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

setTimeout(function() {
var check = Java.use("com.ad2001.frida0x3.Checker");
check.code.value = 512;
}, 2000);

var TextView = Java.use("android.widget.TextView");

TextView.setText.overload('java.lang.CharSequence').implementation = function (text) {
var content = text.toString();
console.log("[*] TextView 显示的内容是: " + content);

return this.setText(text);
};

})
1
frida -U -f com.ad2001.frida0x3 -l .\0x3.js

0x4

这一关就只有一个 textview 了

可以在 res/layout/activity_main.xml 中找到其定义

虽然 MainActivity 中没有内容,但是在源码部分可以看到还有一个 Check类

其中有一个 get_flag 方法,同样没有任何引用,我们需要主动调用来输出 flag

注意,这里的 Check 没有实例化,frida 实例化类的时候要使用 $new**,**同时不需要声明类的类型统一用 var 即可

1
2
3
4
5
6
7
8
9
10
Java.perform(function() {

setTimeout(function() {
var Check = Java.use("com.ad2001.frida0x4.Check");
var check = Check.$new();
var flag = check.get_flag(1337);
console.log("[*] 获取到的 flag 为: " + flag);
}, 2000);

})

0x5

这一关的 flag 函数是直接定义在 MainActivity 中,看似我们可以直接调用 flag 函数让他输出 flag 但是 flag 方法并不是一个静态方法并不能直接通过类名调用,需要从实例化的对象中调用

如果直接调用必然会报错

既然不能直接通过类名调用,那就实例化对象调用,但是当前类是 MainActivity ,如果去创建一个新的 MainActivity 对象 这样就等于非主线程中创建了 Android UI 组件或 Handler 相关的对象,这是 Android 不允许的

所以最好的方法就是使用 Java.choose 从内存直接调用现有的实例

1
2
3
4
5
6
7
8
Java.performNow(function() {
Java.choose('com.ad2001.frida0x5.MainActivity', {
onMatch: function(instance) {
instance.flag(1337);
},
onComplete: function() {}
});
});

注意 hook TextView 的时机需要早于 flag 方法调用时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
setTimeout(function() {
Java.perform(function() {
var TextView = Java.use("android.widget.TextView");
TextView.setText.overload('java.lang.CharSequence').implementation = function (text) {
var content = text.toString();
console.log("[*] TextView 显示的内容是: " + content);
return this.setText(text);
};

Java.choose('com.ad2001.frida0x5.MainActivity', {
onMatch: function(instance) {
instance.flag(1337);
},
onComplete: function() {
}
});
});
}, 2000);

0x6

这一关跟上一关差不多,只不过 get_flag 方法的参数是一个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
setTimeout(function() {
Java.perform(function() {
var TextView = Java.use("android.widget.TextView");
TextView.setText.overload('java.lang.CharSequence').implementation = function (text) {
var content = text.toString();
console.log("[*] TextView 显示的内容是: " + content);
return this.setText(text);
};

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

onMatch: function(instance) {
var Checker = Java.use("com.ad2001.frida0x6.Checker");
var checker = Checker.$new();
checker.num1.value = 1234;
checker.num2.value = 4321;
instance.get_flag(checker);
},
onComplete: function() {
}
});
});
}, 2000);

0x7

这一关跟上关差不多,用同样的脚本就能做

但是注入会提示重载方法参数类型错误

发现 Checker 类中有一个构造函数,对象初始化的时候会调用该方法,所以会产生报错

因此仅需要在实例化对象的时候给构造函数赋值即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
setTimeout(function() {
Java.perform(function() {
var TextView = Java.use("android.widget.TextView");
TextView.setText.overload('java.lang.CharSequence').implementation = function (text) {
var content = text.toString();
console.log("[*] TextView 显示的内容是: " + content);
return this.setText(text);
};

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

onMatch: function(instance) {
var Checker = Java.use("com.ad2001.frida0x7.Checker");
var checker = Checker.$new(600,600);
instance.flag(checker);
},
onComplete: function() {
}
});
});
}, 2000);

既然是通过构造函数来完成对象的初始化,不如直接更近一步 hook 构造函数

$init 在 Frida 中用来表示 Java 类的构造函数

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

var TextView = Java.use("android.widget.TextView");
TextView.setText.overload('java.lang.CharSequence').implementation = function (text) {
var content = text.toString();
console.log("[*] TextView 显示的内容是: " + content);
return this.setText(text);
};

var Checker = Java.use("com.ad2001.frida0x7.Checker");
Checker.$init.implementation = function(param){
this.$init(600, 600);
}
});

0x8

这次主逻辑放在了 native 层,通过 System.loadLibrary 加载了一个 native 函数

通过比较用户输入的值 s1 与 s2 的,返回一个 bool 值

s2 的生成逻辑

1
2
for ( i = 0; i < (unsigned __int64)__strlen_chk("GSJEB|OBUJWF`MBOE~", -1LL); ++i )
s2[i] = aGsjebObujwfMbo[i] - 1;

aGsjebObujwfMbo 数组的内容也是 GSJEB|OBUJWF MBOE~`

对每一个字符串的 ascii 值减一即可得到 flag

1
2
3
4
5
enc = "GSJEB|OBUJWF`MBOE~"

decrypted_str = ''.join([chr(ord(c) - 1) for c in enc])

print("Flag:", decrypted_str)

frida 也支持 native 层的 hook

先通过 frida 获取所有导出函数,获取完整的函数签名

1
2
3
4
5
6
7
8
9
10
var moduleName = "libfrida0x8.so";
var module = Process.findModuleByName(moduleName);
if (module) {
console.log("[*] Found module: " + module.name + " at " + module.base);
var a = module.enumerateExports();
for (var i = 0; i < a.length; i++) {
var e = a[i];
console.log("[*] " + e.name + ": " + e.address);
}
}

native 层中的主要逻辑是调用 strcmp 将输入的字符串和 flag 比较,所以直接 hook libc.so 中的 strcmp 函数就能直接获取到 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var moduleName = "libc.so";
var module = Process.findModuleByName(moduleName);
if (module) {
var strcmpAddr = module.findExportByName("strcmp");
console.log("[*] strcmp address: " + strcmpAddr);

Interceptor.attach(strcmpAddr, {
onEnter: function(args) {
var input = args[0].readUtf8String(); //转换成utf8字符串以免报错
var flag = args[1].readUtf8String();
if (input.includes("flag")) { //只有特定输入值才会打印 以免冗余打印
console.log("[*] flag: " + flag);
}
}
});
}

0x9

这关只有一个 onClick 方法所以我们无法直接输入任何内容,但只需要控制 native 方法 check_flag 的返回值为 1337 即可

遍历导出表,这次就需要直接 hook APK 里的 so 文件里

1
2
3
4
5
6
7
8
9
10
var moduleName = "liba0x9.so";
var module = Process.findModuleByName(moduleName);
if (module) {
console.log("[*] Found module: " + module.name + " at " + module.base);
var a = module.enumerateExports();
for (var i = 0; i < a.length; i++) {
var e = a[i];
console.log("[*] " + e.name + ": " + e.address);
}
}

这次用的是 Toast 弹窗

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
setTimeout(function() {
var moduleName = "liba0x9.so";
var module = Process.findModuleByName(moduleName);
if (module) {
var check_flag = module.findExportByName("Java_com_ad2001_a0x9_MainActivity_check_1flag");
console.log("[*] check_flag address: " + check_flag);

Interceptor.attach(check_flag, {
onEnter: function(args) {

},
onLeave: function(retval) {
retval.replace(1337);
console.log("[*] check_flag called, retval: " + 1337);
}
});
}

Java.perform(function() {
var Toast = Java.use("android.widget.Toast");
Toast.makeText.overload('android.content.Context', 'java.lang.CharSequence', 'int').implementation = function (context, text, duration) {
var content = text.toString();
console.log("[*] Toast 显示的内容是: " + content);
return this.makeText(context, text, duration);
};
});

}, 2000);

0xA

只有一个 onCreate 方法,setText 的内容直接就来自 native 方法

stringFromJNI 函数作用就是把 Hello Hackers 字符串打印出来(建议反编译 x86 架构的 so 文件,arm 架构反编译不出 Hello Hackers 这个字符串)

但是在符号表里面还没有被调用过的 get_flag 方法才是真正的 flag 生成逻辑

这个算法很简单就是每个字符的 ASCII 值加上 2 * i

1
2
3
4
5
encrypted = "FPE>9q8A>BK-)20A-#Y"

flag = ''.join(chr(ord(encrypted[i]) + 2 * i) for i in range(len(encrypted)))

print("[*] Decrypted flag:", flag)

frida 也提供了可以主动调用 native 方法的 api

遍历导出表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var moduleName = "libfrida0xa.so";
var module = Process.findModuleByName(moduleName);
if (module) {
console.log("[*] Found module: " + module.name + " at " + module.base);
var a = module.enumerateExports();
for (var i = 0; i < a.length; i++) {
var e = a[i];
if (e.name.includes("get_flag")){
console.log("[*] " + e.name + ": " + e.address);
}
}
}else{
console.log("[-] Module not found: " + moduleName);
}

发现 get_flag 变成了 _Z8get_flagii,这是因为编译器需要为 C++ 中的所有函数,在符号表中生成唯一的标识符,来区分不同的函数,所以会重命名函数

因为函数还是有导出表的可以可以直接通过函数名获取地址后调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var moduleName = "libfrida0xa.so";
var module = Process.findModuleByName(moduleName);
if (module) {
console.log("[*] Found module: " + module.name + " at " + module.base);
var a = module.enumerateExports();
for (var i = 0; i < a.length; i++) {
var e = a[i];
if (e.name.includes("get_flag")){
console.log("[*] " + e.name + ": " + e.address);
var target = e.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);

}
}
}else{
console.log("[-] Module not found: " + moduleName);
}

最好打印 flag 的使用 log 打印的所以得用 logcat 查看

日志刷新很快,不容易看到 flag,运用上关的知识 直接 hook __android_log_print 将 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
setTimeout(function() {
var loglib = "liblog.so";
var log = Process.findModuleByName(loglib);
if (log) {
console.log("[*] Found module: " + log.name + " at " + log.base);
var a = log.enumerateExports();
for (var i = 0; i < a.length; i++) {
var e = a[i];
if (e.name.includes("__android_log_print")){
console.log("[*] " + e.name + ": " + e.address);
var logPrint = e.address;
Interceptor.attach(logPrint, {
onEnter: function(args) {
var flagPtr = args[3];
var flagValue = flagPtr.readCString();
console.log("[*] flag: " + flagValue + "\n");
}
});

}
}
}

var moduleName = "libfrida0xa.so";
var module = Process.findModuleByName(moduleName);
if (module) {
console.log("[*] Found module: " + module.name + " at " + module.base);
var a = module.enumerateExports();
for (var i = 0; i < a.length; i++) {
var e = a[i];
if (e.name.includes("get_flag")){
console.log("[*] " + e.name + ": " + e.address);
var target = e.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);
}
}
}else{
console.log("[-] Module not found: " + moduleName);
}
}, 2000);

由于 ALSR(地址随机化)的问题,每次运行的时候函数的地址都是不同的,但变化的只是基址,计算出偏移就可以在后续运行的时候直接访问到函数的地址

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

1
2
3
4
var adr = Module.findBaseAddress("libfrida0xa.so").add(0x18BB0) 
var get_flag_ptr = new NativePointer(adr);
const get_flag = new NativeFunction(get_flag_ptr, 'void', ['int', 'int']);
get_flag(1,2);

0xB

提供了一个 onCreate 方法在点击的时候会调用

但是直接点并不会弹 flag

反编译 JNI 函数的时候,发现反编译失败,也就是这个原因导致返回不出内容

反编译器(如 IDA Pro 的 Hex-Rays Decompiler、Ghidra 的 Decompiler、RetDec 等)在分析并尝试将 汇编代码(或机器码)还原为高级语言(如 C/C++)代码 的过程中,会应用一系列 优化与分析策略,比如自动丢弃不可达代码块

直接查看汇编,发现函数一开始会直接去比较 0xdeadbeef0x539 的值,但这两个值永远不可能相等就会 jmp 到 loc_171A6,也就直接 retn 了,所以返回不出任何内容,与花指令的不同的是,这种指令直接影响了代码的执行逻辑

将中间的 不可达代码块(Unreachable Code NOP 掉后就能正常反编译伪代码了

解密代码也很简单,就是将每个字符的 ASCII 值异或 0x2c

1
2
3
4
5
encrypted = "j~ehmWbmxezisdmogi~Q"

flag = ''.join(chr(ord(encrypted[i]) ^ 0x2c) for i in range(len(encrypted)))

print("[*] Decrypted flag:", flag)

但这种解法依赖于手动 patch so 文件,我们也可以直接用 frida 在 app 运行时直接修改 native 代码

计算 NE 指令的偏移

0x15248 - 0x15220 = 0x28

可以直接通过 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
var moduleName = "libfrida0xb.so";
var module = Process.findModuleByName(moduleName);
if (module) {
console.log("[*] Found module: " + module.name + " at " + module.base);
var a = module.enumerateExports();
for (var i = 0; i < a.length; i++) {
var e = a[i];
if (e.name.includes("getFlag")){
console.log("[*] " + e.name + ": " + e.address);

var be = e.address.add(0x28);
var currentAddr = e.address;

for (let i = 0; i < 15; i++) {
const instr = Instruction.parse(currentAddr);
if (!instr) {
console.log(`[!] 无法解析地址: ${currentAddr}`);
break;
}
console.log(`${instr.address} ${instr.toString()}`);
currentAddr = instr.next;
}
}
}
}

可以看到偏移 0x28 后的指令是 b #0x6fc8055248,但根据 ida 反编译的汇编以及上下文来看,BE 跳转指令确实在这个地址上

由于 arm64 架构没有类似 nop 这样的指令,所以只能将指令修改为跳转到下一条指令来绕过

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
55
56
57
58
59
60
61
62
63
setTimeout(function() {
var loglib = "liblog.so";
var log = Process.findModuleByName(loglib);
if (log) {
console.log("[*] Found module: " + log.name + " at " + log.base);
var a = log.enumerateExports();
for (var i = 0; i < a.length; i++) {
var e = a[i];
if (e.name.includes("__android_log_print")){
console.log("[*] " + e.name + ": " + e.address);
var logPrint = e.address;
Interceptor.attach(logPrint, {
onEnter: function(args) {
var flagPtr = args[3];
var flagValue = flagPtr.readCString();
console.log("[*] flag: " + flagValue + "\n");
}
});

}
}
}

var moduleName = "libfrida0xb.so";
var module = Process.findModuleByName(moduleName);
if (module) {
console.log("[*] Found module: " + module.name + " at " + module.base);
var a = module.enumerateExports();
for (var i = 0; i < a.length; i++) {
var e = a[i];
if (e.name.includes("getFlag")){
console.log("[*] " + e.name + ": " + e.address);

var be = e.address.add(0x28);
var target = be.add(0x4);
var currentAddr = e.address;

for (let i = 0; i < 15; i++) {
const instr = Instruction.parse(currentAddr);
if (!instr) {
console.log(`[!] 无法解析地址: ${currentAddr}`);
break;
}
console.log(`${instr.address} ${instr.toString()}`);
currentAddr = instr.next;
}
Memory.protect(be, 0x1000, "rwx");
var writer = new Arm64Writer(be);
try{

writer.putBImm(target); //修改be为bl 跳转到下一条指令
writer.flush();

} finally {

writer.dispose();

}

}
}
}
}, 2000);

需要点击一下按钮才能触发

结合前面几关的内容,我们可以直接主动调用 native 方法自动输出 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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
setTimeout(function() {
var loglib = "liblog.so";
var log = Process.findModuleByName(loglib);
if (log) {
console.log("[*] Found module: " + log.name + " at " + log.base);
var a = log.enumerateExports();
for (var i = 0; i < a.length; i++) {
var e = a[i];
if (e.name.includes("__android_log_print")){
console.log("[*] " + e.name + ": " + e.address);
var logPrint = e.address;
Interceptor.attach(logPrint, {
onEnter: function(args) {
var flagPtr = args[3];
var flagValue = flagPtr.readCString();
console.log("[*] flag: " + flagValue + "\n");
}
});

}
}
}

var moduleName = "libfrida0xb.so";
var module = Process.findModuleByName(moduleName);
if (module) {
console.log("[*] Found module: " + module.name + " at " + module.base);
var a = module.enumerateExports();
for (var i = 0; i < a.length; i++) {
var e = a[i];
if (e.name.includes("getFlag")){
console.log("[*] " + e.name + ": " + e.address);

var be = e.address.add(0x28);
var target = be.add(0x4);
var currentAddr = e.address;

for (let i = 0; i < 15; i++) {
const instr = Instruction.parse(currentAddr);
if (!instr) {
console.log(`[!] 无法解析地址: ${currentAddr}`);
break;
}
console.log(`${instr.address} ${instr.toString()}`);
currentAddr = instr.next;
}
Memory.protect(be, 0x1000, "rwx");
var writer = new Arm64Writer(be);
try{

writer.putBImm(target);
writer.flush();

} finally {

writer.dispose();

}

//主动调用get_flag方法
var target = e.address;
var get_flag_ptr = new NativePointer(target);
const My_get_flag = new NativeFunction(get_flag_ptr, 'void', []);
var flag = My_get_flag();

}
}
}
}, 2000);


frida-labs WP
http://example.com/2025/09/07/frida-labs-WP/
作者
Gilgamesh
发布于
2025年9月7日
许可协议