🔍
📢

Windows DLL注入代码至独立进程

概念

DLL注入(英语:DLL injection)是一种计算机编程技术,它可以强行使另一个进程加载一个动态链接库以在其地址空间内运行指定代码[1]。在Windows操作系统上,每个进程都有独立的进程空间,即一个进程是无法直接操作另一个进程的数据的(事实上,不仅Windows,许多操作系统也是如此)。但是DLL注入是用一种不直接的方式,来实现操作其他进程的数据。假设我们有一个DLL文件,里面有操作目标进程数据的程序代码逻辑,DLL注入就是使目标进程加载这个DLL,加载后,这个DLL就成为目标进程的一部分,目标进程的数据也就可以直接操作了。

本文编写的代码所提及的DLL均为C/C++语言生成的DLL。

DLL注入基本流程

(1) 打开目标进程
(2) 在目标进程开辟一段内存空间
(3) 往开辟的内存空间中写入要注入的DLL的路径
(4) 给目标创建一个线程, 加载DLL

1.打开目标进程

Windows下有个名为OpenProcess的函数可以打开一个进程,它的原型如下:

HANDLE OpenProcess(
  DWORD dwDesiredAccess,
  BOOL  bInheritHandle,
  DWORD dwProcessId
);
  • dwDesiredAccess 访问权限
  • bInheritHandle 是否继承句柄
  • dwProcessId 要打开的进程pid

返回值:如果打开成功,返回一个进程句柄,否则返回NULL

代码实现如下:

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid);
if (NULL == hProcess) {
    OutputDebugString("Cannot open this process.\n");
    return -1;
}

这段代码根据进程的pid以PROCESS_ALL_ACCESS权限来打开一个进程,并返回进程句柄,进程pid可以通过以下方式获得:

int GetPidByProcessName(const char* ProcessName) {
    HANDLE Processes = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,NULL);
    PROCESSENTRY32 ProcessInfo = { 0 };
    ProcessInfo.dwSize = sizeof(PROCESSENTRY32);
    while (Process32Next(Processes, &ProcessInfo)) {
        if (strcmp(ProcessInfo.szExeFile, ProcessName) == 0) {
            return ProcessInfo.th32ProcessID;
        }
    } 
    return -1;
}

该函数通过进程名返回它对应的进程pid

2. 在目标进程开辟一段内存空间

VirtualAllocEx函数可以在目标进程申请一块内存空间,函数原型如下:

LPVOID VirtualAllocEx(
  HANDLE hProcess,
  LPVOID lpAddress,
  SIZE_T dwSize,
  DWORD  flAllocationType,
  DWORD  flProtect
);
  • hProcess 要向其申请内存空间的进程
  • lpAddress 申请的内存所在的地址,传入NULL函数帮我们决定地址
  • dwSize 申请的内存空间的大小,单位为字节
  • flAllocationType 要申请的内存空间类型
  • flProtect 内存保护常量

返回值: 如果内存申请成功,返回内存空间的首地址,否则返回NULL

代码实现如下:

LPVOID lpAddr = VirtualAllocEx(hProcess, NULL, strlen(DllPath), MEM_COMMIT, PAGE_READWRITE);
if (NULL == lpAddr) {
    OutputDebugString("Cannot alloc memory.\n");
    return -1;
}

这段代码是在目标进程申请开辟一块内存空间,申请开辟的内存空间大小是DLL完整路径所占用的字节数,申请成功将会返回内存空间的起始地址

3. 往开辟的内存空间中写入要注入的DLL的路径

WriteProcessMemory可以向指定的内存地址中写入数据,函数原型如下:

BOOL WriteProcessMemory(
  HANDLE  hProcess,
  LPVOID  lpBaseAddress,
  LPCVOID lpBuffer,
  SIZE_T  nSize,
  SIZE_T  *lpNumberOfBytesWritten
);
  • hProcess 要写入的进程的句柄
  • lpBaseAddress 要写入的目标地址
  • lpBuffer 要写入的数据
  • nSize 要写入的数据的大小
  • lpNumberOfBytesWritten 一个用于接收传入目标进程的字节数的指针变量

返回值: 返回一个非零值代表写入成功,返回零则写入失败

代码实现如下:

BOOL isOk = WriteProcessMemory(hProcess , lpAddr, DllPath, strlen(DllPath), NULL);
if (!isOk) {
    OutputDebugString("Cannot write memory.\n");
    return -1;
}

这段代码是将DLL的完整路径写入到上一步开辟的内存空间中

4.给目标创建一个线程, 加载DLL

(1)GetModuleHandle函数根据模块名称得到模块的句柄,原型如下:

HMODULE GetModuleHandleA(
  LPCSTR lpModuleName
);
  • lpModuleName 模块名

返回值:指定模块的句柄

(2)GetProcAddress函数可以根据函数名来得到模块中的一个导出函数的地址,原型如下:

