Post

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的自动类型,最简单的绕开思路就是尝试使用别的类型,比如字符串赋值.

This post is licensed under CC BY 4.0 by the author.