软件安全实验T3

Last updated on October 21, 2023 pm

《软件安全》课程T3实验任务详细步骤

1.修改函数入口点地址,使得hello25.exe直接弹出第二个消息框

知识铺垫:

(1)EXE装载流程:

  • 首先操作系统给exe文件分配4GB的虚拟空间,其中高2G用于操作系统的函数,比如uer32.dll,低2G用于用户的使用。分配完空间后,把exe本身的代码加载到由ImageBase指定的地址,通常来说是0x400000
  • 其次,操作系统往4GB空间中加载各种DLL文件,由于DLL文件往往会有冲突,不能按照预计ImageBase装载,因此需要使用DLL本身提供的重定位表修复自己的地址。(DLL文件与EXE文件一样,都是PE结构)
  • ——执行其他的操作——,后续再探究
  • 然后操作系统跳转到由AddressOfEntryPoint+ImageBase的地址进行执行代码

(2)RVA,FOA是什么:

RVA

在解释此名词之前,需要明白一件事情,即PE文件实际上有两种存在状态,一种是存储在硬盘中,也就是16进制编辑器打开的样子,还有一种则是运行时,操作系统会分配一个独立的虚拟空间给它,通常这个虚拟空间大小是4GB,然后把这个文件装载到这个4GB中,装载到哪,则是先由ImageBase字段决定,一旦确定好从哪个地方装载,就相当于确定了一个基址,而这个RVA就是相对这个基址的偏移。

比如这个文件从内存中0x10000000开始装载,如果RVA是0x32,那么在内存中它的地址就是0x10000032

FOA

既然PE文件有两种格式,那么偏移肯定也有两种,刚刚的提及的RVA是在虚拟内存中的,那么还有一种就是在文件中的偏移,即FOA - File Offset Address,也就是在16进制文本编辑器里的偏移

RVA和FOA的概念会贯穿整个PE文件的学习,一定要理解清楚

现在就可以开始修改OEP了。

利用PE相关工具查询PE文件结构

可以看到其ImageBase0x400000AddressOfEntryPoint0x1000,那么其在加载后应该从0x401000开始执行,将其载入OD或者x32dbg。(我使用的是x32dbg)

发现其果然是从0x401000处开始执行,与预期相符。

那么只需要修改OEP即可,问题是修改去哪呢?

这里需要知道MessageBox()的的使用,以及传参方式:

传参方式:在32位程序中,函数的参数通常都是通过栈来传递的,并且遵循相关的调用约定,比如先把哪个函数压栈,由谁来清栈

而在这里,是先把左边的参数压栈。比如传递参数MessageBox(a,b,c,d),则先push a,再push b等。

参数的含义:Messagebox(a,b,c,d)中,参数a是所属句柄,也就是这个窗口属于谁,在这里默认为0或者保持一致也行,Win没有对此有严格的检查;参数b是标题;参数c是文本内容;参数d是窗口类型,比如是正确错误框,还是警告框,默认为0即可

相关信息可以查询MSDN]

因此只需要把OEP改到0x401016即可,也就是把AddressOfEntryPoint改为1016

2.打开hello25.exe,对其中给定任意RVA地址,将其转换成文件偏移,并在文件中找到对应数据

知识铺垫:

相信你已经知道RVA和FOA是什么,那么继续探究Windows是怎样把PE文件加载进内存的吧!

之前提及Windows会分配4GB空间,然后把PE自己的代码装载进低2G,并且是从ImageBase处开始优先装载,那么是不是就是从ImageBase处开始一个字节一个字节装载呢?

是但不完全是,在此之前,我们需要对PE文件结构有一个宏观的认识

这里我直接贴一张大图,建议分屏放大浏览观看

只看最上面一行,可以发现PE文件大致分为如下几部分

  • IMGAE_DOS_HEADR - DOS头
  • DOS STUB - DOS填充
  • IMAGE_NT_HEADR - NT头
  • SECTION_TABLE - 节表
  • SECTIONS - 节/节区
  • OTHERS

每个部分又或多或少引申出许多小表,其实这些小表就是一个一个结构体,存储在PE文件结构中。可以这么说:PE文件结构就是由许许多多的结构体组成,每个结构体里的每个成员都有相应的含义,比如指示哪个部分的开始,哪个部分的结束,在指来指去的过程中,整个文件运行所需要的信息也就明确了。

而操作系统会把IMGAE_DOS_HEADR DOS-STUB IMAGE_NT_HEADR SECTION_TABLE 直接从ImageBase处开始copy,在OPTIONAL_HEADER里有一个字段SizeOfHeaders指明了这些大小(文件对齐后的大小),因此操作系统可以直接copy这么大的数据

可以简单比较一下

可以发现是一模一样的,可以自行比对后面的0x200字节

这些节表的数据copy完了,那节区的数据呢?

我们需要知道一个东西,也就是一个节表会对应一个节区,节表里面会有节区的相关信息,比如起始位置、大小、执行权限等,这里面与RVA、FOA相关的就是

这里直接给出完整的说明吧

typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//8个字节 IMAGE_SIZEOF_SHORT_NAME是宏 等于8 一般是ascii码 节区名字
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc; // 注意是联合体,其含义是没有对齐的在内存中真实尺寸 该值可以不准确
DWORD VirtualAddress; // 节区在内存中的偏移地址,是RVA
DWORD SizeOfRawData; // 节区在文件中对齐后的尺寸
DWORD PointerToRawData; // 节区在文件中偏移
DWORD PointerToRelocations; // 在obj文件中使用 对exe无意义
DWORD PointerToLinenumbers; // 行号表的位置 调试时使用
WORD NumberOfRelocations; // 在obj中使用 对exe无意义
WORD NumberOfLinenumbers; // 行号表的数量 调试时使用
DWORD Characteristics; // 节区属性 可写可读可执行等 通过或的形式来添加属性
//0x00000020 含可执行代码 0x20000000 该块可执行 0x40000000 可读 0x80000000 可写
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

操作系统就根据其中的PointerToRawData在文件中找到相应位置,copy到VirtualAddress处的地址,大小为SizeOfRawData

遍历完节表,也就copy完所有的数据。

终于,现在可以好好讲讲RVA如何与FOA互转了

先讲FOA到RVA,即给定一个FOA,如何确定RVA?

对于所有的头部数据,即IMGAE_DOS_HEADR DOS-STUB IMAGE_NT_HEADR SECTION_TABLE,操作系统是直接复制,它们的RVA和FOA是相同的。(!要记住,RVA和FOA都是偏移地址,不是绝对地址!)

而对于节区的数据,则先得确定属于哪个节区,这个当然好办,一个节区一个节区的看呗,看FOA是否大于节区的起始地址PointerToRawData,同时又小于这个节区的结束地址PointerToRawData+SizeOfRawData,确定完节区后,就可以知道该节区的在内存的地址即VirtualAddress,计算一下节区偏移FOA-PointerToRawData,再加上节区再内存中起始地址,即可得到RVA

再讲RVA到FOA,即定一个RVA,如何确定其FOA?

同样,对于所有的头部数据,即IMGAE_DOS_HEADR DOS-STUB IMAGE_NT_HEADR SECTION_TABLE,操作系统是直接复制,它们的RVA和FOA是相同的。

然后也是确定RVA属于哪一个节区,按照上面的流程走一遍即可(确定节区 => 计算节区偏移 => 计算文件偏移 => RVA - VirtualAddress + PointerToRawData )

这里给出我的代码

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include<stdio.h>
#include<malloc.h>
#include<windows.h>


DWORD Size(FILE* fp){
DWORD size = 0;
fseek(fp,0,SEEK_END);
size = ftell(fp);
fseek(fp,0,SEEK_SET);
return size;
}


DWORD FOA_TO_RVA(DWORD FOA,char* Buffer){
PIMAGE_DOS_HEADER DosHeader = NULL;
PIMAGE_NT_HEADERS NTHeader = NULL;
PIMAGE_FILE_HEADER FileHeader= NULL;
PIMAGE_OPTIONAL_HEADER OptionalHeader = NULL;
PIMAGE_SECTION_HEADER SectionHeader = NULL;

DosHeader = (PIMAGE_DOS_HEADER)Buffer;
FileHeader = (PIMAGE_FILE_HEADER)(Buffer + DosHeader->e_lfanew + 4);
OptionalHeader = (PIMAGE_OPTIONAL_HEADER)(Buffer + DosHeader->e_lfanew + 4 +IMAGE_SIZEOF_FILE_HEADER);
SectionHeader = (PIMAGE_SECTION_HEADER)(Buffer + DosHeader->e_lfanew + 4 +IMAGE_SIZEOF_FILE_HEADER + FileHeader->SizeOfOptionalHeader);

if (FOA < OptionalHeader->SizeOfHeaders)
{
return FOA;
}


for(int i =0;i<FileHeader->NumberOfSections-1;i++){
if(FOA >= SectionHeader->PointerToRawData&& FOA < (SectionHeader +1)->PointerToRawData){
DWORD offset = FOA - SectionHeader->PointerToRawData;
return SectionHeader->VirtualAddress + offset;
}
else{
SectionHeader += 1;
}
}
//最后一个节区
DWORD offset = FOA - SectionHeader->PointerToRawData;
return SectionHeader->VirtualAddress + offset;
}

DWORD RVA_TO_FOA(DWORD RVA,char* Buffer){
PIMAGE_DOS_HEADER DosHeader = NULL;
PIMAGE_NT_HEADERS32 NTHeader = NULL;
PIMAGE_FILE_HEADER FileHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 OptionalHeader = NULL;
PIMAGE_SECTION_HEADER SectionHeader = NULL;

DosHeader = (PIMAGE_DOS_HEADER)Buffer;


FileHeader = (PIMAGE_FILE_HEADER)(Buffer + DosHeader->e_lfanew + 4);
OptionalHeader = (PIMAGE_OPTIONAL_HEADER32)(Buffer + DosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER);
SectionHeader = (PIMAGE_SECTION_HEADER)(Buffer + DosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER + FileHeader->SizeOfOptionalHeader);

if (RVA < OptionalHeader->SizeOfHeaders)
{
return RVA;
}

for(int i=0;i<FileHeader->NumberOfSections-1;i++){
if(RVA >= SectionHeader->VirtualAddress && RVA < (SectionHeader+1)->VirtualAddress){
DWORD offset = RVA - SectionHeader->VirtualAddress;
return SectionHeader->PointerToRawData + offset;
}
else{
SectionHeader +=1;
}
}
//最后一个节区
DWORD offset = RVA - SectionHeader->VirtualAddress;
return SectionHeader->PointerToRawData + offset;
}

