首页
论坛
专栏
课程
3

[翻译]可执行文件操作工具——LIEF使用教程(一)

梦野间 2018-9-6 21:22 2387

1 - 解析和处理格式

本教程的目的是对LIEF中解析和处理格式的API进行一个总体的介绍

ELF

首先从ELF格式开始。要创建一个ELF.Binary对象,我们只需将ELF文件的路径传给lief.parse()或是lief.ELF.parse()函数。

 

注意:在Python API中,这两个函数的功能是相同的,但在C++的API中,LIEF:Parser:parse()返回值是一个指向LIEF::Binary对象的指针,而LIEF::ELF::Parser::parse()返回的是一个LIEF::ELF::Binary对象。

import lief
binary = lief.parse("/bin/ls")

在解析完ELF后,就可以访问它的Header:
header = binary.header
修改入口点和目标架构(ARCH)。

header.entrypoint = 0x123
header.machine_type = lief.ELF.ARCH.AARCH64

然后重新生成它。

binary.write("ls.modified")

我们也可以像下面这样对Section进行遍历:

for section in binary.sections:
  print(section.name) # section's name
  print(section.size) # section's size
  print(len(section.content)) # Should match the previous print

修改.text区段的内容:

text = binary.get_section(".text")
text.content = bytes([0x33] * text.size)

PE

ELF一样,可以用lief.parse()lief.PE.parse()函数创建PE.Binary

import lief
binary = lief.parse("C:\\Windows\\explorer.exe")

要访问不同的PE头(DosHeaderHeaderOptionalHeader):

print(binary.dos_header)
print(binary.header)
print(binary.optional_header)

你可以用两种方法访问导入函数。其中比较抽象的方法是使用LIEF的抽象层。

for func in binary.imported_functions:
  print(func)

如果你想要使用精度更细的方法以确定函数在哪个动态库中,或是访问PE导入表中的其它字段:

for imported_library in binary.imports:
  print("Library name: " + imported_library.name)
  for func in imported_library.entries:
    if not func.is_ordinal:
      print(func.name)
    print(func.iat_address)

LIEF可以修改ImportImportEntry的所有属性,但是要进行这个操作的话,必须先配置Builder

builder = lief.PE.Builder(binary)
builder.build_imports(True)
builder.patch_imports(True)

builder.build()
builder.write("result.exe")

2 - 从零开始创建一个PE

在这篇教程中,我们将学习怎么从零开始创建一个PE可执行文件。

 

脚本和资源可以在这里找到:materials


 

LIEF支持从零开始创建一个简单的PE文件,在这篇文章会介绍怎么创建一个可以用MessageBoxA弹出"Hello Word"的PE文件。

 

首先需要构造一个Binary:

from lief import PE

binary32 = PE.Binary("pe_from_scratch", PE.PE_TYPE.PE32)

第一个参数是二进制文件的名字,第二个参数是PE文件的类型:PE32或是PE64(见PE_TYPE)。Binary的构造器可以自动创建DosHeaderHeaderOptionalHeader以及一个空的DataDirectory

 

现在我们已经有了一个最小的二进制程序,我们需要添加区段。第一个区段(.text)用来保存汇编代码,第二个区段用来保存字符串(.data):

section_text                 = PE.Section(".text")
section_text.content         = code
section_text.virtual_address = 0x1000

section_data                 = PE.Section(".data")
section_data.content         = data
section_data.virtual_address = 0x2000

MessageBoxA由title和message组成。这两个字符串将存储在.data段中:

title   = "LIEF is awesome\0"
message = "Hello World\0"

data =  list(map(ord, title))
data += list(map(ord, message))

.text段的伪汇编代码如下:

push 0x00              ; uType
push "LIEF is awesome" ; Title
push "Hello World"     ; Message
push 0                 ; hWnd
call MessageBoxA       ;
push 0                 ; uExitCode
call ExitProcess       ;

事实上我们push的不是字符串,而是字符串所在的虚拟地址。在PE格式中,虚拟地址表示的是相对虚拟地址(如果ASLR不开启的话,是相对于Optional.imagebase)。Binary构造器会默认把imagebase设为0x400000
字符串的虚拟地址计算如下的如下:

  • title:imagebase+virtual_address+0=0x402000
  • message:imagebase+virtual_address+len(title)=0x402010
push 0x00              ; uType
push 0x402000          ; Title
push 0x402010          ; Message
push 0                 ; hWnd
call MessageBoxA       ;
push 0                 ; uExitCode
call ExitProcess       ;

因为代码中使用了MessageBoxA,我们需要将user32.dll放进Imports表中,并将MessageBoxA放进ImportEntry中。我们可以使用add_library()add_entry()来实现这一操作。

user32 = binary32.add_library("user32.dll")
user32.add_entry("MessageBoxA")

ExitProcess(kernel32.dll)的导入也是这样操作:

kernel32 = binary32.add_library("kernel32.dll")
kernel32.add_entry("ExitProcess")

在动态库和函数导入进来之后,我们需要确定它们的地址(Import Address Table)。

 

要实现这个功能可以使用predict_funciton_rva()方法,它会返回由Builder设置的IAT地址:

 

Binary.predict_function_rva(self: _pylief.PE.Binary, library: str, function: str) → int

 

尝试预测在指定动态库中某函数的RVA

