版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/

定位JNI方法内存地址

首先先找到 JNI 方法在内存中的地址,具体可以参考这篇文章【使用 Frida 定位 JNI 方法内存地址

比如,这里需要找到 lte.NCall 的 IL 方法的内存地址

methodName-> public static native byte lte.NCall.IB(java.lang.Object[])
Func.offset== libGameVMP.so 0xDFA8
Func.getArtMethod->native_addr: 0X7750F94FA8
methodName-> public static native char lte.NCall.IC(java.lang.Object[])
Func.offset== libGameVMP.so 0xDFA8
Func.getArtMethod->native_addr: 0X7750F94FA8
methodName-> public static native double lte.NCall.ID(java.lang.Object[])
Func.offset== libGameVMP.so 0xE028
Func.getArtMethod->native_addr: 0X7750F95028
methodName-> public static native float lte.NCall.IF(java.lang.Object[])
Func.offset== libGameVMP.so 0xDFE8
Func.getArtMethod->native_addr: 0X7750F94FE8
methodName-> public static native int lte.NCall.II(java.lang.Object[])
Func.offset== libGameVMP.so 0xDFA8
Func.getArtMethod->native_addr: 0X7750F94FA8
methodName-> public static native long lte.NCall.IJ(java.lang.Object[])
Func.offset== libGameVMP.so 0xDFA8
Func.getArtMethod->native_addr: 0X72E9D1CFA8
methodName-> public static native java.lang.Object lte.NCall.IL(java.lang.Object[])
Func.offset== libGameVMP.so 0xDFA8
Func.getArtMethod->native_addr: 0X7750F94FA8
methodName-> public static native short lte.NCall.IS(java.lang.Object[])
Func.offset== libGameVMP.so 0xDFA8
Func.getArtMethod->native_addr: 0X7750F94FA8
methodName-> public static native void lte.NCall.IV(java.lang.Object[])
Func.offset== libGameVMP.so 0xDFA8
Func.getArtMethod->native_addr: 0X7750F94FA8
methodName-> public static native boolean lte.NCall.IZ(java.lang.Object[])
Func.offset== libGameVMP.so 0xDFA8
Func.getArtMethod->native_addr: 0X7750F94FA8
methodName-> public static native int lte.NCall.dI(int)
Func.offset== libGameVMP.so 0xB93C
Func.getArtMethod->native_addr: 0X7750F9293C
methodName-> public static native long lte.NCall.dL(long)
Func.offset== libGameVMP.so 0xBAD0
Func.getArtMethod->native_addr: 0X7750F92AD0
methodName-> public static native java.lang.String lte.NCall.dS(java.lang.String)
Func.offset== libGameVMP.so 0xBAEC
Func.getArtMethod->native_addr: 0X7750F92AEC

通过调用 get_jni_method_addr(“lte.NCall”) ,得到内存地址为 0X72E9D1CFA8

退出 frida

[Remote::com.xxx.duapp]-> exit

Thank you for using Frida!

附加到进程并设置断点

IDA 附近到目标 app image.png

按 G 跳转到 0X72E9D1CFA8 下断点 image.png

有反调试 image.png

把其他线程先全部挂起 image.png

关于使用IDA Pro调试android app详细教程可以参考这篇文章【使用IDA Pro动态调试Android APP】。

使用 IDA trace 跟踪函数执行流程

1. 开启 tracing

tracing options 设置:

  • 设置 trace 文件保存路径

  • 去掉勾选 Trace over debugger segments(控制的是 tracing 操作是否仅限于当前调试器的代码段(segments),还是可以跨越所有内存段执行指令追踪。),减少不必要的 tracing 数据量

image.png

按 G 跳转到目标函数地址,在函数开始地址下断点 image.png

断点触发,打开 Instruction tracing,然后 F8 单步执行 image.png

可以看到执行过的指令都会黄色高亮显示 image.png

执行到函数 RET 位置,关闭 Instruction tracing image.png

2. 查看 trace 记录

打开 Tracing window 可以看到跟踪记录的每条指令和寄存器的值 image.png

image.png

打开 trace 文件也可以看到跟踪记录 image.png

3. trace 回放

保存 trace 文件 image.png

image.png

调试器选择 Trace replayer image.png

tracing window 中加载保存的 trc 文件 image.png

按 F9 开始重新执行 trace 记录

参考:

编写 Python 脚本自动化跟踪

代码流程

编写一个 ida python 脚本,代码流程大概如下:

  • 接受两个参数,传入开始地址和结束地址,

  • 在开始地址和结束地址处下断点,

  • 在开始断点触发时清理 tracing & 开启 Instruction tracing & 挂起其他线程(过反调试),

  • 创建 MyDbgHook 类继承 ida_dbg.DBG_Hooks 实现自定义单步跟踪逻辑

  • 执行到结束地址时关闭 Instruction tracing & 恢复所有线程执行 & 移除断点 & 解除 hook。

ida python3 api 库路径:IDA_Pro_7.7\python\3 image.png

代码实现

import idaapi
import idc
import idautils
import ida_dbg
import ida_funcs


class MyDbgHook(ida_dbg.DBG_Hooks):
    # 是否正在进行指令跟踪
    is_tracing = False

    # 标记是否刚刚到达过结束地址
    was_at_end_addr = False

    def __init__(self, start_addr, end_addr):
        super().__init__()
        self.start_addr = start_addr
        self.end_addr = end_addr

    def dbg_bpt(self, tid, ea):
        """处理断点事件"""
        current_addr = idc.get_reg_value("pc")  # ARM64 使用 PC 寄存器
        print(f"断点触发在: {hex(current_addr)}")

        # 如果是在开始地址处的断点,开启指令跟踪
        if current_addr == self.start_addr:
            # 开启指令跟踪
            self.enable_tracing()
            # 单步跟踪
            self.step_and_trace(self.start_addr, self.end_addr)

        return 0

    def dbg_step_into(self) -> bool:
        """处理单步事件"""
        self.step_and_trace(self.start_addr, self.end_addr)
        return True

    def dbg_step_over(self):
        """处理单步事件"""
        self.step_and_trace(self.start_addr, self.end_addr)
        return True

    def enable_tracing(self):
        """开启指令跟踪"""
        if not self.is_tracing:
            self.is_tracing = True
            # 清除所有 tracing 数据
            ida_dbg.clear_trace()
            # 开启指令跟踪
            ida_dbg.enable_insn_trace()
            # 挂起其他线程
            suspend_other_threads()
            print("清除所有 tracing 数据,开启指令跟踪,挂起其他线程")

    def disable_tracing(self):
        """关闭指令跟踪"""
        self.is_tracing = False
        # 关闭指令跟踪
        ida_dbg.disable_insn_trace()
        print("关闭指令跟踪")

        # 移除开始和结束地址的断点
        idaapi.del_bpt(self.start_addr)
        idaapi.del_bpt(self.end_addr)
        print(f"移除断点: {hex(self.start_addr)}, {hex(self.end_addr)}")

        # 解除 hook
        self.unhook()
        print("解除hook")

        # 恢复线程
        resume_all_threads()

    def step_and_trace(self, start_addr, end_addr):
        """单步跟踪指令,遇到函数调用时步入"""
        if self.is_tracing:
            current_addr = idc.get_reg_value("pc")  # ARM64 用 pc 寄存器

            # 检查当前地址是否为结束地址
            if current_addr == end_addr:
                print(f"[{hex(start_addr)}] 已到达结束地址,执行结束指令")
                ida_dbg.request_step_over()  # 执行完结束地址指令
                self.was_at_end_addr = True
                return

            # 检查是否刚刚执行完结束地址指令
            if current_addr != end_addr and self.was_at_end_addr:
                print(f"[{hex(start_addr)}] 已执行完结束地址指令,关闭 tracing")
                self.disable_tracing()
                idaapi.continue_process()
                return

            print(f"[{hex(current_addr)}] step over")
            ida_dbg.request_step_over()  # 单步跟踪


