Article Index

Category: Pwn Points: 300 Solves: 53 Description:

h3y... can you leave me a note?

For this challenge we were given a binary and the libc used on the server. Up front I should say, I didn't solve this challenge the way it was intended. Other writeups out there will go over the proper way. This writeup goes over my way.

The Binary

Let's take a peek at the binary. First, verify the type:

 

$ file memo
memo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=55dd2460edb641c16f624ddda467a6d4ddc8b14d, stripped

 

Strings had an interesting line in it: "this is hidden memo pad". We'll come back to that later. Giving it a run we see that it is menu driven:

 

$ ./memo 
What's user name: test
Do you wanna set password? (y/n) y
Password must be set to 32 digits or less.
Password: test
Done! have a good day test

1. Leave message on memo
2. Edit message last memo
3. View memo
4. Delete memo
5. Change password
6. Quit.
>>

 

Let's look at the options in turn.


Leave Message

>> 1
Index: 0
Length: 12
Message: test

 

We're given the ability to select an index to leave a message on, a length, and a message. Often times if you can select an index, it might open up opportunities for arbitrary writes. Trying different numbers here is usually a good strategy. I like to try 0 (which is usually a good working case), some large number (can i write to a place far away?), some negative number (can i write to a place likely i shouldn't?).

 

Large index:

 

>> 1
Index: 65536
Segmentation fault (core dumped)

 

Ok, so clearly it's not doing bounds checks there. What about negative small?

 

>> 1
Index: -1
Index too large

 

So something is strange already. It's treating -1 as an unsigned int (thus too large), but it's OK attempting to dereference my 65535? Let's look at size.

 

>> 1
Index: 0
Length: 65536
message too long, you can leave on memo though
test

 

Looks like there's some difference when using a large length. Let's dig into the code a bit here. Note, I'm using IDA in this case as radare2 has issues parsing the jump table correctly.

 

 

We can see why the large index was causing a segfault. In the very first block, this binary prompts for an integer, then uses that in an offset to a static global location. It's interesting to note that the global Index is set prior to any sanity checking. Further, the compare in the second block is a "ja" or "jump above", which is an unsigned comparison operator.

 

We also noted that with a large value for the length we received a message about it being too long. We can see that on the left. Let's take a closer look:

 

 

First, the length is compared against 32 (0x20), and the branch is chosen based on that.

 

Length <= 32

  1. malloc(size)
  2. print message
  3. read size bytes into just malloc'd space
  4. Load offset into global array of memo pointers
  5. Save pointer to malloc'd space into pointer array
  6. Compute another offset into the memo pointers array and store the stack variable address in it (!?)
  7. Store the length of the memo into a global memo length array
  8. print

Note for a second step 6. Consider why this would be unusual. The answer is that you're storing a stack address to a global variable. Remember, the stack is meant to be temporary for that procedure and only contain local variables. There's really no good reason to store a pointer to the local variable in a global variable. Once you exit the function the reference will be moot and the context will be lost. From an exploiter perspective, this looks like a target to leak the stack address.

 

Length > 32

  1.  print message
  2. malloc(32) (?!)
  3. read in size byte into previously malloc'd space (?!)
  4. print

Couple strange things here. First off, you just told the program that you would give it more than 32 bytes of data, and it only allocated 32 bytes. So there's an obvious heap overflow here. Further, it doesn't bother storing the address of your buffer anywhere globally or returning it. This is actually a memory leak in the conventional sense that you have no pointers in your code to this area in memory after the function returns.

 

This turned out to be one of the "correct" attack vectors. However, it involves malloc trick that I'm not as versed in. Read other writeups for that version. :-)

 

What can we say about this function now? Well, it give us the primitive to set a global variable iIndex. It loads an address before bounds checking. On small sizes of length, it provides us the ability to leak the stack address by storing it at a global address. Finally, it provides us a primitive for an arbitrarily large sized malloc (generally a bad idea), and a heap overwrite.

 

