Drunkmars's Blog

PE

字数统计: 5.6k阅读时长: 22 min
2021/04/03 Share

c语言的部分基本完成,进到pe结构的学习,按照惯例开一帖记录一些知识点。

位运算

算术移位指令

SAL:算术左移

SAR:算数右移

逻辑移位指令

SHL:逻辑左移

SHR:逻辑右移

循环移位指令

ROL:循环左移

ROR:循环右移

带进位的循环移位指令

RCL:带进位循环左移

RCR:带进位循环右移

位移运算

1、与运算 &

2、或运算 |

3、非运算 ~

4、异或运算 ^

5、移位运算 << >>

有符号数、无符号数往左位移 反汇编为shl

有符号数、无符号数往右位移 反汇编为shr

内存分配,文件读写

宏定义说明

一、无参数的宏定义的一般形式为:# define 标识符 字符序列

如:# define TRUE 1

define PI 3.1415926

注意事项:

1、只作字符序列的替换工作,不作任何语法的检查

2、如果宏定义不当,错误要到预处理之后的编译阶段才能发现

二、带参数宏定义:#define 标识符(参数表)字符序列

define MAX(A,B) ((A) > (B)?(A):(B))

代码 x= MAX( p, q)将被替换成 y=((p) >(q)?(p):(q))

注意:

1、宏名标识符与左圆括号之间不允许有空白符,应紧接在一起

2、宏与函数的区别:函数分配额外的堆栈空间,而宏只是替换

3、为了避免出错,宏定义中给形参加上括号

4、末尾不需要分号

函数汇总

malloc()

返回一个指针 ,指向已分配大小的内存。如果请求失败,则返回 NULL

需要在头文件声明#include <string.h> #include <stdlib.h>

1、使用sizeof(类型)*n 来定义申请内存的大小

2、malloc返回类型为void*类型 需要强制转换

3、无论申请的内存有多小 一定要判断是否申请成功

4、申请完空间后要记得初始化

5、使用完一定要是否申请的空间

6、将指针的值设置为NULL

memset()

memset(void *str, int c, size_t n) 复制字符 c(一个无符号字符)到参数
str 所指向的字符串的前 n 个字符。

memcpy()

void *memcpy(void *str1, const void *str2, size_t n) 从存储区 str2
复制 n 个字节到存储区 str1

参数

  • str1 – 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。

  • str2 – 指向要复制的数据源,类型强制转换为 void* 指针。

  • n – 要被复制的字节数。

返回值

该函数返回一个指向目标存储区 str1 的指针

fopen()

打开文件

FILE *fopen(char *filename, char *mode);

filename为文件名(包括文件路径),mode为打开方式,它们都是字符串

fseek()

移动文件流的读写位置

int fseek(FILE * stream, long offset, int whence)

1、参数stream 为已打开的文件指针
2、参数offset 为根据参数whence 来移动读写位置的位移数。参数 whence
为下列其中一种:
SEEK_SET 从距文件开头offset 位移量为新的读写位置. SEEK_CUR 以目前的读写位置往后增加offset 个位移量.
SEEK_END 将读写位置指向文件尾后再增加offset 个位移量. 当whence 值为SEEK_CUR 或
SEEK_END 时, 参数offset 允许负值的出现

下列是较特别的使用方式:

  1. 欲将读写位置移动到文件开头时:fseek(FILE *stream, 0, SEEK_SET);
  2. 欲将读写位置移动到文件尾时:fseek(FILE *stream, 0, 0SEEK_END);

返回值:当调用成功时则返回0, 若有错误则返回-1, errno 会存放错误代码

附加说明:fseek()不像lseek()会返回读写位置,
因此必须使用ftell()来取得目前读写的位置

ftell()

获取文件读写指针的当前位置

long ftell(FILE * stream);

成功则返回当前的读写位置,失败返回 -1。

对于二进制文件,则返回从文件开头到结尾的字节数。

对于文本文件,返回的数值可能没有实际意义,但仍然可以用来保存当前的读写位置,供
fseek() 函数使用

