0ctf2017 | babyheap

Author Avatar
Aryb1n 7月 06, 2018

题目一上来…sub_B70函数里一顿操作, 吓傻了

忘了checksec…好吧..保护全开, 这很baby

主要功能

1. Allocate
2. Fill
3. Free
4. Dump
5. Exit

另外…写 IDA的结构体的时候64bit整数不要写int…要用long

struct bheap{
    long inuse;
    long size;
    void *ptr;
};

而且, 看汇编能看到, 是sub_B70函数的返回值应该是个函数指针, 然后这个值作为了Allocate, Fill, Free, Dump的参数…但由于main里到这些功能函数是用了跳转表的形式, 所以这里从ida的反汇编结果里看不到sub_B70返回值的赋值和传参…

很容易就发现一个洞. 就是Allocate的时候会设置一个size, 而在Fill的时候, 会让你新输入一个size, 来作为fill的content的大小, 而且不会比较新的size和原来size…这样就可以任意堆溢出了

但貌似…Fill的时候, 不会更新size字段, 所以, 额, 在Dump的时候还是只能dump出来我们最初设定的size大小的内容

Free的时候貌似没有什么洞, 会把inuse位置为0, size置为0, free掉ptr, 之后在Allocate的时候, 可以在free掉的地方建立新的块

诶…没有UAF的洞, 而且内存分配用的是calloc, 会对内存进行清空,貌似leak稍有困难…

已经可以随意堆溢出了, 所以leak出地址, 改写malloc_hook就OK了吧
leak地址应该使用smallbin
更改malloc_hook用fastbin比较简单

所以怎么leak呢…

我们要用一个smallbin, 然后free掉他, 他会进入到unsorted bin, 然后这个时候…我们没有UAF, 就很尴尬

找了一下WP, …因为没有UAF而要leak地址的艰辛….真是

https://ctf-wiki.github.io/ctf-wiki/pwn/heap/fastbin_attack/#2017-0ctf-babyheap

通过更改smallbin这一块的size, 和某free掉的chunk的fd指针, 使得, 这块smallbin放到了fastbinY里

然后通过两次fastbin大小的请求, 把这块smallbin再申请出来, 再把size改回去, 好让他释放后放到unsorted bin

这一波神奇的操作就是为了让俩块bheap都持有这块smallbin的指针…

使得我们能够释放一个bheap, 把smallbin放入了unsorted bin, 的同时, 用另一块持有这块堆块指针的bheap能够使用dump来leak出来地址

update 2019-3-11
当时感觉上面说的方法很…奇特…今天发现一个dalao的blog里写的感觉很棒..一种不同的方法
dalao的blog链接: http://www.pwndog.top/2018/09/11/%E5%A0%86%E6%BA%A2%E5%87%BA%E7%9A%84%E5%85%AD%E7%A7%8D%E5%88%A9%E7%94%A8%E6%89%8B%E6%B3%95/

对于三个块进行操作,

...
malloc(0x60)
malloc(0x60)
malloc(0x60)
...

对应的内存分布图

-------------
chunk2 0x71
-------------
chunk3 0x71
-------------
chunk4 0x71
-------------

然后fill这个chunk2..溢出使得chunk3的size发生变化

-------------
chunk2 0x71
-------------
chunk3 0xe1 (0x70 * 2 + 1)
-------------
chunk4 0x71
-------------

然后free掉chunk3…因为0xe1大小已经属于fastbin了…所以free掉的时候会放进unsorted bin

然后再进行malloc…这里就是神奇的地方…由于fastbin里没找到合适的, 但unsorted bin里有一块大小0xe0的块, 就会把他切分, 把靠前的0x70字节(原来chunk3位置的块)返回作为这次malloc的分配的块, 然后接下来的0x70(由于我们溢出改了size,才让系统认为我们free掉了一个0xe0的块,所以靠后的这0x70其实是原来的chunk4的位置)就被放回到unsorted bin里继续候着…
所以这个时候就出现了

0x558d9949e0e0: 0x0000000000000000  0x0000000000000071 -> chunk2
0x558d9949e0f0: 0x6161616161616161  0x6161616161616161
0x558d9949e100: 0x6161616161616161  0x6161616161616161
0x558d9949e110: 0x6161616161616161  0x6161616161616161
0x558d9949e120: 0x6161616161616161  0x6161616161616161
0x558d9949e130: 0x6161616161616161  0x6161616161616161
0x558d9949e140: 0x6161616161616161  0x6161616161616161
0x558d9949e150: 0x0000000000000000  0x0000000000000071 -> chunk3, 刚malloc回来
0x558d9949e160: 0x0000000000000000  0x0000000000000000
0x558d9949e170: 0x0000000000000000  0x0000000000000000
0x558d9949e180: 0x0000000000000000  0x0000000000000000
0x558d9949e190: 0x0000000000000000  0x0000000000000000
0x558d9949e1a0: 0x0000000000000000  0x0000000000000000
0x558d9949e1b0: 0x0000000000000000  0x0000000000000000
0x558d9949e1c0: 0x0000000000000000  0x0000000000000071 -> 切剩下放回unsorted bin
0x558d9949e1d0: 0x00007ff6212b8b78  0x00007ff6212b8b78
0x558d9949e1e0: 0x0000000000000000  0x0000000000000000

