TL;DR
This guide explains how to understand and defeat a common anti-analysis technique in malware called “jmp-pop-call”. It involves using indirect jumps, popping values from the stack, and then calling those addresses. We’ll cover how it works, why attackers use it, and practical steps for reversing it.
Understanding Jmp-Pop-Call
The jmp-pop-call technique is a way to hide where code actually goes when a program runs. Instead of directly calling functions or jumping to specific locations, the code does this:
- Push Addresses: It pushes addresses onto the stack. These are usually addresses of important functions or code blocks.
- Pop Address: It pops an address off the stack into a register (like
EIPon x86). - Call/Jump: It then calls or jumps to that popped address.
This makes static analysis harder because you don’t immediately see where the code is going; it depends on what’s been pushed onto the stack.
Why Attackers Use It
- Obfuscation: Hides control flow, making reverse engineering more difficult.
- Anti-Debugging: Can be used to detect debuggers by manipulating the stack or checking the environment.
- Polymorphism: Changes the code’s appearance without changing its functionality.
Decoding Jmp-Pop-Call – Step-by-Step
- Identify the Pattern: Look for sequences like
push address, followed bypop register, and thencall/jmp register. Disassemblers (like IDA Pro or Ghidra) can help highlight these patterns. - Trace Stack Operations: The key is to understand what’s being pushed onto the stack *before* the pop instruction. Use a debugger to step through the code and observe the stack contents.
- Find Push Locations: Locate all instances of
push addressthat feed into the jmp-pop-call sequence. These are where the target addresses are stored. - Dynamic Analysis (Debugging):
- Set a breakpoint before the
pop registerinstruction. - Step over the
pop registerto see what address is loaded into the register. - Step into the
call/jmp registerto follow the execution flow.
- Set a breakpoint before the
- Static Analysis – Address Resolution: Once you know the addresses being pushed, try to resolve them in your disassembler.
- If the address is within the same executable, the disassembler should automatically link it.
- If it’s an external library function, ensure your disassembler has the correct symbol files (PDB for Windows).
- Sometimes addresses are calculated dynamically; you may need to find where these calculations happen and apply them statically.
- Patching (Optional): If you want to simplify analysis, you can patch the code to directly call/jump to the resolved addresses.
// Example: Replace jmp-pop-call with a direct jump; Original Codepush 0x12345678 ; Address of function Apop eax ; Pop address into EAXjmp eax ; Jump to the popped address; Patched Codejmp 0x12345678 ; Direct jump to function A - Scripting (Advanced): For complex jmp-pop-call chains, consider writing a script (e.g., using IDA Python) to automatically resolve addresses and patch the code.
# Example IDA Python snippet (very basic)for func in Functions(): for xref in XrefsTo(func): if xref.type == XREF_TYPE_CALL: print("Potential jmp-pop-call target: %s" % func)
Tools
- Disassemblers: IDA Pro, Ghidra, Radare2
- Debuggers: x64dbg, OllyDbg (Windows), GDB (Linux)
- Decompilers: IDA Pro’s decompiler, Ghidra’s decompiler

