从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_areaoverflow_arg_area + fp_offsetgp_offset 后,始终指向下一个参数的地址。

我们可以看到,这里引入了一个叫做 寄存器保存区域 的概念,我们可能会想,既然已经是寄存器传参了,那寄存器保存区域是干嘛的呢?

事实上,如果我们定义了一个变参函数,我们在进入这个函数的时候,可以发现,这些本该由寄存器传递的参数会被保存到栈上。如下图:

image-20241211114755419 image-20241211114812136

这实际上是为了方便 va_list与其相关宏函数的工作:

va_list的相关宏函数

  • va_start

    va_list ap;
    va_start(ap, last)
    

    函数功能: 初始化 va_list 变量 ap,使其指向函数参数列表中的第一个可变参数。last 是最后一个固定参数的名称。

  • va_arg

    va_arg(va_list ap, type)
    

    获取当前参数的值,并使 ap 指向下一个参数。type 是参数的类型。

  • va_end

    va_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 参数之后的第一个可变参数
  • vprintfprintf 的一个变体,直接接受一个 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的结构体

image-20241212165235491

image-20241212165246894

jstring 结构体 句柄

看源码可以看到,jstring是继承于 jobject类的

image-20241212194722975

事实上,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")

posted @ 2024-12-12 20:05  TLSN  阅读(261)  评论(0)    收藏  举报