深入解析pe结构(下)

发布者:SecIN
发布于:2023-08-14 15:42

数据目录表结构

在可选PE头的最后部分拥有16个数据目录表,其结构如下

typedef struct _IMAGE_DATA_DIRECTORY {          
    DWORD   VirtualAddress;             //内存偏移
    DWORD   Size;               //大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

这16个表是指PE文件中描述各种数据结构的数据目录表,具体如下:

导出表(Export Table)
导入表(Import Table)
资源表(Resource Table)
异常处理表(Exception Table)
安全相关表(Certificate Table)
重定位表(Base Relocation Table)
调试信息表(Debugging Information Table)
版权信息表(Architecture-Specific Data Table)
全局指针寄存器表(Global Pointer Register Table)
TLS表(Thread Local Storage Table)
负载配置表(Load Configuration Table)
网络地址转换表(Bound Import Table)
导出地址表(Import Address Table)
延迟加载导入表(Delay Import Table)
COM运行时描述表(COM Runtime Descriptor Table)
保留(Reserved)

其中导入表,导出表,重定位表,IAT表等等表比较重要

静态链接库与动态链接库

静态链接库的生成与使用
新建项目时选择静态链接库
cpp文件

int Plus(int x,int y)
{
return x+y;
}
int Sub(int x,int y)
{
return x-y;
}
int Mul(int x,int y)
{
return x*y;
}
int Div(int x,int y)
{
return x/y;
}

头文件

#pragma once
int Plus(int x,int y);

int Sub(int x,int y);

int Mul(int x,int y);

int Div(int x,int y);

新建项目将生成的lib文件和头文件复制到新项目中引用即可

wKg0C2Q6nLuAc5OWAACEUXPRNc402.png

但是这种静态链接库是直接加载到了程序中,没有实现模块化
动态链接库的生成与使用
头文件

extern "C" _declspec(dllexport) int Plus (int x,int y);  
extern "C" _declspec(dllexport) int Sub (int x,int y);
extern "C" _declspec(dllexport) int Mul (int x,int y);
extern "C" _declspec(dllexport) int Div (int x,int y);

cpp文件

int Plus(int x,int y)
{
return x+y;
}
int Sub(int x,int y)
{
return x-y;
}
int  Mul(int x,int y)
{
return x*y;
}
int  Div(int x,int y)
{
return x/y;
}

将生成的lib和dll文件放入到新建项目中进行导入

#pragma comment(lib,"dllmy.lib")

extern "C" __declspec(dllimport)  int Plus (int x,int y);  
extern "C" __declspec(dllimport) int Sub (int x,int y);
extern "C" __declspec(dllimport)  int Mul (int x,int y);
extern "C" __declspec(dllimport)  int Div (int x,int y);


int main(int argc, char* argv[])
{
int x=Plus(1,1);
printf("%d\n",x);
return 0;
}

已经在OD发现我们的自定义dll了

wKg0C2Q6nMeABXfcAABsZkGRNOc803.png

定位导入表

这16个数据目录表所描述的各种数据结构,实际上是存储在文件的不同区段(节表)中的,根据上面的数据目录表结构VirtualAddress是内存地址,那如何定位在文件中的地址呢
根据软件查看导入表的RVA为2D0C0

wKg0C2Q6nM2AUrsWAAB1WupF6hA870.png

通过查看节表发现第二个区块的VirtualAddress+VirtualSize是大于导入表的RVA,因此导入表是包含在第二个区块内的

wKg0C2Q6nNOAJBBZAACTiz5Z874377.png

因此导入表的RVA-区块的RVA得出导入表相对于区块的位置,然后加上区块的PointerToRawData也就是导入表在区块中的实际位置

wKg0C2Q6nNiAEjvEAABdxzfCRa4642.png

导出表

导出表结构:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;// 未使用
    DWORD   TimeDateStamp;// 时间戳
    WORD    MajorVersion;// 未使用
    WORD    MinorVersion;// 未使用
    DWORD   Name;// 指向该导出表文件名字符串
    DWORD   Base;// 导出函数起始序号
    DWORD   NumberOfFunctions;// 所有导出函数的个数
    DWORD   NumberOfNames;// 以函数名字导出的函数个数
    DWORD   AddressOfFunctions;     // 导出函数地址表RVA
    DWORD   AddressOfNames;         // 导出函数名称表RVA
    DWORD   AddressOfNameOrdinals;  // 导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

pe结构的导出方式一共分为两种,一种是名字导出,一种是序号导出
名字导出:
如图为AddressOfFunctions,AddressOfNameOrdinals,AddressOfNames的表结构

wKg0C2Q6nOGAYevjAADY5oWN46I948.png

其中AddressOfFunctions存的是函数的地址,AddressOfNameOrdinals为序号表,AddressOfNames中存的是函数名称的地址
假如我们要导出test函数,那么计算机首先会向AddressOfNames的地址进行遍历,如果遍历到了并且发现在下标为2遍历到的,那么就会去寻找AddressOfNameOrdinals下标为2存储的序号,发现为4,然后去AddressOfFunctions寻找下标为4的地址,这个地址就是函数的真正地址
序号导出:
根据序号导出和AddressOfNameOrdinals没有关系,如果给出的值为5,那么真正的地址位置就是5-base,之后去AddressOfFunctions寻找下标即可
注意:导出时要进行RVA转FOA的转换
编写程序打印所有的导出表信息:

#include <iostream>
#include <windows.h>

LPVOID ReadPE(
IN LPCSTR lpszName
){
FILE* file = nullptr;
fopen_s(&file, lpszName, "rb");
if (!file)
{
printf("打开文件失败!\n");
return nullptr;
}

fseek(file, 0, SEEK_END);
size_t size = ftell(file);
fseek(file, 0, SEEK_SET);

LPVOID fileBuff = malloc(size);
if (!fileBuff)
{
printf("申请内存空间失败!\n");
fclose(file);
return nullptr;
}
fread_s(fileBuff, size, 1, size, file);

WORD mz = * ((PWORD)fileBuff);
if (mz != 0x5a4d)
{
printf("该文件不是pe可执行程序!\n");
fclose(file);
free(fileBuff);
return nullptr;
}

return fileBuff;
}

void AnlyzePE(
IN  LPVOID pe,
OUT PIMAGE_DOS_HEADER& dos,
OUT PIMAGE_FILE_HEADER& file,
OUT PIMAGE_OPTIONAL_HEADER32& optional,
OUT PIMAGE_SECTION_HEADER*& section
) {
dos = (PIMAGE_DOS_HEADER)pe;
file = (PIMAGE_FILE_HEADER)((PCHAR)pe + dos->e_lfanew + 4);
optional = (PIMAGE_OPTIONAL_HEADER32)((PCHAR)pe + dos->e_lfanew + 4 + 20);
section = (PIMAGE_SECTION_HEADER*)malloc(file->NumberOfSections * sizeof(IMAGE_SECTION_HEADER));
if (section != nullptr)
{
for (int i = 0; i < file->NumberOfSections; i++)
{
*(section + i) = (PIMAGE_SECTION_HEADER)((PCHAR)pe + dos->e_lfanew + 4 + 20 + file->SizeOfOptionalHeader + (i * sizeof(IMAGE_SECTION_HEADER)));
}
}
}

DWORD RvaToFoa(
IN LPVOID pe, 
IN UINT_PTR rva
){
PIMAGE_DOS_HEADER dos = nullptr;
PIMAGE_FILE_HEADER file = nullptr;
PIMAGE_OPTIONAL_HEADER32 optional = nullptr;
PIMAGE_SECTION_HEADER* section = nullptr;
AnlyzePE(pe, dos, file, optional, section);
DWORD foa = -1;

for (int i = 0; i < file->NumberOfSections; i++)
{
UINT_PTR begin = (*section + i)->VirtualAddress;
UINT_PTR end = (*section + i)->VirtualAddress + (*section + i)->SizeOfRawData;
if (begin <= rva && rva <= end)
{
foa = rva - begin + (*section + i)->PointerToRawData;
break;
}
}
free(section);
return foa;
}

void PrintExport(
IN LPVOID fileBuff
){
PIMAGE_DOS_HEADER dos = nullptr;
PIMAGE_FILE_HEADER file = nullptr;
PIMAGE_OPTIONAL_HEADER32 optional = nullptr;
PIMAGE_SECTION_HEADER* section = nullptr;
AnlyzePE(fileBuff, dos, file, optional, section);

DWORD offset = RvaToFoa(fileBuff, optional->DataDirectory[0].VirtualAddress);
PIMAGE_EXPORT_DIRECTORY exportTable = (PIMAGE_EXPORT_DIRECTORY)((PCHAR)fileBuff + offset);
printf(">>>> 导出表 <<<<\n");
printf("Characteristics          =%x\n", exportTable->Characteristics);
printf("TimeDateStamp            =%x\n", exportTable->TimeDateStamp);
printf("MajorVersion             =%x\n", exportTable->MajorVersion);
printf("MinorVersion             =%x\n", exportTable->MinorVersion);
printf("Name                     =%x\n", exportTable->Name);
printf("Base                     =%x\n", exportTable->Base);
printf("NumberOfFunctions        =%x\n", exportTable->NumberOfFunctions);
printf("NumberOfNames            =%x\n", exportTable->NumberOfNames);
printf("AddressOfFunctions       =%x\n", exportTable->AddressOfFunctions);
printf("AddressOfNames           =%x\n", exportTable->AddressOfNames);
printf("AddressOfNameOrdinals    =%x\n", exportTable->AddressOfNameOrdinals);

DWORD(*function)[1];
function = (DWORD(*)[1])((PCHAR)fileBuff + RvaToFoa(fileBuff, exportTable->AddressOfFunctions));
printf(">>>> Functions <<<<\n");
for (int i = 0; i < exportTable->NumberOfFunctions; i++) 
{
printf("%d = %x\n", i, *(*(function)+i));
}

WORD(*ordinal)[1];
ordinal = (WORD(*)[1])((PCHAR)fileBuff + RvaToFoa(fileBuff, exportTable->AddressOfNameOrdinals));
printf(">>>> Ordinals <<<<\n");
for (int i = 0; i < exportTable->NumberOfFunctions; i++) 
{
printf("%d = %x\n", i, *(*(ordinal)+i));
}

DWORD(*name)[1];
name = (DWORD(*)[1])((PCHAR)fileBuff + RvaToFoa(fileBuff, exportTable->AddressOfNames));
printf(">>>> Names <<<<\n");
for (int i = 0; i < exportTable->NumberOfFunctions; i++) 
{
printf("%d = %s\n", i, (PCHAR)fileBuff + RvaToFoa(fileBuff, *(*(name)+i)));
}
free(section);
return;
}

bool M_strcmp(
IN char* s1,
IN char* s2
) {
int length = strlen(s1);
if (length != strlen(s2)) 
{
return false;
}
else 
{
for (int i = 0; i < length; i++) 
{
if (s1[i] != s2[i]) 
{
return false;
}
}
}
return true;
}


LPVOID GetFunctionAddrByName(
IN LPVOID pe, 
IN LPCSTR funcName
){
PIMAGE_DOS_HEADER dos = nullptr;
PIMAGE_FILE_HEADER file = nullptr;
PIMAGE_OPTIONAL_HEADER32 optional = nullptr;
PIMAGE_SECTION_HEADER* section = nullptr;
AnlyzePE(pe, dos, file, optional, section);

DWORD offset = RvaToFoa(pe, optional->DataDirectory[0].VirtualAddress);
PIMAGE_EXPORT_DIRECTORY exportTable = (PIMAGE_EXPORT_DIRECTORY)((PCHAR)pe + offset);

DWORD(*function)[1];
function = (DWORD(*)[1])((PCHAR)pe + RvaToFoa(pe, exportTable->AddressOfFunctions));
WORD(*ordinal)[1];
ordinal = (WORD(*)[1])((PCHAR)pe + RvaToFoa(pe, exportTable->AddressOfNameOrdinals));
DWORD(*name)[1];
name = (DWORD(*)[1])((PCHAR)pe + RvaToFoa(pe, exportTable->AddressOfNames));
for (int i = 0; i < exportTable->NumberOfFunctions; i++) 
{
LPCSTR tempName = (PCHAR)pe + RvaToFoa(pe, *(*(name)+i));
if (M_strcmp((char*)tempName, (char*)funcName)) 
{
DWORD funcIndex = *(*(ordinal)+i);
free(section);
return (LPVOID)*(*(function)+funcIndex);
}
}
free(section);
return nullptr;
}

LPVOID GetFunctionAddrByOrdinal(
IN LPVOID pe, 
IN DWORD exportNumber
){
PIMAGE_DOS_HEADER dos = nullptr;
PIMAGE_FILE_HEADER file = nullptr;
PIMAGE_OPTIONAL_HEADER32 optional = nullptr;
PIMAGE_SECTION_HEADER* section = nullptr;
AnlyzePE(pe, dos, file, optional, section);

DWORD offset = RvaToFoa(pe, optional->DataDirectory[0].VirtualAddress);
PIMAGE_EXPORT_DIRECTORY exportTable = (PIMAGE_EXPORT_DIRECTORY)((PCHAR)pe + offset);

DWORD(*function)[1];
function = (DWORD(*)[1])((PCHAR)pe + RvaToFoa(pe, exportTable->AddressOfFunctions));
free(section);
return (LPVOID)*(*(function)+(exportNumber - exportTable->Base));
}

int main()
{
LPVOID fileBuff = ReadPE(R"(D:\Data\Project\VisualStudio2022\WaterDroplet\Debug\ds.dll)");
if (fileBuff) {
PrintExport(fileBuff);
LPVOID addAddress = GetFunctionAddrByName(fileBuff, "add");
LPVOID maxAddress = GetFunctionAddrByOrdinal(fileBuff, 5);
printf("null\n");
}

free(fileBuff);
system("pause");
return 0;
}

wKg0C2Q6nPCAAkpAAEKtHofS0156.png

重定位表

定义:记录需要绝对地址修正的表,大多数绝对地址如果imagebase变化的话就无法使用,需要修正程序所调用的那些绝对地址。

  • 修正方法:需要重定位的地址 + 偏移(当前基址 - PE的基址)
  • 开了随机基址的程序才需要重定位,而DLL通常都有重定位表,因为不一定能够加载到DLL指定的ImageBase上。

OS如何判定是否重定位?

  • 先查看随机地址标志,标志开启,地址重定位
  • 再查看数据目录项 5 是否位NULL,不为NULL,基址重定位。

我们现在都是玩固定基址的PE,随机基址涉及到要修代码,如果有重定位信息,就可以在内存中随便申请一块内存,把代码放进去跑。
我们知道随机基址需要重定位表来修代码, 那么是修什么代码呢。
实际上我们修的是使用绝对地址的代码,例如API的调用,通过IAT调用,这里就是使用的绝对地址,当模块基址改变时,原VA地址并没有保存API函数地址,所以就需要修正到正确的位置去获取API地址。

wKg0C2Q6nPmAEOPNAAB6kRp1Dq4475.png

  • 怎么保存需要重定位的数据地址呢?
    • 按分页存,每个分页中需要重定位的地址(基于分页值的偏移)
    • 优点:存储空间小,一个分页有多个地址需要重定位时,只需要存一个分页即可。2字节,和全是2 字节的偏移值。
00001000 0000 0024 0078
需要修的位置有 :1000 + 24 = 1024,1078;

重定位表结构
重定位表描述待修复的值所在的地方,这个值是一个RVA。数据目录处的Size字段有用,是重定位表的总大小。
重定位表位于数据目录第3项。

wKg0C2Q6nQOAHqZHAAGMsUm3Tpw336.png

重定位表结构(一项)

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;//重定位数据所在页的RVA
    DWORD   SizeOfBlock;//当前页中重定位数据块的总大小
//  WORD    TypeOffset[1];//重定位项数组
} IMAGE_BASE_RELOCATION;
  • VirtualAddress
    • 这个虚拟地址是一组重定位数据的开始RVA地址,只有重定位项的有效数据加上这个值才是重定位数据真正的RVA地址
  • SizeOfBlock
    • 它是当前重定位块的总大小,因为VirtualAddress和SizeOfBlock都是4字节的,所以(SizeOfBlock - 8)才是该块所有重定位项的大小,(SizeOfBlock - 8) / 2就是该块所有重定位项的数目。
  • TypeOffset[1]
    • 高4位表示了重定位数据的类型(0x00没有任何作用仅仅用作数据填充,为了4字节对齐。0x03表示这个数据是重定位数据,需要修正。0x0A出现在64位程序中,也是需要修正的地址),
    • 低12位就是重定位数据相对于VirtualAddress的偏移,也就是上面所说的有效数据。之所以是12位,是因为12位的大小足够表示该块中的所有地址(每一个数据块表示一个页中的所有重定位数据,一个页的大小位0x1000)。
    • 重定位项在该结构中没有体现出来,他的位置是紧挨着这个结构的,可以把他当作一个数组,宽度为2字节。表示该地址处有一个地址需要进行重定位
    • 每一个重定位项分为两个部分:高4位和低12位

