Skip to content

Return-Pointer Abuse

Uses push/ret and call-to-pop sequences as obfuscated control transfers so branches do not appear as calls or jumps, defeating call-graph recovery.

Disassemblers build the call graph and cross-references by tracking the targets of call, jmp and jcc instructions. Return-pointer abuse breaks that model by performing control transfers through the stack and the ret instruction instead. A ret normally ends a function and returns to its caller; when the malware pushes an arbitrary address and then executes ret, the processor jumps to that address with no call or jmp in sight. The disassembler sees a function epilogue, terminates the basic block, and records no edge — so the destination shows up as never-called, unreferenced code.

The complementary trick is call-to-pop: a call whose target is the very next instruction, where a pop immediately captures the pushed return address into a register. The malware uses this to obtain its own runtime address (for position-independent code and self-decryption) while simultaneously presenting a call with no real callee body. Together these patterns turn linear control flow into a chain of stack manipulations that automated call-graph recovery and xref-based navigation cannot follow.

How it works

A push/ret pair replaces a direct jump and erases the cross-reference:

asm
; "jmp loc_decrypt" rewritten as push/ret — no jmp/call xref is created
00:  68 EF BE AD DE   push 0xDEADBEEF     ; address of the real next stage
05:  C3               ret                 ; "returns" to 0xDEADBEEF (a jump!)
;                                          ; disassembler ends the block here,
;                                          ; records NO edge to 0xDEADBEEF

The processor pops 0xDEADBEEF and transfers there, but the static tools treat C3 as a return and stop. The target is reached at runtime yet appears orphaned. A call/pop pair, by contrast, is used to read EIP/RIP and to plant a fake return target:

asm
; call-to-pop: get current address, no real subroutine exists
00:  E8 00 00 00 00   call 0x05           ; target = next instruction
05:  58               pop  eax            ; eax = 0x05 (the return address on the stack)
06:  ...                                  ; eax now holds a runtime self-reference

The call pushes the address of 0x05 and "calls" 0x05 itself; pop eax takes that address off the stack. There is no callee — the disassembler invents a phantom subroutine at 0x05, mangling the function layout. A more aggressive form rewrites the return address on the stack before a legitimate-looking ret:

asm
; redirect a normal-looking ret to an attacker-chosen target
00:  58               pop  eax            ; discard the real return address
01:  68 00 10 40 00   push 0x00401000     ; substitute a new "return" target
06:  C3               ret                 ; transfers to 0x00401000, not the caller

To the call-graph builder this is an ordinary function return; the substituted destination is never linked as an edge, so the recovered graph is wrong.

Detection & analysis

Static analysis:

  • Search for push imm / push reg immediately followed by ret — a near-certain obfuscated jump. IDA and Ghidra scripts (and patterns in capa) flag push;ret and call $+5;pop sequences specifically.
  • For call/pop, look for a call whose target is the instruction directly after it (a 5-byte E8 00 00 00 00) followed by a pop — the get-EIP idiom.
  • After identifying the pushed constant, manually add a cross-reference / create a code chunk at that address so the call graph reconnects.

Dynamic analysis:

  • Single-step over the ret in a debugger and read the new EIP/RIP — it reveals the true (otherwise unreferenced) destination. Set a hardware breakpoint there to continue tracing.
  • A branch trace (Intel PT, last-branch records, or an emulator) records the real transfer at the ret/call, reconstructing the edges the static graph is missing.

Detection rule hint:

Flag any ret that is immediately preceded by a push imm32/push reg with no intervening call frame, and any 5-byte call $+5 (E8 00 00 00 00) followed by a pop. These stack-based control transfers create execution edges that no call/jmp instruction expresses, and their presence is a strong indicator of deliberate call-graph obfuscation rather than normal subroutine returns.

Votes

Comments(0)