Several options are available for analyzing ELF binaries with damaged or corrupted headers. These include, but are not limited to:
- Using a
ptrace
-based debugger such as Radare2 (but definitely not gdb)
- Emulation e.g. via the Unicorn emulation framework
- Repairing the header, which may involve rebuilding the binary
This particular binary is a challenge for standard tools for a few reasons:
- The program header table overlaps the ELF header rather than lying outside of it.
- There are no sections, and the fields having to do with sections are overwritten by
the program header table and so - from the perspective of tools that parse section information - contain nonsense values. BFD-based tools such as
objdump
and GDB rely on section information being present and correct, so they will fail even if all of the other fields contain correct information.
- The entry point lies inside the ELF header, meaning there is executable code inside the header
Using a ptrace-based debugger
Radare2 is able to attach to the process:
$ 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]>
For such a small program, something like r2 seems rather heavyweight. There are only 7 bytes of instructions.
One can also roll their own ptrace-based debugger. A good guide for this can be found
at How debuggers work: Part 1 - Basics .
Emulation
Emulation is easy in this case since the program is so simple. Emulation is a good solution for this kind of challenge since no information is needed besides the offsets of the first and last instructions. This information can be retrieved manually from a hex dump without needing to parse the header at all.
Here is a script for emulating the binary in the question:
#!/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()
The following output is produced by the emulated execution of the binary:
$ ./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.
A full write-up can be found here: Analyzing ELF Binaries with Malformed Headers Part 1 - Emulating Tiny Programs. Full disclosure: I'm the author of the article.
Repairing the Header
Since the entirety of the program is contained within the header, repairing it means rebuilding the binary. The program header table must be separated from the ELF header, the code then must be appended to the end of the program header table, and finally the entry point must be recalculate to point to the new offset of the first instruction in the binary. In this particular case, this can be done relatively straightforwardly using a tool called lepton
(I am the developer). Here is the script to accomplish rebuilding the binary:
#!/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()
After being rebuilt, readelf
can successfully parse the new binary:
$ 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
The runtime behavior of the new file is identical to the original:
$ 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 +++
More details, information and examples can be found in the description of the lepton
repository.
Conclusion
In general, if the binary executes, it should be possible to attach ptrace
. GDB, however, is very brittle and is easy to render useless. Emulation seems to be the most robust solution, since parsing the ELF header is largely unecessary and one can hook any instruction executed (total control, essentially).
On a final note, a detailed presentation of how the kernel loads ELF programs can be found in the LWN article How programs get run: ELF binaries. Included in the discussion are links to relevant code in the kernel.