从frida到va_list
背景
在用frida hook CallStaticVoidMethodV 函数参数的时候,发现va_list结构有些复杂,不好hook,故进行了学习
va_list介绍
va_list 是在c语言中解决变参问题的一组宏。
va_list的内部结构由编译器与平台架构决定,不同组合可能会导致va_list 的定义和行为存在显著差异。
在 x86_64 架构下 gcc/clang 编译器下的 va_list定义如下:
typedef struct {
unsigned int gp_offset; // 通用寄存器偏移量
unsigned int fp_offset; // 浮点寄存器偏移量
void *overflow_arg_area; // 超出寄存器传递的参数区指针
void *reg_save_area; // 寄存器保存区指针
} __va_list[1];
typedef __va_list va_list;
参数介绍:
reg_save_area,参数指向寄存器保存区域的开始。overflow_arg_area,该指针用于获取在堆栈上传递的参数。它被初始化为堆栈上传递的第一个参数的地址(如果有的话),然后总是更新为指向堆栈上下一个参数的开始。gp_offset,元素以字节为单位保存从reg_save_area到保存下一个可用通用参数寄存器的位置的偏移量。如果所有参数寄存器都已用尽,则将其设置为值48(6 * 8)。fp_offset,元素保存从reg_save_area到保存下一个可用浮点参数寄存器的位置的偏移量(以字节为单位)。如果所有参数寄存器都已用尽,则将其设置为值304(6 * 8 + 16 * 16)。(每个通用寄存器占8字节,浮点寄存器占16字节)- ps:
经过我的调试,可以发现,gp_offset与fp_offset这俩值都是在不断变化的,使reg_save_area或overflow_arg_area+fp_offset或gp_offset后,始终指向下一个参数的地址。
我们可以看到,这里引入了一个叫做 寄存器保存区域 的概念,我们可能会想,既然已经是寄存器传参了,那寄存器保存区域是干嘛的呢?
事实上,如果我们定义了一个变参函数,我们在进入这个函数的时候,可以发现,这些本该由寄存器传递的参数会被保存到栈上。如下图:
这实际上是为了方便 va_list与其相关宏函数的工作:
va_list的相关宏函数
-
va_startva_list ap; va_start(ap, last)函数功能: 初始化
va_list变量ap,使其指向函数参数列表中的第一个可变参数。last是最后一个固定参数的名称。 -
va_argva_arg(va_list ap, type)获取当前参数的值,并使
ap指向下一个参数。type是参数的类型。 -
va_endva_end(va_list ap)清理
va_list变量ap。
举个例子,printf的内部实现:
#include <stdio.h>
#include <stdarg.h>
int printf(const char *format, ...) {
va_list args;
va_start(args, format); // 初始化 args,使其指向第一个可变参数
int result = vprintf(format, args); // 调用 vprintf 处理可变参数
va_end(args); // 清理 args
return result;
}
va_start宏会初始化args,使其指向format参数之后的第一个可变参数。vprintf是printf的一个变体,直接接受一个va_list参数。它负责解析格式字符串并处理可变参数。va_end宏完成对va_list的清理,释放任何与其关联的资源。
从上面可以看到,va_start会初始化 args,使其指向 format 参数之后的第一个可变参数。如果我们没有把寄存器放到栈上,是不是就不能获取 format参数后的第一个可变参数地址了呢?
我们再来看一下这些宏的实现:
typedef char * va_list;
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) ) //这个宏确保返回的大小是 sizeof(int) 的倍数,以满足对齐要求。
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) //使ap指向第一个变参的位置(紧随v的地址)
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) // 先讲 ap自加t个单位,再通过 (ap-t)取出元素。
#define va_end(ap) ( ap = (va_list)0 ) //清空va_list,即结束变参的获取
#define va_copy(dest,src) // 复制 va_list 对象
手动实现一下变参函数:
实现参数的遍历:
#include <stdio.h>
#include <stdarg.h>
// 可变参数函数示例:打印所有参数两次
void print_args_twice(int count, ...) {
va_list args1, args2;
// 初始化 args1,指向第一个可变参数
va_start(args1, count);
// 复制 args1 到 args2
va_copy(args2, args1);
// 第一次遍历:打印所有参数
printf("First traversal:\n");
for(int i = 0; i < count; i++) {
int num = va_arg(args1, int);
printf("Argument %d: %d\n", i + 1, num);
}
// 第二次遍历:再次打印所有参数
printf("Second traversal:\n");
for(int i = 0; i < count; i++) {
int num = va_arg(args2, int);
printf("Argument %d: %d\n", i + 1, num);
}
// 清理 va_list 对象
va_end(args1);
va_end(args2);
}
int main() {
print_args_twice(3, 10, 20, 30);
return 0;
}
frida hook CallObjectMethodV 中的 va_list参数
aarch64 & gcc编译器下的va_list结构
事实上,arrch64 & gcc编译器下的 va_list结构体与 x86 & gcc下的 va_list 结构体略有不同:
/* 关于函数栈的构成可参考[3], AAPCS64中可变参函数的说明可参考[1] */
typedef struct {
void *__stack; /* __stack 记录下一个需要通过栈传递参数的存储位置(实际上就是上面x86 va_list下的overflow_arg_area的功能), 随着va_arg的调用可能变化 */
void *__gr_top; /*指向通用寄存器(General Registers)区域的顶部。*/
void *__vr_top; /* 指向向量寄存器(Vector Registers)区域的顶部。 */
int __gr_offs; /* __gr_offs 记录下一个匿名通用寄存器参数到__gr_top的偏移(负数),随着va_arg的调用可能变化 */
int __vr_offs; /* __vr_offs 记录下一个匿名浮点寄存器参数到__vr_top的偏移(负数),随着va_arg的调用可能变化 */
} __builtin_va_list;
void __stack;
- 作用:指向参数列表中位于栈上的部分。
- 解释:在调用函数时,参数可能通过寄存器传递,也可能通过栈传递。
__stack指针用于跟踪那些未通过寄存器传递、需要从栈中读取的参数的位置。
void __gr_top;
- 作用:指向通用寄存器(General Registers)区域的顶部。
- 解释:在许多体系结构(例如 x86-64)中,前几个参数会通过通用寄存器(如
RDI,RSI,RDX,RCX,R8,R9)传递。__gr_top用于指向这些寄存器中可用的下一个位置,以便依次读取通过寄存器传递的参数。
void __vr_top;
- 作用:指向向量寄存器(Vector Registers)区域的顶部。
- 解释:某些参数(如浮点数或 SIMD 类型)可能通过向量寄存器传递。
__vr_top用于跟踪这些向量寄存器中可用的位置,确保能够正确读取通过向量寄存器传递的参数。
int __gr_offs;
- 作用:通用寄存器区域的偏移量。
- 解释:
__gr_offs用于记录已经使用的通用寄存器数量或字节偏移量。这有助于在读取下一个通过通用寄存器传递的参数时,正确调整__gr_top指针的位置。
int __vr_offs;
- 作用:向量寄存器区域的偏移量。
- 解释:类似于
__gr_offs,__vr_offs用于记录已经使用的向量寄存器数量或字节偏移量,确保能够正确管理通过向量寄存器传递的参数。
事实上,我觉得不同编译器很有可能影响 va_list的结构,我的建议还是具体编译器具体分析,如下图,就是arrch64下gcc编译的.so的va_list的结构体


