二进制程序插桩实践-x86
AFL分析得差不多了,需要来看看LEIF是如何实现的
环境编译
直接make就可以,会报bits/c++config.h: No such file or directory。
使用命令sudo apt-get install gcc-multilib g++-multilib
如果知道使用的是什么版本的gcc的话也可以sudo apt-get install gcc-4.8-multilib g++-4.8-multilib
elf文件格式
使用010edit来分析elf格式,
文件头格式如下:
e_ident Magic,类别,数据,版本,OS/ABI,ABI 16字节
e_type 类型 2字节
e_machine 系统架构 2字节
e_version 版本 4字节
e_entry 入口点地址 4字节
e_phoff 程序头开始 4字节
e_shoff 段头开始 4字节
e_flags 标志 4字节
e_ehsize 文件头的大小 2字节
e_phentsize 程序头大小 2字节
e_phnum 程序头数量 2字节
e_shentsize 段头大小 2字节
e_shnum 节数量 2字节
e_shstrndx 段符串表段索引 2字节
程序头格式如下:
p_type; /* Segment type*/
p_offset; /* Segment file offset */
p_vaddr; /* Segment virtual address */
p_paddr; /* Segment physical address */
p_filesz; /* Segment size in file */
p_memsz; /* Segment size in memery */
p_flags; /* Segment flags */
p_align; /* Segment alignment */
大小均为4字节
对于有PIE的32位程序
有PIE了就是程序运行时需要计算偏移了,在插桩的方式上会比较麻烦
文件头的处理
处理文件头。由于需要加一个段,所以将段数+1,(e_shnum),然后程序入口点地址会变化,所以要改e_entry,加0x20,是在程序头中添加的。另外e_shoff也要改,因为前面数据变了,这需要计算。
程序头的处理
LOAD:000000B4 ; PHT Entry 4 LOAD:000000B4 dd 1 ; Type: LOAD LOAD:000000B8 dd 2000h ; File offset LOAD:000000BC dd offset loc_3000 ; Virtual address LOAD:000000C0 dd 3000h ; Physical address LOAD:000000C4 dd 265h ; Size in file image LOAD:000000C8 dd 10268h ; Size in memory image LOAD:000000CC dd 7 ; Flags LOAD:000000D0 dd 1000h ; Alignment
需要添加一个LOAD的程序头,用于存放插桩的代码,所以程序头部分会修改p_filesz和p_memsz,各加0x20。
每个程序头占0x20的空间,表示一个结构,有着地址和大小。
动态符号表的处理
动态符号表也存在因为加了一个程序头而出现偏移的情况,不知道为什么,都是只有_IO_stdin_used需要有地址因此要添加偏移,而别的就不用。
动态符号表之后是字符表,这一部分保持不变。
之后的ELF REL Relocation Table需要添加0x20的偏移。
ELF JMPREL Relocation Table则不用
接下来就是对.text里的代码段进行插桩的实现了。
插桩的实现原理
思想是在jmp类指令之后进行插桩,使用长跳转指令跳到新增加的一个段上去,这样会覆盖5字节的指令,这之后需要恢复这5个字节的内容。
LOAD:00003165 pusha ;两行保存环境
LOAD:00003166 pushf
LOAD:00003167 call $+5 ;call下一个地址,就可以获得当前地址了
LOAD:0000316C pop edi ;将call指令的地址存入edi
LOAD:0000316D sub edi, 316Ch ;计算程序加载基地址
LOAD:00003173 mov edx, 3245h ;找到插桩点的位置
LOAD:00003178 add edx, edi
LOAD:0000317A mov bl, 40h ; '@' ;或的方式进行插桩
LOAD:0000317C mov eax, 321Eh ;存入返回的位置,恢复环境调回去
LOAD:00003181 add eax, edi
LOAD:00003183 jmp loc_3000 ;跳入共通的插桩位置
LOAD:00003000 or byte ptr ds:(_GLOBAL_OFFSET_TABLE_ - 1FF0h)[edx], bl
LOAD:00003002 jmp eax ;eax,
LOAD:0000321E popf ;恢复环境
LOAD:0000321F popa
LOAD:00003220 sub esp, 0Ch ;执行被覆盖掉的指令
LOAD:00003223 push 31h ; '1'
LOAD:00003225 jmp loc_638 ;返回插桩的位置
插桩的思路为:先是保存环境信息,然后找到本桩点对应位置,然后将其置为1,然后恢复运行环境。最后321E的这一段是通用的。
这之后的ELF Initialization Function Table和ELF Dynamic Information的地址也是需要加上0x20的偏移的,即符号表中的部分信息。
再接下来就是插入的信息了,先是代码的部分,
段头的处理
程序的最后面是段头的存储位置,一个段头占用0x28的空间
sh name section名称在字符串Section中的索引
sh_type section种类
sh flags flags
sh_addr section运行时虚拟地址
sh offset 名称在文件中的偏移
sh size 大小
sh_link 链接别的section
sh_info 额外section信息
sh_addralign section 文件对齐
sh_entsize Entry size if section holds table
每个都是4字节
其中,有几种特殊的section段,如下所示
SHN_ABS 0xfff1 该符号包含了一个绝对值,比如表示文件名的符号
SHN_COMMON 0xfff2 表示该符号是一个"COMMON块"的符号,一般来说,未初始化的全局符号定义就是这种类型的。
SHN_UNDEF 0 该符号在本目标文件中被引用到,但是定义在其他目标文件中
其他section中都包含其字符串的开始地址。
这部分在插桩中的影响不大,只需要把有地址的其地址加上0x20的偏移即可
遇到的问题
如果程序代码中有0x56555610 <main+35>: push 0x565556e0
这样的情况产生,该指令的汇编码为68 E0 06 00 00
那么由于产生了偏移,会导致push的值不正确,而这个值很可能就是函数执行的参数。
如这个例子,原先的0x6e0是数据的%d,而之后变成了__libc_csu_fini,导致scanf函数调用失败,就无法正常完成流程了。
.text:000006E0 public __libc_csu_fini
...
.rodata:00000700 db 25h ; %
.rodata:00000701 db 64h ; d
而对于没有pie的程序, 这一部分的参数传递是如下的方式进行的,因而没有产生问题
0x8048505 <main+47>: lea eax,[ebx-0x1a00]
对于没有pie的32位程序
编译命令gcc -o easynopie -no-pie -m32 easy.c
首先先处理.text段,使用capstone来得到指令的大小和地址,以及其指令的反汇编内容
//printf("%d %d %d %d\n",offset,all_insn_[offset].id,all_insn_[offset].size,all_insn_[offset].address);
//printf("%s %s\n",all_insn_[offset].mnemonic,all_insn_[offset].op_str);
处理文件头。由于需要加一个段,所以将段数+1,(e_shnum),然后程序入口点地址会变化,所以要改e_entry,加0x20,是在程序头中添加的。另外e_shoff也要改,需要计算。
在各种数据的计算和处理上也会产生区别,在没有pie的程序的程序上,采用将地址直接从0x8048000提到0x8047000的方式,添加一个页来进行数据的布置,这样能够较好的标出程序原有数据之间的关系而不会出现较多的错误。这样的话相比于有PIE情况的0x20的偏移,没有pie的情况偏移就是0x1000,该方法的实现方案是将第一个Loadable Segment的地址减少0x1000。
然后最后写入文件的时候就是写入文件头,写入程序头,写入插桩后的代码,写入插桩后的新段的代码,写入新的Segment。总的来讲这种方式会相比于有PIE的情况少些错误。
对于有pie的arm程序
前文x86版本的插桩实践
这里来进行一下arm架构程序的插桩实践。
由于arm架构是定长的指令集,所以插桩更易于实现而且效果更好
没有找到arm下不启用pie的方法。。
插桩思路由于arm架构不存在会覆盖其他指令的问题,因而比较简单。主要就是需要解决指令中pc位置的偏移问题。插入的流程还是一样的,找到位置,修改为跳转代码,修改插桩标志、恢复环境、执行原指令,跳转回去。
参考资料
https://blog.csdn.net/king_cpp_py/article/details/80334086
https://blog.csdn.net/weixin_41418994/article/details/83027643