Thomas Piacentini
← Atelier

Fiche

Binary Analysis Tools from Scratch

Texte original · anglais

Read ELF and Mach-O, decode x86-64, build the CFG, lift to C and prove it. Python stdlib only.

From bytes to checked C

bytesfunction bytesinstructionsC textObject fileledger.c built by clangstoreELF readersections, symbols, relaprocessDecoderREX, ModRM, SIBprocessCFGblocks, dominatorsprocessLifterIR, flags variableprocessSSAphi, renameprocessSimplifysubstitute, dead codeprocessStructureif, while, do whilemodel
Nodes and connections
NodeRoleKind
Object fileledger.c built by clangstore
ELF readersections, symbols, relaprocess
DecoderREX, ModRM, SIBprocess
CFGblocks, dominatorsprocess
LifterIR, flags variableprocess
SSAphi, renameprocess
Simplifysubstitute, dead codeprocess
Structureif, while, do whilemodel
FromToConnection
Object fileELF readerbytes
ELF readerDecoderfunction bytes
DecoderCFGinstructions
CFGLifter
LifterSSA
SSASimplify
SimplifyStructureC text
Sources

Run the lessons

CommandWhat it does
python3 code/main.py (32)Header, sections, symbols, relocations, map
python3 code/main.py <object> (32)Reads one file. Picks ELF or Mach-O
python3 code/main.py (33)One JSON report for every fixture
python3 code/main.py disasm <object>Listing with relocation targets
python3 code/main.py cfg <object> <function>Analysis of one function
python3 code/main.py (34)IR, SSA, pseudocode next to Ghidra
python3 -m unittest discover -s code/tests -vTest suite of lessons 32 and 34
python3 code/tests/test_main.pyTest suite of lesson 33
Sources

ELF64 record formats

TaskCode
Header after e_ident, offset 16"<HHIQQQIHHHHHH"
Section header, 64 bytes<IIQQQQIIQQ
Symbol, 24 bytes<IBBHQQ
Rela record, 24 bytes<QQq
Program header, 56 bytes<IIQQQQQQ
Symbol index of r_infoELF64_R_SYM(i) is i >> 32
Type of r_infoELF64_R_TYPE(i) is i & 0xffffffff
Mach-O symbol, 16 bytesnlist_64 has no size field
Sources

Address arithmetic

Function bytes
file offset is sh_offset + st_value - sh_addr. Length is st_size.
R_X86_64_PC32
the linker writes S + A - P into the field.
R_X86_64_64
S + A. R_X86_64_PLT32: L + A - P.
RIP target
addend plus 4. .rodata+0x10 on a lea means .rodata+0x14.
PT_LOAD
file offset is p_offset + (address - p_vaddr).
Mach-O size
next symbol in the same section minus this one.
Divide by 10
0x66666667 with shift 34. The error 6 is at most 8.
Sources

Read an object in Python

from main import ElfFile, MachOFile, FIXTURES
elf = ElfFile.from_path(FIXTURES / "ledger-x86_64-O1.o")
elf.header.e_shoff, elf.header.e_shnum0x428 and 11 headers
elf.function_span("percent_change")16 bytes at 0x40
elf.function_bytes("percent_change")85 ff 74 09 ...
elf.relocations_for(".text")3 R_X86_64_PC32 records
elf.file_offset_map()3 gaps of alignment
tiny = ElfFile.from_path(FIXTURES / "tiny-x86_64")
tiny.address_to_file_offset(0x4000b0)0xb0 through PT_LOAD
macho = MachOFile.from_path(FIXTURES / "ledger-arm64-O1.o")
macho.function_span("_percent_change")24 bytes at 648
Sources

Sweep or descent: grade at -O0

FactorLinear sweepRecursive descentDescent with table
Instructions342533
Blocks938
Edges7211
Failuredata decoded as codestops at jmpq *%raxnothing
Use it fororacle agreementwhich bytes runthe full CFG
Sources

Analyze a new object

  1. Oracle compare the decoder with llvm-objdump -d first.
  2. Descent walk from every function symbol. Read unresolved.
  3. Tables match the table idiom and its relocation for each indirect jump.
  4. Bytes bytes that only the sweep claims are padding or data.
  5. CFG build it with recovered tables. Each back edge is a loop.
Sources

Instruction to IR

