修改PE文件引入表实现加载DLL

内容简介

  1. 编写Func.dll,并编写一个EXE程序,该程序能够加载Func.dll,并调用Func.dll中的导出函数,在加载Func.dll的时候,会弹出计算器calc.exe。
  2. 使用PEview查看notepad.exe的结构,使用UltraEdit尝试手动修改notepad.exe并查看修改后程序能够运行;手动修改后,实现打开notepad.exe时加载Func.dll,从而弹出计算器(calc.exe)。
  3. 编程实现hack.exe上述过程,使得运行hack.exe不仅能够对notepad.exe进行修改,也能对一般的可执行文件进行修改,使得被修改后的可执行文件打开时加载Func.dll并弹出calc.exe。
    修改PE文件引入表实现加载DLL

DLL结构

  dll编写需要引用头文件<objbase.h>或者<windows.h>。一个dll需要包含一个类似exe中的main()函数一样的DllMain(),函数原型如下:

    BOOL WINAPI DllMain (
      _In_ HINSTANCE hinstDLL,    // handle to DLL module
      _In_ DWORD     fdwReason,   // reason for calling function
      _In_ LPVOID    lpvReserved   // reserved
    );

※ fdwReason是传入参数,该参数如下表3-1的四种值:
修改PE文件引入表实现加载DLL
※ 返回值:当系统使用DLL_PROCESS_ATTACH值调用DllMain函数时,如果成功则返回TRUE,如果初始化失败则返回FALSE。如果由于进程使用LoadLibrary函数而调用DllMain时返回值为FALSE,则LoadLibrary返回NULL。

DLL的编译链接(VS命令行中)

对于DLL的源代码文件Func.cpp:

编译,得到Func.obj文件: cl /c Func.cpp
链接,得到Func.dll文件: link /dll Func.obj
查看dll导出函数: dumpbin -exports Func.dll

DLL的加载

  对于dll文件,编写程序加载它,需要先使用LoadLibrary,如前所述,当返回值hinst非NULL时,说明加载dll成功。接着可以调用dll中的函数,需要使用GetProcAddress()函数获得导出函数的地址,返回值相当于一个函数指针,可以通过它进行函数调用。

HINSTANCE hinst=::LoadLibrary("Func.dll");
pfFuncInDll = (DLLWITHLIB)GetProcAddress(hinst, "FuncInDll");

使用的DLL源代码

#include <objbase.h>
extern "C" __declspec(dllexport) void FuncInDll(void)//定义输出函数,序号将为0001
{
   printf("%s","hello!\n");	//并没有调用该函数
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
    HANDLE g_hModule;
    switch(dwReason)
    {
	case DLL_PROCESS_ATTACH:   /*加载DLL时会进入到这里*/
 	 /*可以在这里弹出计算器,创建新进程*/
       g_hModule = (HINSTANCE)hModule;
       break;

	case DLL_PROCESS_DETACH:
	/*可以在这里把计算器进程终止掉*/
       g_hModule=NULL;
       break;
    }
    return true;
}

PE文件关键结构

MZ头

  MZ头开始的两个字节从低地址到高地址值为0x4D5A,在偏移0x3C处的4字节,是NT映像头的偏移值。在图3-2中,可以看到,notepad.exe的NT映像头的开始偏移是0xE0。
修改PE文件引入表实现加载DLL

NT映像头

  NT映像头的起始4字节为50450000,表示这是一个PE文件。接着的0x14字节是NT文件头,这里面的重要数据结构如下表3-2所示:
修改PE文件引入表实现加载DLL

可选头部

  Optional Header(可选头部中有很多重要信息),与本实验有关的是从可选头部偏移0x60处开始的,IMAGE_DATA_DIRECTORY(数据目录项),往往有16个这样数据目录项,每个目录项的形式如下所示:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;//所在的起始RVA地址
    DWORD   Size;//实际占用的大小
} IMAGE_DATA_DIRECTORY

  数据目录项中,第1个是关于引出表的,第2个是关于引入表的,实验中,需要修改的是关于引入表的部分。在下图3-3中,IMPORT table项的RVA指向的是IMPORT Directory Table,Size指的是IMPORT Directory Table的大小。实际上,下图中,还能看到BOUND IMPORT Table项不是全0,Binding Imports会让链接器使用绑定的信息,而不是调用GetProcAddress(),可以把该项置0。
修改PE文件引入表实现加载DLL

引入表

  引入表由一个个引入条目项构成,每个项20个字节,引入表的结尾最后一项是一个20个字节的全0项。引入表的每个条目项的结构如下表3-3所示:
修改PE文件引入表实现加载DLL
  在文件中查看PE文件,INT表和IAT表中的内容是一样的,加载到内存之后,IAT表指向的是实际的内存中的地址。在文件中,INT表和IAT表的每项是4字节,是一个指针,指向一个Hint/Name Table,该表的结构如下表3-4所示。特别的,INT表和IAT表中每个dll的最后一项是4字节的0。
修改PE文件引入表实现加载DLL

IDT、INT、IAT关系

  在文件中,三者之间的关系如下图3-4所示,INT和IAT指向的位置是相同的。而在内存中,二者指向的位置不同。
