Microcorruption 0x0E - Vladivostok
Prev: 0x0D Algiers
Next: 0x0F Bangalore
This level's manual is again fairly vague on the new features it's added, and "main" itself isn't very intuitive. There are two calls to a new "rand" function, followed by a second call to "__init_stack." Then, there's a "memcpy", and ultimately "main" finishes with a call to r13.
If we break at this "call r13" instruction in "main," it's not exactly instantaneous - the program is doing some heavy lifting. If we reset and check the Live Memory Dump before and after, we can see that the entire code section of the program is copied to another address in our memory!
Initial code section:
Copied code section:
Even more interesting, if we run it again, we find that it's copied elsewhere:
Why is it being duplicated? Well, if we step into the "call r13" that we've been breaking at within the "main" function, we can see that we're stepping into these duplicated instructions and as such, we cannot view them in the Disassembly window.
If we watch the Current Instruction window, however, we can write down the first few instructions and search for them:
Next: 0x0F Bangalore
This level's manual is again fairly vague on the new features it's added, and "main" itself isn't very intuitive. There are two calls to a new "rand" function, followed by a second call to "__init_stack." Then, there's a "memcpy", and ultimately "main" finishes with a call to r13.
If we break at this "call r13" instruction in "main," it's not exactly instantaneous - the program is doing some heavy lifting. If we reset and check the Live Memory Dump before and after, we can see that the entire code section of the program is copied to another address in our memory!
Initial code section:
Copied code section:
Even more interesting, if we run it again, we find that it's copied elsewhere:
Why is it being duplicated? Well, if we step into the "call r13" that we've been breaking at within the "main" function, we can see that we're stepping into these duplicated instructions and as such, we cannot view them in the Disassembly window.
If we watch the Current Instruction window, however, we can write down the first few instructions and search for them:
- mov r15, r14
- add #0x82, r14
- call r14
- The code section is duplicated to a random address
- The stack is moved to a random address
- The prior code section is overwritten with zeroes, preventing us from reading the Disassembly window
So, this is new. If we generate a payload with absolute addresses like we have been doing we might be able to crack this instance, but it likely won't work in the next instance. Sure, we can peak at the Disassembly to see how the addresses change, but remember that we cannot reference this section when we attempt to solve the level for real.
We do have one thing going for us, however. If we run the program and enter 8 As for the username and 8 Bs for the password, then enter "f" to finish the current function, we can see that the address it returned to is located right after the password buffer on the stack. In my case, this was address 7900.
So, if we are able to figure out the stack address at runtime through our input somehow, we can return into our stack to execute some shellcode. Alternatively, if we can figure out the code section address at runtime, we can return to some desirable instruction there. However, we cannot return to the stack using a code address or vice versa, as both addresses will be randomize and thus will be inconsistently spaced apart from each other.
Now, how do we figure out either of these addresses at runtime? If you continue examining the Disassembly instructions, you may notice a lingering vulnerability: "printf." Previously, we used the "%n" conversion character to perform an arbitrary write to an address of our choosing. However, this also requires us to know addresses ahead of time, so this is not too useful for us.
What about other conversion characters?
The other characters ("%s," "%x," and "%c") are all very similar - they specify the format of the arguments passed into "printf" so that they are printed from the stack correctly. But what happens if you don't provide arguments to the stack? The function really doesn't know any different - it will grab values off of the stack regardless! Try entering "%x" several times as the username, and check the output:
Four hex values are printed right off of the stack! Three are null, but one of them - "dba0" in my case - could be an address.
If we scan the Live Memory Dump for our address, we find several instructions at that address:
Now that we have an address to somewhere in the code section, once we determine an instruction that we'd rather return to, we just need to calculate the number of bytes between them. Then, to solve the level, we take whatever random address is provided to us, add the offset, and use that as the return address!
Where would we like to return to? Unfortunately, we don't have "unlock_door," but we can locate an "_INT" function. In this, 0x2(sp) is popped from the stack into r14 and used as the argument for the program interrupt. If we use 0x7f, as we've done in prior levels, we unlock the door.
So, now we need to check these instructions in the Live Memory Dump to see how they look, so that we can search for them later after we use the username buffer to pop the random code section address. Before randomization, the instructions start at 48ec.
If we continue, and again enter "%x%x" as our username, we get the random code section address. I received 7314. Then, after searching the Live Memory Dump, I found the duplicated "_INT" function at address 7496. Subtracting these two addresses, we get an offset of 0x182.
We now have our solution: first, enter "%x%x" as the username, then for the password, enter:
- 8 bytes of "A" to fill the buffer
- The address you just received plus 0x182, which should be the address to _INT
- 2 bytes of "A" as more filler
- "7f00", which will be treated as 0x2(sp) for the _INT call
Enter the final payload and we're in!