实现DLL注入的方法已经很多了,也都比较成熟,用的最多的方法是通过远程线程进行线程注入,然后导入Dll文件。远程线程里有两个关键技术需要解决,一个是全局变量和字符串的存取问题,另一个是地址重定位问题。如果用汇编来实现这两个问题是很简单的,但在高级语言中就显得有点笨拙了。以前有一篇用C++来实现这个技术的文章,但那篇文章用了一个变通的方法,即用局部变量来代替全局变量,在建立远程线程的时候把变量传给远程线程。这个方法可以说达到了一箭双雕的目的,因为局部变量是存在于堆栈中的,不存在绝对地址的调用问题,对局部变量的访问也就不存在地址重定位问题。但回头想想,那个方法还是没有能够突破C++的这些限制。笔者在参考了罗云彬的《windows环境下32位汇编语言程序设计》中线程隐藏那一章节后,提出了在C++中实现远程线程的方法。因为本文涉及到很多汇编方面的知识,所以我相信本文的意义不仅仅是提出一种DLL注入的方法,更重要的是对编程能力的训练和对加深对操作系统底层的理解。
高级语言的编译过程
C++中的变量分为以下几个情况:全局变量,字符串和局部变量。对于变量我们在用C++编程中定义了直接调用就可以,从来不用考虑这些变量在生成的二进制文件装入内存后是怎么样存储的,但在进行远程线程的操作时这些都是必须考虑的。实际上在编译时,编译器对变量和代码有着不同的处理,全局变量和字符串被放在了数据段中,局部变量存放于堆栈中,代码位于代码段中,而数据段,堆栈区,代码段在内存中是独立存放的,它们的地址并不连续。在编译时对全局变量和字符串的存取被修改为对绝对地址的调用,这个绝对地址是编译时确定的,在程序运行时由操作系统负责在该地址处分配内存并进行初始化。而编译器对局部变量的处理是将其修改为对堆栈的操作,这样局部变量的地址实际上依赖于栈寄存器,栈寄存器不同,其绝对地址也不同。
另外,高级语言中调用函数时,编译器作的处理如下:
1.将需要传递的参数反向压入堆栈;
2.用call语句调用函数地址,执行该函数;
3. 函数执行完成后恢复堆栈的平衡;
在上面三步的第二步中又可以分为几个小步:
1. 把本句call指令的下一指令地址压入堆栈;
2. 转到函数地址执行;
3. 当遇到ret之类的返指令时从栈中弹出返回地址;
4. 转到弹出的地址(也就是call语句的下句指令)继续执行。
现在在拿LoadLibrary来举个例子,该函数是windows中的一个API,位于Kernel32.dll中,其默认装载地址为7C801D7B。因为Kernel32.dll是常驻内存的,所以在一般情况下内存中的这个地址就是LoadLibrary的入口地址,但这不是绝对的。在C++中要调用这个函数我们可以这样写:
LoadLibrary(”Dll.dll”);
在汇编中就得这样写了:
Push 字符串”DLL.dll”在内存中的地址,通常这个地址位于数据区
Call LoadLibrary的入口地址,如7C801D7B
因为Windows中的API皆为标准调用约定,所以恢复堆栈的工作就由被调用的函数完成了。当然在C++中自定义的函数一般为C调用约定,如果调用自己写的函数你就要考虑恢复堆栈的问题了。最后说明一点,在汇编中调用API的返回值一般会放在寄存器eax中,所以要检测函数是否调用成功只要检测eax的值就可以了。
高级语言中远程线程遇到的问题
首先我们来回想一下远程线程的实现过程:
1.在程序中的某个地方写出远程线程代码;
2.用WriteProcessMemory将上步写的远程线程代码复制到目标进程;
3.用CreateRemoteThread建立远程线程,并使其运行。
现在仔细分析一下,不妨假设本进程为进程A,要注入的宿主进程为进程B。第一步中所写的远程线程代码位于A中,此时用到的全局变量和字符串在编译时生成的绝对地址是按进程A确定的,这些地址位于进程A的数据区,第二步复制代码时,我们把代码区的数据完整的复制到了进程B中,而数据区并没有被复制。被复制过来的代码要运行时同样要访问那些全局变量和字符串的绝对地址,在进程B中那个地址可能已经被其它进程占用,也可能是一些随机的数据,这样会造成访问出错。在汇编语言中可以在代码区中申请变量空间,在复制代码时这些变量也相应被复制过去,这样就不存在数据区绝对地址访问的问题。但C++中是不允许在代码区中申请变量空间的,所以如何让变量随代码一起被复制是第一个需要解决的问题。
其二,假如我们己经在代码区中成功的存放了变量。但编译器在编译时把对这个变量的存取修改为绝对地址的存取,这个地址同样是位于进程A中的。在进程B中申请内存空间时,这个空间的地址是不确定的,这样代码被复制到进程B中后对这个地址的存取还是会出错,其原理和上面是一样的。所以这就需要我们对这些变量进行地址重定位。
远程线程代码的实现
因为我们的目的就是要把自己的DLL文件导入到目标进程中,所以远程线程的代码相对来说比较简单,就是调用LoadLibrary,也就是上面举的例子中的代码。我们要在代码中保存的变量有两个,一个是LoadLibrary的地址(虽说这个地址基本上是固定的,但为了保险起见我们还是动态获取,并将其保存,否则就不需要这个变量了),另一个是保存DLL文件名的字符串。为了在写远程线程代码时,在代码区中为这两个变量分配内存,我们可以用空指令给这两个变量占位,复制到进程B中后再把它修改为真正的值。如:API的地址占四个字节,汇编中一个空指令nop占一个字节,所以我们就用四个nop来为其“申请”空间;我们的DLL文件名为”Dll.dll”,占七个字节,考虑到字符要以0作为结尾标记,共占8个字节,所以我们用8个nop来“申请”空间。
下一个问题是解决地址重定位问题,这个技术在病毒,木马等诸多方面有着广泛的应用,当然这个技术并不是由笔者实现,笔者也是通过学习得到的,在这里也简要介绍一下实现的原理。
先看下面这段代码:
1 call relocal
2 relocal:
3 pop ebx
4 sub ebx , offset relocal
现在细细分析一下。第一句话执行时首先会把第三句运行时的地址(注意是运行时的地址,不是绝对地址,这个地址在进程A中与在进程B中是不一样的)压入堆栈,然后执行第三句,而第三句又把该地址弹出到寄存器ebx。第四句的offset relocal,它在编译时被编译器修改为进程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文件在第一次被导入时会自动执行DllMain中的代码,所以我们把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文件而失败。
至此所有功能模块已经介绍完毕,总体来说,这个方法实现了我们的预期功能,它的不足之处是实现起来比较繁琐,但从学习的角度来说不失为一个好方法。若文中有说的不到之处,还请各位高手们批评指正。
文章为原创,发于黑防10年7期上,不知能不能加精,想要个邀请码。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
上传的附件: