Breaking glibc’s ptmalloc2 (fastbin duplication)

10 minute read

Introduction

Before right now, I almost didn’t know anything about heap meta data exploitation, so this is considered my first heap exploitation experiment. The environment will be using is fedora 26 for x86_64 architecture. It wouldn’t matter that much

The challenge

The program we are running looks like this

The main function

The disassembly of the main function is like the following :-

 (fcn) main 39
   main ();
              ; DATA XREF from 0x004008b4 (entry0)
           0x00400870      4883ec08       sub rsp, 8                  ; section 13 va=0x00400870 pa=0x00000870 sz=1474 vsz=1474 rwx=--r-x .text
           0x00400874      488b3d251820.  mov rdi, qword [obj.stdout] ; FILE* stream
           0x0040087b      31c9           xor ecx, ecx                ; size_t size
           0x0040087d      ba02000000     mov edx, 2                  ; int mode
           0x00400882      31f6           xor esi, esi                ; char* buf
           0x00400884      e8b7ffffff     call sym.imp.setvbuf        ; int setvbuf(FILE*stream, char*buf, int mode, size_t size)
           0x00400889      31c0           xor eax, eax
           0x0040088b      e8d0040000     call fcn.00400d60
           0x00400890      31c0           xor eax, eax
           0x00400892      4883c408       add rsp, 8
           0x00400896      c3             ret

so the main function is basically setting buffer mode to no buffer, then it calls fcn.00400d60 and exits. It seams that fcn.00400d60 holds the real functionality. I would refer to that function from now on as real_main.

Analysis of real_main

The first part of real_main function looks like this First 2 basic blocks with in real_main function After setting up stack frame, sub.puts_d30 is called which is defined as following

 (fcn) sub.puts_d30 38
   sub.puts_d30 ();
              ; CALL XREF from 0x00400d72 (real_main)
           0x00400d30      4883ec08       sub rsp, 8
           0x00400d34      bf0c0f4000     mov edi, str.1:_Search_with_a_word ; 0x400f0c ; "1: Search with a word" ; const char * s
           0x00400d39      e862faffff     call sym.imp.puts           ; int puts(const char *s)
           0x00400d3e      bf220f4000     mov edi, str.2:_Index_a_sentence ; 0x400f22 ; "2: Index a sentence" ; const char * s
           0x00400d43      e858faffff     call sym.imp.puts           ; int puts(const char *s)
           0x00400d48      bf360f4000     mov edi, str.3:_Quit        ; rdi ; 0x400f36 ; "3: Quit"
           0x00400d4d      4883c408       add rsp, 8
           0x00400d51      e94afaffff     jmp sym.imp.puts

Make no mistake, this sub.puts_d30 is completely different from sym.imp.puts, sub.puts_d30 is named like this because it is convention in Radare2 to auto rename functions into a name that resembles most of its functionality; However, sym.imp.puts is the puts imported from libc (imp for imported). I would rename this function to print_banner instead for convenience.’ Also function sub.strtol_a40 reads number from user and returns it in eax, if it fails it prints error message and retry. I will rename it to read_number.

The next part of real_main function looks like this switch case in main function so it is basically a switch case on the value returned like this

int real_main(void) {
    while(1) {
        print_banner();
        switch (read_number) {
        case 1:
            //code here
            break;
        case 2:
            //code here
            break;
        case 3:
            //code here
            break;
        default:
            //code here
            break;
        }
    }
}

Code for case 1 starting at 0x00400d98 is basically a call to sub.puts_ad0, Since I ran the program, I do have an idea about what does this function generally do, so I will rename it to search. For case 2 the code is also simple it is just a call to sub.puts_c00. Since I know it adds new sentence to sentences lists ( or maybe replaces the old sentence with new one who knows), so I will rename it add. Case 3 will simply clean the stack and return, And if you entered any number other than 1,2,3 it will call sub.puts_990 and pass string Invalid option to it. That function is defined like this.

 (fcn) sub.puts_990 19
   sub.puts_990 ();
              ; CALL XREF from 0x00400db7 (real_main)
              ; CALL XREF from 0x00400a35 (sub.strtol_9b0)
              ; CALL XREF from 0x00400bf7 (search)
              ; CALL XREF from 0x00400d1f (sub.puts_c00)
           0x00400990      4883ec08       sub rsp, 8
           0x00400994      e807feffff     call sym.imp.puts           ; int puts(const char *s)
           0x00400999      bf01000000     mov edi, 1                  ; int status
           0x0040099e      e8adfeffff     call sym.imp.exit           ; void exit(int status)

It basically prints the string passed to it then exit with exit code 1. I would rename it to puts_exit.The full real_main code looks like this:

