FPU Instruction Pointer Abuse
Malware recovers its own runtime address through the FPU's saved instruction pointer via fnstenv, hiding the GetPC step from static xref analysis.
Decoder stubs need their own runtime address before they can touch embedded
strings, keys, or import data, and the obvious way to get it — a call $+5/pop
GetPC thunk — leaves a loud, recognisable signature. A quieter alternative
exploits the x87 FPU: when any floating-point instruction executes, the CPU
records the address of the last non-control FPU instruction in an internal
register. The fnstenv (or fstenv) instruction dumps that saved environment,
including the FPU instruction pointer, to memory, where the stub can read it
back as its own program counter. This is the heart of the Shikata Ga Nai
encoder's GetPC step.
The anti-disassembly effect is the absence of a signal. Unlike a call, the
fnstenv path creates no cross-reference, no phantom sub-function, and no
suspicious self-referential branch for a disassembler to flag. The address
recovery happens silently inside the FPU state, so an analyst skimming the call
graph sees an innocuous floating-point op followed by a stack read, with no
obvious link to position-independent code. The dependency on a previous FPU
instruction is easy to miss because that instruction is often a harmless fldz
or fnop planted several bytes earlier.
How it works
A throwaway FPU instruction sets the saved IP; fnstenv then writes a 28-byte
environment block to the stack, and offset +0x0C of that block is the address
of the FPU instruction. The stub pops that field straight into a register:
; Bytes: D9 EE D9 74 24 F4 5B 81 C3 ...
00: D9 EE fldz ; harmless FPU op -> saved FPU IP = 0x00
02: D9 74 24 F4 fnstenv [esp-0x0C] ; dump 28-byte env; +0x0C field = 0x00
06: 5B pop ebx ; EBX = saved FPU IP (0x00), i.e. our PC
07: 81 C3 1B 10 00 00 add ebx, 0x101B ; rebase EBX onto the embedded blobThe fnstenv [esp-0x0C] deliberately writes the environment so that its
instruction-pointer field lands exactly at the new top of stack; the immediate
pop ebx then lifts that pointer — the address of the fldz at 0x00 — into
EBX with no call ever executed. A linear sweep decodes all four instructions
cleanly and records no xref, so nothing in the listing hints that EBX now
holds a runtime PC. The connection between fldz and the value popped is purely
a CPU-state side channel the disassembler cannot model.
Packers vary the seed instruction to dodge naive byte signatures:
; Bytes: D9 D0 D9 74 24 F4 59 ...
00: D9 D0 fnop ; alternate seed; saved FPU IP = 0x00
02: D9 74 24 F4 fnstenv [esp-0x0C] ; same env dump, +0x0C = 0x00
06: 59 pop ecx ; ECX = recovered program counterAny non-control FPU instruction (fldz, fld1, fnop, fldpi) works as the
seed, so the only invariant is the fnstenv/pop pair — and even the
[esp-0x0C] displacement shifts when the stub reserves a different stack frame.
Detection & analysis
Static analysis:
- The fingerprint is a non-control FPU instruction (
D9 EEfldz,D9 D0fnop,D9 E8fld1) shortly followed byfnstenv/fstenvand then apop reg. IDA/Ghidra show no xref, so search the bytes, not the call graph. - Treat the
pop regafterfnstenvas the GetPC result: trace the subsequentadd reg, immto compute the file offset of the blob the stub will decode. - Disassemblers rarely annotate the
+0x0Cenvironment field, so manually note that the popped register now equals the address of the seed FPU instruction.
Dynamic analysis:
- Single-step to the
pop; the register then holds the live address of the seed instruction. Subtract the image base to map it to a file offset and follow the decoder loop to plaintext. - Emulators (Unicorn, speakeasy) model the FPU environment block, so they expose the recovered base register without needing real FPU hardware semantics.
Detection rule hint:
Flag any fnstenv/fstenv whose destination is [esp-0x0C] (or an equivalent
stack slot landing the IP field at the top of stack) when immediately followed by
a pop r32 — especially when a seed FPU instruction (fldz/fnop/fld1)
precedes it. Legitimate code saves the full FPU environment around context
switches and never pops a single field straight into a general register, making
the fnstenv+pop couple a high-confidence FPU-GetPC indicator.