修改PE文件引入表实现加载DLL

代码实现思路

  1. 从MZ头中0x3C处找到NT头开始位置,从NT头中获取节数目和可选头的大小(便于后面读取节表)。
  2. 读取可选头,获取引入表位置。
  3. 读取节表,获取所有节的起始RVA和在文件中偏移,这样可以对于每个节,计算二者之间的转换偏移delta,关注数据节.data/data的所在位置。
  4. 检查原引入表所在位置之后有没有足够的空余,如果有,不用修改引入表位置;否则在引入表之后搜索足够大的全0空间;如果引入表之后没有足够大的全0空间,转到数据节中寻找,如果数据节中也没有足够大的空间,报错。
  5. 找到足够大空间后,复制原引入表到该位置,并构造一个新的引入项,引入项的第1个成员,指向INT表的第一个项,INT表中的该项指向Hint/Name Table结构,该结构在实验中为01 00 Func.dll;INT表的第2个项是4字节的全0。第4个成员指向DLL的名字,这里是Func.dll。第5个成员指向IAT表,IAT表中的第1项指向的是上面的那个Hint/Name Table结构,第2项是4字节全0。
  6. 修改可选头中的引入表位置,并将大小加上20字节,保存运行。

关键数据结构

节表

修改PE文件引入表实现加载DLL

IDT项

修改PE文件引入表实现加载DLL

验证结果

  编译生成的hack.exe,用32位的notepad.exe进行测试。文件位置如下图4-1所示,在同一个目录下,notepad.exe是目标可执行文件,hack.cpp是代码,hack.exe是完成实验用的可执行程序:
修改PE文件引入表实现加载DLL
  在运行hack.exe前,notepad.exe的二进制结构可以用PEview打开查看,其有三个节,分别为.text节、.data节和.rsrc节,IDT表现在在.text节中,有bound IDT表,如下图4-2所示:
修改PE文件引入表实现加载DLL
  执行命令 hack.exe notepad.exe,效果如下图4-3所示,显示找到了一个空闲区域,位置在文件偏移为0x7DF4处,这里的RVA为0x91F4,IAT指针在RVA为0x92FC处,执行成功。
修改PE文件引入表实现加载DLL
  双击打开notepad.exe,效果如下图4-4所示,的确弹出了计算器calc.exe,关闭notepad.exe后,calc.exe随之关闭,说明编写的Func.dll正确地被加载了。
修改PE文件引入表实现加载DLL
  用PEview打开被修改了的notepad.exe,可以看到结构和之前不太一样,如图4-5所示,IDT表被放到了.data节中。
修改PE文件引入表实现加载DLL
  hack.exe具体更改的地方有可选头中的第2个目录项,改变前后的notepad.exe该项内容如下图4-6(a)和图4-6(b)所示,从原来的9项加1个空项,大小为200,改为220,位置从RVA=0x7604,改到了RVA=0x91F4:
修改PE文件引入表实现加载DLL
  新增的IDT项如下图4-7所示,新增的DLL名字叫Func.dll,INT表的RVA为0x92D0,IAT表的RVA为0x92FC,实际上它们的位置是相邻的:
修改PE文件引入表实现加载DLL
  在文件中,它们都放在相邻的一块区域中,文件中的内容如图4-8所示。新加的IDT项是紫色框出的,它后面接着20字节的0填充表示IDT结尾。INT pointer指向的是红色框框的INT表,对于自己编写的DLL,只有一个导出函数FuncInDll,所以后面紧跟的是4字节0,INT表第1项指向青色的Hint/Name Table结构,里面放着前2字节0x0001,后面接着DLL中的函数名,表示自己写的Func.dll的导出函数序号为1的函数是FuncInDll。蓝色的IAT pointer指向0x92FC,蓝色框框是IAT表,也只有1项,在文件中,它指向的位置和INT表中第1项指向的位置是相同的。IDT项的第4个成员Name RVA指向的RVA=0x92FF放着的是DLL的名字Func.dll。
修改PE文件引入表实现加载DLL
  用PEview查看INT表和IAT表中的变化,分别为图4-9(a)和图4-9(b)所示,图中最左边的是.text节的RVA。之所以显示的是0x8AD0而不是0x92D0,是因为把IDT表放在了.data节中,而INT表和IAT表在.text节中。.text节中RVA比文件偏移大0xC00,.data节中RVA比文件偏移大0x1400,0x8AD0-0xC00+0x1400=0x92D0。
修改PE文件引入表实现加载DLL

参考网址

[1]PE文件格式. https://docs.microsoft.com/en-us/windows/desktop/debug/pe-format
[2]DLL入口点. https://docs.microsoft.com/en-us/windows/desktop/dlls/dynamic-link-library-entry-point-function
[3]DLL编写教程. http://www.blogjava.net/wxb_nudt/archive/2007/09/11/144371.html
[4] 流水账笔记:PE文件格式(导入表注入—手动). https://blog.csdn.net/liuhw4598/article/details/78245822

源代码地址

github:https://github.com/DXWEIE/software-security/tree/master/PE and dll

PS

  之所以这么改,没有改可选头中IAT表大小什么的,是因为可选头中的IAT这一项其实是可以删掉的,对程序运行没什么影响…