jstring 结构体 句柄
看源码可以看到,jstring是继承于 jobject类的

事实上,jobject以及其派生类(jstring、jclass等)都被视为句柄,而不是直接的结构体指针。
在使用 jstring等jobject类的时候,程序会根据句柄值,去JVM中维护的句柄表中来处理数据,这种做法有助于jvm垃圾回收机制的实现,也能够避免本地代码直接操作JVM内存,从而提高安全性和稳定性。
总的来说,jstring 保存的并不是一个指向结构图的指针,而是一个句柄值。
frida hook CallObjectMethodV 得到va_list中的jstring
js代码如下:
function hook_jni3(offset,jni_name){
var jniEnv = Java.vm.getEnv();
try{
const nativeInterfacePtr = Memory.readPointer(jniEnv);
console.log("JNINativeInterface addr:", nativeInterfacePtr);
var jni_addr = nativeInterfacePtr.add(offset).readPointer()
console.log(jni_name + " addr : " + jni_addr)
Interceptor.attach(ptr(jni_addr), {
onEnter: function(args) {
try{
var env = args[0]
var thiz = args[1]
var methodID = deepClone(args[2])
var va_list = args[3]
// 检测参数是否包含 "String"类 (我这里省略了实现方法)
// ...
Java.perform(function(){
try{
const stack = va_list.readPointer()
const gr_top = va_list.add(8).readPointer()
const vr_top = va_list.add(0x10).readPointer()
const gr_offset = va_list.add(0x18).readInt() // arrch64 & gcc下的va_list的gr_offset 实际上是负数,需要readint,而不是readuint
const vr_offset = va_list.add(0x18+4).readInt()
const arg1_addr = gr_top.add(gr_offset).readPointer() // 读jstring的句柄
var char_string= Java.vm.getEnv().getStringUtfChars(arg1_addr,false).readCString() // 获
console.log("=====> " + char_string)
}catch(e){
console.log(e)
}
})
}catch(e){
console.log(e)
}
},
onLeave: function(retval) {
}
});
}catch(e){
console.log(e)
}
}
var CallObjectMethodV_offset = 0x0000118
hook_jni3(CallObjectMethodV_offset,"CallObjectMethodV")

浙公网安备 33010602011771号