ExitProcess_addr = binary32.predict_function_rva("kernel32.dll", "ExitProcess")
MessageBoxA_addr = binary32.predict_function_rva("user32.dll", "MessageBoxA")
print("Address of 'ExitProcess': 0x{:06x} ".format(ExitProcess_addr))
print("Address of 'MessageBoxA': 0x{:06x} ".format(MessageBoxA_addr))
Address of 'ExitProcess': 0x00304c
Address of 'MessageBoxA': 0x003054

MessageBoxAExitProcess的绝对虚拟地址是:

  • MessageBoxA: imagebase + 0x306a = 0x40306a
  • ExitProcess: imagebase + 0x305c = 0x40305c

相关的汇编代码为:

push 0x00              ; uType
push 0x402000          ; Title
push 0x402010          ; Message
push 0                 ; hWnd
call 0x40306a          ;
push 0                 ; uExitCode
call 0x40305c          ;

Binary对象转化为可执行文件的操作是由Builder类来实现的。

 

默认情况下,导入表不会被重建,所以我们需要手动配置:

builder = lief.PE.Builder(binary32)
builder.build_imports(True)
builder.build()
builder.write("pe_from_scratch.exe")

现在你可以试试这个新创建的二进制文件了。

3 - 玩转ELF符号

在这篇教程我们将学习如何在可执行文件和动态库中修改动态符号。

 

脚本和资源可以在这里找到:materials


 

当一个动态库被链接到可执行文件中,这个动态库会被存储到动态表的DT_NEEDED条目中,所需要的函数会在动态符号表中以下列属性注册:

  • value设为0
  • type设为FUNC

类似的,如果一个库要导出函数,就会在动态表中添加一条DT_SONAME条目,导出的函数会在动态符号表中以下列属性注册:

  • value设为库中函数的地址
  • type设为FUNC
    在LIEF中,已经对导入和导出函数进行了抽象,因此你可以使用exported_functionsimported_functions进行枚举。
import lief
binary  = lief.parse("/usr/bin/ls")
library = lief.parse("/usr/lib/libc.so.6")

print(binary.imported_functions)
print(library.exported_functions)

当逆向分析一个程序时,导入函数名是非常有用的。要防止这种逆向分析,一种做法是将二进制程序和库静态链接起来(译者注:静态链接之后导入表中就不再需要导入函数名,对函数的调用直接通过地址进行)。另一种做法是把某些函数名进行交换,以迷惑逆向工程师。

 

看下列代码:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

double hashme(double input) {
  return pow(input, 4) + log(input + 3);
}

int main(int argc, char** argv) {
  if (argc != 2) {
    printf("Usage: %s N\n", argv[0]);
    return EXIT_FAILURE;
  }

  double N = (double)atoi(argv[1]);
  double hash = hashme(N);
  printf("%f\n", hash);

  return EXIT_SUCCESS;
}

这个程序的功能基本上就是接收一个整数,并对这个值进行计算。

$ hasme 123
228886645.836282

图片描述

 

powlog函数位于libm.so.6这个库中。一个有趣的小技巧就是用LIEF把当前的函数名和另一个函数名进行互换。在这篇文章中,我们把它们分别替换为cossin函数。

 

首先需要载入可执行文件和动态库:

#!/usr/bin/env python3
import lief


hasme = lief.parse("hasme")
libm  = lief.parse("/usr/lib/libm.so.6")

接下来我们需要改变binary中两个导入函数的函数名

hashme_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,并重建这两个对象:

#!/usr/bin/env python3
import lief


hasme = lief.parse("hasme")
libm  = lief.parse("/usr/lib/libm.so.6")


def swap(obj, a, b):
    symbol_a = next(filter(lambda e : e.name == a, obj.dynamic_symbols))
    symbol_b = next(filter(lambda e : e.name == b, obj.dynamic_symbols))
    b_name = symbol_b.name
    symbol_b.name = symbol_a.name
    symbol_a.name = b_name

hashme_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"


swap(libm, "log", "sin")
swap(libm, "pow", "cos")

hashme.write("hashme.obf")
libm.write("libm.so.6")

图片描述

 

在这个脚本中,我们在当前目录下构建了一个修改过后的libm,接下来需要在执行binary.obf的时候强制让Linux加载器加载我们修改过后的这个库。要实现这个功能,我们需要把LD_LIBRARY_PATH导出到当前目录:

$ LD_LIBRARY_PATH=. hashme.obf 123
228886645.836282

(译者注:修改过后的动态库中cos对应的地址是pow函数的地址,sin的对应地址是log函数的地址,而修改后的binary中调用的分别是cos和sin函数。调用的是cos函数,而实际执行的是pow函数的功能,这就会对逆向分析人员造成困扰)

 

如果不这样做,它就会使用默认的libm中的sincos来做hash计算。

 

这个功能的一个实际应用是在加密库OpenSSL中交换两个符号。比如,EVP_DecryptInitEVP_EncryptInit拥有相同的原型,我们可以交换它们。

 

原文链接:https://lief.quarkslab.com/doc/latest/tutorials/01_play_with_formats.html
编译:看雪翻译小组 梦野间
校对:看雪翻译小组 lumou



快讯:[看雪招聘]十八年来,看雪平台输出了大量安全人才,影响三代安全人才!

最后于 6天前 被梦野间编辑 ,原因:
最新回复 (2)
哆啦咪 2018-9-10 15:09
2
感谢分享!
爱吃菠菜 2018-9-16 22:47
3
牛X的工具
返回