void puts_exit(char *msg) {
    puts(msg);
    exit(1);
}
int real_main(void) {
    while(1) {
        print_banner();
        switch (read_number()) {
        case 1:
            search();
            break;
        case 2:
            add();
            break;
        case 3:
            return
        default:
            puts_exit("Invalid option");
            break;
        }
    }
}

We will notice that both function add and search takes no arguments, the program behavior showed that search function can access data created via add function, so this implies that data is stored in data structures here.

Analysis of the add function

first part of add function that we will look at is

     0x00400c00      4155           push r13
     0x00400c02      bfd00e4000     mov edi, str.Enter_the_sentence_size: ; 0x400ed0 ; "Enter the sentence size:" ; const char * str
     0x00400c07      4154           push r12
     0x00400c09      55             push rbp
     0x00400c0a      53             push rbx
     0x00400c0b      4883ec08       sub rsp, 8
     0x00400c0f      e88cfbffff     call sym.imp.puts           ; int puts(const char *s)
     0x00400c14      31c0           xor eax, eax
     0x00400c16      e825feffff     call read_number
     0x00400c1b      8d68ff         lea ebp, [rax - 1]
     0x00400c1e      4189c5         mov r13d, eax
     0x00400c21      81fdfdff0000   cmp ebp, 0xfffd
┌──< 0x00400c27      0f87ed000000   ja 0x400d1a
			....
			....
└──> 0x00400d1a      bf830e4000     mov edi, str.Invalid_size   ; 0x400e83 ; "Invalid size" ; const char * s
     0x00400d1f      e86cfcffff     call puts_exit              ; void exit(int status)

It first reads buffer size from user then make sure that the buffer size is never above 0xfffe. (size - 1 should be <= 0xfffd). We can move on to the next part of the add function.

0x00400c2d      bfe90e4000     mov edi, str.Enter_the_sentence: ; 0x400ee9 ; "Enter the sentence:" ; const char * s
0x00400c32      e869fbffff     call sym.imp.puts           ; int puts(const char *s)
0x00400c37      4963fd         movsxd rdi, r13d            ; size_t size
0x00400c3a      e8e1fbffff     call sym.imp.malloc         ;  void *malloc(size_t size)
0x00400c3f      31d2           xor edx, edx
0x00400c41      4889c7         mov rdi, rax
0x00400c44      4489ee         mov esi, r13d
0x00400c47      4989c4         mov r12, rax
0x00400c4a      e861fdffff     call sub.strtol_9b0
0x00400c4f      bf28000000     mov edi, 0x28               ; '(' ; 40 ; size_t size
0x00400c54      498d5c2401     lea rbx, [r12 + 1]          ; 1
0x00400c59      498d6c2c02     lea rbp, [r12 + rbp + 2]    ; 2
0x00400c5e      e8bdfbffff     call sym.imp.malloc         ;  void *malloc(size_t size)
0x00400c63      31d2           xor edx, edx
0x00400c65      4c8920         mov qword [rax], r12
0x00400c68      c74008000000.  mov dword [rax + 8], 0
0x00400c6f      4c896010       mov qword [rax + 0x10], r12
0x00400c73      44896818       mov dword [rax + 0x18], r13d
0x00400c77      eb16           jmp 0x400c8f

It basically allocates memory with with user chosen size and then calls sub.strtol_9b0 which basically fills the allocated memory with user input data, Next it allocates 0x28 bytes memory, from its size it looks like a structure, First 8 bytes is pointer to string we passed, second 8 bytes are initialized to zero, third 8 bytes are for now also pointing to the same string, and finally there is 4 bytes pointing to the size of string, the rest is unused for now. The C representation looks like this:

struct sentence {
    char *str1;            //  0x0 - 0x8
    uint64_t reserved;     //  0x8 - 0x10
    char *str2;            // 0x10 - 0x18
    uint32_t size;         // 0x18 - 0x1c
    uint8_t reserved2[12]; // 0x1c - 0x28
}

The next part is basically a loop over all characters in our string skipping all spaces loop in add function

The full psuedo-c representation looks like this

void add() {
    puts("Enter the sentence size:")
    uint32_t sz = read_number()
    if (sz - 1 > 0xfffd) {
    	puts_exit("Invalid size")
    }
    puts("Enter the sentence:");
    char *s = malloc(sz);
    strtol_9b0(s);
    struct sentence *holder = malloc(sizeof(struct sentence));
    holder->str1 = s;
    holder->reserved = 0;
    holder->str2 = s;
    holder->size = sz;
    char *x = s + 1;
    char *y = x + sz + 1;
    int space = 0;
    do {
        if (x[-1] == ' ') {
	    space += 1;
	    continue;
	}
	if (!space) {
	    holder->str1 = x;
	    continue;
	}

    } while (++x != y);
}

Tags: ,

Updated: