分类 逆向 下的文章

0x01 PEB相关标志位反调试

PEB是一个非常庞大的数据结构,它是用来存储每个进程的运行时数据。下图为Windows 10的_PEB部分结构。

image-20211118204902448

在VS中获取当前进程PEB结构体地址方法

#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
#endif // _WIN64

1.1 IsDebuggerPresent函数反调试

这个API是最经典检测调试器的函数,它底层原理就是返回PEB结构中BeingDebugged位的值,当有调试器附加的时候BeingDebugged位被置为1。

image-20211118210405744

1.2 NtGlobalFlag标志位

PEB的NtGlobalFlag字段(32位Windows的0x68偏移,64位Windows的0xBC)默认为0。

image-20211118212313105

附加一个调试器并不改变NtGlobalFlag的值。但是如果进程是由调试器创建的,则将设置以下标志:

FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)

NtGlobalFlag值:
0000 0000 0111 0000

反调试检测:

#define FLG_HEAP_ENABLE_TAIL_CHECK   0x10
#define FLG_HEAP_ENABLE_FREE_CHECK   0x20
#define FLG_HEAP_VALIDATE_PARAMETERS 0x40
#define NT_GLOBAL_FLAG_DEBUGGED (FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS)

#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0x68);
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0xBC);
#endif // _WIN64
 
if (dwNtGlobalFlag & NT_GLOBAL_FLAG_DEBUGGED)
    do something...

1.3 Heap Flags

在PEB的ProcessHeap位指向_HEAP结构体,该结构体中有俩个字段会受到调试器的影响,具体如何影响,取决于Windows的版本,主要是修改原始的内容,这两个字段是Flags和ForceFlags。

image-20211119125259826

image-20211118220517877

Win10 x86环境下检测代码:

BOOL CheckHeapFlagsDebug()
{
    PPEB pPeb = (PPEB)__readfsdword(0x30);
    PVOID pHeapBase = (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x18));
    DWORD dwHeapFlagsOffset = 0x40;
    DWORD dwHeapForceFlagsOffset = 0x44;
    
    PDWORD pdwHeapFlags = (PDWORD)((PBYTE)pHeapBase + dwHeapFlagsOffset);
    PDWORD pdwHeapForceFlags = (PDWORD)((PBYTE)pHeapBase + dwHeapForceFlagsOffset);
    
    //HEAP_GROWABLE (2)
    return (*pdwHeapFlags & ~HEAP_GROWABLE) || (*pdwHeapForceFlags != 0); 
}

1.4 堆Magic标志

当进程被调试器调试时该进程堆会被一些特殊的标志填充,这些特殊标记分别是0xABABABAB , 0xFEEEFEEE。在调试模式下, NtGlobalFlag的HEAP_TAIL_CHECKING_ENABLED 标志将被默认设置,堆内存分配会在末尾追加 0xABABABAB标志进行安全检查,如果NtGlobalFlag设置了HEAP_FREE_CHECKING_ENABLED标志,那么当需要额外的字节来填充堆块尾部时, 就会使用0xFEEEFEEE(或一部分) 来填充。PROCESS_HEAP_ENTRY结构详细介绍

BOOL CheckHeapMagic()
{
    PROCESS_HEAP_ENTRY HeapEntry = { 0 };
    do
    {
        if (!HeapWalk(GetProcessHeap(), &HeapEntry))
            return false;
    } while (HeapEntry.wFlags != PROCESS_HEAP_ENTRY_BUSY);

    PVOID pOverlapped = (PBYTE)HeapEntry.lpData + HeapEntry.cbData;
    return ((DWORD)(*(PDWORD)pOverlapped) == 0xABABABAB);
}

0x02 ThreadHideFromDebugger线程属性反调试

2.1 ZwSetInformationThread反调试

ThreadHideFromDebugger是线程的一个属性值,当线程具备ThreadHideFromDebugger特性时则该线程(一般是主线程)对“调试器”隐藏,使调试器无法继续接收该线程的调试事件。所以如果线程设置了ThreadHideFromDebugger那么当断点等调试事件触发时调试器表现为卡死。当线程启动后可以通过ZwSetInformationThread函数来设置,该函数需要动态从ntdll.dll中获取,以下是函数声明:

NTSTATUS (NTAPI*) pfnZwSetInformationThread(
   HANDLE          ThreadHandle,
   THREADINFOCLASS ThreadInformationClass,
   PVOID           ThreadInformation,
   ULONG           ThreadInformationLength
);

调用方式:

HINSTANCE hModule;
pfnZwSetInformationThread ZwSetInformationThread;
hModule = GetModuleHandleA("Ntdll.dll");
ZwSetInformationThread = (pfnZwSetInformationThread)GetProcAddress(hModule,"ZwSetInformationThread");
ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, 0, 0);

2.2 NtCreateThreadEx反调试

Windows Vista新引入了NtCreateThreadEx函数,以下是函数声明:

#define THREAD_CREATE_FLAGS_CREATE_SUSPENDED 0x00000001
#define THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH 0x00000002
#define THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER 0x00000004
#define THREAD_CREATE_FLAGS_HAS_SECURITY_DESCRIPTOR 0x00000010
#define THREAD_CREATE_FLAGS_ACCESS_CHECK_IN_TARGET 0x00000020
#define THREAD_CREATE_FLAGS_INITIAL_THREAD 0x00000080

NTSTATUS NTAPI NtCreateThreadEx (
    _Out_    PHANDLE              ThreadHandle,
    _In_     ACCESS_MASK          DesiredAccess,
    _In_opt_ POBJECT_ATTRIBUTES   ObjectAttributes,
    _In_     HANDLE               ProcessHandle,
    _In_     PVOID                StartRoutine,
    _In_opt_ PVOID                Argument,
    _In_     ULONG                CreateFlags,
    _In_opt_ ULONG_PTR            ZeroBits,
    _In_opt_ SIZE_T               StackSize,
    _In_opt_ SIZE_T               MaximumStackSize,
    _In_opt_ PVOID                AttributeList
);

在CreateFlags这个参数中设置THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER标志,则线程创建时将对调试器隐藏该线程。这与NtSetInformationThread函数设置的ThreadHideFromDebugger相同。