int main(){
char* buffer;

FILE* fp1 = NULL;
errno_t err_1 = fopen_s(&fp1, "YOU_PATH_TO_EXE", "rb");

DWORD size = Size(fp1);
buffer = (char*)malloc(size);
fread(buffer,size,1,fp1);

//Choose one to execute
//printf("%X",RVA_TO_FOA(RVA,buffer));
//printf("%X",FOA_TO_RVA(FOA,buffer));
return 0;
}

3.修改hello25.exe的代码段,使其弹出第三个消息框(包含自己学号和姓名)

这里我的思路也是修改OEP,然后jmp回原来的代码处

先在原文件处查找一些空白区(没有权限,就给他加权限就是了),填入学号姓名

然后编写弹窗函数,也就是4个push和1个call,可以看看原来的是怎么调用的,

这里push操作码是0x68,call的操作码是0xE8jmp的操作码是0xE9,这两个后面都跟一个4字节地址,先把操作码填入

然后我们可以先修改OEP,检查是否正确设置。

修改OEP也就是修改AddressOfEntryPoint,也就是我们需要计算0x8B0的RVA然后填入。

这里就利用刚刚贴出的代码直接计算了(后续不再贴出计算截图,无非就是换个数据,换个函数而已)

可见是RVA是30B0,那么就把AddressOfEntryPoint改为30B0

然后再拖入x32dbg,查看

发现已经修改成功,只差计算那几个地址了!

在这里我们只需要关注第二第三个push的值和call以及jmp的值,另外两个push不是很重要。

这里要注意,不管我们是push还是进行跳转,它跟的操作数都是一个地址,而这个地址是都是在内存中的地址,并不是FOA。

因此我们需要计算一下我们填入的两个字符串的RVA是多少,在这里我的学号FOA是0x890,它对应的RVA是0x3090,姓名的FOA是0x8A0,其对应的RVA是0x30A0,那么就把RVA加上ImageBase就得到在内存中的值,也就是0x403090 0x4030A0

NOTE:修改的时候注意小端序

再拖入x32dbg查看

发现可以成功找到字符串,现在就是要计算跳转地址

在win32程序里面跳转地址是这样计算的:要跳转的地址-下一条指令的地址

比方说有一个指令call a,这条指令的下一条指令地址是0x1010,a的地址是0x1008,那么其硬编码就是E8 8,即E8 0x1010 - 0x1008

所以我们要找到MessageBoxA的地址,因为我们想调用这个函数

这里贴出如何寻找的截图

可以看到MessageBoxA的地址是0x767F9050(不同电脑会不一样,所以你跟我的不一样也正常),call的下一条指令地址是0x4030C9(但这个应该一样),那么操作码就应该这样计算0x767F9D50 - 0x4030C9

同理jmp的操作码也是这样计算,我们需要跳回原来的OEP处即0x401000

分别得到call的操作码763F5F87jmp的操作码FFFFDF32,现在写入文件(也可以直接在x32dbg里面patch)

现在就可以执行我们的弹窗了

4.修改hello25.exe,删除文件中引入函数节的MessageBoxA字符串,之后自行进行相应调整,使得hello25.exe弹框功能恢复正常。

首先我们需要知道引入函数节是个啥东西,这个字符串是咋用的

其实引入函数节就是导入表,所谓的导入表就是就相当于告诉别人自己要用什么。它可以通过OptionalHeader数据目录项的第二项找到

同理这个VirtualAddress也是一个RVA,需要转化为FOA后才能在文件中找到真正的导入表结构

1
2
3
4
5
6
7
8
9
10
11
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // 指向 导入名称表INT(ImportNameTable)的RVA
} DUMMYUNIONNAME;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // 指向 DLL名称的地址 RVA
DWORD FirstThunk; // 指向 导入地址表IAT(ImportAddressTable)的RVA
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

其中OrginalFirstThunkFirstThunk各自再指向两张表,分别是INT表和IAT表,name则是要用到的dll的名字。在进一步解析IAT表和INT表之前,先做个实验。

随便使用调试器打开一个PE文件(我这里就没有用老师给出的文件了,但过程都是一模一样的,大家依葫芦画瓢就好啦),在里面找一找一些使用了系统调用的函数

比如这里,实际上这条指令就是mov rax ds:[4083b0](大家可以捣鼓捣鼓怎么在x32里面看实际指令),在内存中转去4083B0这个地址,看看是什么

也就是说4083B0这个地址存放的是7FFFE86C3190,借助于调试器很方便的知道这个地址就是MessageBoxA的函数地址,这条指令也就等价于把MessageBoxa的地址给rax,然后再调用此函数。

