使用电脑浏览效果更佳!
摘要
记录LIEF在CTF中打patch的常用函数,更多详细参考官方文档。 链接,此处记录在CTF方面的应用详细。LIEF不仅仅用于ELF文件的修改,还可以在PE,Mach-o,Android等方面都有应用。按照官方文档的说明,LIEF的目的是提供一个跨平台的库,可以解析,修改和抽象ELF,PE,MachO和Android格式,目前支持的格式有ELF
, PE
, MachO
, DEX
, OAT
, ART
and VDEX
。
在CTF中linux的ELF应用中在于在机器语言级别修改binary的漏洞(patch),下面将介绍常用的LIEF的函数使用,围绕函数API展开接收该函数接收的参数、返回结果、应用代码。
详细
LIEF的体系架构
一个paser把参数目标文件按照对应文件格式进行解释,并提供API对其进行查询,修改。
Linux下ELF文件格式基础
LIEF的编程对应了ELF的文件格式,paser的解释相当于一个readelf的过程,返回一个Binary对象(object)。在CTF的patch中是以链接视角对目标文件进行修改,有关于目标文件格式的链接视角与加载视角可以参阅《程序员的自我修养》,在这里只需知道链接(编译)视角用到的是.section(节),而加载(运行)视角用的是.segment(段),如在binary中hook一个函数就用到增加binary.add(segment),即只使用Binary对象中方法add的一个实现。下面是常用的函数与对象
parser
通常是便写lief脚本patch的开始,parse对ELF按格式解释,返回一个Bianry对象,Binary对象存在函数返回Section、Segment对象。
Binary对象
# binary = lief.parse("./vul") class lief.ELF.Binary # 常用方法 1.add(self: lief.ELF.Binary, segment: LIEF::ELF::Segment, base: int=0) -> LIEF::ELF::Segment Add a segment in the binary(patch usually add segment) 2.get_symbol(self: lief.Binary, symbol_name: str) → LIEF::Symbol Return the Symbol with the given name 3.get_section(self: lief.ELF.Binary, section_name: str) → LIEF::ELF::Section Return the Section with the given name 4.patch_address(self: lief.Binary, address: int, patch_value: List[int], va_type: lief.Binary.VA_TYPES=VA_TYPES.AUTO) -> None Virtual address is specified in the first argument and the content in the second (as a list of bytes). If the underlying binary is a PE, one can specify if the virtual address is a RVA or a VA. By default it is set to AUTO 5.patch_pltgot(self: lief.ELF.Binary, symbol_name: str, address: int) -> None Patch the imported symbol’s name with the address 6.write(self: lief.ELF.Binary, output: str) → None Rebuild the binary and write it in a file
Segment对象
class lief.ELF.Segment property content Segment’s raw data property virtual_address Address where the segment will be mapped
Section对象
class lief.ELF.Section property content section raw content property name section name property virtual_address Return address where the section will be mapped in memory
Symbol对象
class lief.ELF.Symbol(self: lief.ELF.Symbol) property name symbol name property value This member have slightly different interpretations In relocatable files, value holds alignment constraints for a symbol whose section index is SHN_COMMON. In relocatable files, value holds a section offset for a defined symbol. That is, value is an offset from the beginning of the section associated with this symbol. In executable and shared object files, value holds a virtual address. To make these files’ssymbols more useful for the dynamic linker, the section offset (file interpretation) gives way to a virtual address (memory interpretation) for which the section number is irrelevant.
应用
dmeo准备
源码文件
#include
int main(){ printf("hello world\n"); puts("test of lief"); printf("hhhhhh"); return 0; } 编译成二进制文件
gcc -o vulner vulner.c
编译hook
通过手写A&T格式的汇编指令文件,实现对vulner中函数的逻辑修改,包括重写函数,添加逻辑处理等。编译成位置无关的无其他库函数链接的第三方patch二进制文件。
对于call、jmp的指令地址跳转根据生成的指令长度使用nop进行占位,如call指令占5 bytes,对于jmp有近跳转、短跳转、远跳转之分,通常是2bytes或者5bytes。
汇编指令的编写要符合A&T格式要求:如立即数不能直接赋值段寄存器等。
void myprintf(){ asm( "mov $0xa6e75736e6f6874,%rsi\n" "mov $0x0,%rdi\n" "mov $0x8,%rdx\n" "mov $0x1,%rax\n" "syscall" ); }
编译:
gcc -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook
通过增加segment进行patch
hook指定地址函数调用
对原binary中一个call func进行修改,改变该call func的行为(只改变这次的调用,后面其他的call func没有被改变,因为只指定这次地址处的函数修改hook)
# coding:utf-8
import lief
from pwn import *
def patch_call(file,srcaddr,dstaddr,arch = "amd64"):
print hex(dstaddr)
length = p32((dstaddr - (srcaddr + 5 )) & 0xffffffff) # dst-(src+5)的一个补码call形式
order = '\xe8'+length
print disasm(order,arch=arch)
file.patch_address(srcaddr,[ord(i) for i in order])
binary = lief.parse("./vulner") # gcc -o vulner vulner.c
hook = lief.parse('./hook') # gcc -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook
# inject hook program to binary
segment_added = binary.add(hook.segments[0])
hook_fun = hook.get_symbol("myprintf")
dstaddr = segment_added.virtual_address + hook_fun.value # 计算添加后该函数在binary的地址(相当于一个reloacation过程)
srcaddr = 0x40056f # 指定修改的地址
patch_call(binary,srcaddr,dstaddr)
binary.write('vulner.patched')
hook全部该函数的调用
对原binary的全部调用函数进行修改,即通过改写plt的地址
# coding:utf-8
import lief
binary = lief.parse("./vulner")
hook = lief.parse('./hook')
# inject hook program to binary
segment_added = binary.add(hook.segments[0])
# hook got
my_printf = hook.get_symbol("myprintf")
my_printf_addr = segment_added.virtual_address + my_printf.value # 加段后映射的基址 + 函数的偏移
binary.patch_pltgot('printf', my_printf_addr)
binary.write('vulner_patch_plt')
通过改写eh_frame段内容
上面通过增加section使得binary的变化很大,我们可以通过把代码写入eh_frame中(覆盖其原来的内容)可以实现binary大小基本没有变化。有关于.eh_frame的作用 详情,这里只需知道若存在该段,我们能够进行改写并无影响。
# coding:utf-8
import lief
from pwn import *
def patch_call(file,srcaddr,dstaddr,arch = "amd64"):
print hex(dstaddr)
length = p32((dstaddr - (srcaddr + 5 )) & 0xffffffff)
order = '\xe8'+length
print disasm(order,arch=arch)
file.patch_address(srcaddr,[ord(i) for i in order])
binary = lief.parse("./vulner")
hook = lief.parse('./hook')
# write hook's .text content to binary's .eh_frame content
sec_ehrame = binary.get_section('.eh_frame')
print sec_ehrame.content
sec_text = hook.get_section('.text')
print sec_text.content
sec_ehrame.content = sec_text.content
print binary.get_section('.eh_frame').content
# hook target call
dstaddr = sec_ehrame.virtual_address
srcaddr = 0x400584
patch_call(binary,srcaddr,dstaddr)
binary.write('vulner_patched_ehframe')
上述对vuler的修改运行如图所示:
三者对原来binary的改动变化:
可以看到对原有的.eh_frame的修改改动最小。(通常CTF中大小不变,此处是因为hook.c编写的是一个函数,.text段内容包括初始化等代码,大小超过了vulner的.eh_frame,所以变大,但是我们可以不用写一个my_printf函数直接在hook.c写asm()那部分是通常的修改.eh_frame的做法。)
补充:Reloacation file 与 Executable file、Shared Object File的区别
The file header Type field tells us what the purpose of the file is. There are a few common file types.(readelf -h binary)
CORE (value 4)
DYN (Shared object file), for libraries (value 3)
EXEC (Executable file), for binaries (value 2)
REL (Relocatable file), before linked into an executable file (value 1)
Relocation entries for different object files have slightly different interpretations for the
r_offset
member.
- In relocatable files,
r_offset
holds a section offset. That is, the relocation section itself describes how to modify another section in the file; relocation offsets designate a storage unit within the second section.- In executable and shared object files,
r_offset
holds a virtual address. To make these files’ relocation entries more useful for the dynamic linker, the section offset (file interpretation) gives way to a virtual address (memory interpretation).
Relocatable files are still fully relocatable, whereas shared objects are one step further along the linking process and have been largely relocated. Shared objects are only relocatable if their code is position-independent (e.g. it was built with GCC’s -fPIC
option).
Relocatalbe file是只编译还没有链接的文件:
gcc -c -o xxx xxx.c
里面的符号只是对应一个section基址的offset
Executable file是经过编译、链接的文件,里面的符号(变量、函数)是经过重定位之后的虚拟内存地址
Shared object file只有编译时带 -PIC的选项才保存的是一个对应一个section基址的offset
这就可以解释在LIEF给elf binary(executable file)增加一个段之后,call funtion_address的函数地址计算