这个时候由于chunk4切剩下放回了unsorted bin, 就使的现在show 一下chunk4就能得到libc

dalao tql, 我们做的就是写chunk2溢出修改到chunk3的size, 然后free掉chunk3, show chunk4

但…不知道我理解的对不对…因为之前一直因为, 这个切一部分unsorted bin的操作是只有没找到合适的smallbin才切…没发现…这tm也能切…没找到合适的fastbin也能切???

所以准备在free之后..在malloc的地方打断点,调试的学习一下

woc…我的这个源码好像对不上…醉了, 明明都是2.23

只能依稀的观察到这个0xe0的块开始在 bins0 …. 然后 到了…bins26…然后他…好像就被切了..之后的0x70又在bins0里

在这个中有提到 http://brieflyx.me/2016/heap/glibc-heap/

这个应该是前面的fastbinY和smallbin都没有找到合适的,所以开始处理unsorted的块

迭代过程中,找到大小正好的就返回, 不然的话, 就把这个块放到对应的small bin或者large bin里
等到unsorted里没有块l
然后在small bins和large bins里找到满足需求的最小块, 然后切分, 前一块给用户, 后面一块放到unsorted bin里

好了,学到了….我去再仔细看看源代码

调试一下可以发现, 我们free掉chunk3的时候, unsorted bin里的0xe0这块不叫last_reminder, 而之后切剩下这块叫做last_reminder
(p main_arena 能看到这个结构)

看一下last_reminder定义是…
/ The remainder from the most recent split of a small request /
那….为什么我的一个split of a fast request也叫….last_reminder

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

int main() {
    void * ck2 = malloc(0x60);
    void * ck3 = malloc(0x60);
    void * ck4 = malloc(0x60);

    int i = 0;
    char * l = (char*)ck2;
    l[96] = 0x00;
    l[96 + 8] = 0xe1;

    getchar();
    free(ck3);
    getchar();
    malloc(0x60);
    getchar();
    return 0;
}

验证一下…

gdb-peda$ heap all
Top Chunk:  0x602560
Last Remainder:  0x6020e0
0x602000 SIZE=0x70 DATA[0x602010] |................................| INUSED PREV_INUSE
0x602070 SIZE=0x70 DATA[0x602080] |H.......H.......................| INUSED PREV_INUSE
0x6020e0 SIZE=0x70 DATA[0x6020f0] |x.......x.......................| PREV_INUSE INUSED
0x602150 SIZE=0x410 DATA[0x602160] |................................| INUSED
0x602560 SIZE=0x20aa0 TOP_CHUNK

看来我一直对这个last_reminder有误解

喔,不是,原来是我对这个 smallbin理解有误…fastbin大小是包含在了smallbin大小里, 好吧, 我好菜啊

