什么是线程注入?
线程注入,是通过开启远程线程的方式,将DLL加载到目标宿主进程中的常用方式。
什么是动态链接库?
首先Windows中链接库分为两种:动态链接库DLL、静态链接库LIB。
① 静态链接库:在运行的时候就直接把代码全部加载到程序中,调用方式比如:
#prama comment (lib,"Psapi.lib")
② 动态链接库:在需要的时候加载,加载方式为:
——使用LoadLibrary动态加载DLL
——使用GetProcAddress获取DLL中导出函数的指针
——最后用FreeLibrary卸载指定的DLL
动态链接库设计的目的是为了动态加载功能,动态地释放内存,节约内存,方便每一个程序的4GB内存的管理,4GB有2GB用来放系统内核,即系统大量的DLL。其本质上也是一个可以被加载的程序。同时系统对于DLL只会保存一份。
在Visual Studio编译环境下,DLL又分为三类:
① 非MFC的DLL——使用SDK API进行编程,能被所有语言调用
② MFC规则DLL——使用MFC进行编程,能被所有语言调用
③ MFC扩展DLL——使用MFC进行编程,只能被MFC编写的程序调用
(下面使用第一种)
MFC——Microsoft Foundation Class-Library :微软用C++对API进行的封装,全部封装成了类,方便使
需要注意:
DLL的导出函数使用
extern "C" _declspec(dllexport)
而导入函数使用
extern "C" _declspec(dllimport)
extern "C" 的作用是作为一种编译约定
那么进程是如何调用DLL的呢?
①使用LoadLibrary加载进所需DLL
HMODULE hMod = LoadLibrary(DLL路径)
②定义导入函数指针
typedef int(*ADD_IMPORT) (int a,int b); // 定义一个指向返回值为int类型的函数指针
③使用GetProcAddress获得函数入口点
ADD_IMPORT add_proc = (ADD_IMPORT) GetProcAddress(hMod,"Add");
④然后就可以使用了:比如 int result = add_proc(1,2);
如下使用VS2019进行编写编译:
1、新建Win32项目,选择DLL
关于DLL入口主函数第二个参数ul_reason_for_call,即DLL四种当前的状态:
2、新建如下头文件
3、在dajiDLL.cpp中
然后同一解决方案下新建MFC项目
这里需要说一下注入的可行性:
①kernel32和user32是两个在大部分程序上都会调用的DLL
②同一个DLL,在不同的进程中,不一定被映射(加载)到同一个内存地址
③但是kernel32和user32除外,他们总是被映射到进程的内存首选地址
④因此在所有使用这两个DLL的进程中,这两个DLL的内存地址是相同的
⑤所以在本进程获取的kernel32.dll中的函数的地址,在目标进程中也是一样的
流程:目标进程-传入DLL地址-开启远程线程-加载DLL-实现DLL注入
具体使用函数如下:
OpenProcess // 获取已知进程的句柄
VirtualAllocEx // 在远程进程中申请内存空间
WriteProcessMemory // 向进程中写入东西
GetProcAddress // 取得函数在DLL中的地址
CreateRemoteThreadEx // 创建远程线程——即在其他进程中创建新的线程
CloseHandle // 关闭句柄
利用以上函数即可完成线程的注入
上面讲了这么多下面开始编写远程线程注入的代码:
① 在资源视图中找到主Dialog文件新建如下组件并命名,其中Inject按钮设为默认按钮,两个Button的ID分别为IDC_INJECT和IDC_OPEN
② 双击Open按钮编写如下代码,需要提前将编辑框1和2的ID分别命名为IDC_DLLPATH、IDC_EXENAME
// 获取DLL路径名,打开DLL文件
void CdajiInjecterDlg::OnBnClickedOpen()
{
// 定义一个filedialog对象,通过它来获取dll的路径
CFileDialog filedialog(TRUE,0,0,6UL,_T("DLL Files|*.dll|"));
// 如果弹出了对话框并且选择了DLL,那么将获得的路径填充显示到DLLPath
if (filedialog.DoModal() == IDOK)
{
CString DLLpath;
DLLpath = filedialog.GetPathName();
// 将路径填充到IDC_DLLPATH的对话框
SetDlgItemText(IDC_DLLPATH, DLLpath);
}
}
③ 双击Inject按钮,首先需要在inject按钮函数的前面添加注入功能的函数Inject
// 注入函数,同时返回值为是否注入成功,默认返回成功
BOOL Inject(LPCTSTR DLLPath,DWORD ProcessID)
{
// 定义一个句柄获取目标进程的所有权限,包括子进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, TRUE, ProcessID);
// 判读是否获取到
if (!hProcess)
{
return FALSE;
}
// 计算一下获取到的DLLpath究竟有多长,要在内存空间中申请内存来存放它,注意+1,为\0
SIZE_T PathSize = (_tcslen(DLLPath) + 1) * sizeof(TCHAR); // 宽字节
// 在晋城中申请内存存放此DLLpath
LPVOID StartAddress = VirtualAllocEx(hProcess, NULL, PathSize, MEM_COMMIT, PAGE_READWRITE);
// 如果未能获取到地址,返回失败
if (!StartAddress)
{
return FALSE;
}
// 获取到地址则将DLLPath写入此地址
if (!WriteProcessMemory(hProcess, StartAddress, DLLPath, PathSize, NULL))
{
return FALSE;
}
// 获取LoadLibrary的入口点地址,需要进行强制类型转换
PTHREAD_START_ROUTINE pfnStartAddress = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "LoadLibraryW");
// 在这里获取到的地址,在目标进程中也一样,因此直接传递过去即可
// 先判断是否获取到
if (!pfnStartAddress)
{
return FALSE;
}
// 最重要的一步,创建远程线程,首先获取句柄,createremotethreadex函数只在win7以上系统有
HANDLE hThread = CreateRemoteThreadEx(hProcess,NULL,NULL,pfnStartAddress,StartAddress,NULL,NULL,NULL);
// 判断线程是否创建成功
if (!hThread)
{
return FALSE;
}
// 最后等待线程结束,然后清理线程和进程
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}
④ 在上面的注入函数之前还需要添加一个进程查找函数,如下
// 获取进程,其参数为进程名——在进程列表中获取我们想要的参数
DWORD ProcessFind(LPCTSTR Exename)
{
// 第一个参数只获取进程名
HANDLE hProcess = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
// 对获取的进程进行判断
if(!hProcess)
{
return FALSE;
}
// 获取成功
PROCESSENTRY32 info;
info.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(hProcess,&info))
{
return FALSE;
}
while (true) // 遍历进程中的所有项
{
// 判断是否与所给进程名相符,就是输入的进程名
if(_tcscmp(info.szExeFile,Exename)==0)
{
// 相符的话返回该进程的PID
return info.th32ProcessID;
}
// 如果遍历了整个进程列表都没有找到,那么返回FALSE
if (!Process32Next(hProcess, &info))
{
return FALSE;
}
}
// 函数默认返回FALSE
return FALSE;
}
⑤ 最后是注入按钮Inject的函数代码:
// 点击按钮执行注入
void CdajiInjecterDlg::OnBnClickedInject()
{
CString Dllpath;
CString Exename;
GetDlgItemText(IDC_EXENAME, Exename);
GetDlgItemText(IDC_DLLPATH, Dllpath);
// 如果用户什么进程名都没输入,那么提示
if (Exename.GetLength() == 0)
{
MessageBox(_T("请输入进程名!"));
return; // 未填写进程名,直接返回,不让此函数继续执行
}
// DWORD类型变量用于存储PID
DWORD ProcessID = ProcessFind(Exename);
// 如果在进程列表中没找到此程序,提示
if (!ProcessID)
{
MessageBox(_T("未找到该程序!"));
return; // 不再执行下面的
}
// 执行注入,并且对注入结果进行获取
BOOL IsInjected = Inject(Dllpath, ProcessID);
// 注入成功与失败,提示
if (IsInjected)
{
MessageBox(_T("注入成功!"));
}
else
{
MessageBox(_T("注入失败!"));
}
}
最后在dllmain.cpp中添加一个清楚的注入成功提醒标识:
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
// 进程连接时进程提示
case DLL_PROCESS_ATTACH:
MessageBox(NULL,L"Hello daji!",L"Welcome!",NULL);
break;
// 注意到此只能注入32位的进程,如果想要注入64位的进程
// 需要在配置管理器中进行修改
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
如下进行测试,选择x64版本DLL(注意编译的时候选择编译的版本):
目标进程为Everything.exe
可以在任务栏的Evething中发现DLL的线程,二者此时属于同一个进程。