PE文件结构(四)

Last updated on October 7, 2023 pm

PE练习,修改OEP执行shellcode

回顾

在可选PE头OptionalHeader中有这样一个成员AddressOfEntryPoint,这个成员记录整个exe程序开始入口地址,而且是一个RVA,即虚拟相对地址,需要将其加上程序装载的基址即ImageBase才是在内存中真正运行的地址。

那么自然就有一种想法,能否通过修改这个OEP,在程序执行之前先执行其他代码,然后再执行源代码?

练习

要求

1.通过修改OEP,在程序执行前执行一个弹窗

2.弹窗后恢复正常执行流

需要解决的问题

Q1:弹窗函数在哪里

A1:可以先使用OD等调试工具装载exe文件,在调试工具里面找到弹窗函数MessageBoxA

函数原型

1
int MessageBoxA( HWND hWnd,LPCTSTR lpText, LPCTSTR lpCaption = NULL, UINT nType = MB_OK );

参数说明

hWnd:表示窗口句柄,指定该对话框的所有者窗口;如果该参数为空(0/NULL),则该对话框不属于任何窗口
lpText:字符串,指显示在对话框中的内容
lpCaption:字符串,指对话框的标题;如果此参数为空,则默认使用“错误”作为标题
nType:指定显示按钮的数目及形式,表名使用的图标样式、缺省按钮是什么、以及消息框的强制回应等

Q2:如何调用

A2:MessageBoxA有四个参数,通过栈来传递参数,简单起见,只需要连续的四个push 0即可,在push完后,就应该使用call指令调用这个函数,类似于call x。然而注意的是,这里的x并不是在调试工具里找到MessageBoxA的地址,而是找到的地址减去call指令的下一条地址。

举个例子,比如call指令地址是0x1000,下一条指令地址是0x1005(call指令占5个字节长度),需要call的函数地址位置为0x1105,那么就应该写为call 0x100,这里的100就是0x1105 - 0x1005得到。

由于是在文件里面进行修改,这里修改的就是机器码,push 0的机器码是6A 00call x的机器码是e8 x,其中x占四个字节,e8即为call的操作码

Q3:如何返回

A3:在call完MessageBoxA后,就应该返回原来的正常执行流,那么可以在call指令下一条使用jmp指令直接跳回原来的OEP,格式为jmp x,同样的,这里的x也是jmp下一条指令地址减去要跳转的指令地址,x也为四字节,机器码格式为e9 x,e9即为jmp的操作码

Q4:在哪里插入shellcode

A4:理想的插入位置就是Code区或者Text区,因为这两个地方存放的就是代码片段,实际上任何地方都可以(得是空白段,即不能影响原来的代码),只要相应的地方拥有执行权限即可。

根据Q2,Q3,Q4可以插入对应的shellcode雏形

Q5:如何修改原来的OEP,使其执行shellcode

A5:OEP存储的是RVA,也就是在内存中偏移,那么就需要从文件偏移计算内存偏移,也就是涉及到RVA和FOA的互相转化。值得一提的是,这里的RVA和FOA其实只会对节区以后的数据有影响,所有的头其实都是不变的,因为这些头之间都是紧紧挨在一起的,中间没有填充。

那么给出一个FOA,如何计算它的RVA呢。实际上只需要确定它在哪一个节区,减去这个节区的起始地址,算出它在节区中的偏移offset,再加上节区在内存中的起始地址即可算出在内存中的地址。确定节区也好办,只需要大于此节区开始地址VirtualAddress,小于下一个节区开始地址,就可以确定它是在此节区中。

从RVA计算FOA也是一样的,确定节区,算节区偏移,加上节区在文件中起始地址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
#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_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 (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, "C:\\Users\\yongrin\\Desktop\\PE_info.exe", "rb");

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

printf("%X",FOA_TO_RVA(0x5d380,buffer));
printf("%X",RVA_TO_FOA(0x5df80,buffer));
return 0;
}

计算地址

真正ImageBase

