how2heap分析-_2.23

how2heap

用how2heap辅助一下

按说明书操作就对了

源码加载进去

directory /home/shark/Desktop/ctf/claude_code/how2heap/glibc_2.23/malloc

start

add-symbol-file /home/shark/Desktop/ctf/claude_code/how2heap/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/.debug/lib/x86_64-linux-gnu/libc-2.23.so 0x7ffff7800000(gibc开始地址)

不用了,有更好的方法

太超模了

fastbin_dup

fastbin中简单的double free

先看看正常运行后的结果

可以看到第二轮malloc的时候返回了两次同样的addr

用pwndbg调试看看

先free了一个a,这是bins链表和heap空间的布局

free b后用头插法进入fastbins bins

接下来看看double free

可以看出a的next指针指向了b

看看

仅检查 fastbin 头部是否 == 被 free 的指针

也就是说只要隔一个chunk就可以实现double free

old装的chunk b

然后malloc回来

为什么malloc后fastbin的链表没变化?

不对,有变化

从aba变成bab了

循环了,怪不得

fastbin的检查还是太weak了

fastbin_dup_into_stack

任意地址分配

先run一下

用fastbin_dup伪造后还得伪造size

看看源码

stack_var在stack上

伪造size

一个正常的 Chunk 结构是:prev_size (8 bytes) -> size (8 bytes) -> user_data

这时候的d指向a,我们要return stack_var+8就得在&stack_var伪造size通过检查

改完后

malloc出来了

house_of_spirit

fake chunk这一块

在可控空间内伪造chunk结构**(比如栈、BSS段)**,然后调用free,malloc后返回,实现任意地址写

pwn.college里写过好多了

这回先看看源码

初始化栈空间

fake_chunk注意16字节对齐

next size的value只要>16, <128kb左右(系统分配的堆内存总大小)就可以了,不要忘了top chunk也是chunk

ni步进


_int_free检查

成了

返回satck_addr

unsorted_bin_attack

将一个libc的地址写入到一个任意的内存地址中

glibc2.23中的_int_malloc在遍历unsorted bin并取出chunk时缺少链表完整性检查

看看unsorted_chunks()宏,

简单粗暴

可以修改unsorted bin chunk中的bk,取出时bck->fd = libc_addr

先看看源码

19:此时,这个 Chunk 会被 glibc 放入 Unsorted Bin 的双向链表中。

在这个状态下,p 的前向指针 (fd) 和后向指针 (bk) 都会指向 libc 内部的 main_arena 结构体中的某个地址。p[1] 其实就是访问这个 Chunk 的 bk 指针

25:模拟UAF,把chunk的bk指针改成&stack_var - 2,即目标地址-0x10

free后

改bk成功

利用free时chunk向后合并,触发unlink

fastbin的free不会改变相邻chunk的PREV_INUSE,也不会触发合并,所以要malloc大一点

利用条件:

1.有一个已知地址的指针(p)指向堆块

2.off-by-one,要清空prev_inuse位

3.malloc要大于fastbin的范围, 这样free的时候才会触发合并调用unlink宏

我们先看看unlink的校验

也就是说当调用free(p)时,unlink会检查:我的前一个节点的下一个节点必须是我,我的后一个节点的前一个节点也必须是我

我们只要在p指向的chunk附近(0x10,0x18 处)伪造fd,bk就可以绕过。

设p指向chunk0

p->fd就是&chunk0+0x18,p->bk就是&chunk0+0x10,我们的检查要求&chunk0+0x18处的指针指向的地址+0x18处的地址储存的指针指向p,&chunk0+0x10处的指针指向的地址+0x10处储存的指针指向p

通过检查后就开始双向链表摘除,

FD->bk = BK => *(&P_ptr) = &P_ptr - 0x10
BK->fd = FD => *(&P_ptr) = &P_ptr - 0x18

p被赋值了两次

将p指针的值改成自己地址-0x18的位置(p一般在bss,data段)

开始伪造chunk1 head

prev_size = 0x80

还有这写法

看看_int_free中的consolidate backward

chunk1 - 0x80 = chunk0 + 0x10 = chunk0_ptr + 0x10