而我们在文件中去看看4083B0这个地址存放的是什么呢(这里要减去ImageBase得到83B0,同时RVA转换FOA,才能在文件中找到,此地址在文件中对应的地址为37B0

37B0这个地址存放的则是一个86F6,很显然与运行时存放的值不一样。继续深究86F6又是什么,首先这是一个RVA,将其转换为FOA后,得到3AF6

当在文件中转去3AF6时,就会意料中地发现居然就是MessageBoxA这个函数名字,虽然前面还带了E9 01两字节不知道什么东西。

也就是说在内存中4083B0存放的是MessageBoxA的地址,而文件中则对应存放的是MessageBoxA的名字的RVA。换句话说存放的东西不一样,但两者之间肯定有一定的联系。实际上,3AF6处就是IAT表中的一项。

目前可以明确的是IAT表运行前运行后确实会发生变化,这其实就跟PE文件加载过程有关了。

在PE文件加载之前,INT表和IAT表指向同一块地方,换句话说INT表和IAT表在运行前是完全一样的但是独立的两张表,在多数情况下它们处于不同的地址处,但是存放相同的值。在PE文件加载之后,操作系统就会根据INT表去找那些函数地址,比如MesaageBoxA的地址,然后填入IAT表中。

现在再来深入研究一下PE文件的加载

  • 首先操作系统给exe文件分配4GB的虚拟空间,然后把exe本身的代码加载到ImageBase处,通常来说exe是首先加载的,往往都能预计加载到ImageBase处,因此不需要使用重定位表修改。
  • 其次,操作系统往4GB空间中加载各种DLL文件,由于DLL文件往往会有冲突,不能按照预计ImageBase装载,因此需要使用DLL本身提供的重定位表修复自己的地址
  • 待所有DLL都加载并修复完后,操作系统根据exe的INT表去找函数地址,找到一个地址就填入IAT表中,遍历完INT表也就遍历完IAT表,由此完成对IAT表的修改(这个找函数地址的过程,其实就是调用GetPrcoAddress这个函数)
  • 此时INT表存储了函数名称或者序号,IAT表存储了对应的函数地址,想要使用DLL一个函数也就简单了。

也就是说文件加载前后INT表不变,文件加载前IAT表和INT表相同,文件加载后IAT表发生变化

那么INT表是如何存储函数名称或者序号呢?

先来看看INT表表项结构

1
2
3
4
5
6
7
8
typedef struct _IMAGE_THUNK_DATA32 {						
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal; //序号
PIMAGE_IMPORT_BY_NAME AddressOfData;//指向IMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;

注意这是个联合体,也就是说这个结构宽度是4个字节,虽然看起来这个联合体存放了很多东西。但又最简单的方法判断,如果这4个字节最高位是1,那么剩下31位就代表函数序号;如果最高位不为1,那么剩下31为就代表一个RVA指向另一个结构IMAGE_IMPORT_BY_NAME,即if(INT & 0x80000000 == 0x80000000) :序号导出 else RVA = IMAGE_IMPORT_BY_NAME

这个IMAGE_IMPORT_BY_NAME结构也简单

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {						
WORD Hint; //可能为空,编译器决定 如果不为空 是函数在导出表中的索引
BYTE Name[1]; //函数名称,以0结尾
} IMAGE_IMPORT_BY_NAME

第二个成员就指向名字的第一个字节。

IAT表和INT表在加载前是完全一样的,其结构也是一样的,它们的成员都会指向相同的序号或者IMAGE_IMPORT_BY_NAME结构

至此你应该明白INT以及IAT是什么,他们是如何互相联系的,那么就来找一找hello25.exe的导入表吧!

同样利用PE相关工具可以很方便的找到

0x614

来划分一下结构吧

结合前面给出的结构体,五个DWIRD划分一组就可以了,相信你应该能看懂吧 =)

我把最后的全零结构也划出来,是想告诉你操作系统就是根据同等大小的全零结构来判断一个表是不是结束,这个在很多地方都会见到

我们感兴趣的是第二个导入表,也就是绿线里面的,(因为MessageBoxA跟这个相关

那就对着结构体来解析吧!

1
2
3
4
5
DWORD   OriginalFirstThunk INT表:0x2058 RVA -> 0x658 FOA
DWORD TimeDateStamp; 时间戳:0x00
DWORD ForwarderChain; 不重要的玩意儿:0x00
DWORD Name; 指向DLL的名称:0x209A RVA
DWORD FirstThunk; IAT表:0x2008 RVA -> 0x608 FOA

看看INT表和IAT表

由RVA0x208C转FOA0x68c查看

也就是划绿线部分啦(思考为什么到0停止)

这个划绿线部分就是_IMAGE_IMPORT_BY_NAME 的结构,第一个Word是由编译器决定,第二个则是函数名字

终于我们找到了函数名字的用武之地

现在我们只需要把这个绿色结构体换个地方,然后把原来INT表、IAT表修改一下就好啦

思考:不修改IAT表,照样能够正常运行,为什么?

5.编写一个弹出消息框的msg.dll文件,仅修改hello25.exe的引入函数节部分数据,使得hello25.exe运行时自动加载msg.dll,并弹出消息框。

(看此题之前,建议先看第六题,你会对导入表有一个更清晰的认识)

首先,你需要会编写一个dll文件,并使用,但这个并不是重点,可以参考这篇文章

这是我的DLL代码

这题的原理是这样的:操作系统会根据导入表来加载DLL,而在DLL加载的时候就可以执行回调函数(与C++里面的魔法方法类似,就是自动执行那种),那么我们就要想办法让操作系统加载我们的DLL。

还记不记得之前提及导入表以一个全零的结构作为结束,如果我们给这个结构改一改,我们人为的在导入表加一份我们自己DLL

换句话说,我们需要人为在导入表里添加一项DLL,来骗过操作系统。但是会面临一个问题,即原来的导入表不够位置,因为操作系统是根据导入表最后的全0结构来判断是否导入表是否结束,如果说原来的导入表后面只有20个零(一个导入表结构的大小),那么显然是不能在此基础上添加导入表的,因为这样就会加载进错误的导入表,从而导致程序崩溃。

我这里采取移动导入表的形式(还有一种方式把原来的ExitProcess等字符串全部后移,这样就有空间增加我们的自己的导入表了,但是也得修复,这个方法我放在第六题讲)

移动最好的方式是新开一个节(如何新增一个节也是一个值得讨论的话题,参考下这篇文章),然后根据目录项开始逐表逐表copy,你可能有一个疑问——为什么要逐表,而不直接使用目录项的size成员呢?原因有二,第一这个size是不准确的,在许多编译器中都会将此值置0,你也可以自己把它设置成0,程序一样可以正常运行;第二这个size可能不只包含导入表,还可能包含了INT表,IAT表等所有的大小,但是我们只想copy单纯的导入表。

因此需要逐表逐表copy,然后用20字节来做新表的大小,再用20字节做结束的标志

新增INT表、IAT表

仅仅创建新表是不够的,因此此时表结构全为0,即使设置了DLL的名字。因为操作系统会看这个表的INT表、IAT表,如果这两张表为0,就说明没有用到这个DLL,就不会把他加载程序4GB空间。因此需要新增INT表、IAT表,我们只需要新增一项就可以使得操作系统加载DLL了。

同理INT表、IAT表也需要全零0结构作为结束,而这两张表表项大小是4字节,因此每一张表都需要8字节来完成新增操作。此后就是设置这两张表的表项,使其指向PIMAGE_IMPORT_BY_NAME结构(当然使用序号的方式也是可以的)

简单来说,就是重新构这几张表的指向关系,也就是如图的所有箭头

只要重新建立这些箭头指向,也就代表一个新的导入表成功建立

这里我也直接给出我的代码吧

代码有注释,相信你能琢磨明白的!

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
#include<stdio.h>
#include<malloc.h>
#include<windows.h>

#define ADD_SIZE 0x3000

DWORD Size(FILE* fp){
DWORD size = 0;
fseek(fp,0,SEEK_END);
size = ftell(fp);
fseek(fp,0,SEEK_SET);
return size;
}

DWORD RVA_TO_FOA(DWORD RVA,char* Buffer){
PIMAGE_DOS_HEADER DosHeader = NULL;
PIMAGE_NT_HEADERS NTHeader = NULL;
PIMAGE_FILE_HEADER FileHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 OptionalHeader = NULL;
PIMAGE_SECTION_HEADER SectionHeader = NULL;

DosHeader = (PIMAGE_DOS_HEADER)Buffer;
FileHeader = (PIMAGE_FILE_HEADER)(Buffer + DosHeader->e_lfanew + 4);
OptionalHeader = (PIMAGE_OPTIONAL_HEADER32)(Buffer + DosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER);
SectionHeader = (PIMAGE_SECTION_HEADER)(Buffer + DosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER + FileHeader->SizeOfOptionalHeader);

if (RVA < OptionalHeader->SizeOfHeaders)
{
return RVA;
}

for(int i=0;i<FileHeader->NumberOfSections-1;i++){
if(RVA >= SectionHeader->VirtualAddress && RVA < (SectionHeader+1)->VirtualAddress){
DWORD offset = RVA - SectionHeader->VirtualAddress;
return SectionHeader->PointerToRawData + offset;
}
else{
SectionHeader +=1;
}
}

//最后一个节区
DWORD offset = RVA - SectionHeader->VirtualAddress;
return SectionHeader->PointerToRawData + offset;
}

DWORD FOA_TO_RVA(DWORD FOA,char* Buffer){
PIMAGE_DOS_HEADER DosHeader = NULL;
PIMAGE_NT_HEADERS NTHeader = NULL;
PIMAGE_FILE_HEADER FileHeader= NULL;
PIMAGE_OPTIONAL_HEADER OptionalHeader = NULL;
PIMAGE_SECTION_HEADER SectionHeader = NULL;

DosHeader = (PIMAGE_DOS_HEADER)Buffer;
FileHeader = (PIMAGE_FILE_HEADER)(Buffer + DosHeader->e_lfanew + 4);
OptionalHeader = (PIMAGE_OPTIONAL_HEADER)(Buffer + DosHeader->e_lfanew + 4 +IMAGE_SIZEOF_FILE_HEADER);
SectionHeader = (PIMAGE_SECTION_HEADER)(Buffer + DosHeader->e_lfanew + 4 +IMAGE_SIZEOF_FILE_HEADER + FileHeader->SizeOfOptionalHeader);

if (FOA < OptionalHeader->SizeOfHeaders)
{
return FOA;
}


for(int i =0;i<FileHeader->NumberOfSections-1;i++){
if(FOA >= SectionHeader->PointerToRawData&& FOA < (SectionHeader +1)->PointerToRawData){

DWORD offset = FOA - SectionHeader->PointerToRawData;
return SectionHeader->VirtualAddress + offset;
}
else{
SectionHeader += 1;
}
}
//最后一个节区
DWORD offset = FOA - SectionHeader->PointerToRawData;
return SectionHeader->VirtualAddress + offset;
}

char* Section_add(FILE* fp,char* buffer){
DWORD size = Size(fp);
DWORD new_size = size + ADD_SIZE ;

char* NewBuffer = (char*)malloc(new_size);
if (!NewBuffer)
{
printf("failed to creat buffer!\n");
return 0;
}

PIMAGE_DOS_HEADER DosHeader = NULL;
PIMAGE_NT_HEADERS NTHeader = NULL;
PIMAGE_FILE_HEADER FILEHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 OptionalHeader = NULL;
PIMAGE_SECTION_HEADER SectionHeader = NULL;

memset(NewBuffer,0,new_size);
memcpy(NewBuffer,buffer,size);

//初始化PE头部信息
DosHeader = (PIMAGE_DOS_HEADER)NewBuffer;
NTHeader = (PIMAGE_NT_HEADERS)(NewBuffer + DosHeader->e_lfanew);
FILEHeader = (PIMAGE_FILE_HEADER)(NewBuffer+DosHeader->e_lfanew + 4);
OptionalHeader = (PIMAGE_OPTIONAL_HEADER32)(NewBuffer + DosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER);
SectionHeader = (PIMAGE_SECTION_HEADER)(NewBuffer + DosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER + FILEHeader->SizeOfOptionalHeader);

//最后一个节表地址
PIMAGE_SECTION_HEADER LastSection = (PIMAGE_SECTION_HEADER)(NewBuffer + DosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER + FILEHeader->SizeOfOptionalHeader+IMAGE_SIZEOF_SECTION_HEADER*(FILEHeader->NumberOfSections-1));
//新节表地址
PIMAGE_SECTION_HEADER NewSection = (PIMAGE_SECTION_HEADER)(NewBuffer + DosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER + FILEHeader->SizeOfOptionalHeader + IMAGE_SIZEOF_SECTION_HEADER * (FILEHeader->NumberOfSections));

//判断剩余空间
DWORD Remained_size = (DWORD)(OptionalHeader->SizeOfHeaders - DosHeader->e_lfanew - 4
- IMAGE_SIZEOF_FILE_HEADER - IMAGE_SIZEOF_SECTION_HEADER * FILEHeader->NumberOfSections);
if (Remained_size < 2 * IMAGE_SIZEOF_SECTION_HEADER)
{
printf("do not have enough space!\n");
free(NewBuffer);
return 0;
}

//===================修改信息====================
//其他头部需要修改的信息
FILEHeader->NumberOfSections += 1;
OptionalHeader->SizeOfImage += ADD_SIZE;

//填入数据
memcpy(NewSection->Name,".NewSec",8);
NewSection->Misc.VirtualSize = ADD_SIZE;
NewSection->SizeOfRawData = ADD_SIZE;
NewSection->PointerToRawData = LastSection->PointerToRawData + LastSection->SizeOfRawData;

DWORD add_size = LastSection->Misc.VirtualSize > LastSection->SizeOfRawData ? LastSection->Misc.VirtualSize : LastSection->SizeOfRawData;

NewSection->VirtualAddress = LastSection->VirtualAddress + add_size;

//找到开始的地方
if (NewSection->VirtualAddress % OptionalHeader->SectionAlignment)
{
NewSection->VirtualAddress = NewSection->VirtualAddress / OptionalHeader->SectionAlignment * OptionalHeader->SectionAlignment +OptionalHeader->SectionAlignment;
}

NewSection->Characteristics = 0xC0000040;

return NewBuffer;
}

void DLL_INJECT(char* Buffer){
PIMAGE_DOS_HEADER DosHeader = NULL;
PIMAGE_NT_HEADERS NTHeader = NULL;
PIMAGE_FILE_HEADER FILEHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 OptionalHeader = NULL;
PIMAGE_SECTION_HEADER SectionHeader = NULL;

//初始化PE头部信息
DosHeader = (PIMAGE_DOS_HEADER)Buffer;
NTHeader = (PIMAGE_NT_HEADERS)(Buffer + DosHeader->e_lfanew);
FILEHeader = (PIMAGE_FILE_HEADER)(Buffer+DosHeader->e_lfanew + 4);
OptionalHeader = (PIMAGE_OPTIONAL_HEADER32)(Buffer + DosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER);
SectionHeader = (PIMAGE_SECTION_HEADER)(Buffer + DosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER + FILEHeader->SizeOfOptionalHeader);

//定位到新节区开始
PIMAGE_SECTION_HEADER NewSec = SectionHeader + FILEHeader->NumberOfSections - 1;
char* pNewSec = Buffer + NewSec->PointerToRawData;
//定位到导入表
DWORD Import_Rva = OptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
DWORD Import_Foa = RVA_TO_FOA(Import_Rva,Buffer);

PIMAGE_IMPORT_DESCRIPTOR pImport_Table =(PIMAGE_IMPORT_DESCRIPTOR)(Buffer + Import_Foa);
// printf("%d",OptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size);

//复制原导入表
int index = 0;
while(pImport_Table->FirstThunk){
memcpy(pNewSec + 20*index,(pImport_Table),20);
pImport_Table++;
index++;
printf("name:%s\n",(RVA_TO_FOA(pImport_Table->Name,Buffer)+Buffer));
}

//指向新复制导入表结束
PIMAGE_IMPORT_DESCRIPTOR New_Import = PIMAGE_IMPORT_DESCRIPTOR(pNewSec + index * 20);

//新增INT表
DWORD* New_INT = (DWORD*)(pNewSec + index * 20+ 40);
//新增IAT表
DWORD* New_IAT =(DWORD*)(pNewSec + index * 20 + 48);

//新增IMAGE_IMPORT_BY_NAME结构
PIMAGE_IMPORT_BY_NAME New_NAME = PIMAGE_IMPORT_BY_NAME(pNewSec + index * 20 + 56);

memcpy(New_NAME->Name,"THE_FUNC_YOUR_DLL_EXPORT",THE_LEGTH_OF_THE_FUNC);

*New_INT = FOA_TO_RVA((char*)New_NAME - Buffer,Buffer);
//printf("%x",*(New_INT));
*New_IAT = FOA_TO_RVA((char*)New_NAME - Buffer,Buffer);
//printf("%x",*(New_INT + 2));

New_Import->OriginalFirstThunk = FOA_TO_RVA(((char*)New_INT - Buffer),Buffer);
New_Import->FirstThunk = FOA_TO_RVA((char*)New_IAT - Buffer,Buffer);

char* New_DLL_NAME = pNewSec + index * 20 + 100;
memcpy(New_DLL_NAME,"YOU_DLL_NAME",YOUR_DLL_NAME_LENGTH_PLUS_1);

New_Import->Name = FOA_TO_RVA(New_DLL_NAME - Buffer,Buffer);


OptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress = NewSec->VirtualAddress;
}

int main(){
char* buffer;
char* new_buffer;

FILE* fp1 = NULL;
FILE* fp2 = NULL;

errno_t err_1 = fopen_s(&fp1, "YOUR_PATH_TO_EXE", "rb");
errno_t err_2 = fopen_s(&fp2, "YOUR_PATH_TO_EXE_PATHCHED", "wb");

DWORD size = Size(fp1);
buffer = (char*)malloc(size);
fread(buffer,size,1,fp1);

new_buffer = Section_add(fp1,buffer);
DLL_INJECT(new_buffer);

fwrite(new_buffer,size+ADD_SIZE,1,fp2);

return 0;
}

6.首先删除hello25.exe引入函数节所有数据,然后将所有引入的DLL名字及函数名重新定义在引入函数节开始部分,之后继续修改引入函数节其他部分(需要时可再修改DataDirectory相关项),使得该程序再次恢复原有功能。

这题明确要求我们删除导入表的所有数据,然后再自己重构一个导入表,这就要求我们对导入表有一个比较充分的认识。

如果你认认真真看了第四题,并且动手操作过,那么这题我相信你也一定做得出来:)

首先我们还是定位到导入表的位置

这里再贴出与导入表相关的所有数据

其实这里面最重要的是那些字符串数据,其他的都是地址我们都可以一个一个计算出来。

现在让我们尝试重构一个导入表吧

我们先复制这些字符串

然后在下面填入导入表结构,有两个dll,所以需要两个导入表,其中每张表的INT还有IAT我们现在是不知道的,那就索性填零,先空着

这里直接空一行在0x700开始,一方面是因为要阻断uer32.dll字符串,一方面是方便计算

如图填入两张导入表共计10个DWORD,其中dll名字我们是可以计算的,利用FOA转RVA即可

接下来就需要补充每张表各自的INT和IAT了,所谓的INT和IAT,其具体项都是一个DWORD,指向另外一个结构体 _IMAGE_IMPORT_BY_NAME,这个结构体由一个WORD型数据和字符串数据组成,字符串也就是具体函数名,即我们刚刚所复制的

NOTE:始终要记住,每一张表结束的标志都是同等大小(即每一项是4字节大小,则至少有4字节全0)的全0结构,这里我就直接空行以方便计算

稍微解释一下,这里0x740处的B0 20 00 00是作为第一张表的INT,它指向FOA6B0的数据80 00 45 78 69 74 50 72 6F 63 65 73 73,也就是一个WORD型数据加一个字符串的数据类型,然后空4字节作为INT的结束,然后是IAT表,其值与INT一样。

下面0x7500x760两行也是一样

有了INT和IAT就可以填入导入表结构了

最后别忘了修改数据目录项的导入表地址

当你欣喜做完这一切的时候,你会发现你无法运行你的exe,即使你使用PE相关工具已经能够成功解析导入表数据

不知你是否还记得在问题4做的小实验,程序在调用系统函数是直接调用系统函数吗?

其实不是,是通过jmp去IAT表来调用系统函数,程序在装进内存中时,操作系统就会把要用到的其他DLL函数一项一项填入IAT表(回忆PE文件装载过程)

让我们把程序丢去x32dbg查看,并转去查看内存0x4021000(导入表的位置)

可以发现画了红线的地方,就是IAT的地方,可以再去找找MessageBoxA函数地址,一定就是在这里面

再来看看汇编代码

果然出问题了,下面的三个jmp不知道jmp去了哪里。

这是因为在没有改之前,这三个jmp是jmp去IAT表的,而IAT表是存放了函数地址的,那我们现在都把IAT表改了,这里跳转的地址却没有改,当然无法运行。因此这里只需要改一改jmp的地址就可以了(重定位表就干这事儿用的)

(Patch流程:右键->汇编;如果你懂得一点汇编代码,那么这会是非常简单的问题;当然汇编也不是什么难事)

现在程序就可以正常运行啦~

7.使用16进制编辑器提取ZoomIt程序中尺寸最大的图标,并存储为.ico文件,确认ico文件打开可显示对应图标。

首先我们可以通过数据目录项的第三项找到资源表位置,这里直接使用PE查看器寻找

可以看到资源表在文件偏移67E00处,转去16进制编辑器查看

现在就需要知道PE文件资源的编排方式

简单来说

PE 文件中的资源是按照 资源类型 -> 资源ID -> 资源代码页 的3层树型目录结构来组织资源的,通过层层索引才能够进入相应的子目录找到正确的资源。

每一层都是以IMAGE_RESOURCE_DIRECTORY结构为头部的,并且后面跟着一个IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组。其中IMAGE_RESOURCE_DIRECTORY负责指出后面数组中的成员个数,IMAGE_RESOURCE_DIRECTORY_ENTRY数组成员分别指向下一层目录结构。

结构IMAGE_RESOURCE_DIRECTORY定义如下

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; //属性,一般为0
DWORD TimeDateStamp; //资源的产生时刻,一般为0
WORD MajorVersion; //主版本号,一般为0
WORD MinorVersion; //次版本号,一般为0
WORD NumberOfNamedEntries; //以名称(字符串)命名的资源数量
WORD NumberOfIdEntries; //以ID(整型数字)命名的资源数量
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;

IMAGE_RESOURCE_DIRECTORY_ENTRY定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31;
DWORD NameIsString:1;
};
DWORD Name;
WORD Id;
};
union {
DWORD OffsetToData;
struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
};
};
}

