前言
修改PE文件一般有两种思路。
- 第一种是通过编程语言来修改,这种方式的Visual Studio的
Windows.h
头文件里面有完全的PE相关管理结构的完整定义,通过编程来修改比较烦,但是优点是可以自动化进行,并且不需要去找相关的工具; - 第二种方法是通过一些二进制工具来进行修改,这种方式是交互式的,能够直观的感受到整个过程,缺点是需要熟悉相关工具的UI使用方式,而且修改大量方式的时候市面上的这些工具一般都不提供编程接口;
因为本次实验仅仅是一个探索,所以就采用第二种方式。
需要的工具
本次实验用到的工具如下:
- CFF Explorer:用于结构化查看PE管理结构,同时也提供方便的PE文件修改(之前不知道的时候一直是用Winhex直接修改的);
- Winhex:用于在二进制层面修改PE文件,在本次实验中主要用于在新添加的节里面添加自己的代码;
- Ollydbg:用于调试自己的ShellCode;
- 相关能够生成本地字节码的编译环境:用于生成和观察相关汇编指令的字节码
实验前需要注意的事项
由于PE文件中的很多字段是相互关联的,这里需要先列一下要修改/注意的事项,等到修改的时候对照清单一步一步的看:
- FileAlignment:文件中的节对齐大小,等下设置新的节的时候节大小必须对齐;
- SectionAlignment:内存中的节大小,设置节内存起始偏移的时候必须对齐;
- SizeofImage:内存中的PE镜像对齐后的总大小
- SizeofHeader:这个值是按照FileAlignment的值对齐的,也就是说大部分情况下Header的地址范围内是有空洞的,可以利用,如果没有空洞的话这个值需要修改,并需要依次后移PE文件的数据;
- AddressOfEntryPoint:入口点
- NumberOFSections:节表长度
整个的修改流程应该是:
- 计算新增加的节的内存布局范围
- 修改NumberOFSections
- 使用Winhex扩展新节
- 在新节里面填入汇编代码
任意选择一个PE文件,我这里选择的是DbgView,一款用于查看OutputDebugString
函数输出字符串的软件。
新增一个节
计算PE新增加的节的内存布局
首先需要先观察一下原PE文件的内存布局:
我们选择在最后一个节后面插入自己的节,这里列一下计算过程:
- Name:随便取
- Virtual Size:都可,这里为了方便起见就去0x1000吧
- Virtual Address:这里是RVA,之前的的节的地址范围为
[0x55000,0x55000+0x31FBB+(0x1000-0x31fbb%0x1000)-1]
,所以实际的范围应该是[0x55000,0x86fff]
,那么新的节的起始地址应该是0x87000
- Raw Size: 这里取最小的粒度0x200
- Raw Address:
0X3D000+0x32000=0x6F000
- Reloc Address、Linenumbers…:PE文件中不是很重要的东西;
- Characteristics:属性,这里取和.text段属性一样的值,顺便提一嘴这个位图实际上包括了两种类型的标记,一种是内存管理的属性,这种属性和MMU硬件有着密切关联,另外一种用途,和操作系统设计有着密切关系;
修改后的节如下。
其他可能修改的值的计算
SizeofHeader、SizeofImage和AddressOfEntryPoint就不说了,很简单。这里着重说一下SizeOfHeader:
我们可以看到初始文件的SizeOfHeader值为0x400(一般编译器都是这个值),而原始的最后一个节表项的文件偏移为0x258
所以新增一个节并不会超限。
新增一个节的话使用Winhex比较简单,具体的地方是菜单栏里面的编辑中粘贴0字节这个选项。
在节中写入自己的汇编代码
因为手上并没有相关的工具,所以这次做的是传说程序员的工作——用二进制直接写代码。说一点题外话,以后如果有时间的话,想编写一个能够以类C文件为输入的
汇编的相关知识的补充
- jmp指令:jmp指令的间接跳转的相关字节码为:
E8 xx xx xx xx
,后四个字节是跳转地址-当前EIP-5
的补码; - call指令:call指令的内存寻址的相关字节码为:
FF xx xx xx xx xx
,跳转地址为地址为xx xx xx xx xx
的内存单元所指的地址;
编写汇编指令
这次想弹出一个提示框,需要使用的外部函数是Messagebox
。需要的汇编伪代码应该如下:
call eip+0x05;
jmp DataLength;
Data
push 0
add esi,0ah ;call指令和jmp指令加起来一共10个字节
push esi
add esi,FirstStringLength
push esi
push 0
call MessageboxA
jmp OriginEnterPonit
其他的偏移我们都可以手工计算,其中的关键是call MessageboxA
的机械码:
由于我们这次选的软件是一个GUI软件,并且导入表暂时没有学(其实也很简单,比较懒),那就使用IDA看一下编译器生成汇编码吧。
这样我们就获得了call MessageboxA
。
完整的汇编代码应该如下:
mycode:00487000 start:
mycode:00487000 ; __unwind { // __SEH_prolog4
mycode:00487000 call $+5
mycode:00487005 jmp loc_487020
mycode:00487005 ; ---------------------------------------------------------------------------
mycode:0048700A aHelloWorld db 'Hello World!',0
mycode:00487017 aWarning_0 db 'Warning!',0
mycode:00487020 ; ---------------------------------------------------------------------------
mycode:00487020
mycode:00487020 loc_487020: ; CODE XREF: mycode:00487005↑j
mycode:00487020 push 0 ; uType
mycode:00487022 add esi, 0Ah
mycode:00487025 push esi ; lpCaption
mycode:00487026 add esi, 0Dh
mycode:00487029 push esi ; lpText
mycode:0048702A push 0 ; hWnd
mycode:0048702C call ds:MessageBoxA
mycode:00487032 jmp loc_4153B7
mycode:00487032 ; } // starts at 487000
这样我们就获得了一份插入自己代码的实验。另外这个代码微软自带的杀软会报错,特征应该是位于call eip+0
这一段。
题外话
这个实验最费时间的是ShellCode的编写,之前在做这方面工作的时候也没有好的解决方案。看看考完研后能不能抽空写输入为某个PE文件和类C源码,输出为地址无关的ShellCode的编译器。需要用到的知识有:
- 编译原理
- 相关汇编知识
- 相关平台的已有编译器
- PE文件结构
具体的思路为:
- 解析ShellCode所依赖的PE文件,生成自己的符号表;
- 使用汇编原理的相关知识将类C源码解析成相关语法树,对类C源码的语法进行插桩(建立相关映射),生成一份C语言文件,这份文件作为gcc等编译器的输入输出一份可执行文件,这一步实际上是省去了编译器的完整后端工作;
- 利用插桩的信息将可执行文件中的变量地址和跳转地址修改为地址无关的及正确的;
插桩的内容可能包括:
- 将变量修饰为符合C语言的语法,如果类C源码中使用到了PE文件中的相关函数
- 将相关函数的调用修饰为能够识别函数指针,在这个过程中建立映射表。
最后一步的修改过程可能包括,这一步的话有两种思路,一种是以函数为单位利用现有编译器进行编译,最后进行组装;第二种是将整个类C源码进行编译吗,后进行修改:
- 将变量地址修改为使用基址寻址的地址无关码,对于局部变量不需要修改,主要修改的是对全局变量的引用;
- 由于地址无关码需要使用寄存器,需要进行寄存器保护;
- 使用地址自识别技术来获取基址;
- 对于使用了Call和jmp的指令,需要根据基址来修改,目前想到两个方法,一个是在编译期通过用户指定基址来修改,另外一个办法就是编写一个子例程在获取了基址之后进行修改;
当然目前的整个构思还有一定程度的问题,如无法使用在运行环境中没有引入的第三方库。