CommandWhat it does
testl %edi, %ediflags = test(edi, edi)
je 0xdif je(flags) goto 0xd else goto 0x4
xorl %eax, %eaxeax = 0
cltdedx = signfill(eax)
movslq %edi, %raxrax = sext64(edi)
sarq $0x22, %rcxrcx = rcx >>s 34
pushq %rbp and frame movesnothing, frame bookkeeping
retqreturn rax
Sources

Failures and fixes

SymptomCauseFix
"percent_change is at 0"st_value used as a file offsetUse sh_offset + st_value - sh_addr, here 0x40
"grade is 48 bytes"Padding 66 90 counted as codest_size is 46. Padding belongs to no symbol
"the table starts at 0x10"Addend read as the target offsetAdd the 4 bytes to the instruction end: 0x14
A plausible listing over a data tableLinear sweep decodes data as codeDescent from the symbol claims no data byte
"jmp rax" and dead blocksIndirect jump with no tableRead targets from .rela.rodata and the bounds check
Decompiler reports a third parameteridivl writes %rdx and the return width is a guessRead before write on an argument register is a parameter
Sources

The two projects

Factorbinary-inspectormini-decompiler
BuildsStatic inspector for ELF, PE/COFF, Mach-ODecompiler for an x86-64 subset
InputELF64 object, COFF object, PE32+ image, Mach-O 64ELF64 ET_REL object built by clang
OutputJSON report, HTML byte map, build diffStructured C and report.json
Runpython3 cli.py inspect <file> --json report.jsonpython3 -m minidec verify <object> --source <source>
CheckFunction starts scored with precision and recallMasked bytes and a behaviour grid
Exit2 with error: <message> on a bad file0 on pass or behaviour-only, else 1
Sources

Project commands

CommandWhat it does
python3 scripts/project_test.py <project> --init learning-artifacts/<project>Makes the workspace. Stage 1 fails on purpose
python3 scripts/project_test.py <project> --all --path learning-artifacts/<project> --strictRuns the cumulative grader
python3 cli.py inspect <stripped> --truth <unstripped> --html report.htmlScores function starts against the unstripped build
python3 cli.py diff <old> <new> --json diff.jsonSection growth, moved symbols, changed imports
python3 -m minidec decompile <object> --ir --no-division-idiomsShows IR and SSA, keeps the magic multiply
python3 -m minidec verify <object> --source <source> --opt O1 --out <dir>Recompiles, compares bytes, runs the grid
Sources

Questions

Why does the oracle agree with the sweep on the trap?
llvm-objdump is a linear sweep too.
When is an edge a back edge?
Its target dominates its source. It marks a loop.
Why do while in total at -O1?
The header is also the block with the back edge.
Why no jump table in grade at -O1?
Every case returns a constant. A value table replaced the jumps.
Does a behaviour pass prove equivalence?
No. It covers the declared grid on one host.
Sources
Original source
---
id: re-from-scratch-tools
title: Binary Analysis Tools from Scratch
code: BA
subtitle: Read ELF and Mach-O, decode x86-64, build the CFG, lift to C and prove it. Python stdlib only.
hue: 28
sources: phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch, phases/13-tools-and-protocols/33-disassembly-and-control-flow, phases/13-tools-and-protocols/34-lifting-to-pseudocode, projects/binary-inspector, projects/mini-decompiler
source_hash: 41d66339fce016da
---

## From bytes to checked C
type: architecture
from: phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch/docs/en.md, phases/13-tools-and-protocols/33-disassembly-and-control-flow/docs/en.md, phases/13-tools-and-protocols/34-lifting-to-pseudocode/docs/en.md, projects/mini-decompiler/README.md

```arch
node | obj | 1,1 | Object file | ledger.c built by clang | store
node | elf | 2,1 | ELF reader | sections, symbols, rela | process
node | dec | 3,1 | Decoder | REX, ModRM, SIB | process
node | cfg | 4,1 | CFG | blocks, dominators | process
node | ir | 4,2 | Lifter | IR, flags variable | process
node | ssa | 3,2 | SSA | phi, rename | process
node | opt | 2,2 | Simplify | substitute, dead code | process
node | c | 1,2 | Structure | if, while, do while | model
edge | obj | elf | bytes
edge | elf | dec | function bytes
edge | dec | cfg | instructions
edge | cfg | ir |
edge | ir | ssa |
edge | ssa | opt |
edge | opt | c | C text
```

## Run the lessons
type: command-table
from: phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch/docs/en.md, phases/13-tools-and-protocols/33-disassembly-and-control-flow/docs/en.md, phases/13-tools-and-protocols/34-lifting-to-pseudocode/docs/en.md

