ISITDTU CTF: dead_note_lv1

I had some spare cycles so I decided to play around with a CTF this weekend. As usually I start by running file:

dead_note_lv1.dms: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a8b6c5096dc1c4810e01267e43ac90a5a61c1fb6, stripped

The checksec script gives us:

Capture d’écran 2018-07-28 à 8.43.08 PM.png

With this I bring up the relevant vagrant image and opened the binary in binja. The first function called in main, sub_c6f, is used to call setvbuf on stdin, stdout, and stderr as well set a signal handler and timeout for alarm. In case you didn’t know, you can use setvbuf to tell a file stream not to buffer data but rather send any data that it receives immediately.

The next function, sub_d5a,  which is also the first function in the main processing loop simply prints the menu.

Capture d_écran 2018-07-28 à 8.26.54 PM

Take note of the selections because it will come up again later. In sub_c06 we see a call to sub_b90 which appears to take a stack buffer and integer. This buffer is later passed to atoi which should make you start thinking of a read function. Drilling down into sub_b90 we do in fact see a call to read using stdin, the stack buffer and 0x10 as a size. A quick check of the stack layout in the previous function shows that we have plenty of room so no overflow here. After a successful read we have the following two basic blocks:

Capture d’écran 2018-07-28 à 8.33.18 PM.png

I named the local variables to make it easier to read. You can see that if the final byte read is a new line then replace it with a null byte. Otherwise if just returns the data as it received it.

Following the call to sub_c06 which I labeled read_int we get into the switch statement. It is straightforward to see which options are handled by which functions so I just renamed them.

sub_dc6 — add_note

sub_f13 — delete_note

In add note there are two calls to read_int each of which is preceded by a prompt. First for “Index” followed by “Number of Note”. The response to “Number of Note” is compared against the value stored at 0x202098 which is 0x3e8. The block beginning at 0xe3e prompts you for “Content” and while it will read 8 bytes, the call to strlen at 0xe7d and the following comparison limits you to only 3.

Beginning at 0xed1 you will see what looks like a while loop. The value at rbp-0x3c, which was initialized to 0 at 0xe96,  is compared to the number of notes value read earlier.

Here is the main block of the while loop:

Capture d_écran 2018-07-28 à 8.49.41 PM

First the index value that was read along with the current counter are added together and stored in ebx. Then, the stack location where the content data was stored to is passed to strdup. This function simply returns a duplicate copy of the string in a heap buffer.

At 0xeba the previously calculated index value is multiplied by 8. This is done when you are going to access an array of 8 byte pointers. At 0xec9 the duplicated string is stored in the array beginning at the offset we specified. So what we can see happening is the content we specified is being copied “Number of Note” times beginning at “Index”. Pretty straight forward. What I noticed though is that index is not bounds checked. Looking in binja there isn’t anything following the note array but there is definitely some interesting stuff before it. I wanted to do a quick check though on the memory mapped regions just to confirm the checksec results..

Capture d_écran 2018-07-28 à 9.00.19 PM

Oh yeah, this thing is executable everywhere. Scrolling up from the note array the first thing I see is the GOT. It would be great to overwrite those pointers with heap pointers containing executable bytes that I control. The downside is that I can only send 3 bytes at a time. I decide to overwrite strlen. With 3 bytes I can zero out rax and ret giving me 8 bytes to play with. By subtracting the address of strlen (0x202028) from the address of the note array (0x2020e0) then dividing by 8 I find that the index I need is -23. Let’s try this in GDB.

Here you can see the interaction with the application:

Capture d_écran 2018-07-28 à 9.08.40 PM

As well as the resulting crash:

Capture d_écran 2018-07-28 à 9.08.51 PM

What this means is that I can now execute 3 bytes of shell code. Because xor is actually a 3 byte instruction and I still need to return from the call I found that ebx is always 0. Here is the assembly that I used to defeat the strlen issue:

Capture d_écran 2018-07-28 à 9.12.29 PM

Now that I can execute 8 bytes I need to figure out how to leverage that. After some experimenting I decide that I don’t want to just overwrite a GOT entry to get execution. I want to execute more than that. Since my 8 bytes are passed to strdup soon after they are received I decide that this would be a good jumping off point.

When strdup is called my buffer is pointed to by rdi. So my first instruction will be a call to rdi. I then decide to just return NULL. The index I used was -12.

Capture d_écran 2018-07-28 à 9.18.52 PM

Now, every time I send data it will be executed immediately with a maximum of 8 bytes. So now what do I do? This part actually took me some time. What I knew was I had a pointer to the buffer that I was executing and I had 8 bytes to work with. Eventually I decided to use sys_read to overwrite where I was currently executing. Getting this done in 8 bytes was a fun exercise. Here is my final result:

Capture d_écran 2018-07-28 à 9.22.40 PM

The rdx register was NULL which I needed for rax, the sys call number and rdi, the file descriptor for stdin. Fortunately rsi already pointed to where I was executing. I just needed to put a size value in rdx.

Now I had plenty of room. I found some execve shellcode and added that to my script.

Capture d_écran 2018-07-28 à 9.30.16 PM

Files can be found here.

 

4 thoughts on “ISITDTU CTF: dead_note_lv1

  1. If I followed correctly, on the part where you inject the syscall, you are actually setting rax to be 0.
    You said that the syscall you are using is sys_read yet when I look online the syscall with number 0 is actually sys_restart_syscall which it was hard to find explanations on.

    This syscall, if I understand correctly, is for adjusting time arguments on syscalls that got continued after being stopped. But in this case there is no syscall that was stopped.

    I understood from this that if you call sys_restart_syscall it restarts the *previous* syscall that ran, whether it was halted mid-way or finished, which in our case is sys_read.

    Is all of this correct or did I understand anything wrong?
    Very cool read, thank you!

    Like

Leave a comment