修正方法:被重定位处原来的地址 + 偏移(当前基址 - PE的基址)
:(VA - ImageBase) + NewImageBase
重定位表只是记录了修哪里,以及怎么修,并不会记录修多少,因为既然是随机基址,那么基址的值就不固定,必须每次软件起来才能确定。

wKg0C2Q6nSyAGpi3AACrYUVrOZU168.png

wKg0C2Q6nTSAZbm5AACPKKRafOE203.png

注意:.reloc: 的节一般用于存储重定位表,但是不作为定位重定位表的依据,应使用数据目录定位。
重定位表应用之 LoadDll

  • 介绍:Dll 加载器 ,单独装载一个独立的DLL
  • 返回值:返回模块的实例句柄
  • 思考:
    1. Dll的代码装载到哪个内存?申请一块内存,进行装载。
    2. 处理重定位数据,遍历重定位表

步骤:

  1. 申请Dll 装载所需的内存空间
  2. 拷贝PE头
  3. 根据节表拷贝节,对齐空隙使用 00 填充
  4. 拷贝节
  5. 处理导入表
  6. 处理重定位表
  7. 清理资源
  8. 返回模块句柄
  • 7:处理重定位
  • 获取分页内偏移数组地址 和 需要重定位的偏移个数
  • 判断 TypeOffset 是否为填充 00。
    • 是:跳过不处理,进行下一个
    • 否:需要重定位,修正该地址处的重定位数据
  1. GetprocAddress 从模块链里面找模块。TEB里面
  2. DLL加载成功后,不能摸出DLL的MZ 和 PE 标志么?
    1. 不能。有的API调用的时候会检测这两个标志。
    2. 应用:API模拟,反dump

