跳转至

击败二进制验证

击败二进制验证就像不在游戏特定二进制内放置钩子一样简单。Windows API 函数上的跳转钩子和 IAT 钩子极其常见,所以只要可以,尽量使用这些方法,而不是在游戏二进制中使用跳转或近调用钩子。在必须直接钩住游戏代码的情况下,你可以通过拦截二进制扫描并欺骗数据匹配软件期望内容,来欺骗反作弊软件的二进制验证例程。

与 SBD 一样,二进制验证通常用 NtQueryVirtualMemory()扫描内存。要欺骗验证代码,从该函数上的钩子开始。然后,编写一个像这样的函数,在调用 NtQueryVirtualMemory() 时欺骗数据:

NTSTATUS onNtQueryVirtualMemory(
    HANDLE process, PVOID baseAddress,
    MEMORY_INFORMATION_CLASS 
memoryInformationClass,
    PVOID buffer, ULONG numberOfBytes, PULONG 
numberOfBytesRead) {
        process, baseAddress,
        memoryInformationClass,
        buffer, numberOfBytes, numberOfBytesRead);
    // 把棘手代码放在这里的某处
    return ret;
}

在这个钩子内部,你需要留意对已被你某个钩子修改的内存的任何扫描。

 这个示例假设机器人只有一个钩子,并且前缀为 HOOK_ 的变量已经存在,描述钩子替换的代码。

清单 12-2 展示了一些扫描监控代码。

// 扫描是否针对当前进程?
bool currentProcess =
    process == INVALID_HANDLE_VALUE ||
    process == GetCurrentProcess();
// 钩子是否在被扫描的内存范围内?
auto endAddress = baseAddress + numberOfBytesRead 
- 1;
bool containsHook =
    (HOOK_START_ADDRESS >= baseAddress &&
    HOOK_START_ADDRESS <= endAddress) ||
    (HOOK_END_ADDRESS >= baseAddress &&
    HOOK_END_ADDRESS <= endAddress);
 if (currentProcess && containsHook) {
    // 隐藏钩子
}

清单 12-2:检查被钩内存是否正在被扫描

对被钩代码进行内存扫描时(这会让 currentProcess 和 containsHook 同时变为 true),if() 语句①内部的代码会更新输出缓冲区,反映原始代码。这意味着你必须知道被钩代码在扫描块内的位置,同时考虑到扫描块可能只跨越被钩代码的一个子集。

所以如果 baseAddress 标记扫描开始地址,HOOK_START_ADDRESS 标记修改代码开始位置,endAddress 标记扫描结束地址,HOOK_END_ADDRESS 标记修改代码结束位置,你可以用一些简单数学计算修改代码的哪些部分出现在缓冲区的哪些部分。你这样做如下,用 writeStart 存储修改代码在扫描缓冲区中的偏移量,用 readStart 存储扫描缓冲区相对于修改代码的偏移量(以防扫描缓冲区从修改代码中间开始):

int readStart, writeStart;
if (HOOK_START_ADDRESS >= baseAddress) {
    readStart = 0;
    writeStart = HOOK_START_ADDRESS - baseAddress;
} else {
    readStart = baseAddress - HOOK_START_ADDRESS;
    writeStart = baseAddress;
}
int readEnd;
if (HOOK_END_ADDRESS <= endAddress)
    readEnd = HOOK_LENGTH - readStart - 1;
else
    readEnd = endAddress - HOOK_START_ADDRESS;

一旦你知道需要替换多少字节、放哪里以及从哪里获取,你可以用三行代码进行欺骗:

char* replaceBuffer = (char*)buffer;
for ( ; readStart <= readEnd; readStart++, 
writeStart++)
    replaceBuffer[writeStart] = 
HOOK_ORIG_DATA[readStart];

完整组装后,代码看起来像这样:

NTSTATUS onNtQueryVirtualMemory(
    HANDLE process, PVOID baseAddress,
    MEMORY_INFORMATION_CLASS 
memoryInformationClass,
    PVOID buffer, ULONG numberOfBytes, PULONG 
numberOfBytesRead) {
        process, baseAddress,
        memoryInformationClass,
        buffer, numberOfBytes, numberOfBytesRead);
    bool currentProcess =
        process == INVALID_HANDLE_VALUE ||
        process == GetCurrentProcess();
    auto endAddress = baseAddress + 
numberOfBytesRead - 1;
    bool containsHook =
        (HOOK_START_ADDRESS >= baseAddress &&
        HOOK_START_ADDRESS <= endAddress) ||
        (HOOK_END_ADDRESS >= baseAddress &&
        HOOK_END_ADDRESS <= endAddress);
    if (currentProcess && containsHook) {
        int readStart, writeStart;
        if (HOOK_START_ADDRESS >= baseAddress) {
            readStart = 0;
            writeStart = HOOK_START_ADDRESS - 
baseAddress;
        } else {
            readStart = baseAddress - 
HOOK_START_ADDRESS;
            writeStart = baseAddress;
        }
        int readEnd;
        if (HOOK_END_ADDRESS <= endAddress)
            readEnd = HOOK_LENGTH - readStart - 1;
        else
            readEnd = endAddress - 
HOOK_START_ADDRESS;
        char* replaceBuffer = (char*)buffer;
        for ( ; readStart <= readEnd; readStart++, 
writeStart++)
            replaceBuffer[writeStart] = 
HOOK_ORIG_DATA[readStart];
    }
    return ret;
}

当然,如果你有多个需从二进制验证扫描中隐藏的钩子,你需要以更健壮的方式实现这个功能,让它能相应跟踪多个修改过的代码区域。