0x03 NtQueryInformationProcess函数反调试

NtQueryInformationProcess函数是操作系统中十分有用的一个关键函数,可以用来查找进程的很多相关信息。以下是函数声明:

NTSTATUS NTAPI NtQueryInformationProcess(
  [in]            HANDLE           ProcessHandle,
  [in]            PROCESSINFOCLASS ProcessInformationClass,
  [out]           PVOID            ProcessInformation,
  [in]            ULONG            ProcessInformationLength,
  [out, optional] PULONG           ReturnLength
);
https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess

第一个参数表明待查询的目标进程句柄,而第二个参数则是标明查询的信息种类。PROCESSINFOCALASS是一个枚举类型,能查询近百种信息,其中以下四种信息是最常见可用于检测调试器的存在。

ProcessDebugPort            // 0x7
ProcessDebugFlags            // 0x1F
ProcessDebugObjectHandle    // 0x1E
ProcessBasicInformation        // 0x0

3.1 CheckRemoteDebuggerPresent函数

CheckRemoteDebuggerPresent函数是用来检测另一个进程是否处于调试状态,以下是利用该函数进行反调试的示例代码:

BOOL bDebugger = FALSE;
if (CheckRemoteDebuggerPresent(GetCurrentProcess(), &bDebugger ))
{
        if (bDebugger)
            do something...
}

ProcessDebugPort端口是Windows调试子系统依赖的一个数据结构,可以通过检测调试端口的方式来检测进程是否被调试。在CheckRemoteDebuggerPresent函数内部就是调用NtQueryInformationProcess函数查询ProcessDebugPort信息,判断目标进程是否在调试状态,没有调试器的时候值为0。

image-20211119171510823

3.2 ProcessDebugObjectHandle反调试

ProcessDebugObjectHandle的内容为调试对象的句柄,没有调试器的时候值为0,以下为示例代码:

DWORD bDebugger = -1;
NTSTATUS status = NtQueryInformationProcess(
                GetCurrentProcess(),        // 进程句柄
                0x1E,                        // 要检索的进程信息类型,ProcessDebugObjectHandle
                &bDebugger,                    // 接收进程信息的缓冲区指针
                sizeof(DWORD),                // 缓冲区大小
                NULL                        // 实际返回进程信息的大小
                );

3.3 ProcessDebugFlags反调试

进程调试标志位,当程序处于调试状态的时候ProcessDebugFlags = 0,以下为示例代码:

DWORD bDebugger = 0;
NTSTATUS status = NtQueryInformationProcess(
                GetCurrentProcess(),        // 进程句柄
                0x1F,                        // 要检索的进程信息类型,ProcessDebugFlags
                &bDebugger,                    // 接收进程信息的缓冲区指针
                sizeof(DWORD),                // 缓冲区大小
                NULL                        // 实际返回进程信息的大小
                );

3.4 ProcessBasicInformation反调试

当使用ProcessBasicInformation标志调用NtQueryInformationProcess函数时,将返回PROCESS_BASIC_INFORMATION结构,以下是在官方定义的基础上进行完整化的结构信息:

typedef struct _PROCESS_BASIC_INFORMATION 
{
      DWORD ExitStatus;                     // 接收进程终止状态
      DWORD PebBaseAddress;                 // 接收进程环境块地址(PEB)
      DWORD AffinityMask;                     // 接收进程关联掩码
      DWORD BasePriority;                     // 接收进程的优先级类
      ULONG UniqueProcessId;                 // 接收进程ID
      ULONG InheritedFromUniqueProcessId;     // 接收父进程ID
} PROCESS_BASIC_INFORMATION;

我们知道一般情况下都是通过Windows的资源管理器来打开程序,所以一般的手动打开的进程父进程都是explorer.exe,如果通过调试器创建进程则父进程就不会是explorer.exe。所以可以通过检测父进程是否是explorer.exe来检测调试器的存在。

通过这个标志我们就可以获得父进程ID(Reserved3),后续在通过OpenProcess获取父进程句柄,调用GetProcessImageFileName获得父进程名进行比较即可判断是否被调试。

0x04 基于时间的反调试

在现代计算机中一段代码的执行通常不会消耗很多时间,但是如果程序处于调试状态,则他的执行时间就不可控了。这种检测本质就是通过获取时间的函数比较时间差进程检测。

4.1 rdtsc函数反调试

原理很直接了就是在代码的俩个地方执行rdtsc函数,然后检测两次执行的时间差。

BOOL IsDebugged(DWORD64 qwNativeElapsed)
{
    ULARGE_INTEGER Start, End;
    __asm
    {
        xor  ecx, ecx
        rdtsc
        mov  Start.LowPart, eax
        mov  Start.HighPart, edx
    }
    // do something...
    __asm
    {
        xor  ecx, ecx
        rdtsc
        mov  End.LowPart, eax
        mov  End.HighPart, edx
    }
    return (End.QuadPart - Start.QuadPart) > qwNativeElapsed;
}

4.2 GetTickCount函数反调试

BOOL IsDebugged(DWORD dwNativeElapsed)
{
    DWORD dwStart = GetTickCount();
    // do something...
    DWORD dwEnd = GetTickCount();
    return (dwEnd - dwStart) > dwNativeElapsed;
}

4.3 GetLocalTime函数反调试

BOOL IsDebugged(DWORD64 qwNativeElapsed)
{
    SYSTEMTIME stStart, stEnd;
    FILETIME ftStart, ftEnd;
    ULARGE_INTEGER uiStart, uiEnd;

    GetLocalTime(&stStart);
    // do something...
    GetLocalTime(&stEnd);

    if (!SystemTimeToFileTime(&stStart, &ftStart))
        return FALSE;
    if (!SystemTimeToFileTime(&stEnd, &ftEnd))
        return FALSE;

    uiStart.LowPart  = ftStart.dwLowDateTime;
    uiStart.HighPart = ftStart.dwHighDateTime;
    uiEnd.LowPart  = ftEnd.dwLowDateTime;
    uiEnd.HighPart = ftEnd.dwHighDateTime;
    return (uiEnd.QuadPart - uiStart.QuadPart) > qwNativeElapsed;
}

