PWN堆house-of-lore
- 这个堆利用就需要伪造堆块了,
house of lore
这个利用方式只对glibc2.23-glibc2.31
之间的这个版本 glibc2.31
以上的版本house of lore
的利用方式基本失效。
前置知识
-
这里也统一认知,之前介绍了在堆块的物理地址中
高地址
为前,低地址
为后,接下来对堆块的逻辑地址的前后进行一个认知的统一。 -
通常在释放的堆块如果被
bins
管理,他们都会将fd
指针和bk
指针给利用起来,之前一直没搞明白fd
指针指向前面还是后面,bk
指针指向前面还是后面。 -
fd
指针的全称为forward pointer
,bk
指针的全称为backward pointer
。在fastbin
和tcachebin
中我们使用了fd
指针,并且这个时候fd
指针都指向的是远离bins
堆块的方向。这边就以tcache_bins
的图片为例子。
- 所以这次我们对逻辑地址做一个认知统一。
- 远离
bins
的堆块是更前面的堆块,所以fd
指针指向的是远离bins
的堆块 - 靠近
bins
的堆块是更后面的堆块,所以bk
指针指向的是靠近bins
的堆块
- 远离
smallbin机制
-
这里为了能更好的理解
house of lore
接下来就要了解一下smallbin
这个bins
的运行机制,这样更有利于我们学习house of lore
-
之前我们释放的堆块基本上都是被放入
fastbin
、tachacbin
、unsortedbin
,这三个bins中,很少有见到释放的堆块被放入smallbin
中。所以我们先研究一下怎样才能使得被释放的堆块放入smallbin
中。 -
smallbin
是一个双向循环链表的结构,并且smallbin
是一个长为64
的一个数组。在相关宏定义中这个#define NSMALLBINS 64
可以知道是64
个堆块的数组。 -
从
malloc.c
中的这个结构体定义中struct malloc_state
还可以得知mchunkptr bins[NBINS * 2 - 2];
,这个堆块长度为254
长度,其中包含了smallbin
和largebin
。- 注意:这里会存在一个误区,这边的bins既然是254个元素是否与之前unlink 图片所说的largebins和smallbins的个数不一样
- 注意:由于smallbins和largebins是一个双向链表结构,而bins数组中的每一个元素相当与一个指针,为了使得bins能够有两个指针,所以对应smallbins[0]它会将bins[0]作为fd指针,bins[1]作为bk指针,利用这种方式构造一个双向头结点
-
从
62-63
行代码的宏定义in_smallbin_range
可以得知,chunk_size<MIN_LARGE_SIZE
即在64
位系统中小于0x400
大小的chunk都会被放入smallbin
的堆块里面,在32
位系统中小于0x200
大小的chunk都会被放入smallbin
的堆块里面(在相关代码中的宏定义有做具体介绍) -
从
66-68
行代码的宏定义smallbin_index
可以得知,每个相邻smallbin
存储的空闲堆块size位在64
位中相差0x10
字节,在32
位中相差0x8
字节
1 | // 堆块结构 |
smallbin
这个堆块结构也是一个先进先出的堆块结构,(堆块被释放后放入smallbin的过程与fastbin放入机制相同,都是使用头插法),只不过smallbin
是双向链表。接下来给出一个smallbin
大致的结构图,这样更有利于我们了解(以64位系统为例子),注意smallbin
和largebin
中是这样的- 而
smallbin
这个堆块的取出与fastbin
堆块取出机制不同,fastbin取出的是最靠近bins的堆块。而smallbin
是先取出的是最远离bins
的堆块,即如果要使用smallbin的堆块时先取出的是chunk0
-
被释放的堆块被放入
smallbin
的条件:- 首先被释放的堆块要符合
smallbin
堆块大小的范围: - 接下来分析一下代码得出如下结论:当我们释放一个堆块,这个堆块被放入
unsortedbin
后,我们之后再申请一个堆块,这个堆块的申请如果既没有使用unsortedbin
中的堆块,也没有使用smallbin
中的堆块,这样先前被放入unsortedbin
中的堆块就会被放入smallbin
- 当
smallbin
中的堆块被切割后,切割后的堆块会先放入unsortedbin
中(因为size位改变,现在的chunk中的size并不在当前smallbin
范围中),之后我们再使用malloc
申请一个堆块,并且申请的堆块没有使得unsortedbin
中的堆块被切割,那么在unsortedbin
中的堆块就会被放入smallbin
中的堆块
- 首先被释放的堆块要符合
-
接下来我们给出一个示例程序:
1 |
|
- 接下来我们使用gdb动态调试查看一下堆块的运行机制。我们先申请一个
p1
堆块和p2
堆块。
- 然后将
p1
堆块释放,该堆块就会被放入unsortedbin
中
- 然后我们再申请一个
p3
堆块,这样p1
堆块就会被放入smallbin[idx1]
中管理
- 然后我们再申请一个
p4
堆块,这样原来的p1
堆块就会被切割,并放入unsortebin
中
- 之后申请一个
p5
的堆块,这样p1
堆块就会又被放入smallbin[idx2]
中管理,其中idx2 < idx1
- 此外还可以进行这样的动态调试去查看
smallbins
是的双向链表机制
1 |
|
相关代码
- 接下来给出
glibc2.23
中的smallbin
的相关源码。 - 下面是
malloc
中关于smallbin的代码
1 | if (in_smallbin_range (nb)) |
- 下面是
_int_free
中的smallbin相关操作
_int_free中的源码
1 | /* |
相关宏定义
1 | // min_large_size存储在largebin的最小值,也就是smallbin存储堆块size的最大范围 |
- 在
glibc2.27
中添加了一个检查机制
1 | if (__glibc_unlikely (bck->fd != victim)) |
实验
源码
1 | /* |
- 还是老样子,将源码翻译一遍。
1 | /* |
- 接下来进行一下动态调试,我们先申请一个大小为
0x100
的堆块,这个堆块实际的size
位大小为0x110
- 并且我们在栈上分别申请了
0x18
大小和0x20
大小的内存空间
- 接下来我们就会利用栈上的内存空间去伪造一个
fake_chunk
,在伪造这个fake_chunk
的时候我们需要我们前面申请的victim
堆地址(victim的prev_size的地址即堆块的起始地址),将fake_chunk
的fd
指针指向victim
的起始地址。这样是为了绕过small bin corrupted的检查
的检查。
- 之后我们修改
stack_buffer_1[3]=stack_buffer_2
,stack_buffer_2[2]=stack_buffer_1
,这样做的目的是为了绕过最后一次malloc中small bin corruped的检查
。这样伪造后,伪造的堆块就会构成一个双向链表。
- 之后我们申请一个
1000
大小的堆块,这样就使得我们之后将victim
释放后不会与topchunk
合并,并且victim
这个堆块就会被放入unsortedbin
中管理 - 并且由于
unsortedbin
也是一个双向链表,所以victim->fd
和victim->bk
指向的是unsortedbin
这个堆块地址
- 之后我们再申请一个更大的堆块,使得申请的堆块不是由
unsortedbin
、smallbins
中的堆块切割下来的。在使用malloc
过程中就会使得原来在unsortedbin
链表中的victim
堆块,被放入smallbins
中
- 之后我们再通过
UAF
漏洞修改victim
的bk
指针,这时我们smallbin
堆块的链表结构就如下图
- 这个时候我们再申请一个与之前申请
victim
大小的堆块,就会将smallbin
中的堆块给申请回来,这样就会使得fake_chunk
被链到smallbin
中 - 这时就会触发
smallbin->bk = victim->bk=stack_buffer1_addr
- 这时我们再申请一个
0x100
大小的堆块,由于smallbin
中的堆块是先进先出
,主要取出的是bins->bk
所指堆块,所以我们就将stack_buffer1
处的这个堆块给申请过来了。
- 这时我们就申请到栈上的数据了,并且我们可以将
jackpot
的地址写入到栈上了,这时我们就覆盖这个函数的地址为返回地址
利用方式
- 现在就给这个
house of lore
的利用方式做一个总结。首先house of lore
这个堆块的利用方式是针对的是smallbin
。 - 漏洞利用的条件:
- 需要
UAF
漏洞,这样才能对smallbin
中堆块的bk
指针进行修改(如果没有UAF
漏洞可以尝试使用off-by-null
漏洞尝试使用堆风水,去构造UAF漏洞) - 还需要知道堆地址,这样我们在伪造
fake_chunk
的时候才能向fake_chunk->fd
指针这个位置写入chunk_adddr
从而绕过检查 - 可能还需要栈地址或者是其他段地址,这样我们在申请任意堆块内存的时候就可以绕过检查
- 需要
- 接下来归纳一下我们需要伪造的堆块的数据有哪些:
- 对于放入
smallbin
中的堆块victim
,我们需要修改其bk
指针,修改bk
指针为fake_chunk1_addr
- 对于我们要申请的目标地址
fake_chunk1
,我们要伪造其fd
指针,使得其fd
指针指向victim
,使得bk
指针指向fake_chunk2
- 对于我们要借助绕过申请目标地址的辅助
chunk
即fake_chunk2
,我们要修改其fd
指针为fake_chunk1
- 对于放入
house-of-lore_level1
- 接下尝试写一题,题目来源:
ctfhub-house-of-lore
level_1分析1
- 拿到附件后我们就先
check
一下附件,看看附件开启了什么保护机制。发现没有开PIE
- 然后我们再逆向该程序,使用
IDA pro
,先来查看一下main
函数的运行逻辑。main
函数的大致逻辑如下:- 先对输入输出进行初始化
- 然后再初始化一个
name_message
- 之后就会进入一个
menu
的循环之中,这个时候程序会让用户read_int
即输入一个选项,这里我们就来归纳一下对应选项的菜单,这里的menu
函数就不进去查看了。- 选项1:
add
操作 - 选项2:
edit
操作 - 选项3:
dele
操作 - 选项4:
change name
操作 - 选项5:
change message
操作,注意在程序只能调用一次change message
,调用完之后再调用就不会调用change message
函数了 - 选项6:
exit
- 选项其他:
Invalid choice
- 选项1:
- 以上就是main函数的执行流程,然后我们现在先来查看一下
init_message
这个函数的功能- 首先会让用户输入
name
,name
也是一个全局变量 - 之后会申请一个堆块保存给全局变量
message
,申请堆块后会让用户向这个堆块中输入内容
- 首先会让用户输入
- 接下来我们继续安装
增删改查
的顺序查看每个函数的功能- 先查看的是
add
函数,这里出现了一个名为page_list
的全局变量,这个全局变量是一个指针数组,存储的是地址,数组里面有7
个元素 - 如果
page_list
满了就会输出Full
并且结束该函数 - 如果没满,就会提示用户输入
size
,这个size
就是之后我们要申请的堆块的大小 - 申请完之后,
malloc
返回的指针就赋值给对应page_list
的地方 - 之后又出现了一个名为
size_list
的全局变量,这个全局变量是一个int
类型的数组,这个数组存储的是我们所申请的size
的值
- 先查看的是
- 之后来查看
dele
函数,该函数的程序运行逻辑如下:- 用户首先要选择需要释放的堆块,之后程序会检查这个索引是否合理
- 之后释放这个堆块,然后同时将相应的
page_list
和size_list
置零。
- 然后查看
edit
函数,函数逻辑如下:- 首先要求用户输入要
edit
堆块的index
- 然后就使用
read
向堆块中输入内容,指定输入长度为size_list
- 首先要求用户输入要
- 然后查看
change_name()
函数,函数的主要逻辑如下,就是重新向name
写入东西
- 再查看
change_message()
这个函数,函数的具体逻辑如下:- 首先泄露出这个
message
的堆地址,然后会释放message
所指向的堆块 - 然后再让用户输入之后要申请的堆块大小
- 申请堆块,
malloc
的返回值将赋值给buf
- 之后就是向
buf
写入内容,长度不能超过我们所申请的。 - 之后还会修改
message
所指向的堆块注意这里就存在着UAF漏洞 - 修改
message
所指向堆块后就会更新message
- 首先泄露出这个
- 逆向该程序的逻辑后,我们找到了漏洞利用的主要地方,这个地方就是在
change_message()
这个函数中,我们可以利用这个函数进行UAF漏洞 - 我们现在也对全局变量进行一个归纳和汇总:
name
:存储着用户输入的数据,相当于字符数组
,长度为0x40
大小。message
:是一个指针,指向malloc
返回的堆块地址page_list
:是一个指针数组,指向malloc
返回的堆块地址,一共有7
个元素size_list
:是一个int类型的数组,存储着page_list
对应索引申请的堆块大小
level_1分析2
- 接下来我们边写脚本,边进行动态调试。根据程序运行逻辑我们编写了如下代码进行交互。
1 | from pwn import * |
- 根据分析1,我们可以修改
释放后的message指向的堆块
,接下来我们来查看释放后的堆块会被放在什么bins
中。我们会发现这个堆块会被放入smallbins
中,这时我们可以read(0, message, 0x10uLL)
从而修改这个堆块的fd
、bk
指针
- 所以这题的考点就是
house-of-lore
,现在我们就要对堆块进行伪造,从而可以使用malloc
申请到任意地址。这里由于我们bss
段地址是固定的,并且我们的name
变量是可以写的,所以我们就使用name
这个数据块伪造堆块,使其绕过检查 - 这时我们通过
house-of-lore
的利用,就可以将这个name
的地址申请过来,并且由于之前的实验中的堆块并没有伪造size
位,只需要伪造fd
、bk
即可,所以我们接下来就对其进行伪造。 - 首先我们已经将堆块的地址泄露出来了,泄露出来的同时我们要修改处于
smallbins
中堆块的bk
指针。我们先来修改一下 - 这时我们修改的堆块就会出现一个问题,就是我们只要修改
bk
指针,我们会顺手把fd
指针也被修改了。在写这题的时候我有注意到这一点,但是后面发现,在house of lore中修改smallbins中的fd指针貌似不会对这种堆利用方式有什么影响
-
查看
glibc
源码时发现,在malloc
中只进行了bck->fd != victim
检查,在free
中才对fwd->bk != bck
进行检查,而house-of-lore
利用中我们后续并没有再释放堆块,所以并不会调用free
,所以并不需要关系fd
指针。 -
现在我们就开始进行
house-of-lore
的堆块伪造,由于之前我们确定name
这个.bss
段,但是我们只能对.bss
这个段写入0x20
的数据,所以我们要在利用name-0x10
这个字段,伪造fake_chunk1
的prevsize
和size
字段。
- 这时我们就通过
change_message
修改放在smallbin
中的chunk
。
1 | payload1 = b'a' |
- 现在我们在修改
bk
指针的同时就已经把堆地址给泄露出来了。现在我们就可以使用change_name
伪造堆块,由于输入字节数的原因,我们就可以进行如下操作,将fake_chunk2
与fake_chunk1
的bk
指针共用一个内存空间。
1 | payload = p64(heap_addr-0x10)+p64(0x6020A0+0x8) |
- 这时我们再进行两次申请,就可以将
name
申请回来,并且可以使用edit
编辑,编辑到page_list
这个数组
1 | add(0xb0) |
- 这时我们就还差libc地址没有泄露,在做这题的时候一直卡在泄露这块,看了
wp
才发现,可以这么泄露:- 我们可以修改我们申请回来的
name
这个空间溢出到page_list[0]
,将这个地址修改为free_got
的地址 - 然后再修改
page_list[1]
,将其地址修改为puts_got
表 - 再一次通过
edit
修改这时我们修改的时free_got
表存储的值,将其改为puts_plt
表,这时我们调用 - 最后我们再
free(page_list[1])
,这时传递的是puts_got
表的地址,该地址存储着puts
的地址 - 我们
free(page_list[1])
实际上是puts(puts_got)
,这时我们就泄露了libc
的地址
- 我们可以修改我们申请回来的
1 | free_got = 0x602018 |
- 最后我们再劫持
atoi_got
,将其劫持为system
的地址,之后我们在read_int
的时候直接输入/bin/sh\x00
,这样就可以直接getshell
1 | dele(1) |
level_1_exp
- exp如下:
1 | from pwn import * |
house-of-lore_level2