关于64位进程注入32位进程的分析
故事开始于有人在我的项目中提了issue,也是我注册 github 来收到的第一个issue,因此我也非常重视。<!--more-->
## 前言
issue 的内容提到了,我的项目 Xprocess 注入器,没有办法实现注入 32 位进程的操作。他也给出了出错的原因,我没有在代码中获取远端的 LoadLibraryW 函数的地址。我一开始会以为很简单,网上应该有很多的实现,但是事实上,居然很难找到现成的代码。
## 解决思路
常见的方法可以获取目标模块的 `kernel32.dll` 的地址然后获取到 LoadLibraryW 函数的地址,但是遍历模块发现 64 位的程序无法使用 `HANDLE ths = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,PID);` 的方式去获得模块的地址,因为只能获取到 ntdll 和 Wow 开头的几个模块,而在 32 位下编译就可以使用这个 API 获取到真实的模块地址,但是这与我们的目标不符,因此不考虑。
后面搜搜找找找到了一个可以用的 API 是 EnumProcessModulesEx。它能够获取 32 位进程远程模块加载的基地址。获取了基地址之后我又想了很久想怎么找到 LoadLibraryW。最初的一个想法是希望 ntdll 中存在函数 GetProcAddress,然后先通过一个远线程调用得到返回之后,等待线程返回就可以找到这个函数的地址了。可惜现实给了我当头一棒,它也在 kernel32.dll 里导出的。
最后我找到了一篇手动实现 GetProcAddress 的帖子,于是有了一个灵感,将这个手动实现 GetProcAddress 去实现,然后替换为远程版本的。
## 实现过程
首先确定这个代码是可运行且无误的。
```cpp
DWORD MyGetProcAddress(
HMODULE hModule, // handle to DLL module
LPCSTR lpProcName // function name
)
{
int i=0;
PIMAGE_DOS_HEADER pImageDosHeader = NULL;
PIMAGE_NT_HEADERS pImageNtHeader = NULL;
PIMAGE_EXPORT_DIRECTORY pImageExportDirectory = NULL;
pImageDosHeader=(PIMAGE_DOS_HEADER)hModule;
pImageNtHeader=(PIMAGE_NT_HEADERS)((DWORD)hModule+pImageDosHeader->e_lfanew);
pImageExportDirectory=(PIMAGE_EXPORT_DIRECTORY)((DWORD)hModule+pImageNtHeader->OptionalHeader.DataDirectory.VirtualAddress);
DWORD *pAddressOfFunction = (DWORD*)(pImageExportDirectory->AddressOfFunctions + (DWORD)hModule);
DWORD *pAddressOfNames = (DWORD*)(pImageExportDirectory->AddressOfNames + (DWORD)hModule);
DWORD dwNumberOfNames = (DWORD)(pImageExportDirectory->NumberOfNames);
DWORD dwBase = (DWORD)(pImageExportDirectory->Base);
WORD *pAddressOfNameOrdinals = (WORD*)(pImageExportDirectory->AddressOfNameOrdinals + (DWORD)hModule);
DWORD dwName = (DWORD)lpProcName;
if ((dwName & 0xFFFF0000) == 0)
{
goto xuhao;
}
for (i=0; i<(int)dwNumberOfNames; i++)
{
char *strFunction = (char *)(pAddressOfNames + (DWORD)hModule);
if (lstrcmp(lpProcName, strFunction) == 0)
{
return (pAddressOfFunction] + (DWORD)hModule);
}
}
return 0;
// 这个是通过以序号的方式来查函数地址的
xuhao:
if (dwName < dwBase || dwName > dwBase + pImageExportDirectory->NumberOfFunctions - 1)
{
return 0;
}
return (pAddressOfFunction + (DWORD)hModule);
}
```
这个 hModule 其实就是当前模块的地址。可以发现它采用解析 PE 文件的方式去遍历模块的导出表。
本地的实现了下一步就是实现远程的版本,这里需要非常仔细地去研究每一个访存的位置,因为在这个代码里一个简简单单的变量访问很有可能在远程版本中就需要通过 ReadProcessMemory 来实现。
下面我给出我写好的结果(只适配了32位的,64位的需要改一下 NT 头结构体):
```cpp
FARPROC GetRemoteProcAddress(HANDLE hProcess, HMODULE hModule, LPCSTR lpProcName) {
BYTE buffer;
SIZE_T bytesRead;
if (!ReadProcessMemory(hProcess, hModule, buffer, sizeof(buffer), &bytesRead)) {
return NULL;
}
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS32 ntHeaders = (PIMAGE_NT_HEADERS32)((BYTE*)buffer + dosHeader->e_lfanew);
DWORD RVAForExpDir = ntHeaders->OptionalHeader.DataDirectory.VirtualAddress;
if (!ReadProcessMemory(hProcess, (BYTE*)hModule + RVAForExpDir, buffer, sizeof(IMAGE_EXPORT_DIRECTORY), &bytesRead)) {
return NULL;
}
PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)buffer ;
DWORD funcAddr = (DWORD)( exportDir->AddressOfFunctions);
DWORD nameAddr = (DWORD)( exportDir->AddressOfNames);
DWORD nameOrdAddr = (DWORD)( exportDir->AddressOfNameOrdinals);
for (DWORD i = 0; i < exportDir->NumberOfNames; i++) {
char name;
DWORD TrueNameAddr;
WORD TrueOrd;
DWORD TrueFuncAddr;
if (!ReadProcessMemory(hProcess, (BYTE*)hModule + nameAddr + sizeof(DWORD)*i, &TrueNameAddr, sizeof(TrueNameAddr), &bytesRead)) {
return NULL;
}
if (!ReadProcessMemory(hProcess, (LPCVOID)((BYTE*)hModule + (DWORD)TrueNameAddr), name, sizeof(name), &bytesRead)) {
return NULL;
}
if (stricmp(name, lpProcName) == 0) {
DWORD LoadLibraryAddr = 0;
if (!ReadProcessMemory(hProcess, (BYTE*)hModule + nameOrdAddr + sizeof(WORD)*i, &TrueOrd, sizeof(TrueOrd), &bytesRead)) {
return NULL;
}
if (!ReadProcessMemory(hProcess, (BYTE*)hModule + funcAddr + sizeof(DWORD)*(TrueOrd), &TrueFuncAddr, sizeof(TrueFuncAddr), &bytesRead)) {
return NULL;
}
return (FARPROC)(TrueFuncAddr + (BYTE*)hModule);
}
}
return NULL;
}
```
最后再判断注入的目标进程是不是 32 位的来选择合适的获取地址的方式去注入,最后实现也非常成功。
(https://github.com/xia0ji233/Xprocess/issues/2)
[本次的 commit](https://github.com/xia0ji233/Xprocess/commit/cb091a4347fdcc3a1d812b1740f6a14d214b7888)
特此分享一下本次的经历,也给各位师傅们一个 64 位注入 32 位进程的参考案例。
## 参考文献
- https://cloud.tencent.com/developer/article/1471341 大佬我是纯小白C语言毫无基础 我用vcC语言64去执行都是得出的dll地址数据都是0是麻烦帮忙看看哪出问题了,,, #include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <windows.h>
#include <tchar.h>
#include <tlhelp32.h>
#include <psapi.h>
// 函数声明
char* replace_substring(char* str, const char* old_substr, const char* new_substr) {
char* result;
int i, cnt = 0;
int new_len = strlen(new_substr);
int old_len = strlen(old_substr);
// 遍历str,计算需要替换的次数
for (i = 0; str != '\0'; i++) {
if (strstr(&str, old_substr) == &str) {
cnt++;
// 跳过已经匹配的字符串
i += old_len - 1;
}
}
// 分配足够的内存来保存新的字符串
// 新字符串的长度 = 原字符串长度 - 替换次数 * 旧字符串长度 + 替换次数 * 新字符串长度 + 1(空字符)
int new_str_len = i - cnt * (old_len - 1) + cnt * new_len + 1;
result = (char*)malloc(new_str_len * sizeof(char));
if (!result) {
// 内存分配失败
perror("Memory allocation failed");
exit(EXIT_FAILURE);
}
// 使用两个指针来遍历和构建新的字符串
i = 0;
char* ptr = result;
while (*str) {
if (strstr(str, old_substr) == str) {
// 复制新的子串
char buffer;
//strcpy_s(new_substr, sizeof(new_substr), ptr);
strcpy_s(ptr, sizeof(ptr), new_substr);
//strcpy(ptr, new_substr);
ptr += new_len;
str += old_len;
}
else {
// 复制单个字符
*ptr++ = *str++;
}
}
*ptr = '\0'; // 添加字符串结束符
return result;
}
FARPROC GetRemoteProcAddress(HANDLE hProcess, HMODULE hModule, LPCSTR lpProcName) {
BYTE buffer;
SIZE_T bytesRead;
if (!ReadProcessMemory(hProcess, hModule, buffer, sizeof(buffer), &bytesRead)) {
return NULL;
}
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS32 ntHeaders = (PIMAGE_NT_HEADERS32)((BYTE*)buffer + dosHeader->e_lfanew);
DWORD RVAForExpDir = ntHeaders->OptionalHeader.DataDirectory.VirtualAddress;
if (!ReadProcessMemory(hProcess, (BYTE*)hModule + RVAForExpDir, buffer, sizeof(IMAGE_EXPORT_DIRECTORY), &bytesRead)) {
return NULL;
}
PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)buffer;
DWORD funcAddr = (DWORD)(exportDir->AddressOfFunctions);
DWORD nameAddr = (DWORD)(exportDir->AddressOfNames);
DWORD nameOrdAddr = (DWORD)(exportDir->AddressOfNameOrdinals);
for (DWORD i = 0; i < exportDir->NumberOfNames; i++) {
char name;
DWORD TrueNameAddr;
WORD TrueOrd;
DWORD TrueFuncAddr;
if (!ReadProcessMemory(hProcess, (BYTE*)hModule + nameAddr + sizeof(DWORD) * i, &TrueNameAddr, sizeof(TrueNameAddr), &bytesRead)) {
return NULL;
}
if (!ReadProcessMemory(hProcess, (LPCVOID)((BYTE*)hModule + (DWORD)TrueNameAddr), name, sizeof(name), &bytesRead)) {
return NULL;
}
if (_stricmp(name, lpProcName) == 0) {
DWORD LoadLibraryAddr = 0;
if (!ReadProcessMemory(hProcess, (BYTE*)hModule + nameOrdAddr + sizeof(WORD) * i, &TrueOrd, sizeof(TrueOrd), &bytesRead)) {
return NULL;
}
if (!ReadProcessMemory(hProcess, (BYTE*)hModule + funcAddr + sizeof(DWORD) * (TrueOrd), &TrueFuncAddr, sizeof(TrueFuncAddr), &bytesRead)) {
return NULL;
}
return (FARPROC)(TrueFuncAddr + (BYTE*)hModule);
}
}
return NULL;
}
FARPROC GetLoadLibraryW(HANDLE hProcess) {
HMODULE hMods;
DWORD cbNeeded;
unsigned int i;
FARPROC ret = NULL;
if (EnumProcessModulesEx(hProcess, hMods, sizeof(hMods), &cbNeeded, LIST_MODULES_32BIT)) {
for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) {
TCHAR szModName;
if (GetModuleBaseName(hProcess, hMods, szModName, sizeof(szModName) / sizeof(TCHAR))) {
//MessageBox(NULL, szModName, L"warn", MB_OK);
if (!_wcsicmp(L"kernel32.dll", szModName)) {
ret = GetRemoteProcAddress(hProcess, hMods, "LoadLibraryA");
}
}
}
}
return ret;
}
int main() {
const char* str1 = "外1";
const char* str2 = "外";
//char* result;
char* result = "qwe外额";
char* new_text = replace_substring(result, str2, str1);
printf(new_text);
DWORD dwProcessId;
HWND hWnd = FindWindowA("#32770", "krc2lrc v1.2"); // 查找窗口,类名为NULL,窗口名为"krc2lrc v1.2"
if (hWnd != NULL) {
printf("窗口句柄: %d\n", (void*)hWnd); // 打印窗口句柄
GetWindowThreadProcessId(hWnd, &dwProcessId);
}
else {
printf("未找到窗口\n");
}
printf("pLoadLibraryA failed (%d).\n", dwProcessId);
FARPROC pLoadLibraryA = GetLoadLibraryW(dwProcessId);
printf("pLoadLibraryA failed (%d).\n", pLoadLibraryA);
printf("pLoadLibraryA1 failed (%p).\n", pLoadLibraryA);
return 0;
}
本帖最后由 爱飞的猫 于 2024-6-19 08:57 编辑
也可以尝试注入 ShellCode,透过 PEB 爬链表(没找到就利用 `LdrLoadDll` 加载),得到这个地址。
不过如果都用 ShellCode 了… 完全可以直接带着 DLL 二进制数据一同写出到目标进程内存,然后触发执行 ShellCode 来初始化 DLL 加载(如区段映射、处理 IAT 和重定位等),这样注入的 PE 模块不能透过系统 API 枚举。
---
> 再就是往往我需要反复测试反复注入一个进程的情况(这是第二点),我的 dll 和进程一般是不变的,而我每次都需要很麻烦地重复那几个步骤,于是我花了点时间写了这个项目。
命令行程序或许可以让注入器的设计更简洁?例如写一个 CMD 文件放到编译的 DLL 旁边,编译后手动执行或设定自动执行实现注入。
```pwsh
# 已知目标是 `target.exe` 且 DLL 文件是 `hello.dll` 的情况
.\my_dll_inject.exe "target.exe" .\hello.dll
```
---
实验了一下,整了个简单地 POC 来透过 ShellCode 注入 DLL:
- 二进制文件:https://pan.baidu.com/s/1RbUr4p9D_SLgH3DjvF0d_w?pwd=wyin
- 项目源码:https://github.com/FlyingRainyCats/DllInject
只实现了 32/64 位注入器本体注入 32 位 DLL 到 32 位程序的功能。要扩展的话,写一点 x64 的汇编引导代码应该就行了。
![](https://imgsrc.baidu.com/forum/pic/item/d788d43f8794a4c2c3371f1548f41bd5ad6e39fc.png)
前排,火钳刘明{:301_1010:} 非常好的案例。谢谢! 这么一对比,我在github上的回复好敷衍啊 谢谢大佬 支持,,,,,,,,,,, 学习了,感谢分享!
学习了,感谢分享! 学习了,感谢分享! 膜拜一下!~·······························