Academy
Reverse Engineering experimental
Reverse Engineering Taking software apart to understand it, with tools you build and tools the field uses Machine code and instruction sets. How instructions encode: opcodes, operands, addressing modes, and the difference between fixed-width and variable-length instruction sets.. Compute the fields of a fixed-width instruction by hand, decoding it directly.. Explain addressing modes and operand encoding.. Contrast a clean fixed-width ISA with variable-length x86.. Opcodes and operands, Addressing modes, Fixed-width ISAs (MIPS16), Variable-length x86 Machine code and instruction sets A processor only ever sees bytes. Reverse engineering begins with the discipline of turning those bytes back into the operations and data they represent. Every instruction set architecture (ISA) defines a strict encoding for that translation, and reading it by hand is the foundation every disassembler you build later depends on. Opcodes and operands An instruction word packs together an opcode, which names the operation (add, load, branch), and one or more operands, which name the data the operation acts on. Operands can be registers, immediate constants embedded in the instruction, or memory locations. In a classic Reduced Instruction Set Computer (RISC) encoding such as the Microprocessor without Interlocked Pipeline Stages (MIPS) architecture, a 32-bit word is sliced into fixed bit-fields: a 6-bit opcode, then register fields (rs, rt, rd), a shift amount, and a function code that disambiguates opcode 0 into many arithmetic operations. Decoding is mechanical: mask and shift each field out by its known bit position and width, then look the opcode (and function code, where present) up in a table that maps it to a mnemonic and an operand format. Addressing modes An addressing mode is the rule for finding an operand's actual value. Immediate mode reads the value straight from the instruction. Register mode reads it from a named register. Base-plus-displacement (also called base-plus-offset) computes an effective address as a register's contents plus a constant encoded in the instruction, which is how load/store ISAs like MIPS reach memory: arithmetic instructions never touch memory directly, only explicit load and store instructions do, moving values between registers and memory through this addressing mode. Fixed-width ISAs (MIPS16) A fixed-width ISA gives every instruction the same size, for example a constant 16 or 32 bits. This is a deliberate simplification: a decoder never has to ask 'how long is this instruction' before it can find the next one, it simply advances the instruction pointer by the fixed width every step. That property is exactly what makes linear-sweep disassembly reliable on such an ISA, a point the next module builds on directly. Variable-length x86 x86 instructions range from one byte to fifteen. Length depends on a chain of optional pieces: prefixes, an opcode (one, two, or three bytes), a ModR/M byte, an optional Scale-Index-Base (SIB) byte, an optional displacement, and an optional immediate. The decoder must read a variable number of these fields sequentially to learn the instruction's own length, and only then does it know where the next instruction starts. This makes x86 far more expressive per byte, but it also means a single misjudged boundary desynchronises every instruction that follows, a fragility fixed-width ISAs simply do not have. Related CCI capabilities Computer Architecture (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/computer-architecture/). Optics Primer Series (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/optics/). Maths Refresher Series, Finance (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/maths-finance/). System Dynamics (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/system-dynamics/). CCI Lab: Run it, build with it, read the thinking, reuse the data. (https://www.cambridgecyberinternational.com/en/insights/lab/) The instruction sets this course draws on The MIPS architecture, first shipped by MIPS Computer Systems in 1985 as one of the earliest commercial reduced-instruction-set designs, remains a popular teaching architecture precisely because its fixed 32-bit word and small, regular field layout make hand-decoding tractable in a way x86 never is. Intel's original 8086, first shipped in 1978, established the variable-length encoding tradition x86 has carried forward and extended through every generation since, trading decode simplicity for code density and backward compatibility spanning nearly five decades. Advanced RISC Machines (ARM), first shipped by Acorn Computers in 1985 and now the dominant architecture in mobile and embedded silicon, occupies a middle ground this course returns to in Module 04: a clean fixed-width base encoding with an optional compact Thumb mode layered on top. Reading these three architectures side by side is the fastest way to internalise why encoding design is a genuine engineering trade-off rather than an arbitrary historical accident: MIPS optimises for decode simplicity, x86 optimises for code density and compatibility, and ARM's Thumb mode tries to have some of both by offering a second, denser encoding alongside the primary one. This course keeps returning to the same three architectures, MIPS for its clarity, x86 for its ubiquity and messiness, ARM for its middle ground, precisely so that a technique learned against one transfers recognisably to the next rather than being relearned from scratch each time a new module changes target. Calling conventions and the ABI. Stack frames, register use, prologue and epilogue, argument passing and return values, and caller and callee saved registers.. Identify a function's prologue and epilogue.. Locate arguments and the return value from the convention, and compute the stack offset each one lives at.. Explain caller-saved versus callee-saved registers.. Stack frames, Prologue and epilogue, Argument passing and returns, Caller and callee saved registers Calling conventions and the ABI A compiled function is meaningless without a convention: an agreement, external to the instructions themselves, about where arguments live, where the result goes, and which registers survive a call. That agreement is the Application Binary Interface (ABI). Reading it off a disassembly, rather than assuming it, is what lets a reverser reconstruct a function's signature from raw bytes. Stack frames Each active function call owns a stack frame: a region of the stack holding its local variables, saved registers, and the address to return to. Frames stack on top of each other as calls nest, and each frame is torn down when its function returns. A frame pointer (rbp on x86-64, or a dedicated frame-pointer register on other Instruction Set Architectures (ISAs)) can give a fixed reference point inside the frame, though optimised code often omits it and addresses locals directly off the stack pointer instead. Prologue and epilogue The prologue is the opening sequence of a function that builds its frame: on x86-64 this is classically 'push rbp' to save the caller's frame pointer, 'mov rbp, rsp' to establish the new one, and 'sub rsp, N' to reserve space for locals. The epilogue reverses this at the end, often compressed into the single 'leave' instruction (equivalent to 'mov rsp, rbp' then 'pop rbp'), followed by 'ret'. Recognising this shape in a listing is usually the fastest way to find a function's boundaries in stripped code. Argument passing and returns Under the System V Advanced Micro Devices 64-bit (AMD64) Application Binary Interface (ABI) (Linux, macOS), the first six integer or pointer arguments pass in rdi, rsi, rdx, rcx, r8, r9, with an integer or pointer return value coming back in rax; the Microsoft x64 convention instead uses rcx, rdx, r8, r9. Advanced Reduced Instruction Set Computer (RISC) Machines (ARM)'s Procedure Call Standard (AAPCS) passes the first four arguments in r0 through r3, with the link register (lr, r14) holding the return address rather than the stack. Extra arguments beyond the register budget spill to the stack. Knowing the active convention tells a reverser exactly where to look to recover a call's signature. Caller and callee saved registers Registers split into two survivorship classes. Caller-saved (volatile) registers may be clobbered by any call, so the caller must save them first if it needs their values afterward. Callee-saved (non-volatile) registers must be preserved by the function that uses them, typically pushed in the prologue and popped in the epilogue. This distinction explains which pushes in a prologue are bookkeeping for the callee's own use of a register, versus frame setup, and it is essential to reconstructing which values remain live across a call. Related CCI capabilities Computer Architecture (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/computer-architecture/). Optics Primer Series (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/optics/). Maths Refresher Series, Finance (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/maths-finance/). System Dynamics (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/system-dynamics/). CCI Lab: Run it, build with it, read the thinking, reuse the data. (https://www.cambridgecyberinternational.com/en/insights/lab/) Calling conventions as a reverse-engineering shortcut A calling convention is a contract, not a law of physics: nothing stops a compiler from breaking it, and hand-written assembly routinely does, but every mainstream compiler for a given platform follows its platform's convention by default precisely so that separately compiled object files can call each other without either side knowing the other's source. This is exactly why recognising the active convention is a reverse-engineering shortcut rather than a mere detail: once you know a stripped function follows, say, the System V AMD64 convention, you already know where its first six integer arguments live before reading a single instruction of its body. The convention also predicts what a function's prologue and epilogue will look like before you read them: a function that will call other functions typically needs to preserve callee-saved registers it intends to use, which shows up as a predictable pattern of register pushes near the entry point and matching pops near each return, while a leaf function that calls nothing may skip this entirely. Recognising this pattern at a glance, rather than tracing every instruction, is one of the fastest ways an experienced reverser orients inside an unfamiliar stripped binary. This is also why Module 04's real-instruction-set work and Module 09's capstone both lean on convention-recognition as a first step: identifying the active calling convention before reading a single instruction body is consistently the fastest way to orient inside an unfamiliar stripped function, on any of the architectures this course covers. Building a disassembler, part one. A linear-sweep decoder for the teaching instruction set, reusing the MIPS16 assets from the computer-architecture course.. Implement a linear-sweep decoder for a fixed-width ISA.. Check decoded instructions against the assembler's output.. State the assumption linear sweep makes about code layout.. Linear sweep, Decoding MIPS16 fields, Checking against the assembler, Where linear sweep assumes too much Building a disassembler, part one The simplest disassembly strategy is linear sweep: start at an address and decode instructions one after another, in byte order, until you run out of bytes. It is the first disassembler worth building because it is small, and because understanding where it breaks motivates every more sophisticated strategy that follows. Linear sweep A linear-sweep decoder keeps an instruction pointer starting at the sweep's base address. At each step it reads the instruction at that pointer, decodes it against an opcode table, prints or records it, and advances the pointer by the instruction's width. On a fixed-width ISA that advance is always a constant, which is exactly why linear sweep is a natural fit for the compact 16-bit MIPS16 encoding of the Microprocessor without Interlocked Pipeline Stages (MIPS) instruction set (MIPS16): there is no ambiguity about where the next instruction begins. Decoding MIPS16 fields Decoding one instruction means extracting its bit-fields with masks and shifts: isolate the opcode bits first, look up its mnemonic and operand format, then extract the register and immediate fields the format calls for, sign-extending immediates where the Instruction Set Architecture (ISA) specifies signed use. A minimal decoder therefore needs three things: an instruction pointer that advances by the fixed width, an opcode-to-mnemonic table describing each instruction's operand format, and the mask-and-shift logic to pull operands out correctly. Checking against the assembler The strongest correctness check available is round-tripping: take source you understand, assemble it with the course's MIPS16 assembler, then run your decoder over the resulting bytes and confirm the recovered mnemonics and operands match the original source exactly. Any mismatch points precisely at a wrong mask, a wrong shift, or a missing table entry, which is far more reliable than eyeballing a hex dump. Where linear sweep assumes too much Linear sweep's entire correctness rests on one assumption: that every byte in the swept range is a genuine instruction, laid out contiguously with nothing else mixed in. Real binaries violate this constantly, embedding jump tables, string constants, or alignment padding directly inside a code section. Decode straight through such a region and you get instructions that were never meant to execute, and on a fixed-width ISA an even worse failure is simple: advancing by the wrong constant (say 4 bytes over 2-byte MIPS16 words) silently skips every other real instruction. Tools like objdump's default mode are essentially linear sweep, and hand-crafted or obfuscated code can make them print convincing garbage. The fix is not to abandon linear sweep, which stays valuable for its speed and completeness over a region, but to recognise when its assumption fails and reach for a strategy that follows control flow instead, which is the subject of the next module. Related CCI capabilities Computer Architecture (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/computer-architecture/). Optics Primer Series (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/optics/). Maths Refresher Series, Finance (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/maths-finance/). System Dynamics (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/system-dynamics/). CCI Lab: Run it, build with it, read the thinking, reuse the data. (https://www.cambridgecyberinternational.com/en/insights/lab/) Why build a decoder instead of only using one Building a linear-sweep decoder from scratch, rather than starting directly with a production tool, forces an understanding that using a finished disassembler never requires: exactly which bits in an instruction word mean what, and exactly why an assumption as simple as "code and only code fills this address range" can fail. Every production disassembler, including the ones Module 05 and Module 06 introduce, makes this same assumption somewhere in its default path, so understanding where and how it fails is not a detour from real reverse engineering, it is a prerequisite for reading a production tool's output critically rather than trusting it blindly. A homegrown decoder built and verified against an assembler's own output, as this module's lab requires, also gives you a known-correct reference for later modules: when Module 04's production-grade Capstone decoder and this module's homegrown one disagree on a MIPS16 byte stream, you have an independent way to determine which one is right rather than simply trusting whichever tool happens to be more polished. The discipline of checking a homegrown decoder's output against an assembler's own output, rather than merely eyeballing whether the result looks plausible, is the same verification habit Module 05's Ghidra work and Module 09's capstone both require of every claimed finding. Building a disassembler, part two. Recursive-descent disassembly, basic blocks, control-flow graph recovery, and the failure modes of each strategy.. Implement recursive-descent disassembly following branches.. Partition instructions into basic blocks.. Explain what a control-flow graph represents, then recover and read one.. Recursive descent, Basic blocks, Control-flow graph recovery, Failure modes compared Building a disassembler, part two Linear sweep decodes everything in a range whether or not it is really code. Recursive descent instead decodes only what control flow actually reaches, starting from known entry points and following the program's own branches outward. It trades completeness over a byte range for fidelity to the program's real structure. Recursive descent Starting from an entry point (a function start, or the program entry), a recursive-descent decoder decodes instructions sequentially until it hits a control-transfer instruction. An unconditional jump or call adds its target address to a work list to decode next; a conditional branch adds both the taken target and the fall-through address; a return terminates that path. The decoder recurses (or iterates with an explicit work list) over every address discovered this way, decoding each region once. Because it follows control flow rather than sweeping blindly, it naturally steps over data interleaved with code, so long as that data is never actually a branch target. Basic blocks As recursive descent decodes, it naturally partitions code into basic blocks: maximal straight-line instruction sequences with exactly one entry point and one exit, ending at a conditional branch, an unconditional jump, or a return, and never branched into except at their first instruction. A new block boundary is created wherever another block jumps in, or wherever a block itself ends in a transfer. Control-flow graph recovery Once a function is partitioned into basic blocks, the control-flow graph (CFG) represents each block as a node and each possible transfer of control as a directed edge: a conditional branch produces two outgoing edges, to its taken target and its fall-through; an unconditional jump produces one; a return produces none. A node B is dominated by node A when every path from the function's entry to B passes through A, a relationship the decompiler leans on heavily to recover loops and structured conditionals from the raw graph. Recovering the CFG is the necessary precondition for decompilation, for reasoning about reachability, and for finding dead code. Failure modes compared Linear sweep's failure is decoding data as if it were code. Recursive descent's mirror-image failure is missing code that is reached only through indirect control transfers, a computed jump through a register, a function pointer call, or a compiled switch statement's jump table, none of which reveal their targets by static inspection of the transfer instruction alone. A switch statement compiled to an indirect jump through a table, for instance, leaves the CFG missing edges to every case block until the reverser recovers the table's base, bounds, and entries by hand and adds those edges explicitly. This is exactly why production tools like the Interactive DisAssembler (IDA) Pro and Ghidra layer heuristics, such as jump-table recovery and function-signature matching, on top of recursive descent rather than relying on it alone. Related CCI capabilities Computer Architecture (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/computer-architecture/). Optics Primer Series (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/optics/). Maths Refresher Series, Finance (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/maths-finance/). System Dynamics (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/system-dynamics/). CCI Lab: Run it, build with it, read the thinking, reuse the data. (https://www.cambridgecyberinternational.com/en/insights/lab/) Recursive descent as the industry-standard approach Recursive-descent disassembly is not a niche academic technique; it is the approach production-grade tools like IDA Pro, first released by DataRescue in 1991, and Ghidra, open-sourced by the National Security Agency in 2019, both build on, precisely because following actual control flow rather than sweeping byte-by-byte through an address range avoids linear sweep's characteristic failure mode of decoding embedded data as if it were code. Both tools layer substantial heuristics on top of pure recursive descent, jump-table recovery, function-signature matching, prologue detection, because pure recursive descent has its own characteristic blind spot: any indirect control transfer whose target cannot be determined by static inspection alone leaves a gap in the recovered control-flow graph. The dominance relation this module introduces, that node B is dominated by node A when every path from the function's entry to B passes through A, is not an abstract mathematical nicety; it underlies real compiler optimisations and real reverse-engineering techniques, including the loop-detection and structuring algorithms a decompiler like Ghidra's uses to turn a control-flow graph back into readable, structured pseudocode rather than a raw tangle of basic blocks and jumps. This dominance relation, together with the basic-block partitioning this module also introduces, is the shared vocabulary Module 05's Ghidra work and Module 06's automation work both assume without re-explaining, so getting comfortable with it here pays off directly in every later module. Real instruction sets in the sandbox. Decoding real x86 and ARM with a disassembler compiled to WebAssembly, meeting variable-length encoding and compiler idioms.. Run a WebAssembly (WASM) disassembler in the sandbox to decode a real code snippet.. Handle variable-length x86 decoding.. Recognise common compiler idioms.. A WASM disassembler in the browser, Variable-length decoding, Compiler idioms, Homegrown versus production decoding Real instruction sets in the sandbox Everything built so far has targeted a teaching ISA. Real reverse engineering means x86 and ARM, encodings far messier than the compact 16-bit MIPS16 encoding of the Microprocessor without Interlocked Pipeline Stages (MIPS) instruction set (MIPS16), and this module meets them safely by running a production disassembly engine directly inside the browser sandbox. A WebAssembly (WASM) disassembler in the browser Compiling a real disassembler such as Capstone to WebAssembly lets it run entirely inside the page, decoding genuine x86 and Advanced Reduced Instruction Set Computer (RISC) Machines (ARM) byte streams with no native install, no container, and no shell-out to an external process. That matters for the course as much as for you: it keeps the lab deterministic (the tool and inputs are fixed, so grading is reproducible) and keeps analysis contained inside the sandbox rather than trusting a native binary on the host. Variable-length decoding x86 decoding must resolve a chain of optional fields before it knows an instruction's length: prefixes (the Register EXtension (REX) prefix, enabling 64-bit operands and extended registers, operand-size, and repeat prefixes such as REPeat (REP)), the opcode itself, a ModR/M byte encoding register-versus-memory operand selection, an optional Scale-Index-Base (SIB) byte encoding a scaled-index address of the form base + index*scale + displacement, an optional displacement, and an optional immediate. Each prefix changes how the remaining bytes must be read, so getting one field wrong desynchronises everything after it, producing convincing-looking but entirely wrong instructions until the stream happens to resynchronise by chance. Compiler idioms Compiled code is full of recognisable shorthand. 'xor eax, eax' is a compact way to zero a register (and, as a side effect on x86-64, clears the upper 32 bits of rax). 'test reg, reg' followed by a conditional jump is a zero-or-sign check standing in for an if-statement. 'lea' is often used for pure arithmetic rather than genuine address computation, because it computes an address without touching memory or flags. Multiplication by a constant frequently compiles to a sequence of shifts and adds instead of an actual multiply instruction. And on x86-64, Register Instruction Pointer (RIP)-relative addressing references data at a fixed offset from the instruction pointer, letting position-independent code reach globals without baking in absolute addresses. Learning to read these idioms at a glance is what separates reading a disassembly from merely decoding it. Homegrown versus production decoding The decoder you built for MIPS16 still earns its place beside Capstone: it makes the encoding fully legible, and it is the only option for custom or embedded Instruction Set Architectures (ISAs) a production tool was never taught to decode. Capstone wins on real, standard architectures, where its coverage, correctness, and speed on x86 and ARM outclass anything built for a course. Knowing which tool is right for a given target, rather than reaching for the same one out of habit, is itself a reverse-engineering skill. Related CCI capabilities Computer Architecture (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/computer-architecture/). Optics Primer Series (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/optics/). Maths Refresher Series, Finance (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/maths-finance/). System Dynamics (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/system-dynamics/). CCI Lab: Run it, build with it, read the thinking, reuse the data. (https://www.cambridgecyberinternational.com/en/insights/lab/) Production disassemblers versus homegrown ones Capstone, released as open-source software by Nguyen Anh Quynh in 2013, has become one of the most widely embedded disassembly engines precisely because it decouples the disassembly engine itself from any particular analysis tool's user interface, which is exactly why compiling it to WebAssembly for this module's sandbox is a natural fit rather than an unusual choice: the same engine that powers standalone reverse-engineering tools runs identically, and deterministically, inside the browser. Running it as WebAssembly, rather than shelling out to a native binary, keeps the lab's grading reproducible and keeps the sandbox's isolation guarantees intact, the same design constraint that shaped every other lab this course has built so far. x86's messiness is not accidental complexity; every optional prefix, every variant addressing mode the ModR/M and SIB bytes encode, exists because Intel's 8086, first shipped in 1978, and every processor generation since had to remain backward compatible with the last, adding new capability without ever being free to simplify what came before. Reading x86 fluently, recognising a REX prefix's effect at a glance, recognising RIP-relative addressing's role in position-independent code, is less about memorising an encoding table and more about recognising this compatibility-driven layering for what it is. Knowing when to reach for Capstone and when to reach for the homegrown decoder from Modules 02 and 03 is itself the subject Module 06 develops further, comparing the two instruments directly on the same analysis task. Static analysis with Ghidra. The loader, the listing and decompiler, data types and cross-references, and the script interface, run as a guided desktop component.. Locate key structures in a prepared binary while loading it and navigating the listing.. Read the decompiler output and apply data types.. Follow cross-references to recover structure.. Loading a binary, Listing and decompiler, Data types and cross-references, The Ghidra script interface Static analysis with Ghidra Ghidra is a cross-platform desktop application, written in Java and released as open-source software by the United States National Security Agency in 2019. It is the field's leading free static-analysis platform, and this module is run as a guided desktop component because its decompiler and scripting need the full desktop runtime rather than a browser sandbox. Loading a binary When Ghidra imports a file, its loader's first job is to parse the container format (Portable Executable (PE) on Windows, Executable and Linkable Format (ELF) on Linux, Mach-O on macOS), map its sections into a virtual address space, and identify the target architecture and entry point. Ghidra then lifts the raw machine code to P-code, a processor-independent intermediate representation that drives every later analysis pass and the decompiler itself. Each processor's instruction semantics are described to this lifter in a specification language named SLEIGH (SLEIGH), a proper name coined by Ghidra's authors rather than an acronym for anything further, which is how Ghidra supports dozens of architectures from one analysis engine. Listing and decompiler The listing is the disassembly view: addresses, bytes, mnemonics, and operands, organised by function. The decompiler sits above it, reconstructing a higher-level C-like approximation of a function from its machine code. That output is best treated as a faithful reconstruction to check against the disassembly, not as the literal original source; even on a stripped binary, Ghidra can still delimit and recover many functions by detecting prologues, following calls, and applying signature and heuristic analysis, all without symbols. Data types and cross-references Applying correct data types and a function signature changes nothing about the machine code, but it changes everything about the decompilation: the decompiler propagates types through the reconstruction, turning raw pointer arithmetic into named struct field accesses and turning anonymous calls into readable, typed function calls. Cross-references (xrefs) let you navigate the other axis of the binary's structure, from a function to every place it is called, from a global or string to every place it is read or written, and from a data item back to the code that consumes it, which is often the fastest way to find the logic that matters inside an unfamiliar binary. The Ghidra script interface Ghidra exposes its analysis results, not just raw bytes, to scripts written in Java or in Python through its bundled interpreter: the listing, the function manager, the symbol table, and the reference graph are all reachable as API objects. A script can walk every function, inspect its callers via xrefs, or apply a naming convention across the whole program. Ghidra's headless analyzer runs this same import-analyze-script pipeline from the command line with no Graphical User Interface (GUI) at all, which is what lets static analysis be batched across many files rather than repeated by hand, a theme the next module develops directly. Related CCI capabilities Computer Architecture (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/computer-architecture/). Optics Primer Series (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/optics/). Maths Refresher Series, Finance (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/maths-finance/). System Dynamics (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/system-dynamics/). CCI Lab: Run it, build with it, read the thinking, reuse the data. (https://www.cambridgecyberinternational.com/en/insights/lab/) Ghidra's place in a reverse engineer's toolkit The National Security Agency's 2019 decision to release Ghidra as open-source software, first announced publicly at that year's Rivest-Shamir-Adleman (RSA) Conference, the industry's largest annual security-industry gathering, gave the wider security community free access to a static-analysis and decompilation tool previously used only inside classified government work, and its rapid, wide adoption since is itself informative: a capable decompiler had previously been a expensive, specialised purchase, and Ghidra's release measurably changed who could afford to do serious static reverse engineering. Its P-code intermediate representation and the SLEIGH specification language that describes each processor to it are what let one analysis engine support dozens of architectures without a separate hand-written decompiler for each. Treating decompiler output as a strong hypothesis rather than verified ground truth, the discipline this module insists on, matters most exactly where the decompiler's type inference is weakest: an undefined blob of bytes that the decompiler renders as raw arithmetic is not evidence there is no structure there, it is evidence the decompiler has not yet been told what structure to expect, and supplying that structure, a struct definition, a function signature, is analyst work no automation yet fully replaces. Ghidra's static analysis, and the discipline of treating its decompiler output as a hypothesis rather than ground truth, sets up directly for Module 06's automation work and Module 07's dynamic analysis, both of which exist to confirm or correct what static reading alone could only suggest. Automating and comparing. Writing a Ghidra script to automate a task, and judging when the homegrown decoder or in-browser automation wins instead.. Describe a static-analysis task, then automate it with a Ghidra script.. Automate the same task over the homegrown decoder.. Assess which instrument fits a given job.. Scripting Ghidra, In-browser automation, When each instrument wins, Combining the two Automating and comparing Manual analysis does not scale: a task worth doing once by hand is usually worth automating the moment it repeats across many functions or many samples. This module turns the two instruments you now have, Ghidra and the homegrown decoder, into repeatable pipelines, and asks the harder question of which one actually fits a given job. Scripting Ghidra A Ghidra script automates a repetitive analysis task, for example tagging every call to a given function, enumerating all strings referenced by a function, counting cross-references to an import, or renaming functions by convention. Because scripts reach the program's listing, function manager, symbol table, and references through Ghidra's API, they can express in a few lines what would be hundreds of manual clicks, and the same script reruns identically as new samples arrive. The headless analyzer runs this pipeline without a Graphical User Interface (GUI) at all, letting the same script batch over an entire corpus of binaries from the command line. In-browser automation The homegrown decoder, and the WebAssembly (WASM) disassembler from Module 04, can be scripted just as directly inside the sandbox: walk every decoded instruction, filter for a pattern, or diff two decodings of the same target. This automation stays inside the deterministic, auto-graded sandbox, which is exactly what makes a lab's pass condition reproducible. When each instrument wins Ghidra is the better instrument whenever you need its decompiler, its mature cross-reference analysis, or its multi-architecture loaders operating on real binaries, Portable Executable (PE), Executable and Linkable Format (ELF), or Mach-O. The homegrown decoder, or in-browser automation more generally, wins when the Instruction Set Architecture (ISA) is custom, embedded, or teaching-only and unsupported by a production tool, when the task must stay inside a deterministic sandbox for grading, or when you specifically want a transparent, auditable decode over a real tool's opaque internals. Neither instrument is universally correct; the choice is a judgement about the target and the constraint. Combining the two A mature workflow uses Ghidra for decompilation and heavy static analysis and the homegrown decoder for custom encodings or sandboxed automation, each where it is strongest, rather than forcing one tool to do both jobs. Running the same automated task on both instruments and comparing results is itself a valuable check: agreement raises confidence, and disagreement localises exactly where one tool's blind spot lies, since static automation of any kind inherits the disassembler's inability to resolve indirect control flow, so any automated result is properly read as a lower bound on what is really there, not a complete account. Related CCI capabilities Computer Architecture (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/computer-architecture/). Optics Primer Series (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/optics/). Maths Refresher Series, Finance (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/maths-finance/). System Dynamics (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/system-dynamics/). CCI Lab: Run it, build with it, read the thinking, reuse the data. (https://www.cambridgecyberinternational.com/en/insights/lab/) Automation as a force multiplier, not a replacement A Ghidra script is valuable exactly where manual, repetitive work does not scale: tagging every caller of a sensitive function across a large binary, or running the identical analysis pass across a corpus of hundreds of malware samples, is the kind of task no analyst should do by hand even once, let alone consistently across every sample. The headless analyzer, which runs Ghidra's full import-analyze-script pipeline from the command line with no graphical interface at all, exists specifically to make this kind of batch automation practical at a scale a single analyst clicking through a user interface never could reach. Automation does not replace judgement, though, which is exactly why this module pairs it with the homegrown decoder from Modules 02 and 03: a script that reliably tags callers reached through direct calls will just as reliably miss callers reached only through an unresolved indirect call, and knowing that limitation, rather than trusting a script's output as complete, is the same discipline this course has built from Module 03's recursive-descent blind spot onward. Deciding which instrument fits a given job, rather than defaulting to whichever is most familiar, is exactly the judgement Module 09's capstone asks you to exercise across all three instruments this course has built: the homegrown decoder, Capstone, and Ghidra. Dynamic analysis. Tracing, breakpoints and observing state in the sandbox emulator, against what static reading alone can recover.. Trace a target's execution in the sandbox.. Configure breakpoints and observe state.. Recover a value that static analysis alone hides.. Tracing execution, Breakpoints, Observing state, Static versus dynamic Dynamic analysis Everything so far has read a binary without running it. Dynamic analysis flips that: it observes the program as it actually executes, including register and memory state over time, recovering values that static reading alone can only guess at. Tracing execution An execution trace is the ordered record of instructions actually executed, and often the state at each step, showing the real path taken through the code rather than every path the code merely contains. In the sandbox emulator you can single-step a target and record this trace directly, which turns an abstract Control-Flow Graph (CFG) into a concrete, followed route through it. Breakpoints A breakpoint halts execution at a chosen location so state can be inspected. The classic x86 software breakpoint overwrites the target byte with 0xCC (the software INTerrupt (INT) 3 instruction), traps when the CPU hits it, and restores the original byte so execution can continue cleanly. A hardware breakpoint instead uses dedicated CPU debug registers, so it never modifies the instruction stream at all, and it can additionally watch data accesses (a read or write to an address) rather than only instruction fetches. Real malware and protected software actively look for both: calling IsDebuggerPresent or reading the being-debugged flag in the Process Environment Block (PEB) on Windows, timing code with the ReaD Time-Stamp Counter (RDTSC) instruction to notice the slowdown single-stepping introduces, or scanning its own code for the 0xCC byte a software breakpoint leaves behind. A reverser has to anticipate all three. Observing state Dynamic analysis earns its keep on values that only exist at runtime: a string decrypted just before use and wiped immediately after, or a key derived from the environment, will never appear as a literal in a static disassembly. The reliable way to capture such a value is to set a breakpoint immediately after the routine that produces it and dump the live buffer before it is destroyed, observing state that static reading structurally cannot recover. Static versus dynamic Static analysis gives full structure but no runtime values; dynamic analysis gives real values but only for the paths actually exercised by the inputs given, leaving untriggered branches unobserved. The two are complementary rather than competing: pairing them covers each one's blind spot. This is not merely academic. In May 2017, researcher Marcus Hutchins helped halt the spread of the WannaCry ransomware by reversing a sample, spotting a hard-coded, unregistered domain it queried before encrypting files, and registering that domain, which acted as an unintended kill switch. Malware analysts run this kind of dynamic observation inside an isolated sandbox specifically to contain the sample, so a live, executing threat cannot harm the host or reach the network while its behaviour is being studied. Related CCI capabilities Computer Architecture (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/computer-architecture/). Optics Primer Series (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/optics/). Maths Refresher Series, Finance (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/maths-finance/). System Dynamics (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/system-dynamics/). CCI Lab: Run it, build with it, read the thinking, reuse the data. (https://www.cambridgecyberinternational.com/en/insights/lab/) Dynamic analysis as static analysis's necessary complement Static analysis reads what a binary could do across every path it might take; dynamic analysis observes what a binary actually did along one specific, executed path, and neither view alone gives a complete picture. A value that a static reading can only describe as "computed somewhere from unknown inputs," because it depends on a decrypted string, a network response, or an environment check, is often trivial to simply observe once the target is actually run under a debugger, which is exactly the trade this module's lab exercises directly. Anti-debugging techniques exist precisely because malware authors and, less adversarially, commercial software vendors protecting a licence check both know dynamic analysis is often the faster route to an answer, and every anti-debugging technique this module names, IsDebuggerPresent, the Process Environment Block's being-debugged flag, timing checks built on the ReaD Time-Stamp Counter instruction, self-scanning for a software breakpoint's telltale 0xCC byte, is a documented, real technique found in production malware and copy-protection schemes alike, not a hypothetical this course invented for teaching purposes. Module 08's binary-patching work depends directly on this module's dynamic-analysis skills: verifying that a patch actually changed the target's runtime behaviour, rather than merely looks correct in a hex editor, is dynamic analysis applied to your own edit rather than to an unknown target. Binary patching. Editing instructions, neutralising a check, redirecting control flow, and verifying the patch by re-running the target.. Explain how editing instructions changes a target's behaviour, then edit them.. Construct a safe control-flow redirect.. Verify a patch by re-running the target.. Editing instructions, Neutralising a check, Redirecting control flow, Verifying the patch Binary patching Reading a binary tells you what it does; patching it changes what it does. This module turns analysis into an edit: modifying instruction bytes directly, in a way that is safe for the Instruction Set Architecture (ISA)'s encoding rules and confirmed to actually work by re-running the target. Editing instructions On a fixed-width ISA, replacing one instruction with another is only safe if the replacement fits the same instruction slot width, so nothing after it shifts out of place. On a variable-length ISA like x86, the safe way to remove an unwanted instruction without shifting anything downstream is to overwrite its bytes with No-OPeration (NOP) instructions (0x90 on x86) totalling the same length, leaving every later address unchanged. When there truly is no room to insert new logic in place, a code cave, unused padding or gap space already present in the binary, gives a patcher somewhere to place new instructions, reached by a jump inserted at the point that needs them. Neutralising a check A common goal is inverting a decision a binary makes, most often by flipping a conditional branch: changing the Jump-if-Zero (JZ) instruction to Jump-if-Not-Zero (JNZ), or vice versa, reverses which outcome a check leads to. This is a minimal, surgical edit compared with removing the check's logic entirely, and it is far easier to verify. Redirecting control flow Patching a call target, or inserting a new relative jump, requires more than writing a new address: relative branches and calls encode a displacement measured from the instruction following them, so the displacement must be recomputed relative to the new instruction's address, not copied from wherever the target happened to be reached from before. Getting this arithmetic wrong sends control to the wrong place entirely, often crashing far from the point of the actual patch. If the target verifies its own integrity with a checksum or hash, any naive byte patch will trip that self-check, so the patch must also satisfy or neutralise the integrity check itself, not just the behaviour you set out to change. Some values worth changing live only on the stack or heap at runtime rather than as a fixed instruction; those are better handled as a dynamic patch, writing memory live while the target runs under a debugger or emulator, rather than editing the file on disk. Verifying the patch A patch is not done when the bytes are written; it is done when the target is re-run and the intended behaviour is confirmed to have actually changed, and nothing else has broken. Re-running, not just re-reading, is essential because the pass condition is defined on observed runtime behaviour, and because it catches side effects a static edit did not anticipate: a learner who flips a licence check's JNZ to JZ but finds the program crashing later has not finished, and the disciplined response is to trace past the patch to find the side effect the original branch was guarding, and adjust the patch so the intended behaviour holds without the crash, rather than patching more bytes at random. Related CCI capabilities Computer Architecture (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/computer-architecture/). Optics Primer Series (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/optics/). Maths Refresher Series, Finance (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/maths-finance/). System Dynamics (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/system-dynamics/). CCI Lab: Run it, build with it, read the thinking, reuse the data. (https://www.cambridgecyberinternational.com/en/insights/lab/) Patching as the final, verifiable step Binary patching is where reverse engineering stops being purely descriptive and becomes an intervention: every earlier module in this course builds understanding, but a patch changes the target's actual behaviour, which is exactly why this module insists a patch is only complete once the patched target has actually been re-run and observed to behave as intended, not merely once the bytes look correct on inspection. A patch that looks correct in a hex editor but has never been executed is a hypothesis, not a verified result, the same discipline Module 09's capstone applies to an entire unknown binary rather than a single instruction. The code-cave technique this module introduces, redirecting execution into unused padding space to run new logic that would not otherwise fit, is a direct consequence of the same fixed-width-versus-variable-length distinction Module 00 opened the course with: a fixed-width ISA leaves no room to grow an instruction in place at all, while x86's variable length at least permits shrinking, via NOP padding, but rarely permits growing without a jump to borrowed space elsewhere. This same re-run-to-verify discipline, not just re-read, is what Module 09's capstone scores a patch against: a patch that has not been confirmed by actually re-running the target is not yet a finished patch, regardless of how carefully the bytes were chosen. Capstone: dissect a small unknown binary. Use the homegrown decoder, the WASM disassembler and Ghidra together to map a small unknown binary's behaviour and patch a defined property, graded at the review gates.. Identify an unknown binary's behaviour using all three instruments.. Implement a patch for a stated property and verify it.. Defend the analysis at the review gates, within the legal and ethical bounds.. Planning the analysis, Static and dynamic together, Patching the property, Defending the analysis Capstone: dissect a small unknown binary The capstone asks you to bring every instrument built or introduced across this course to bear on one target: the homegrown decoder, the WebAssembly (WASM) disassembler, and Ghidra, used together to map an unknown binary's behaviour and patch a defined property, all within the course's legal and ethical bounds. Planning the analysis A sound plan begins with triage, not patching: identify the file format, the target architecture, the entry point, and scan the strings and imports for early hypotheses about what the binary does before committing to deep analysis. Real-world first-pass triage is complicated by packers such as the Ultimate Packer for eXecutables (UPX), which compress or transform the real code so that it only appears in memory after an unpacking stub runs, meaning the code you see on disk may not be the code that executes. The right division of labour follows each instrument's strength: static disassembly and decompilation for structure, dynamic tracing for runtime values, and the homegrown decoder for any custom encoding the target might use. Static and dynamic together When static and dynamic findings disagree about the target's behaviour, that disagreement is informative, not a failure: it usually marks a path the dynamic run never executed, or an anti-analysis trick worth understanding on its own terms, and it should be reconciled rather than discarded. Control flow is not always as localised as a single function either; return-oriented programming, introduced by Hovav Shacham in 2007, chains together existing code fragments that each end in a ret instruction, so a behaviour can be expressed as a sequence of gadgets scattered across the binary rather than as one obvious routine, a pattern worth checking for when a target's behaviour resists a straightforward Control-Flow Graph (CFG) reading. Patching the property Once the relevant logic is located and understood, the same discipline from the patching module applies: make the minimal instruction-level edit that changes the stated property, respecting the Instruction Set Architecture (ISA)'s encoding rules so nothing downstream shifts or desynchronises, and then re-run the target to confirm the change actually took effect rather than assuming it from the static edit alone. Defending the analysis A strong submission shows three things at the review gates: a recovered behaviour map of the binary's functions, key logic, and data that is evidence-backed rather than guessed; a verified patch of the stated property, confirmed by re-running the target; and an account of the legal and ethical bounds the work stayed within. Those bounds are not incidental. In Sega Enterprises Ltd. v. Accolade, Inc. (9th Cir. 1992), the court found that intermediate copying through disassembly can be fair use where it is the only way to access unprotected functional elements needed for interoperability, and in Sony Computer Entertainment v. Connectix Corp. (9th Cir. 2000), reverse engineering the PlayStation Basic Input/Output System (BIOS) to build a compatible emulator was likewise found to be fair use. The Digital Millennium Copyright Act (DMCA)'s 17 U.S.C. 1201(f) codifies a related exemption for reverse engineering undertaken to achieve interoperability of an independently created program. SCALPEL is explicitly a defensive course: its targets are course-provided teaching binaries, analysed within law and licence, never shipping third-party products the learner does not own. Related CCI capabilities Computer Architecture (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/computer-architecture/). Optics Primer Series (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/optics/). Maths Refresher Series, Finance (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/maths-finance/). System Dynamics (Course) (https://www.cambridgecyberinternational.com/en/insights/academy/system-dynamics/). CCI Lab: Run it, build with it, read the thinking, reuse the data. (https://www.cambridgecyberinternational.com/en/insights/lab/) Reverse engineering within legal and ethical bounds The two landmark United States court decisions this module cites, Sega Enterprises Ltd. v. Accolade, Inc. in 1992 and Sony Computer Entertainment v. Connectix Corp. in 2000, both establish the same underlying principle from different angles: intermediate copying through disassembly, undertaken specifically to achieve interoperability with a system whose unprotected functional elements are otherwise inaccessible, can be lawful fair use even though the disassembly process itself necessarily creates a full copy of copyrighted code along the way. The Digital Millennium Copyright Act's own interoperability exemption, codified at 17 U.S.C. 1201(f) after the Act's passage in 1998, was written with exactly this kind of legitimate reverse engineering in mind, not as a loophole but as a deliberate accommodation for research and compatibility work Congress recognised as legitimate. None of this licenses reverse engineering a commercial product you do not own or have not been licensed to analyse, which is precisely why this course, from its very first module, has worked exclusively against course-provided teaching binaries: the skills this capstone exercises, mapping behaviour with all three instruments, patching a stated property, defending the analysis at a review gate, transfer directly to authorised security research, but the authorisation itself is not optional, and a capstone submission that could not honestly state its target was course-provided and its analysis stayed within licence has not met this course's own bar regardless of its technical quality. Every skill this capstone draws on, decoding, disassembling, reading a decompiler's output, tracing execution, patching and verifying, was built and checked module by module across this entire course specifically so the capstone could ask you to combine them under the same legal and ethical discipline Module 09 opened with, not to introduce anything genuinely new.