4.4 GetSystemTime函数反调试

BOOL IsDebugged(DWORD64 qwNativeElapsed)
{
    SYSTEMTIME stStart, stEnd;
    FILETIME ftStart, ftEnd;
    ULARGE_INTEGER uiStart, uiEnd;

    GetSystemTime(&stStart);
    // do something...
    GetSystemTime(&stEnd);

    if (!SystemTimeToFileTime(&stStart, &ftStart))
        return FALSE;
    if (!SystemTimeToFileTime(&stEnd, &ftEnd))
        return FALSE;

    uiStart.LowPart  = ftStart.dwLowDateTime;
    uiStart.HighPart = ftStart.dwHighDateTime;
    uiEnd.LowPart  = ftEnd.dwLowDateTime;
    uiEnd.HighPart = ftEnd.dwHighDateTime;
    return (uiEnd.QuadPart - uiStart.QuadPart) > qwNativeElapsed;
}

4.5 QueryPerformanceCounter函数反调试

BOOL IsDebugged(DWORD64 qwNativeElapsed)
{
    LARGE_INTEGER liStart, liEnd;
    QueryPerformanceCounter(&liStart);
       // do something...
    QueryPerformanceCounter(&liEnd);
    
    return (liEnd.QuadPart - liStart.QuadPart) > qwNativeElapsed;
}

0x05 基于异常的反调试

异常处理是Windows操作系统中非常重要的一种机制,它包括VEH/SEH等等,异常处理程序通常会脱离原有的程序执行流程,它很多时候可以被用于反调试。若进程在调试运行中发生异常,调试器就会接受处理。利用该特征就可以判断进程使正常运行还是调试运行,然后根据不同的结果执行不同的操作。

在代码编写过程中使用\_\_try/\_\_except 关键字可以设置一个SEH异常处理函数

int main()
{
    __try
    {
        cout << "hello,world" << endl;
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        cout << "异常" << endl;
    }
    return 0;
}

__except中值的含义:

image-20211120174502674

1:处理异常
0:不处理异常交给下一个异常节点去处理
-1:继续执行也就是继续EIP处理执行,但是这里又有异常,所以这里就会一直卡在这里

Windows的异常处理流程大致为:

1. 交给调试器(进程必须被调试)
2. 执行VEH
3. 执行SEH
4. TopLevelEH(进程被调试时不会被执行)
5. 交给调试器(上面的异常处理都说处理不了,就再次交给调试器)
6. 调用异常端口通知csrss.exe

也就是说当一个异常被触发最先会到调试器(如果被调试),所以我们可以在__except内部动手脚以实现反调试,示例代码如下:

BOOL IsDebugged()
{
    __try{
        __asm int 3
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        return FALSE;
    }
    return TRUE;
}

0x06 总结

以上是在此前学习过程中收集的一些应用层反调试原理,其中还有很多比如获取窗口,文件完整性检验,代码重映射,硬件断点检测等由于篇幅的原因无法详细展开,如果文中出现什么错误还望师傅指正。

下一篇打算结合SycllaHide这个开源反调试插件项目探索一下Windows应用层反反调试的原理和实现。

在创建游戏程序的快捷方式,然后在快捷方式中添加-insecure关闭VAC验证服务方便进行初步的调试。如果是用7Launcher平台的得添加Run_CSGO.exe的设置并启动。

image-20211107154920433image-20211107170154792

进到游戏后记得在设置里面打开开发者控制台,方便控制机器人

image-20211107154724783

控制台常用指令
net_graph “0/1”  关闭/显示当前fps以及ping、loss、choke、tick等服务器与网络参数
mp_roundtime_defuse 50 休闲/竞技模式每局时间60分钟
sv_cheats 0/1 关闭/开启作弊功能
bot_stop 1 bot原地不动
bot_kick 踢出所有电脑
bot_add 随机添加一个bot
https://www.csgo.com.cn/news/gamenews/20170825/205831.shtml

0x01 自瞄 & 透视准备

1.0 寻找自身矩阵

我们知道矩阵的值一般是以浮点数为主,所以我们可以在CE里面搜索未知初始值的单浮点数,通过开关镜鼠标移动键盘移动不断搜索。

image-20211107171601541

找到自身4x4矩阵,可能会找到很多个

image-20211107203248709

1.1 寻找角度信息

角度信息就比较好找了,CSGO当准星指向最上方的时候,Y轴角度值为-89,当准星指向最下方的时候,上下角度的角度值为89。通过这个特点我们就能很容易定位到。

image-20211107211400488

唯一要注意的用默认调试器选项可能会照成游戏进程闪退,推荐使用VEH调试器。

image-20211107211634118

1.2 寻找位置信息

这个位置信息就是任务在整个游戏环境中的XYZ坐标,我们可以通过敌人坐标值与我们坐标值进行某种计算得到与敌人的相对距离。

最方便的是从Z坐标开始寻找,因为XY我们不能确认是增大还是减小,Z坐标可以通过所在地势高低很好判断。不过很容易会找成坐标数组这个得具体分析。

image-20211107215946272

1.3 寻找敌人和队友位置信息

这个就是通过自身间接寻找敌人的坐标,就是先走到和敌人同一水平高度,然后搜索数值范围,不断开启bot运动多次搜索进行定位。

image-20211108185444998

我们利用CE的结构分析观察上层指针

image-20211108185612149

我们通过大胆猜测和测试,可以发现每间隔0x10就是另一个人物的结构体指针,在个结构体0xA8是该人物的Z坐标,0x100是血量等。

image-20211108185911638

图中一共有四个结构体指针,和游戏中人物数量匹配,敌人和队友都在这一个数组中。

image-20211108190024088

1.4 寻找人物骨骼

为了实现骨骼透视和锁头,还需要得到敌人的骨骼坐标,在游戏建模的时候为了实现人物模型的生动性,一个人物是由多个模块组成的比如头、手、腿、脚等,这样设计可以实现各个骨骼的各自移动。因此,每块骨骼都应该具有一个独立的坐标点,我们得到了其中某些骨骼的坐标点,才可以实现出骨骼透视和锁头。

