【Windows 核心编程学习笔记】远程注入DLL
Abstract Keywords
Citation Yao Qing-sheng.【Windows 核心编程学习笔记】远程注入DLL.FUTURE & CIVILIZATION Natural/Social Philosophy & Infomation Sciences,20240324. https://yaoqs.github.io/20240324/windows-he-xin-bian-cheng-xue-xi-bi-ji-yuan-cheng-zhu-ru-dll/
远程注入 DLL
一、概述
为了隐藏自身的进程信息,我们希望将进程作为一个合法进程的线程运行。由于系统进程间不允许直接操作资源,因而我们需要在合法进程内部创建一个线程,为其指定要执行的代码。一种简单的方式是令远程线程载入一个我们编写的 DLL,通过 DllMain () 函数执行我们需要的代码。基本思路是将 LoadLibrary () 函数作为一个线程函数来调用:
CreateRemoteThread()---->LoadLibrary()---->DllMain()
这里的核心函数是 CreateRemoteThread (),它用来在远程进程中创建一新线程。我们来看一下这个函数:
HANDLE WINAPI CreateRemoteThread(
HANDLE hProcess, // 要创建远程线程的进程句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 用于定义新线程的安全属性,这里设为 NULL 采用默认值即可
SIZE_T dwStackSize, // 初始化线程堆栈大小,NULL 为默认大小
LPTHREAD_START_ROUTINE lpStartAddress, // 线程函数开始的地址
LPVOID lpParameter, // 线程函数参数
DWORD dwCreationFlags, // 函数表示创建线程后线程的运行状态
LPDWORD lpThreadId // 返回线程 ID,不关心可以设为 NULL 不返回
);
使用这个函数关键要解决三个参数问题:
l 获得远程线程的进程句柄,而且要确保相应权限
l 获取远程进程中线程函数的开始地址,而非本地地址
l 向远程线程成功传入 DLL 路径字符串
解决了这三个问题,我们的远程注入 DLL 就基本完成了。接下来,这篇笔记的组织结构如下:
F 获取远程进程句柄
l 枚举系统进程
l 提升进程权限
F 获取 LoadLibrary () 函数在远程进程中的地址
F 向远程线程中写入 DLL 路径字符串
l 利用 VirtualAllocEx () 分配远程地址空间
l 利用 WriteProcessMemory () 写入远程地址空间
F 程序源码
F 运行测试
二、获取远程进程句柄
我们主要利用 OpenProcess () 函数来获得要注入的进程的句柄,句柄是系统中可以起到唯一标识作用的一个对象。我们来看一下 OpenProcess () 函数:
HANDLE WINAPI OpenProcess(
DWORD dwDesiredAccess, // 获取的句柄的访问权限
BOOL bInheritHandle, // 是否可为新进程继承
DWORD dwProcessId // 要获取句柄的进程 ID
);
句柄的访问权限是指我们要使用该进程的句柄做哪些访问操作,对于远程注入 DLL 来说,主要有:
PROCESS_CREATE_THREAD | //For CreateRemoteThread()
PROCESS_VM_OPERATION | //For VirtualAllocEx()/VirtualFreeEx()
PROCESS_VM_WRITE //For WriteProcessMemory(0
当然,我们也可以直接设为最高权限:PROCESS_ALL_ACCESS。
第二个参数说明了是否可为新进程继承,第三个参数需要借助我们编写的子函数 ListProcess () 来获得。另外需要注意的是,对于很多系统和服务进程而言,获取其带有写权限的句柄需要主调进程拥有调试权限,我们利用子函数 EnableDebugPriv () 来提升权限。这样在 XP 下就足够了,在 VISTA 之后的系统中需要进一步提升另一个隐藏权限,这里只讨论在 XP 上的情况。
l ListProcess()
我们使用 ToolHelpAPI 获取当前运行程序的信息,从而编写适合自己需要的工具(@MSDN)。它支持的平台比较广泛,可以在 Windows CE 下使用。在 Windows Mobile SDK 的 Samples 里面有一个 PViewCE 的样例程序,就是用这个来查看进程和线程信息的。
使用方法就是先用 CreateToolhelp32Snapshot 将当前系统的进程、线程、DLL、堆的信息保存到一个缓冲区,这就是一个系统快照。如果你只是对进程信息感兴趣,那么只要包含 TH32CS_SNAPPROCESS 标志即可。 常见标志如下:
TH32CS_SNAPHEAPLIST:列举 th32ProcessID 指定进程中的堆
TH32CS_SNAPMODULE:列举 th32ProcessID 指定进程中的模块
TH32CS_SNAPPROCESS:列举系统范围内的所有进程
TH32CS_SNAPTHREAD:列举系统范围内的所有线程
函数执行成功返回快照句柄,否则返回 INVALID_HANDLE_VALUE。
得到系统快照句柄后,我们调用 Process32First 和 Process32Next 来依次获取系统中每个进程的信息,将信息存入 PROCESSENTRY32 结构体中,该结构体中存放着进程的主要信息,如
DWORD th32ProcessID; // 进程 ID
DWORD th32ModuleID; // 进程模块 ID
CHAR szExeFile [MAX_PATH]; // 进程的可执行文件名
这两个函数当枚举到进程时返回 TRUE,否则返回 FALSE。
然后调用一次 Process32First 函数,从快照中获取第一个进程,然后重复调用 Process32Next,直到函数返回 FALSE 为止,这样将遍历快照中进程列表。这两个函数都带两个参数,它们分别是快照句柄和一个 PROCESSENTRY32 结构。调用完 Process32First 或 Process32Next 之后,PROCESSENTRY32 中将包含系统中某个进程的关键信息。其中进程 ID 就存储在此结构的 th32ProcessID。此 ID 传给 OpenProcess API 可以获得该进程的句柄。对应的可执行文件名及其存放路径存放在 szExeFile 结构成员中。在该结构中还可以找到其它一些有用的信息。
需要注意的是:在调用 Process32First () 之前,要将 PROCESSENTRY32 结构的 dwSize 成员设置成 sizeof (PROCESSENTRY32)。 然后再用 Process32First、Process32Next 来枚举进程。使用结束后要调用 CloseHandle 来释放保存的系统快照。具体程序代码如下:
// 利用 ToolHelp32 库来枚举当前系统进程
#include
#include
#include
#include
int ListProcess()
{
// 获取系统快照
HANDLE hProcessSnap = CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0); // 不要写错 CreateToolhelp32Snapshot ()
if (hProcessSnap == INVALID_HANDLE_VALUE)
{
printf(“CreateToolHelp32Snapshot error!\n”);
return -1;
}
// 创建单个进程快照结构体,初始化大小
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof (PROCESSENTRY32); // 务必提前初始化,否则默认的大小不一定满足要求
// 初始化缓冲区
WCHAR buff [1024] = {0}; //PROCESSENTRY32 中的 szExeFile 为 WCHAR 类型数组,此处应一致,使用 Unicode 码
// 枚举系统快照链表中的第一个进程项目
BOOL bProcess = Process32First(hProcessSnap, &pe32);
while (bProcess)
{
// 格式化进程名和进程 ID,这里要使用 printf 的宽字符版
// 格式字符串 “” 都需要用 L 转换为宽字符形式
wsprintf(buff, L"FileName:%-30sID:%-6d\r\n", pe32.szExeFile, pe32.th32ProcessID);
wprintf(L"%s\n",buff);
// 缓冲区复位
memset(buff, 0, sizeof(buff));
// 继续枚举下一个进程
bProcess = Process32Next(hProcessSnap, &pe32);
}
CloseHandle(hProcessSnap);
return 0;
}
l EnableDebugPriv()
提升权限主要利用下面四个函数:
GetCurrentProcessID () // 得到当前进程的 ID
OpenProcessToken () // 得到进程的令牌句柄
LookupPrivilegeValue () // 查询进程的权限
AdjustTokenPrivileges () // 调整令牌权限
进程的权限设置存储在令牌句柄中,我们需要先获取进程的令牌句柄,其次获取进程中权限类型的 LUID 值,利用此值来设置进程新的权限,具体函数调用顺序如下:
OpenProcessToken()---->LookupPrivilegeValue()---->AdjustTokenPrivileges()
具体代码如下:
#include
#include
int EnableDebugPriv(const WCHAR *name)
{
HANDLE hToken; // 进程令牌句柄
TOKEN_PRIVILEGES tp; //TOKEN_PRIVILEGES 结构体,其中包含一个【类型 + 操作】的权限数组
LUID luid; // 上述结构体中的类型值
// 打开进程令牌环
//GetCurrentProcess () 获取当前进程的伪句柄,只会指向当前进程或者线程句柄,随时变化
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, &hToken))
{
printf(“OpenProcessToken error\n”);
return -8;
}
// 获得本地进程 name 所代表的权限类型的局部唯一 ID
if (!LookupPrivilegeValue(NULL, name, &luid))
{
printf(“LookupPrivilegeValue error\n”);
}
tp.PrivilegeCount = 1; // 权限数组中只有一个 “元素”
tp.Privileges [0].Attributes = SE_PRIVILEGE_ENABLED; // 权限操作
tp.Privileges [0].Luid = luid; // 权限类型
// 调整进程权限
if (!AdjustTokenPrivileges(hToken, 0, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL))
{
printf(“AdjustTokenPrivileges error!\n”);
return -9;
}
return 0;
}
三、获取 LoadLibrary () 的远程地址
对于 Windows 系统而言,本地进程和远程进程中的 Kernel32.dll 被映射到地址空间的同一内存地址,因而只要获取本地进程中 LoadLibrary () 的地址,在远程进程中也同样是这个地址,可以直接传给 CreateRemoteThread ():
LPTHREAD_START_ROUTINE pLoadLibrary
=
(LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT(“kernel32.dll”)), “LoadLibraryA”);
GetProcAddress 函数检索指定的动态链接库 (DLL) 中的输出库函数地址。
函数原型:
FARPROC GetProcAddress(
HMODULE hModule, // DLL 模块句柄
LPCSTR lpProcName // 函数名,以 NULL 结尾的字符串
);
返回值:
如果函数调用成功,返回值是 DLL 中的输出函数地址。
如果函数调用失败,返回值是 NULL。得到进一步的错误信息,调用函数 GetLastError。
四、向远程进程中写入 DLL 路径字符串
l VirtualAllocEx()
如果直接向 CreateRemoteThread () 传入 DLL 路径,如”C:\\Windows\\System32\\MyDLL.dll” 那么实际向远程线程传递的是一个本地的指针值,这个值在远程进程的地址空间中是没有意义的。所以我们需要使用 VirtualAllocEx () 函数在远程进程中先分配一段空间,用于直接写入我们的 DLL 路径。
函数原形:
LPVOID VirtualAllocEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
hProcess:
申请内存所在的进程句柄。
lpAddress:
保留页面的内存地址;一般用 NULL 自动分配 。
dwSize:
欲分配的内存大小,字节单位;注意实际分 配的内存大小是页内存大小的整数倍。
我们这里的实际代码为:
// 在远程进程中分配内存,准备拷入 DLL 路径字符串
// 取得当前 DLL 路径
char DllPath [260]; //Windows 路径最大为
GetCurrentDirectoryA (260, DllPath); // 获取当前进程执行目录
printf(“Proces***e Directory is %s\n”, DllPath);
strcat (DllPath, “\\…\\Debug\\MyDLL.dll”); // 链接到 DLL 路径
LPVOID pRemoteDllPath = VirtualAllocEx(hRemoteProcess, NULL, strlen(DllPath) + 1, MEM_COMMIT, PAGE_READWRITE);
if (pRemoteDllPath == NULL)
{
printf(“VirtualAllocEx error\n”);
return -3;
}
l WriteProcessMemory()
我们利用该函数直接向远程进程中分配好的空间中写入 DLL 路径字符串
BOOL WriteProcessMemory(
HANDLE hProcess, // 进程的句柄,是用 OpenProcess 打开的
LPVOID lpBaseAddress, // 要写入的起始地址
LPVOID lpBuffer, // 写入的缓存区
DWORD nSize, // 要写入缓存区的大小
LPDWORD lpNumberOfBytesWritten // 这个是返回实际写入的字节。
);
我们这里的实际代码为:
// 向远程进程空间中写入 DLL 路径字符串
printf(“DllPath is %s\n”, DllPath);
DWORD Size;
if (WriteProcessMemory(hRemoteProcess, pRemoteDllPath, DllPath, strlen(DllPath) +1, &Size) == NULL)
{
printf(“WriteProcessMemory error\n”);
return -4;
}
printf(“WriteRrmoyrProcess Size is %d\n\n”, Size);
五、程序源码
F DLL**** 源码:
#include
#include
#include
BOOL APIENTRY DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH :
{
//The DLL is being mapped into the process’s address space.
//DWORD ThreadId;
//CreateThread(NULL, NULL, MessageThread, NULL, NULL, &ThreadId);
MessageBox(NULL, L"DLL has been mapped!“, L"1st RemoteThread”, MB_OK);
// 打开文件,定义文件指针,指定打开方式为写 + 追加
FILE *fp = fopen (“C:\\test.txt”, “w”); // 打开方式参数为字符串
// 文件读写函数:
// 读写字符:getc (), putc (); 读写字符串:fgets (), fputs ()
// 向标准输入输出读入写出:
//getchar(), putchar(); gets(0, puts(0;
fputs (“一个 DLL 测试文本 \n”, fp);
//printf(“Test finished\n”);
// 关闭文件指针,释放内存
fclose(fp);
}
case DLL_THREAD_ATTACH:
//A Thread is being created.
MessageBox(NULL, L"RemoteThread has been created!“, L"2nd RemoteThread”, MB_OK);
break;
case DLL_THREAD_DETACH:
//A Thtread is exiting cleanly.
MessageBox(NULL, L"RemoteThread exit!“, L"13rd RemoteThread”, MB_OK);
break;
case DLL_PROCESS_DETACH:
//The DLL is being ummapped from the process’ address space
MessageBox(NULL, L"DLL has been unmapped!“, L"4th RemoteThread”, MB_OK);
break;
}
return TRUE; //Used only for DLL_PROCESS_ATTACH
}
F RemoteInjectExe.cpp
#include
#include
#include
#include
#include
extern int ListProcess();
extern int EnableDebugPriv(const WCHAR *);
int _tmain(int argc, TCHAR *argv[], TCHAR *env[])
{
// 为了成功使用 CreateRemoteThread () 函数,必须:
//1. 利用 OpenProcess() 获得远程进程的句柄
//2. 利用 VirtualAllocEx(),WriteProcessMemory () 写入 DLL 路径字符串
//3. 获得远程进程中 LoadLibrary() 的确切地址
// 输入进程 ID 获得进程句柄
char YesNo;
printf (“是否查看当前进程列表获得进程 ID: Y or N?”);
scanf(“%c”, &YesNo);
Sleep(250);
if (YesNo == ‘Y’ || YesNo == ‘y’)
ListProcess();
printf (“请输入要注入的进程 ID【‘’表示自身进程】:\n”);
DWORD dwRemoteProcessId;
scanf(“%d”,&dwRemoteProcessId);
// 如果输入 “” 表示向自身进程注入
if (dwRemoteProcessId == 0)
dwRemoteProcessId = GetCurrentProcessId();
// 获得调试权限
if (EnableDebugPriv(SE_DEBUG_NAME))
{
printf(“Add Privilege error\n”);
return -1;
}
// 调用 OpenProcess () 获得句柄
HANDLE hRemoteProcess;
if ((hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwRemoteProcessId)) == NULL)
{
printf(“OpenProcess error\n”);
printf(“Error Code:%d\n”,GetLastError());
system(“pause”);
return -2;
}
// 在远程进程中分配内存,准备拷入 DLL 路径字符串
// 取得当前 DLL 路径
char DllPath [260]; //Windows 路径最大为
GetCurrentDirectoryA (260, DllPath); // 获取当前进程执行目录
printf(“Proces***e Directory is %s\n”, DllPath);
strcat (DllPath, “\\…\\Debug\\MyDLL.dll”); // 链接到 DLL 路径
LPVOID pRemoteDllPath = VirtualAllocEx(hRemoteProcess, NULL, strlen(DllPath) + 1, MEM_COMMIT, PAGE_READWRITE);
if (pRemoteDllPath == NULL)
{
printf(“VirtualAllocEx error\n”);
return -3;
}
// 向远程进程空间中写入 DLL 路径字符串
printf(“DllPath is %s\n”, DllPath);
DWORD Size;
if (WriteProcessMemory(hRemoteProcess, pRemoteDllPath, DllPath, strlen(DllPath) +1, &Size) == NULL)
{
printf(“WriteProcessMemory error\n”);
return -4;
}
printf(“WriteRrmoyrProcess Size is %d\n\n”, Size);
// 获得远程进程中 LoadLibrary () 的地址
LPTHREAD_START_ROUTINE pLoadLibrary = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT(“kernel32.dll”)), “LoadLibraryA”);
if (pLoadLibrary == NULL)
{
printf(“GetProcAddress error\n”);
return -5;
}
else
{
printf(“LoadLibrary’s Address is 0x%x\n\n”, pLoadLibrary);
}
// 启动远程线程
DWORD dwThreadId;
HANDLE hThread;
if ((hThread = CreateRemoteThread(hRemoteProcess, NULL, 0, pLoadLibrary, pRemoteDllPath, 0, &dwThreadId)) == NULL)
{
printf(“CreateRemoteThread error\n”);
return -6;
}
else
{
WaitForSingleObject(hThread, INFINITE);
printf(“dwThreadId is %d\n\n”, dwThreadId);
printf(“Inject is done\n”);
}
// 释放分配内存
if (VirtualFreeEx(hRemoteProcess, pRemoteDllPath, 0, MEM_RELEASE) == 0)
{
printf(“VitualFreeEx error\n”);
return -8;
}
// 释放句柄
if (hThread != NULL) CloseHandle(hThread);
if (hRemoteProcess != NULL) CloseHandle(hRemoteProcess);
system(“pause”);
return 0;
}
六、运行测试
1. 向记事本进程注入 DLL
这里输出了一些数据作为调试时查看的参考,可以看到写入的 DLL 路径
这是 DLL 加载进远程进程地址空间时的 DLL_PROCESS_ATTACH 提示
这是远程线程创建时的 DLL_THREAD_ATTACH 提示
这是远程线程退出时 DLL_THREAD_DETACH 提示
查看此时记事本进程中的DLL模块
此时没有弹出 DLL_PROCESS_DETACH 提示,因为我们的 DLL 还存在于记事本进程中,关闭记事本
2. 向自身进程注入
在实际编写时,常常会出现各种各样的问题而弄不清原因在本地进程还是远程进程。因而我们设定当输入的进程ID为0时,向自身进程注入DLL
然而当最后结束的时候却会出现错误:
在网上查询可以知道这种错误常常是由于杀毒软件或者防火墙造成的,关闭 360 木马防火墙后运行正常:
3. 向其他进程注入
l 向 Kugoo7.exe 注入会弹出 360 提示,允许后注入成功,之后正常退出,Kugoo7 正常运行
有意思的发现是,由于我们没有卸载 Kugou7 中的注入的 DLL,因而在 Kugou7 播放的过程中时不时弹出创建线程的消息框提示,可见 Kugou7 本身在播放音乐的过程中也在不断创建、释放线程。
l 向搜狗输入法注入,依然是 360 弹出提示,允许后成功
仔细观察发现上面 360 弹出警告的程序点都在源码调用 CreateRemoteThread () 的时间点,可见 360 对该 API 进行了检测。
l 当向进程查看工具 IceSword1.22 注入时,在调用 OpenProcess () 时失败,提示内存分配访问无效,估计是设定了更高的访问权限,使得 Ring3 级别的访问基本都无效
Address:Department of Natural/Social Philosophy & Infomation Sciences, CHINA
Biography...
转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!
Like this article? Support the author with