如未特殊说明, 均假定 libc 2.23, 64 位.
Unsortbin 就是 malloc state 中的 bins[1], 相关定义如下:
1
2
3
4
5
6
7
8
9
10
11
struct malloc_state
{
/* Normal bins packed as described above */
mchunkptr bins [ NBINS * 2 - 2 ];
};
/* The otherwise unindexable 1-bin is used to hold unsorted chunks. */
#define unsorted_chunks(M) (bin_at (M, 1))
/* addressing -- note that bin_at(0) does not exist */
#define bin_at(m, i) \
(mbinptr) (((char *) &((m)->bins[((i) - 1) * 2])) \
- offsetof (struct malloc_chunk, fd))
Unsortbin 是一个双向链表结构. 其中的 freed chunk 由 fd, bk 指针双向链接.
只看和数据结构相关的部分, free 的时候, 如果不是放在 fastbin (和 tcache bin) 中, 则是在 unsorted bin 的双向链表的头部插入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void
_int_free ( mstate av , mchunkptr p , int have_lock )
{
...
/*
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 ;
...
p -> fd = fwd ;
p -> bk = bck ;
...
bck -> fd = p ;
fwd -> bk = p ;
...
}
malloc 一个比 fast chunk 大的块时, 会先去 smallbin 里找. 如果没有合适的 chunk, 就会去整理 fastbin, 将能够合并的 chunk 尽量合并, 然后把所有 chunk 也放入 unsortbin 中. 然后开始从后往前遍历 unsortbin.
如果是请求的块大小是 small chunk, 并且上一个切割剩余的 (last_remainder) 正好是 unsortbin 中唯一的一块, 那么尝试分割这个 chunk, 剩下的部分放回 unsortbin 中, 同时更新 last remainder. 返回这个块.
否则, 看当前遍历到的这个块大小是不是大小正好, 是的话就返回, 不是的话就把这块放到对应的 bin 中去.
遍历完了以后还不满足, 会通过 block map 来 重新 找 smallbin 和 largebin 的 best fit, 从这里切割, 并设置 last_remainder. 最后还不满足, 则去从 top chunk 中取.
初始时, unsortbin 的 fd 和 bk 都指向自己. 所以, 如果我们能够泄漏双向链 表头部节点的 bk 指针 , 或者 尾部节点的 fd 指针 , 那么就能够泄漏出 bins[1] 的地址了. 而在 main arena 中, bins 是存在 libc 的数据段的. 所以, 相当于泄漏出了 libc 地址.
特别地, 如果 unsortbin 中只有一个节点, 那么它的 fd 和 bk 指针都可以泄漏 libc.
虽然有很多种情况, 都能往 unsortbin 中存放 chunk, 但是对利用来说, 最好就是直接把一个 smalll chunk free 掉, 这样就到了 unsortbin 中, 然后 leak fd 或者 bk.
(前半段是 unsortbin leak libc, 后半段是 malloc malloc_hook nearby)
64 位 ELF, 保护全开. 菜单题, Allocate, Fill, Free, Dump.
main:
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
__int64 __fastcall main ( __int64 a1 , char ** a2 , char ** a3 )
{
Node * heap ; // [rsp+8h] [rbp-8h]
heap = ( Node * ) Init ();
while ( 1 )
{
menu ();
switch ( readll () )
{
case 1LL :
Allocate ( heap );
break ;
case 2LL :
Fill ( heap );
break ;
case 3LL :
Free ( heap );
break ;
case 4LL :
Dump ( heap );
break ;
case 5LL :
return 0LL ;
default :
continue ;
}
}
}
Init 是一些 IO 初始化操作, 并 mmap 了一个足够大的地址并返回.
Allocate:
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
void __fastcall Allocate ( Node * a1 )
{
int i ; // [rsp+10h] [rbp-10h]
int size ; // [rsp+14h] [rbp-Ch]
char * str ; // [rsp+18h] [rbp-8h]
for ( i = 0 ; i <= 15 ; ++ i )
{
if ( ! a1 [ i ]. inuse )
{
printf ( "Size: " );
size = readll ();
if ( size > 0 )
{
if ( size > 4096 )
size = 4096 ;
str = ( char * ) calloc ( size , 1uLL );
if ( ! str )
exit ( - 1 );
a1 [ i ]. inuse = 1 ;
a1 [ i ]. size = size ;
a1 [ i ]. str = str ;
printf ( "Allocate Index %d \n " , ( unsigned int ) i );
}
return ;
}
}
}
可以看到, 最多可以分配 16 个堆, 堆大小最大是 4096. 且用的是 calloc, 会将用户数据部分清空.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void __fastcall Fill ( Node * a1 )
{
int i ; // [rsp+18h] [rbp-8h]
int size ; // [rsp+1Ch] [rbp-4h]
printf ( "Index: " );
i = readll ();
if ( i >= 0 && i <= 15 && a1 [ i ]. inuse == 1 )
{
printf ( "Size: " );
size = readll ();
if ( size > 0 )
{
printf ( "Content: " );
my_read ( a1 [ i ]. str , size );
}
}
}
检测 inuse, 为 1 表示该位置被分配, 即 str 有效. 但是, 这里输入的 size 没有和之前 calloc 的进行比较, 所以可以产生 堆溢出漏洞 . my_read 没有漏洞, 不放代码了.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void __fastcall Free ( Node * a1 )
{
int i ; // [rsp+1Ch] [rbp-4h]
printf ( "Index: " );
i = readll ();
if ( i >= 0 && i <= 15 && a1 [ i ]. inuse == 1 )
{
a1 [ i ]. inuse = 0 ;
a1 [ i ]. size = 0LL ;
free ( a1 [ i ]. str );
a1 [ i ]. str = 0LL ;
}
}
全清空.
1
2
3
4
5
6
7
8
9
10
11
12
13
void __fastcall Dump ( Node * a1 )
{
int i ; // [rsp+1Ch] [rbp-4h]
printf ( "Index: " );
i = readll ();
if ( i >= 0 && i <= 15 && a1 [ i ]. inuse == 1 )
{
puts ( "Content: " );
my_puts ( a1 [ i ]. str , a1 [ i ]. size );
puts ( "" );
}
}
my_puts 也没漏洞, 不放了.
程序主要就是堆溢出漏洞.
由于保护全开, 所以想办法修改 hook. 那就必须要先泄漏 libc.
由于在 unsortbin 中的 chunk 的 fd / bk 保存了 bins[1] 的值, 同时有打印堆块功能. 于是可以尝试利用溢出, 覆盖 freed chunk 的 fd, 再 malloc, 造成堆重叠. Fastbin attack 比较好实现堆重叠. 但是, 我们无法知道堆块的地址. 不过, 由于初始化时, 堆其实地址要进行页对齐 (0x1000), 所以, 我们其实是知道每个堆块 低 12 位 的值, 或者说, 相对位置.
那么可以这样想, 假如我 free 了两个大小相同的 fast chunk, 那么头节点的 fd 就已经指向了堆处了. 然后可以通过溢出, 覆盖 fd 的低 8 位, 从而使其指向其他堆的其他位置. 如果这个位置正好是另一个堆的位置, 那么我们就成功造成了堆重叠.
于是, 构造如下左图这样的堆. 其中, chunk 0, 1, 2, 3 都是 fast chunk, chunk 4 是 small chunk, 也就是我们尝试 free 掉然后去泄漏 libc 的地方. 由于在 unsortbin 中的 chunk free 时会尝试合并到 top chunk, 所以这里还需要一个堆 chunk 5 块顶在上面, 才能确保 chunk 4 被放入到 unsortbin 中. 这里我们先 free 掉 chunk 4.
0
0
0
0
0
0
x
x
x
x
x
x
1
0
0
0
0
0
1
8
6
4
2
0
0
0
0
0
0
0
p
s
r
b
f
s
p
s
p
s
p
s
p
s
p
i
e
k
d
i
r
i
r
i
r
i
r
i
r
z
s
z
e
z
e
z
e
z
e
z
e
e
i
&
&
e
s
e
s
e
s
e
s
e
s
z
b
b
i
i
i
i
i
0
e
i
i
0
z
0
z
0
z
0
z
0
z
x
n
n
x
e
x
e
x
e
x
e
x
e
2
0
s
s
9
2
2
2
2
0
x
[
[
1
0
1
0
1
0
1
0
1
0
9
1
1
0
]
]
0
(
P
R
E
I
c
c
c
c
c
c
N
h
h
h
h
h
h
U
u
u
u
u
u
u
S
n
n
n
n
n
n
E
k
k
k
k
k
k
0
5
4
3
2
1
0
)
0
0
0
0
0
0
x
x
x
x
x
x
1
0
0
0
0
0
1
8
6
4
2
0
0
0
0
0
0
0
p
s
r
b
f
s
p
s
p
s
p
f
s
p
s
p
i
e
k
d
i
r
i
r
i
r
d
i
r
i
r
z
s
z
e
z
e
b
f
z
e
b
z
e
z
e
e
i
&
&
e
s
e
s
k
d
e
s
k
0
e
s
e
s
z
b
b
i
i
i
x
i
i
0
e
i
i
0
z
0
z
0
0
0
z
0
X
0
z
0
z
x
n
n
x
e
x
e
x
e
0
x
e
x
e
2
0
s
s
9
2
2
4
2
2
0
x
[
[
1
0
1
0
1
0
0
1
0
1
0
9
1
1
0
]
]
0
(
P
R
E
I
c
c
c
c
c
c
N
h
h
h
h
h
h
U
u
u
u
u
u
u
S
n
n
n
n
n
n
E
k
k
k
k
k
k
0
5
4
3
2
1
0
)
0
0
0
0
0
0
x
x
x
x
x
x
1
0
0
0
0
0
1
8
6
4
2
0
0
0
0
0
0
0
p
s
r
b
f
s
p
s
p
s
p
f
s
p
s
p
i
e
k
d
i
r
i
r
i
r
d
i
r
i
r
z
s
z
e
z
e
b
f
z
e
b
z
e
z
e
e
i
&
&
e
s
e
s
k
d
e
s
k
0
e
s
e
s
z
b
b
i
i
i
x
i
i
0
e
i
i
0
z
0
z
0
0
0
z
0
X
0
z
0
z
x
n
n
x
e
x
e
x
e
0
x
e
x
e
2
0
s
s
9
2
2
4
2
2
0
x
[
[
1
0
1
0
1
0
0
1
0
1
0
9
1
1
0
]
]
0
(
P
R
E
I
c
c
c
c
c
c
N
h
h
h
h
h
h
U
u
u
u
u
u
u
S
n
n
n
n
n
n
E
k
k
k
k
k
k
0
5
4
3
2
1
0
)
首先, 需要构造一个指向堆某处的 fd. 可以先后 free 掉 chunk 2, 1. 这样根据 fastbin 的机制, chunk 1 的 fd 指针就会指向 chunk 2 处, 上中图所示. 然后对 chunk 0 进行 Fill 操作, 覆盖 chunk 1 的最低位字节为 0x80, 这样就使其指向了 chunk 4. 如上右图.
接下来, 如果想要 malloc 到 chunk 4, 还需要改一下 chunk 4 的 size 以绕过检测. 对 chunk 3 进行 Fill 操作, 覆盖 size 为 0x21 即可. 然后申请两个 0x20 的堆块, 第二个就是 chunk 4 所在位置了. 打印 Chunk 2, 就能够得到 libc 上的 &bins[1] 地址了.
后半段在这里