DLL注入的实现方式有很多种,都比较成熟。 使用最多的方法是通过远程线程进行线程注入,然后导入Dll文件。 远程线程中有两个关键技术需要解决,一是全局变量和字符串的访问问题,二是地址重定位问题。 这两个问题用汇编实现很简单,但是用高级语言就显得有点笨拙了。 之前有一篇文章用C++来实现这个技术,但是那篇文章使用了,就是用局部变量代替全局变量,在创建远程线程的时候把变量传递给远程线程。 这种方式可以说是达到了以一石二鸟的目的,因为栈中存在局部变量,不存在调用绝对地址的问题,访问局部变量时也不存在地址重定位的问题。 但是回过头来看,那个方法还是没能突破C++的这些限制。 作者参考了罗云斌的《环境中的32位汇编语言程序设计》中线程隐藏的章节,提出了一种在C++中实现远程线程的方法。 因为这篇文章涉及到很多汇编知识,所以我认为这篇文章的意义不仅仅在于提出一种DLL注入的方法,更重要的是锻炼编程能力,加深对底层的理解。操作系统。
高级语言编译过程
C++中的变量分为以下几种情况:全局变量、字符串和局部变量。 对于变量,我们可以在C++编程中定义直接调用。 我们永远不需要考虑生成的二进制文件加载到内存后这些变量是如何存储的,但是在执行远程线程操作时必须考虑这些。 事实上,在编译时,编译器对待变量和代码的方式是不同的。 全局变量和字符串放在数据段,局部变量放在栈,代码位于代码段,而数据段、栈区、代码段在内存中是独立存放的,它们的地址是不连续的。 在编译时对全局变量和字符串的访问更改为对绝对地址的调用。 这个绝对地址是在编译时确定的,操作系统负责在程序运行时在这个地址分配和初始化内存。 编译器对局部变量的处理是修改它们对栈进行操作,这样局部变量的地址实际上取决于栈寄存器。 不同的堆栈寄存器有不同的绝对地址。
另外,在高级语言中调用函数时,编译器会做如下处理:
1.将需要传回的参数压栈;
2、使用call语句调用函数地址,执行函数;
3、函数执行完成后恢复栈的平衡;
上述三步中的第二步,又可以分为几个小步骤:
1.将本句call指令的下一条指令地址压入栈中;
2. 到函数地址去执行;
3、遇到ret等返回指令时,将返回地址从栈中弹出;
4. 转到弹出地址(即调用语句的下一条指令)继续执行。
现在举个例子,这个函数是.dll中的一个API,它的默认加载地址是。 因为.dll是常驻内存的,所以一般来说内存中的这个地址就是入口地址,但这并不是绝对的。 要在 C++ 中调用此函数,我们可以编写:
LoadLibrary(”Dll.dll”);
在汇编中,它必须这样写:
Push 字符串”DLL.dll”在内存中的地址,通常这个地址位于数据区
Call LoadLibrary的入口地址,如7C801D7B
因为里面的API都是标准的调用约定,恢复栈的工作都是由被调用的函数完成的。 当然,C++中的自定义函数一般都使用C调用约定。 如果调用自己写的函数,就得考虑栈恢复的问题。 最后要指出的是,在汇编中调用API的返回值一般是放在寄存器eax中的,所以判断函数调用是否成功只需要查看eax的值即可。
高级语言远程线程遇到的问题
首先我们回顾一下远程线程的实现过程:
1.在程序某处写出远程线程代码;
2、将上一步编写的远程线程代码复制到目标进程;
3.用于创建远程线程并使其运行。
现在仔细分析一下,我们假设这个进程是A进程,要注入的宿主进程是B进程,第一步写的远程线程代码在A中。使用的全局变量和字符串的绝对地址此时由进程A在编译时决定。 这些地址位于进程A的数据区。 第二步复制代码时,我们将代码区的数据完全复制到进程B中,但是数据区并没有复制。 复制的代码运行时,还需要访问那些全局变量和字符串的绝对地址。 在进程B中,那个地址可能已经被其他进程占用了,也可能是一些随机数据,会导致访问错误。 在汇编语言中,可以在代码区申请变量空间,在复制代码的时候这些变量也被复制,这样就不存在数据区绝对地址访问的问题。 但是C++是不允许在代码区申请变量空间的,那么如何让变量和代码一起复制是首先需要解决的问题。
第二,如果我们在代码区成功存储了变量。 但是编译器在编译时将这个变量的访问权限改为了一个绝对地址,这个地址也位于进程A中,在进程B中申请内存空间时,这个空间的地址是不确定的,所以复制代码后到进程B,访问这个地址还是会出错。 原理同上。 所以这就需要我们重新定位这些变量的地址。
远程线程代码的实现
因为我们的目的是将自己的DLL文件导入到目标进程中,所以远程线程的代码比较简单,就是调用,也就是上面例子中的代码。 代码中我们要保存的变量有两个,一个是地址(虽然这个地址基本是固定的,但是为了保险起见,还是动态获取并保存,否则就不需要这个变量了),还有other One 是一个包含 DLL 文件名的字符串。 为了在编写远程线程代码时为这两个变量在代码区分配内存,我们可以使用空指令为这两个变量占用空间,将它们复制到进程B中,然后修改为真实值。 例如:API的地址占用四个字节,汇编中一个空指令nop占用一个字节,所以我们用四个nop为其“申请”空间; 我们的DLL文件名为“Dll.dll”,占用7个字节,考虑到字符要以0为结尾,一共占用8个字节,所以我们用8个nop来“申请”空间。
下一个问题就是解决地址重定位的问题。 该技术广泛应用于病毒、木马等诸多方面。 当然,这个技术不是作者自己实现的。 笔者也是通过学习得到的。 这里简单介绍一下实现原理。
先看下面的代码:
1 call relocal
2 relocal:
3 pop ebx
4 sub ebx , offset relocal
现在详细分析一下。 第一句执行时,将第三句运行时的地址(注意是运行时的地址,不是绝对地址,这个地址在进程A和进程B中是不一样的)压栈,然后执行第三句,第三句将地址弹出到寄存器ebx中。 对于第四句,在编译时被编译器修改为进程A中第三句的绝对地址。 如果现在代码运行在进程B中,减去第四句后,ebx不为0,而是这段代码在进程A中的地址偏移量与进程B中的地址偏移量之差! 得到这个差值后,每当在进程B中访问一个包含绝对地址的变量时,只要加上这个差值就可以得到正确的地址。
好了,关键技术实现后远程线程的代码如下:
REMOTE_THREAD_BEGIN: //远程线程代码开始标记
_asm
{
//*******给LoadLibrary函数地址占位*******
LoadLibraryAddr:
nop
nop
nop
nop
//*******给FreeLibrary函数地址占位*******
FreeLibraryAddr:
nop
nop
nop
nop
//*******给动态链接库名占位*******
LibraryName:
nop
nop
nop
nop
nop
nop
nop
nop
//*******代码开始的真正位置*******
REMOTE_THREAD_CODE:
//*******实现地址重定位,ebx保存差值*******
call relocal
relocal:
pop ebx
sub ebx , offset relocal
//*******1.调用LoadLibrary*******
//*******1.1.压入LoadLibrary参数(动态链接库名)*******
mov eax , ebx
add eax , offset LibraryName //变量地址加上ebx,实现地址重定位
push eax
//*******1.2.调用LoadLibrary*******
mov eax , ebx
add eax , offset LoadLibraryAddr //同样实现地址重定位
mov eax , [eax] //从变量中取出LoadLibrary的地址
call eax
//*******1.3.检测是否成功,如果失败了就直接返回,防止程序异常*******
or eax , eax
jnz NEXT1 //执行成功,跳转到位NEXT1继续执行
ret
NEXT1:
// *******2.释放动态链接库*******
// *******2.1.压入FreeLibrary参数*******
push eax
// *******2.2.调用FreeLibrary*******
mov eax , ebx
add eax , offset FreeLibraryAddr //地址重定位
mov eax , [eax] //从变量中取出FreeLibrary的地址
call eax
ret
}
REMOTE_THREAD_END:
因为第一次导入时会自动执行DLL文件中的代码,所以我们把写在DLL中的代码放在这个函数中,这样只要导入了DLL文件就可以执行代码; 必须获取DLL文件中的函数地址,会增加工作量。 当然,如果你不怕麻烦,也可以试试。
主程序的实现
其实远程线程代码实现之后,这一段的技术含量相对低很多。 这段代码主要实现了以下功能:
1、在宿主进程中申请代码空间;
2.将远程线程的代码复制到宿主进程;
3、修正远程线程变量的值;
4. 创建远程线程,实现远程代码执行。
关键代码如下:
//*******1. 在宿主进程中申请代码空间*******
//*******1.1. 通过进程ID打开进程句柄,并获得进程句柄*******
HANDLE hSelectedProcHandle; //保存宿主进程句柄
hSelectedProcHandle = OpenProcess(PROCESS_ALL_ACCESS , FALSE ,
nSelectedThreadId); //进程ID的获取方法,完整的源代码中有介绍,这里就不介绍了
//*******1.2.得到远程线程代码长度,目的是得到要申请的空间的大小******
int nRemoteThreadCodeLength; //保存代码长度
_asm
{
mov eax , offset REMOTE_THREAD_END
mov ebx , offset REMOTE_THREAD_BEGIN
sub eax , ebx //用代码结尾偏移减去开始的偏移,得到代码长度
mov nRemoteThreadCodeLength , eax
}
//*******1.3.在宿主进程中申请空间*******
LPVOID pRemoteThreadAddr; //保存申请空间的基址
pRemoteThreadAddr = VirtualAllocEx(hSelectedProcHandle , NULL , nRemoteThreadCodeLength , MEM_COMMIT,PAGE_EXECUTE_READWRITE);
//*******2.把远程线程的代码复制到宿主进程*******
//*******2.1.得到本进程中远程线程代码的起始地址*******
LPVOID pRemoteThreadCodeBuf; //指向本进程中远程线程代码的起始位置
DWORD nWritenNum , nSuccess; //临时变量
_asm mov eax , offset REMOTE_THREAD_BEGIN
_asm mov pRemoteThreadCodeBuf , eax
//*******2.2.向宿主进程中复制代码*******
nSuccess = WriteProcessMemory(hSelectedProcHandle , pRemoteThreadAddr , pRemoteThreadCodeBuf , nRemoteThreadCodeLength , &nWritenNum);
// *******3.修正远程线程中变量的值*******
// *******3.1.首先获取两个关键函数的地址*******
HMODULE hKernel32;
hKernel32 = LoadLibrary("Kernel32.dll");
LPVOID pLoadLibrary , pFreeLibrary;
pLoadLibrary = (LPVOID)GetProcAddress(hKernel32 , "LoadLibraryA");
pFreeLibrary = (LPVOID)GetProcAddress(hKernel32 , "FreeLibrary");
// *******3.2.修正代码*******
PBYTE pRemoteAddrMove; //在远程线程地址上移动的指针
pRemoteAddrMove = (PBYTE)pRemoteThreadAddr;
// *******3.2.1.修正LoadLibrary地址*******
nSuccess = WriteProcessMemory(hSelectedProcHandle ,
pRemoteAddrMove ,
&pLoadLibrary ,
4 ,
&nWritenNum);
//*******3.2.2.修正FreeLibrary地址*******
pRemoteAddrMove +=4; //定位到保存FreeLibrary的变量
nSuccess = WriteProcessMemory(hSelectedProcHandle ,
pRemoteAddrMove ,
&pFreeLibrary ,
4 ,
&nWritenNum);
//*******3.2.3.修正动态链接库名*******
char szDllName[8] = {"Dll.dll"}; //注意这里必须是8个字符,
//并且必须与你的DLL文件名相同
pRemoteAddrMove +=4;
nSuccess = WriteProcessMemory(hSelectedProcHandle ,
pRemoteAddrMove ,
szDllName ,
8 ,
&nWritenNum);
//*******4.创建远程线程,使远程代码执行*******
//*******4.1.把指针移动到远程线程代码开始处*******
pRemoteAddrMove +=8;
HANDLE hRemoteThreadHandle; //远程线程句柄
// *******4.2.定义远程线程函数类型*******
typedef unsigned long (WINAPI *stRemoteThreadProc)(LPVOID);
stRemoteThreadProc pRemoteThreadProc;
// *******4.3.把入口地址赋给声明的函数*******
pRemoteThreadProc = (stRemoteThreadProc)pRemoteAddrMove;
//*******4.4.创建远程线程*******
hRemoteThreadHandle = CreateRemoteThread(hSelectedProcHandle , NULL , 0 ,
pRemoteThreadProc , 0 , 0 , NULL);
因为这个模块主要是调用了一些API,通过查看这些API的信息就可以知道它们的用法,这里就不详细介绍了。 附件源码是一个基于对话框的MFC程序,还有一个获取当前系统进程的模块,这里不再介绍实现过程。 它还附带一个简单的 DLL 项目作为测试。 运行程序时一定要将DLL文件放在系统搜索路径下,否则会因为找不到DLL文件而失败。
至此,所有的功能模块都介绍完了。 总的来说,这个方法达到了我们预期的功能。 它的缺点是实现起来比较麻烦,但是从学习的角度来说是一个很好的方法。 如果文章有不对的地方,欢迎批评指正。