长亭百川云 - 文章详情

原创 | 深入解析pe结构(上)

SecIN技术平台

55

2024-07-13

内存分配与文件读写

宏定义

1.无参数的宏定义
正常例子:

#define TRUE 1

注意:只作字符序列的替换工作,不作任何语法的检查

特例:

#include<stdio.h>

如果先把TEST=3计算出来 带入结果是8 但是运行结果却为6

因为C语言会直接把TEST替换成11+2,变成21*1+2+2,结果为

2.带参数的宏定义
例子:

#include<stdio.h>

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

与函数的区别:函数需要额外的内存空间,而宏定义不需要

头文件与重复包含问题

如下一段代码,根据c语言的特性,如果函数定义在main函数以下,那么执行这个函数会报错

#include<stdio.h>

因此为了避免报错,我们可以在main函数以上进行一个空定义就可以正常运行

#include<stdio.h>

于是我们可以把void Function();添加到一个头文件中,包含头文件即可

重复包含问题
当x.h  y.h两个头文件都包含了z.h头文件时,如果一个程序同时包含了x和y头文件,就相当于重复包含了两次z.h,这时候会引起编译报错

避免方式:
在新版visual studio code中创建头文件时会自动添加#pragma once来避免
在vc++6.0中,可以利用如下代码来避免

#if !defined(ZZZ)  //其中zzz任意起名,越乱越好为了确保不会重复

内存的分配释放

int* ptr;//声明指针

文件读写练习

题目:将记事本的.exe文件读取到内存,并返回读取后在内存中的地址,并将内存中的数据存储到一个文件中,(.exe格式),然后双击打开,看是否能够使用

#include<windows.h>

成功执行

DOS头与PE头解析

现在正式进入pe结构的学习

pe文件结构图表

内存对齐与硬盘对齐

在 Windows 操作系统中:
硬盘对齐:通常为 512 字节,也可以是 4KB。200h
内存对齐:在 x86 架构中通常是 4 字节,而在 x64 架构中通常是 8 字节。1000h
在内存中会比在硬盘中占据的空间大,pe文件在内存中大小会被拉伸

当我们用c语言申请一块1字节的内存空间

#include<stdio.h>

在x32dbg中发现其实是占用了1000h字节的空间,这就是内存对齐的结果

当我们分别用winhex打开磁盘文件和内存文件时也会发现起始地址和文件长度不同
磁盘文件

内存映像文件

DOS头

dos头结构,根据大小相加可以看到DOS头占了64个字节

