• Home
  • TTP
  • Pages
  • p5.1_decompilation.logic // The Ghost in the Machine.

0x01: What is Decompilation?

Decompilation is the process of translating low-level Assembly back into a high-level language like C. While a disassembler shows you every single MOV and ADD instruction, a decompiler analyzes those patterns and generates Pseudocode.

The Advantage: It allows you to “skim” through thousands of lines of code to find the core logic quickly.

The Limitation: Compilers are “lossy.” When code is compiled, the “human” parts—variable names, comments, and structure—are stripped away and destroyed.

0x02: The “Lossy” Nature of the Binary

When you decompile a function, you aren’t seeing the original source code; you are seeing a “best guess” by the decompiler (like Ghidra or IDA Pro).

FeatureIn Original SourceIn Decompiled Pseudocode
Variable Namesint retry_countint iVar1
Comments// Check for overflow[REDACTED]
Logic StructureClear for or while loopsOften complex goto or if jumps
Function Namesvalidate_user()FUN_00401234 (if symbols are stripped)

Because aggressive compilers optimize code to be fast rather than readable, they might reuse the same register for three different variables. A decompiler might get confused and merge those three variables into one, leading to an Analysis Error.


0x03: IDA Pro vs. Ghidra

In the field, you will likely use one of the two “Giants”:

  1. IDA Pro (Hex-Rays): The industry standard. Known for extremely clean pseudocode but comes with a high price tag.
  2. Ghidra (NSA): A powerful, free, open-source alternative. Its decompiler is excellent and highly customizable.

As seen in the file_record example, both tools show the “ghost” of the logic. You can see the if statements and function calls, but the intuitive feel of the original code is gone. This is why you must still be able to read the Assembly—it is the only source of truth when the decompiler makes a mistake.


0x04: Tactical Warning: Decompiler Deception

A decompiler can lie to you. If a compiler uses a clever optimization (like using a subtraction to perform a comparison), the decompiler might misinterpret the intent.

Interactive Lab (Pseudocode vs. Truth)

You are reviewing a decompiler’s output. The decompiler suggests a variable is a simple integer, but the assembly shows it is being used as an array pointer.

[DECOMPILATION_INTEGRITY_v5.1]
DECOMPILER OUTPUT if (uVar1 == 0) {
  return 1;
}
RAW ASSEMBLY cmp r0, #0
moveq r0, #1
bx lr

MISSION: The decompiler named the variable uVar1. Looking at the assembly, which Register did the decompiler use to generate that variable name?

0x05: Mission Task

Current Objective: Open Ghidra and load the hello binary we compiled in previous steps.

Challenge: Locate the main function in the Symbol Tree and view the Decompiler window. Compare the decompiled code to your original hello.c source.

Observation: Did Ghidra successfully recover the string "Hello!\n", or did it just show a memory address? This reveals how well the decompiler handles data references.

0x06: The C++ Abstraction Layer

C++ is a “superset” of C, meaning it adds features like Classes, Constructors, and Operator Overloading. These features make life easier for developers but add “noise” for the Reverse Engineer.

When we decompile C++, we often see:

  1. The this Pointer: In C++, every class method needs to know which specific object it is working on. It does this by passing the memory address of the object as the first argument (usually in X0 or R0).
  2. Mangled Names: C++ allows multiple functions to have the same name (overloading). To handle this, the compiler “mangles” the names into complex strings like _ZN7OcsalyC1Ei.
  3. Constructors: These look like standard functions but are called automatically when memory is allocated.

0x07: Case Study — C++ Class to Assembly

Let’s look at a simple C++ Class and see how a decompiler/disassembler breaks it down.

class Vault {
public:
    int balance;
    Vault(int initial) { balance = initial; }
    void deposit(int amount) { balance += amount; }
};

int main() {
    Vault myVault(100);
    myVault.deposit(50);
    return 0;
}

The Disassembled Assembly (ARM64 Logic):

When we disassemble the deposit method, the “Magic” of C++ disappears, revealing the raw math.

<Vault::deposit(int)>:
  // X0 holds the 'this' pointer (address of myVault object)
  // W1 holds the 'amount' (50)
  ldr w2, [x0]      // Load current balance from memory at [x0] into w2
  add w2, w2, w1    // Add amount (w1) to current balance (w2)
  str w2, [x0]      // Store the new balance back into memory at [x0]
  ret               // Return to main

