在动态链接库一章提到DllMain,这里再回顾一次
当dll被加载进4GB空间时,会调用一次DllMain(入口方法)
当程序执行完了要把dll从4GB空间被卸载,也会调用一次DllMain
注入的本质就是想方设法把自己的dll扔到别人进程的4GB空间里
而前面导入表一章在最后提到,只有在PE文件内隐式调用(静态使用)时才会在调用者PE文件的导入表内出现该 dll 的名字和相关函数,若为显式调用(动态使用),则这个被调用的dll名和相关函数不会在调用者导入表内出现。
那么反过来也就是说,写入导入表中的 dll ,必定是被调用者隐式调用(静态使用)的。而事实是也确实如此,我们把毫不相关的 dll 的写入被调用者PE文件完整的导入表中,那打开这个PE文件时,这个 dll 会当做隐式调用(静态使用)直接被加载!(这是操作系统的规则,也是操作系统干的事)这个过程就叫导入表注入,下面是一些细节:
我们可以把 dll 通过导入表注入弄进其他 exe 的4GB空间。只要将一个 dll 相关的信息以一张新导入表写入.exe中,并且具有至少一个函数的 INT 表和 IAT 表,和对应的 IMPORT_BY_NAME 表,也就是整个导入表结构必须完整!操作系统就会在.exe启动时加载这个 dll,而当加载 dll,就会先执行一次 DllMain 中的代码,于是实现我们的注入目的。
若INT表或IAT表为空、或 IMPORT_BY_NAME 表为空、通过INT、IAT无法找对对应的函数名称或序号,操作系统都不会把 dll 加载进 4GB 空间。总之让整个导入表结构正确完整即可。而且只用写入一个函数就够了。
具体使用的 dll,如果有滴水三期课件的(网上都能找到),直接在课件里就有可用的 dll,实在没有可以按照下面海东给的代码自己做一个,顺便看了代码能知道这个 dll 是干啥的
dll 的 MyDll.h 头文件如下,具体为一个初始化函数,一个销毁函数,一个导出函数(前两个函数不导出):
void Init();
void Destroy();
extern "C" _declspec(dllexport) void ExportFunction();
这三个函数具体实现为 MyDll.cpp 如下: 三个函数都是弹窗功能
// MyDll.cpp: implementation of the MyDll class.
//
void Init()
{
MessageBox(0,"Init","Init",MB_OK);
}
void Destroy()
{
MessageBox(0,"Destroy","Destroy",MB_OK);
}
void ExportFunction()
{
MessageBox(0,"ExportFunction","ExportFunction",MB_OK);
}
dll 的 DllMain 所在 InjectDll.cpp 如下:
DllMain函数内通过switch DllMain的二参 来判断 dll 状态,即当dll 加载时执行 Init() 函数弹窗, dll 卸载时 执行Destroy() 函数弹窗
// InjectDll.cpp : Defines the entry point for the DLL application.
//
#include "stdafx.h"
#include "MyDll.h"
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Init();
break;
case DLL_PROCESS_DETACH:
Destroy();
break;
}
return TRUE;
}
可见如果 dll 注入成功,我们可以在打开 exe 后,先看到弹窗。和前面的新增节、扩大节注入代码差不多的效果。
但这个导入表注入过程需要我们写代码修改掉被注入PE文件的导入表实现。
再复述一遍原理:
当.exe被加载时,系统会根据.exe导入表信息来加载需要用到的 DLL ,导入表注入的原理就是修改.exe 导入表,将自己的 DLL 添加到.exe 的导入表中,这样.exe 运行时可以将自己的DLL加载到.exe 的进程空间.
导入表注入实现步骤如下:
目的无非就是多加一张导入表,导入表通常在其他的节,而原导入表后面大概率没有空位给我们新增一张表和对应的附表。所以这里直接选择在新增节中把整个旧导入表移动过去,并且在其后增加新导入表和对应附表,其实也可以在各节空白区中找合适的位置添加。
所以首先是移动导入表到新增节
其次就是增加新的导入表在紧接着移动后的导入表之后
也就是增加下面这个东西,在新增节的一众导入表之后:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
但新增这个东西后,有一堆内容需要填充,比如 OriginalFirstThunk 需要填充指向这个dll 的 INT表的 RVA,Name 填充指向 dll 名的 RVA,FirstThunk 填充指向该dll IAT表的RVA,也就是下图所示
要把这个新导入表的附表全部补充完整。按上图分为 ABCD 四个部分
注意 INT 和 IAT 表,只用填写一个内容,保证至少有一个导入函数,并且保证第二个内容为全0,即要让 INT和 IAT不为空,操作系统才会加载我们导入表中的 dll
那么这ABCD四部分的长度可以根据自己需要自行划分大小了, 从新节第0字节开始,我的划分为:
IMAGE_IMPORT_BY_NAME 结构,30字节
dll 名称,30字节
OriginalFirstThunk 指向的INT表,8 字节 (只用一个函数)
FirstThunk 指向的IAT表,8 字节
原导入表本身,SizeOfImport 字节
新增的导入表,20字节
IMAGE_IMPORT_BY_NAME 从2字节之后直接写入需要导入dll 的导出函数名的字符串(Hint的2字节为空)
dll 名直接填 dll 名字符串
INT表 内容 就是指向 IMAGE_IMPORT_BY_NAME 的 RVA,这里直接就填充新增节的 VirtualAddress 属性
下一个 IAT表同理,一样的内容
导入表的移动就不讲了,移动完后,再原有的导入表后加入新导入表。这里只举一个例子,新导入表的 OriginalFirstThunk 是RVA,内容看上面的 加粗部分,相对于新节RVA的偏移为30+30,于是新导入表OriginalFirstThunk填充的内容就是 新增节VirtualAddress +30+30
代码+详细注释如下:
#include "Currency.h"
#include "windows.h"
#include "stdio.h"
VOID h331() //移动导入表到新增节
{
char FilePath[] = "CrackHead.exe"; //CRACKME.EXE CrackHead.exe Dll1.dll R.DLL notepad.exe LoadDll.dll PETool.exe 打印dll用最后一个看
char CopyFilePath[] = "CrackHeadcopy.exe"; //CRACKMEcopy.EXE CrackHeadcopy.exe
LPVOID pFileBuffer = NULL; //会被函数改变的 函数输出之一
LPVOID* ppFileBuffer = &pFileBuffer; //传进函数的形参
LPVOID pNewFileBuffer = NULL; //用于新增节后的新 FileBuffer
LPVOID* ppNewFileBuffer = &pNewFileBuffer; //传进函数的形参
PIMAGE_DOS_HEADER pDos = 0; //头相关信息
PIMAGE_NT_HEADERS32 pNts = 0;
PIMAGE_DATA_DIRECTORY pDir = 0;
PIMAGE_IMPORT_DESCRIPTOR pImportTable = 0;
PIMAGE_SECTION_HEADER pSection = 0;
IMAGE_IMPORT_DESCRIPTOR NewImportTable = { 0 }; //新增的导入表
int NumOfImport = 0; //导入表个数
int SizeOfImport = 0; //导入表长度
DWORD SizeOfNewFileBuffer = 0; // NewFileBuffer 的大小
DWORD SizeOfFileBuffer = ReadPEFile(FilePath, ppFileBuffer);// FileBuffer 的大小
if (!SizeOfFileBuffer)
{
printf("文件读取失败\n");
return;
}
// 头查找
pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
pNts = (PIMAGE_NT_HEADERS32)((DWORD)pFileBuffer + pDos->e_lfanew);
pDir = &pNts->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; //导入表
pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pDir->VirtualAddress));
// 判断导入表最保险的是全部为0才结束,但其实不可能没有dll名,这里就只用INT表和 dll名是否都为0 作为结束条件
while(pImportTable->OriginalFirstThunk && pImportTable->Name)
{
NumOfImport++;
pImportTable++;
}
SizeOfImport = NumOfImport * sizeof(IMAGE_IMPORT_DESCRIPTOR);
// 新增节,内容为重新移动的导入表(因为原导入表后面大概率没有空位给我们新增)+ 一张我们新增的导入表和对应的附表
// 三参为新节大小,用 导入表个数 * 导入表大小 + 256 作为 新节大小,因为我们只新增一张导入表和对应附表,256足矣
SizeOfNewFileBuffer = AddNewSection(pFileBuffer, SizeOfFileBuffer, SizeOfImport + 256, ppNewFileBuffer);
free(pFileBuffer); // copy 完,旧的pFileBuffer就没用了
// 因为加上新节了,整个空间都变了,得以pNewFileBuffer重新头查找,重新找到导入表
pDos = (PIMAGE_DOS_HEADER)pNewFileBuffer;
pNts = (PIMAGE_NT_HEADERS32)((DWORD)pNewFileBuffer + pDos->e_lfanew);
pDir = &pNts->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; //导入表
pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pNewFileBuffer + RVA2FOA(pNewFileBuffer, pDir->VirtualAddress));
pSection = IMAGE_FIRST_SECTION(pNts); //指向第一个节表
for (int i = 1; i < pNts->FileHeader.NumberOfSections; i++) //注意这里 i 从1 开始,i<NumberOfSections
{
pSection++; //下一个节表
} //出循环后pSection指向最后一个节表,也即新的节表
// 下面开始构造新节内容,内容按顺序排列如下:
// IMAGE_IMPORT_BY_NAME 结构,30字节
// dll 名称,30字节
// OriginalFirstThunk 指向的INT表,8 字节 (只用一个函数)
// FirstThunk 指向的IAT表,8 字节
// 原导入表本身,SizeOfImport 字节
// 新增的导入表,20字节(sizeof(IMAGE_IMPORT_DESCRIPTOR)==20字节 )
// 于是往新节表中逐一填充以上内容,只在有数据的地方memcpy,无数据的地方AddNewSection函数已经帮填充0了,所以不需要管
// 首先填充 IMAGE_IMPORT_BY_NAME ,其中Hint首2字节为0不管,∴从+2的位置开始填充字符串,字符串为海东dll的导出函数名ExportFunction
memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 2), "ExportFunction", sizeof("ExportFunction"));
// 填充 dll名,目的地址从+30 开始,字符串为海东dll名,"InjectDll.dll"
memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 30), "InjectDll.dll", sizeof("InjectDll.dll"));
// 填充 INT表,从+30+30 开始,内容为IMAGE_IMPORT_BY_NAME 的 RVA,显然这个RVA值就是新节的内存起始位置,长度4
memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 30 + 30), &pSection->VirtualAddress, 4);
// IAT表,思想和上面一致,也是一样的 RVA 值
memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 30 + 30 + 8), &pSection->VirtualAddress, 4);
// 移动原导入表到新节 目的地址:新节表的绝对地址 源地址:导入表绝对地址 长度:全部导入表长度
memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 30 + 30 + 8 + 8), pImportTable, SizeOfImport);
// 移动完后 别忘了把数据目录中导入表的 VirtualAddr 给改了, 改成新节的RVA
pDir->VirtualAddress = pSection->VirtualAddress + 30 + 30 + 8 + 8;
pDir->Size += sizeof(IMAGE_IMPORT_DESCRIPTOR);
// 新增导入表,由于NewImportTable已经初始化全0,因此时间戳和ForwarderChain没必要赋0值了
//指向INT表的RVA,即新节的内存起始位置 + 30 + 30
NewImportTable.OriginalFirstThunk = pSection->VirtualAddress + 30 + 30;
// 指向IAT表的RVA
NewImportTable.FirstThunk = pSection->VirtualAddress + 30 + 30 + 8;
// Name 指向
NewImportTable.Name = pSection->VirtualAddress + 30;
// 最后别忘了拷贝整个 NewImportTable ,别忘了二参要取地址
memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 30 + 30 + 8 + 8 + SizeOfImport), &NewImportTable, sizeof(IMAGE_IMPORT_DESCRIPTOR));
MemeryToFile(pNewFileBuffer, SizeOfNewFileBuffer, CopyFilePath);
free(pNewFileBuffer);
}
注意这里有个坑,就是新增节的属性,即节表记录的Characteristics, 必须包含 C000 0040。如果没有这个属性,那运行exe 后会报错0xc0000005。(遇到这个坑后我直接把新增节函数的节属性改为e000 0060了,既可以执行代码,也可以放导入表及其附表)同理如果找节空白区的做法,也要保证添加的导入表和附表所在节包含这个属性,可以在所在节表直接或上 C000 0040。
这里用的海东课件里提供的 dll ,这个 dll 位置其实在这一课文件夹的 UseDll 文件夹中,InjectDll.dll 和 InjectDll.lib,(查了InjectDll文件夹里的 dll 不是上面 dll 源代码写出来的 dll,导出函数都不一样)没有课件的 也可以按上面的dll源代码自己造一个即可。把导入表注入的代码运行后,把新exe 和 InjectDll.dll 和 InjectDll.lib 放在同一个文件夹下。这时候打开 exe 会先弹窗,执行了 dll 中加载时的弹窗代码
确定后才是exe原版内容,关闭exe后也会另一个弹窗,执行了 dll 中卸载时的弹窗代码。
那么导入表注入成功。