RazviOverflow

Exploit code, not people.

Root page Twitter View on GitHub

Microcorruption - Cusco

Posted on 25 Sep 2018.
microcorruption tutorial assembly ctf cusco walkthrough debug buffer overflow

In this level we’ll discover and apply one of the most common, famous and basic memory corruption techniques. Keep reading to find it out :)

When you enter the level, the manual that pops up states the following:

We have fixed issues with passwords which may be too long.

This is Software Revision 02. We have improved the security of the lock by removing a conditional flag that could accidentally get set by passwords that were too long.

As always, we start inspecting the code from the main function. Just like the previous level, Hanoi, it simply calls login.

If we take a closer look to login’s code, we can see there’s nothing special about it.

4500 <login>
4500:  3150 f0ff      add	#0xfff0, sp
4504:  3f40 7c44      mov	#0x447c "Enter the password to continue.", r15
4508:  b012 a645      call	#0x45a6 <puts>
450c:  3f40 9c44      mov	#0x449c "Remember: passwords are between 8 and 16 characters.", r15
4510:  b012 a645      call	#0x45a6 <puts>
4514:  3e40 3000      mov	#0x30, r14
4518:  0f41           mov	sp, r15
451a:  b012 9645      call	#0x4596 <getsn>
451e:  0f41           mov	sp, r15
4520:  b012 5244      call	#0x4452 <test_password_valid>
4524:  0f93           tst	r15
4526:  0524           jz	#0x4532 <login+0x32>
4528:  b012 4644      call	#0x4446 <unlock_door>
452c:  3f40 d144      mov	#0x44d1 "Access granted.", r15
4530:  023c           jmp	#0x4536 <login+0x36>
4532:  3f40 e144      mov	#0x44e1 "That password is not correct.", r15
4536:  b012 a645      call	#0x45a6 <puts>
453a:  3150 1000      add	#0x10, sp
453e:  3041           ret

There is a call to getsn. We can see, once again, the number of bytes to read is 0x30 (48 decimal), our input will be stored starting at sp (remember in the previous level we had mov #0x2400, r15 instead mov sp, r15), a call to test_password_valid and afterwards some testing on r15. Depending on r15’s value the door will or will not be unlocked. So far it looks exactly like the previous level except for the memory address where our input will be stored. We must now inspect rest_password_valid’s code.

4452 <test_password_valid>
4452:  0412           push	r4
4454:  0441           mov	sp, r4
4456:  2453           incd	r4
4458:  2183           decd	sp
445a:  c443 fcff      mov.b	#0x0, -0x4(r4)
445e:  3e40 fcff      mov	#0xfffc, r14
4462:  0e54           add	r4, r14
4464:  0e12           push	r14
4466:  0f12           push	r15
4468:  3012 7d00      push	#0x7d
446c:  b012 4245      call	#0x4542 <INT>
4470:  5f44 fcff      mov.b	-0x4(r4), r15
4474:  8f11           sxt	r15
4476:  3152           add	#0x8, sp
4478:  3441           pop	r4
447a:  3041           ret

After some operations, a call to INT is made at instruction 0x446c. It takes 0x7d as a parameter, just like the previous level.

So far it seems there’s no way for us to know the correct password. Let’s, then, execute the program and see what happens.

As soon as we execute it, we can see there are some instructions printed out:

Wait a second, how come password must be between 8 and 16 characters when we’ve inspected the code and already stated that they can be up to 48 chars? Well, once again, the moral is to never trust what printed instructions say but the code.

Let’s then stress the program, let’s see what happens if we input more than 16 chars. I will input exactly 48 x A.

The password is obviously not correct.

But we got some precious information. We can see the message insn address unaligned in the debugger console. If we take a look at Live Memory Dump:

We’ll notice the sp register is pointing at some memory whose content is what we just provided as input. If we take a look again at the code, we’ll se the following:

Instructions starting at address 0x4400 have been overwritten. If we look again at Live Memory Dump we can indeed see how that memory address is now fullfilled with our input.

Well, what happened? We have just overwritten pc (Program Counter) also known as ip (Instruction Pointer). That’s happening because the buffer where our input lives is too close to where other instructions live in memory and we can overwrite them. That means we can write more character than the intended buffer size. That’s commonly known as Buffer Overflow. We will know see why and how Buffer Overflow actually happens and how to exploit it.

I will first briefly explain it and then reference some good reads for you to go deeply into this matter.

Buffer Overflow happens, basically, because stack grows towards lower addresses of memory and memory writes are done towards higher addresses of memory. Everytime an element is inserted into the stack, it is inserted towards lower addresses of memory. That’s why we say the stack grows towards lower addresses of memory, towards the heap. You can read more about the stack here.

Everytime a function is called, the Instruction Pointer jumps somewhere else in memory. It must, then, know somehow where to come back. That’s called the Function prologue. In the function prologue, the address of the next instruction before the jump is saved. This is, the address of the instruction where the Instruction Pointer must come back, among other things. That address is usually called RETADDR, return address or @ret. Imagine now that in the function that has just been called there is a buffer, since the stack grows towards lower memory addresses, the buffer will be “below” the return address. Now imagine we have no bounds checking for that bufffer, that is, we can write without limits into it. If the buffer is intented to be 16 bytes long but we have no limits in our write, we can perfectly write 200 bytes overwritter whatever is 200 bytes “above” the starting address of our buffer.

Now, when the funcition finished, the Instruction Pointer must recover it’s value, the return address. If that value has been overwritten, the program might crash but, what happens if that address is actually a valid address? What happens if we overwrite the return address with the address of some instruction of our choice, for example one that unlocks the door? That is exactly what we’re going to do.

Please, if you have never faced buffer overflow nor worked with them, read the following articles. There are tons of articles and entries about this very topic, I will mention some. There are even thousands of research papers you can find using the search engine of you choice, like Google Scholar.

Solution

Now that we know what a buffer overflow is and how to exploit it, we will overwrite Instruction Point to take advantage of this vulnerability.

The return address we want to overwrite is the return address of login. In order to do that, we will set a breakpoint right at instruction address 0x453e in order to see where the Stack Pointer sp is at that very moment.

Why is login the function whose retaddr must be overwritten? Simple. Remember memory writes are made towards higher addresses of memory and the stack grows towards lower addresses. WE cannot overwrite a function that has been declared after the declaration of our buffer. main is the function that calls login and at some point the Instruction Pointer must return to main. Inside login our buffer is declared and, after that declaration, the rest of the calls. Since writing into the buffer will grow toward higher memory addresses, the only retaddr we can overwritte is the login one.

First of all we must know how many padding bytes we need before actually writting into memory the address we want to jump to. As we can see, at the moment of ret, sp points to 0x43fe.

Since our buffer starts at address 0x43ee, it is as simply as counting the bytes. 0x43fe - 0x43ee = 0x10 (16 decimal). We must insert 16 padding bytes.

Now we must figure out what address we want to jump to. If we take a close look, in main there is the function that unlocks the door and solves the level, unlock_door. Its address is 0x4528 and that’s where we want to jump to.

Do not forget about endianness. Since MSP430 is little-endian and has a 16bits architecture, we must reverse the bytes two by two when writing into memory. This means 0x4528 will become 0x2845 in order for the little-endian machine to actually read it as we want.

So, the solving input will be (hex encoded): 414141414141414141414141414141412845

Recap

We’ve seen, once again, how important is to not trust what printed instructions say and only believe in code. We’ve seen what buffer overflow is and how we can use this vulnerability to overwrite Instruction Pointer and hijack the program’s execution flow.

More levels