; tiny.asm
BITS 32
org 0x00010000
db 0x7F, "ELF" ; e_ident
dd 1 ; p_type
dd 0 ; p_offset
dd $$ ; p_vaddr
dw 2 ; e_type ; p_paddr
dw 3 ; e_machine
dd _start ; e_version ; p_filesz
dd _start ; e_entry ; p_memsz
dd 4 ; e_phoff ; p_flags
_start:
mov bl, 42 ; e_shoff ; p_align
xor eax, eax
inc eax ; e_flags
int 0x80
db 0
dw 0x34 ; e_ehsize
dw 0x20 ; e_phentsize
db 1 ; e_phnum
; e_shentsize
; e_shnum
; e_shstrndx
filesize equ $ - $$ ; tiny.asm
要使用
nasm -f bin -o tiny nasm.asm;chmod +x tiny
进行二进制编译。它本身可以执行,是一个微小的怪物。它比ELF标头小,但包含ELF标头,程序标头和程序代码-但Linux(至少在我的64 Debian上)运行它。 我希望能够调试具有(有意或无意)损坏/不正确的elf标头的此类文件。有修复精灵标题的工具吗?
我尝试过的调试程序是获取入口点:
readelf -h tiny
,但readelf
甚至拒绝查看文件:readelf: Error: tiny: Failed to read file header
。 objdump
也不是更好。 运行
rabin2 -e tiny
,我们获得了入口地址(带有一些警告):[Entrypoints]
vaddr=0x00010020 paddr=0x00010020 baddr=0x00000000 laddr=0x00000000 haddr=0x00000018 type=program
我设法使用
radare2 tiny
和pd
命令获得了一些不适: br /> [0x00010020]> pd
;-- entry0:
0x00010020 b32a mov bl, 0x2a ; '*' ; 42
0x00010022 31c0 xor eax, eax
0x00010024 40 inc eax
0x00010025 cd80 int 0x80
0x00010027 003400 add byte [eax + eax], dh
0x0001002a 2000 and byte [eax], al
;-- section_end.uphdr:
0x0001002c ~ 01ff add edi, edi
接下来,我尝试了
gdb tiny
,lldb tiny
,但均无济于事。 IDA的免费版本5.0在无限循环时停止。 那么有没有办法自动/半自动地修理精灵?还是其他一些技巧可以调试此(或类似的)二进制文件?我想到的一个主意是用循环的指令修补入口点并附加
gdb
。 如果不存在用于修复elf的工具,内核源文件中的哪些文件包含负责加载二进制文件的代码?
#1 楼
有几种选项可用于分析头文件已损坏或损坏的ELF二进制文件。这些包括但不限于:使用基于ptrace
的调试器,例如Radare2(但绝对不是gdb)。通过Unicorn仿真框架修复标头,这可能涉及重建二进制文件
由于某些原因,这种特殊的二进制文件是标准工具的挑战:
程序头表与ELF头重叠,而不是位于其外部。
没有节,并且与节有关的字段被程序头表覆盖等等-从分析节信息的工具的角度来看-包含废话值。基于BFD的工具(例如
objdump
和GDB)依赖于部分信息的存在和正确性,因此,即使所有其他字段都包含正确的信息,它们也将失败。入口点位于ELF标头中,这意味着可执行文件标头中的代码
使用基于ptrace的调试器
Radare2可以附加到该进程:
$ r2 -d tiny-i386
Process with PID 6756 started...
= attach 6756 6756
bin.baddr 0x00010000
Using 0x10000
Warning: Cannot initialize program headers
Warning: Cannot initialize section headers
Warning: Cannot initialize strings table
Warning: Cannot initialize dynamic strings
Warning: Cannot initialize dynamic section
Warning: read (init_offset)
asm.bits 32
[0x00010020]> pd 5
;-- eip:
0x00010020 b32a mov bl, 0x2a ; '*' ; 42
0x00010022 31c0 xor eax, eax
0x00010024 40 inc eax
0x00010025 cd80 int 0x80
0x00010027 003400 add byte [eax + eax], dh
[0x00010020]>
对于这么小的程序,r2之类的东西似乎很重。指令只有7个字节。
还可以滚动自己的基于ptrace的调试器。在调试器的工作原理:第1部分-基础知识中可以找到一个很好的指南。
仿真
在这种情况下,仿真很容易,因为程序是如此简单。对于这种挑战,仿真是一个很好的解决方案,因为除了第一个和最后一个指令的偏移量之外,不需要任何信息。可以从十六进制转储中手动检索此信息,而根本不需要解析标头。
这里是一个脚本,用于模拟问题中的二进制文件:
#!/usr/bin/python3
from unicorn import *
from unicorn.x86_const import *
from capstone import *
import struct
BASE = 0x100000
STACK_ADDR = 0x0
STACK_SIZE = 1024 * 1024
def read(name):
with open(name, 'rb') as f:
return f.read()
#https://github.com/unicorn-engine/unicorn/blob/master/bindings/python/shellcode.py
# callback for tracing instructions
def hook_code(uc, address, size, user_data):
instruction = uc.mem_read(address, size) # read this instruction code from memory
md = user_data
for i in md.disasm(instruction, address):
print(">>> Tracing instruction at 0x%x, instruction size = 0x%x, disassembly:\t%s\t%s" %(i.address, i.size, i.mnemonic, i.op_str))
# callback for tracing Linux interrupt
def hook_intr(uc, intno, user_data):
# only handle syscall
if intno != 0x80:
print("got interrupt %x ???" %intno);
uc.emu_stop()
return
eax = uc.reg_read(UC_X86_REG_EAX)
eip = uc.reg_read(UC_X86_REG_EIP)
print(">>> 0x%x: INTERRUPT: 0x%x, EAX = 0x%x" %(eip, intno, eax))
uc.emu_stop()
def main():
mu = Uc(UC_ARCH_X86, UC_MODE_32) # initialize emulation engine class
mu.mem_map(BASE, STACK_SIZE) # allocate space at base address
mu.mem_map(STACK_ADDR, STACK_SIZE) # allocate space for stack
mu.mem_write(BASE, read("./tiny_binaries/tiny-i386")) # write file to memory
mu.reg_write(UC_X86_REG_ESP, STACK_ADDR + STACK_SIZE - 1) # initialize stack
md = Cs(CS_ARCH_X86, CS_MODE_32) # initialize disassembler engine class
# add hooks
mu.hook_add(UC_HOOK_CODE, hook_code, md) # pass disassembler engine to hook
mu.hook_add(UC_HOOK_INTR, hook_intr)
mu.emu_start(BASE + 0x20, BASE + 0x27)
print(">>> Emulation Complete.")
if __name__ == "__main__":
main()
以下输出是通过模拟二进制执行而产生的:
$ ./emulate_tiny-i386.py
>>> Tracing instruction at 0x100020, instruction size = 0x2, disassembly: mov bl, 0x2a
>>> Tracing instruction at 0x100022, instruction size = 0x2, disassembly: xor eax, eax
>>> Tracing instruction at 0x100024, instruction size = 0x1, disassembly: inc eax
>>> Tracing instruction at 0x100025, instruction size = 0x2, disassembly: int 0x80
>>> 0x100025: INTERRUPT: 0x80, EAX = 0x1
>>> Emulation Complete.
完整的文章可以在这里找到:使用格式错误的标题分析ELF二进制文件第1部分-模拟微型程序。完全公开:我是本文的作者。
修复标头
由于程序的整体都包含在标头中,因此对其进行修复就意味着要重建二进制文件。程序头表必须与ELF头分开,然后必须将代码附加到程序头表的末尾,最后必须重新计算入口点以指向二进制中第一条指令的新偏移量。在这种特殊情况下,可以使用称为
lepton
的工具(我是开发人员)相对简单地完成此操作。这是完成二进制文件重建的脚本:#!/usr/bin/python3
from lepton import *
def main():
# create new headers
with open("tiny-i386", "rb") as f:
elf_file = ELFFile(f, new_header=True)
# recompose binary
with open("repaired_tiny-i386", "wb") as f:
f.write(elf_file.recompose_binary()) # this moves the program header out of the file
# header and recalculates the entry point
print("\n\tRepaired header field values:\n")
elf_file.ELF_header.print_fields() # call once entry point has been recalculated
if __name__=="__main__":
main()
重建后,
readelf
可以成功解析新的二进制文件:$ readelf -h repaired_tiny-i386
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x10054
Start of program headers: 52 (bytes into file)
Start of section headers: 0 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 1
Size of section headers: 0 (bytes)
Number of section headers: 0
Section header string table index: 0
$ readelf -l repaired_tiny-i386
Elf file type is EXEC (Executable file)
Entry point 0x10054
There is 1 program header, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00010000 0x00030002 0x10020 0x10020 R 0xc0312ab3
新文件的运行时行为与原始文件相同:
$ strace ./repaired_tiny-i386
execve("./repaired_tiny-i386", ["./repaired_tiny-i386"], 0x7ffd19a0f1b0 /* 52 vars */) = 0
strace: [ Process PID=5822 runs in 32 bit mode. ]
exit(42) = ?
+++ exited with 42 +++
更多详细信息,信息和示例,请参见
lepton
存储库。结论
通常,如果执行二进制文件,则应该可以附加
ptrace
。但是,GDB非常脆弱,很容易变得无用。仿真似乎是最可靠的解决方案,因为解析ELF标头在很大程度上是不必要的,并且可以挂钩任何已执行的指令(本质上是完全控制)。 最后,在LWN文章“如何运行程序:ELF二进制文件”中可以找到有关内核如何加载ELF程序的详细介绍。讨论中包括指向内核中相关代码的链接。
评论
尝试用零填充