Frida in CTF Android Reverse Engineering

j1ya Lv5

原理

Frida是一款跨平台的动态代码插桩框架,核心原理是向目标进程注入包含 Frida Runtime 的代码库。该 Runtime 在进程内构建轻量级 JavaScript 环境,并利用底层引擎(Gum)动态修改目标函数的内存指令。通过插入跳转指令(Inline Hook)等,将执行流重定向到代理函数(trampoline/thunk),代理函数负责调用用户编写的 JavaScript 回调,实现拦截、修改参数/返回值或追踪逻辑

Frida 通过多种方式(例如 Android 的 ptrace,Windows 的 DLL 注入,iOS 的 Dylib)将自己的动态库注入到目标进程地址空间中,核心思想是在目标进程内存中加载 Frida 动态库并初始化 Gum 插桩引擎,最终建立双向通信通道,使 JavaScript 脚本能动态操控原生代码执行流

组成

frida-server (目标端): 在目标设备或模拟器上运行的后台服务

frida-gadget (嵌入式): 核心功能与frida-server相同,存在形式为动态链接库 (*.so/*.dylib/*.dll),可静态嵌入到目标应用程序中,无需每次注入,适合在无 root 环境中使用

frida-core: 由 server 或 gadget 加载的核心库,包含 Frida运行最重要的组件gum,提供了将 Native 与 JavaScript连接起来的桥梁

frida-gum:实现代码插桩,其中Interceptor提供hook主要功能,Stalker提供代码追踪功能

frida CLI / Python Bindings / Node.js Bindings (客户端): 开发者使用的工具或库,通过进程间通信(IPC)与目标端的 server 或 gadget 通信,Python/Node.js Bindings允许开发者在 Python 或 Node.js 环境中直接调用 Frida 的功能,frida CLI提供快速交互式操作,需提前写入 .js 文件,依赖命令行参数

电脑模拟器

本地环境:雷电模拟器Android9 x64;夜神模拟器Android7 x86

root

先安装Magisk(面具),这里把模拟手机型号从PCLM10改为PCLM9发现就有直接安装选项

安装MagiskHidePropsConfig模块

共享文件夹在shared/Pictures目录

连接设备

windows上的模拟器选择下载frida-server-16.2.1-android-x86_64.xz,双击解压出二进制文件,然后通过adb push到/data/local/tmp

报错10061说明模拟器没开

adb devices查看设备,adb root获取root权限,adb shell`连接到远程

进入frida-server安装目录cd /data/local/tmp,然后运行frida-server

进行端口转发监听,使电脑上的 Frida 客户端能够访问模拟器里的 frida-server,如果用的是 libfrida-gadget.so 就需要adb forward tcp:27042 tcp:27042

经验总结

1.打开的方式安装不了可以拖动或者adb install

2.可通过下面路径查找日志

3.如果处于开发者模式一直未被调试

可以关闭开发者模式

4.如果是多开的模拟器需要先找到对应端口再连接:.\nox_adb -s 127.0.0.1:62026 shell

手机真机

连接设备

同一网段下手机开启开发者模式,允许usb/无线调试

无线调试Android11及以上需要先配对,手机选择使用配对码配对设备

查看手机架构,下载对应架构frida-server

Objection

Objection 在 Frida 上增加了一层命令封装

/data/local/tmp目录下用root权限启动frida-server

搜索特定进程:frida-ps -U | grep -i settings

查看进程对应真正包名:frida-ps -Ua

输入空格键会有提示与补全

静态

模拟器上启动软件后再去注入mobile包:objection -g mobilego explore

有时候进程名用空格隔开,用引号标注

在内存中所有已加载的类中搜索包含game的类:android hooking search classes game

查看com.android.settings.DisplaySettings类有哪些方法:android hooking list class_methods com.android.settings.DisplaySettings

直接生成代码但可能没参数:android hooking generate simple com.android.settings.DisplaySettings

进入显示设置,软件如果没有做校验,可以直接使用这个命令跳过注册登录:android intent launch_activity com.android.settings.DisplaySettings

动态

hook类:android hooking watch class android.bluetooth.BluetoothDevice

hook方法,查看参数、返回值和调用栈:**android hooking watch class_method game.Game.checkflag --dump-args --dump-return --dump-backtrace**

在模拟器输入符合题目要求的字符串后,再Called game.Game.checkflag(java.lang.String),得到return值

还可以import js代码,效果和直接frida类似

Frida

包名通过frida-ps -Ua查看

Frida两种启动方式的区别:

1
2
spawn` 模式:frida 重新打开一个进程并挂起,在启动的同时注入frida代码 `frida -U -f 包名 -l js路径 
attch` 模式: attach到已经存在的进程,核心原理是ptrace修改进程内存,如果进程处于调试状态(traceid不等于0),则attach失败 `frida -U -n 包名 -l js路径

以下代码基于frida16

java层

hook函数返回值

1
2
3
4
5
6
7
8
9
10
function main() { Java.perform(function () {
let func = Java.use("com.example.whathappened.MyJNI.Myjni");
func["getstr"].implementation = function () {
let re0 = this["getstr"]();
console.log(`${re0}`);
return "Hello";
};
});
}
setImmediate(main);

native层

hook自定义函数

_Z10murmur3_32PKcjj是真实导出表的名字,murmur3_32是demangle后的名字

1
2
3
4
5
6
7
8
var func_addr = Module.findExportByName("libmidand.so" , "_Z10murmur3_32PKcjj");
console.log("func addr is ---" + func_addr);
Interceptor.attach(func_addr, {
onLeave: function(retval){
console.log("enter murmur3_32 retvalue->\n" + hexdump(retval));

}
});

hook之后在模拟器输入字符然后点login

发现能看到返回值

hook系统函数

v6参数(账号)会经过好几次加密,需要hook strcmp函数查看s2(密码)和比较的值

!

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
70
71
72
73
74
75
76
77
78
79
function hook_strcmp(chall) {

const strcmpAddr = Module.findExportByName(
'libc.so',
'strcmp'
);
if (strcmpAddr === null) {
console.log('[-] strcmp not found');
return;
}
const start = chall.base;
const end = chall.base.add(chall.size);

console.log('[+] libchall1.so loaded');
console.log('[+] base : ' + start);
console.log('[+] strcmp : ' + strcmpAddr);

Interceptor.attach(strcmpAddr, {

onEnter: function(args) {
this.matched = false;
// 判断strcmp调用者是否来自libchall1.so
if (
this.returnAddress.compare(start) < 0 ||
this.returnAddress.compare(end) >= 0
) {
return;
}
try {
this.s1 = args[0].readCString();
this.s2 = args[1].readCString();
this.matched = true;
} catch(e) {
console.log(
'[-] read error: ' + e
);
}
},

onLeave: function(retval) {
if(!this.matched)
return;
console.log(
'=============================='
);
console.log(
'[+] p_s1 : ' + this.s1
);
console.log(
'[+] s2 : ' + this.s2
);
// console.log(
// '[+] ret : ' + retval.toInt32()
// );
console.log(
'=============================='
);
}
});
console.log('[+] strcmp hook installed');
}

function wait_libchall1(){
let chall = Process.findModuleByName(
'libchall1.so'
);
if(chall === null){
console.log(
'[*] waiting libchall1.so ...'
);
setTimeout(
wait_libchall1,
500
);
return;
}
hook_strcmp(chall);
}
setImmediate(wait_libchall1);

![img]

主动调用主函数中未被调用的方法

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
function hook(){
var a = Module.findBaseAddress("libfrida0xa.so");
var b = Module.enumerateExports("libfrida0xa.so");
var get_flagaddress = null;
var mvaddress = null;
for(var i = 0 ; b[i]!= null ; i ++ ){
// console.log(b[i]["name"])
if(b[i]["name"] == "_Z8get_flagii"){
console.log("function get_flag : ",b[i]["address"]);
console.log((b[i]["address"] - a).toString(16));
// mvaddress = b[i]["address"] - a;
get_flagaddress = b[i]["address"];
}
}
console.log(ptr.toString(16));

var get_flag_ptr = new NativePointer(get_flagaddress);
const get_flag = new NativeFunction(get_flag_ptr,'char',['int','int']);
var flag = get_flag(1,2);
console.log(flag)
}

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


//也可以用偏移量的方式
const libBase = Module.findBaseAddress(
"libnative.so"
);
const funcAddr = libBase.add(0x1234);
const hidden_func = new NativeFunction(
funcAddr,
'pointer',
[
'int',
'int'
]
);
let ret = hidden_func(10,20);
console.log(ret.readCString());

frida-dump

通过 Frida 注入脚本到目标进程,动态获取进程运行时内存信息,包括已加载的 DEX 文件、Native 库以及关键内存区域,并根据内存映射信息将其导出为原始文件(如 .dex、.so)。对于经过加固、混淆或运行时解密后的代码,通常需要结合动态分析进一步恢复

在 ART 类加载过程中,FindClassDef 会遍历当前 ClassLoader 关联的 DexFile 集合,根据目标类描述符查找对应的 ClassDef。当找到目标类所在的 DexFile 及 ClassDef 后,ClassLinker::DefineClass 使用这些信息创建对应的 Class 对象,并继续完成字段解析、方法加载以及代码链接

frida-dump 的核心思想是在 ART 类加载关键路径上 Hook ClassLinker::DefineClass,获取正在加载类对应的 DexFile 对象,通过读取 DexFile 中保存的 begin 地址和 size 信息定位内存中的完整 dex 数据,并将其导出

frida-dexdump

frida-dexdump 支持在 App 启动阶段注入,通过捕获运行时已经解密并加载到内存中的 dex 文件,可以直接导出动态加载后的 dex

连接frida-server之后用 adb shell pm list packages找到真正的包名(有时候和jadx里面显示的不太一样)

frida-dexdump -U -f com.example.wyy直接dump出动态解密的dex,会自动修复

-d开启深度搜索模式

参考链接:

https://bbs.kanxue.com/thread-280758.htm

https://bbs.kanxue.com/thread-280812.htm#msg_header_h2_1

https://blog.csdn.net/Ruaki/article/details/135580772

https://www.bilibili.com/video/BV1wT411N7sV/

https://www.cnblogs.com/lxh2cwl/p/14842544.html

  • 标题: Frida in CTF Android Reverse Engineering
  • 作者: j1ya
  • 创建于 : 2024-10-28 18:15:48
  • 更新于 : 2026-09-01 21:52:10
  • 链接: https://redefine.ohevan.com/2024/10/28/frida/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论