That's probably enough for one function. Let's move on.


Edit Last Memo

Option 2. Let's give it a run.

  

 

A few things to notice. First, it doesn't prompt you what item to edit. Second, notice those strange unprintable characters at the end. These are often times indicators that the program is over-reading memory. This is because the way strings are stored in C code is a null terminated character array. Note the null terminated portion. If the array is not null-terminated, then when you print a string (such as printf("%s",mystring)), your program will happily keep printing along until it find a null. The problems are notoriously difficult to find with fuzzing since the program often times will not crash. Let's see what's going on under the hood.

 

 

The first block it performs a similar action to leave message. It loads up an offset to the memo array using the index global that we just set in leave message. It then loads up what is at that index and makes sure that it isn't zero. If it is zero, it assumes that we haven't left a memo there yet and gives an error. Let's break down the left block:

 

  1. print message
  2. using the global index value, load up the offset into the memo lengths array (figure out how much space was allocated... ish...)
  3. grab the pointer to your global array based on the last memo index
  4. Read up to the previously designated size of bytes into previously malloc'd space
  5. print message
  6. print it back as a string (??)

 

Take a close look at that print statement. Turns out it's missing a step. It's not dereferencing the pointer before printing. What it's actually doing is printing the array of pointers as a string. Pointers don't generally print well as strings, thus it looks weird. We can verify this by breaking at that print statement and seeing what's there.

 

[0x00400e39]> drr
   rax 0x0000000000000000  rax
   rbx 0x0000000000000000  rax
   rcx 0x00007f8d8f1fc6e0  (/lib/x86_64-linux-gnu/libc-2.23.so) rcx library R X 'cmp rax, -0xfff' 'libc-2.23.so'
   rdx 0x00007f8d8f4cb780  (unk0) rdx R W 0x0 --> rax
    r8 0x00007f8d8f6c3700  (unk1) r8 R W 0x7f8d8f6c3700
    r9 0x000000000000000e  (.comment) r9
   r10 0x0000000000000000  rax
   r11 0x0000000000000246  r11
   r12 0x00000000004008a0  (.text) (/home/user/bkp/pwn/memo/memo) r12 entry0 program R X 'xor ebp, ebp' 'memo'
   r13 0x00007ffe5ff19e40  r13 stack R W 0x1 --> (.comment)
   r14 0x0000000000000000  rax
   r15 0x0000000000000000  rax
   rsi 0x0000000000602a70  (.bss) (/home/user/bkp/pwn/memo/memo) rsi program ascii R W 0x209b010 --> heap R W 0xa74736574 (test
)
   rdi 0x00000000004013d8  (.rodata) (/home/user/bkp/pwn/memo/memo) rdi fcn.0040127d program R X 'and eax, 0xa0a73' 'memo' (%s

)
   rsp 0x00007ffe5ff19d10  rsp stack R W 0x400a32 (2
@) --> (.text) (/home/user/bkp/pwn/memo/memo) fcn.004009ea program R X 'add al, ch' 'memo'
   rbp 0x00007ffe5ff19d20  rbp stack R W 0x7ffe5ff19d60 --> stack R W 0x401200 --> (.text) (/home/user/bkp/pwn/memo/memo) fcn.00401200 fcn.00401200 program R X 'push r15' 'memo'
   rip 0x0000000000400e39  (.text) (/home/user/bkp/pwn/memo/memo) rip fcn.00400da8 program R X 'call 0x400830' 'memo'
    cs 0x0000000000000033  (.comment) ascii
rflags                 1I  rflags
  orax 0xffffffffffffffff  orax
    ss 0x000000000000002b  (.comment) ascii
fs_base 0x00007f8d8f6c3700  (unk1) r8 R W 0x7f8d8f6c3700
gs_base 0x0000000000000000  rax
    ds 0x0000000000000000  rax
    es 0x0000000000000000  rax
    fs 0x0000000000000000  rax
    gs 0x0000000000000000  rax

 