#define in_smallbin_range(sz)                                                  \
    ((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)

明天再继续仔细看一看

仔细的阅读了一下
‘_int_malloc’会
先精准的匹配fastbin
然后精准匹配smallbin
然后..还没匹配到的话, 如果是large的请求, 就把fastbin合并丢到unsorted bin里了, 就是传说中的malloc_consolidate

这里的说明就是他代码是

if(in_smallbin_range(nb)) {
 ...
} else {
 ...
 if(hava_fastchunks(av)) {
    malloc_consolidate(av)
 }
}

就算是请求smallbin, 但没有匹配到, 也不会进行malloc_consolidate, 我之前理解错了

之后就是一个大循环
进入大循环 说明

  1. Size falls into ‘fastbin’ range but no fastchunk is available.
  2. Size falls into ‘smallbin’ range but no smallchunk is available (calls malloc_consolidate during initialization).
  3. Size falls into ‘largbin’ range.
    (而不是某些文章分析说的, 进入大循环说明就是一定要分配large bin了

大循环(有点复杂….)…不过这个大循环怎么结束呢, 没有强制最多多少次?

1. 小循环, 会反向的遍历unsorted bin...最多1w次
    - 先对当前块检查大小是不是符合规矩, 不然就报个错
    - 如果当前块大小符合smallbin 并且 是last_reminder 并且 unsorted里就这一个块 并且 大小够: 就把这块给切分, 前一块返回给用户, 后一块做为新的last_reminder
    > 那么我有个问题就是, last_reminder是从last_reminder切出来得到的, 那最开始的那一块last_reminder从何而来, 通过搜索...`av->last_remainder =`, 我找到了另一处赋值, 在小循环之外
    - 如果不满足上述, 那么就把当前块放回对应的smallbin或者largebin里, 这个就是题目中看到的unsorted先回到了smallbin的地方
2. 寻找largebin
3. 到了这里说明...不能精准匹配了, 尝试找一块更大的块切开, 我们题目exp里切切开就是在这个地方..., 产生了最初的last_reminder, 切完之后, 剩下的部分好像是属于smallbin, 才给last_reminder赋值 

  /* advertise as last remainder */
  if (in_smallbin_range (nb))
    av->last_remainder = remainder;
  if (!in_smallbin_range (remainder_size))
    {
      remainder->fd_nextsize = NULL;
      remainder->bk_nextsize = NULL;
    }
4. 到了这里就要用top了...
5. ~~再次合并fastbin, 就是malloc_consolidate...~~ 这里有点问题 ... 并不是要用top就malloc_consolidate
6. 用sysmalloc

4.5.6 这里的逻辑是

use_top:
victim = av -> top;
size = chunsize(victim);

if((unsigned long) size >= (unsigned long) (nb + MINSIZE)) {
    // top大小够用...使用top
    ...
} else if (hava_fastchunks(av)) {
    malloc_consolidate(av);
    // top大小不够...才malloc_consolidate
} else {
    // 使用sysmalloc....
}

也就是说…其实吧, 这里出现的malloc_consolidate的几率也比较小…
所以大概认为malloc_consolidate基本只会粗线在largebin的申请中

我有个问题, 在这里最后都用sysmalloc了….肯定就结束了, 那这个大循环有什么意义呢, 大循环这里代码大概是 问题解决

  for (;; )
    {
      int iters = 0;
      while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {

        ....

          if (++iters >= MAX_ITERS) // 小循环最多1k次
            break;
        }
        ... // 寻找largbin, 切大块, 用top....无数代码
        ... // 合并fastbin, 用sysmalloc
    }

有没有大佬能解答我的问题, 私撩我好嘛 问题解决..自己没好好读代码

我的理解仿佛和大佬不一样, a, 我好菜啊

不过这个dalao的完整的exp在我这边跑不起来

以下是之前的记录

再说这个错位分配fake_chunkmalloc_hook附近的操作

我的环境是这样, 和ctf-wiki中介绍的一样, malloc_hook和realloc_hook挨着的, 和main_arena差一点

gdb-peda$ x/gx 0x7ffff7dd1b08
0x7ffff7dd1b08 <__realloc_hook>:    0x00007ffff7a92a00
gdb-peda$ x/gx 0x7ffff7dd1b10
0x7ffff7dd1b10 <__malloc_hook>:    0x0000000000000000
gdb-peda$ x/gx 0x7ffff7dd1b20
0x7ffff7dd1b20 <main_arena>:    0x0000000000000000

同样的是在0x7ffff7dd1af5, 这个地方可以错位伪造一个size

0x7ffff7dd1af5 <_IO_wide_data_0+309>:    0x000000000000007f    0xfff7a92e20000000
0x7ffff7dd1b05 <__memalign_hook+5>:    0xfff7a92a0000007f    0x000000000000007f
0x7ffff7dd1b15 <__malloc_hook+5>:    0x0000000000000000    0x0000000000000000

加上prev_size的8字节, 所以是要在0x7ffff7dd1aed处伪造一个chunk, 就是使得其他chunk的fd指向这里…

这个地方在我的环境里是距离main_arena距离为0x7ffff7dd1b20 - 0x7ffff7dd1aed, 所以fake_chunk地址是main_arena - 0x33, 这个0x33应该是出现在通过fake_chunk来更改malloc_hook的各种WP里…可以记住这值了
23333, 也有的人写malloc_hook - 0x23, 一样的

而且由于伪造的size是0x7f, 所以…在计算大小的时候, fastbin_index(0x7f), 是属于fastbinY[5], 对应chunk大小是0x70, data部分是0x60的…所以要选择分配对应大小的块, 才可以

喔, That’ all.

关于跳转表

IDA里出现了这个内容…虽然算一下就OK, 但看着很不直观

  JUMPOUT(__CS__, (char *)dword_14F4 + dword_14F4[v3]);