Skip to content
Anti-Disassemblyintermediate

GetPC Thunk / call $+5 pop

Position-independent code recovers its own runtime address with a call/pop pair, spawning a phantom call xref that confuses static analysis.

Shellcode and packed stages rarely know where they will be mapped, so they need a way to learn their own runtime address before referencing embedded strings, keys, or import tables. The classic GetPC (get-program-counter) idiom does this with a call to the very next instruction followed by a pop: the call pushes the return address — which is the current location — onto the stack, and the pop lifts it into a register. It is the foundation of nearly every x86 decoder stub, including the Metasploit call $+4/call $+5 thunks and the Shikata Ga Nai polymorphic encoder.

The side effect is an anti-disassembly one. A linear or naive recursive disassembler treats every call rel32/call rel8 as a function invocation and creates a code cross-reference to the target. But a GetPC call targets the middle of its own basic block — the instruction immediately after it — so the tool emits a phantom sub-function and a bogus xref, fragments the block, and often mislabels the pop as a separate routine. The "called" address is never a real function; the call is just an arithmetic trick to read EIP.

How it works

The canonical thunk is five bytes for the call and one for the pop. The call displacement is 0 (target = the next byte), so it does nothing but push its own end address:

asm
; Bytes:  E8 00 00 00 00  5B  81 C3 ...
00: E8 00 00 00 00   call  0x05        ; rel32 = 0 -> target is the next insn (0x05)
05: 5B               pop   ebx         ; EBX = 0x05  (the runtime address of this byte)
06: 81 C3 27 10 00 00 add  ebx, 0x1027 ; rebase EBX to the embedded data blob

A linear sweep sees call 0x05 and dutifully records a cross-reference to 0x05, the same address it is about to decode anyway — so the byte at 0x05 becomes both "fall-through from the call" and "target of the call," and many tools split the block, attach a spurious sub_5 label, and lose the fact that pop ebx is simply consuming the pushed PC. The call/pop pair is pure self-reference: no control transfer of consequence occurs, yet the static xref graph now contains an invented edge.

A compact variant overlaps the displacement with real code to compound the confusion:

asm
; Bytes:  EB 03 E8 ...  /  ... E8 F8 FF FF FF 59
00: EB 03            jmp  short 0x05    ; skip the next 3 bytes
05: E8 F8 FF FF FF   call 0x02          ; rel32 = -8 -> backward into the skipped region
0A: 59               pop  ecx           ; ECX = 0x0A, recovered from the backward call

Here the call jumps backward into bytes the jmp already skipped, so the disassembler's forward sweep and the real call target disagree, while the pop still harvests the program counter for rebasing.

Detection & analysis

Static analysis:

  • The fingerprint is a call whose rel8/rel32 displacement is 0, -5, or another tiny value pointing into or just past itself, immediately followed by a pop reg. IDA/Ghidra will frequently show a one-instruction "function" or a red xref arrow that loops back inside the same block.
  • Treat any E8 00 00 00 00 (call $+5) as a GetPC marker, not a call; undefine the bogus sub-function and re-form the block so the pop and subsequent add reg, imm (the rebase) read as a single prologue.

Dynamic analysis:

  • Single-step the stub: after the call/pop the recovered register holds the live address of the thunk, which you can subtract back to a file offset to locate the embedded blob the code is about to decode or rebase against.
  • Emulators (Unicorn, IDA's appcall, speakeasy) execute the thunk and expose the resolved base register, letting you follow the decoder loop to plaintext.

Detection rule hint:

Flag the byte pattern E8 00 00 00 00 followed by any 5x/58–5F (pop r32), and the jmp-over-then-backward-call-then-pop variant. A call with a near-zero self-referential displacement directly feeding a pop is a high-confidence GetPC indicator: legitimate compilers reach a PC via lea/RIP-relative addressing or __x86.get_pc_thunk helper calls, not via a zero-displacement call consumed immediately by a pop.

Votes

Comments(0)