fclose()

关闭一个流

int fclose( FILE *fp );

如果流成功关闭,fclose 返回 0,否则返回-1。(如果流为NULL,而且程序可以继续执行,fclose设定error number给EINVAL,并返回-1。)

fread()

size_t n = fread(void *ptr, size_t size, size_t nmemb, FILE *stream)

  • ptr – 这是指向带有最小尺寸 size*nmemb 字节的内存块的指针。

  • size – 这是要读取的每个元素的大小,以字节为单位。

  • nmemb – 这是元素的个数,每个元素的大小为 size 字节。

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流。

成功读取的元素总数会以 size_t 对象返回,size_t 对象是一个整型数据类型。如果总数与 nmemb 参数不同,则可能发生了一个错误或者到达了文件末尾。

ftell()

long int ftell(FILE *stream)

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。

该函数返回位置标识符的当前值。如果发生错误,则返回 -1L,全局变量 errno 被设置为一个正值。

LRSTR

在visual c++程序中经常在WinMain函数的参数表中见到”LPSTR”,其相当于char*

Windows有两种字符集:ANSI和Unicode。char这种类型是C语言标准的类型,它由几个字符组成,通常要看编译器,一般情况下是一个字节。Windows为了消除各种编译器之间的差别,重新定义了一些数据类型,其中就包括LPSTR。

LPSTR即为以零结尾的字符串指针,相当于char*

LPVOID

LPVOID是一个没有类型的指针,也就是说你可以将任意类型的指针赋值给LPVOID类型的变量(一般作为参数传递),然后在使用的时候再转换回来。可以将其理解为long型的指针,指向void型

打开文件读入内存

PE分节

1、节省硬盘空间(由编译器决定)

2、一个应用程序多开

word头

0x00 WORD e_magic为doc头 0x3c DWORD

e_lfanew为doc尾,且0x3c所存的数字即为标准pe头的地址

0x3c中所存的数字并不固定,从0x3c开始到pe头地址中间的值为垃圾数据

标准PE头

这里需要注意的是DOS头指向地址并不是直接指向了标准pe头,而是有4个字节作为pe标识,4个字节结束后才为pe头的开始

可选PE头

ImageBase决定文件的起始位置,加ImageBase一是为了空出内存保护指针,二是为了让模块对齐

OEP(AddressOfEntryPoint)即函数入口可能会存在代码区的任何一个位置,不一定在代码区的开始

如图,OEP为1588,内存基址为400000,所以函数入口为401588

拖入od

这里我把OEP改成1050,基址改为500000,对应的入口应该为501050

拖入od验证一下

SizeOfHeaders为所有头+节表按照文件对齐后的大小,即DOS+垃圾数据+PE标记+标准PE+可选PE+节表

这里的大小必须为1000的整数倍,例如我这里所有头+节表加起来的大小为1800,那么SizeOfHeaders就为2000

打印pe文件

联合体

1
2
3
4
union fuction{  
char x;
int y;
};

1、联合体的成员是共享内存空间的

2、联合体的内存空间大小是联合体成员中对内存空间大小要求最大的空间大小

3、联合体最多只有一个成员有效

此处的fuction为一种类型,即联合体类型

此处的fuction为一种变量

节表

RVA和FOA

VA: 全名virtualAddress 虚拟地址,就是内存中虚拟地址,例如 0x00401000

RVA: RVA就是相对虚拟偏移,就是偏移地址,例如
0x1000,虚拟地址0x00401000的RVA就是0x1000,RVA = 虚拟地址-ImageBase

FOA: 文件偏移,就是文件中所在的地址

内存转文件偏移总结:

1.计算RVA 公式: x - ImageBase == RVA

2.计算差值偏移 RVA - 节.VirtualAddress == 差值偏移

3.计算FOA 差值偏移 + 节.PointerToRawData == FOA

代码空白区添加代码(手动)

