最近在学习PE结构,网上找到滴水第三期中关于PE的讲解,视频中是用impsg
做的讲解,用那个实现起来比较简单,因为impsg
文件对齐和内存对齐粒度一样,所以不需要考虑文件对齐和内存对齐,为了加深自己对PE文件的理解,事后我用notepad做实验时候总是出错,主要原因是notepad文件对齐和内存粒度不同导致FOA与RAW转换出错,最后将过程记录下来。文中难免有差错,请指正。
在此之前需要对Windows PE
结构有个基本的了解。
1.准备工作
1.1获取MessageBox地址
方法①:
#include <stdio.h>
#include <windows.h>
typedef void (*FuncPointer)(LPTSTR); // 函数指针
int main()
{
HINSTANCE LibHandle;
FuncPointer GetAddr;
// 加载成功后返回库模块的句柄
LibHandle = LoadLibrary("user32");
printf("user32 LibHandle = 0x%X\n", LibHandle);
// 返回动态链接库(DLL)中的输出库函数地址
GetAddr=(FuncPointer)GetProcAddress(LibHandle,"MessageBoxA");
printf("MessageBoxA = 0x%X\n", GetAddr);
return 0;
}
方法②:
用OllyDBG加载notepad,左下角command
框中输入bp MessageBoxA
下断点。查看断点即可得MessageBox地址为:0x77D507EA
1.2CALL和JMP的计算
#include <stdio.h>
#include <windows.h>
void func()
{
MessageBox(0, 0, 0, 0);
}
int main()
{
func();
return 0;
}
main
函数反汇编:
11: func();
0040D478 E8 8D 3B FF FF call @ILT+5(func) (0040100a)
12: return 0;
0040D47D 33 C0 xor eax,eax
得到CALL
的硬编码是E8
,
call func
继续跟进去:
0040100A E9 01 00 00 00 jmp func (00401010)
0040100F CC int 3
得到JMP
的硬编码是E9
,
func
函数反汇编:
6: MessageBox(0, 0, 0, 0);
00401028 8B F4 mov esi,esp
0040102A 6A 00 push 0
0040102C 6A 00 push 0
0040102E 6A 00 push 0
00401030 6A 00 push 0
00401032 FF 15 AC A2 42 00 call dword ptr [__imp__MessageBoxA@16 (0042a2ac)]
观察到:
地址 机器指令 汇编指令 指令所占字节数
0040D478 E8 8D 3B FF FF call @ILT+5(func)(0040100a) 5
0040D47D
E8后边的值并不是真正我们要调用的函数地址,
他们有以下关系:
E8这条指令的下一行地址 E8当前指令的地址 E8当前指令所占大小
0x0040D47D = 0x0040D478 + 0x5
真正要跳转的地址 E8后边的值 E8当前指令的下一行地址
0x0040100A = 0xFFFF3B8D + 0x0040D47D
总结出:
E8后边的值 = 真正要跳转的地址 - (E8当前指令的地址 + E8当前指令所占大小)
= 真正要跳转的地址 - E8当前指令的下一行地址
1.3构造shellcode
通过之前的学习,可以构造出:
// 18 Bytes
shellcode [] = {0x6A, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0x6A, 0x00, /* MessageBox4个参数入栈 */
0xE8, 0x00, 0x00, 0x00, 0x00, /* 调用MessageBox */
0xE9, 0x00, 0x00, 0x00, 0x00 /* 跳到原程序的入口(AddressOfEntryPoint) */
};
2.代码区添加shellcode
2.1分析PE结构
图1
图2
从图2中可以得到以下信息:
IMAGE_OPTIONAL_HEADER(可选头)中部分成员信息:
DWORD AddressOfEntryPoint; // 程序执行入口RVA (0x0000739D)
DWORD ImageBase; // 程序的优先装载地址(基址) (0x01000000)
// 程序运行时,PE装载器先创建进程,再将文件载入内存,然后把EIP寄存器的值设置为:ImageBase + AddressOfEntryPoint;
DWORD SectionAlignment; // 内存中节的对齐粒度 (0x00001000)
DWORD FileAlignment; // 文件中节的对齐粒度 (0x00000200
IMAGE_SECTION_HEADER(节表第一项即代码区部分成员信息):
DWORD VirtualSize; // 节区在内存中没有对齐前的实际大小 (0x00007748)
DWORD VirtualAddress; // 节区在内存中起始位置(RVA) (0x00001000)
DWORD SizeOfRawData; // 节区在文件中对齐后的大小 (0x00007800)
DWORD PointerToRawData; // 节区在在文件中的偏移 (0x00000400)
2.2判断空闲区域是否能放下shellcode
计算文件中代码区空闲空间
SizeOfRawData(0x7800) - VirtualSize(0x007748) > 0x12
空闲空间大于shellcode长度,可以放得下。
2.3将构造好的ShellCode写入空闲区:
PointerToRawData(0x0400)+SizeOfRawData(0x7800)=0x7C00,
在0x7B48与0x7C00之间写入shellcode:
图3
2.4计算E8后边的值
在计算相关值由文件映射到内存时,需要考虑内存对齐和文件对齐。
真正要跳转的地址:MessageBox地址:0x77D507EA
文件中,E8下一行地址相对PointerToRawData偏移量:0x7B5D - 0x400 = 0x775D
映射到内存中,E8下一行地址:ImageBase + VirtualAddress + 0x775D = 0x0100875D
E8后边的值:MessageBox - 0x0100875D = 0x76D4808D
2.5计算E9后边的值
要保证MessageBox关闭后,程序能够正常运行,需要jmp到原来的OEP
原来OEP(真正要跳转的地址):ImageBase + AddressOfEntryPoint = 0x0100739D
文件中,E9下一行地址相对PointerToRawData偏移量:0x7B62 - 0x400 = 7762
映射到内存中,E9下一行地址:ImageBase + VirtualAddress + 0x7762 = 0x01008762
E9后边的值:`0x0100739D - 0x01008762 = 0xFFFFEC3B`
2.6修改OEP(AddressOfEntryPoint)
文件中shellcode起始地址相对PointerToRawData偏移量:0x7B50 - 0x400 = 0x7750
映射到内存中,相对ImageBase偏移:VirtualAddress + 0x7750 = 0x8750
将原来的OEP修改为,映射到内存后的shellcode起始地址(0x8750)。
另存文件,双击,成功弹出MessageBox,如图4:
[课程]Android-CTF解题方法汇总!