新的注入方式 LoadDll

  1. 不调用LoadLibary,使用远程注入的方式 DLL的内容注入到别人的进程里,然后调用DllMian
  2. 远程线程调用Dllmain。

导入表

IAT与INT

PE(Portable Executable)结构是Windows操作系统下可执行文件的格式之一,导入表(Import Table)是PE结构中重要的一个部分。导入表记录了程序需要引用的外部库函数以及这些函数在内存中的位置,这些外部库函数通常由其他DLL文件提供。

wKg0C2Q6nT6Afya0AACyKsaAOf0645.png

假设我们汇编调用的是call CreateFile其本质是调用call CreateFile的函数地址那我么你在哪里存储这个函数地址呢?其实pe文件在导入DLL后会遍历DLL中的函数并且将函数的地址存放到另一个内存地址中,因此我们调用函数都是间接调用

wKg0C2Q6nUSACsfMAACovVCQWCQ030.png

我们用OD加载飞鸽传书 ALT+E跳转到可执行模块

wKg0C2Q6nUmAL9PCAAGvfIUj4w424.png

选中feige.exe Ctrl+N查看调用的DLL函数

wKg0C2Q6nVCAMA86AACiRSIC7Vo340.png

选择一个函数按回车键查看调用过这个函数的位置

wKg0C2Q6nVWAJXHAAA1JvjuvA003.png

