小记一则不一样的Fastbin利用


在翻ZDI的历史博客时,看到一个NETGEAR路由器中的堆溢出利用,很是少见,一般路由器中大多是命令执行或栈溢出,很少见到有堆溢出相关的,便跟着走了一遍。


漏洞点及成因

漏洞在现在看来其实很无厘头,在文件上传逻辑中获取Content-Length值时通过对整个http的请求做stristr(s1, “Content-Length: “)来定位的,且没有其他的条件限制。

1
2
3
4
5
6
7
.text:00017308 04 A0 80 E2       ADD             R10, R0, #4
.text:0001730C A4 17 1F E5 LDR R1, =aContentLength_0 ; "Content-Length: "
.text:00017310 04 00 A0 E1 MOV R0, R4
.text:00017314 0A 80 64 E0 RSB R8, R4, R10
.text:00017318 E7 DA FF EB BL stristr
.text:0001731C 00 90 50 E2 SUBS R9, R0, #0
.text:00017320 B3 00 00 0A BEQ loc_175F4

所以attacker在请求头中原Content-Length之前如果包含了伪造的字段,即可控制该值。这个值的计算逻辑也很有意思:

1
2
3
4
5
6
7
v117 = stristr(v115 + 16, "\r\n") - (v115 + 16);
fileSize1 = 0;
for ( i = 0; i < v117; ++i )
{
_chr = *(char *)++v116;
fileSize1 = _chr - '0' + 10 * fileSize1;
}

可以看到是以\r\n作为结束符,在结束符之前的都将作为数字的str,计算时将每个字符的ascii减去0的ascii,然后组合到一起作为实际值来使用。这里的fileSize1是unsigned int,所以这里必然存在整数溢出问题。这个fileSize值在后面用作内存分配的值,申请fileSize的空间后再将上传的内存拷贝至对应位置。 所以在控制了fileSize值后,将其改成比上传内容实际值小便可以触发堆溢出的问题。

1
2
3
4
5
6
7
8
9
10
11
if ( dword_1A870C )
{
free((void *)dword_1A870C);
dword_1A870C = 0;
}
...
dword_1A870C = (int)malloc(fileSize1 + 0x258);
...
memset((void *)dword_1A870C, 32, fileSize1 + 0x258);
v208 = v89 - v102;
memcpy((void *)dword_1A870C, &s1[v102], v89 - v102);

漏洞的成因大致就是这样,我觉得完全是开发人员不严谨导致的;另一方面也是早起嵌入式开发人员习惯什么事情都自己来完成,所以解析字段时也就直接stristr去获取了,却没做严格的限制。同时在计算数字时也没有考虑到存在非数字字符的情况。

如何利用

利用部分主要记录的是堆相关的操作,至于路径选择以及其它跟路由器密切相关的部分这里就不记录了,感兴趣的可以去看原blog。

1
2
3
4
5
6
7
➜ checksec httpd
[*] '/tftpboot/httpd'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)

不存在PIE所以我们不用考虑如何去获取heap base,把重点放在堆相关的利用上即可。现在有一个堆溢出且能将任意数据写入堆内存,常规思路有:

  1. 通过溢出直接覆盖后面堆块的链表指针去控制后面的堆分配,然覆盖got表…
  2. 溢出覆盖__malloc_hook、__free_hook的值,将其修改成gadget…

但是很可惜,这里这两种方式都不大行得通。由于固件中使用的是uClibc,属于glibc的最小libc版本,所以其中并没有__malloc_hook这种机制,所以方法二彻底废了,方法一行不通是因为漏洞点前后有其它因素影响:

  1. 溢出点分配的内存会存储在全局变量中,每次使用前会检查全局变量中是否有内容,有的话则先free对应内存将全局变量置0再分配
  2. memcpy拷贝内容触发堆溢出后,会返回错误页面,在这里会调用fopen函数,而在uClibc中fopen会触发两次内存操作,大小分别为0x60和0x1000,大致流程为:
1
free(malloc(0x60)), free(malloc(0x1000))

看到0x60的时候很自然会想到fastbinDup,但是后面这个0x1000就让人头疼了,会触发malloc_consolidate来整理fastbin。这里简单介绍一下malloc_consolidate:

这个函数的作用就是将 fastbin 合并后置入 unsorted bin,一般调用的情况有以下几种:

  • malloc
    1. malloc 的大小在 smallbin 范围内,若对应的 smallbin 没初始化的时候。
    2. 当申请大于 small bin 范围的堆快时(large chunk) **if (have_fastchunks(av))
      **malloc_consolidate(av);
    3. 投 topchunk 中没有空闲内存,向系统申请内存时,如果 fastbin 中有空间,则会先尝试整理 fastbin 看能否满足需求,不行再从 system 中申请
  • free
    1. free 一块大内存后会合并其附近的空闲内存,如果合并后的 size 大于 FASTBIN_CONSOLIDATION_THRESHOLD 时,如果有 fastbins 则会调用 malloc_consolidate, 同时如果 top chunk 的 size 大于 trim_threshold,会向操作系统归还内存,也会调用 malloc_consolidate.

由于malloc_consolidate整理了fastbin,所以我们不能用fastbinDup,但是又没用其它更好的办法,似乎无解了。

这里的破局之法还是出现在uClibc中,毕竟是最小版本的glibc,所以其中很多实现都不怎么完善。uClibc中的free 和malloc与glibc中还是有比较大的不同,其中在释放fastbin时存在越界写问题:

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
struct malloc_state {
/* The maximum chunk size to be eligible for fastbin */
size_t max_fast; /* low 2 bits used as flags */
/* Fastbins */
mfastbinptr fastbins[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];
/* Bitmap of bins. Trailing zero map handles cases of largest binned size */
unsigned int binmap[BINMAPSIZE+1];
/* Tunable parameters */
...
};

