宏定义
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>
成功执行
现在正式进入pe结构的学习
在 Windows 操作系统中:
硬盘对齐:通常为 512 字节,也可以是 4KB。200h
内存对齐:在 x86 架构中通常是 4 字节,而在 x64 架构中通常是 8 字节。1000h
在内存中会比在硬盘中占据的空间大,pe文件在内存中大小会被拉伸
当我们用c语言申请一块1字节的内存空间
#include<stdio.h>
在x32dbg中发现其实是占用了1000h字节的空间,这就是内存对齐的结果
当我们分别用winhex打开磁盘文件和内存文件时也会发现起始地址和文件长度不同
磁盘文件
内存映像文件
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>
当我们需要存储一个内容来辨识某人的身份,用学号或者身份证号之一就可以,但是如果我们设置一个结构体,其中总有一块空间是浪费的
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是在内存上存储状
虽然拥有了ImageBuffer的状态但是还不能完全满足运行的条件,但是这是让程序运行的第一步,所以我们要学会将FileBufeer手动转换为ImageBuffer
首先思考一下该流程在c语言的实现过程(FileBuffer ImageBuffer ImageBuffer)
1.读取文件内容并申请内存将内容放到内存中(FileBuffer)
ImageBuffer To FileBuffer 流程与上方大同小异
C语言实现代码
#include<stdio.h>
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我们也要记录下来
我们既然要弹窗,肯定要用call调用MessageBoxA的地址,由于他是存在于系统文件user32.dll中,所以MessageBox函数的入口点地址也是固定的
因此我们任意载入一个带有弹窗的软件,使用bp MessageBoxA添加断点
点击b就可以看到MessageBoxA的入口地址77E5425F并且是永恒不变的
由于飞鸽传书这个软件的文件对齐和内存对齐都是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结构只有多看多练才能真正属于自己
往期推荐