DLL 注入是一种常见的技术,用于向目标进程注入外部的动态链接库(DLL),以执行某些特定的操作。这种技术在恶意软件、游戏作弊等场景中被广泛使用,因此,研究和实施一些反注入技术对于提高应用程序的安全性是至关重要的。本文将介绍一种基于返回地址检测的反注入技术的实现,以防范非法的 DLL 注入。
在实际应用中,很多恶意注入行为都是通过劫持目标函数的调用来实现的。因此,通过检测调用栈上的返回地址,我们可以辨别调用者的身份,从而判断注入是否合法。
通过使用 Detours 库,我们可以实现对目标函数的挂钩(hooking)。挂钩后,所有对目标函数的调用都会被重定向到我们指定的挂钩函数,这是作弊方可能会采用的一种方法。在目标函数返回前,我们可以进行一些检测,例如检查返回地址是否在合法的范围内,从而判断是否存在非法注入。
通过汇编原理我们知道,对于 call 指令其返回地址是下一条指令的地址,这会存储在程序计数寄存器(PC)中。以便于返回时,恢复控制流程。一个合法的程序过程调用,目标函数的返回地址应该位于合法的上级调用者函数的地址空间(栈帧)内,如果发生了外部注入,钩子例程通常会修改目标函数的开头几个字节,使得其导向攻击者所期望的函数入口地址上,并在执行相关处理后重新执行原函数,来达到劫持正常函数的返回值或处理流程的目的。但是,我们在原函数返回前(或者自己再实现一个返回地址导向挂钩)可以进行一些对返回地址的判断,如果程序被挂钩,返回地址在钩子函数的地址空间内,而不存在于正常例程的空间范围内。
利用返回地址检测可以在一定程度上辨别出非法的调用过程,但不是绝对和安全的防御,因为攻击者可以修改寄存器或者返回地址或者判断条件等来达到对安全手段绕过,本文只是作为一种普及讲解。并且为了安全起见,只给出最简单的判断流程,实际使用中需要更为精细化的检测流程。
以下是实现基于返回地址检测的反注入技术的主要步骤:
定义目标函数和挂钩函数: 在代码中定义目标函数(例如 TargetFunction
)、挂钩函数(例如 HookedFunction
)、返回地址计算函数、函数地址模式匹配函数。
初始化 Detours 库: 使用 Detours 库提供的函数初始化挂钩,通过模拟挂钩非法劫持原函数的执行流程。
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&OriginalFunction, HookedFunction);
DetourTransactionCommit();
调用目标函数: 此时,任何对目标函数的调用都会触发挂钩函数。
int result = TargetFunction(42);
在原函数返回前执行检测流程:计算返回地址和合法的地址空间(mian 函数的入口地址到返回地址之间的空间)来判断是否存在非法调用。
PVOID thisAddress = _ReturnAddress();
size_t searchSize = 0x100;
PVOID EndAddress = nullptr;
if (!RelGetFunctionRange(thisAddress, &EndAddress, &searchSize))
printf("NofoundAddress.\n");
// 判断返回地址是否在内部调用方(main)范围内
if (targetCallPtr <= lowerBounds || targetCallPtr >= uperBounds || classEndPtr != uperBounds)
{
printf("Target Function call is invalid!\n");
}
卸载挂钩: 在检测完成后,卸载挂钩,使目标函数恢复正常调用。
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&OriginalFunction, HookedFunction);
DetourTransactionCommit();
#include <iostream>
#include <Windows.h>
#include <intrin.h>
#include "detours.h"
#pragma intrinsic(_ReturnAddress)
#pragma comment(lib, "detours.lib")
// 模拟原函数声明
typedef int(WINAPI* OriginalFunctionType)(int);
// 导出函数
extern "C" { // only need to export C interface if
// used by C++ source code
__declspec(dllexport) int main(); //主函数的声明
__declspec(dllexport) int WINAPI TargetFunction(int param);
}
// 原始函数指针
PVOID OriginalFunction = nullptr;
typedef struct FuncAddressInfo {
PVOID CallFunctionAddress;
PVOID BaseAddress;
BOOL IsValidCall;
}FuncAddressInfo;
// 获取函数区间范围(用于非调试模式)
BOOL RelGetFunctionRange(
LPVOID functionAddress,
LPVOID* searchAddress,
size_t* stackSearchSize
)
{
// 计算结束位置
unsigned char refCode[2] = { 0 };
for (size_t i = 0; i < *stackSearchSize; i++)
{
memcpy(&refCode, (unsigned char*)(functionAddress) + i, sizeof(refCode));
switch (refCode[0])
{
case 0xc3:
case 0xc2:
if (refCode[1] == 0xcc)
{
*stackSearchSize = i + 1;
*searchAddress = reinterpret_cast<PVOID>(i + reinterpret_cast<DWORD>(functionAddress));
}
break;
}
if (*searchAddress != nullptr) break;
}
return TRUE;
}
// 定义目标函数
__declspec(dllexport) int WINAPI TargetFunction(int param) {
printf("TargetFunction called with param: %d\n",param);
size_t searchSize = 0x100;
PVOID EndAddress = nullptr;
PVOID thisAddress = _ReturnAddress();
if (!RelGetFunctionRange(thisAddress, &EndAddress, &searchSize))
printf("NofoundAddress.\n");
printf("lastCallAddress: 0x%01X AtEndAddress: 0x%01X\n",
reinterpret_cast<DWORD>(thisAddress),
reinterpret_cast<DWORD>(EndAddress));
// 获取 main 函数入口地址
PVOID mainStartAddress = &main;
// 计算函数出口地址
PVOID mainEndAddress = nullptr;
searchSize = 0x100;
if (!RelGetFunctionRange(mainStartAddress, &mainEndAddress, &searchSize))
printf("NofoundAddress.\n");
printf("mainStartAddress: 0x%01X mainEndAddress: 0x%01X\n",
reinterpret_cast<DWORD>(mainStartAddress),
reinterpret_cast<DWORD>(mainEndAddress));
const DWORD lowerBounds = reinterpret_cast<DWORD>(mainStartAddress);
const DWORD uperBounds = reinterpret_cast<DWORD>(mainEndAddress);
const DWORD targetCallPtr = reinterpret_cast<DWORD>(thisAddress);
const DWORD classEndPtr = reinterpret_cast<DWORD>(EndAddress);
// 判断返回地址是否在内部调用方(main)范围内
if (targetCallPtr <= lowerBounds || targetCallPtr >= uperBounds || classEndPtr != uperBounds)
{
printf("Target Function call is invalid!\n");
}
else {
printf("Target Function call is valid!\n");
}
return param * 2;
}
// 挂钩函数
int WINAPI HookedFunction(int param) {
// 进行钩子毒化处理
// ... ...
//
// 调用原始函数
int result = ((OriginalFunctionType)OriginalFunction)(param);
// 在这里可以添加其他处理逻辑
return result;
}
__declspec(dllexport) int main() {
// 获取要挂钩的目标函数地址
OriginalFunction = &TargetFunction;
// 初始化 Detours 库
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&OriginalFunction, HookedFunction);
DetourTransactionCommit();
// 此时调用目标函数将触发挂钩
int result = TargetFunction(42);
// 卸载挂钩
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&OriginalFunction, HookedFunction);
DetourTransactionCommit();
// 此时调用目标函数将正常执行流程
result = TargetFunction(42);
system("pause");
return 0;
}
运行就可以看出,挂钩时候 call 的下一条指令地址不在 mian 的地址范围内,并且回溯上级调用者的返回地址不是 mian 的返回地址。(这里以MSVC?release 版本进行模式匹配的,debug 版本会有跳板函数,需要更多的处理才能获得地址,并且返回值的机器码不止 0xc2cc, 0xc3cc,我这里保留了热补丁区域,所以前后都有有 0xcc )
首先找到未挂钩时候的 call 返回地址(0x391269)和上级函数返回地址(0x39127F),位于 main 中:
和检测的结果比对(源代码里面用的是 0xc3cc,应该改成 0xc355 才对,所以检测跑后面去了,但不影响结果):
挂钩时的地址的分析是类似的,但需要挂断点,拦截脱钩过程,或者用 system("pause) 暂停一下。
通过在目标函数的调用路径上添加检测机制,我们可以有效地防范非法的 DLL 注入。这种技术对于保护应用程序免受外部注入的威胁具有一定的效果。然而,需要注意的是,这只是一种基础的防御机制,更高级的恶意注入技术可能需要更复杂的防御手段。
更新于:2023.12.24