ble55ing的技术专栏 code analysis ,fuzzing technique and ctf

二进制插桩方式探究-x86

2019-07-09
ble55ing

elf

二进制程序插桩实践-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

https://cloud.tencent.com/developer/news/244988


Similar Posts

上一篇 arm架构初探

下一篇 cybricsctf Tone

Content