双击进入,查看代码都是间接调用

wKg0C2Q6nVyAY1nEAABrP8tlzkc389.png

复制这个地址在数据窗口Ctrl+G跟随,之后右键选择 长型》地址 就可以发现简介调用了这个函数

wKg0C2Q6nWGAatDyAABxoHLqXms313.png

由于有两处函数调用我们查看另一处发现也是相同的地址

wKg0C2Q6nWaAEmLAACEIlwFKNQ514.png

而存储函数地址的表叫做IAT(导入地址表)

wKg0C2Q6nW2AKwZAADMg4pxAZE451.png

导入表结构

IMAGE_IMPORT_DESCRIPTOR 是Windows操作系统中PE文件格式的结构体之一,用于描述动态链接库(DLL)的导入信息。
这个结构体在Windows SDK中的头文件winnt.h中被定义,可以在C/C++程序中进行引用。以下是该结构体的定义:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME; 
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

该结构体定义了一个DLL的导入信息,包括以下几项:

  • OriginalFirstThunk: 指向导入函数名称表(Import Name Table,INT)的指针,这个表中存储了需要导入DLL中的函数名字。
  • TimeDateStamp:DLL文件的创建时间或重新绑定时间戳。
  • ForwarderChain:指向一个链表,记录该DLL导入的其他DLL列表(也就是链表的下一个DLL)。
  • Name:DLL的名称。
  • FirstThunk:指向导入地址表(Import Address Table,IAT)的指针,这个表中存储了需要导入DLL中的函数的地址。