正常free chunk0时prev_size应该是0x90但这里要配合fake chunk

free chunk1后经过!prev_inuse检查,调用unlink()宏

chunk0_ptr = &chunk0_ptr - 0x18

这样就可以控制p指针了,

vic_str 里面的value本来是hello,将vic_str的地址赋到chunk0[3]上,即将p指针重新赋值然后对chunk0[0]赋值 等价于*(victim_string) =?????

这就是利用unlink对任意地址的值覆盖流程

下面用pwndbg看看

chunk0 malloc了0x90

fd赋值

bk赋值

改chunk1的prev_size

清空prev_inuse

unlink之前

unlink后

ok

成功

house_of_force

通过改top chunk的size实现对任意内存的读写

这个在pwn.college里听yan教授讲过的,yan好像挺喜欢这个,原理上不难,把top chunk的size值改成-1就行了,

看看源码

目标是覆写bss段的数据

改top chunk size

当前的 malloc(evil_size) 执行完后,新的 Top Chunk 地址变成 bss_var - 0x10

这时再malloc返回的就是bss_var,然后就任意覆写了

初始化heap

改top chunk size


使top chunk的addr到bss_var -0x10

成了

0xffffffffffffffff - 0xffffffffffffef00 = 0x10ff,glibc算出来的top chunk剩余空间

0x555555558028: 0x676e697274732061 -> “a string”

0x555555558030: 0x6577207461687420 -> “ that we”

成了

fastbin_dup_consolidate

利用fastbin的free时的chunk合并跟UAF,拿到两个指针指向同一个内存

看看源码

先free p1(fastbin)

为了触发chunk合并,第二次得malloc超过fastbin的chunk(检查small bins失败后调用malloc_consolidate)

此时, p3 指向的addr 应该等同于p1

free p1等同于free p3,但p3指针仍然可用

再malloc跟p3一样大的chunk,就得到了指向同一chunk且可用的指针

pwndbg启动

free p1进入fastbin,不合并,不会清除Top chunk的PREV_INUSE

还有这细节

malloc 0x400,加上header是0x410,属于large chunk,在到常规bin遍历前glibc会调用malloc_consolidate()防止内存碎片化,然后p1就会被合并进top chunk

触发合并,

p3,p1指向同一chunk

free p1

chunk3被top chunk吞了

p4 = p3

成功

house_of_lore

好熟悉的手法

利用 smallbin 链表伪造实现在栈上分配 chunk

伪造chunk

先malloc 0x100

free 掉

然后malloc一个大块

0x110被归类到了smallbins

改bk指针

取出

检查

bk指针指向stack_buffer_1

malloc 出来

成功

large_bin_attack

向任意地址写入一个heap的指针,可以用它改global_max_fast,使后续的chunk malloc,free 都当fastbin处理

heap初始布局

free p1,p2,进入unsorted bin

malloc 0x90

然后free 3

改p2

(这里回顾一下large bin的排序规则,large bin中有两套链,fd/bk 正常双向链表和fd_nextsize / bk_nextsize 按size排序的辅助链用来连接

可以概括成同一个large bin里,chunk按size从大到小排列,相同size的chunk会插到该size组的第二个位置,这样可以不动 fd_nextsize / bk_nextsize链表

large bin本身是按size范围分bin的,进入同一个large bin之后,还会按精确的size值排序

先选一个large bin 按size分组,然后在同一个large bin的内部按chunk的size排序

fd_nextsize / bk_nextsize链表可以减少遍历次数

然后malloc 0x90,遍历unsorted bin,p3 进入large bin,因为p3 size >p2 size

largebin排序进入这个分支(else)

(fwk,bck 是链表插入位置的前后节点的临时指针,glibc会先根据size找到插入位置,然后设置bck和fwd)

victim = p3 chunk header

fwd = p2 chunk header

我们看看这两行指令

victim->bk_nextsize = fwd->bk_nextsize

victim->bk_nextseize->fd_nextsize = victim

可知

fwd = p2

p2->bk_nextsize = &stack_var2 - 0x20

victim = p3

so

victim->bk_nextsize = &stack_var2 - 0x20

(&stack_var2 - 0x20)->fd_nextsize = p3

the offset of fd_nextsize is 0x20

stack_var2 = p3 chunk header

同理

gdb启动

初始布局

free 1,2后

用malloc触发遍历

然后free 3

然后改p2

改完后再用malloc触发遍历

开始malloc 0x90

成了

overlapping_chunks

通过改unsorted bin 中的freed chunk的size,使接下来return的chunk覆盖下一个chunk,原理不难

看看源码

先malloc了三个chunk

把chunk2 free了

改了chunk2的size段

malloc一块size差不多的内存,这样p4就可以包含p3

pwndbg启动

free掉了chunk2

改size

malloc后,可以看到p4跟p3重合了

overlapping_chunks2

我看看,非相邻chunk合并?

这回是改正在使用中的chunk,然后通过free使其产生更大的free chunk

看看源码

先malloc了5个chunk,chunk5防止chunk4被top chunk合并

填入字符方便观察

free掉chunk4

如果有堆溢出的话,通过chunk1可以改chunk2的size段

把 p2 的 size 从 0x400 改成了 0x800(即 p2 和 p3 大小的总和),并保留 prev_inuse 为 1(防止触发向后合并)

然后把chunk2free掉

这里因为chunk4 已经free掉了,保留了chunk2的prev_inuse,不向前合并

进入分支

改了 p2 的 size,此时 nextchunk 跨过了 p3,直接指向了 p4!因为 p4 后面还有个 p5 挡着,所以 nextchunk 不是 Top Chunk,进入分支

4014~4017会把chunk4加上,chunk4本就是合法的,不存在问题

将大chunk malloc回来

开始调试

free掉chunk4

改chunk2的size

成功伪造大freed chunk

malloc

验证

成功

poison_null_byte

单字节溢出?

用一个NULL byte 污染chunk size

源码分析

malloc 4个chunk

malloc unlink的时候会有检查

chunksize(P) == prev_size(next_chunk(P))

提前写0x200绕过

先free,后覆盖size低字节

从之前free掉的b中切一块给b1

malloc b2,依然从之前free 掉的 chunk中切

并把其中塞满’B’

接着free 掉 b1和c

当 b1,b2,c 其中b2在使用时,free b1,c不会让它们合并成一个大chunk

但是现在的metadata错位了,从0x210变成了0x200

free c时,glibc根据c->prev_size 和前一个chunk的状态判断是否合并

这时候glibc就会忽略b2

随后malloc 0x300会return那个合并后的chunk,其中包含正在使用的b2

gdb启动

初始化

提前写0x200绕过

free b

poison null

malloc 0x100

从free掉的b中切

malloc 0x80

free b1,c

free c时,glibc根据c->prev_size 和前一个chunk的状态判断是否合并

这时候glibc就会忽略b2

feee b1

free c

变成一个0x300多的大块

然后会malloc 0x300,return刚才合并的大块

成了

house_of_einherjar

利用off-by-one null byte 清掉下一个chunk的PREV_INUSE位,让free()以为前面有一个free chunk,然后向后合并到我们伪造的fake chunk,最后malloc 返回可控的地址附近

源码分析

malloc a = 0x38

造fake chunk

为通过unlink的检查,fwd和bck都指向自己

malloc 0xf8 ,b

prve_inuse清0

free b 后跟fake chunk合并

return fake chunk附近的可控地址

gdb启动

fake chunk

malloc b

清0

写fake prev_size

把 B 的 prev_size 改成:

B - fake_chunk_addr

更新fake chunk【1】

free b, 触发合并

成功

malloc回来

ok

unsorted_bin_into_stack

篡改 unsorted bin chunk 的 bk,让下一次 malloc() 从伪造的栈上 fake chunk 返回地址

源码分析

我去,就这么点

先初始化一块地方用来造fake chunk

然后malloc 0x100,malloc p1用来防止后续与top chunk合并

free 掉victim进入unsorted bin

create a fake chunk

set size and bk pointer

victim[-1] = 32; // 改小 victim 的 size (0x20) , 16字节对齐
victim[1] = (intptr_t)stack_buffer; // 篡改 victim 的 bk 指向栈上的 Fake Chunk

当 malloc 遍历 unsorted bin 时,会顺着 bk 指针往下找。把 bk 改成 stack_buffer,就能把栈强行拉入 unsorted bin 的链表中

改成 32 后,malloc 遍历到 victim 时,发现 size (32) 不等于请求的 size (0x110),就会将 victim 从 unsorted bin 中卸下,并丢进对应的 Small Bin,然后继续顺着 bk 遍历下一个 chunk(stack_buffer)

然后malloc出来

把ret 地址改成后门函数来检验

gdb启动

那个0x400的是缓冲区,下面两个0x111的才是victim和p1

free victim

fake chunk

改victim,uaf

之前

改完

这时malloc 0x100

成了

house_of_orange

终于看到io了,top chunk还能这么用?

the core of the house of orange is to get a free unsorted chunk without free function

this operation uses an overflow in the heap to corrupt the io_list_all pointer

It requries a leak of heap and libc

源码分析

先malloc一块chunk

利用溢出把 top chunk size 改成0xc01,要页对齐,同时PREV_INUSE位要保留

malloc一个大chunk,此时top chunk size 小于0x1000

触发sysmalloc()扩heap

old top chunk 进入unsorted bin

这就在没有明面上的free()的情况下实现了unsorted bin attack

此时old top chunk进入unsorted bin

这样就可以反推libc地址

old top chunk在unsorted bin中,它的fd/bk会指向main_arena里的unsorted bin 链表头附近,而io_list_all也是libc的全局符号

然后改bk地址

将file结构写入old top chunk

old top chunk作为fake file

把old top chunk的size改坏,以便后续触发malloc_printer(),malloc_printer()会触发abort(),abort()会触发_io_flush_all_lockp(),它会遍历io_list_all,然后fake file就有用了

继续伪造fake file

fp ->_mode = 0是为了通过检查

fp->_mode <= 0 是 glibc 在刷新 FILE 流时用来判断:

这个流是不是普通的字节流,能不能按普通 _IO_FILE 的方式去 flush

_mode < 0 还没确定 / 未初始化状态

_mode = 0 普通字节流

_mode > 0 宽字符流

glibc 在刷新所有流时,会判断这个 FILE 有没有待输出的数据:

if (fp->_IO_write_ptr > fp->_IO_write_base)

所以这里故意设置成:

_IO_write_base = 2
_IO_write_ptr = 3

于是:

3 > 2

条件成立。

glibc 就会认为:

这个 FILE 的写缓冲区里还有数据没刷新

然后继续调用:

_IO_OVERFLOW(fp, EOF);

底层就是:

fp->vtable->__overflow(fp, EOF);

overflow是写缓冲区满了,或者需要把缓冲区内容刷出去时,调用的处理函数,即输出缓冲区处理函数

伪造vtable

jump_table对应io_overflow,,这里是jump到winner()函数

winner()调用的是system(ptr)这里prt指向fake file开头,也就是”/bin/sh\x00”

触发malloc_printerr()

get shell

gdb启动

改top chunk

malloc 0x1000, unsorted bin attack

ok

对的对的

改bk

ok

最后触发

ok

house_of_mind_fastbin

伪造非main arena,然后把一个fastbin chunk 的non_main_arena位打开,这样在free时glibc会通过chunk地址找fake heap_info,然后取其中的ar_ptr当arena,最后把freed chunk 指针写入fake arena 的fastbinsY,这样就能将一个chunk地址写入指定内存中

源码分析

伪造fake arena

伪造malloc_state.system_mem

把heap推到fake_heap_info附近

malloc 0x50

伪造heap_info 的ar_ptr

改size标志位

free掉

ok

house_of_roman

还有hook hijack

直接利用unsorted bin attack 中的libc地址

不全记笔记了,之后遇到调试方面的问题再记


how2heap分析-_2.23
https://ghostshark-pro.github.io/2026/05/24/how2heap分析-_2.23/
Author
shark
Posted
2026年5月24日
License