MessageBox指的是显示一个模态对话框,其中包含一个系统图标、一组按钮和一个简短的特定于应用程序消息,如状态或错误的信息。消息框中返回一个整数值,该值指示用户单击了哪个按钮。

看一下MessageBox的反汇编,这里有一个提栈的操作,对应的硬编码为6A 00

再看一下调用函数的地方

call对应的硬编码为E8jmp对应的硬编码为E9

call举例

E8 82 FF FF FF 这里的82 FF FF FF并不是直接要跳转的地址,而是用 要跳转的地址 - (E8地址 + 5)

0040100F - 0040108C(00401088 + 5 = 0040108C)= FFFF FF82,从右往左压栈即 82 FF FF FF

同理

E9 0C 00 00 00

00401020 - 00401014(0040100F + 5 = 00401014) = C ,从右往左压栈即 0C 00 00 00

往代码区加入数据需要注意的地方是SizeOfRawData - VirtualSize的值一定要大于所写入的数据字节

例如这里VirtualSize的值为440A2SizeOfRawData的值为45000,可存入数据的大小为F5E

image-20210603232712797

PointerToRawData1000SizeOfRawData45000,所以第一个节结束的地方为45000 + 1000 = 46000,第一个节数据结束的地方为450A2

image-20210603233129847

这里看一下ImageBase的值为00400000,所以可以得出减数为400000 + 450BD = 4450BD

image-20210603233324239

断一下MessageBoxA所在的位置,可以看到MessageBoxA的值为77D5058A

image-20210603232914999

image-20210603232934305

算出硬编码填入 即E8 CD B4 90 77

image-20210603233434723

找到原OEP000441EC加上ImageBase,即400000 + 441EC = 4441EC

image-20210603233520833

E9 2A F1 FF FF

image-20210603233747633

完整如下(6A为硬编码的push,压栈为数据进入做准备)

image-20210603233825200

修改OEP地址到messagebox的地址B0 50 04 00OEP的位置在pe标识 + 20字节(标准pe头大小) + 16字节

image-20210603234004956

成功弹框

image-20210603234028616

添加节

查看基本信息

sectionofhanders为8

添加节将sectionofheaders改为9

查看内存对齐跟文件对齐

文件对齐为200,内存对齐为1000

找到最后一个节所在的位置,复制第一个节的节表到最后一个节表后面方便修改数据

内存:D000+1000=E000

文件:3C00+E00=4A00

添加大小为1000的节表,保存即可

打印目录信息

静态链接库

生成.lib和.h文件

cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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;

}

.h:

1
2
3
4
5
6
7
int Plus(int x, int y);

int Sub(int x, int y);

int Mul(int x, int y);

int Div(int x, int y);

复制到新项目里面

方法一

声明文件

1
2
3
#include "test.h"

#pragma comment(lib, "static Lib.lib")

方法二

静态链接库的缺点:

会把二进制代码直接编译进exe,不会生成模块,导致逆向的时候分不清是自己写的还是导入的静态链接库

使用静态链接生成的可执行文件体积较大,造成浪费

我们常用的printf、memcpy、strcpy等就来自这种静态库

动态链接库

说明:

1、extern 表示这是个全局函数,可以供各个其他的函数调用;

2、”C” 按照C语言的方式进行编译、链接;

3、__declspec(dllexport)告诉编译器此函数为导出函数。

cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int __stdcall Plus(int x,int y)

{

return x+y;

}

int __stdcall Sub(int x,int y)

{

return x-y;

}

int __stdcall Mul(int x,int y)

{

return x*y;

}

int __stdcall Div(int x,int y)

{

return x/y;

}

.h:

1
2
3
4
5
6
7
extern "C" _declspec(dllexport) __stdcall int Plus (int x,int y);

extern "C" _declspec(dllexport) __stdcall int Sub (int x,int y);

extern "C" _declspec(dllexport) __stdcall int Mul (int x,int y);

extern "C" _declspec(dllexport) __stdcall int Div (int x,int y);

编译后复制出.dll和.lib

把.lib和.dll放入目录下

隐式调用

头文件加入