Note the rsi variable. It's pointing to a pointer that points to text. Thus, what we're actually seeing is the printf function frantically trying to make sense of printing a "string" that definitely isn't a string. What this means from an attacker perspective is, we have a trivial memory leak of heap space. Since the heap itself isn't randomized, we now have completely derandomized the heap.

 

On to view memo.


View Memo

First step is always to play around with it:

 

1. Leave message on memo
2. Edit message last memo
3. View memo
4. Delete memo
5. Change password
6. Quit.
>> 1
Index: 0
Length: 32
Message: test

1. Leave message on memo
2. Edit message last memo
3. View memo
4. Delete memo
5. Change password
6. Quit.
>> 3
Index: 0
View Message: test

 

Ok, that seems good. As before, let's try a large number and a negative number.

 

>> 3
Index: 65536
Segmentation fault (core dumped)

 

Oops. How about negative?

 

>> 3
Index: -1
Segmentation fault (core dumped)

 

So that's interesting. Before when we entered a negative number it complained. This time it segfaults. This means it might not be validating negative numbers correctly. Let's peek at the code now.

 

 

 

By now, you all can probably see what's going on at the beginning. It's prompting for an integer and using that as an index into memo pointers array. There's a little trickery when you're reading this assembly. I missed it the first few times reading and wondering why the negative number was causing a segfault. The key is in the first block. Can you find it?

 

The key is actually what isn't in this function. After prompting for an integer, this time the code does not perform the cdqe. This one instruction changes the exploitability for this function. cdqe performs a conversion from double to quad word sign extended. Thus, the integer was read in to a double word size. When converting it to a quad word (64-bits or a "long long"), there's a question of if to assume the source integer is signed or not. Thus, skipping cdqe causes a negative integer to just be treated as a rather large positive 32-bit integer. This is why the negative number crashes the same way as the large positive number. It actually is a large positive integer in this case.

 

One implication of this is that we can really only use view indexes forward, not backwards (bummer). The second step locks us in to only being able to view integers 0-4. Finally, the last block does as you would expect and dereferences your memo pointer and prints it.

 

Now is as good a time as any to point out yet another strangeness of this binary. Take a peek at the global memory layout...

 

 

Can you see what's wrong? If you've been following along you should know that this binary is basically telling the user that they have 5 slots available to use to store memos. Pointers to those slots go into the aMemoPointers global. However, aMemoPointers doesn't appears to have enough space. It looks to be able to handle a single pointer, as if the source code was written something like "char *aMemoPointers[1];". The memory runs into the externs section so it doesn't really cause problems. But... strange.


Delete Memo

 

Checking out delete menu. Let's try the following cases:

 

  1. Index that we have created a memo on (legit case)
  2. Index that we have not created a memo on
  3. Negative Index
  4. Large positive index
  5. Double delete

 

Index we've created a memo on

>> 4
Index: 0
Deleted!

 

Index we have not created a memo on

>> 4
Index: 2
there isn't message

 

Negative Index

>> 4
Index: -1
there isn't message

 

Large Positive Index

>> 4
Index: 65536
Segmentation fault (core dumped)

 

 

Double delete

>> 4
Index: 0
Deleted!

1. Leave message on memo
2. Edit message last memo
3. View memo
4. Delete memo
5. Change password
6. Quit.
>> 4
Index: 0
there isn't message=

 

We're getting a segfault when we delete a large index, but no error aside from "no message" when we delete negative. With those in mind, let's look at the code.

 

 

The beginning is the same as others. Note that it does cdqe, so we can put in negative numbers and have it subtract instead of add large numbers. It dereferences whatever is at that index and checks if it's already zero. Note, there's no bounds checking here, so this is why you get a segfault on a large number (it runs off the page).

 