| Command | What it does |
|---|---|
| `python3 code/main.py` (32) | Header, sections, symbols, relocations, map |
| `python3 code/main.py <object>` (32) | Reads one file. Picks ELF or Mach-O |
| `python3 code/main.py` (33) | One JSON report for every fixture |
| `python3 code/main.py disasm <object>` | Listing with relocation targets |
| `python3 code/main.py cfg <object> <function>` | Analysis of one function |
| `python3 code/main.py` (34) | IR, SSA, pseudocode next to Ghidra |
| `python3 -m unittest discover -s code/tests -v` | Test suite of lessons 32 and 34 |
| `python3 code/tests/test_main.py` | Test suite of lesson 33 |

## ELF64 record formats
type: task-table
from: phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch/docs/en.md

| Task | Code |
|---|---|
| Header after e_ident, offset 16 | `"<HHIQQQIHHHHHH"` |
| Section header, 64 bytes | `<IIQQQQIIQQ` |
| Symbol, 24 bytes | `<IBBHQQ` |
| Rela record, 24 bytes | `<QQq` |
| Program header, 56 bytes | `<IIQQQQQQ` |
| Symbol index of r_info | `ELF64_R_SYM(i)` is `i >> 32` |
| Type of r_info | `ELF64_R_TYPE(i)` is `i & 0xffffffff` |
| Mach-O symbol, 16 bytes | `nlist_64` has no size field |

## Address arithmetic
type: concepts
from: phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch/docs/en.md, phases/13-tools-and-protocols/33-disassembly-and-control-flow/docs/en.md, phases/13-tools-and-protocols/34-lifting-to-pseudocode/docs/en.md

- **Function bytes**: file offset is `sh_offset + st_value - sh_addr`. Length is `st_size`.
- **R_X86_64_PC32**: the linker writes `S + A - P` into the field.
- **R_X86_64_64**: `S + A`. **R_X86_64_PLT32**: `L + A - P`.
- **RIP target**: addend plus 4. `.rodata+0x10` on a `lea` means `.rodata+0x14`.
- **PT_LOAD**: file offset is `p_offset + (address - p_vaddr)`.
- **Mach-O size**: next symbol in the same section minus this one.
- **Divide by 10**: `0x66666667` with shift 34. The error 6 is at most 8.

## Read an object in Python
type: annotated-code
lang: python
from: phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch/docs/en.md, phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch/code/main.py

```python
from main import ElfFile, MachOFile, FIXTURES
elf = ElfFile.from_path(FIXTURES / "ledger-x86_64-O1.o")
elf.header.e_shoff, elf.header.e_shnum      # 0x428 and 11 headers
elf.function_span("percent_change")         # 16 bytes at 0x40
elf.function_bytes("percent_change")        # 85 ff 74 09 ...
elf.relocations_for(".text")                # 3 R_X86_64_PC32 records
elf.file_offset_map()                       # 3 gaps of alignment
tiny = ElfFile.from_path(FIXTURES / "tiny-x86_64")
tiny.address_to_file_offset(0x4000b0)       # 0xb0 through PT_LOAD
macho = MachOFile.from_path(FIXTURES / "ledger-arm64-O1.o")
macho.function_span("_percent_change")      # 24 bytes at 648
```

## Sweep or descent: grade at -O0
type: comparison
from: phases/13-tools-and-protocols/33-disassembly-and-control-flow/docs/en.md

| Factor | Linear sweep | Recursive descent | Descent with table |
|---|---|---|---|
| Instructions | 34 | 25 | 33 |
| Blocks | 9 | 3 | 8 |
| Edges | 7 | 2 | 11 |
| Failure | data decoded as code | stops at `jmpq *%rax` | nothing |
| Use it for | oracle agreement | which bytes run | the full CFG |

## Analyze a new object
type: flow
from: phases/13-tools-and-protocols/33-disassembly-and-control-flow/docs/en.md

1. **Oracle**: compare the decoder with `llvm-objdump -d` first.
2. **Descent**: walk from every function symbol. Read `unresolved`.
3. **Tables**: match the table idiom and its relocation for each indirect jump.
4. **Bytes**: bytes that only the sweep claims are padding or data.
5. **CFG**: build it with recovered tables. Each back edge is a loop.

## Instruction to IR
type: command-table
from: phases/13-tools-and-protocols/34-lifting-to-pseudocode/docs/en.md