1
2
3
4
5
6
7
8
9
#pragma comment(lib,"dynamic.dll")

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);

编译发现报错

MSDN查看报错原因是链接不到

添加lib后可运行

代码如下

拖入od看下区别,动态链接库有dll模块,而静态链接库没有

看一手反汇编发现这里调用的一个函数

F10步入

跟进了一下,发现动态链接库是一个单独的模块,而静态链接库是编译进代码的

显式链接

(1)定义函数指针

(2)声明指针变量

(3)动态加载DLL到内存中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
特别说明:

Handle 是代表系统的内核对象,如文件句柄,线程句柄,进程句柄。

HMODULE 是代表应用程序载入的模块

HINSTANCE 在win32下与HMODULE是相同的东西 Win16 遗留

HWND 是窗口句柄

其实就是一个无符号整型,Windows之所以这样设计有2个目的:

1、可读性更好

2、避免在无意中进行运算

(4)获取函数地址

这个地方因为之前生成的.dll文件、函数指针使用的是_cdcall,即c语言默认内平栈,如果在dll里使用的是_stdcall,则应该写成如下形式:

1
2
3
4
5
6
7
myPlus = (lpPlus)GetProcAddress(hModule, "_Plus@8");

mySub = (lpSub)GetProcAddress(hModule, "_Sub@8");

myMul = (lpMul)GetProcAddress(hModule, "_Mul@8");

myDiv = (lpDiv)GetProcAddress(hModule, "_Div@8");

注:需要加上<Windows.h></Windows.h>头文件

隐藏函数

建立一个动态链接库,生成.h和.cpp

.h:

1
2
3
4
5
6
7
int Plus (int x,int y);

int Sub (int x,int y);

int Mul (int x,int y);

int Div (int x,int y);

.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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;

}

生成一个.def文件

1
2
3
4
5
6
7
8
9
10
11
.def

EXPORTS

Plus @12

Sub @15 NONAME

Mul @13

Div @16

将生成的dll拖进dependency walker,发现NONAME的那一行的函数名是看不见的

导出表

位于数据目录项的第一个结构

1
2
3
4
5
6
7
typedef struct _IMAGE_DATA_DIRECTORY {

DWORD VirtualAddress;

DWORD Size;

} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

VirtualAddress为导出表的RVA,Size为导出表的大小

上面的结构只是说明导出表在哪里、有多大,并不是真正的导出表

VirtualAddress中存储的是RVA,如果要在FileBuffer中定位,需要将RVA转换成FOA,即内存偏移->文件偏移

真正的导出表的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;

AddressOfFunctions:

该表中元素宽度为4个字节

该表中存储所有导出函数的地址

该表中个数由NumberOfFunctions决定

该表项中的值是RVA, 加上ImageBase才是函数真正的地址

定位:

IMAGE_EXPORT_DIRECTORY->AddressOfFunctions 中存储的是该表的RVA 需要先转换成FOA

AddressOfNames:

该表中元素宽度为4个字节

该表中存储所有以名字导出函数的名字的RVA

该表项中的值是RVA, 指向函数真正的名称

AddressOfNameOrdinals:

该表中元素宽度为2个字节

该表中存储的内容 + Base = 函数的导出序号

导出表寻址

名字寻址

AddressOfNames->AddressOfNameOrdinals->AddressOfFuctions

这里就引出了几个问题需要总结:

0x01
AddressOfFuctions和AddressOfNames大小不一定相等,而AddressOfFuctions也不一定比AddressOfNames大,因为可能存在AddressOfNames里的两个名字指向同一个地址的情况

0x02
AddressOfNameOrdinals与Base关联紧密,Base取的是函数序列表里面的最小值,而寻找AddressOfFuctions则是用AddressOfNameOrdinals里面的数字减去Base得到

0x03
NumberOfNames不准确,它的计算方法是用AddressOfNameOrdinals里面的最大值减去最小值后加1,但是这就出现了一个问题,可能表里面的值不是按序号排列的(即2.3.5.6),就会导致不准确的情况发生

0x04
接0x03的原理,因为NumberOfNames不准确,导致用名字寻址的方法取找AddressOfFuctions时就可能出现找不到的情况,所以AddressOfFuctions里面的值可以为0

序号寻址

如果用序号寻址,就不需要用到AddressOfNames和AddressOfNameOrdinals这两张表

重定位表

位于数据目录项的第6个结构

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

VirtualAddress为重定位表的RVA,Size为重定位表的大小

上面的结构只是说明重定位表在哪里、有多大,并不是真正的重定位表

VirtualAddress中存储的是RVA,如果要在FileBuffer中定位,需要将RVA转换成FOA,即内存偏移->文件偏移

真正的重定位表的结构如下:

1
2
3
4
5
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION ,* PIMAGE_BASE_RELOCATION;

一般情况下,EXE都是可以按照ImageBase的地址进行加载的.因为Exe拥有自己独立的4GB 的虚拟内存空间。但DLL 不是,DLL是有EXE使用它,才加载到相关EXE的进程空间的。为了提高搜索的速度,模块间地址也是要对齐的 模块地址对齐为10000H 也就是64K。

程序加载的过程如下:

image-20210603153037335

为什么要使用重定位表?

打开一个程序,观察一下全局变量的反汇编。

image-20210603153233911

1、也就是说,如果程序能够按照预定的ImageBase来加载的话,那么就不需要重定位表。这也是为什么exe很少有重定位表,而DLL大多都有重定位表的原因。

2、一旦某个模块没有按照ImageBase进行加载,那么所有类似上面中的地址就都需要修正,否则,引用的地址就是无效的。

3、一个EXE中,需要修正的地方会很多,用重定位表来记录哪些地方需要修正。

image-20210603153402317

导入表

位于数据目录项的第二个结构

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

VirtualAddress为导入表的RVA,Size为导入表的大小

上面的结构只是说明导入表哪里、有多大,并不是真正的导入表

VirtualAddress中存储的是RVA,如果要在FileBuffer中定位,需要将RVA转换成FOA,即内存偏移->文件偏移

真正的导入表的结构如下:

1
2
3
4
5
6
7
8
9
10
11
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //RVA 指向IMAGE_THUNK_DATA结构数组(即INT表)
};
DWORD TimeDateStamp; //时间戳
DWORD ForwarderChain;
DWORD Name; //RVA,指向dll名字,该名字已0结尾
DWORD FirstThunk; //RVA,指向IMAGE_THUNK_DATA结构数组(即IAT表)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

image-20210603154147994

image-20210603154156784

打印导入表的步骤:

1.定位导入表

image-20210603154326755

2.输出DLL名字

image-20210603154337676

3.遍历OriginalFirstThunk(即IAT表)

IAT的结构为

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {					
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

image-20210603154409185

4.遍历FirstThunk(即IAT表)

image-20210603154459737

绑定导入表

PE加载EXE相关的DLL时,首先会根据IMAGE_IMPORT_DESCRIPTOR结构中的TimeDateStamp来判断是否要重新计算IAT表中的地址

TimeDataStamp == 0 则未绑定

TimeDataStamp == -1 则已绑定

image-20210531160209856

一般的PE文件在加载前INT和IAT表中都是指向IMAGE_IMPORT_BY_NAME这张表的,也就是说INT表和IAT表在PE加载前表中所存的内容都是一样的,PE在加载后,IAT表里存的才是函数的地址,这种情况就属于没有绑定导入表的情况,即TimeDataStamp为0的情况。

我们知道,IAT表的本质是在PE文件加载后存放是函数地址,因为dll存在可能被其他系统dll占用空间的情况出现,所以一般都不会将函数地址直接写在IAT表里面。如果将要使用绑定导入表,最大的优点就是程序的启动速度会变快,因为省去了一个IAT重组的过程,一般windows系统自带的一些exe会选择将导入表绑定,即TimeDataStamp = fffffff,转换为十进制便是0

image-20210531160316079

image-20210531162321161

真正的绑定导入表位于目录的第13项,其中TimeDataStamp为真正的时间戳,OffsetModuleName为剩余dll的名字,NumberOfModuleForwarderRefs为依赖dll的数量。

其中要注意的是OffsetModuleName这个值有点特殊,它既不是foa,也不是rva,它的计算公式为第一个DESCRIPTOR的值加上所在结构体的OffsetMoudeleName得到。

如果NumberOfModuleForwarderRefs的值为2,则绑定导入表一共就有3个dll

image-20210531162529777

如果NumberOfModuleForwarderRefs的值不为0,绑定导入表下面还会跟一张依赖dll的绑定导入表结构,含义的话跟绑定导入表相同,Reserved值可以不用管。

image-20210531163613777

导入表注入

动态链接库入口方法:

image-20210601114348673

注入的种类:

1
2
3
4
5
6
7
1.注册表注入					2.导入表注入

3.特洛伊注入 4.远程线程注入

5.无dll注入 6.apc注入

7.windows挂钩注入dll 8.输入法注入

导入表注入的原理:

当Exe被加载时,系统会根据Exe导入表信息来加载需要用到的DLL,导入表注入的原理就是修改exe导入表,将自己的DLL添加到exe的导入表中,这样exe运行时可以将自己的DLL加载到exe的进程空间。

导入表注入的实现步骤:

第一步:

根据目录项的第二个得到导入表信息,VirtualAddress指向导入表结构,Size为导入表的总大小

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

第二步:

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

新增一个导入表所需的空间:

1
2
3
4
5
6
7
8
9
A:20字节,B:16字节,C:取决于dll名称的长度+1,D:取决于函数名的长度+1+2。



判断哪一个节的空白区 > Size(原导入表的大小) + 20 + A + B + C + D。



如果空间不够则可以将C/D存储在其他的空白区,即空白区 > Size + 0x20即可,如果仍然不够则需要扩大最后一个节或者新增一个节。

image-20210601115147473

第三步:将原导入表全部Copy到空白区

第四步:在新的导入表后面,追加一个导入表

第五步:追加8个字节的INT表 8个字节的IAT表

第六步:追加一个IMAGE_IMPORT_BY_NAME 结构,前2个字节是0 后面是函数名称字符串

第七步:将IMAGE_IMPORT_BY_NAME结构的RVA赋值给INT和IAT表中的第一项

第八步:分配空间存储DLL名称字符串 并将该字符串的RVA赋值给Name属性

第九步:修正IMAGE_DATA_DIRECTORY结构的VirtualAddressSize

CATALOG
  1. 1. 位运算
    1. 1.1. 算术移位指令
    2. 1.2. 逻辑移位指令
    3. 1.3. 循环移位指令
    4. 1.4. 带进位的循环移位指令
  2. 2. 位移运算
  3. 3. 内存分配,文件读写
  4. 4. 函数汇总
    1. 4.1. malloc()
    2. 4.2. memset()
    3. 4.3. memcpy()
    4. 4.4. fopen()
    5. 4.5. fseek()
    6. 4.6. ftell()
    7. 4.7. fclose()
    8. 4.8. fread()
    9. 4.9. ftell()
    10. 4.10. LRSTR
    11. 4.11. LPVOID
  5. 5. PE分节
  6. 6. word头
  7. 7. 标准PE头
  8. 8. 可选PE头
  9. 9. 联合体
  10. 10. 节表
  11. 11. RVA和FOA
  12. 12. 代码空白区添加代码(手动)
  13. 13. 添加节
  14. 14. 静态链接库
  15. 15. 动态链接库
    1. 15.1. 隐式调用
    2. 15.2. 显式链接
    3. 15.3. 隐藏函数
  16. 16. 导出表
  17. 17. 导出表寻址
    1. 17.1. 名字寻址
    2. 17.2. 序号寻址
  18. 18. 重定位表
  19. 19. 导入表
  20. 20. 绑定导入表
  21. 21. 导入表注入