LIEF
分析和操作格式
这部分教程的目的是概述LIEF的API用来分析处理文件的格式
ELF
- 我们从ELF格式开始。要从一个文件创建一个ELF.Binary,我们只需朝lief.parse()或lief.ELF.parse()函数传入他的路径。
>
注意:如果使用的是python的API,那么lief.parse()和lief.ELF.parse()具有相同的行为。但是在C++中,LIEF::Parser::parse()将返回指向LIEF::Binary对象的指针,而LIEF::ELF::Parser::parse()将返回LIEF::ELF::**
1 | import lief |
一旦ELF文件被解析,我们可以访问它的Header
header = binary.header
我们还可以更改它的入口点和目标架构(ARCH)
1 | header.entrypoint = 0x123 |
并重建这个文件binary.write("ls.modified")
我们也可以遍历这个二进制文件的段部分
1
2
3
4for section in sections:
print section.name
print section.size
print len(section.content)也可以修改它的
.text部分1
2text = binary.get_section(".text")
text.content = bytes([0x33] * text.size)
玩转ELF符号
在本教程中,我们将会介绍如何修改二进制及库中的动态符号
当二进制文件将要链接到库的时候,所需要用到的库储存在动态表的DT_NEEDED条目中,所需要的功能在表中注册并具有以下属性:
value设置为0种类设置为FUNC
类似的,当一个库导出函数时,它在动态表中有一个DT_SONAME条目,导出的函数在动态符号表中注册,并具有如下属性:value设置为库中函数地址type设置为FUNC
而导入导出函数由LIEF来抽象,因此你可以使用exported_functions和imported_functions来遍历这些元素1
2
3
4
5import lief
binary = lief.parse("/usr/bin/ls")
library = lief.parse("/usr/lib/libc.so.6")
print(binary.imported_functions)
print(library.exported_functions)
在分析二进制文件时,导入的函数名称对逆向工程非常有用。 一个解决方案是静态链接二进制文件和库。 另一个解决方案是通过交换这些符号来打击逆转者的思维。比如以下代码:
1 | #include <stdio.h> |
这里基本上是让这个程序接受一个整数作为参数,并对这个值进行一些计算。1
2$ hasme 123
228886645.836282

该pow和log功能都位于libm.so.6库中。使用LIEF的一个有趣的技巧是将此函数名称与其他函数名称交换。在本教程中,我们将交换cos和sin功能。首先,我们必须加载库和二进制文件:1
2
3
4#!/ usr / bin / env python3
import lief
hasme = lief 。解析(“hasme” )
libm = lief 。解析(“/usr/lib/libm.so.6” )
然后在更改二进制中的两个导入函数的名称时:1
2
3
4hashme_pow_sym = next(filter(lambda e : e.name == "pow", my_binary.imported_symbols))
hashme_log_sym = next(filter(lambda e : e.name == "log", my_binary.imported_symbols))
hashme_pow_sym.name = "cos"
hashme_log_sym.name = "sin"
最后我们在库中用log交换sin,用pow交换cos,然后重构两个对象
1 | #!/usr/bin/env python3 |

