跳转至

生产中绕过 ASLR

禁用 ASLR 适合机器人开发,但在生产中不可取;不能指望最终用户关闭 ASLR。相反,你可以编写一个函数在运行时动态重新定位地址。如果你使用 XP 基址的地址,重新定位的代码看起来像这样:

DWORD rebase(DWORD address, DWORD newBase) {
    DWORD diff = address - 0x400000;
    return diff + newBase;
}

当你知道游戏的基址(newBase)时,这个函数允许你通过重新定位 address 来基本上忽略 ASLR。

不过,要找到 newBase,你需要使用 GetModuleHandle() 函数。当 GetModuleHandle() 的参数为 NULL 时,它总是返回进程中主二进制的句柄。函数的返回类型是 HMODULE,但该值实际上就是二进制被映射到的地址。这就是基址,所以你可以直接把它转换为 DWORD 得到 newBase。但由于你要在另一个进程中寻找基址,你需要一种在该进程上下文中执行函数的方法。

要做到这一点,使用 CreateRemoteThread() API 函数调用 GetModuleHandle(),这个函数可以在远程进程中生成线程并执行代码。它的原型如清单 6-8 所示。

HANDLE CreateRemoteThread(
    HANDLE Process,
    LPSECURITY_ATTRIBUTES ThreadAttributes,
    DWORD StackSize,
    LPTHREAD_START_ROUTINE StartAddress,
    LPVOID Param,
    DWORD CreationFlags,
    LPDWORD ThreadId
);

清单 6-8:生成线程的函数

生成的线程将在 StartAddress 开始执行,把它当作一个单参数函数,Param 作为输入,并把返回值设置为线程退出代码。这很理想,因为线程可以用 StartAddress 指向 GetModuleHandle() 的地址、Param 设为 NULL 来启动。然后你可以用 API 函数 WaitForSingleObject() 等待线程执行完毕,并用 API 函数 GetExitCodeThread() 获取返回的基址。

把所有内容串在一起后,从外部机器人获取 newBase 的代码应该看起来像清单 6-9。

DWORD newBase;
// 获取 kernel32.dll 的地址
HMODULE k32 = GetModuleHandle("kernel32.dll");
// 获取 GetModuleHandle() 的地址
LPVOID funcAdr = GetProcAddress(k32, "
GetModuleHandleA");
    funcAdr = GetProcAddress(k32, "
GetModuleHandleW");
// 创建线程
HANDLE thread =
    CreateRemoteThread(process, NULL, NULL,
        (LPTHREAD_START_ROUTINE)funcAdr,
        NULL, NULL, NULL);
// 让线程完成
WaitForSingleObject(thread, INFINITE);
// 获取退出代码
GetExitCodeThread(thread, &newBase);
// 清理线程句柄
CloseHandle(thread);

清单 6-9:用 API 函数找到游戏的基址

GetModuleHandle() 函数是 kernel32.dll 的一部分,它在每个进程中都有相同的基址,所以这段代码首先获取 kernel32.dll 的地址。由于 kernel32.dll 的基址在每个进程中都相同,GetModuleHandle() 的地址在游戏中与在外部机器人中相同。有了 kernel32.dll 的基址,这段代码用 API 函数 GetProcAddress()轻松找到 GetModuleHandle() 的地址。然后它调用清单 6-8 中的 CreateRemoteThread() 函数,让线程完成它的工作,并获取退出代码得到 newBase。