typedef struct _IMAGE_DOS_HEADER {  

我们用winhex加载之后对应寻找即可

由于计算机是小端排序,word大小是2个字节 e_magic的内容是5A4D,根据winhex计算即可

typedef struct _IMAGE_DOS_HEADER {  

我们可以使用软件来进行对比查看

当DOS头找完之后,根据上方的PE结构图表我们需要找PE头

在DOS头最后的e_lfanew,他的值就是PE头开始的位置,也就是E8的位置

而我圈框的地方也就是DOS头到PE头之间,全部为垃圾数据,因为早期是16位的电脑,现在都是32位和64位的电脑,为了保持兼容性,所以出现了上面的垃圾数据,里面的内容默认是编译器自动添加,我们可以随意修改对PE文件没有任何影响

观察标准PE头,开始是4字节的一个标识也就是00 00 45 50,之后是两个结构体,第一个是标准PE头(IMAGE_FILE_HEADER)

第二个是可选PE头(IMAGE_OPTIONAL_HEADER)

标准pe头

Machine用来确定运行程序的cpu型号
NumberOfSections图中说是区块的数量,其实是节的数量,如果内容为3也就说明有3节,如下图在扩展pe头(OPTION PE)之后就是节

TimeDateStamp为时间戳,记录软件生成的时间,如果时间戳大小超出范围则自然溢出,可以修改

SizeOfOptionalHeader为可选pe头的大小,32位默认E0h,64位默认F0h ,可以被手动修改

Characteristics一共四字节 16位 每一位都有不同的含义,可以通过软件来查看,但是在软件上一共有15位,因为第六位有个此标志保留没有显示出来,软件标相当于1,没有标相当于0,这样可以算出二进制0000 0001 0000 1111

扩展pe头

Magic 说明文件类型:10B 32位下的PE文件 20B 64位下的PE文件

AddressOfEntryPoint(OEP)是程序的入口点(偏移)需要加ImageBase(内存镜像基址)的值才是程序真正入口点

而我们用winhex通过内存加载的值就是ImageBase的值,而通过OD加载的是OEP+ImageBase的值

oep可改,但是改完之后得让他可以运行,之后会学到

再次进入od发现入口地址已经改变,但是没有加节运行起来会报错
SizeOfHeaders所有头加节表的总大小,节表存储着每一节的属性信息

这里放出海东老师总结的重点(带)*

编写程序读取一个.exe文件,输出所有的PE头信息.

#include<stdio.h>

节表

c++联合体

当我们需要存储一个内容来辨识某人的身份,用学号或者身份证号之一就可以,但是如果我们设置一个结构体,其中总有一块空间是浪费的

struct Student

于是引出了联合体,联合体所有变量共用一块内存空间,内存空间的大小取决你最大的数据类型的大小

union Student

示例代码

#include<stdio.h>

节表结构

节表的位置为DOS头中获取到e_lfanew的值+4(pe标识)+20(标准pe头大小)+标准pe头中SizeOfOptionalHeader(可选pe头的大小)的值

typedef struct _IMAGE_SECTION_HEADER {

Name参数8位,指节的名字,如果我们要用c语言读取名字,c语言要字符串以0结尾,但是他的名字是任意的,如果占了八位,这时我们读取完名字由于没有0填充可能会出现越界行为,因此读取名字时不能用char *,要用char [9]最后一位存0

Misc是该节在内存中没有对齐前的真实尺寸,该值可以不准确,因此可以被修改,例如A到B就是没有对齐前的大小,而后面的0是为了对齐而填充的。

VirtualAddress是节区在内存中的偏移地址。加上ImageBase才是在内存中的真正地址.
SizeOfRawData 节在文件中对齐后的尺寸

PointerToRawData 节区在文件中的偏移,找到偏移为400,找到400即可,因为在文件中是0开始

找到text段

Characteristics 节的属性,我们可以根据对照表

Characteristics的值是不同的属性相加而成,所以60000020的意思是包含可执行代码,可读,可执行

练习:编写程序打印节表中的信息
在之前程序加上这一段即可

//节表

===

FileBuffer与ImageBuffer

FileBuffer与ImageBuffer的相互转换(重要)

FileBuffer是在硬盘上的存储状态 ImageBuffer是在内存上存储状

虽然拥有了ImageBuffer的状态但是还不能完全满足运行的条件,但是这是让程序运行的第一步,所以我们要学会将FileBufeer手动转换为ImageBuffer

首先思考一下该流程在c语言的实现过程(FileBuffer ImageBuffer ImageBuffer)

1.读取文件内容并申请内存将内容放到内存中(FileBuffer)

ImageBuffer To FileBuffer 流程与上方大同小异

C语言实现代码

#include<stdio.h>

RVA与FOA的转换

RVA就是相对虚拟地址(运行时的位置)(1234),FOA(434)就是文件偏移地址(磁盘静态的位置)

#include<windows.h>

注意以上所有的操作均是在全部代码已初始化的条件上完成的

===

代码节空白区添加代码

目的及思路分析

这里有一个程序,运行之后就会弹出一个对话框,我们如何把这个功能移植到飞鸽传书上呢,这时就需要往空白的节区添加一段我们想要的代码来实现

我们的思路是修改OEP(入口点)为空白区添加代码的位置,然后利用call调用MessageBoxA,随后在使用jmp跳回到程序真正的运行位置

硬编码

我们知道程序在运行的时候都是0和1的形式,并且pe结构都是16进制,我们不可能真正的写汇编代码如call,jmp

这时计算机为我们提供了硬编码这种东西,他是16进制字符,不同的16进制字符表示了不同的含义,那如何知道call和jmp表示的16进制字符呢

将MessageBoxA写入一个函数,这样调用函数时就会在汇编层面调用call

#include<windows.h>

我们f9给funciton()下断点,f5运行,右键进入反汇编界面,发现call了function,并且可以看到他的硬编码E8 8D 3B FF FF

如果没有显示硬编码把Code Bytes选上即可

我们在call那里选中看f11可以看到jmp的硬编码E9 01 00 00 00

通过上面我们可以看到E8和E9分别代表着call和jmp,后面的4个字节代表的是要跳转的地址,其实这四字节并非真正要跳转的地址,而是根据这四字节计算机会自动计算出要跳转的真正地址

如果把后面这四字节当做X,跳转地址有一个计算公式

 真正要跳转的地址 = E8这条指令的下一行地址 + X

而我们在跳转时候我们关心的是X的值,根据上方公式

 X = 真正要跳转的地址 - E8这条指令的下一行地址

观察如下图,由于jmp和call都占了五个字节,于是下一条指令的地址就是当前地址+5,因此 

X = 要跳转的地址 - (E8的地址 + 5)

通过上面我们只知道了如何call messagebox,但是我们还要确定调用之前的准备工作,对messagebox下断点

发现在call之前还调用了四次push 6A 00我们也要记录下来

MessageBoxA的地址

我们既然要弹窗,肯定要用call调用MessageBoxA的地址,由于他是存在于系统文件user32.dll中,所以MessageBox函数的入口点地址也是固定的

因此我们任意载入一个带有弹窗的软件,使用bp MessageBoxA添加断点

点击b就可以看到MessageBoxA的入口地址77E5425F并且是永恒不变的

修改PE文件

由于飞鸽传书这个软件的文件对齐和内存对齐都是1000,因此不需要考虑对齐问题
通过软件查看text表,表是从1000开始从46000(45000+1000)结束

先找一个空白位置,添加4个6A00 然后E8调用messagebox

要计算X,拿出我们之前的公式

X = 要跳转的地址 - (E8的地址 + 5)

要跳转的地址是77E5425F,E8的地址是450B8,但是这只是相对地址,我们真正在内存中执行时需要加上imageBase的地址

因此X=77E5425F-(4450B8+5) X=77 A0 F1 A2 (要倒着写)
使用jmp跳转到程序入口点AddressOfEntryPoint,000441EC X = 4441ec - 4450c2 FF FF F1 2A

注意拿计算器计算时需要选择双字DWORD

找到OEP的地址

将OEP修改为我们添加代码的入口处450BD,因为oep本身就是偏移地址,因此没必要加imagebase

修改完毕保存即可查看效果,同理我们可以添加我们自己生成的shellcode

对齐大小不同情况下的计算方法

选择Notepad发现文件和内存对齐大小不同,这也是大多数软件的情况

根据表格找到Misc+shellcode的大小+Imagebase+virtualaddress就是在Imagebuffer中的内存位置,因此可以找到call和jmp函数的位置
oep的偏移地址就是Misc+shellcode的大小+virtualaddress
大家可以自行使用notepad进行测试

c语言实现:

//func.c 函数的实现

这部分主要进行了节表相关的讲解,下部分会进行动态链接库导入导出表的讲解

pe结构只有多看多练才能真正属于自己

往期推荐

原创 | 2023 CISCN 第十六届全国大学生信息安全竞赛初赛 WriteUp

原创 | CVE-2022-24481

原创 | 一文带你理解AST Injection

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2