看着很大其实就是2个DWORD大小,也就8字节(struct的语法意思为分配一个DWORD大小,其中最高位做NameIsString,低31位做NameOffset)

第一层:

现在就一层一层的来找,第一层数据如下

红色框是结构_IMAGE_RESOURCE_DIRECTORY,可知NumberOfNamedEntries为1,NumberOfIdEntries为8,两者加起来就是结构_IMAGE_RESOURCE_DIRECTORY_ENTRY 数量,所以后面就有9个_IMAGE_RESOURCE_DIRECTORY_ENTRY 结构,也即绿色方框的内容

再使用_IMAGE_RESOURCE_DIRECTORY_ENTRY 来解读

这里的前四个字节是表示资源类型(使用Id字段),后四个字节表示这个资源类型的偏移(使用结构体字段,最高位表示是否为目录偏移),但要注意这里的偏移是相对整个资源的,即地址0x67E00

资源类型对应如下

所以我们只需要寻找前4个字节为03的就可以了,

也即地址0x67E20处,后四字节的低31位表示偏移,即0x90,转去0x67E90

第二层

这里的排列与第一层是一模一样的,红色框是结构体IMAGE_RESOURCE_DIRECTORY,绿色框是结构体数组IMAGE_RESOURCE_DIRECTORY_ENTRY

