0xresetti

making blog posts and memes about malware

View on GitHub

Binary Exploitation Challenge Writeups

Here are my writeups from any Binary Exploitation Challenges I have done. Mainly from CTF Workshop which you can download yourself and follow along if you like.

Recently, I’ve been learning Binary Exploitation and Exploit Development, I have been putting my notes into my Binary Exploitation Notes page, and after a few weeks of learning, I’m starting to get the hang of some Buffer Overflow concepts, so I thought I would do some writeups on some challenges I have been doing.

Buffer Overflows.

Challenge 1: BOF1

The first challenge we have is BOF1, which is a 32-bit executable, with no RELRO, no canary, no PIE/ASLR, however NX is enabled, which means we cannot jump to custom shellcode for example.

image

Running the file with some input doesn’t do anything, so lets have a look at the file in Ghidra

image

Ghidra shows us the main function, which asks the user to scan in input, the input variable has a 64 byte buffer, after that, the function checks to see if the local_14 variable is “not-equal-to” (!=) 0, if it isn’t, then it will print “you win!” and call system("/bin/sh") which will pop a shell.

image

To exploit this, we just need to scan in more than 64 bytes into the input! Which should be easy, just spam A’s!

image

Now that was easy, but kinda boring, lets move onto the next challenge.

Challenge 2: BOF2

The second challenge has the same protections and compilation as the BOF1 binary, 32-bit executable, no RELRO, no canary, no PIE/ASLR, but the NX bit is enabled

image

Running the file and giving it some random input, we can see the binary thinks we suck because we didn’t exploit it!

image

Lets take a look at the binary in Ghidra and see what it’s doing…

image

Now I renamed some of the variables here to make it a bit easier to understand (you can do this by clicking on a variable and pressing “L” on your keyboard to change the name).

The binary has a few variables, the ones that matter are input and isDEADBEEF. input has a buffer size of 64 bytes, and isDEADBEEF is at first set to 0. Then, the user input is scanned in, after that, a check is made to see if the isDEADBEEF variable is set to 0x21524111. If we hover over this value, we can see that in hex it translates to DEADBEEF

image

We can also see this more clearly in the disassembly without having to hover over the 0x21524111 value:

image

As you can see the != operator is in the if statement again, remember that means “not-equal-to”, in this case, if the isDEADBEEF variable is NOT equal to 0xdeadbeef, then the binary prints out “you suck!” and we lose.

In order to get the binary to print out “you win!” and pop a shell, we need to write 0xdeadbeef to the end of our input, lets write an exploit using pwntools to do this.

image

You can copy the above exploit below:

from pwn import *

target = process("./bof2") # set the process we are targeting

payload = b"" 
payload += b"A"*64 # fill up the `input` variable with 64 bytes, since it has a 64 byte buffer
payload += p32(0xdeadbeef) # write 0xdeadbeef with 32-bit endianness, since the binary is 32-bit


target.send(payload) # fire the payload

target.interactive() # go into interactive mode

This is what the exploit is doing:

Lets see if it worked:

image

As you can see, it worked perfectly and we popped a shell!

Challenge 3: BOF3

The next binary is the exact same as the last two binaries, we load it up, input something, and it tells us we suck. It also has the same protections

image

Lets dive into Ghidra to see whats going on in the background:

image

In the main function, we can see that we have a local_14 variable which calls the lose function, then we have our input scanned in, which again has a 64 byte buffer, then finally, the local_14 variable is called which in turn calls the lose function.

In the list of functions, we can see the lose function, and another hidden function which isnt called anywhere else named win

image

The lose function just prints “you suck!” however the win function prints “you win!” and gives us a shell:

image

image

So, how can we call the win function before the lose function is called?

Simple, by overwriting the return address with the memory address of the win function…

In this case, the memory address of the win function is 0x080484ab which you can see highlighted in green below:

image

What I mean by overwriting the return address is, when a function is called and it completes (for example, the lose function), the return() function is called at the end, which contains a memory address which either calls back to the main function memory address, another function memory address (if we were in a loop for example) or exits entirely.

REMEMBER: The eip register (rip on 64-bit binaries) is the Instruction Pointer, if the next process in the code flow is to return back to another function for example, the eip register would have the memory address of the function to return to inside it

Lets take a look at the way this would be exploited in GEF debugger before writing anything. (Feel free to use your favorite debugger, aka plain old GDB, pwndbg, r2 or PEDA):

I’m going to debug it in GEF:

image

As you can see, there is a line at main+36 which reads 0x08048518 <+36>: call 0x80483a0 <__isoc99_scanf@plt>, this is where our input is scanned into the binary, I’m going to set a breakpoint at the instruction just after this one at main+41 or 0x0804851d using b *0x0804851d

image

Once this breakpoint is set, I can run the program, and it will ask me for my input, since we know the buffer of the input variable is 64 from Ghidra and I am doing a demonstration, I’m going to use this buffer overflow pattern generator to generate a 64 character long pattern:

image

Once I run the program and paste this 64 character long pattern, anything AFTER those 64 characters should go into the eip register, to prove this, I’m going to add 4 B’s to the end of this 64 character pattern, making it a total of 68 characters, overflowing the buffer by 4 characters.

