You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Windows调试器如何获取目标进程动态函数表列表

Windows动态注册函数表获取方法

在调试器附加到运行中进程、无法提前Hook注册API的场景下,不需要注入目标进程,就可以直接枚举所有通过RtlAddFunctionTable、RtlInstallFunctionTableCallback注册的动态函数表,核心逻辑是利用ntdll.dll内部维护的全局动态函数表链表——两个API注册的所有条目都会被加入这个链表,系统本身调用RtlLookupFunctionEntry查找不存在于PE异常目录的函数表时,也是遍历这个结构获取数据。

具体实现步骤

  • 定位ntdll.dll中的链表头
    链表头是ntdll的未导出全局变量RtlpDynamicFunctionTableListHead,你可以通过逆向当前系统版本ntdll中RtlAddFunctionTable的函数实现快速定位:RtlAddFunctionTable内部在完成条目填充后,会调用InsertTailList把新节点插入这个链表,顺着InsertTailList的第二个参数就能直接找到链表头地址。
    从Win10 1709到最新的Win11 24H2,64位系统下这个全局变量的偏移在不同ntdll版本间没有固定值,但定位逻辑完全通用,不需要硬编码偏移。
  • 按节点结构遍历链表
    64位系统下链表节点的结构在近10年的Windows版本中保持稳定,可直接使用如下定义解析:
    typedef enum _RTL_DYNAMIC_FTABLE_TYPE {
        RtlDynamicFtableNormal = 0,  // RtlAddFunctionTable注册的静态数组类型表
        RtlDynamicFtableCallback = 1 // RtlInstallFunctionTableCallback注册的回调类型表
    } RTL_DYNAMIC_FTABLE_TYPE;
    
    typedef struct _RTL_DYNAMIC_FTABLE_ENTRY {
        LIST_ENTRY Link;                // 双向链表节点
        PRUNTIME_FUNCTION FunctionTable;// 普通表类型下指向RUNTIME_FUNCTION数组首地址
        DWORD EntryCount;               // 普通表类型下数组的条目总数
        DWORD64 RangeBase;              // 当前函数表覆盖的内存地址范围基址
        DWORD64 RangeSize;              // 当前函数表覆盖的内存地址范围长度
        DWORD64 Reserved;
        PGET_RUNTIME_FUNCTION_CALLBACK CallbackFunc; // 回调表类型下的回调函数指针
        PVOID CallbackContext;          // 回调表注册时传入的上下文参数
        DWORD64 MaxGpReg;
        RTL_DYNAMIC_FTABLE_TYPE FtableType;
        PWSTR OutOfProcessCallbackDll;
    } RTL_DYNAMIC_FTABLE_ENTRY, *PRTL_DYNAMIC_FTABLE_ENTRY;
    
  • 遍历过程加入校验逻辑避免崩溃
    由于访问的是未导出结构,遍历过程必须做合法性校验,防止不同版本结构差异导致的内存访问错误:
    • 每拿到一个节点,先调用VirtualQueryEx校验RangeBase和RangeSize覆盖的内存区域是已提交的可执行内存,过滤无效节点
    • 对普通表类型的节点,校验FunctionTable指向的内存具备可读权限,且数组内每个RUNTIME_FUNCTION记录的起止地址都落在RangeBase~RangeBase+RangeSize区间内
    • 对回调表类型的节点,不需要手动解析函数条目,栈展开时如果RIP落在对应地址范围,直接调用保存的CallbackFunc并传入CallbackContext,就能拿到对应的RUNTIME_FUNCTION结构,和系统原生逻辑完全一致。

兜底兼容方案

如果不想维护不同Windows版本的结构偏移,可以直接实现一套和RtlLookupFunctionEntry逻辑一致的函数表查找逻辑:

对任意待展开的指令地址RIP,先枚举目标进程所有已加载模块,判断RIP是否落在某个模块的地址范围内,如果是则直接读取模块PE头的异常目录,获取对应的RUNTIME_FUNCTION;如果不在任何已加载模块范围内,再遍历上述动态函数表链表匹配地址范围,普通表用二分查找定位对应条目,回调表调用回调拿条目,最终拿到的结果和系统原生返回结果完全一致,可以直接传入RtlVirtualUnwind完成栈展开。

注意:Win7及更早版本的Windows没有使用双向链表存储动态函数表,而是采用红黑树结构,如果需要兼容这类已停止支持的系统,可以逆向对应版本ntdll中RtlAddFunctionTable的实现定位树的根节点,遍历逻辑和链表类似。

内容的提问来源于stack exchange,提问作者Artem Razin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 12:31:09