Fun with Assembly 12: A bigger challenge

Posted by Hywel Carver on August 26, 2015

This is the twelfth in a series. You might want to read the previous post before reading this.

This post is based on the Santa Cruz level on microcorruption.com. Like last time, we’re trying to find an input to open a lock without knowing the correct password, using knowledge of assembly language.

First steps

Wow, this is the most complicated level yet. It also takes both a username and a password. The main function adds 50 bytes to the stack, then calls login. login is a huge function that contains all of the logic itself, until it eventually calls unlock_door.

So let’s break down the whole login function and work out what it’s doing.

Topping and Tailing

The first and last instructions in login relate to each other, so it helps to look at them together.

4550:  0b12           push  r11
4552:  0412           push  r4
...
4662:  3441           pop   r4
4664:  3b41           pop   r11
4666:  3041           ret

This pushes the current values of registers 11 and 4 onto the stack. At the end of the function it pops them off the stack again. That’s because this function uses r4 and r11, so will change their values. But other functions up the stack might be already using r4 and r11, and we have to be careful not to change their values between the function being called and returning. So right now the bottom of the stack looks like this:

From top of stack: 3  2 | 1  0  | -1 -2
                   <r4> | <r11> | <stored return address>

Then:

4554:  0441           mov   sp, r4
4556:  2452           add   #0x4, r4
4558:  3150 d8ff      add   #0xffd8, sp
455c:  c443 faff      mov.b #0x0, -0x6(r4)
4560:  f442 e7ff      mov.b #0x8, -0x19(r4)
4564:  f440 1000 e8ff mov.b #0x10, -0x18(r4)
...
465e:  3150 2800      add   #0x28, sp

Register 4 is set to 4 bytes above the bottom of the stack, which is currently equal to the top of the stack. The stack pointer is decremented by 0x28 = 40 bytes. We then set some specific byte values in the stack. Right at the end of the login function, we’ll then remove the extra space that was added to the stack. But for the middle part of the function we have (from the top of the stack down):

4 bytes of stored return address (r4 points to first byte)
--------------
2 bytes of r11
2 bytes of r4
1 blank byte
1 byte set to 0x0
17 blank bytes
1 byte set to 0x10
1 byte set to 0x8
19 blank bytes (sp points to first byte)

Hopefully we’ll work out why soon.

Getting going

We’re onto the middle bit of the function now.

456a:  3f40 8444      mov   #0x4484 "Authentication now requires a username and password.", r15
456e:  b012 2847      call  #0x4728 <puts>
4572:  3f40 b944      mov   #0x44b9 "Remember: both are between 8 and 16 characters.", r15
4576:  b012 2847      call  #0x4728 <puts>
457a:  3f40 e944      mov   #0x44e9 "Please enter your username:", r15
457e:  b012 2847      call  #0x4728 <puts>

We print out some info for the user. Apparently we accept usernames and passwords between 8 and 16 characters long.

4582:  3e40 6300      mov   #0x63, r14
4586:  3f40 0424      mov   #0x2404, r15
458a:  b012 1847      call  #0x4718 <getsn>
458e:  3f40 0424      mov   #0x2404, r15
4592:  b012 2847      call  #0x4728 <puts>

