追踪Android方法调用2
字数 1486 2025-08-29 22:41:24

Android方法调用追踪技术详解

1. 前言

本文是《追踪Android方法调用》系列的第二部分,重点介绍如何通过Frida工具hook关键ART函数来追踪Android应用中的各种方法调用关系,包括:

  • Java->Java调用
  • Java->JNI调用
  • JNI->Java调用
  • JNI->JNI调用

2. 核心原理分析

所有方法调用最终都会进入ArtMethod::Invoke函数,但直接hook该函数存在局限性。更深入的追踪需要关注解释执行逻辑中的关键函数:

  • ArtInterpreterToInterpreterBridge:处理Java->Java调用
  • ArtInterpreterToCompiledCodeBridge:处理Java->JNI调用

3. hook ArtMethod::Invoke

3.1 函数签名

void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result, const char* shorty)

3.2 参数解析

通过shorty参数可以解析传入参数的类型和值:

function parse_arg_arrays(arg_arrays: NativePointer, shorty: string) {
  let result:any = {};
  let count = 0
  let j = 0;
  let tmp_shorty = shorty.slice(1);
  for (let i of tmp_shorty) {
    switch (i) {
      case 'Z': case 'B': case 'C': case 'S': case 'I': case 'F': case 'L':
        result[`arg${j}`] = "0x" + arg_arrays.add(count).readU32().toString(16)
        count += 4; j += 1; break;
      case 'D': case 'J':
        result[`arg${j}`] = "0x" + arg_arrays.add(count).readU64().toString(16)
        count += 8; j += 1; break;
      case 'V': break;
      default: console.log("error shorty:", i); break
    }
  }
  return result;
}

3.3 获取方法名

通过ArtMethod::PrettyMethod获取被调用方法名:

function find_func(so_name: string, export_name: string, ret_type: any, args_type: any) {
  let addr = Module.findExportByName(so_name, export_name);
  let called_name = null;
  if (addr) {
    called_name = new NativeFunction(addr, ret_type, args_type);
  }
  return called_name;
}

let pretty_method_func: any = find_func("libart.so", "_ZN3art9ArtMethod12PrettyMethodEb", 
                                      ["pointer", "pointer", "pointer"], ["pointer", "bool"]);

3.4 完整hook代码

export function hook_ArtMethod_Invoke() {
    let module = Process.findModuleByName("libart.so");
    let symbols = module ? module.enumerateSymbols() : [];
    let addr_artmethod_invoke = null;
    
    // 查找ArtMethod::Invoke符号
    for (let symbol of symbols) {
        if (symbol.name.indexOf("_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc") >= 0) {
            addr_artmethod_invoke = symbol.address;
            break;
        }
    }

    if (addr_artmethod_invoke) {
        Interceptor.attach(addr_artmethod_invoke, {
            onEnter: function (args) {
                this.artmethod = args[0];
                this.tid = args[1].add(0x10).readU32();
                this.arg_arrays = args[2];
                this.shorty = args[5].readCString();
                
                let func_name = readStdString(pretty_method_func(ptr(this.artmethod), 1));
                this.hooked = 0;
                
                if (func_name?.includes("com.example")) {
                    this.hooked = 1;
                    let artmethod_args = parse_arg_arrays(this.arg_arrays, this.shorty);
                    console.log("[+] [" + this.threadId + "] ArtMethod::Invoke onEnter:", 
                              ptr(this.artmethod), func_name);
                    
                    if (Object.keys(artmethod_args).length > 0) {
                        console.log("----------args----------\n", 
                                  JSON.stringify(artmethod_args), "\n----------end-----------");
                    }
                }
            },
            onLeave: function (retval) {
                // 可在此处处理返回值
            }
        });
    }
}

3.5 局限性

  • 无法捕获Java->Java的直接调用(绕过ArtMethod::Invoke)
  • 无法获取调用者(caller)信息

4. 处理Java层调用追踪

4.1 ShadowFrame结构分析

ShadowFrame内存布局:

ShadowFrame* link_;           // 指向调用者frame
ArtMethod* method_;           // 当前方法
JValue* result_register_;     // 结果寄存器
const uint16_t* dex_pc_ptr_;   // dex pc指针
const uint16_t* dex_instructions_; // dex指令
LockCountData lock_count_data_; // 锁计数数据(指针大小)
const uint32_t number_of_vregs_; // 虚拟寄存器数量
uint32_t dex_pc_;             // dex pc值
int16_t cached_hotness_countdown_;
int16_t hotness_countdown_;   // 热度计数
uint32_t frame_flags_;        // 帧标志
uint32_t vregs_[0];           // 虚拟寄存器数组

4.2 参数解析关键点

  • vregs_包含参数和局部变量
  • 实际参数数量由CodeItem中的ins_size_决定
  • 参数位置:vregs[number_of_vregs_ - ins_size : number_of_vregs]

4.3 hook ArtInterpreterToInterpreterBridge

用于追踪Java->Java调用:

void ArtInterpreterToInterpreterBridge(Thread* self,
                                     const CodeItemDataAccessor& accessor,
                                     ShadowFrame* shadow_frame,
                                     JValue* result);