有了这个脚本,我们libm在当前目录下建立了一个修改,我们必须强制Linux加载器在执行时使用这个binary.obf。为此,我们导出LD_LIBRARY_PATH到当前目录:1
2$ LD_LIBRARY_PATH=. hashme.obf 123
228886645.836282
如果我们忽略它,它会使用默认libm和哈希计算完成sin和cos:1
2$ hashme.obf 123
-0.557978
一个真正的用例可能是在像OpenSSL这样的密码库中交换符号。例如EVP_DecryptInit,EVP_EncryptInit有相同的原型,所以我们可以交换它们。
ELF挂钩
本教程的目标是钩住一个库函数
在前面的教程中,我们看到了如何从共享库中交换符号名称,现在我们将看到在共享库中挂钩函数的机制。
目标库是标准的数学库(libm.so),我们将在exp函数中插入一个钩子,使得\(exp(x)= x + 1 \)。下面的清单给出了使用这个函数的样例的源代码:
1 | #include <stdio.h> |
挂钩功能如下:1
2
3double hook(double x) {
return x + 1;
}
编译gcc -Os -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook
为了将这个钩子注入到库中,我们使用add()(段)方法Binary.add(*args, **kwargs)
重载函数
1 | 1.add(self: _pylief.ELF.Binary, arg0: LIEF::ELF::DynamicEntry) -> LIEF::ELF::DynamicEntry |
将给的Section添加到二进制文件中。
如果该部分不应加载到内存中,loaded参数必须设置为False(默认值:True)1
3.add(self:_pylief.ELF.Binary,segment:LIEF :: ELF :: Segment,base:int = 0) - > LIEF :: ELF :: Segment
在二进制文件中添加一个段1
4.add(self: _pylief.ELF.Binary, note: LIEF::ELF::Note) -> LIEF::ELF::Note
在二进制文件中添加一个行的Note
一旦存根被注入,我们只需要改变exp符号的地址:1
2
3
4exp_symbol = libm.get_symbol("exp")
hook_symbol = hook.get_symbol("hook")
exp_symbol.value = segment_added.virtual_address + hook_symbol.value
测试修补过的库:1
2
3
4./do_math.bin 1
exp(1) = 2.718282
LD_LIBRARY_PATH=. ./do_math.bin 1
exp(1) = 2.000000
感染plt / got
本教程的目标是在ELF二进制文件中挂接导入的函数。
通过感染.got部分挂钩导入的函数是一个众所周知的技术[1] [2],本教程将重点介绍使用LIEF的实现。
这些数字说明了这个plt/got机制:
使用延迟绑定,第一次调用该函数时,该got条目将重定向到plt指令。
第二次,got条目在共享库中保存地址
基本上感染分两步完成:
- 首先,我们注入我们的钩子
- 其次,我们通过打补丁将目标函数重定向到我们的钩子
got
可以用下图来总结:
作为例子,我们将使用一个基本的crackme在memcmp(3)上的Flag和用户的输入。
1 | #include <stdio.h> |
这个Flag的值和0x5c进行了xor操作,为了验证crackme,用户必须输入Damn_YoU_Got_The_Flag:1
2
3
4$ crackme.bin foo
Wrong
$ crackme.bin Damn_YoU_Got_The_Flag
You got it !!
挂钩将包含打印参数memcmp并返回0:
1 | #include "arch/x86_64/syscall.c" |
由于购置将被注入Creakme,因此它必须具备以下要求:
- 汇编代码必须是位置独立的(使用
-fPIC或-pie/-fPIE标记编译) - 不要使用外部库比如
libc.so(标志)-nostdlib -nodefaultlibs(标志)
基于要求,这个钩子的编译为:gcc -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook
注入钩子
第一步是将钩子注入二进制。为此我们将添加一个’Segment’:
1 | import lief |
钩子的所有汇编代码都存在hook的第一段的LOAD中。
一旦钩子被添加,钩子的虚拟地址是segment_added的虚拟地址virtual_address,我们可以用got修补。
修补got
LIEF提供了一个功能,可以轻松修补got与Symbol相关的条目:Binary.patch_pltgot(* args,** kwargs )(重载函数)1.patch_pltgot(self: _pylief.ELF.Binary, symbol_name: str, address: int) -> None
用导入的符号名称修补 address2.patch_pltgot(self: _pylief.ELF.Binary, symbol: LIEF::ELF::Symbol, address: int) -> None
修复导入的Symbol和addressmemcmp函数的偏移量存储在value关联的动态符号的属性中。因此,它的虚拟地址将是:
my_memcpy= value + segment_added.virtual_address1
2my_memcmp = hook.get_symbol("my_memcmp")
my_memcmp_addr = segment_added.virtual_address + my_memcmp.value
最后我们可以用memcmp的值来修复这个crackme.crackme.patch_pltgot('memcmp', my_memcmp_addr)
最后重建他crackme.write("crackme.hooked")
###运行
由于在检查标志值之前检查输入大小,我们必须提供正确长度的输入(不管其内容):1
2
3
4
5$ crackme.hooked XXXXXXXXXXXXXXXXXXXXX
Hook add
Damn_YoU_Got_The_Flag
XXXXXXXXXXXXXXXXXXXXX
You got it !!