Learning Frida - 01
在SekaiCTF-2025中,面对一个程序,我用繁琐的动态调试方法来手动dump数据,每次运行都需要按下几十次 F9, 甚是繁琐,在被折磨一通耻辱下播(bushi)。比赛结束后我学习了一位大佬的解法,他使用了frida脚本来一键dump, 深深地震惊了我:这就是科技的力量吗?虽然很早以前就听过hook技术,但却没有想过还能这么玩。
不久后的FortID CTF中,其中有一道名为toilet的题目,在搞清楚他在干什么后,我感觉“嘿,为什么我不趁此机会试着用一用frida hook呢”,然而当时我对frida的一些关键概念并不了解,在查阅了官方的api文档后,我编写了一个粗糙的脚本,然而总是无法按照我的期望来工作。
最近比较闲,于是我便萌生了系统地学习frida的想法。这个系列会记录一些学习过程中的零碎东西。
module, symbol & 函数指针
在跟着一些blog编写脚本的时候,发现一些写法无法通过编译,后来发现是新旧版本的frida写法不同:
https://stackoverflow.com/questions/79700740/frida-17-module-getexportbyname-typeerror-not-a-function
一个简单的测试对象:
1
2
3
4
5
6
7
8
9
10
11
12
// big.c
#include <stdint.h>
#include<stdio.h>
uint64_t test(uint64_t a){
return a;
}
int main(){
printf("---------------\n");
printf("program exit: %llx\n",test(0xeafffffffffffaafLL));
printf("---------------\n");
return 0;
}
使用Process.enumerateModule可以返回一个多个Module组成的列表
1
2
3
4
5
6
7
8
9
10
11
12
const module_list = Process.enumerateModules();
for (const tmpModule of module_list) {
console.log(tmpModule.name);
if (tmpModule.name == "big") {
const sym = tmpModule.enumerateSymbols();
for (const tmpSym of sym) {
console.log("* ", tmpSym.name);
}
}
}
//注意: js/ts中和python for i in ...平行的是 for (x of ...),而'in'版本是用于遍历map的
运行结果:
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
Compiled a.ts (48 ms)
big
*
* big.c
*
* _DYNAMIC
* __GNU_EH_FRAME_HDR
* _GLOBAL_OFFSET_TABLE_
* __libc_start_main@GLIBC_2.34
* _ITM_deregisterTMCloneTable
* data_start
* puts@GLIBC_2.2.5
* _edata
* _fini
* printf@GLIBC_2.2.5
* __data_start
* __gmon_start__
* __dso_handle
* _IO_stdin_used
* _end
* _start
* __bss_start
* main
* __TMC_END__
* _ITM_registerTMCloneTable
* __cxa_finalize@GLIBC_2.2.5
* _init
* test
linux-vdso.so.1
libc.so.6
ld-linux-x86-64.so.2
libdl.so.2
librt.so.1
libm.so.6
libpthread.so.0
其中和ELF文件同名的模块就是主模块。
获得Module对象后,可以用 x.base获得装载地址, x.name获得模块名称。 还可以使用 x.enumerateSymbols获得所有符号组成的列表 (如果使用enumerateExports只会包含到处符号,比如我们内部定义的fun函数就不会显示)
如果存在符号表,我们能使用 module_name.findSymbolByName 获得函数的pointer;但是如果函数是stripped的,比如在编译的时候:
1
gcc -s ./big.c -o big
那么enumerateSymbols的时候将返回空列表,而且findSymbolByName将会失效。 在逆向分析中经常能遇到这种程序。不过尽管名字消失了,但是地址是不变的,因此ida中会用sub_xxx来命令。在frida里,也可以用base + offset的方法来计算函数指针.
使用objdump找出目标函数的偏移量:
0000000000001149 <test>:
1149: 55 push %rbp
114a: 48 89 e5 mov %rsp,%rbp
114d: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1151: 48 8b 45 f8 mov -0x8(%rbp),%rax
1155: 5d pop %rbp
1156: c3 ret
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
// typescript对于类型很严格,mainModule可能为空,所以需要增加if判断
const mainModule = Process.findModuleByName('big');
if (mainModule != null) {
const baseAddr = mainModule.base;
console.log("************");
let testPtr_findByName = mainModule.getSymbolByName("test");
let testPtr_findByOffset = baseAddr.add(0x1149);
console.log(testPtr_findByName);
console.log(testPtr_findByOffset);
Interceptor.attach(testPtr_findByName, {
onEnter: function(args) {
console.log("first arg: ");
console.log(args[0], "\n");
},
onLeave: function(retval) {
// retval.replace(ptr("0x1234567890abcdef"));
console.log(retval);
console.log("return\n");
}
});
}
else {
console.log("main module not found");
}
intercepor
接着上面的程序,onEnter的args是一个NativePointer列表,通过下标访问的值可以直接用console.log打印出来(16进制)
修改参数:
1
arg[0] = ptr(xxx)
其中ptr就是NativePointer构造函数的简写方式,可以用数字或者字符串赋值。然而,js/ts有一个限制:普通的number类型最多记录2^53-1大小的数字,在处理64bit参数的时候会将高位截断。既然问题来自于js/ts的自动类型,最简单的绕开思路就是尝试使用别的类型,比如字符串赋值.