骨骼坐标也是由XYZ的,搜索骨骼坐标有个非常关键的技巧,当我们看向敌人的时候,敌人的骨骼坐标就会发生变动,而当我们远离且不看向敌人时,这个值就不变。所以我们就可以利用这个规律进行骨骼坐标搜索。

image-20211108213622222

我们找到骨骼坐标头后可以发现,当我们看向敌人的时候这个数组中的XYZ值一直在浮动,远离不看就变动停止。而且这个骨骼头位置是通过人物结构体二级指针指向的。

image-20211108213838528

1.5 寻找人物结构体的阵营信息

通过结构分析工具进行对比查看

image-20211108214513897

我们把0xF4偏移的值改成一样的造成了游戏标识判断错误,改成1敌人模型消失,所以可以大胆猜测这里就是阵营判断。

image-20211108214816890

0x02 内置辅助

对于透视分为内置辅助和外置辅助,内置的意思就是我们编写一个Dll文件注入到目标进程Hook掉D3D绘制的相关函数,以实现我们的透视方框;而外置辅助就是在游戏窗口上生成一个透明的窗口,然后通过跨进程读取游戏内存数据,在自己的透明窗口上进行透视方框绘制。

现在我们先讨论内置辅助的前置准备-HOOKD3D绘制相关函数,电脑在安装DirectX后会有相关的例子,本节以官方的ShadowMap.exe程序进行测试,我们使用MinHook这个HOOK框架进行操作。

image-20211109195650911

image-20211110111605425

HRESULT WINAPI MyReset(IDirect3DDevice9* direct3dDevice9, D3DPRESENT_PARAMETERS* pPresentationParameters)
{
    OutputDebugString(L"MyReset");

    HRESULT result = fpReset(direct3dDevice9, pPresentationParameters);
    return result;
}

HRESULT WINAPI MyEndScene(IDirect3DDevice9* direct3dDevice9)
{


    HRESULT result = fpEndScene(direct3dDevice9);
    return result;
}

unsigned int WINAPI InitD3D9(PVOID data)
{
    OutputDebugString(L"InitD3D9");

    g_Direct3d9 = Direct3DCreate9(D3D_SDK_VERSION);
    OutputDebugString(L"Direct3DCreate9");

    memset(&g_Present, 0, sizeof(g_Present));
    g_Present.Windowed = TRUE;
    g_Present.SwapEffect = D3DSWAPEFFECT_DISCARD;
    g_Present.BackBufferFormat = D3DFMT_UNKNOWN;

    HRESULT result = g_Direct3d9->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, FindWindowW(L"Direct3DWindowClass", NULL),
        D3DCREATE_SOFTWARE_VERTEXPROCESSING, &g_Present, &g_Direct3ddevice9);
    OutputDebugString(L"CreateDevice");

    //HOOK
    int* g_direct3d9Table = (int*)*(int*)g_Direct3d9;
    int* g_direct3dDevice9Table = (int*)*(int*)g_Direct3ddevice9;

    if (MH_Initialize() != MH_OK)
       return 1;
    OutputDebugString(L"MH_Initialize");

    if (MH_CreateHook((LPVOID)g_direct3dDevice9Table[16], &MyReset,
        reinterpret_cast<LPVOID*>(&fpReset)) != MH_OK)
        return 1;
    if (MH_CreateHook((LPVOID)g_direct3dDevice9Table[42], &MyEndScene,
        reinterpret_cast<LPVOID*>(&fpEndScene)) != MH_OK)
        return 1;
    OutputDebugString(L"MH_CreateHook");
    
    if (MH_EnableHook((LPVOID)g_direct3dDevice9Table[16]) != MH_OK)
        return 1;
    if (MH_EnableHook((LPVOID)g_direct3dDevice9Table[42]) != MH_OK)
        return 1;
    OutputDebugString(L"MH_EnableHook");
    
    return 0;
}

2.1 ImGui绘制菜单

ImGui是一个开源的图形库上手比较简单,本节拿来做菜单的绘制测试。我们参考imgui官方给的例子把需要的cpp和h文件加入到我们测试工程。

image-20211110194921684

模仿着官方实例代码我们可以编写出我们自己的菜单,主要就是依赖于此前对D3D9的HOOK在对应绘制函数上进行操作。

LRESULT CALLBACK MyProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    if (ImGui_ImplWin32_WndProcHandler(hWnd, uMsg, wParam, lParam))
        return true;

    return CallWindowProcW(g_fpProc, hWnd, uMsg, wParam, lParam);
}

void InitImGui(IDirect3DDevice9* direct3dDevice9)
{
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGui::StyleColorsLight();
    ImGuiIO& io = ImGui::GetIO(); 
    io.IniFilename = NULL;
    io.LogFilename = NULL;

    ImGui_ImplWin32_Init(FindWindowW(L"Direct3DWindowClass", NULL));
    ImGui_ImplDX9_Init(direct3dDevice9);
}

HRESULT WINAPI MyReset(IDirect3DDevice9* direct3dDevice9, D3DPRESENT_PARAMETERS* pPresentationParameters)
{
    OutputDebugString(L"MyReset");

    ImGui_ImplDX9_InvalidateDeviceObjects();
    HRESULT result = g_fpReset(direct3dDevice9, pPresentationParameters);
    ImGui_ImplDX9_CreateDeviceObjects();

    return result;
}

HRESULT WINAPI MyEndScene(IDirect3DDevice9* direct3dDevice9)
{
    static BOOL b_First = TRUE;
    if (b_First)
    {
        b_First = FALSE;
        InitImGui(direct3dDevice9);
        g_fpProc = (WNDPROC)SetWindowLongA(FindWindowW(L"Direct3DWindowClass", NULL), GWL_WNDPROC, (LONG)MyProc);
    }

    ImGui_ImplDX9_NewFrame();
    ImGui_ImplWin32_NewFrame();
    ImGui::NewFrame();

    ImGui::Begin("MyWindow");
    ImGui::Text("Test Windows");
    ImGui::End();

    ImGui::EndFrame();
    ImGui::Render();
    ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());

    HRESULT result = g_fpEndScene(direct3dDevice9);

    return result;
}

image-20211110195143944

0x03 外置辅助

外置辅助就是游戏进程窗口上方建立一个透明窗口,本例子将配合Blackbone这个库进行跨进程读写操作,直接上源码:

