滴水逆向三期实践17:导入表注入

在动态链接库一章提到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 中卸载时的弹窗代码。

那么导入表注入成功。

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值