0x10 Operator Insight: Notice that there is no variable named balance in the assembly. There is only an Offset. The decompiler sees [x0] and has to “guess” that this memory location represents a class member.

0x08: Tactical Lab (C++ Logic Extraction)

You have intercepted a C++ binary. You must determine how the decompiler represents a class member variable during a subtraction operation.

[CPP_RECON_STATION_v5.2]
DECOMPILED PSEUDOCODE this->health = this->health - damage;
RAW ASSEMBLY LDR W2, [X0]
SUB W2, W2, W1
STR W2, [X0]

MISSION: In the assembly above, which register acts as the ‘this’ pointer (the address of the object in memory)?


0x09: Mission Task

Current Objective: Compile the C++ Vault code using g++ -O0 vault.cpp -o vault.

Challenge: Run nm vault | grep deposit. You will see a strange, long name like _ZN5Vault7depositEi.

Explanation: This is Name Mangling. Use the command c++filt _ZN5Vault7depositEi to “demangle” it back into human-readable C++. As a Reverse Engineer, c++filt is your primary tool for making C++ binaries readable.

0x10: Visualizing Logic (The Branching Path)

In high-level C++, a simple if-else statement looks like a choice. In a binary, that choice becomes a Conditional Branch.

Modern reverse engineering tools (Ghidra, IDA, Binary Ninja) don’t just show you a list of instructions; they generate a visual graph.

  • Nodes: Blocks of code that execute sequentially without jumping.
  • Edges: Arrows representing the “jumps” (branches) between those blocks.

0x11: The “If-Else” Logic in Binary

When the CPU encounters a decision, it uses two steps:

  1. The Comparison (CMP): This subtracts two values and sets the Status Flags (Zero, Negative, Carry).
  2. The Conditional Jump (B.EQ, B.NE, B.GT): This looks at the flags and decides whether to jump to a new address or continue to the next line.
if (balance >= 100) {
    status = "VIP";
} else {
    status = "Standard";
}

Assembly Flow (ARM64):

CMP W0, #100      // Compare balance (W0) to 100
  B.LT label_else   // If Less Than (LT), jump to 'else' block
label_if:
  ADR X0, vip_str   // If we didn't jump, we are in the 'if' block
  B label_end       // Must jump over the else block!
label_else:
  ADR X0, std_str   // Code for 'else' block
label_end:
  RET               // End of function

0x10 Operator Note: In the Control Flow Graph, label_if and label_else would be two separate boxes. The box containing the CMP instruction would have two arrows coming out of it: a Green arrow (Condition Met) and a Red arrow (Condition Failed).


0x12: Loop Reconnaissance

Loops are just “Backward Branches.” In a CFG, you can spot a loop instantly because one of the arrows points upward to a previous block.

  • For Loops: Usually have a counter (index) that is incremented and compared at the end of each block.
  • Infinite Loops: In malware, you often see a block that only has one arrow pointing back to itself—this is a “Stall” or a “Heartbeat” mechanism.

0x13: Tactical Lab (Branch Intelligence)

You are analyzing a security check binary. You must determine which branch leads to the “Access Granted” logic.

[CFG_LOGIC_GATE_v5.3]
BLOCK_A
CMP W0, #0xDEADC0DE
B.NE BLOCK_C
NOT_EQUAL (Jump) EQUAL (Fallthrough)
BLOCK_C
PRINT “ACCESS_DENIED”
BLOCK_B
PRINT “ACCESS_GRANTED”

MISSION: If register W0 contains the value 0xDEADC0DE, which block will the CPU execute next?

0x14: The Importance of Dead Code

In complex binaries, you will often find Orphan Blocks—pieces of code in the CFG that have no arrows pointing to them.

  • Malware Analysis: These are often “dormant” payloads waiting to be activated by a remote command.
  • Obfuscation: Developers sometimes add “Junk Code” with impossible conditions (e.g., if (1 == 2)) to confuse reverse engineers and break decompilers.

0x15: Mission Task

Current Objective: Open the vault binary in Ghidra. Click on the “Display Function Graph” icon in the top toolbar.

Challenge: Look at the deposit function. Does it have a straight vertical line (Single Block) or multiple branches?

Explanation: If you didn’t include an if statement in the deposit code, it should be a single box. If you added a check (e.g., if (amount > 0)), you will see the graph split into two paths. This is the visual proof of your code’s decision-making.