由于设备等原因,程序并不一定会加载到ImageBase的地方,这时就需要判断程序会加载到什么基址上。最好的办法就是使用调试工具,程序就会自动断在OEP处。

此时的OEP地址为0x6DEFC,是用PE查看器或者自己手动找文件中的OEP,就是可选头的AddressOfEntryPoint字段可得0x5DEFC,那么很显然,真正的ImageBase就是0x10000。

修改OEP

shellcode在文件中起始地址是0x5D380,转化为RVA可得0x5DF80,那么只需要把OEP改写为0x5DF80即可,修改完再拖入调试器应该是这样的,可以从中看见插入的shellcode,下一步就是计算偏移

函数MessageBoxA在内存中地址是0x75B23D90(可以通过符号查看等方式找,此函数在user32.dll里,不同设备这个值也会不同),那么计算call的地址就是要跳转的地址 - 下一条指令地址,具体计算就是0x75B23D90 - 0x6DF8D = 0x75AB5E03 ,同理jmp的地址0x6DEFC - 0x6DF92 =0xffffff6a (要jmp的地址是原来的OEP)

因此在文件中改为

在调试器中为,成功显示相关函数!

此时也能按照预期的方式去运行,即先弹窗再正常执行

代码实现

用到了以前的几个函数

定义了ImageBase 的宏 以及 MessageBox的宏

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
#include<stdio.h>
#include<malloc.h>
#include<windows.h>

#define ImgaeBase 0x10000
#define MessageBox_Addr 0x75C93D90
BYTE Shell[] = {0x6A,0x00,0x6A,0x00,0x6A,0x00,0x6A,0x00,
0xE8,0x00,0x00,0x00,0x00,
0xe9,0x00,0x00,0x00,0x00};

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;
}
}
//最后一个节区
SectionHeader +=1;
DWORD offset = FOA - SectionHeader->PointerToRawData;
return SectionHeader->VirtualAddress + offset;
}

void Shell_add(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;
NTHeader = (PIMAGE_NT_HEADERS)(buffer + DosHeader->e_lfanew);
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);

char* shell_begin = buffer + SectionHeader->PointerToRawData + SectionHeader->Misc.VirtualSize ;

DWORD begin_FOA = SectionHeader->PointerToRawData + SectionHeader->Misc.VirtualSize;


DWORD e8_FOA = begin_FOA + 8;
DWORD e9_FOA = e8_FOA + 5;

DWORD begin_RVA = FOA_TO_RVA(begin_FOA,buffer);
//计算call地址的编码 即e8 xxxxx
DWORD e8_x_addr = MessageBox_Addr - (FOA_TO_RVA(e9_FOA,buffer)+ImgaeBase);
//计算原oep的地址编码 即e9 xxxxx
DWORD e9_x_addr = OptionalHeader->AddressOfEntryPoint - (FOA_TO_RVA(e9_FOA,buffer)+5);

//先修改原OEP
OptionalHeader->AddressOfEntryPoint = FOA_TO_RVA(begin_FOA,buffer);

memcpy(shell_begin,Shell,18);

//这里要注意memcpy的用法
DWORD* e8 = (DWORD*)(shell_begin + 9);
DWORD* e9 = (DWORD*)(shell_begin + 14);
memcpy(e8,&e8_x_addr,4);//这里也可能是1,把4改为1
memcpy(e9,&e9_x_addr,4);
}
int main(){
char* buffer;
char* new_buffer;

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

errno_t err_1 = fopen_s(&fp1, "C:\\Users\\yongrin\\Desktop\\PE_info.exe", "rb");
errno_t err_2 = fopen_s(&fp2, "C:\\Users\\yongrin\\Desktop\\PE_info_shell.exe", "wb");

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

Shell_add(buffer);
fwrite(buffer,size,1,fp2);

return 0;
}

后记

1.也可以直接拉伸成ImageBuffer后再patch,都是一样

2.也可以在IDA里面直接patch,构造几个简单ROP即可


PE文件结构(四)
http://example.com/2023/07/02/PE_3/
Author
yring
Posted on
July 2, 2023
Licensed under