找索引擎seo,佛山网站快速优化排名,当地的网站建设,长春网站制作价格文章目录1. 什么是LIEF2. 加载可执行文件3. 修改ELF的symbols4. ELF Hooking5. 修改got表6. 总结1. 什么是LIEF
LIEF是一个能够用于对各种类型的可执行文件#xff08;包括Linux ELF文件、Windows exe文件、Android Dex文件等#xff09;进行转换、提取、修改的项目#xf…
文章目录1. 什么是LIEF2. 加载可执行文件3. 修改ELF的symbols4. ELF Hooking5. 修改got表6. 总结1. 什么是LIEF
LIEF是一个能够用于对各种类型的可执行文件包括Linux ELF文件、Windows exe文件、Android Dex文件等进行转换、提取、修改的项目能够在Python、C和C语言中调用其API进行简单便捷的可执行文件相关操作。
在AWD pwn中我们常常需要对官方给出的ELF文件进行修补称为Patch当不能简单地通过在IDA中修改指令的方式patch时就需要使用LIEF工具完成patch。
本文通过LIEF的官方文档对LIEF的python包进行学习与解释并通过实例演示我们应该如何在AWD pwn中利用LIEF进行patch。
LIEF的安装方法与一般python包相同apt install lief
文档地址文档
2. 加载可执行文件
在python中可以通过调用 lief.parse() 函数加载一个可执行文件。如果输入的文件为ELF文件则返回的对象为lief.ELF.Binary。
加载完成之后假设返回的对象赋值给名为binary的变量其有一个header字段保存有该ELF文件的一些相关信息包括程序的入口地址binary.header.entrypoint、程序运行的机器类型binary.header.machine_type这两个值都是可以直接修改的如果将machine_type从ELF修改为exe那么后续将这个对象输出产生的可执行文件就是exe格式。通过对lief项目的python源码进行查看可知header中还保存有以下ELF文件的属性列出部分较为重要的其余可在lief/ELF/Header.py中查看 file_type文件类型表示该文件是一个可执行文件还是库文件还是其他文件。 header_sizeELF文件头部的长度。 identityELF的前几个字节的值用于标识ELF类型。 identity_classELF程序的类型。 identity_data数据表示方式大端序或小端序 numberof_sectionssection段的数量 numberof_segmentssegment节的数量一个segment中包含至少一个section 注通过命令 objdump -x xxx.elf 也能够输出ELF的信息。
对象输出可以使用 binary.write(filename) 函数实现。
lief.ELF.binary对象也有一些属性可以查看下面列出较为重要的其余可在lief/ELF/binary.py中查看 dtor_functions析构函数列表。 functions函数列表。 imagebaseELF文件的加载基地址在64位程序下不开启PIE时Image Base0x401000开启后在程序开始执行前动态加载地址不定。 is_pie是否开启了PIE。 sections段列表迭代器。 segments节列表迭代器。 static_symbols静态符号列表迭代器。 strings字符串列表迭代器。 symbols所有符号的列表迭代器。 imported_functions导入函数列表即got表中的函数列表。 示例
#include stdio.hint main(){puts(Please input your name: );char name[0x10];scanf(%s, name);puts(Hello, );printf(name);return 0;
}jupyter运行结果
# in
import lief
binary: lief.ELF.Binary lief.ELF.parse(./vuln)
header: lief.ELF.Header binary.header# in
print(hex(binary.entrypoint))
print(hex(header.entrypoint))# out
0x1080 # _start的地址
0x1080# in
print(header.numberof_sections)
print(header.numberof_segments)# out
31
13# in
for f in binary.functions:print(f)# out
_init - - -
_start -
deregister_tm_clones -
register_tm_clones -
__do_global_dtors... -
frame_dummy -
main -
_fini -# in
for s in binary.sections:print(s)# outNULL
.interp PROGBITS
.note.gnu.property NOTE
.note.gnu.build-id NOTE
.note.ABI-tag NOTE
.gnu.hash GNU_HASH
.dynsym DYNSYM
.dynstr STRTAB
.gnu.version HIOS
.gnu.version_r GNU_VERNEED
.rela.dyn RELA
.rela.plt RELA
.init PROGBITS
.plt PROGBITS
.plt.got PROGBITS
.text PROGBITS
.fini PROGBITS
.rodata PROGBITS
.eh_frame_hdr PROGBITS
.eh_frame PROGBITS
.init_array INIT_ARRAY
.fini_array FINI_ARRAY
.dynamic DYNAMIC
.got PROGBITS
.got.plt PROGBITS
.data PROGBITS
.bss NOBITS
.comment PROGBITS
.symtab SYMTAB
.strtab STRTAB
.shstrtab STRTAB# in
for s in binary.segments:print(s)# out
PHDR r--
INTERP r--
LOAD r--
LOAD r-x
LOAD r--
LOAD rw-
DYNAMIC rw-
NOTE r--
NOTE r--
GNU_PROPERTY r--
GNU_EH_FRAME r--
GNU_STACK rw-
GNU_RELRO r--3. 修改ELF的symbols
通过binary.import_functions可以获取所有导入函数的列表通过binary.import_symbols可以获取所有导入符号的列表。经过实际测试发现只有import_symbols可以修改成功而import_functions无法修改成功原因未知。
修改ELF的symbols很简单只需要遍历所有的symbols找到你想要修改的symbols修改其name为新的字符串即可。经过IDA反编译发现修改后的输出与原ELF程序相比在结构上稍有不同但不影响执行。
示例程序
#include stdio.h
#include math.hint main(){puts(Input a number: );double num;scanf(%lf, num);printf(sin(x) %f, sin(num));return 0;
}脚本功能是将上述程序中调用sin函数改为调用cos函数也就是将导出符号中的sin修改为cos即可。这里需要注意如果遍历所有符号会发现有一个符号是sin还有一个符号是sinGlibc_2.2.5这里最好是将两个都替换一下否则可能会产生未知的结果
import liefif __name__ __main__:binary: lief.ELF.Binary lief.ELF.parse(./newvuln)header: lief.ELF.Header binary.headerfor s in binary.imported_symbols:if sin in s.name:original_name s.names.name s.name.replace(sin, cos)print(original_name - s.name)else:print(s.name)binary.write(./newvuln)执行脚本后执行两个可执行文件就会发现输出的结果已经发生了改变。
在官方文档中给出的实例是修改libm.so.6即math库的符号将两个数学计算的函数的名称换了一下这样当程序加载输出的新的libm.so.6时使用这两个函数计算就会产生看似不合理的结果。这种替换可以误导攻击者。
4. ELF Hooking
在AWD pwn中ELF hooking是一种常用的patch方法hook的意思是我们写一个新的函数然后让原ELF执行某个函数时实际上执行这个函数。通过ELF hooking我们可以有效修复很多的栈溢出漏洞和格式化字符串漏洞对于read函数造成的栈溢出漏洞可以首先在hook function中判断缓冲区的大小和输入长度的大小如果有溢出风险则修改输入长度大小为不大于缓冲区长度。
实际上hook函数替换了原来的函数如果在hook函数中没有调用原来的函数那么原来的函数就相当于永远都不会被调用。
这里使用官方文档中的示例理解。文档中实现的功能是hook一个数学函数exp(x)e^x使其返回x1因为hook函数要实现的功能与原函数的功能完全不同且hook函数无需借助原函数就能够实现目标功能因此可以直接返回x1。
不过由于hook不会修改原函数因此我们需要将hook function插入到原来的ELF中这需要我们首先对hook函数进行编译生成二进制文件
gcc -Os -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook
然后使用binary.add函数将这个函数添加到ELF中并设置hook。
import lieflibm lief.parse(/usr/lib/libm.so.6)
hook lief.parse(hook)exp_symbol libm.get_symbol(exp)
hook_symbol hook.get_symbol(hook)code_segment hook.segment_from_virtual_address(hook_symbol.value)
segment_added libm.add(code_segment)new_address segment_added.virtual_address hook_symbol.value - code_segment.virtual_address
exp_symbol.value new_addresslibm.write(libm.so.6)分析一下上面的脚本get_symbol函数返回lief.Symbol对象实例其value属性对应该符号的地址。segment_from_virtual_address函数可通过传入的地址获取该地址所在的节lief.Segment对象实例。下面在libm中添加这个节然后修改exp函数的地址到hook function的地址。
5. 修改got表
lief还能够通过修改got表实现对导入函数的替换。
与ELF hooking相同我们同样需要创建一个函数用于替换导入函数。注意我们新创建的函数要写在一个单独的文件中且编译时一定要加上开启PIE选项-fPIC且不使用任何外部库文件。如果需要进行输入输出可以通过系统调用实现包含arch/x86_64/syscall.c函数即可。
通过binary.patch_gotplt函数能够实现对got表的替换。还是使用文档中的例子进行说明。
文档模拟了一个逆向题假设现在要求用户输入一段字符串作为密码只有密码正确才能进行后续关键操作。为了安全程序会将用户的输入加密然后与程序中保存的固定的密文进行比较。将用户密码与程序中保存的密文进行比较的函数为memcpy函数如果程序是通过解密内含密文与用户输入的明文进行比较那么memcpy在比较时就是明文与明文的比较使用got表替换操作让比较的双方输出即可获取正确的密码。
// compile: gcc -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook
#include arch/x86_64/syscall.c
#define stdout 1int my_memcmp(const void* lhs, const void* rhs, int n) {const char msg[] Hook memcmp\n;_write(stdout, msg, sizeof(msg));_write(stdout, (const char*)lhs, n);_write(stdout, \n, 2);_write(stdout, (const char*)rhs, n);_write(stdout, \n, 2);return 0;
}import liefcrackme lief.parse(crackme.bin)
hook lief.parse(hook)segment_added crackme.add(hook.segments[0])
my_memcmp hook.get_symbol(my_memcmp)
my_memcmp_addr segment_added.virtual_address my_memcmp.value
crackme.patch_pltgot(memcmp, my_memcmp_addr)
crackme.write(crackme.hooked)可以看到这里将memcpy函数的调用替换为自定义的函数。
6. 总结
本文介绍了python库LIEF在awd pwn中的运用方式。虽然其能够很方便地对程序进行修改但还有一个很重要的操作其无法完成——直接修改汇编代码。想象这样的情景官方提供了一个程序这个程序有对栈缓冲区的输入操作也有对堆缓冲区的输入操作。假设两者都使用了read函数那么输入的长度就会明确提供给被调用方。考虑到堆中chunk头部有size字段可以通过获取chunk的size字段来确定输入的最大长度从而发现可能的缓冲区溢出风险但栈缓冲区中并没有这样的size字段因此可能需要写两个函数用于对read函数的替换。此时对于不同的read函数调用指令我们可能需要将调用的目标地址指向两个不同的地址这就不再LIEF的业务范围之内了。不过幸运的是我们还可以使用功能更为强大的angr进行进一步的处理。不过考虑到LIEF便捷的API当patch使用LIEF就可以完成时我们也就不必使用angr库这样可以在紧张的比赛环境中为我们争取到宝贵的时间。