void StartCheat()
{
    InitCheat(L"csgo.exe");

    extern pDoingCheat g_CheatFunc;
    g_CheatFunc = DoingCheat;

    // 以下和内置辅助类似,创建透明D3D窗口,在窗口过程函数中WM_PAINT调用DoingCheat函数
    HWND g_hTransparentHwnd =  CreateTransparentWindow(g_hGameWnd);
    InitDirect3d9(g_hTransparentHwnd);
    
    MessageLoop(g_hGameWnd, g_hTransparentHwnd);
}

void InitCheat(LPCWSTR lpProcessName)
{
    g_hGameWnd = FindWindow(NULL, L"Counter-Strike: Global Offensive");

    auto pids = Process::EnumByName(lpProcessName);
    if (!pids.empty())
    {
        procCSGO.Attach(pids[0]);

        auto clientModule = procCSGO.modules().GetModule(L"client.dll");
        auto serverModule = procCSGO.modules().GetModule(L"server.dll");
        auto engineModule = procCSGO.modules().GetModule(L"engine.dll");

        g_MatrixAddress = clientModule->baseAddress + dwMatrix;
        g_PlayersAddress = clientModule->baseAddress + dwCharacter;
        g_AngleAddress = engineModule->baseAddress + dwAngleBase;
        g_SelfAddress = serverModule->baseAddress + dwXYZBase;

        printf("自己矩阵基地址 : %8x \n", g_MatrixAddress);
        printf("自己角度基地址 : %8x \n", g_AngleAddress);
        printf("自己位置基地址 : %8x \n", g_SelfAddress);
        printf("玩家信息基地址 : %8x \n", g_PlayersAddress);
        printf("\n");
    }
}
void DoingCheat() 
{
    Player* player = GetPlayerList();// 遍历读取玩家列表信息

    DrawPlayerBox(player);// 绘制人物矩形

    if (GetMouseRightDown())// 自瞄
        StartAimbot(player);

    FreePlayList(player);
}

玩家结构体

struct Player
{
    BOOL effective;        //是否有效
    int aimbot_len;        //自瞄长度
    BOOL self;            //是否自己
    float location[3];    //身体位置
    float head_bone[3];    //头骨位置
    BOOL mirror;        //是否开镜
    int camp;            //阵营
    int blood;            //血量
    struct Player* next = NULL;
};

遍历获取玩家信息

