Microcorruption: Lagos

Wed 19 July 2023

The Scenario

Once again I found myself somewhere far too hot, facing off against a product I'm sure the Lockitall devs put a lot of effort into even though it never seems to come through in the finished product. This time my opponent was revision c.04 of the Lockitall LockIT Pro, with a LockIT Pro HSM-2 handling password validation and deadbolt duty. Skimming through the user manual, I noticed they saved the surprise for the end: "Due to user confusion over which characters passwords may contain, only alphanumeric passwords are accepted." Perhaps they read a particularly compelling article and realized alphanumeric passwords contain more than enough entropy, even though I couldn't find an article suggesting the restriction of character sets. Nevertheless, this lock now had my interest; it was time to see what special kind of awful awaited me.

Understanding The Code

This lock's firmware was substantially similar to previous ones: the return of an interrupt function was appreciated; all the usual library functions like getchar(), putchar(), getsn() and puts() were there; main() was yet again a stub for login() which handled core functionality.

login() informs the user of the input restrictions and maximum password length before collecting the candidate password and calling conditional_unlock_door() to let the HSM handle the heavy lifting - of course, since the same Lockitall devs developed this firmware it accepts 512 bytes of input for a 19-byte buffer. [1]

However, the code for enforcing the input restrictions was solid: storing the initial input on the stack, confirming each character is an ASCII letter or digit before copying into position, immediately ending upon finding an invalid character and using memset() to zero the stack afterwards to ensure there were no invalid bytes floating around.

A screenshot of the disassembly of the login function

Exploiting The Code

The return address for login() is stored right after the password buffer so I figured I'd return to some of my own shellcode and figure things out from there, which is when I realized what kind of situation I was in: due to the input restrictions the earliest address I could return to from login() was 0x4430 - not a problem by itself, but conditional_unlock_door() starts at 0x4446 and since it's called before my shellcode I couldn't overwrite it. I only had 22 bytes for instructions - there was some additional space for constants, but that was it.

That shouldn't have been much of a problem, though - all I needed was to get 0x7f in the right register and call the interrupt function. Credit where it's due, the input restrictions didn't stop me but they did slow me down; after a lot of trial and error I got 0x7f where it needed to be - only to realize I couldn't write the bytes for the CALL instruction. This was a bummer, but I could easily modify my shellcode to use a jump instruction instead - until I realized I also couldn't write the bytes for a jump instruction either. [2] OK, then what about writing an address to the stack and using RET to jump there? As it turned out, I had no way to write to the stack - an instruction reference I found online informed me I was limited to some MOV instructions, some ADD and ADDC instructions, some SUBC instructions, POP and RET; at this point I realized I had a first-class ticket for the struggle bus.

I must have spent at least the next 12 hours looking for some write primitive, trying rabbit hole after dead end and coming up empty; I'd searched for so long it was no longer early, instead the time of day nobody wants to be awake. I decided to take a break before the sun came up, and after getting an insufficient amount of sleep I dug back into the firmware. Looking at the firmware with still-tired eyes, I realized I could reuse the PUSH instruction at the start of conditional_unlock_door() to write an address but it was a bit away from the stack pointer and I needed to RET to it. Just to make things even tougher, I only had eight bytes left to massage the stack pointer into place - not a whole lot when the smallest instruction is two bytes long and I could only subtract the stack pointer by one due to the input restrictions.

A screenshot of a memory dump of the lock in the middle of executing my shellcode

Thankfully I had literally the exact amount of space necessary for my shellcode; the relief I felt when I saw the lock open was immense. As I boarded my next flight, Yombinator bonds stowed securely in my luggage, I couldn't help but respect the Lockitall devs - they still sucked, writing ridiculously exploitable code, but they were trying out-of-the-box strategies that did actually manage to stump me for a long time; if they could manage to remember how large their buffers were they might write some actually-secure code soon enough.

[1]It would have been a 20-byte buffer but I think there's some kind of off-by-one thing happening? I'm not sure it isn't some compiler shenanigans but I wouldn't want to give too much credit to the devs
[2]I could technically use JN and JGE, but I couldn't use CMP to set the status register to actually use them