Nope! We actually accept 0x63 = 100 characters of input for the username (at 0x2404. Then we write it out to the console.

4596:  3e40 0424      mov   #0x2404, r14
459a:  0f44           mov   r4, r15
459c:  3f50 d6ff      add   #0xffd6, r15
45a0:  b012 5447      call  #0x4754 <strcpy>

Then we copy it onto the stack, 0x2a = 42 bytes behind r4. So, our stack now looks like this (with * meaning bytes that we can overwrite with our username input).

*4 bytes of stored return address (r4 points to first byte)
--------------
*2 bytes of the calling function's r11
*2 bytes of the calling function's r4
*1 blank byte
*1 byte set to 0x0
*17 blank bytes
*1 byte set to 0x10
*1 byte set to 0x8
*17 bytes where the username is intended to go.
2 blank bytes (sp points to first byte)

Interesting. What’s next?

45a4:  3f40 0545      mov   #0x4505 "Please enter your password:", r15
45a8:  b012 2847      call  #0x4728 <puts>
45ac:  3e40 6300      mov   #0x63, r14
45b0:  3f40 0424      mov   #0x2404, r15
45b4:  b012 1847      call  #0x4718 <getsn>
45b8:  3f40 0424      mov   #0x2404, r15
45bc:  b012 2847      call  #0x4728 <puts>

We get asked for our password, which can also be 0x63 = 99 bytes. That then gets written back out for the user to see.

45c0:  0b44           mov   r4, r11
45c2:  3b50 e9ff      add   #0xffe9, r11
45c6:  3e40 0424      mov   #0x2404, r14
45ca:  0f4b           mov   r11, r15
45cc:  b012 5447      call  #0x4754 <strcpy>

And now we copy it to the stack, at a position 0x17 = 23 bytes behind r4. So now the stack looks this (* = we can write to it with our username, & = we can write to it with our password):

*&4 bytes of stored return address (r4 points to first byte)
--------------
*&2 bytes of the calling function's r11
*&2 bytes of the calling function's r4
*&1 blank byte
*&1 byte set to 0x0
*17 bytes where the password is intended to go
*1 byte set to 0x10
*1 byte set to 0x8
*17 bytes where the username is intended to go.
2 blank bytes (sp points to first byte)

Well that’s interesting. There’s lots of bytes we can write to, and some of them we can write to twice.

45d0:  0f4b           mov   r11, r15
45d2:  0e44           mov   r4, r14
45d4:  3e50 e8ff      add   #0xffe8, r14
45d8:  1e53           inc   r14
45da:  ce93 0000      tst.b 0x0(r14)
45de:  fc23           jnz   #0x45d8 <login+0x88>

We copy the contents of r11 (which is pointing at the password on the stack) into r15, which must be useful later because it’s not used here. Then we make r14 point to r4 - 0x18 + 1, which will be 17 bytes behind r4, which is where the password is stored on the stack.

The last 3 instructions are interesting. Once r14 points to the start of the password, we see if that byte is 0x0 (i.e. whether we’ve reached the end of the string). If the byte isn’t NULL, we got back 2 instructions and increment r14, then we test again. The end result is the r14 points to the 0x0 byte that marks the end of the string.

45e0:  0b4e           mov   r14, r11
45e2:  0b8f           sub   r15, r11
45e4:  5f44 e8ff      mov.b -0x18(r4), r15
45e8:  8f11           sxt   r15
45ea:  0b9f           cmp   r15, r11
45ec:  0628           jnc   #0x45fa <login+0xaa>

r11 now points to the NULL byte at the end of the password. Then we subtract r15 which is the first byte of the password. So now r11 is set to the length of the password in bytes.

Then the value 0x18 = 24 bytes down from r4 (which was initialized to 0x10) is copied into r15, then in sign-extended (sxt r15 so that it fills the whole 2 bytes of the register). Now we compare that value with r11. If r15 is bigger than r11 (i.e. the value that was initialized to 0x10 is bigger than the length of the password), then we jump over the next section.

45ee:  1f42 0024      mov   &0x2400, r15
45f2:  b012 2847      call  #0x4728 <puts>
45f6:  3040 4044      br    #0x4440 <__stop_progExec__>

And the next section writes some output then kills the program. So if we want this to be avoided, we need to make sure that the value 0x18 bytes down from r4 is bigger than the length of the password.

45fa:  5f44 e7ff      mov.b -0x19(r4), r15
45fe:  8f11           sxt   r15
4600:  0b9f           cmp   r15, r11
4602:  062c           jc    #0x4610 <login+0xc0>

Now the byte 0x19 = 25 bytes down from r4 (which was initialized to 0x8) is copied into r15, then extended to fill the whole byte. That gets compared with r11 (which is still the length of the password). If the password’s length is bigger than the value, we jump the next section.

4604:  1f42 0224      mov   &0x2402, r15
4608:  b012 2847      call  #0x4728 <puts>
460c:  3040 4044      br    #0x4440 <__stop_progExec__>

And the next section writes some output then stops the program. So if we want that to be avoided, we need to make sure that the value 0x19 bytes down from r4 is less than the length of the password.

4610:  c443 d4ff      mov.b #0x0, -0x2c(r4)
4614:  3f40 d4ff      mov   #0xffd4, r15
4618:  0f54           add   r4, r15
461a:  0f12           push  r15
461c:  0f44           mov   r4, r15
461e:  3f50 e9ff      add   #0xffe9, r15
4622:  0f12           push  r15
4624:  3f50 edff      add   #0xffed, r15
4628:  0f12           push  r15
462a:  3012 7d00      push  #0x7d
462e:  b012 c446      call  #0x46c4 <INT>
4632:  3152           add   #0x8, sp

First of all, this sets a 0 byte, 44 bytes down from r4, so that our stack looks like this:

*&4 bytes of stored return address (r4 points to first byte)
--------------
*&2 bytes of the calling function's r11
*&2 bytes of the calling function's r4
*&1 blank byte
*&1 byte set to 0x0
*17 bytes where the password is intended to go
*1 byte set to 0x10
*1 byte set to 0x8
*17 bytes where the username is intended to go.
1 blank bytes 
1 byte set to 0x0 (sp points here)

Then r15 is set to point 44 bytes below r4, which is the address that has just been set to 0x0. That address is pushed to the stack, followed by the address 23 bytes below r4 (which is where the password is stored), followed by the address 19 bytes below that (which is where the username is stored). Then INT, the interrupt function, is called with a first argument of 0x7d.

Looking through the manual, this interfaces with the lock to set a flag in memory if the username and password are correct. Judging by what’s been pushed to the stack, that flag is 44 bytes down the stack from r4.

The last instruction decreases the stack by 8 bytes, to compensate for the 4 2-byte arguments that were pushed for the call to interrupt.

4634:  c493 d4ff      tst.b -0x2c(r4)
4638:  0524           jz    #0x4644 <login+0xf4>
463a:  b012 4a44      call  #0x444a <unlock_door>
463e:  3f40 2145      mov   #0x4521 "Access granted.", r15
4642:  023c           jmp   #0x4648 <login+0xf8>
4644:  3f40 3145      mov   #0x4531 "That password is not correct.", r15
4648:  b012 2847      call  #0x4728 <puts>

This part of the programme tests the magic byte at the bottom of the stack, 44 bytes below r4. If it’s zero, we jump to printing that the password isn’t correct. But if it’s non-zero, we call the function to unlock the door, and print a message saying that access has been granted.

464c:  c493 faff      tst.b -0x6(r4)
4650:  0624           jz    #0x465e <login+0x10e>
4652:  1f42 0024      mov   &0x2400, r15
4656:  b012 2847      call  #0x4728 <puts>
465a:  3040 4044      br    #0x4440 <__stop_progExec__>

The last few instructions now. Firstly, we test 6 bytes down from r4 (the byte which was initialised to be zero). If it’s zero, we jump to the return section of the function. If not, we print something out then halt the program. So if that byte is non-zero, we won’t do a normal return from the login function.

Exploit

Let’s look at that final copy of the stack.

*&4 bytes of stored return address (r4 points to first byte)
--------------
*&2 bytes of the calling function's r11
*&2 bytes of the calling function's r4
*&1 blank byte
*&1 byte set to 0x0
*17 bytes where the password is intended to go
*1 byte set to 0x10
*1 byte set to 0x8
*17 bytes where the username is intended to go.
1 blank bytes 
1 byte set to 0x0 (sp points here)

One of the areas we can update is the return address. If we could rewrite that with the address 0x444a, then when login returns it would drop into the unlock_door function (which exists at 0x444a).

But we have to be careful - for the function to return, we have to make sure that canary 6 bytes below r4 is still 0x0. And we need to make sure the password’s length is between the values in the bytes 25 and 24 bytes below r4.

Using 0x0 as the canary actually create some problems for us. Because strings are terminated with a null byte, you can’t have 0x0 in the middle of a string. It has to be at the very end.

Let’s say we used the password to overwrite the return address. We can’t do that and get the canary byte to be 0. And we’ve already entered the username by this point, so we have no way of changing it after the password’s entered.

But if we write in a long password, we can overwrite the return address, then use password to reset the canary byte to 0. To do that, we’ll need a password that’s exactly 17 characters long, which will then be saved with a null byte where the canary is.

Password (bytes): 1010101010101010101010101010101010

The user name is more complicated. Look at the stack again (copied below for reference). We’ll need 17 bytes to fill the space (0x1010101010101010101010101010101010), then one byte that’s less than the length of the password (e.g. 0x01, then one that’s larger than the length of the password (e.g. 0xff). Then 23 bytes of filler to get through the password storage, canary byte + 5 other bytes (0x1010101010101010101010101010101010101010101010). Remember, this is OK because we’re going to write over the canary byte again when we enter the password.

Then we need the bytes we want to be our new return address. I’m going to use 0x4a44, so that we return to the unlock_door function.

*&4 bytes of stored return address (r4 points to first byte)
--------------
*&2 bytes of the calling function's r11
*&2 bytes of the calling function's r4
*&1 blank byte
*&1 byte set to 0x0
*17 bytes where the password is intended to go
*1 byte set to 0x10
*1 byte set to 0x8
*17 bytes where the username is intended to go.
1 blank bytes 
1 byte set to 0x0 (sp points here)

That gives a username of (bytes) 101010101010101010101010101010101001ff10101010101010101010101010101010101010101010104a44

The door springs open

This is the most tricky level yet. Having two inputs complicated the exploit, but the hardest part was just working out what every instruction was doing. Once we had that, and could map out what was meant to be on the stack, it was pretty straightforward to see how it could be exploited.


Subscribe for email updates