在PE文件解析或修改中,可以通过 IMAGE_IMPORT_DESCRIPTOR 结构体读取或修改DLL的导入信息。

我们看一下OriginalFirstThunk指向的结构体

wKg0C2Q6nXWASMDcAABpvnOMrl4871.png

DLL函数的导入方式有两种,分别是序号导入和函数名称导入,如果DWORD的最高位是1是序号导入,低2个字节就是序号值
最高位是0是RVA,而RVA指向PIMAGE_IMPORT_BY_NAME,其中Name[1]指向函数的名称

wKg0C2Q6nXuAUM8tAAA27pH0uc8750.png

导入表遍历

导入表遍历顺序 = 导入表加载的顺序

  1. 检查Name 和FirstThunk ,如果任一为NULL,则停止遍历
  2. 取FirstThunk 的项(数组中的元素),如果为NULL, 就取OriginalFirstThunk  对应的项,如果为NULL,则遍历下一项
  3. 判断项的最高位,如果为1,则取低WORD为序号,如果为0,则作为RVA 取出IMAGE_IMPORT_BY_NAME 中的函数名
  4. 循环遍历下一项
while(Name != NULL && FirstThunK != NULL)
{
    IMAGE_DATA_THUNK* pTmpThunk = OriginalFirstThunk;
    if(OriginalFirstThunk == NULL)
    {
        pTmpThunk = FirstThunk;
    }
    while(*pTmpThunk != NULL)
    {
        if(*pTmpThunk & 0x80000000)
        {
            WORD dwNumber = *pTmpThunk & 0xffff; //低字为序号
        }
        else
        {
IMAGE_IMPORT_BY_NAME* pName = *pTmpThunk;//获取导入函数名称的RVA
        }
        pTmpThunk++;
    }
}

