awd pwn——LIEF学习

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_size:ELF文件头部的长度。
identity:ELF的前几个字节的值,用于标识ELF类型。
identity_class:ELF程序的类型。
identity_data:数据表示方式(大端序或小端序)
numberof_sections:section(段)的数量
numberof_segments:segment(节)的数量(一个segment中包含至少一个section)

注:通过命令 objdump -x xxx.elf 也能够输出ELF的信息。

对象输出可以使用 binary.write(filename) 函数实现。

lief.ELF.binary对象也有一些属性可以查看,下面列出较为重要的(其余可在lief/ELF/binary.py中查看)

dtor_functions:析构函数列表。
functions:函数列表。
imagebase:ELF文件的加载基地址,在64位程序下,不开启PIE时Image Base=0x401000,开启后在程序开始执行前动态加载,地址不定。
is_pie:是否开启了PIE。
sections:段列表,迭代器。
segments:节列表,迭代器。
static_symbols:静态符号列表,迭代器。
strings:字符串列表,迭代器。
symbols:所有符号的列表,迭代器。
imported_functions:导入函数列表,即got表中的函数列表。

示例:

#include <stdio.h>

int 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)

# out
                    NULL           
.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.h>

int main(){
    puts("Input a number: ");
    double num;
    scanf("%lf", &num);
    printf("sin(x) = %f", sin(num));
    return 0;
}

脚本,功能是将上述程序中调用sin函数改为调用cos函数,也就是将导出符号中的sin修改为cos即可。这里需要注意如果遍历所有符号,会发现有一个符号是sin,还有一个符号是sin@Glibc_2.2.5,这里最好是将两个都替换一下,否则可能会产生未知的结果:

import lief

if __name__ == '__main__':
    binary: lief.ELF.Binary = lief.ELF.parse('./newvuln')
    header: lief.ELF.Header = binary.header
    for s in binary.imported_symbols:
        if 'sin' in s.name:
            original_name = s.name
            s.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,使其返回x+1,因为hook函数要实现的功能与原函数的功能完全不同,且hook函数无需借助原函数就能够实现目标功能,因此可以直接返回x+1。

不过由于hook不会修改原函数,因此我们需要将hook function插入到原来的ELF中,这需要我们首先对hook函数进行编译生成二进制文件:

gcc -Os -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook

然后使用binary.add函数将这个函数添加到ELF中,并设置hook。

import lief

libm = 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_address

libm.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 1

int 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 lief

crackme = 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库,这样可以在紧张的比赛环境中为我们争取到宝贵的时间。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在运行Python脚本时,我们可以使用AWD(也称为Auto Web Discovery)来轻松地启动和管理Python的Web应用程序。 首先,确保已经安装了Python解释器。在命令行窗口中,可以输入"python --version"来检查Python版本。如果未安装Python,则需要先下载并安装。 接下来,我们需要安装AWD库。可以使用pip命令来安装。在命令行窗口中输入"pip install awd",等待安装完成。 安装完成后,可以通过在命令行窗口中输入"awd init"来初始化AWD。这将在当前目录下创建一个awd.ini文件,其中包含必要的配置信息。 然后,可以创建一个Python脚本,用以编写我们的Web应用程序。例如,可以使用Flask框架来创建一个简单的Web应用程序。在脚本中,我们需要导入Flask库,并定义一个应用对象。可以指定路由和处理函数来处理不同的URL请求。 完成脚本编写后,可以使用"awd run"命令来运行我们的Web应用程序。这将自动启动一个本地服务器,并将Web应用程序绑定到指定的主机和端口上。 在浏览器中输入指定的主机和端口,就可以访问我们的Web应用程序了。例如,如果我们将应用绑定到localhost和5000端口上,可以在浏览器中输入"http://localhost:5000"来访问。 同时,AWD还提供了其他功能,例如自动重新加载和处理静态文件等。这些功能可以在awd.ini文件中进行配置。 总的来说,AWD可以帮助我们更方便地运行和管理Python的Web应用程序,使得开发过程更加高效和便捷。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值