0ctf2017 | babyheap
题目一上来…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, 我之前理解错了
之后就是一个大循环
进入大循环 说明
- Size falls into ‘fastbin’ range but no fastchunk is available.
- Size falls into ‘smallbin’ range but no smallchunk is available (calls malloc_consolidate during initialization).
- 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_chunk
到malloc_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]);