Internal Hack

这是一个“内部”的hack,内部的意思就是通过dll注入的方法将可执行代码直接注入到目标进程”内部“,并且新开始一个线程执行这部分代码。

内部代码具有隐蔽,高效的特点,但是注入部分其实是更难的部分,并且如果代码写的有问题,会更容易被检测。

下边是一个简单的实例代码,重要的部分或者API使用地方会被更加详细的说明。

实验程序为AssaultCube

DLL 模板

#include <iostream>
#include <Windows.h>
#include "mem.h"
#include "proc.h"

DWORD WINAPI HackThread(HMODULE hModule)
{
    
}

BOOL APIENTRY DLLMain(HMODULE hModule,
    DWORD ul_reason_for_call,
    LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
        case DLL_PROCESS_ATTACH:
            CloseHandle(CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)HackThread, hModule, 0, nullptr));
            break;
        case DLL_THREAD_ATTACH:
            ;
        case DLL_THREAD_DETACH:
            ;
        case DLL_PROCESS_DETACH:
            break;
    }
}

这里就是一个dll的模板,dllmain是一个swtich结构,其中四个case就是dll触发的原因,其实也挺清楚的,就是线程和进程的加载和分离。将要执行的事件写在case里面即可。

CreateThread

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  SIZE_T                 dwStackSize,
  LPTHREAD_START_ROUTINE lpStartAddress,
  LPVOID                 lpParameter,
  DWORD                  dwCreationFlags,
  LPDWORD                lpThreadId
);
  • lpThreadAttributes:安全属性,通常传 nullptr(默认安全)。
  • dwStackSize:线程栈大小,0 代表默认大小。
  • lpStartAddress:线程函数的指针(这里是 HackThread)。
  • lpParameter:传递给线程的参数(这里是 hModule)。
  • dwCreationFlags:控制线程创建状态,0 表示立即运行。
  • lpThreadId:返回线程 ID,传 nullptr 忽略。

接下来分析下边代码就很容易了,默认安全属性默认大小,第三个参数要写需要执行代码的位置,这里HackThread 是线程函数,必须是 DWORD WINAPI ThreadFunction(LPVOID lpParam) 这样的格式,第四个参数传递的是线程函数参数立即运行不需要知道线程的id所以最后一个参数写零。

最外面套一个CloseHandle,表明关闭这个线程的句柄,不需要进一步操作了,但是线程其实已经在运行了。

CloseHandle(CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)HackThread, hModule, 0, nullptr));

线程函数

DWORD WINAPI HackThread(HMODULE hModule)
{
    AllocConsole();
    FILE* f;
    freopen_s(&f, "CONOUT$", "w", stdout);

    std::cout << "my internal\n";

    uintptr_t moduleBase = (uintptr_t)GetModuleHandle(L"ac_client.exe");

    bool bHealth = false, bAmmo = false, bRecoil = false;
    while (true)
    {
        if (GetAsyncKeyState(VK_END) & 1)
        {
            break;
        }
        if (GetAsyncKeyState(VK_NUMPAD1) & 1)
        {
            bHealth = !bHealth;
        }

        if (GetAsyncKeyState(VK_NUMPAD2) & 1)
        {
            bAmmo = !bAmmo;
        }
        if (GetAsyncKeyState(VK_NUMPAD3) & 1)
        {
            bRecoil = !bRecoil;
            if (bRecoil)
            {
                mem::Nop((BYTE*)moduleBase + 0x63786, 10);
            }
            else
            {
                mem::Patch((BYTE*)(moduleBase + 0x63786), (BYTE*)"\x50\x8D\x4C\x24\x1C\x51\x8B\xCE\xFF\xD2", 10);
            }
        }
        uintptr_t* loaclPlayerPtr = (uintptr_t*)(moduleBase + 0x10f4f4);
        if (loaclPlayerPtr)
        {
            if (bHealth)
            {
                *(int*)(*loaclPlayerPtr + 0xF8) = 1337;
            }
            if (bAmmo)
            {
                uintptr_t AmmoAddr = 0;
                AmmoAddr = mem::FindDMAAddy((uintptr_t)loaclPlayerPtr, { 0x374, 0x14,0x0 });
                *(int*)AmmoAddr = 1337;
            }
        }
        Sleep(5);
    }   

    fclose(f);
    FreeConsole();
    FreeLibraryAndExitThread(hModule, 0);
    return 0;
}

线程代码大部分都是之前重复的功能,这里只是添加了一个控制台,让我们的代码注入是否成功更容易看到。

重复功能就不讲了,大概就是通过键盘按键来控制功能的开关。

    // 创建新的控制台 
    AllocConsole();
    // 创建新的文件指针 指向freopen_s重定向之后的文件
    FILE* f;
    // 将标准输出绑定到 CONOUT$ 终端输出,f会指向新的文件,这里就是CONOUT$
    freopen_s(&f, "CONOUT$", "w", stdout);
    std::cout << "my internal\n";
    
    
    // 清扫工作 卸载dll
    fclose(f);
    FreeConsole();
    FreeLibraryAndExitThread(hModule, 0);
    

成功注入之后就是这个样子。
image.png