通过shadow_frame->link_可以获取调用者信息。

4.4 hook ArtInterpreterToCompiledCodeBridge

用于追踪Java->JNI调用:

void ArtInterpreterToCompiledCodeBridge(Thread* self,
                                      ArtMethod* caller,
                                      ShadowFrame* shadow_frame,
                                      uint16_t arg_offset,
                                      JValue* result);

该函数直接提供了caller参数,无需通过link_解析。

5. 处理JNI层调用追踪

5.1 挑战

  • ArtInterpreterToXxxBridge无法监测JNI层发起的调用
  • JNI调用会通过art_quick_invoke_stubart_quick_invoke_static_stub

5.2 解决方案

hook InvokeWithArgArray函数,其签名:

void InvokeWithArgArray(const ScopedObjectAccessAlreadyRunnable& soa,
                       ArtMethod* method,
                       ArgArray* arg_array,
                       JValue* result,
                       const char* shorty)

可以使用类似ArtMethod::Invoke的参数解析方法来处理。

6. 读取内存字符串

当参数类型为java.lang.String时,需要特殊处理:

// 在parse_arg_arrays函数中添加字符串处理
case 'L':
    if (func_name.includes("java.lang.String")) {
        let str_ptr = arg_arrays.add(count).readPointer();
        if (!str_ptr.isNull()) {
            let is_compressed = str_ptr.add(8).readU32() & 1;
            let count = str_ptr.add(12).readU32();
            let value_ptr = str_ptr.add(16);
            
            if (is_compressed) {
                result[`arg${j}`] = value_ptr.readUtf8String(count);
            } else {
                result[`arg${j}`] = value_ptr.readUtf16String(count);
            }
        }
    }
    count += 4; j += 1; break;

7. 完整实现

完整代码可参考GitHub仓库:
https://github.com/youncyb/trace_android_call

8. 参考资源

  1. 纯frida实现smali追踪
  2. 使用Frida打印Java类函数调用关系
  3. [libc++'s implementation of std::string](libc++'s implementation of std::string)

9. 总结

本文详细介绍了通过Frida hook ART关键函数来追踪Android方法调用的技术,包括:

  • 参数解析技术
  • ShadowFrame结构分析
  • Java和JNI调用的不同处理方式
  • 内存字符串读取方法

这套技术可以用于Android应用逆向分析、安全审计和性能优化等多个领域。

Android方法调用追踪技术详解 1. 前言 本文是《追踪Android方法调用》系列的第二部分,重点介绍如何通过Frida工具hook关键ART函数来追踪Android应用中的各种方法调用关系,包括: Java->Java调用 Java->JNI调用 JNI->Java调用 JNI->JNI调用 2. 核心原理分析 所有方法调用最终都会进入 ArtMethod::Invoke 函数,但直接hook该函数存在局限性。更深入的追踪需要关注解释执行逻辑中的关键函数: ArtInterpreterToInterpreterBridge :处理Java->Java调用 ArtInterpreterToCompiledCodeBridge :处理Java->JNI调用 3. hook ArtMethod::Invoke 3.1 函数签名 3.2 参数解析 通过 shorty 参数可以解析传入参数的类型和值: 3.3 获取方法名 通过 ArtMethod::PrettyMethod 获取被调用方法名: 3.4 完整hook代码 3.5 局限性 无法捕获Java->Java的直接调用(绕过ArtMethod::Invoke) 无法获取调用者(caller)信息 4. 处理Java层调用追踪 4.1 ShadowFrame结构分析 ShadowFrame内存布局: 4.2 参数解析关键点 vregs_ 包含参数和局部变量 实际参数数量由CodeItem中的 ins_size_ 决定 参数位置: vregs[number_of_vregs_ - ins_size : number_of_vregs] 4.3 hook ArtInterpreterToInterpreterBridge 用于追踪Java->Java调用: 通过 shadow_frame->link_ 可以获取调用者信息。 4.4 hook ArtInterpreterToCompiledCodeBridge 用于追踪Java->JNI调用: 该函数直接提供了 caller 参数,无需通过 link_ 解析。 5. 处理JNI层调用追踪 5.1 挑战 ArtInterpreterToXxxBridge 无法监测JNI层发起的调用 JNI调用会通过 art_quick_invoke_stub 和 art_quick_invoke_static_stub 5.2 解决方案 hook InvokeWithArgArray 函数,其签名: 可以使用类似 ArtMethod::Invoke 的参数解析方法来处理。 6. 读取内存字符串 当参数类型为 java.lang.String 时,需要特殊处理: 7. 完整实现 完整代码可参考GitHub仓库: https://github.com/youncyb/trace_ android_ call 8. 参考资源 纯frida实现smali追踪 使用Frida打印Java类函数调用关系 [ libc++'s implementation of std::string ](libc++'s implementation of std::string) 9. 总结 本文详细介绍了通过Frida hook ART关键函数来追踪Android方法调用的技术,包括: 参数解析技术 ShadowFrame结构分析 Java和JNI调用的不同处理方式 内存字符串读取方法 这套技术可以用于Android应用逆向分析、安全审计和性能优化等多个领域。