二进制安全-PE文件基础-手动添加ShellCode

目录

前言
前置知识
CALL与JMP的地址计算
OEP地址
ShellCode生成
结语

前言

在之前几节中,我们讲解了PE的一些基本知识,理论知识是很枯燥的,只有实际操作才能对PE文件的认知更加深刻。

前置知识

节表属性

被添加的节表属性必须有可执行属性,一般添加到.text节中。

硬编码

在PE文件中存储的都是硬编码,既是汇编指令对应的字节,硬编码是程序编译后的数据是写死的。所以ShellCode插入PE文件中也要遵守PE文件的格式,必须是硬编码不然无法执行。

直接调用(E8)和间接调用(FF)

直接调用:CALL 函数地址
直接调用后的函数地址是该函数真正的地址,在编译后这个地址可能不会发生改变。
间接调用:MOV EAX,EBX;CALL [EAX];
间接调用则不同,间接调用的函数函数地址虽然不会发生改变,但是会有一个地址保存该函数的地址。
比如这个,EAX的值是根据EBX的值得来的,那么当CALL [EAX]的时候真正调用的不是EAX函数,而是EAX保存的数据,这个数据就是该函数的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<iostream>
#include<Windows.h>

using namespace std;

typedef void(*MySum)();
MySum Sum = nullptr;

void sum()
{
return;
}
int main(int argc, char* argv[])
{
sum(); // 直接调用 int3

Sum = (MySum)sum;
Sum(); // 间接调用 int3
return 0;
}


直接调用了sum函数,此时CALL的硬编码是E8。

间接调用,把sum函数的地址存放到了004303F8h地址处,call的是004303F8h中保存的数据,此时call是FF。

CALL和JMP

CALL通常用来调用某个函数,而JMP是无条件跳转到指定地址后开始执行代码。
在硬编码中CALL的硬编码的E8,JMP的硬编码是E9。

OEP地址

OEP地址是程序在内存中真正执行的地址。该值是由该程序的AddressOfEntryPoint+ImageBase得到的。

ShellCode

这里就用最简单的ShellCode演示,使用MessageBox函数,我们在程序中写一个没有任何参数的MessageBox用来生成他的硬编码。

所以我们的硬编码就是:
0x6A, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00,
同时呢这里也可以看到,我们调用MessageBox也是间接调用,因为MessageBox是在dll中的,程序每次运行后MessageBox的值可能发生改变。
这里手动添加ShellCode限制比较多,我们直接使用E8 CALL去调用MessageBox
但是这样我们的硬编码是不完整的,因为我们让程序先执行我们的ShellCode后要在跳回程序原本的OEP让程序继续执行,所以就需要一个JMP,那么完整的硬编码就是:
0x6A, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00, 0xE9, 0x00, 0x00, 0x00, 0x00

手动添加ShellCode

在本案例中我将ShellCode插入到了.text节的PointerToRawData + Misc.VirtualSize处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
#include<Windows.h>

using namespace std;

BOOL PrintHello()
{
cout << "[·INFO·] HelloWorld" << endl;
return TRUE;
}

int main(int argc, char* argv[])
{
PrintHello();
return 0;
}

我们就以这个程序作为例子添加我们的ShellCode

找到MessageBox函数地址

我们使用x96dbg调式该程序,在”符号”中找到user32.dllMessageBox函数的地址:

MessageBox地址是:76770F40

计算E8 CALL的地址

这里要延伸出一个问题了,CALL 地址在转为硬编码的时候并不是E8 地址

看这里,我要CALL的是00411604地址,而硬编码却是:E8 CE CF FE FF

那么这个函数地址与E8的地址之间的计算关系:

函数地址 = CALL当前指令的下一条指令的地址 + X
X就是E8后的的4个硬编码,那么X就等于:
X = 函数地址 - CALL当前指令的下一条指令的地址。

那么问题又来了,如何计算CALL当前指令的下一条指令的地址呢?
那就需要确定ShellCode插入的位置了,在本示例中我把ShellCode插入到了.text节表中的VirtualSize位置处。

所以E8 下一条指令的地址就是:
PointerToRawData + VirtualSize + 0x8 + 0x5
其中 0x8 是 ShellCode 的前 8 个字节,0x5 是 E8 本身,这样就计算出了 E8 下一条指令的地址了。

所以我们这里E8 后4个字节的值就是:
400h + 199A6h + 8h + 5h = 19DB3
`76770F40 - ((((19DB3 - PointerToRawData) + VirtualAddress) + ImageBase(400000h)) = 7634658D
这里之所要这样计算是因为我们要模拟ShellCode在内存中的状态,也就是ShellCode在内存中距离内存地址的距离。

所以硬编码就是 E8 8D 65 34 76

计算E9 JMP的地址

经过E8的计算,我们很快可以计算出来E9的硬编码,E9要跳回程序原来的OEP
400h + 199A6h + 8h + 5h + 5h = 19DB8
11028h + 400000h - ((((19DB8 - PointerToRawData) + VirtualAddress) + 400000)) = FFFE6670 E9 70 66 FE FF`

修改原有OEP

新的OEP计算,ShellCode内存地址 - ImageBase
ShellCode内存地址:
199A6 + 11000h + 400000h = 42A9A6
OEP
42A9A6 - 400000 = 2A9A6

验证

使用X96dbg调试程序



结语

通过手动添加ShellCode使得对PE文件格式有更加深刻的认识,特别是文件状态和内存状态。这两种状态特别容易混乱。手动添加ShellCode成功后再编写代码添加ShellCode是很快的,