1.三种DLL加载时机:
进程创建加载输入表中的DLL(静态输入)
通过调用LoadLibrary主动加载(动态加载)
系统预设加载
通过干预输入表处理过程加载目标dll
1.静态修改PE输入表法(测试程序 Notepad.exe)
准备工作:自行编写一个MsgDLL,到处一个函数Msg();
#include "stdafx.h" BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { if (ul_reason_for_call == DLL_PROCESS_ATTACH) { CreateThread(NULL, 0, ThreadShow, NULL, 0, NULL); } return TRUE; } DWORD WINAPI ThreadShow(LPVOID lpParameter) { char szPath[MAX_PATH] = { 0 }; char szBuf[1024] = { 0 }; //获取宿主进程的路径 GetModuleFileName(NULL, (LPWSTR)szPath, MAX_PATH); sprintf(szBuf, "DLL已经注入到进程:%s [pid =%d] ", szPath, GetCurrentProcessId()); //以三种方式显示自己的存在 //1. MessageBox(NULL, (LPWSTR)szBuf, "DLL存在", MB_OK); //2. printf("%s ", szBuf); //3. OutputDebugString((LPWSTR)szBuf); return 0; }
参数意义:
①hModule参数:指向DLL本身的实例句柄;
②ul_reason_for_call参数:指明了DLL被调用的原因,可以有以下4个取值:
1. DLL_PROCESS_ATTACH:
当DLL被进程 <<第一次>> 调用时,导致DllMain函数被调用,
同时ul_reason_for_call的值为DLL_PROCESS_ATTACH,
如果同一个进程后来再次调用此DLL时,操作系统只会增加DLL的使用次数,
不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。
2.DLL_PROCESS_DETACH:
当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递的ul_reason_for_call值是DLL_PROCESS_DETACH。
★如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。
3.DLL_THREAD_ATTACH:
当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,
并用值DLL_THREAD_ATTACH调用DLL的DllMain函数。
新创建的线程负责执行这次的DLL的DllMain函数,
只有当所有的DLL都处理完这一通知后,系统才允许线程开始执行它的线程函数。
4.DLL_THREAD_DETACH:
如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),
系统查看当前映射到进程空间中的所有DLL文件映像,
并用DLL_THREAD_DETACH来调用DllMain函数,
通知所有的DLL去执行线程级的清理工作。
★注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread,
系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。
③lpReserved参数:保留
第二步:判断是否还有足够的空间存储我们的导出函数 —–》(PE格式知识)
)如果空间不足,那么我们则需要采取扩大节或者新增节来进行位置存储;
Notepad无法存储我们的导出函数,那么我这里采取扩大最后一个节的方法
根据PE格式可以看到再数据目录项中的导入表RVA为00007604
那么我们这里讲RVA转成FOA文件偏移 :0x7604 -0x1000(Virtual Address) + 0x400 (Raw Address) =0x6a04 —》PE知识,看不懂重学PE
然后用16进制软件打开notepad,我这里使用的是010 editor:
首先我们可以看到一个导入表的结构为20字节也就是16h,输入表中,每20个字节(一个Image_Import_Directory)对应一个动态链接库Dll的调用数据:
并且导入表是连续的,直到它以一组0x14大小的全0的结束标记来结束
定位到0x6a04,如图所示,后面则是0x14大小的结束标记
接下来,我们将最后一个节区进行扩充:
将原有导入表搬入新地址(直接复制粘贴到新地址):
粘贴好后,在原有导入表区域构建新的OriginalFirstThunk、name和FirstThunk结构(注意我们粘贴后把原有导入表区域清零,腾出空间,做我们自己的结构)
首先清零
然后构建我们的OriginalFirstThunk、name和FirstThunk结构
在PE文件被加载前 ,OriginalFirstThunk和FirstThunk都是指向IMPORT_BY_NAME
根据结构我们可以知道:
DLLName RawOffset =0x6A14 RVA= 0x6A14 -0x400(Raw Address)+0x1000(Virtual Address) =0x7614
IMPORT_BY_NAME RawOffset =0x6A20 RVA= 0x6A20 -0x400(Raw Address)+0x1000(Virtual Address) =0x7620
在手动填写数据的时候一定要注意字节顺序问题:
然后根据刚填充的两个结构和Name的偏移,填写新的导入表结构
OriginalFirstThunk :RawOffset =0x6a04 RVA =0x6a04 -0x400(Raw Address)+0x1000(Virtual Address) =0x7604
FirstThunk:RawOffset =0x6a0c RVA = 0x6a0c -0x400(Raw Address)+0x1000(Virtual Address) =0x7620
DLLName RawOffset =0x6A14 RVA= 0x6A14 -0x400(Raw Address)+0x1000(Virtual Address) =0x7614
修改完成后,我们修正PE文件头信息:
输入表目录指向位置
FirstThunk -》可写属性
首先修正输入表目录指向位置:
在010中定位到导入表的RVA :
将它原本指向的值修改成我们新替换的位置也就是最后一节的位置:
内存偏移 = 0XB000 +0X8000 =0X13000
文件偏移 = 0x8400+0x8000=0x10400
由于使用了原来导入表数组的位置存放FirstThunk,而它原来的位置的RVA是0X7604,根据各节的起始位置和偏移量,可以确定该节属于text节,而该节原来的属性是0X60000020,写属性定义如下
# define IMAGE_SCN_MEM_WRITE 0x800000000 //节是可写的
0x 60000020+0x80000000 =0xE0000020
然后把新节属性就是原属性加上这个值也就是0xE0000020
至此我们的修改工作全部完成,保存修改结果
接下来运行修改后的NotePad,结果MessageBox没有弹出来!
这里是因为IMAGE_IMPORT_DESCRIPTOR中定义的TimeDateStamp为0xFFFFFFFF也就是-1,表示改输入项是原来预先Bound的,如果系统检测发现预绑定是有效的,那么就不会再去处理输入表加载了,所以我们只需把0x1B0到0x1B8 内容清零再次保存即可
可以看到确实加载了我们的MsgDll.dll