直接分析结构体IMAGE_RESOURCE_DIRECTORY_ENTRY

前四字节可能使用NameIsString、NameOffset、Id,这取决于最高位。如果NameIsString=1,说明该资源以名称(UNICODE编码的字符串)定义的,NameOffset是名称的相对整个资源结构的偏移地址。相反,如果NameIsString=0,说明该资源以ID(整型数字)定义的,ID号为Id。显然这里都是以ID导出的,即3,4,5,6,7

后面四字节使用OffsetToDirectory,与第一层一样,代表了第三层的数据偏移地址,同样是相对整个资源结构来说的。

那就直接转去0x67E00+0x1E0=0x67FE0查看

第三层

与前两层一样,第二层起始于一个IMAGE_RESOURCE_DIRECTORY头,后面紧接着是IMAGE_RESOURCE_DIRECTORY_ENTRY数组,但不同的是数组个数=1。

IMAGE_RESOURCE_DIRECTORY_ENTRY使用的是Name与OffsetToData,分别代表了资源语言类型与资源数据相对地址。Name是指语言内码,表示资源类型信息。

OffsetToData是相对整个资源结构的偏移地址,指向一个IMAGE_RESOURCE_DATA_ENTRY结构体,该结构体定义如下:

1
2
3
4
5
6
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; //资源数据的RVA
DWORD Size; //资源数据的长度
DWORD CodePage; //代码页, 一般为0
DWORD Reserved; //保留字段
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;

