堆利用--3Days
我可以告诉你曾经我也一样迷茫吗
堆结构:
use after free
uaf漏洞:
原理,当一个chunk释放过之后,再次申请相同大小的chunk
例题:actf_2019_babyheap
拖进ida查看,先逆向一下
主要看create,delete,show这三个函数
create:
unsigned __int64 sub_400A78()
{
void **v0; // rbx
int i; // [rsp+8h] [rbp-38h]
int v3; // [rsp+Ch] [rbp-34h]
char buf[24]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-18h]
v5 = __readfsqword(0x28u);
if ( dword_60204C <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !*(&ptr + i) )
{
*(&ptr + i) = malloc(0x10uLL);
*((_QWORD *)*(&ptr + i) + 1) = sub_40098A;
puts("Please input size: ");
read(0, buf, 8uLL);
v3 = atoi(buf);
v0 = (void **)*(&ptr + i);
*v0 = malloc(v3);
puts("Please input content: ");
read(0, *(void **)*(&ptr + i), v3);
++dword_60204C;
return __readfsqword(0x28u) ^ v5;
}
}
}
else
{
puts("The list is full");
}
return __readfsqword(0x28u) ^ v5;
}
这里标记一下,canary保护
v5 = __readfsqword(0x28u);
return __readfsqword(0x28u) ^ v5;
*(&ptr + i) = malloc(0x10uLL);程序在这里开辟了一个0x10的堆,向用户索要了一段数据, v3 = atoi(buf); v0 = (void **)*(&ptr + i); *v0 = malloc(v3);atoi是将用户的输入转换为一个整数,接下来在调用一次malloc,开辟一个空间为用户输入大小的堆
再看delete:
unsigned __int64 sub_400BAE()
{
int v1; // [rsp+Ch] [rbp-24h]
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("Please input list index: ");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( v1 >= 0 && v1 < dword_60204C )
{
if ( *(&ptr + v1) )
{
free(**(&ptr + v1));
free(*(&ptr + v1));
}
}
else
{
puts("Out of bound!");
}
return __readfsqword(0x28u) ^ v3;
}
这里free掉了两次内容,第一次释放掉了数据部分的内存,第二次释放掉了结构体的内存,但是这里出了一个问题,并没有将开辟堆的指针置空
接下来看show:
void __fastcall show_0()
{
int v0; // [rsp+Ch] [rbp-24h]
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v2; // [rsp+28h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("Please input list index: ");
read(0, buf, 4uLL);
v0 = atoi(buf);
if ( v0 >= 0 && v0 < dword_60204C )
{
if ( *(&ptr + v0) )
(*((void (__fastcall **)(_QWORD))*(&ptr + v0) + 1))(*(_QWORD *)*(&ptr + v0));
}
else
{
puts("Out of bound!");
}
}
补充:
unsigned __int64 __fastcall show(const char *a1)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("Content is '%s'\n", a1);
return __readfsqword(0x28u) ^ v2;
}
将开辟的块的内容打印了出来,但这里也出现了问题,(((&ptr + v0) + 1))(**(&ptr + v0));,这是用函数指针去打印内容,如果第一次开辟的堆随后释放掉,再开辟一个相同大小的堆(0x10),,修改将其地址的内容改为system的地址和binsh的地址,就达到了攻击的目的。
exp:
off by one
unlink
bin:
指针是放在栈上,而指针所指向的空间是指向了堆空间
unlink就是将链表头处的free堆块unsorted bin中脱离出来,然后和物理地址相邻的新free的堆块合并成大堆块(向前合并或者向后合并),再放入到unsorted bin中。
通过构造一个“fake chunk”,即伪造的内存块,使其看起来像是一个已释放的chunk,并且通过操纵这个fake chunk的fd和bk指针,使其指向想要篡改的内存地址。当malloc函数尝试对fake chunk执行unlink操作时,它会按照fd和bk指针(fd在chunk中指向下一个空闲的chunk,bk在chunk中指向下一个chunk,)指示的位置进行写入,通常是将当前chunk的地址减去0x18(这是一个常见的偏移量,因为 unlink 函数内部会对chunk地址做一定的偏移计算)的值写入到fd或bk指针所指向的位置,从而实现了对内存任意地址的写入能力。
设指向可 UAF chunk 的指针的地址为 ptr
修改 fd 为 ptr - 0x18
修改 bk 为 ptr - 0x10
触发 unlink
ptr 处的指针会变为 ptr - 0x18。
实现效果
使得已指向 UAF chunk 的指针 ptr 变为 ptr - 0x18
这里并没有找到合适的例题,使用how2heap上的例子
glibc_2.23为例:
源c文件:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
uint64_t *chunk0_ptr;
int main()
{
setbuf(stdout, NULL);
printf("Welcome to unsafe unlink 2.0!\n");
printf("Tested in Ubuntu 14.04/16.04 64bit.\n");
printf("This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
printf("The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\n");
int malloc_size = 0x80; //we want to be big enough not to use fastbins
int header_size = 2;
printf("The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\n\n");
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
printf("The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
printf("The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);
printf("We create a fake chunk inside chunk0.\n");
printf("We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\n");
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
printf("We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
printf("With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\n");
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
printf("Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
printf("Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);
printf("We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\n");
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
printf("We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk.\n");
printf("It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\n");
chunk1_hdr[0] = malloc_size;
printf("If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x90, however this is its new value: %p\n",(void*)chunk1_hdr[0]);
printf("We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
chunk1_hdr[1] &= ~1;
printf("Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
printf("You can find the source of the unlink macro at https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=ef04360b918bceca424482c6db03cc5ec90c3e00;hb=07c18a008c2ed8f5660adba2b778671db159a141#l1344\n\n");
free(chunk1_ptr);
printf("At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\n");
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;
printf("chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
printf("Original value: %s\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
printf("New Value: %s\n",victim_string);
// sanity check
assert(*(long *)victim_string == 0x4141414142424242L);
}
这里等遇到合适的题再写……翻了翻题库没有找到,利用unlink的前提是由uaf漏洞或者程序存在堆溢出(单字节就ok),我们通过chunk的地址减去0x18(可以通过构造我们精心制造的堆,写入binsh和system的值)的值写入到fd或bk指针所指向的位置,从而实现了对内存任意地址的写入能力。
fastbin chunk attack
(用户并不诚实,盗贼也会修改用户凭据)
tcache attack
tcache是glibc 2.26(Ubuntu 17.10)之后引入的一种技术,其目的是为了提升堆管理的性能
tcache由于省略了很多安全保护机制,所以在pwn中的利用方式有很多,这里先写tcache poisoning(话说好多坑没写😀,以后再说😀,也一直没有找到合适的题🤔,现在的题型都好新,只边打比赛边总结,所以这里还是主要往原理靠,写完了会开漏洞总结与wp)
Tcache usage
tcache执行流程如下:
第一次malloc时,回显malloc一块内存用来存放tcache_perthread_struct,这块内存size一般为0x251
释放chunk时,如果chunk的size小于small bin size,在进入tcache之前会先放进fastbin或者unsorted bin中
在放入tcache后:
先放到对应的tcache中,直到tcache被填满(7个)
tcache被填满后,接下来再释放chunk,就会直接放进fastbin或者unsorted bin中
tcache中的chunk不会发生合并,不取消inuse bit
重新申请chunk,并且申请的size符合tcache的范围,则先从tcache中取chunk,直到tcache为空
tcache为空后,从bin中找
tcache为空时,如果fastbin、small bin、unsorted bin中有size符合的chunk,会先把fastbin、small bin、unsorted bin中的chunk放到tcache中,直到填满,之后再从tcache中取
需要注意的是,在采用tcache的情况下,只要是bin中存在符合size大小的chunk,那么在重启之前都需要经过tcache一手。并且由于tcache为空时先从其他bin中导入到tcache,所以此时chunk在bin中和在tcache中的顺序会反过来
源码分析
内存申请:
// 从 tcache list 中获取内存
if (tc_idx < mp_.tcache_bins && tcache && tcache->entries[tc_idx] != NULL)
{
return tcache_get (tc_idx);
}
DIAG_POP_NEEDS_COMMENT;
#endif
}
这部分的代码描述的是从tcache中取chunk的一系列步骤,首先是在tcache中有chunk的时候,if判断要取出的chunk的size是否满足idx的合法范围,在tcache->entries不为空时调用tcache_get()函数获取chunk。
tcache_get()函数
接下来看一下tcache_get()函数的代码:
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
–(tcache->counts[tc_idx]);
return (void *) e;
}
可以看到tcache_get()函数的执行流程很简单,从tcache->entries[tc_idx]获取一个chunk指针,并且tcache->counts减一,没有过多的安全检查或者保护
内存释放
我们看一下在有tcache的情况下_int_free()中的执行流程:
static void
int_free (mstate av, mchunkptr p, int have_lock)
{
……
……
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
if (tcache
&& tc_idx < mp_.tcache_bins // 64
&& tcache->counts[tc_idx] < mp.tcache_count) // 7
{
tcache_put (p, tc_idx);
return;
}
}
#endif
可以看到首先判断tc_idx的合法性,判断tcache->counts[tc_idx]在7个以内时,进入tcache_put()函数,传递的一参为要释放的chunk指针,二参为chunk对应的size在tcache中的下标
tcache_put()函数
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
这里先用how2heap里面的glibc_2.27
tcache poisoning:
!()[https://cdn.jsdelivr.net/gh/kiki1e/imagess@main/img/20240623015238.png]
其实在最开始free(b)之后b[0] = (intptr_t)&stack_var;
printf(“Now the tcache list has [ %p -> %p ].\n”, b, &stack_var);
这里为什么tcache list里面&stack_var还是被写进去了,修改b[0] = (intptr_t)&stack_var;的意义
通过将b[0]设置为&stack_var的地址,攻击者实际上篡改了tcache链表的结构。原本b后的下一个块应该是a(如果按照正常的tcache逻辑),但现在它被篡改为栈上变量stack_var的地址。这种篡改使得当后续有新的malloc请求时,程序可能会错误地从tcache中返回stack_var所在地址作为已释放内存的地址,从而实现对栈上数据的控制,这是许多内存破坏攻击的基础,比如实现任意代码执行。
largebin attack
在free的时候,chunk要么被放入fastbin,要么就被放到unsorted bin。当我们再次malloc的时候,如果对unsorted bin做了遍历,unsorted bin里的chunk才会被放到对应的bin里,比如large bin、small bin。比如我在unsorted bin里有一个size为0x420的chunk,那么它会被放到对应的large bin里。而large bin attack就是利用了unsorted bin里未归位的chunk插入到large bin时的解链、成链操作。来看一段malloc中的源码
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) //遍历unsorted bin
{
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); //获得当前unsorted bin chunk的大小
..........................
if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else //属于large bin范围
{
victim_index = largebin_index (size); //根据size,得到对应的large bin索引
bck = bin_at (av, victim_index); //获取对应索引的large bin里的最后一个chunk
fwd = bck->fd; //获得对应索引的large bin的第一个chunk
/* maintain large bins in sorted order */
if (fwd != bck) //这意味着当前索引的large bin里chunk不为空
{
/* 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->bk_nextsize可控
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim; //第一次任意地址写入unsorted bin chunk的地址
}
bck = fwd->bk; //bk也就是large bin的bk位置的数据,因此bck可控
}
}
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; //第二次任意地址写入unsorted bin chunk的地址
define MAX_ITERS 10000
if (++iters >= MAX_ITERS)
break;
}
首先,victim就是unsorted bin chunk,也就在unsorted bin里未归位的large bin chunk。而fwd就是large bin的chunk。从上来看,首先,在unsorted bin里我们得有一个large bin chunk,并且在large bin里,我们也要有一个chunk,但是,我们得保证unsorted bin里的那个large bin chunk的size要比large bin里已有的这个chunk的size要大一点,但是都属于同一个index。
这样做的目的是我们想绕过前面的这一大块,直接到后面的那个else处。
然后,假设我们通过UAF或其他漏洞,控制了large bin里的这个chunk的bk_nextsize为addr1,那么victim->bk_nextsize->fd_nextsize = victim; //第一次任意地址写入unsorted bin chunk的地址 也就是addr1->fd_nextsize = victim,也就是*(addr1+0x20) = victim,这就是第一次任意地址写一个堆地址;接下来,假如,我们还能控制large bin里那个chunk的bk为addr2,那么首先bck = fwd->bk; 使得bck = addr2,接下来bck->fd = victim; //第二次任意地址写入unsorted bin chunk的地址 也就是addr2->fd = victim,也就是*(addr2+0x10) = victim。这样利用large bin attack,我们可以有两次往任意地址写入堆地址的机会。
结语
某日2:25,大体上将堆利用的基本原理(其实还差几个,就像house of sprit)整到了这上面,当然这是初稿,后面会对其做系统的描述,至少将题目搞上,主要还是拖的太久了,也产生了时不待我的愧疚感