Binary Lifting with Remill
| Instructor: | Duncan Ogilvie |
|---|---|
| Duration: | 3 days |
| Format: | On-site training with lectures and guided exercises. |
| Price: | TBD |
| Registration: | training@ogilvie.pl |
Description
Binary lifting translates native machine code into an intermediate representation that can be analyzed and transformed with compiler infrastructure. This training teaches participants how to use Remill to lift binaries to LLVM IR and how to turn raw lifted output into code that can be analyzed, optimized, recompiled, and used for deobfuscation.
The course starts from first principles with LLVM IR. Participants learn how modules, functions, basic blocks, values, memory operations, phi nodes, select, getelementptr, intrinsics, and undefined behavior work. They read IR generated by Clang, write LLVM IR by hand, inspect optimization pipelines, and translate IR back into pseudo-C.
The second part moves from reading IR to building tools with the LLVM C++ API. Participants learn the LLVM object model, verification, Value/User relationships, isa/dyn_cast, PatternMatch, IRBuilder, and safe mutation patterns. Exercises focus on command-line tools that inspect, annotate, visualize, instrument, and transform bitcode.
The final part applies these foundations to Remill. Participants study Remill's architecture, instruction semantics, State, Memory, helper functions, and the lifting pipeline. They recover calling conventions, clean symbolic memory accesses, recover stack variables, handle external calls, extract stack strings, and use lifted IR for deobfuscation tasks such as MBA simplification and exception-based control-flow recovery.
Teaching
The training is exercise-driven. Each lecture block introduces a concept that is applied immediately in a practical task. Participants work inside a prepared development environment and build tools that become part of a lifting pipeline.
The course goal is independence. Participants learn how to diagnose verifier failures, optimizer surprises, unsupported instructions, wrong calling-convention assumptions, @RAM memory artifacts, and stack recovery failures using LLVM documentation, bc-annotate, source inspection, and incremental testing.
Binary lifting is a broad topic, so the class focuses on fundamentals and gives follow-up references for continued study.
Learning Objectives
- Read, write, and reason about LLVM IR.
- Understand SSA, control flow, memory operations,
getelementptr, intrinsics,undef, andpoison. - Use LLVM tools such as
clang,llvm-dis,opt,llvm-link, and Godbolt pipeline views. - Build C++ tools on top of the LLVM API.
- Inspect and transform bitcode with
IRBuilder,PatternMatch, and safe use-def traversal. - Understand Remill's
State,Memory, helpers, instruction semantics, and lifted basic block model. - Lift raw bytes, object code, and small binaries with Remill.
- Recover calling conventions with signatures such as
RAX(RDI,RSI). - Convert lifted memory operations from
@RAMaccesses into cleaner pointer operations. - Recover stack variables so LLVM alias analysis and optimization passes can simplify lifted code.
- Handle external calls and indirect-call artifacts in lifted IR.
- Extract stack strings and analyze common obfuscation patterns.
- Use lifted IR as input for deobfuscation, slicing, SMT generation, and pipeline experiments.
Outline
-
Day 1: LLVM IR Fundamentals
- Environment setup
- GitHub Codespaces onboarding
- Repository fork, build preset, and tool verification
remill-lift --versionsanity check
- LLVM IR model
- Modules, functions, basic blocks, instructions, globals, and metadata
- Textual IR (
.ll) and binary bitcode (.bc) - Global identifiers (
@name) and local values (%name) - LLVM types and the absence of signed/unsigned integer types
- SSA and control flow
- Values versus mutable variables
load,store, and memory as the exception to SSA- Branches, returns, basic block predecessors, and CFG shape
phinodes andselectinstructions
- Optimization pipeline
- How Clang emits IR at different optimization levels
- SROA, InstCombine, SimplifyCFG, EarlyCSE, and related passes
- Godbolt pipeline viewer workflow
- Pointer and memory IR
getelementptrfor arrays, structs, nested members, and flattened layoutsptradddirection in LLVM- Common intrinsics:
llvm.memcpy,llvm.assume,llvm.debugtrap, overflow intrinsics
- Undefined behavior
- Immediate UB, deferred UB,
undef, andpoison - Optimization effects caused by undefined values
- Immediate UB, deferred UB,
- Exercises
- Compile C++ to LLVM bitcode and disassemble it
- Inspect modules, globals, functions, blocks, attributes, and
phinodes - Use Godbolt to identify optimization passes
- Write LLVM IR by hand for arithmetic, conditionals, and calls
- Translate
getelementptrexamples into pseudo-C
- Environment setup
-
Day 2: LLVM C++ API and Tooling
- LLVM API foundations
LLVMContext,Module,Function,BasicBlock,Instruction,Value, andUser- Verifier checks for type correctness and CFG integrity
- Value identity, use-def chains, and users
isa,cast, anddyn_cast
- Inspecting IR from C++
bc-annotateoutput and LLVM class hierarchy comments- Instruction matching with manual casts
llvm::PatternMatchfor common IR patterns
- Transforming IR
IRBuilderinsertion points- Creating arithmetic, memory, control-flow, cast, and comparison instructions
- Replacing uses and deleting instructions safely
- Iterator invalidation and mutation order
- Debugging LLVM tools
- Printing values and instructions
- Verifier error patterns
- Diagnosing dominance, malformed
phi, stale uses, and misplaced terminators
- Tool template workflow
bc-tool.cppstructure- CMake target creation
- Command-line bitcode input/output
- Exercises
bc-stats: count loads, stores, and calls per functionbc-demangle: demangle C++ function names in IRbc-graphviz: emit a GraphViz control-flow graphbc-profile: insert runtime instrumentation calls
- Extended exercises
- Build an LLVM REPL with load/save, navigation, annotated printing, renaming, optimization, verification, history, diffing, pattern search, and optional JIT/code generation
- LLVM API foundations
-
Day 3: Remill, Brightening, and Deobfuscation
- Remill architecture
- Instruction semantics written in C++
- Native instructions lifted to LLVM IR
- Architecture support: x86, ARM, PPC, SPARC, and Sleigh-backed lifters
- The
Statestructure as CPU register storage - The
Memoryparameter as a semantic ordering and memory abstraction - Remill helpers and intrinsics for reads, writes, calls, returns, jumps, and async events
- Raw lifting model
__remill_basic_block(State&, addr_t, Memory*)- Inlined instruction semantics
- Raw lifted output versus analyzable lifted code
- Calling convention recovery
- Mapping function arguments into registers
- Mapping return registers back into typed function returns
- Manual helper-based recovery
- Signature-based recovery with
remill-lift -signature - Diagnosing wrong or incomplete signatures
- Lifting workflow
- Minimal constant-return function (
lift1) - Argument recovery (
lift2) - Control-flow recovery with
autolift.py(lift3) - Pipeline stages: raw lift, link helpers, optimize, clean, recover
- Minimal constant-return function (
- Memory recovery
- Remill memory helpers
- The symbolic
@RAMglobal - Manual cleanup of
@RAMGEPs ram-cleanerimplementation- Rewriting lifted program memory accesses to
inttoptr
- Stack recovery
- Symbolic stack markers such as
__remill_symbolic_STACK() - Stack-relative address matching
- Local stack buffer insertion with
alloca - Rewriting stack
@RAMaccesses into local GEPs - Enabling alias analysis, mem2reg, and dead-store/load forwarding
- Symbolic stack markers such as
- External calls
- Modeling calls such as
putchar - Recovering arguments and return values through helper functions
call-cleanerfor indirect and unknown calls
- Modeling calls such as
- Stack strings and analysis tasks
- Detecting stack buffers created by
ram-cleaner - Tracking constant stores into symbolic stack memory
- Extracting runtime-built strings
- Handling optimized and unoptimized string builders
- XOR stack strings as a bonus case
- Detecting stack buffers created by
- Deobfuscation exercises
- AVX-backed
xorstrexamples - MBA expression lifting
- Return-value slicing
- SMT-LIB generation and Z3 equivalence checks
- Opaque predicate proof experiments
- AVX-backed
- Practical case study
- GuLoader-style exception dispatch
- Modeling interrupt/debug-trap behavior
- Lifting exception handlers and rejoining hidden control flow
- Annotation versus transformation in practical pipelines
- Real-world pipeline discussion
- Project-based lifting configuration
- Incremental analyst feedback
- Partial control-flow discovery
- Handling unsupported instructions, segment registers, and platform state
- Follow-up paths for emulation, symbolic execution, and recompilation
- Remill architecture
Requirements and Recommendations
Prerequisites
Participants should be familiar with:
- C++ programming at a modest level.
- Reverse engineering and x86 assembly.
- Python basics.
Workstation Requirements
Each participant needs their own workstation. The prepared environment requires:
- A browser.
- A free personal GitHub account.
- Access to GitHub Codespaces during the training.
After the training, the environment can be deployed offline with Docker.
Classroom Requirements
The training is delivered on-site only. A dedicated classroom with a projector is required. The training uses a collaborative format with frequent questions, live troubleshooting, and shared exercise discussion.
Instructor
Duncan Ogilvie is the creator of x64dbg and has professional experience in DRM, mobile security, reverse engineering, and binary analysis tooling. The course materials focus on practical troubleshooting, transparent lifting internals, and tool-building. Duncan also authored Striga: Lifting x86 to LLVM IR with Python going over the design of a lifter.