If everything goes correctly, the eip register will be filled with BBBB

image

As you can see, we hit our breakpoint, but the eip register has not been filled up yet, that is because we need to continue through some more instructions before until we see the return address being called… You can use the ni command in most GDB-based debuggers to do this:

After one ni, you can see the stack being filled up, but the eip register still hasnt changed yet:

image

After two ni, we can see the eax register has been filled with “BBBB”, this is a good sign:

image

And after a third ni, boom! We can see the program crashed and is no longer running, and the eip register has been filled with “BBBB”:

image

Perfect! So, now all we have to do to exploit the program and call the win function is to replace the “BBBB”’s we filled the eip register up with, with the memory address of the win function, lets write an exploit for this with pwntools!

image

Here is the exploit for you to copy:

from pwn import *

target = process("./bof3")

payload = b""
payload += b"A"*64
payload += p32(0x80484ab)

f = open("payload", "wb") # make "payload" file
f.write(payload) # Write payload to "payload" file
f.close() # finito


target.sendline(payload)

target.interactive()

And when we run the exploit:

image

We successfully overwrote the eip register with the win function memory address, called the win function which printed “you win!”, and popped a shell.

Now, you’re probably wondering what the whole f = open and f.write(payload) business is about. That is just the script outputting the payload to a file called “payload”, you can use these files with GDB-like debuggers to input the payload straight into the execution of the binary, like below, you can see inside the “payload” file is all 64 bytes of A’s to fill up the buffer until we get to the return address, then the final ��^D^H, is actually the win functions memory address in 32-bit endianness.

This payload can be ran within GEF and other debuggers like this:

image

As you can see above, I used r < payload to run the program with the “payload” string as the input, and we got the “you win!” message.

ROP

Challenge 1: ROP1

The first ROP challenge fried my head a bit, but I managed to figure it out. The file is a 32-bit executable, with no RELRO, no canary, no PIE/ASLR, but it does have the NX bit enabled:

image

Opening the file in Ghidra, the main function just calls the func function.

image

Inside the func function, the porgram scans in our input into a 64 byte buffer and we have a check that is similar to the previous BOF2 challenge, however, no win function is called after the check is complete, the program just exits:

image

On the other hand, we do have a win function, at memory address 0x80484ab

image

So, what do we need to do in order to exploit this?

Since we have done the first deadbeef check before in one of the previous challenges, we know how that is going to work in the exploit:

image

But what about overwriting the return address?

Well, first off we need to find the offset between our scanned in input, and the eip register (return address), which can be found in Ghidra or GEF

In Ghidra:

image

The [-0x50] is the offset between the input (start of our scanned in input) and the eip register (return address)

In GEF:

This is a bit more complex, but will give 100% accurate results, as sometimes, Ghidra doesnt figure out the offsets, or something, idk i heard it somewhere. Either way its good to know both methods

First off, set a breakpoint for just after the input is entered:

image

Second, run the binary and input something, I inputted 33012002, and use the search-pattern and i f (shortened version of "info frame") commands to get the addresses of the start of our input, and the eip register

image

As seen above, the start of my input is at 0xffffd0dc, and as you can see near the bottom, the eip at 0xffffd12c

If you go into any hex calculator or just use Python, you can figure out the offset by subtracting the value of the start of the input with the eip register, like this:

image

As you can see, we get the same offset from Ghidra, which is 0x50, or 80 in decimal (this will become important soon)

So, we have the first check done by filling up the input buffer with 64 bytes and adding 0xdeadbeef to the end of it, but if we just run that, we dont actually win, we just don’t get the “you suck!” message

image

We still need to overwrite the return address with the address of the win function. Now, we know that the offset is 0x50 which is 80 bytes (0x50 in decimal is 80), and we have already filled up 68 bytes of the stack. The reason i say 68 bytes instead of 64 is because we filled up the initial buffer with 64 A’s, then added the final 0xdeadbeef onto the end which allowed us to complete the inital check, and since deadbeef is 4 bytes (de, ad, be, ef) (each 2 letters is 1 byte), we get a total of 68 bytes filled up because 64 + 4 = 68.

So, we have already filled up 68 bytes of the stack, and we need to pad the rest in order to get to the eip register so we can overwrite it, so lets do 80 - 68 = 12, so we need to pad 12 more bytes in order to reach the eip register (return address)

image

(For less confusion, the initial A’s are for the first 64 byte overflow, and the B’s are for the padding to the ```eip`` register)

After we have done that, we are now at the eip register (return address) and we should now be able to add the address of the win function to the end of our payload, and that should overwrite the return address with the address of the ````win``` function

The address of the win function is 0x080484ab and can be found in Ghidra, seen below highlighted in green:

image

Below is the complete exploit, which you can copy if you want:

image

from pwn import *

target = process("./rop1")

payload = b""
payload += b"A"*64
payload += p32(0xdeadbeef)
payload += b"B"*12
payload += p32(0x080484ab)

f = open("payload", "wb") # make "payload" file
f.write(payload) # Write payload to "payload" file
f.close() # finito

target.send(payload)
target.interactive()

And as you can see, the exploit worked flawlessly:

image

Count ya bytes, people!