4

My question is general, but to have an example to work with, let us take one from Whirlwind Tutorial.

; 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

To get a binary compile with nasm -f bin -o tiny nasm.asm;chmod +x tiny. Executable in it self, is a bit of tiny monster. It is smaller than ELF header, yet contains ELF header, program header and program code - yet Linux (at least on my 64 Debian) runs it.

I would like to be able to debug this types files that have (intentionally or not) damaged/incorrect elf header. Is there a tool to fix elf header? Is there debugger that will manage to run this file?

What I tried is to get entry point: readelf -h tiny but readelf refuses even to look at the file: readelf: Error: tiny: Failed to read file header. objdump is no better.

Running rabin2 -e tiny we get entry address (with some warnings):

[Entrypoints]
vaddr=0x00010020 paddr=0x00010020 baddr=0x00000000 laddr=0x00000000 haddr=0x00000018 type=program

I managed to get some dissasembly using radare2 tiny and pd command:

 [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

Next I tried gdb tiny, lldb tiny but neither worked. Free version 5.0 of IDA stops at infinite loop.

So is there a way to repair elf in automatic/semi-automatic way? Or maybe some other trick that would allow debugging this (or similar) binary? One idea that comes to my mind is to patch entry point with instruction that loops and attach gdb. Would that work?

If no tool for repairing elf exists, which files in kernel source contain code responsible for loading binary?

1
  • try padding it with zeroes
    – Igor Skochinsky
    Commented Apr 27, 2017 at 19:00

1 Answer 1

4

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.