FARPROC GetProcAddress(
  HMODULE hModule,
  LPCSTR  lpProcName
);
  • hModule 模块句柄
  • lpProcName 导出函数名

返回值:相应的导出函数的地址

(3)CreateRemoteThread用于在指定进程的虚拟空间中开启一个线程,原型如下:

HANDLE CreateRemoteThread(
  HANDLE                 hProcess,
  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
  SIZE_T                 dwStackSize,
  LPTHREAD_START_ROUTINE lpStartAddress,
  LPVOID                 lpParameter,
  DWORD                  dwCreationFlags,
  LPDWORD                lpThreadId
);
  • hProcess 目标进程句柄
  • lpThreadAttributes 线程安全属性,传入NULL时使用默认属性
  • dwStackSize 线程初始栈大小,传入0使用默认栈大小
  • lpStartAddress 线程中要执行的函数的地址
  • lpParameter 传入线程函数的参数
  • dwCreationFlags 创建线程的参数,传入0时线程立即执行
  • lpThreadId 接收线程标识的指针,传入NULL时线程不返回标识

返回值:创建成功返回一个线程句柄,否则返回一个NULL

(4)LoadLibraryA是LoadLibrary函数的ASCII码版本,它的函数原型如下:

HMODULE LoadLibraryA(
  LPCSTR lpLibFileName
);
  • lpLibFileName 要加载的DLL的完整路径名

返回值:加载成功会返回模块的句柄,加载失败返回NULL

代码实现如下:

HMODULE hKernel32Module = GetModuleHandle("kernel32.dll");
if (NULL == hKernel32Module) {
    OutputDebugString("Cannot find kernel32.dll.\n");
    return -1;
}
FARPROC hFarProc = GetProcAddress(hKernel32Module, "LoadLibraryA");
if (NULL == hFarProc) {
    OutputDebugString("Cannot get function address.\n");
    return -1;
}
HANDLE hThread = CreateRemoteThread(hProcess
    , NULL
    , 0
    , (LPTHREAD_START_ROUTINE)hFarProc
    , lpAddr
    , 0
    , NULL 
);

上面的代码使用GetProcAddress函数获得kernel32.dll模块中LoadLibraryA函数的地址,然后在目标进程开启一个线程调用LoadLibraryA函数。lpAddr被写入DLL的完整路径,把它传入CreateRemoteThread函数,相当于就是把DLL的完整路径传给LoadLibraryA函数。

卸载DLL注入流程

DLL注入了目标进程后,如果想要把它从目标进程卸载,需要进行以下步骤:

(1) 打开目标进程
(2) 给目标创建一个线程, 卸载DLL

1. 打开目标进程

打开目标进程的操作和注入一样,不再详细展开,代码如下:

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid);
if (NULL == hProcess) {
    OutputDebugString("Cannot open this process.\n");
    return -1;
}

2. 给目标创建一个线程, 卸载DLL

这一步代码实现和注入的最后一步大体一样,同样是开启线程调用一个函数,但这个在线程里执行的函数是FreeLibrary。
FreeLibrary函数原型如下:

BOOL FreeLibrary(
  HMODULE hLibModule
);
  • hLibModule 要释放的模块的句柄

返回值:成功会返回一个非0值

代码实现如下:

HMODULE hKernel32Module = GetModuleHandle("kernel32.dll");
    if (NULL == hKernel32Module) {
        OutputDebugString("Cannot find kernel32.dll.\n");
        return -1;
    }
    FARPROC hFarProc = GetProcAddress(hKernel32Module, "FreeLibrary");
    if (NULL == hFarProc) {
        OutputDebugString("Cannot get function address.\n");
        return -1;
    }

    HMODULE hModule = GetModuleHandleByName(ModuleName,pid);

    if (NULL == hModule) {
        OutputDebugString("Cannot find this module.\n");
        return -1;
    }
    HANDLE hThread = CreateRemoteThread(hProcess
        ,NULL
        , 0
        , (LPTHREAD_START_ROUTINE)hFarProc
        , hModule
        , 0
        , NULL 
    );

由于FreeLibrary函数需要传入一个模块的句柄,那么我们需要从目标进程中扫描并找到我们想要卸载的模块,然后返回它的句柄:

HMODULE GetModuleHandleByName(const char* ModuleName,DWORD pid) {
    HANDLE Processes = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid);
    MODULEENTRY32 ModuleInfo = { 0 };
    ModuleInfo.dwSize = sizeof(MODULEENTRY32);
    char buf[0x100];
    while (Module32Next(Processes, &ModuleInfo)) {
        if (strcmp(ModuleInfo.szModule, ModuleName) == 0) {
            return ModuleInfo.hModule;
        }
    }
    return NULL;
}

完整代码 https://github.com/luoyesiqiu/LibInject