Player* GetPlayerList()
{
    Player* headPlayer = NULL, * tmpPlayer = NULL;
    auto& memoryCSGO = procCSGO.memory();

    for (int i = 0; i < g_PlayerNumberMax; i++)
    {
        DWORD dwPlayerBaseAddress = 0;
        DWORD dwPlayerBlood = 0;
        DWORD dwBoneBaseAddress = 0;
        memoryCSGO.Read((g_PlayersAddress + i * 0x10), sizeof(dwPlayerBaseAddress), &dwPlayerBaseAddress);
        if (dwPlayerBaseAddress == 0)
            break;

        // Blood
        memoryCSGO.Read((dwPlayerBaseAddress + 0x100), sizeof(dwPlayerBlood), &dwPlayerBlood);
        if (dwPlayerBaseAddress <= 0)
            continue;

        Player* temp = (Player*)VirtualAlloc(NULL, sizeof(Player), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
        temp->blood = dwPlayerBlood;
        temp->aimbot_len = 9999;
        temp->effective = TRUE;

        // Bone
        if (!memoryCSGO.Read((dwPlayerBaseAddress + 0x26A8), sizeof(dwBoneBaseAddress) ,&dwBoneBaseAddress))
        {
            memoryCSGO.Read((dwBoneBaseAddress + 99 * sizeof(float)), sizeof(float), &(temp->head_bone[0]));
            memoryCSGO.Read((dwBoneBaseAddress + 103 * sizeof(float)), sizeof(float), &(temp->head_bone[1]));
            memoryCSGO.Read((dwBoneBaseAddress + 107 * sizeof(float)), sizeof(float), &(temp->head_bone[2]));
        }

        memoryCSGO.Read(dwPlayerBaseAddress + 0xA0, sizeof(temp->location), temp->location);
        memoryCSGO.Read(dwPlayerBaseAddress + 0xF4, sizeof(int), &temp->camp);
        memoryCSGO.Read(dwPlayerBaseAddress + 0x3914, sizeof(BOOL), &temp->mirror);

        if (headPlayer == NULL)
        {
            headPlayer = temp;
            tmpPlayer = headPlayer;
        }
        else
        {
            tmpPlayer->next = temp;
            tmpPlayer = temp;
        }
    }
    return headPlayer;
}

绘制透视方框

void DrawPlayerBox(Player* playerHead)
{
    int x, y, width, height;
    GetWindowSize(g_hGameWnd, x, y, width, height);
    width /= 2;
    height /= 2;

    float matrix[4][4];
    auto& memoryCSGO = procCSGO.memory();
    memoryCSGO.Read(g_MatrixAddress, (sizeof(float) * 4 * 4), matrix);

    // SelfCoordinate
    float selfLocation[3];
    DWORD dwLocationBaseAddress = 0;
    memoryCSGO.Read(g_SelfAddress, sizeof(dwLocationBaseAddress), &dwLocationBaseAddress);
    if (dwLocationBaseAddress)
        memoryCSGO.Read(dwLocationBaseAddress + 0x1DC, (sizeof(float) * 3), selfLocation);

    // SelfCamp
    Player* tempPlayerHead = playerHead;
    for(int i = 0;i < g_PlayerNumberMax && tempPlayerHead; ++i)
    {
        if (tempPlayerHead->effective)
        {
            int temp_x = abs(selfLocation[0] - tempPlayerHead->location[0]);
            int temp_y = abs(selfLocation[1] - tempPlayerHead->location[1]);
            if (temp_x < 5.0f && temp_y < 5.0f)
            {
                tempPlayerHead->self = TRUE;
                g_MyCamp = tempPlayerHead->camp;
                break;
            }
            tempPlayerHead = tempPlayerHead->next;
        }
    }

    for (int i = 0; i < g_PlayerNumberMax && playerHead; ++i)
    {
        int x, y, w, h;
        if (playerHead->effective && playerHead->self == FALSE && ChangeMatrixInfo(matrix, playerHead->location, width, height, x, y, w, h))
        {
            if (playerHead->blood > 0)
            {
                if(playerHead->camp!=3 && playerHead->camp!=2)
                    printf("%d\n", playerHead->camp);
                D3DCOLOR color = D3DCOLOR_XRGB(255, 255, 0);
                if (g_MyCamp != playerHead->camp)
                {
                    color = D3DCOLOR_XRGB(255, 0, 0);
                    playerHead->aimbot_len = GetAimbotLen(width, height, x, y);
                }
                DrawRect(color, x, y, w, h);
                DrawPlayerBlood(playerHead->blood, x - 5, y, h);
                DrawUnderLine(color, width, height, x + (w / 2), y + h);
            }
        }
        playerHead = playerHead->next;
    }
}

自瞄

void StartAimbot(Player* player, float maxFov = 30.0f)
{
    float selfLocation[3];
    auto& memoryCSGO = procCSGO.memory();
    DWORD dwLocationBaseAddress = 0;
    memoryCSGO.Read(g_SelfAddress, sizeof(dwLocationBaseAddress), &dwLocationBaseAddress);
    if (dwLocationBaseAddress)
        memoryCSGO.Read(dwLocationBaseAddress + 0x1DC, (sizeof(float) * 3), selfLocation);

    // 获取最近人物骨骼
    Player* aimPlayer = GetRecentHeadBone(player);
    if (aimPlayer == NULL)
        return;

    // 获取当前角度
    float flCurrentAngle[2];
    DWORD dwAngleBaseAddress = 0;
    memoryCSGO.Read(g_AngleAddress, sizeof(dwAngleBaseAddress), &dwAngleBaseAddress);
    if (dwLocationBaseAddress)
        memoryCSGO.Read(dwAngleBaseAddress + 0x4D90, (sizeof(float) * 2), flCurrentAngle);

    float flAimAngle[2];
    GetAimBotAngle(selfLocation, aimPlayer->head_bone, flAimAngle, -15.0f);

    if (abs(flAimAngle[0] - flCurrentAngle[0]) > maxFov
        || abs(flAimAngle[1] - flCurrentAngle[1]) > maxFov)
        return;
    // 自瞄
    memoryCSGO.Write(dwAngleBaseAddress + 0x4D90, (sizeof(float) * 2), flAimAngle);
}

成果

image-20211112164514553

image-20211112164546054

这里有两个我认为值得一提的点,一个是创建透明窗口的名字我使用了时间作为种子随机生成窗口名;另外一个我把所有的偏移放到了一个头文件里方便以后进行修改,当然可以用BlackBone的特征码匹配自动寻找基址。

image-20211112165849750

image-20211112165645814

0x04 总结

花了一周左右的时间比较完整的实现了CSGO辅助的学习,最后配合MinHook,D3D和BlackBone了解了内外置自瞄+透视的辅助功能实现原理,收获还是比较多的,这里放几个我个人认为比较好的学习资料以供后来者学习参考。

Osiris --- 开源的CSGO辅助

How-to-create-a-csgo-cheating-program --- 本文主要参考的教程

Xenos --- 好用的开源DLL注入器

0x00 前言

最近粗浅的研究了一下Windows应用层相关调试API和对应调试原理,以达到实现反附加的功能。本文内容主要参考《软件调试》和网络上相关优秀文章,并且主要侧重在应用层调试附加方面,关于内核层面因为水平有限本文没有详细展现。

0x01 用户态调试基本流程

首先我们先用调试API编写一个最简单的附加调试器

int main(int argc,TCHAR *argv[])
{
    DWORD dwPID;
       BOOL waitEvent = TRUE;
    if (argc > 1) {
        dwPID = atoi(argv[1]);
    }
    else {
        printf("usage: MyDebugger.exe dwPID\n");
        exit(0);
    }

    DebugActiveProcess(dwPID);
    while (waitEvent)
    {
        DEBUG_EVENT MyDebugInfo;
        waitEvent = WaitForDebugEvent(&MyDebugInfo, INFINITE); // Waiting
        switch (MyDebugInfo.dwDebugEventCode)
        {
            case EXIT_PROCESS_DEBUG_EVENT:
                waitEvent = FALSE
                break;
        }
        if (waitEvent) {
            ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
        }
    }
    return 0;
}

上文主要用到的就是 DebugActiveProcess 这个调试API对目标PID进程进行调试附加操作,如果我们要在程序创建的时候就对程序进行调试可以在Debugger中执行CreateProcess并将第6个参数传入DEBUG_ONLY_THIS_PROCESS,这样设置之后,子进程发生的调试事件会通知给父进程处理。

CreateProcess(path, // 可执行模块路径
    NULL, // 命令行
    NULL, // 安全描述符
    NULL, // 线程属性是否可继承
    FALSE, // 否从调用进程处继承了句柄
    DEBUG_ONLY_THIS_PROCESS, // 以“只”调试的方式启动
    NULL, // 新进程的环境块
    NULL, // 新进程的当前工作路径(当前目录)
    &stcStartupInfo, // 指定进程的主窗口特性
    &stcProcInfo)) // 接收新进程的识别信息

DEBUG_EVENT中的dwDebugEventCode表示调试信息的种类,对于DEBUG_EVENT详细的介绍可以查看MSDN,简单来说就是用共用体来存储具体的数据。

typedef struct _DEBUG_EVENT {
    DWORD dwDebugEventCode;
    DWORD dwProcessId;
    DWORD dwThreadId;
    union {
        EXCEPTION_DEBUG_INFO Exception;
        CREATE_THREAD_DEBUG_INFO CreateThread;
        CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
        EXIT_THREAD_DEBUG_INFO ExitThread;
        EXIT_PROCESS_DEBUG_INFO ExitProcess;
        LOAD_DLL_DEBUG_INFO LoadDll;
        UNLOAD_DLL_DEBUG_INFO UnloadDll;
        OUTPUT_DEBUG_STRING_INFO DebugString;
        RIP_INFO RipInfo;
    } u;
} DEBUG_EVENT, *LPDEBUG_EVENT;

