┌───────────────────────┐
▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄ │
│ █ █ █ █ █ █ │
│ █ █ █ █ █▀▀▀▀ │
│ █ █ █ █ ▄ │
│ ▄▄▄▄▄ │
│ █ █ │
│ █ █ │
│ █▄▄▄█ │
│ ▄ ▄ │
│ █ █ │
│ █ █ │
Control Flow That Isn't There: │ █▄▄▄█ │
State Without State via Flags, Timing, │ ▄▄▄▄▄ │
and Overlapping Execution │ █ │
│ █ │
~ febnug └───────────────────█ ──┘
[Abstract]
----------
Most shellcode keeps state somewhere. Registers, stack, memory, there is always
something you can point at.
This one doesn’t.
State is carried in flags. Control flow is selected at runtime. A single bit
derived from timing decides how execution enters a shared region of bytes.
The binary looks fixed. It is.
Same bytes, different executions. Static analysis recovers a path that may
never actually run.
[1. Introduction]
-----------------
Reverse engineering usually starts with a simple assumption: the binary
describes one program.
Instructions are static. Control flow is fixed. State is visible.
That assumption doesn’t always hold.
If control flow depends on flags that are never stored, and execution can begin
at different offsets within the same byte sequence, the structure you recover
is only a guess.
What you see is not necessarily what runs.
[2. Overview]
-------------
The whole construction is built from three small pieces:
- one bit stored in a flag
- one bit derived from timing
- one control transfer selected at runtime
Pseudo-flow:
t = rdtsc()
bit = (t.low ^ t.high) & 1
if bit:
CF = 1
else:
CF = 0
if CF:
jump overlap_shift
else:
jump overlap
[3. Flag-Based State]
---------------------
The Carry Flag (CF) is used as a one-bit state container. No memory writes or
explicit state tracking are required.
Pseudo-asm:
if entropy_bit:
stc
else:
clc
jc entry_B
The state is implicit. It is not stored or restored, but persists as a side
effect of execution and directly influences control flow. No variable holds
this state. It exists only as a flag.
[4. Timing as Entropy]
----------------------
A single bit of entropy is derived from rdtsc:
rdtsc
xor eax, edx
test al, 1
This value varies across executions and is sensitive to timing conditions,
including debugging and instrumentation.
The entropy is minimal (one bit), but sufficient to influence control flow.
[5. Runtime Control Selection]
------------------------------
This construction does not rely on instruction rewriting. Instead, control flow
is determined by how execution enters a shared region of bytes.
A timing-derived bit is mapped into CF and used to select between two entry
points:
jc entry_B
entry_A:
jmp overlap
entry_B:
jmp overlap_shift
Both entry points lead into the same contiguous byte region, but at different
offsets.
Because x86 decoding is alignment-sensitive, starting execution at different
offsets produces different instruction streams, even though the underlying
bytes are identical. The instructions do not change. The interpretation does.
[6. Overlapping Execution]
--------------------------
The overlap region consists of adjacent jump encodings:
overlap: db 0xeb, <rel real>
overlap_shift: db 0xeb, <rel alt>
These bytes are fixed at compile time.
However, depending on the entry point, execution begins at a different offset
within this region, resulting in a different decoded instruction sequence, and
therefore a different control flow path.
This is not a case of multiple code paths being stored separately. It is a
single byte sequence interpreted in multiple ways.
[7. Implementation]
-------------------
Execution begins by deriving a single bit from rdtsc and mapping it into the
Carry Flag (CF). This bit determines which control path is taken.
rdtsc
xor eax, edx
test al, 1
If the bit is set, CF is set via stc; otherwise it is cleared. The resulting
flag state is then used to select between two entry points:
jc entry_B
entry_A:
jmp overlap
entry_B:
jmp overlap_shift
Both entry points lead into the same contiguous byte region, but at different
offsets.
The overlap region is defined as two adjacent jump encodings:
overlap: db 0xeb, <rel real>
overlap_shift: db 0xeb, <rel alt>
These bytes are not modified at runtime. However, because execution may begin
at different offsets, they are decoded differently, leading to distinct control
flow paths.
The divergence is therefore not encoded in separate instructions, but emerges
from how a shared byte sequence is entered.
Payload data is embedded inline and accessed using short call/pop sequences:
jmp short data
ret:
pop rsi
This avoids absolute addressing and keeps the code position-independent.
No writable memory is required, and no instruction rewriting is performed.
The code remains static throughout execution. What varies is only the entry
point into the overlapping region.
[8. Shellcode Realization]
--------------------------
The construction can be expressed as position-independent shellcode.
The implementation avoids absolute addressing and relies only on relative
control transfers. Inline data is accessed via short call/pop sequences.
To support injection scenarios, the code can be written without null bytes by
using byte-sized immediates and short-form control transfers.
The resulting shellcode preserves the same properties:
- no explicit state storage
- runtime-dependent control flow
- overlapping execution regions
This shows that the technique is not tied to ELF binaries, but applies
directly to injected code.
[9. Implications]
-----------------
Static analysis assumes a fixed mapping from bytes to instructions.
This assumption breaks when execution depends on entry offset and implicit
state such as flags.
A disassembler will typically recover one valid control flow graph. That graph
may not correspond to any runtime execution.
The discrepancy is not due to obfuscation layers or encryption, but to
fundamental properties of the execution model.
[10. Behavior]
--------------
Running the program multiple times yields different outputs:
hello
holla
hello
hello
The exact sequence depends on timing and flag state.
Under debugging, timing shifts can bias execution toward a specific path,
making behavior environment-dependent.
[10. Conclusion]
----------------
A minimal construction has been presented where:
- state is carried in flags
- control flow is selected at runtime
- execution depends on entry into overlapping byte regions
No explicit state storage is required. No code mutation is necessary.
The code does not change. Only the path through it does.
[11. Proof-of-Concept]
----------------------
Below is a minimal implementation of the idea. It is position-independent and
works both as a small ELF and as raw shellcode.
Execution starts with rdtsc. One bit is extracted and pushed into CF using
stc/clc. Nothing is stored anywhere. The flag is the only state.
jc entry_B
From there, execution is forced into one of two entry points. Both land inside
the same byte region, just at different offsets.
The overlap itself is just two adjacent jumps:
overlap: jmp real
overlap_shift: jmp alt
No bytes are changed at runtime. The difference comes entirely from where
execution begins. Same bytes, different decode.
The payload is inline and recovered with a short call/pop. No absolute
references, no relocations.
At runtime, the program prints either "hello\n" or "holla\n". The choice is not
obvious from the static layout. You get one path. The CPU might take another.
global _start
section .text
_start:
rdtsc
xor eax, edx
test al, 1
jz noflip
stc
jmp decide
noflip:
clc
decide:
jc entry_B
entry_A:
jmp overlap
entry_B:
jmp overlap_shift
; --- OVERLAP ---
overlap:
db 0xeb, real - overlap - 2
overlap_shift:
db 0xeb, alt - overlap_shift - 2
; --- PAYLOAD ---
alt:
jmp short alt_data
alt_pop:
pop rsi
jmp write
alt_data:
call alt_pop
db 'h','o','l','l','a',0x0a
real:
jmp short real_data
real_pop:
pop rsi
jmp write
real_data:
call real_pop
db 'h','e','l','l','o',0x0a
write:
push 1
pop rdi ; stdout
xor eax, eax
mov al, 1
xor edx, edx
mov dl, 6
syscall
xor eax, eax
mov al, 60
xor edi, edi
syscall
This version is null-byte free and fully position-independent,
making it suitable as shellcode.
unsigned char shellcode[] =
"\x0f\x31\x31\xd0\xa8\x01\x74\x03\xf9\xeb\x01\xf8\x72\x02"
"\xeb\x02\xeb\x02\xeb\x12\xeb\x00\xeb\x03\x5e\xeb\x1b\xe8"
"\xf8\xff\xff\xff\x68\x6f\x6c\x6c\x61\x0a\xeb\x03\x5e\xeb"
"\x0b\xe8\xf8\xff\xff\xff\x68\x65\x6c\x6c\x6f\x0a\x6a\x01"
"\x5f\x31\xc0\xb0\x01\x31\xd2\xb2\x06\x0f\x05\x31\xc0\xb0"
"\x3c\x31\xff\x0f\x05";
[12. References]
----------------
[1] Covert Computation: Hiding Code in Code
Computers & Security, 2014
https://www.sciencedirect.com/science/article/pii/S0167404814000030
[2] Christopher Eagle
The IDA Pro Book: The Unofficial Guide to the World's Most
Popular Disassembler, 2nd Edition, 2011
[3] Skape & Skywing
Understanding Windows Shellcode, Uninformed Journal, 2007
[4] A Taxonomy of Self-Modifying Code for Obfuscation
Computers & Security, 2011
https://www.sciencedirect.com/science/article/pii/S0167404811001076
--[
PREV |
HOME |
NEXT ]--