User‑Level Debugging & Executable Formats
Practical debugging, memory inspection, and the internals of ELF and PE binaries.
DDD – The Data Display Debugger
DDD provides a graphical front‑end to GDB, the GNU debugger. While command‑line GDB is perfectly capable, reverse engineering often benefits from having multiple views—stack, registers, disassembly—simultaneously visible in one workspace. DDD offers this, and also includes a GDB command‑line pane, so nothing is lost.
Knowing GDB’s internal commands remains useful for actions that are awkward to perform via the GUI. GDB has a topic‑based help system (help lists categories). DDD can even echo GUI‑triggered commands to the console, helping you learn the command syntax. Key commands we frequently use include:
run, break, cont, stepi, nexti, finish, disassemble, bt, info [registers|frame], and x.
Every GDB command can be prefixed with a repeat count, e.g., stepi 1000 steps through 1000 assembly instructions.
Setting Breakpoints
A breakpoint halts execution at a specific location. The break command accepts a function name, a filename:line_number, or *0xaddress. For instance, break __libc_start_main sets a breakpoint on that well‑known symbol. GDB also supports tab completion, which helps when exploring available symbols—though in production binaries, those are often scarce.
Viewing Assembly
After setting a breakpoint (say, on __libc_start_main), DDD can display the disassembly in the lower half of the source window. To switch to Intel syntax, go to Edit → GDB Settings and choose the desired Disassembly flavor, or issue set disassembly-flavor intel at the GDB prompt. Using DDD’s menus saves your preference across sessions.
Examining Memory and the Stack
The x (examine) command is the primary tool for memory inspection. Its syntax is:
x /<count><format><size> <address>
- Format letters:
o (octal), x (hex), d (decimal), u (unsigned), t (binary), f (float), a (address), i (instruction), c (char), s (string).
- Size letters:
b (byte), h (halfword), w (word), g (giant, 8 bytes).
For example, x /32xw 0x400000 dumps 32 words (4‑byte values) starting at that address. You can also use registers as addresses by prefixing with $, e.g., x /32xw $esp displays the top 32 words on the stack.
In DDD, the Data Window (via View → Data Window) lets you create persistent displays. You can type any expression or even a GDB command (in backticks) such as `x /32xw $esp` and have it update automatically as you step. Add these to the menu for quick access.
Displaying Memory as Data Structures
DDD excels at visualising structured data. By casting an address to a pointer of a known type, DDD will render the structure graphically. If the structure contains pointers, clicking on them opens further displays.
When the target binary lacks debug symbols, you can still define custom types. Compile a .c file with your suspect structures (and any required headers) as a shared library (gcc -shared). Then, before debugging, set set env LD_PRELOAD=file.so in GDB. From then on, those types become available as if they were native to the program.
💡 Watchpoints – a separate breakpoint type that triggers on data access – are also supported; the document notes they are useful but leaves detailed examples for later.
WinDbg – The Windows Debugger
WinDbg is part of Microsoft’s free Debugging Tools for Windows. It provides a GUI with an embedded command line, similar to console debuggers like ntsd. The help file offers comprehensive documentation.
Breakpoints
Breakpoints can be set via the GUI (Edit → Breakpoints, or Alt+F9). From the command line: bp (set), bl (list), bc (delete). Breakpoints can be placed on function names (if symbols exist) or absolute addresses, and even on source lines when source is available (bX filename:linenumber).
Viewing Assembly
Use View → Disassembly in WinDbg, or the u command in ntsd.
Stack Operations
The k command (and variants) shows the call stack. To examine local stack frame contents, db esp ebp is a common shortcut, but this assumes ebp is the frame pointer. If frame‑pointer omission is used, you can inspect memory starting from esp directly. Use .frame X to switch to a specific stack frame (frame numbers from kn).
Memory Access
The d* family of commands reads memory. For instance, dp displays pointers, dw shows words, db dumps bytes. You can specify ranges or lengths: db 77f75a58 l 10 displays 0x10 bytes. The dt command, when symbols are available, attempts to format the data according to its type.
Tips & Tricks
The poi() function dereferences a pointer and returns the pointed‑to value. Combined with user‑defined aliases, this becomes a handy shortcut.
The document includes a full WinDbg session example, stepping through a simple C program, setting breakpoints, examining variables (dt), and dumping memory.
Executable Formats – ELF and PE
Once you understand low‑level code generation, the next question is: how is this code stored on disk? This chapter examines ELF (Linux/UNIX) and PE (Windows) formats, with a focus on the information needed for code modification.
Working with ELF
Under Linux, program execution starts at _start, then __libc_start_main, and eventually main. The OS loads code from various sources into memory based on the ELF specification. ELF defines a standard mapping from disk to a complete runtime image (code, stack, heap, libraries).
ELF Layout
Three main header areas:
- ELF file header (at the very beginning) – contains the magic number, entry point (
e_entry), offsets to program headers (e_phoff) and section headers (e_shoff), plus sizes and counts.
- Program headers – an array of
Elf32_Phdr structures that describe loadable segments (code, data, etc.). Key fields: p_offset (file offset), p_filesz (size in file), p_memsz (size in memory).
- Section headers – optional, describe named sections (e.g.,
.text, .data). Used by linkers and debuggers but not required for execution.
Editing ELF
Tools like HT Editor allow direct modification of headers and instructions. However, changing segment sizes can break the binary; more nuanced insertion techniques are discussed in the code modification chapter.
📘 Note: ELF is extremely flexible; some programs deliberately craft abnormal but valid headers to hinder reverse engineering.
Working with PE (Portable Executable)
The PE format is more complex, with multiple ways to refer to locations:
- File Offset – raw offset in the file.
- Relative Virtual Address (RVA) – offset from the image base when loaded (not the same as file offset, due to alignment padding).
- Section Offset – offset within a specific section.
- Virtual Address (VA) – absolute address in process memory:
VA = RVA + ImageBase (typically 0x400000 for executables, variable for DLLs).
PE Headers
Three main structures:
IMAGE_DOS_HEADER – a legacy DOS stub; only the e_lfanew field matters, pointing to the NT headers.
IMAGE_NT_HEADERS – contains the signature (PE\0\0), a IMAGE_FILE_HEADER (with NumberOfSections and SizeOfOptionalHeader), and the IMAGE_OPTIONAL_HEADER.
IMAGE_OPTIONAL_HEADER – holds critical fields like ImageBase, AddressOfEntryPoint, SectionAlignment, FileAlignment, and a DataDirectory array of 16 IMAGE_DATA_DIRECTORY entries.
The Data Directory
Each entry gives an RVA and size. Index 1 points to the Import Directory, which is an array of IMAGE_IMPORT_DESCRIPTOR structures, one per imported DLL.
Import Descriptor
Contains:
OriginalFirstThunk (RVA to the “unbound” import thunk table)
Name (RVA to the DLL name string)
FirstThunk (RVA to the Import Address Table, IAT)
Thunk Tables
Each thunk is a union (IMAGE_THUNK_DATA) that can be an ordinal (high bit set) or an RVA to an IMAGE_IMPORT_BY_NAME structure. The IAT is overwritten at load time with the actual function addresses, serving a role similar to the ELF PLT. Pre‑bound executables may already have guessed addresses, but these can become stale.
The document also notes that PEView is a helpful tool for visual exploration, and that the IAT’s function pointers are the primary target for code hooking (covered in the next chapter).
Code Modification – Overview
The final chapter introduces practical techniques for altering binary behaviour.
Motivations
Modifying closed‑source software, bypassing copy protection, or injecting new functionality.
Library Hooking
On Linux, LD_PRELOAD lets you override standard library functions with your own shared library (except for setuid programs). Example: compiling a .so and setting the variable before running the target.
Instruction Modification
The simplest form: change the mnemonic or its arguments. HT Editor allows direct hex editing with real‑time disassembly feedback. Changing arguments is straightforward; changing the mnemonic is trickier because of varying instruction lengths.
Inserting Instructions / Functions
Single instructions can be inserted if there is unused space (identified by disasm.pl). Entire functions can be added by placing code in unused regions (careful with main and indirect calls). Multiple function insertion may require more elaborate methods, such as using mmap to allocate executable memory.
Attacking Copy Protection
The chapter promises concrete examples, applying these techniques to defeat common protection schemes.
💡 Key insight: Understanding the binary format and the debugger’s capabilities is the foundation for any successful modification. Practice with small test programs before tackling real targets.
📘 Note: The original document contained placeholders (FIXME) for some diagrams, additional examples, and deeper coverage of COM and memory layouts. Those have been summarised here to maintain focus on the core concepts.
This HTML rendering is based on the “User‑Level Debugging & Executable Formats” document. All content is for educational purposes.