这个结构体描述一个具体的资源。那现在就转去0x67E00+0x3D8=0x681D8查看

所以这个资源数据偏移RVA是0x6C880,转换成FOA就是0x68680,大小是0x2E8

至此就可以找到所有的资源,并比较大小

这里介绍一个工具,Resoure hacker,可以方便的找到所有资源,当然也可以自己来写一个脚本来遍历提取:)

其ID也是我们之前找到的3,4,5,6,7

Binary起始地址也是我们找到的0x68680

说明我们分析正确啦~

现在就可以方便的提图标了~~

8.将hello25.exe程序的ImageBase从00400000H修改为00600000H,然后通过对该程序其他位置进行相应修改,使得该程序功能恢复正常。

如果你做了第六题,那么这题就很容易,当然没有做的话,也是很容易的

把注意力放在地址0x401005,0x40100A,0x40101B,0x401020以及最下面的三个jmp指令,你会发现他们的操作码都是地址,而这个地址是由ImageBase+RVA决定的,如果我们改变了ImageBase,那么这个地址就会受到影响,我们就需要去修复这些地址。

实际上重定位表记录的就是这些需要修正的地址,操作系统就检查是否按照预计的ImageBase加载,如果不是就查重定位表一项一项的去修。我们现在要做的,就是这个工作。

但是如果你使用PE查看器去找这个exe的重定位表,你会发现找不到,因为它根本就没有。

其原因是exe的ImageBase是比较小的,且它是优先装载,绝大多数情况下都会按照预计的ImageBase装载,只有极少情况下会改变,比如我们手动去改。

但是重定位表对于DLL来说是十分重要的,因为一个exe可能使用很多的DLL,而DLL全部都处于高2G,那就很容易发生冲突,就不能装载到预定位置,于是会被装载到其他地方,此时就需要重定位表来修复。

那么这里没有重定位表咋整?

没有就没有吧,反正这个程序也简单,需要修改的地址也就是我列出来的那几个,把他们的操作码全部加上0x200000就可以了,也就是改成0x6xxxxx形式

然后再利用PE工具修改ImageBase就可以啦

再用32dbg打开验证

成功~


软件安全实验T3
http://example.com/2023/10/21/software_security_task3/
Author
yring
Posted on
October 21, 2023
Licensed under