内存标记追踪法

适用场景

当我从后往前追数据时,使用堆栈回溯除了效率很低,有些数据还不在这个堆栈中赋值的,这样追踪起来很麻烦,而ttd录制对电脑的性能有一定要求,所以内存标记追踪法就比较有效的补充了这方面的技巧

概念

在内存创建时多创建一些字节记录创建时的堆栈或者大小信息,这样我们在追踪数据的时候可以查看这些信息定位到创建这个内存的代码位置,使用硬件断点即可定位到修改数据的关键位置

frida脚本



const kernelbase = Process.getModuleByName("KernelBase.dll");

const HeapAllocPtr = kernelbase.getExportByName("HeapAlloc");
const HeapFreePtr  = kernelbase.getExportByName("HeapFree");

console.log("[+] KernelBase.dll base:", kernelbase.base);
console.log("[+] HeapAlloc:", HeapAllocPtr);
console.log("[+] HeapFree :", HeapFreePtr);

const HeapAlloc = new NativeFunction(HeapAllocPtr,'pointer', ['pointer', 'uint32', 'size_t']);
const HeapFree = new NativeFunction(HeapFreePtr,'int', ['pointer', 'uint32', 'pointer']);
// 记录我们“改造过”的指针,防止误处理
const allocMap = new Map();



/*
BOOL HeapAlloc(
  HANDLE hHeap,
  DWORD  dwFlags,
  SIZE_T dwBytes
);
*/
Interceptor.replace(
    HeapAllocPtr,
    new NativeCallback(function (hHeap, dwFlags, dwBytes) {

        if(dwBytes < 0x38c*0x38c){
              // 调用原始 HeapAlloc
            return HeapAlloc(hHeap, dwFlags, dwBytes);
        }


        const extra = 0x10;
        const realSize = dwBytes.toNumber() + extra;

        // 调用原始 HeapAlloc
        const realPtr = HeapAlloc(hHeap, dwFlags, realSize);
        if (realPtr.isNull()) {
            return realPtr;
        }

        // 写 header
        // [0x00 - 0x07] "CZL"
        // [0x08 - 0x0F] size
        Memory.writeUtf8String(realPtr, "CZL");
        Memory.writeU64(realPtr.add(0x8), dwBytes);

        const userPtr = realPtr.add(extra);

        // 记录映射关系
        allocMap.set(userPtr.toString(), realPtr);
        if(dwBytes == 0x25BDC7)
        {  
            const tid = Process.getCurrentThreadId();

            console.log("========== HeapAlloc HIT ==========");
            console.log("TID:", tid);
            console.log("Size:", dwBytes);
            console.log("userPtr:",userPtr.toString());

            const bt = Thread.backtrace(this.context, Backtracer.ACCURATE)
                .map(DebugSymbol.fromAddress)
                .join("\n");

            console.log("Backtrace:\n" + bt);
            console.log("===================================");
        }    
        return userPtr;

    }, 'pointer', ['pointer', 'uint32', 'size_t'])
);

/*
BOOL HeapFree(
  HANDLE hHeap,
  DWORD  dwFlags,
  LPVOID lpMem
);
*/
Interceptor.replace(
    HeapFreePtr,
    new NativeCallback(function (hHeap, dwFlags, lpMem) {

        if (lpMem.isNull()) {
            return 1;
        }

        const key = lpMem.toString();
        let realPtr = null;

        if (allocMap.has(key)) {
            realPtr = allocMap.get(key);
            allocMap.delete(key);
        } else {
            // 不是我们分配的,直接走原逻辑
            return HeapFree(hHeap, dwFlags, lpMem);
        }

        return HeapFree(hHeap, dwFlags, realPtr);

    }, 'int', ['pointer', 'uint32', 'pointer'])
);

console.log("[+] HeapAlloc / HeapFree hooked");

使用方法

   if(dwBytes < 0x38c*0x38c){
              // 调用原始 HeapAlloc
            return HeapAlloc(hHeap, dwFlags, dwBytes);
        }

首先我们设置第一个添加,我们时大概知道目标内存的大小,但是不知道具体大小,所以我们设置一个该内存一定满足的条件,如上所示,当不满足条件则调用原始HeapAlloc

将脚本注入进程后,我们运行功能,定位到目标内存,这时在内存前面找一找我们设置的标记(CZL),找到标记后,在标记+8处有内存的大小,此时我们获取了目标内存的具体大小。

这次我们设置第二个条件

if(dwBytes == 0x25BDC7)
        {  
            const tid = Process.getCurrentThreadId();

            console.log("========== HeapAlloc HIT ==========");
            console.log("TID:", tid);
            console.log("Size:", dwBytes);
            console.log("userPtr:",userPtr.toString());

            const bt = Thread.backtrace(this.context, Backtracer.ACCURATE)
                .map(DebugSymbol.fromAddress)
                .join("\n");

            console.log("Backtrace:\n" + bt);
            console.log("===================================");
        }    

我们使用内存大小作为条件打印内存创建的堆栈信息、线程id和地址,然后再次注入脚本,运行功能,定位到目标内存,对比frida输出的地址(userPtr),就找到了目标内存创建时的堆栈,然后就可以定位到目标内存创建的代码位置,再使用硬件断点去定位修改内存的地方即可

posted @ 2026-01-11 17:44  乘舟凉  阅读(2)  评论(0)    收藏  举报