Work Smart (and Hard)
In information security news, the stories that tend to get the most press are discoveries of dreaded Zero-Day vulnerabilities. The more widespread the software, the sexier the story. Just look at the bug found in openssl in 2014 – so cool it even got a name – Heartbleed. In recent years, there have been many stories like this as well as many publicly disclosed breaches of large organizations. All this coverage might lead some to think that our biggest threat is the dreaded Zero-Day, but the reality is that simple misconfigurations and years-old, unpatched software vulnerabilities are still a greater threat to most organizations.
Level 3 of the MicroCorruption CTF highlighted this important lesson for me personally. It’s easy to jump to the most complicated, sophisticated, interesting solution. But it is far better to cover all our bases (in order!), and pick ALL the low hanging fruit, before trying to tackle the larger, more nuanced problems.
On the previous two revisions of the LockIT Pro — the password was available for extraction directly from the debugger.
According to the latest update, the new lock revision b.01 employs a separate hardware security module (HSM) which will store the password separate from the lock firmware. In theory, the HSM contents will not be directly accessible from the debugger.
Scrolling through the debugger, it looks like the main function simply makes a call to login. So I will examine the login function to get an idea of how this lock will operate.
As with previous locks, we see some preliminary call puts operations to display prompts to the user. Previous locks used a get_password function to get user input, but this lock employs calls to getsn. The lock manual (provided with the debugger) has this to say about the gets function:
The function takes two arguments – an address pointer to a buffer in memory and an integer specifying the number of bytes to read from the input device.
Looking at the two instructions just before call getsn, we can see what the values for these arguments are:
4534: 3e40 1c00 mov #0x1c, r14
4538: 3f40 0024 mov #0x2400, r15
453c: b012 ce45 call #0x45ce <getsn>
The function declaration, as shown in the documentation, shows the first argument is a pointer to a buffer. In the debugger, working backwards from the call operation, this argument is: mov #0x2400, r15. The input specified by the user will be placed in memory at address 0x2400.
Continuing on to the second argument – the length of the input to copy – mov #0x1c, r14. The length is 0x1c bytes(hex) or 28 bytes(decimal). This is interesting. According to the user prompt a few instructions earlier, passwords are not expected to be any larger than than 16 characters (bytes).
452c: 3f40 9e44 mov #0x449e "Remember: passwords are between 8 and 16 characters.", r15
The prompt says 16, but the getsn function will read in up to 28 bytes (only stopping if a null byte – 0x00 – is encountered in the input ). If the firmware is only expecting 16 characters but we can input 28, there may be an opportunity for a buffer overflow.
Before I take a deep dive in debugging specifics, I want to first see what normal looks like. The prompt says passwords should be between 8 and 16 characters, so I will run the program and submit a string of 12 characters and observe the result.
Test password: TWELVECHARS!
As expected, the string “TWELVECHARS!” is not the correct password and the lock exited normally.
Next, I will reset the lock and try a string of 18 characters – two more than expected.
Test password: EIGHTEENCHARACTERS
Even with two extra characters the program exits normally. Let’s try the full 28 characters and see what happens.
Test password: TWENTYEIGHTCHARACTERPASSWORDAA
Even with 28 characters there was no abnormal functionality. This is where my initial investigation went off the rails. As soon as I saw buffer overflow could be an option, I assumed the most complicated, difficult to exploit version of such an overflow would be necessary. I assumed the overflow would overwrite a return address somewhere which would lead to a program crash. By analyzing the crash, I thought I would be able to control execution and solve this level with custom shellcode. I spent a lot of time trying to figure out why my 28 byte input wasn’t crashing the lock.
As it turns out – I was overthinking this problem by a mile.
Taking another look at the login function and at the I/O console output of a normal program run – I noticed something interesting.
The string “Testing if password is valid” is only output to the console after the call to the test_password_valid function. Why?
Directly after the string “Testing if password is valid” is output to the screen, we see a cmp.b operation.
455a: f290 4c00 1024 cmp.b #0x4c, &0x2410
4560: 0720 jne #0x4570 <login+0x50>
This is checking if the byte found at address 0x2410 is equal to 0x4c (hex) or capital L (ascii). The next operation will jump to offset 0x50 in the login function (exit) if the comparison is NOT equal (Password is not correct).
If the result of the comparison is equal, we will get an “Access Granted” message and the unlock_door function will be called.
4562: 3f40 f144 mov #0x44f1 "Access granted.", r15
4566: b012 de45 call #0x45de <puts>
456a: b012 4844 call #0x4448 <unlock_door>
User input is saved at memory offset 0x2400. The software expects the password to be, at most, 16 characters long which would take up memory from 0x2400 – 0x240F. The very next byte, 0x2410, is used as a “flag” to indicate a successful password – we know this from the cmp.b operation at instruction 455a. This byte is well within our 28 byte limit.
Backing up for just a moment – let’s look at how this value gets set (or unset). Just after the test_password_valid function, the return value (at register r15) is tested. Presumably, this is checking if the result of test_password_valid was True or False. If the wrong password is entered, then the flag value at 0x2410 should be set to 0x35 (as seen at instruction 454c). If the correct password is entered, the mov.b 0x35, 0x2410 operation should be skipped – leaving the 0x2410 value as 0x4c which we know will unlock the door in a later instruction.
This check was implemented incorrectly. As seen at instruction 454a, if the result of the tst r15 instruction is zero, execution jumps to instruction 4552 (jz $+0x8) – completely skipping 454c. This lock software always returns 0 for incorrect passwords (and possibly for correct passwords, but we don’t have one to test). So we can enter an incorrect password, and we know that whatever value is stored at 0x2410 will remain — it will NOT be overwritten.
Since password input is stored at 0x2400, and we can input more than 16 characters, we can control what byte is placed at position 0x2410. We also know that, as long as our password is incorrect, whichever byte we place at 0x2410 will be unchanged.
0x2410 is 17 bytes in to the input buffer. I will use a password such that the 17th character is capital L ( or 0x4c ).
Test password: SEVENTEENTHCHRISL
Success! With this password we get the satisfying door unlocked message.
Just one extra character in the password input. No need to spend hours trying to figure out how to fit custom shellcode in to 28 bytes and how to take over code execution (which may not be possible anyway). A few extra minutes spent analyzing the problem before diving in would have saved hours of effort.
The same is true when it comes to securing the enterprise. The key is to follow a sensible security maturity model, making sure to cover the fundamentals well. Once the fundamentals are handled appropriately, time can be well spent thinking about the Zero-Days an attacker would have to find in order to breach the perimeter.
Contact us now to discuss how we can put your security operations on track.