| Command | What it does |
|---|---|
| `testl %edi, %edi` | `flags = test(edi, edi)` |
| `je 0xd` | `if je(flags) goto 0xd else goto 0x4` |
| `xorl %eax, %eax` | `eax = 0` |
| `cltd` | `edx = signfill(eax)` |
| `movslq %edi, %rax` | `rax = sext64(edi)` |
| `sarq $0x22, %rcx` | `rcx = rcx >>s 34` |
| `pushq %rbp` and frame moves | nothing, frame bookkeeping |
| `retq` | `return rax` |

## Failures and fixes
type: troubleshooting
span: 2
from: phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch/docs/en.md, phases/13-tools-and-protocols/33-disassembly-and-control-flow/docs/en.md, phases/13-tools-and-protocols/34-lifting-to-pseudocode/docs/en.md

| Symptom | Cause | Fix |
|---|---|---|
| "`percent_change` is at 0" | `st_value` used as a file offset | Use `sh_offset + st_value - sh_addr`, here `0x40` |
| "`grade` is 48 bytes" | Padding `66 90` counted as code | `st_size` is 46. Padding belongs to no symbol |
| "the table starts at `0x10`" | Addend read as the target offset | Add the 4 bytes to the instruction end: `0x14` |
| A plausible listing over a data table | Linear sweep decodes data as code | Descent from the symbol claims no data byte |
| "jmp rax" and dead blocks | Indirect jump with no table | Read targets from `.rela.rodata` and the bounds check |
| Decompiler reports a third parameter | `idivl` writes `%rdx` and the return width is a guess | Read before write on an argument register is a parameter |

## The two projects
type: comparison
span: 2
from: projects/binary-inspector/README.md, projects/binary-inspector/API.md, projects/mini-decompiler/README.md

| Factor | binary-inspector | mini-decompiler |
|---|---|---|
| Builds | Static inspector for ELF, PE/COFF, Mach-O | Decompiler for an x86-64 subset |
| Input | ELF64 object, COFF object, PE32+ image, Mach-O 64 | ELF64 `ET_REL` object built by clang |
| Output | JSON report, HTML byte map, build diff | Structured C and `report.json` |
| Run | `python3 cli.py inspect <file> --json report.json` | `python3 -m minidec verify <object> --source <source>` |
| Check | Function starts scored with precision and recall | Masked bytes and a behaviour grid |
| Exit | 2 with `error: <message>` on a bad file | 0 on `pass` or `behaviour-only`, else 1 |

## Project commands
type: command-table
span: 2
from: projects/binary-inspector/README.md, projects/binary-inspector/API.md, projects/mini-decompiler/README.md, projects/mini-decompiler/solution/minidec/cli.py

| Command | What it does |
|---|---|
| `python3 scripts/project_test.py <project> --init learning-artifacts/<project>` | Makes the workspace. Stage 1 fails on purpose |
| `python3 scripts/project_test.py <project> --all --path learning-artifacts/<project> --strict` | Runs the cumulative grader |
| `python3 cli.py inspect <stripped> --truth <unstripped> --html report.html` | Scores function starts against the unstripped build |
| `python3 cli.py diff <old> <new> --json diff.json` | Section growth, moved symbols, changed imports |
| `python3 -m minidec decompile <object> --ir --no-division-idioms` | Shows IR and SSA, keeps the magic multiply |
| `python3 -m minidec verify <object> --source <source> --opt O1 --out <dir>` | Recompiles, compares bytes, runs the grid |

## Questions
type: qa
from: phases/13-tools-and-protocols/33-disassembly-and-control-flow/docs/en.md, phases/13-tools-and-protocols/34-lifting-to-pseudocode/docs/en.md, phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch/docs/en.md, projects/mini-decompiler/README.md

- **Why does the oracle agree with the sweep on the trap?** `llvm-objdump` is a linear sweep too.
- **When is an edge a back edge?** Its target dominates its source. It marks a loop.
- **Why `do while` in `total` at -O1?** The header is also the block with the back edge.
- **Why no jump table in `grade` at -O1?** Every case returns a constant. A value table replaced the jumps.
- **Does a behaviour pass prove equivalence?** No. It covers the declared grid on one host.
Code et livrables 1
Ce que vous en retenez

Enregistré dans votre carnet sur cet appareil.

Sources et limites

Les repères de lecture sont déclaratifs. Les résultats de tests sont enregistrés séparément ; ils valent pour les cas exécutés.

AI Engineering from Scratch · 3f9715c · MIT

Télécharger ce fichier ↓