If the entry is non-zero (not really checking for a valid pointer, it will free the pointer that's in that array and zero out the array slot. Not much else going on here.


Change Password

 Before we change password, let's take a peek at what happens when we set the password.

 

 

Writing down the steps as follows:

 

  1. print message
  2. read 32 bytes into global username buffer
  3. ask if want to set pass
    1. Get 2 chars
    2. if not y, print ok and exit function
  4. print message
  5. read in 32 bytes into global password buffer
  6. print message

 

Pretty strait forward, just filling in global variables. As shown before, the username and password fields are right next to each other in the global address space and appear to each legitimately have 32 bytes of space.

 

Here's an example run of password change:

 

What's user name: user
Do you wanna set password? (y/n) y
Password must be set to 32 digits or less.
Password: test
Done! have a good day user

1. Leave message on memo
2. Edit message last memo
3. View memo
4. Delete memo
5. Change password
6. Quit.
>> 5

Password: test
New user name: user
New password: pass
Done! changed user by user

1. Leave message on memo
2. Edit message last memo
3. View memo
4. Delete memo
5. Change password
6. Quit.
>> 5

Password: pass
wrong password!

 

Interestingly, it does not accept the password that i just gave it as my new password. Sounds like something is a bit off here.

 

 

Looking at the first block, it reads in 32 bytes of input into a buffer, then string compares it against what's in the global variable. This seems normal enough. The second block checks the length of the existing username and password fields, then adds one and zeros that much space out. Strange and excessive calls in my opinion, but ok.

 

 

Finally, it goes into an iterative loop to get the new password... This is also strange since all the rest of the times we've gotten input as been simply read calls. In C, it would look something like this:

 

for (i = 0; i <= 32; i++) {

    cChar = getchar();

    if ( cChar == 0xff )

        break;

    if (cChar == 0xa)

        break;

    global_password[i] = cChar;

}

printf("Done! changed user by %s\n",global_username);

 

So basically, not only is it going to stop reading at the newline, it will also stop reading at 0xff. Perhaps this affected the solution you were supposed to use, but I didn't notice it affect my solution.

 

Note, however, that this read function does not null terminate. This is why the new password didn't work right. Given the way it is being read, this is overcome by simply using a null terminator in your new password. I.e.: instead of setting the password to "pass", set it to "pass\x00".


Arbitrary Write

Putting it together, we can realize that an arbitrary memory write exists. The edit function allows you to edit the last written memo. It utilizes the global index value to track what the last written index was, and allows you to use negative indexes. What this means is, we can edit a message that never existed, and we can control the location that is being written to, thus an arbitrary write.

 

Step one is we want to be able to set the global index variable without causing the program to exit. Recall that if we try a negative index, it caused the program to spit out an error. However, look closely at that function:

 

 

We get the "Index too large" error, but there's a check before that. The very first block has a check to see if the index is in use before checking if the index is too large. This means if we can select a negative index and ensure that it is non-zero, we can set our index to it. Using a debugger, we can take a peek at where this is.

 

[0x7f81fb542cc0]> db 0x400C8E
[0x7f81fb542cc0]> dc
Selecting and continuing: 121375
What's user name: AAAAAAAAAAAAAAAAA
Do you wanna set password? (y/n) y
Password must be set to 32 digits or less.
Password: BBBBBBBBBBBBBBB
Done! have a good day AAAAAAAAAAAAAAAAA

1. Leave message on memo
2. Edit message last memo
3. View memo
4. Delete memo
5. Change password
6. Quit.
>> 1
Index: -8
hit breakpoint at: 400c8e
[0x00400c8e]> drr
   rax 0x0000000000602a30  (.bss) (/home/user/bkp/pwn/memo/memo) rax program ascii R W 0xa41 (A
)
   rbx 0x0000000000000000  r8
   rcx 0x00000000ffffffda  rcx
   rdx 0x0000000000000001  (.comment) rdx
    r8 0x0000000000000000  r8
    r9 0x1999999999999999  r9
   r10 0x0000000000000000  r8
   r11 0x00007f81fb2efa00  (/lib/x86_64-linux-gnu/libc-2.23.so) r11 library R X 'add al, byte [rax]' 'libc-2.23.so' (𠀂𠀂𠀂𠀂𠀂𠀂𠀂𠀂𠀂𠀂𠀂𠀂𠀂�����������������翰< 翿翆��p喪�k�喪�翿翆��p喪�喪�喪�翿翆翆0喪�翿翆翆0喪 翿< 翿翆�喪�唁)
   r12 0x00000000004008a0  (.text) (/home/user/bkp/pwn/memo/memo) r12 entry0 program R X 'xor ebp, ebp' 'memo'
   r13 0x00007ffe3a0ffe40  r13 stack R W 0x1 --> (.comment) rdx
   r14 0x0000000000000000  r8
   r15 0x0000000000000000  r8
   rsi 0x0000000000000008  (.comment) rsi
   rdi 0x00007ffe3a0ffcf2  rdi stack R W 0xc4a00000000000a
   rsp 0x00007ffe3a0ffd10  rsp stack R W 0x0 --> r8
   rbp 0x00007ffe3a0ffd20  rbp stack R W 0x7ffe3a0ffd60 --> stack R W 0x401200 --> (.text) (/home/user/bkp/pwn/memo/memo) fcn.00401200 fcn.00401200 program R X 'push r15' 'memo'
   rip 0x0000000000400c8e  (.text) (/home/user/bkp/pwn/memo/memo) rip fcn.00400c52 program R X 'mov rax, qword [rax]' 'memo'
    cs 0x0000000000000033  (.comment) ascii
rflags               C1PI  rflags
  orax 0xffffffffffffffff  orax
    ss 0x000000000000002b  (.comment) ascii
fs_base 0x00007f81fb736700  (unk1) R W 0x7f81fb736700
gs_base 0x0000000000000000  r8
    ds 0x0000000000000000  r8
    es 0x0000000000000000  r8
    fs 0x0000000000000000  r8
    gs 0x0000000000000000  r8

 

Note rax. We can see that when i use the index -8 (chosen mostly arbitrarily), the leave message function looks for the index 16 bytes into the username. Since I didn't want to bother moving offsets around, i just stuck with this index for the rest. However, if you wanted to move it you certainly could. In fact, -10 might be a better choice as it puts the index right at the beginning of your username global.

 

Now that we can set the global index variable, we can use that to control a write using the edit function. However, one things that happen in the edit function is it checks the memo length array for how much to read.

 

 

We can calculate where this value will end up fairly easily.

 

hex(0x602A60 - (8*4)) == 0x602a40

 

So we know that 0x602a40 is actually the start of our password buffer. We now know where we must store a size value. These two pieces combined give us our arbitrary memory write. In python, I wrote it up as follows:

 

def mem_write(addr,value,size):
    global username
    global password

    # Bad chars
    if "\x0a" in p64(addr) or "\x0a" in p32(size) or "\x0a" in value:
        return False

    new_username = "A"*0x10 + p64(addr) + "\n" # Write to addr
    new_password = p32(size) + "\x00" # Number of bytes to read

    # Setup our pointers
    if not change_password(password,new_username,new_password):
        return False

    # Remember our pass
    password = new_password

    if not edit_message(value + "\n"): # Write to memory address
        return False

    return True

 

Note that I have written helper functions (such as change_password) that just wrap those binary calls. I'm not going to go into those as they are fairly strait forward.

 

To use this, we need to be able to remove some randomization of the binary first. That means, in this case, a memory leak.


Arbitrary Read

Having an arbitrary read allows you to not have to guess on your exploits. With a minimal arbitrary read you should be able to fully remove randomization from almost any binary. I spent a while looking for memory leaks and I found a few.

 

Heap Address Leak

This was already mentioned in the edit memo section. It's blatant, and it allows you to know exactly where the heap is.

 

Arbitrary Read

So view memo shows us the heap address by default. However, since we have an arbitrary write and it's reading the address to print from the global variable section, we have the ability to mess with that and cause it to print whatever we want. The view memo code is as follows:

 

 

We can see that it simply dereferences the index that we give it, and prints it as a string. The dereference happens at a static global offset that we control, and we have an arbitrary write. This makes an easy arbitrary read now:

 

  1. Use arbitrary write to write the address we want to leak at the global address of the pointers array (0x602A70)
  2. Use the view memo option with an index of 0 when prompted
  3. Profit

 

There's one little problem still though. While we have an arbitrary read and should theoretically be able to determine the stack location using that, pwntools wasn't working correctly at the time... Luckily for me, there's a strange behavior that I mentioned in the leave message section where it actually writes a stack pointer address to a global variable. This behavior in general is odd. Stack variables are local. If the goal was to keep track of a local pointer, well you already have it. Placing that pointer into global space just gives attackers the ability to de-randomize the stack address.

 

 

Notice in the far left block at the bottom, we load the address of a stack variable. We then save it into the memo pointers array. At this point, all we have to do to leak the stack address is:

 

  1. Write a memo of size 32 (or less)
  2. Use arbitrary read to read the pointer value off of the global pointers variable

 

We now have a pretty good idea of where everything is on a fully randomized binary. Time to exploit.


Exploit

Now that we have arbitrary read and write, there's one major thing left. We would like code execution. Taking into account the following things:

 

  1. We have a stack address leak
  2. Relative offsets on the stack will be deterministic
  3. Arbitrary Read
  4. Arbitrary Write

 

The game plan for execution will be to overwrite a return pointer. This can be accomplished in the following way:

 

  1. Leak stack address (as mentioned in previous page)
  2. Use debugger to determine correct address of stored return pointer
  3. Calculate relative offset from leaked address to stored return address

 

It turns out that, in my example, this was the stack address + 88. If you used different values you can directly calculate your own offset. After overwriting that, simply exiting from the program will trigger our code execution.

 

So finally, we have an arbitrary read, write, and ability to jump execution to wherever we want. The choice of what to do at this point is pretty open. We were given the libc that they are using. This is always handy and means that if we can leak a resolved address, we will immediately know the address of any function in libc.

 

Remember that this binary is FULL RELRO. What that means is that lazy linking of the functions is disabled, and all the addresses are linked up front. Further, the Global Offset Table (which stores these pointers) is made read-only. This is why we aren't just overwriting a GOT entry. However, we can absolutely still read the GOT entry to leak the address of libc. From there, we use the known libc binary to calculate the offset to what function we want. We have now fully derandomized libc as well.

 

I chose to use "system()" as my function call of choice to execute. However, given the prctl stuff up front, I was not able to call /bin/sh directly. Not a huge deal, because I can still call "ls", "cat" and others. To do this, I need to use a pointer to the command I want to run. Given we have a large globally read/write area available, and an arbitrary write, this is strait forward.

 

My end exploit worked something like this:

 

  1. leak the stack address by leaving a generic message
  2. reset the global index using -8 to allow arbitrary read/write
  3. calculate address of the return pointer on the stack using the leaked address
  4. leak the address of printf using the GOT (it could have been any function, i just chose this one)
  5. calculate offset of system() given what we now know about printf and their compiled binary
  6. write the command i want to run to a globally (static) read write address (i chose 0x602100)
  7. create my ROP chain to load this address into rdi then call the system() function I calculated
  8. use arbitrary write to write this rop chain over the return pointer on the stack
  9. "exit" the program to execute my code and get the results

 

Flag: bkp{you are a talented and ambitious hacker}

 

The full script can be found here: https://github.com/Owlz/CTF/blob/master/2017/boston_key_party/win.py