隐藏导入函数

MessageBoxA当做常量还是能找到

#include <Windows.h>
#include <stdio.h>
typedef int (__stdcall *pMessageBoxA)(
_In_opt_ HWND hWnd,
_In_opt_ LPCSTR lpText,
_In_opt_ LPCSTR lpCaption,
_In_ UINT uType);
int main() {
char arr[] = { 'M','e','s','s','a','g','e','B','o','x','A',0 };
HMODULE hdll = LoadLibraryA("user32.dll");
pMessageBoxA  pfun0 = (pMessageBoxA)GetProcAddress(hdll, "MessageBoxA");
pfun0(0, "hhhhh", "ccccc", 0);
}

wKg0C2Q6nYmANAizAAE6ctFkkE683.png

但是如果换成数组就找不到了

wKg0C2Q6nYADx0VAABN8Drbup4281.png

绑定导入表

何为绑定导入
一般情况下,在程序加载前IAT表和INT表中的内容相同,都是程序引用的dll中的函数的函数名或序号;
加载完成后IAT表中将替换为函数的真正地址;
但在加载前IAT表中直接写绝对地址是可以实现的;
加载前在IAT表中保存绝对地址的优点:

  • 启动程序快;
  • 在启动程序时需要:申请4gb内存空间、贴exe、贴dll、将IAT表修复为地址等等;
  • 如果直接用绝对地址,则省去了修复IAT表的操作;

缺点:

  • dll重定位时,如果dll没能占据自身ImageBase处的地址,则需要修复绝对地址;
  • dll被修改时,dll被修改,IAT表中对应的函数地址可能被改,需要修复函数地址;

如何判断绑定导入
在导入表中结构中有个属性:TimeDateStamp;
该属性表示时间戳;
如果值为0则表示当前的dll的函数没有被绑定,在程序加载时会调用系统函数获取函数地址;
如果值为-1则表示当前的dll的函数已经绑定,而且绑定的时间存在另外一张表里;那张表就是绑定导入表;
绑定导入表的结构

typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
    DWORD   TimeDateStamp;
    WORD    OffsetModuleName;
    WORD    NumberOfModuleForwarderRefs;    // Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR,  *PIMAGE_BOUND_IMPORT_DESCRIPTOR;

打印绑定导入表

#include "PE.h"


bool StructIsNull(
IN LPVOID obj,
IN size_t size
) {
for (DWORD i = 0; i < size; i++)
{
if (*((PCHAR)obj + i) != 0)
{
return false;
}
}
return true;
}

void PrintBoundImportTable(
IN LPVOID pe
) {
PIMAGE_DOS_HEADER pDosHeader = nullptr;
PIMAGE_FILE_HEADER pFileHeader = nullptr;
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = nullptr;
PIMAGE_SECTION_HEADER* pSectionHeaderArr = nullptr;
AnlyzePE(pe, pDosHeader, pFileHeader, pOptionalHeader, pSectionHeaderArr);

PIMAGE_BOUND_IMPORT_DESCRIPTOR pBoundImportTable = (PIMAGE_BOUND_IMPORT_DESCRIPTOR)((PCHAR)pe + RvaToFoa(pe, pOptionalHeader->DataDirectory[12].VirtualAddress));
PIMAGE_BOUND_IMPORT_DESCRIPTOR pFirstBoundImportTable = pBoundImportTable;
while (!StructIsNull(pBoundImportTable, sizeof(*pBoundImportTable)))
{
printf(">>>>>>>>>> 主DLL <<<<<<<<<<\n");
printf("主DLL绑定时间戳 = %d\n", pBoundImportTable->TimeDateStamp);
printf("主DLL名称 = %s\n", (PCHAR)pFirstBoundImportTable + pBoundImportTable->OffsetModuleName);
printf("主DLL依赖数量 = %d\n", pBoundImportTable->NumberOfModuleForwarderRefs);
printf(">>>>> 副DLL <<<<<\n");
for (DWORD i = 0; i < pBoundImportTable->NumberOfModuleForwarderRefs; i++)
{
PIMAGE_BOUND_FORWARDER_REF rely = (PIMAGE_BOUND_FORWARDER_REF)((PCHAR)pBoundImportTable + (sizeof(IMAGE_BOUND_FORWARDER_REF) * i));
printf("依赖DLL绑定时间戳 = %d\n", rely->TimeDateStamp);
printf("依赖DLL名称 = %s\n", (PCHAR)pFirstBoundImportTable + rely->OffsetModuleName);
}
pBoundImportTable = (PIMAGE_BOUND_IMPORT_DESCRIPTOR)((PCHAR)pBoundImportTable + (sizeof(IMAGE_BOUND_FORWARDER_REF) * pBoundImportTable->NumberOfModuleForwarderRefs));
}
free(pSectionHeaderArr);
}

int main()
{
LPVOID pe = ReadPE("xxx");
if (pe)
{
PrintBoundImportTable(pe);
free(pe);
}
return 0;
}

pe结构是学习二进制以及免杀必备的知识,不能只看理论要多练多敲才能掌握真谛


声明:该文观点仅代表作者本人,转载请注明来自看雪