write4

Building write-what-where abilities to process new memory.

5KB
archive
Open

This is a challenging binary but teaches a lot about how ROP can produce some great results. We'll discuss the inspiration for why we decided to choose our attack vector, discuss why it's possible, and then build a supporting ROP chain. This challenge taught me to do ROP (shoutout LT King); I think it's a valuable challenge to learn from.

Attack Vector Inspiration

Running the binary proves to be pretty useless because none of the output is particularly helpful. Instead, we choose to do some static analysis and some gadget hunting to find what we need to beat this challenge.

When we open the binary in gdb / radare2, we notice that pwnme function is actually inside libwrite4.so, so we go there first. Checking the contents of pwnme in radare2:

gef➤  disas pwnme 
Dump of assembler code for function pwnme:
   0x00000000000008aa <+0>:	push   rbp
   0x00000000000008ab <+1>:	mov    rbp,rsp
   0x00000000000008ae <+4>:	sub    rsp,0x20
   0x00000000000008b2 <+8>:	mov    rax,QWORD PTR [rip+0x200727]        # 0x200fe0
   0x00000000000008b9 <+15>:	mov    rax,QWORD PTR [rax]
   0x00000000000008bc <+18>:	mov    ecx,0x0
   0x00000000000008c1 <+23>:	mov    edx,0x2
   0x00000000000008c6 <+28>:	mov    esi,0x0
   0x00000000000008cb <+33>:	mov    rdi,rax
   0x00000000000008ce <+36>:	call   0x790 <setvbuf@plt>
   0x00000000000008d3 <+41>:	lea    rdi,[rip+0x106]        # 0x9e0
   0x00000000000008da <+48>:	call   0x730 <puts@plt>
   0x00000000000008df <+53>:	lea    rdi,[rip+0x111]        # 0x9f7
   0x00000000000008e6 <+60>:	call   0x730 <puts@plt>
   0x00000000000008eb <+65>:	lea    rax,[rbp-0x20]
   0x00000000000008ef <+69>:	mov    edx,0x20
   0x00000000000008f4 <+74>:	mov    esi,0x0
   0x00000000000008f9 <+79>:	mov    rdi,rax
   0x00000000000008fc <+82>:	call   0x760 <memset@plt>
   0x0000000000000901 <+87>:	lea    rdi,[rip+0xf8]        # 0xa00
   0x0000000000000908 <+94>:	call   0x730 <puts@plt>
   0x000000000000090d <+99>:	lea    rdi,[rip+0x115]        # 0xa29
   0x0000000000000914 <+106>:	mov    eax,0x0
   0x0000000000000919 <+111>:	call   0x750 <printf@plt>
   0x000000000000091e <+116>:	lea    rax,[rbp-0x20]
   0x0000000000000922 <+120>:	mov    edx,0x200
   0x0000000000000927 <+125>:	mov    rsi,rax
   0x000000000000092a <+128>:	mov    edi,0x0
   0x000000000000092f <+133>:	call   0x770 <read@plt>
   0x0000000000000934 <+138>:	lea    rdi,[rip+0xf1]        # 0xa2c
   0x000000000000093b <+145>:	call   0x730 <puts@plt>
   0x0000000000000940 <+150>:	nop
   0x0000000000000941 <+151>:	leave  
   0x0000000000000942 <+152>:	ret    
End of assembler dump.

The reason that I like radare2 for static analysis is that it provides function headers and resolves strings automatically. This makes the disassembly process a lot easier. I personally think that gdb has better stepping usability for dynamic analysis but is less feature-friendly for static analysis.

From this function, we notice that we're allowed a 0x200 byte payload to be read on the stack. It is being read to rbp-0x20 so we can quickly deduce it takes 0x28=40 bytes to reach the return pointer. Then, the rest is up to us.

Searching around the binary, we find the function print_file:

Based on the C code provided, this binary takes an int64_t, resolves the string at that address, then prints the file's contents with that name. This means that we need to find the address of flag.txt in memory and then pass this address into print_file, and we'll have the flag!

Building the ROP Chain

The first step you should take is to find the flag in memory. strings write4 | grep flag tells us it's not there. Bummer. Can we introduce it into the binary somehow?

Our next step should be to check the gadgets to see if there's a way to pass data into memory. We need a way to store the string flag.txt at an address of our choice and then pass that address into print_file. Let's look around ROPgadget for some pop gadgets:

This seems useful enough. This provides us a way to load the first two parameter registers, meaning that we can pass an address into print_file. We know that the end of our payload will look something like:

Now, we need a way to store flag.txt somewhere. We'll check for a mov gadget that moves a string to the *contents of an address. Something like this might be helpful:

This would let us put flag.txt in register_2, and then store it at the value of register_1. We would also need to control register_1 to make this happen.

Let's check ROPgadget for some options:

We find the following gadget which suits our needs:

This gadget lets us write the contents of r15 at the location pointed to by r14. We can use this to write flag.txt to an address of our choice. We'll need to control r14 and r15 to make this happen.

Just like last time, there is more than one solution. I wrote another solution in exploit2.py that uses the following gadget:

I'll talk more about this solution at the end of the writeup.

We'll go back and take note of the following gadget, which lets us control r14 and r15:

In this case, we'll load r14 with the address to write to and r15 with the string to write.

Deciding Where to Write

Now, we need to figure out where we want to write. This is a crucial step because we don't want to overwrite crucial memory that forces our program to crash. Another essential check is ensuring we can write to the address we choose. Not every memory section has write permissions, so we must find somewhere we are allowed to write.

We can check the mappings inside gdb and find a writeable location:

We see that the 0x601000-0x602000 range is the only writeable range, so let's check around in there. We're looking for memory that's hopefully not used.

We see that 0x601030 doesn't seem to be used by anything, so we'll choose there. We could play it safer and choose something further away, but in this case, we'll see it doesn't matter.

Writing the Exploit

Now, let's put this all together. We'll start by defining the binary, library, and the process:

Then, we'll define all our essentials. The functions, variables, addresses, and gadgets:

Then, we'll build the chain.

Finally, we'll send the payload and get the flag:

This works! This gets us the flag.

If we want to make our exploit more robust...

Alternative Solution

This is the solution I mentioned earlier. This solution uses the following gadget:

This means we need to control rsi and edi. We'll use the following gadgets to do this:

Notice that our gadget pops rdi, but uses edi to move into memory. This means that we can only move 4 bytes at a time (since edi is the lower four bytes of rdi). From here, our chain would do the following:

  • Align the stack

  • Move flag into addr (the address we write to)

  • Move .txt into addr+4

  • Move a null byte into addr+8

  • Call print_file with addr as the argument

This is a bit more complicated, but it works just as well. Note that we use b'C' * 0x8 as a junk variable. I chose this for debugging purposes because it differentiates from the padding. We use v_junk to populate r15 every time we use the pop rsi gadget.

Here is that exploit:

Last updated

Was this helpful?