0x02 用户态 DebugActiveProcess 实现

我们通过查找NT5的源码可以看到DebugActiveProcess 具体实现代码,主要就是调用了DbgUiConnectToDbg ,ProcessIdToHandle和DbgUiDebugActiveProcess这三个函数

image-20210818203841617

image-20210818205033766

DbgUiConnectToDbg

首先判断TEB->DbgSsReserved[1]是否保存着调试对象的句柄,如果存在则直接返回函数如果不存在就进行初始化并调用NtCreateDebugObject创建调试对象

image-20210818205728745

image-20210818211454716

ProcessIdToHandle

如果是伪句柄则调用CsrGetProcessId获取csrss.exe的PID,然后调用NtOpenProcess获得对应进程句柄,给后续调用做准备。

image-20210818213529075

image-20210818214255906

DbgUiDebugActiveProcess

此函数首先传入进程和调试对象的句柄进入内核,然后调用DbgUiIssueRemoteBreakin函数创建线程开始地址为DbgUiRemoteBreakin的远程线程让被调试进程断下来,如果远程线程设置失败则调用DbgUiStopDebugging停止调试。这个地方创建了远程线程是在将来反附加检测的主要检测点。

image-20210818211939159

image-20210818213257737

全览图

0x03 调试子系统

Windows的调试子系统使用调试事件驱动,这个和窗体的消息驱动是很类似的。在调试子系统中使用WaitForDebugEvent在用户态等待调试事件,当调试器处理调试事件时,被调试进程会被挂起,所以调试器处理完毕后要调用ContinueDebugEvent使被挂起的被调试进程继续运行。

下图是张银奎老师在软件调试纵横谈中的调试模型,在用户空间部分就是我们此前通过源码分析的用户态附加调试API DebugActiveProcess 的基本流程,在系统空间维护着一个DebugObject的链表并且通过Dbgk*例程采集和传递调试事件。

用户态调试模型

0x04 反附加手段

根据此前内容知道,当调试器附加一个进程的时候是调用DebugActiveProcess函数,该函数内部调用了DbgUiDebugActiveProcess,此函数内部会调用DbgUiIssueRemoteBreakin函数,最后内部则会通过RtlCreateUserThread在被调试进程内创建一个线程,线程的起始地址是DbgUiRemoteBreakin。

image-20210819204553058

在被调试进程内DbgUiRemoteBreakin会判断PEB中的BeingDebugged标志位,如果在调试则调用DbgBreakPoint函数,调试器附加后被调试进程就中断在DbgBreakPoint函数内。

image-20210819204416047

根据上述流程我们可以知道在被调试进程(我们的程序)会创建新线程,线程起始函数是DbgUiRemoteBreakin。所以我们可以Hook DbgUiRemoteBreakin 直接调用 ExitProcess 结束我们的程序。

image-20210819205656353

0x05 反反附加插件

本文以ScyllaHide为例子,因为ScyllaHide是开源项目可以直接分析源码,插件加载后我们此前设置的HOOK会被还原。

image-20210819210704926

image-20210819210717166

对于SharpOD,他会在插件加载的时候Hook调试器的DebugActiveProcess和CreateProcessInternalW,在DebugActiveProcessDetour 会在被调试进程中添加ShellCode,并HOOK LdrInitializeThunk 跳转到ShellCode中断,并在附加进程的调试事件到达后还原LdrInitializeThunk 。

image-20210819210932220

image-20210819210957423

0x06 x64dbg的Titan Engine

对于新版x64dbg会内嵌Titan Engine(2020年11月12日版本后添加),所以SharpOD插件对于反反附加对新版本x64dbg起不到作用。在Titan Engine内部自己实现了DebugActiveProcess一套流程。

image-20210819211542046

并且没有调用DbgUiIssueRemoteBreakin让程序被调试器附加的时候断下。

image-20210819211614725

0x07 总结及参考链接

本文初步学习了Windows用户层基本的调试原理和模型,并给出了一个简单的反附加方案,同时分析了目前常见的反反附加插件。不过同时存在不知道如何绕过SharpOD的反反附加检测等问题,还需要进一步学习研究。因为本人水平有限,如果存在错误还望各位前辈指正。

Windows 调试原理学习

调试器原理

软件调试纵横谈

程序节区重新映射时设置Section属性为SEC_NO_CHANGE,避免代码被inline hook和设置断点调试。

本方法主要参考 https://github.com/changeofpace/Self-Remapping-Code 这个GitHub项目,作者在19年重构了一版但是没有放到release上。

0x01 编译注意事项

  1. 因为在使用NtMapViewOfSection函数时基址或文件的偏移量参数需要和系统分配颗粒度对齐,这个颗粒度一般为0x10000,所以在项目配置中设置节区对齐大小 #pragma comment(linker, "/ALIGN:0x10000")
  2. 在x64环境下调用相关导入表函数多为 CALL + 偏移,而x86环境是直接CALL导入表地址,但是在Unmap原始IMAGE的节区空间之后原先导入表地址访问是不合法的,所以想要代码在x86环境下运行就需要做重定位,使用新VirtualAlloc后申请的地址内容。
  3. 在属性选项中需要关闭符合模式

image-20210916115817386

0x02 原理介绍

使用未文档化的SEC_NO_CHANGE属性调用NtCreateSection函数创建一个Section

image-20210916152810251

映射Section并复制原始模块代码至映射的Section空间

image-20210916152901621

然后使用NtUnmapViewOfSection把原始IMAGE的节区空间和映射的Section空间给Unmap掉

image-20210916152928554

最后在循环重新映射刚才被Unmap的Section空间到原始模块代码内存地址空间。

image-20210916153059925

0x03 效果

image-20210916155828713

0x04 参考链接

NTSTATUS Values

ZwUnmapViewOfSection function (wdm.h)

ZwMapViewOfSection function (wdm.h)

Self-Remapping Code

0x05 修改后的代码

https://github.com/Jevon101/Self-Remapping-Code

该方法主要是对PEB结构体中的LDR链表进行操作,分别把三种排序的LDR链进行断链操作以达到模块隐藏的效果,TEB和PEB的结构随着系统的不同会有所变化,本文以Windows 10中的32位程序为例子。该例子仅为简单的断链,通过测试目前会影响CreateToolhelp32Snapshot + Module32First获取模块信息。

image-20210608195249580

0x00 PEB&TEB介绍

PEB和TEB是Windows进程管理的两个重要数据结构,在PEB和TEB结构中存在许多有用的成员,本文主要着重于PEB里的LDR双向链表,链表的内容为各模块的相关信息。

FS:[0x00] == TEB
FS:[0x30] == TEB.ProcessEnvironmentBlock == address of PEB
// 使用内联汇编的方式获取PEB位于内存的虚拟地址。
mov eax, dword ptr fs:[30h]

TEB32

image-20210608194106387

PEB32

image-20210608194233783

PEB_LDR_DATA

image-20210608194452597

0x01 隐藏思路

在LDR表中有三种链表排序顺序,分别是模块加载顺序、模块内存顺序、模块初始化顺序。image-20210608201141096

这三个链表结构是PEB_LDR_DATA和LDR_DATA_TABLE_ENTRY共用的

img

LDR_DATA_TABLE_ENTRY

image-20210608200825359

所以需要分别遍历这三种排序方式,将目标模块的LDR_DATA_TABLE_ENTRY从链上断开。

image-20210608201401472

0x02 代码实现

模块遍历代码

#include <stdio.h>
#include <windows.h>
#include <Tlhelp32.h>
int main(int argc, char* argv[])
{
    HANDLE hProcessSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (INVALID_HANDLE_VALUE == hProcessSnapshot)
    {
        return 0;
    }
    DWORD dwPid = 0;
    PROCESSENTRY32 pi;
    pi.dwSize = sizeof(PROCESSENTRY32);
    BOOL Ret = Process32First(hProcessSnapshot, &pi);
    while (Ret)
    {
        if (strcmp("LDR_Break_Link.exe", pi.szExeFile) == 0)
        {
            dwPid = pi.th32ProcessID;
            break;
        }
        Ret = Process32Next(hProcessSnapshot, &pi);
    }
    CloseHandle(hProcessSnapshot);


    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPid);//上面获取了进程PID,下面使用即可.
    if (INVALID_HANDLE_VALUE == hSnapshot)
    {
        return 0;
    }
    MODULEENTRY32 mi;
    mi.dwSize = sizeof(MODULEENTRY32); 
    BOOL  bRet = Module32First(hSnapshot, &mi);
    while (bRet)
    {
        printf("%s\n", mi.szModule);

        bRet = Module32Next(hSnapshot, &mi);
    }
    CloseHandle(hSnapshot);
    return 0;
}

断链代码

// LDR断链 隐藏模块
// Coding By Jev0n
#include <stdio.h>
#include <windows.h>


typedef struct _UNICODE_STRING
{
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

typedef struct _PEB_LDR_DATA {
    ULONG                   Length;
    BOOLEAN                 Initialized;
    PVOID                   SsHandle;
    LIST_ENTRY              InLoadOrderModuleList;
    LIST_ENTRY              InMemoryOrderModuleList;
    LIST_ENTRY              InInitializationOrderModuleList;
} PEB_LDR_DATA, * PPEB_LDR_DATA;

typedef struct _LDR_DATA_TABLE_ENTRY
{
    LIST_ENTRY          InLoadOrderModuleList;
    LIST_ENTRY          InMemoryOrderModuleList;
    LIST_ENTRY          InInitializationOrderModuleList;
    LPVOID              BaseAddress;
    LPVOID              EntryPoint;
    ULONG               SizeOfImage;
    UNICODE_STRING      FullDllName;
    UNICODE_STRING      BaseDllName;
    ULONG               Flags;
    SHORT               LoadCount;
    SHORT               TlsIndex;
    HANDLE              SectionHandle;
    ULONG               CheckSum;
    ULONG               TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;


int main(int argc, const char* argv[])
{

    if (argc != 2)
    {
        printf("[+] Using Example: LDR_Break_Link.exe ntdll.dll \n");
        return -1;
    }


    // PEB LDR
    DWORD pPEB = 0;
    PPEB_LDR_DATA pLDR = NULL;
    _asm 
    {
        mov eax ,fs:[0x30]
        mov pPEB ,eax
        mov eax ,[eax + 0x0C]
        mov pLDR ,eax
    }

    // Break_Link
    DWORD status = 0;
    char* pDllName = argv[1];
    HMODULE hMod = GetModuleHandle(pDllName);
    PLIST_ENTRY pMiddle, pNext;
    PLDR_DATA_TABLE_ENTRY pLDTE;
    pMiddle = &(pLDR->InLoadOrderModuleList);
    pNext = pMiddle->Flink;
    

    do
    {
        pLDTE = CONTAINING_RECORD(pNext, LDR_DATA_TABLE_ENTRY, InLoadOrderModuleList);
        

        if (hMod == pLDTE->BaseAddress)
        {
            status = 1;

            // InLoadOrderModuleList
            pLDTE->InLoadOrderModuleList.Blink->Flink = pLDTE->InLoadOrderModuleList.Flink;
            pLDTE->InLoadOrderModuleList.Flink->Blink = pLDTE->InLoadOrderModuleList.Blink;

            // InInitializationOrderModuleList
            pLDTE->InInitializationOrderModuleList.Blink->Flink = pLDTE->InInitializationOrderModuleList.Flink;
            pLDTE->InInitializationOrderModuleList.Flink->Blink = pLDTE->InInitializationOrderModuleList.Blink;

            // InMemoryOrderModuleList
            pLDTE->InMemoryOrderModuleList.Blink->Flink = pLDTE->InMemoryOrderModuleList.Flink;
            pLDTE->InMemoryOrderModuleList.Flink->Blink = pLDTE->InMemoryOrderModuleList.Blink;

            break;
        }
        pNext = pNext->Flink;
    } while (pMiddle != pNext);

    if (status)
    {
        printf("Success !\n");
    }
    else 
    {
        printf("Error !\n");
    }

    getchar();

    return 0;
}

0x03 参考链接

VERGILIUS

基于断链的DLL隐藏

关于LDR的疑问与探索

Windows编程之模块遍历