/* ------------------------------ malloc ------------------------------ */
void* malloc(size_t bytes)
{
size_t nb; /* normalized request size */
...
av = get_malloc_state();
/*
Convert request size to internal form by adding (sizeof(size_t)) bytes
overhead plus possibly more to obtain necessary alignment and/or
to obtain a size of at least MINSIZE, the smallest allocatable
size. Also, checked_request2size traps (returning 0) request sizes
that are so large that they wrap around zero when padded and
aligned.
*/
checked_request2size(bytes, nb);
/*
Bypass search if no frees yet
*/
if (!have_anychunks(av)) {
if (av->max_fast == 0) /* initialization check */
__malloc_consolidate(av);
goto use_top;
}
/*
If the size qualifies as a fastbin, first check corresponding bin.
*/
...
/* If this is a large request, consolidate fastbins before continuing.
While it might look excessive to kill all fastbins before
even seeing if there is space available, this avoids
fragmentation problems normally associated with fastbins.
Also, in practice, programs tend to have runs of either small or
large requests, but less often mixtures, so consolidation is not
invoked all that often in most programs. And the programs that
it is called frequently in otherwise tend to fragment.
*/
else {
idx = __malloc_largebin_index(nb);
if (have_fastchunks(av))
__malloc_consolidate(av);
}
...
}
#define ANYCHUNKS_BIT (1U)
#define have_anychunks(M) (((M)->max_fast & ANYCHUNKS_BIT))
#define have_fastchunks(M) (((M)->max_fast & FASTCHUNKS_BIT))

#define fastbin_index(sz) ((((unsigned int)(sz)) >> 3) -2)

/* ------------------------------ free ------------------------------ */
void free(void* mem)
{
mstate av;

mchunkptr p; /* chunk corresponding to mem */
size_t size; /* its size */
mfastbinptr* fb; /* associated fastbin */
mchunkptr nextchunk; /* next contiguous chunk */
size_t nextsize; /* its size */
int nextinuse; /* true if nextchunk is used */
size_t prevsize; /* size of previous contiguous chunk */
mchunkptr bck; /* misc temp for linking */
mchunkptr fwd; /* misc temp for linking */

/* free(0) has no effect */
if (mem == NULL)
return;

__MALLOC_LOCK;
av = get_malloc_state();
p = mem2chunk(mem);
size = chunksize(p);

check_inuse_chunk(p);

/*
If eligible, place chunk on a fastbin so it can be found
and used quickly in malloc.
*/

if ((unsigned long)(size) <= (unsigned long)(av->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
) {

set_fastchunks(av);
fb = &(av->fastbins[fastbin_index(size)]); /// <--------- OOB
p->fd = *fb;
*fb = p;
}

/*
Consolidate other non-mmapped chunks as they arrive.
*/

else if (!chunk_is_mmapped(p)) {
set_anychunks(av);

nextchunk = chunk_at_offset(p, size);
nextsize = chunksize(nextchunk);

...

/*
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 ((unsigned long)(chunksize(av->top)) >=
(unsigned long)(av->trim_threshold))
__malloc_trim(av->top_pad, av);
}

}
/*
If the chunk was allocated via mmap, release via munmap()
Note that if HAVE_MMAP is false but chunk_is_mmapped is
true, then user must have overwritten memory. There's nothing
we can do to catch this error unless DEBUG is set, in which case
check_inuse_chunk (above) will have triggered error.
*/

else {
size_t offset = p->prev_size;
av->n_mmaps--;
av->mmapped_mem -= (size + offset);
munmap((char*)p - offset, size + offset);
}
__MALLOC_UNLOCK;
}

可以看到在访问fastbins数组时没有边界检查,而fastbins在malloc_state中位于max_fast之后,同时max_fast又是是否执行malloc_consolidate的判断条件,所以只要通过fastbins的越界写来修改max_fast,便可以控制malloc_consolidate让malloc中不整理fastbin,这样我们就可以使用fastbinDup来完成利用了。

控制max_fast的方法也比较好理解,根据fastbin_index可以看到我们只需要将设置成7或8时,index为-1,就可以访问到max_fast。sz又是堆块中表示size的字段,可以通过堆溢出去覆盖修改,所以修改max_fast也就不是问题了。

最后利用流程如下:

  • 通过触发堆溢出漏洞修改下一个堆快的flag,覆盖REV_INUSE标志为0,使其错误地表示前一个chunk是空闲的;
  • 由于错误的PREV_INUSE标志,我们可以malloc()返回一个与实际存在的块重叠的块。因此我们可以修改上一个堆快的size设置为不可能的 8;
  • 当释放这个”size = 8”的堆快后并放置在fastbin 上时,malloc_stats->max_fast会被改成一个大值(堆快的地址)
  • 当malloc_stats->max_fast被更改后,malloc(0x1000)便不会再调用malloc_consolidate,所以就可以使用fastbinDup了
  • 再次触发堆溢出漏洞,溢出修改下一个空闲块的fd为free()的GOT地址
  • 再次申请堆快,会返回free的GOT地址,我们可以向其中写入system 的plt地址
  • 最后,在调用free的时候,会调用system,我们可以将其参数指向payload,从而实现RCE

小结

这里比较精巧的就是利用uClibc中free函数存在的OOB,是的可以控制malloc_consolidate是否被执行。后面对这个原语搜了一波,发现还有挺多利用中都有用到,是我愚钝了(lll¬ω¬)

在嵌入式设备中,除了常规的程序发掘外,对于一些使用的库也存在很多漏洞。有时候结合这些库中存在的问题可能会得到意想不到的结果。

作者

vlinkstone

发布于

2020-12-28

更新于

2020-12-28

许可协议

评论