def get_module_by_addr(addr):
    """根据地址获取所属的模块(段)起始地址"""
    for seg in idautils.Segments():
        if seg <= addr < idc.get_segm_end(seg):
            return seg
    return None


def is_in_current_module(addr, start_addr):
    """判断当前地址是否与开始地址处于相同的模块"""
    start_module = get_module_by_addr(start_addr)
    current_module = get_module_by_addr(addr)

    # 比较当前地址和开始地址是否在相同的模块(段)中
    if start_module == current_module:
        return True
    return False


def is_call_instruction(ea):
    """判断当前地址是否是一个调用函数的指令(包括间接跳转)"""
    mnem = idc.print_insn_mnem(ea)
    # ARM64 中的函数调用指令包括 BL, BLR, BR
    return mnem in ["BL", "BLR", "BR"]


def get_call_target(ea):
    """获取调用指令的目标地址,如果是间接调用则返回寄存器中的值"""
    if is_call_instruction(ea):
        mnem = idc.print_insn_mnem(ea)

        # 对于 BL/BLR 指令,获取目标地址
        if mnem in ["BL", "BLR"]:
            return idc.get_operand_value(ea, 0)

        # 对于 BR 指令,目标地址存储在寄存器中
        if mnem == "BR":
            reg_name = idc.print_operand(ea, 0)  # 获取寄存器名,例如 X17
            reg_val = idc.get_reg_value(reg_name)  # 获取寄存器中的值(目标地址)
            return reg_val
    return None


def set_breakpoints(start_addr, end_addr):
    """设置断点"""
    # 在开始地址设置断点
    idaapi.add_bpt(start_addr)
    print(f"在 {hex(start_addr)} 设置断点")

    # 在结束地址设置断点
    idaapi.add_bpt(end_addr)
    print(f"在 {hex(end_addr)} 设置断点")


def suspend_other_threads():
    """
    挂起除当前线程外的其他线程
    """
    # 获取当前线程ID
    current_tid = ida_dbg.get_current_thread()

    # 获取线程数量
    thread_qty = ida_dbg.get_thread_qty()

    # 遍历所有线程并获取线程 ID
    for i in range(thread_qty):
        tid = ida_dbg.getn_thread(i)
        if tid != current_tid:
            # 挂起所有非当前线程
            success = ida_dbg.suspend_thread(tid)
            if success:
                print(f"Suspended thread {tid}")
            else:
                print(f"Failed to suspend thread {tid}")

    print(f"Current thread {current_tid} remains active.")


def resume_all_threads():
    """
    恢复所有线程的运行状态
    """
    # 获取线程数量
    thread_qty = ida_dbg.get_thread_qty()

    # 遍历所有线程并获取线程 ID
    for i in range(thread_qty):
        tid = ida_dbg.getn_thread(i)
        # 恢复线程
        success = ida_dbg.resume_thread(tid)
        if success:
            print(f"Resumed thread {tid}")

    print("All threads resumed.")


if __name__ == "__main__":
    # 开始和结束地址
    start_addr = 0X77518A5F10
    end_addr = start_addr + 0X84

    print(f"ida trace start[{hex(start_addr)}] end[{hex(end_addr)}]")

    # 设置断点
    set_breakpoints(start_addr, end_addr)

    # 创建并启用调试钩子
    hook = MyDbgHook(start_addr, end_addr)
    hook.hook()  # 激活钩子

IDA 中使用

修改脚本代码中的开始和结束地址 image.png

Alt + F7 执行脚本文件 image.png

断点触发后按 F8 执行自动跟踪 image.png

执行完成 image.png