Next up, Vladivostok! This level took me the better part of a (very sunny) weekend*, but I finally got it!
This blog post walks through through how I solved the level, and how I would explain it to another person. As with other levels, it was a bit circuitous but that’s because there were new things to learn.
*Don’t worry, my dog still lobbied for and received plenty of walks and time outside. 🙂
Vladivostok Release Notes
Let’s see what’s new about this level…. randomization, oh no!
Lockitall developers further used the hardware randomization to improve lock security.
Despite the year of development effort which went in to it, we have heard reports that the memory protection introduced in to LockIT Pro r e.01 is insufficient. We have removed this feature in favor of the tried-and-true HSM-2. The engineers responsible for LockIT Pro r e.01 have been sacked.
Poor engineers. 😢
After skimming the release notes, I look through the code. Scanning through
main, I don’t really see any calls to login, unlock the door, etc.
I keep stepping and then let the program run until the username prompt appears. Then I realize, oh yeah. We’re doing the same obfuscated thing that we did in an earlier level. In other words, there’s no disassembly for each instruction, and instead you can either look at the “Current Instruction” window, or pull the instructions out of the memory view and disassemble them. This feels like walking through a blizzard and only being able to see one step ahead at a time.
Other Vladivostok Functions
Might as well scroll through the code and see what else is visible before I try. The idea here is to get as much of the “big picture” as possible before diving in to the nitty gritty.
A glance through the different functions shows some of the usual suspects, and a few newcomers.
We’ve got ourselves a
_memcpy (with an underscore, possibly denoting that it’s a non-standard implementation?)
We’ve got a
_bzero… which seems to be called by no one?
Ohhh neat we’ve got a
rand function. … what does it get used for?
The manual gives us a little bit more information on
We have the familiar conditional door unlock function, which calls a 0x7e interrupt:
We’ve got our old pal
We’ve also got two
aslr_main functions, one with an preceding underscore:
We’ve also got
gets, and other helper functions.
It does seem a bit tedious to spell out each of these functions, but if you continue running the program until it prompts you for input, you’ll see that the entire disassembly view is “overwritten”:
So uh, what gives?
Maybe these “overwritten messages” are because of ASLR? If we read up on it, we see that it stands for Address Space Layout Randomization. It’s intended to obfuscate locations of memory so that hackers can’t do Bad Things. A lot of exploits rely on knowing where certain things are located in memory, so making it difficult to find (or guess) those locations means that it’s harder to attack the program. Of course, I’m sure people have found ways around it.
I’m reminded of a comment that my friend made about how developers “work real hard to make something and there’s a whole universe of people out to break it immediately.” Oop. Well, at least the disappointment won’t be anything new.
Considering that we have a few
aslr functions, as well as a few calls to
rand, it seems plausible that ASLR is why we can’t see any of the memory. I’m not sure that that would correspond to the instructions getting “overwritten” but perhaps that’s how Microcorruption simulates an ASLR scenario. I think the result would be the same: you can’t get a good view into the disassembly. We can still see the instructions, one at a time, in the “Current Instruction” window.
So, we have ASLR going on, and we also have
printf. From my CTFing, I have a recently visited page about “bypassing ASLR with printf string format attacks” in my history. It didn’t apply to the CTF challenge but maybe it will be useful here. : )
Previous levels have made use of a string format attack using
printf. As the blog post discusses, printf trusts that we know what we’re doing when we pass string format specifiers (%d, %s, etc.) and will grab the contents of the stack and show them to us. If we ask for specifiers that do not have corresponding variables on the stack, then we’ll leak information to the user (hacker) about what the stack contains.
We’ll return back to the actual method of using
printf to get our bearings in an ASLR situation, but what is our strategy? If you run through the program once, you can see that your username gets printed back to you. So, we have one
printf opportunity to get our bearings, and maybe also overwrite something. What might we overwrite?
We could try swapping out the 0x7E (conditional) unlock door interrupt type with a 0x7F (unconditional) type. For now, let’s say that that’s our strategy. That means that we have to figure out how, or more importantly, where to do this.
Getting Our Bearings
Before we use
printf to leak information about the program’s memory, let’s see if we can glean anything from looking at the functions and trying a few things out.
The program tells us that the username is limited to 8 characters. Based on what we see here in memory (for a longer input), they actually mean it this time:
It’s funny, I wrote notes here that longer passwords sometimes resulted in an
insn address unaligned message. I thought that the inconsistency in receiving the message was due to the ASLR, but really, it was because I was made a mistake and thus had inconsistent inputs. Whoops. Lesson learned for next time.
I also notice in the aslr function (before everything gets all jumbly and overwritten), there’s a bunch of ASCII-range chars being moved into memory.
Here’s one such block:
44a0: f240 5500 0224 mov.b #0x55, &0x2402 44a6: f240 7300 0324 mov.b #0x73, &0x2403 44ac: f240 6500 0424 mov.b #0x65, &0x2404 44b2: f240 7200 0524 mov.b #0x72, &0x2405 44b8: f240 6e00 0624 mov.b #0x6e, &0x2406 44be: f240 6100 0724 mov.b #0x61, &0x2407 44c4: f240 6d00 0824 mov.b #0x6d, &0x2408 44ca: f240 6500 0924 mov.b #0x65, &0x2409 44d0: f240 2000 0a24 mov.b #0x20, &0x240a 44d6: f240 2800 0b24 mov.b #0x28, &0x240b 44dc: f240 3800 0c24 mov.b #0x38, &0x240c 44e2: f240 2000 0d24 mov.b #0x20, &0x240d 44e8: f240 6300 0e24 mov.b #0x63, &0x240e 44ee: f240 6800 0f24 mov.b #0x68, &0x240f 44f4: f240 6100 1024 mov.b #0x61, &0x2410 44fa: f240 7200 1124 mov.b #0x72, &0x2411 4500: f240 2000 1224 mov.b #0x20, &0x2412 4506: f240 6d00 1324 mov.b #0x6d, &0x2413 450c: f240 6100 1424 mov.b #0x61, &0x2414 4512: f240 7800 1524 mov.b #0x78, &0x2415 4518: f240 2900 1624 mov.b #0x29, &0x2416 451e: f240 3a00 1724 mov.b #0x3a, &0x2417
This also happens at 0x45f8 and 0x46d0.
These values are:
55 73 65 72 6e 61 6d 65 20 28 38 20 63 68 61 72 20 6d 61 78 29 3a => "Username (8 char max):" 57 72 6f 6e 67 21 => "Wrong!" 0a 50 61 73 73 77 6f 72 64 3a => Password:
It’s a bummer that those values weren’t more interesting, but it’s good that we checked.
These blocks of ASCII values are all in the
_aslr_main function (with the preceding underscore). Since these values get printed out during the course of program execution, we can say that
_aslr_main is where the bulk of the program logic is.
So, figuring out the ASCII strings was useful, because it helped me understand
_aslr_main, as we’ll see next.
I know that no one asked for a dump of assembly, but you’re getting it anyway. I’ve replaced the ASCII char blocks in
_aslr_main with pseudo-code.
From the conditional unlock door function, we know that an interrupt call looks like this:
... 4a46: 3d40 7e00 mov [provide interrupt code here], r13 4a4a: 0c41 mov sp, r12 4a4c: 0c12 push r12 4a4e: 0e12 push r14 4a50: 0d12 push r13 4a52: 0012 push pc 4a54: 0212 push sr 4a56: 0f4d mov r13, r15 4a58: 8f10 swpb r15 4a5a: 024f mov r15, sr 4a5c: 32d0 0080 bis #0x8000, sr 4a60: b012 1000 call #0x10 4a64: 3241 pop sr 4a66: 3152 add #0x8, sp ...
The interrupt type needs to get into r15 somehow, then we do the
bis 0x8000 (and following) lines. So we can write pseudo-code for interrupt calls, too.
With those two changes in mind, here’s
4482 <_aslr_main> 4482: 0b12 push r11 4484: 0a12 push r10 4486: 3182 sub #0x8, sp 4488: 0c4f mov r15, r12 448a: 3c50 6a03 add #0x36a, r12 448e: 814c 0200 mov r12, 0x2(sp) 4492: 0e43 clr r14 4494: ce43 0044 mov.b #0x0, 0x4400(r14) 4498: 1e53 inc r14 449a: 3e90 0010 cmp #0x1000, r14 449e: fa23 jne #0x4494 <_aslr_main+0x12> /* Beginning of block to move username into memory and then print it out */ 44a0-451e: // move username prompt into address 0x2402 4528: b240 1700 0024 mov #0x17, &0x2400 452e: 3e40 0224 mov #0x2402, r14 4532: 0b43 clr r11 4534: 103c jmp #0x4556 <_aslr_main+0xd4> 4536: 1e53 inc r14 4538-4554 // we're clearing r11 and then later moving it into r15 // that is getting used as the interrupt type. // we're also looping via jmp statements at 4534 and 455a. 4556: 6d4e mov.b @r14, r13 4558: 4d93 tst.b r13 455a: ed23 jnz #0x4536 <_aslr_main+0xb4> /* End of this block */ // Another putchar interrupt (also, r13 is set to 0xa) 455c-457c // r14 is cleared (0x0) and provided as the interrupt type -> putchar interrupt // we previously moved 0xa into r13, now we're adding 0x34. 457e: 3d50 3400 add #0x34, r13 4582-459c: // meanwhile, r14 hasn't changed, and we're moving that into r15 and using that as our interrupt type -> putchar 459e-45b8 // r14 is still 0, so another putchar interrupt? 45ba: 3a42 mov #0x8, r10 45bc: 3b40 2624 mov #0x2426, r11 45c0: 2d43 mov #0x2, r13 45c2-45dc: // we moved 0x2 into r13, which gets moved into r15. this is a 'gets' interrupt 45de: c24e 2e24 mov.b r14, &0x242e 45e2: 0b12 push r11 45e4: 8c12 call r12 45e6: 2153 incd sp 45e8: 0f4b mov r11, r15 45ea: 033c jmp #0x45f2 <_aslr_main+0x170> 45ec: cf43 0000 mov.b #0x0, 0x0(r15) 45f0: 1f53 inc r15 45f2: 3f90 3224 cmp #0x2432, r15 45f6: fa23 jne #0x45ec <_aslr_main+0x16a> /* Move password prompt into memory and print it out */ 45f8-4634: // move password prompt into 0x2402 4638: 3e40 0224 mov #0x2402, r14 463c: 0c43 clr r12 463e: 103c jmp #0x4660 <_aslr_main+0x1de> 4640-465e: 1e53 // we previously cleared r12 to 0. That is getting moved into r15 -> putchar interrupt. // notice that 463e and 4664 are jump statements. // this section is a loop, printing up the entire password prompt. 4660: 6d4e mov.b @r14, r13 4662: 4d93 tst.b r13 4664: ed23 jnz #0x4640 <_aslr_main+0x1be> /* End of password prompt */ 4666: 0e43 clr r14 4668: 3d40 0a00 mov #0xa, r13 466c-4686 // since we cleared r14, this will be a 0x0 type putchar interrupt 4688: 0b41 mov sp, r11 468a: 2b52 add #0x4, r11 468c: 3c40 1400 mov #0x14, r12 4690: 2d43 mov #0x2, r13 4692-46ac // since 0x2 -> r13 -> r15 -> INT, this is a type 0x2 or 'gets' interrupt 46ae: 3d50 7c00 add #0x7c, r13 46b2-46ce // r13 previously had 0x2 in it, we're adding 0x7c to get 0x7e. // This is a conditional unlock interrupt. 46d0-46ee: // move "Wrong!" into 0x2402 46f8: b240 0700 0024 mov #0x7, &0x2400 46fe: 3d40 0224 mov #0x2402, r13 4702: 103c jmp #0x4724 <_aslr_main+0x2a2> 4704: 1d53 inc r13 4706: 8c11 sxt r12 4708-4722 // not sure about this section but I'm guessing it's another putchar interrupt. // It's likely that there's one for the success case, and one for the failure case. 4724: 6c4d mov.b @r13, r12 4726: 4c93 tst.b r12 4728: ed23 jnz #0x4704 <_aslr_main+0x282> 472a-474a: // we're clearing r14, and moving it into r15, which then determines our interrupt type. // Another putchar interrupt. 474c: 0e41 mov sp, r14 474e: 2e53 incd r14 4750: 0e12 push r14 4752: 3f41 pop r15 4754: 3152 add #0x8, sp 4756: 3a41 pop r10 4758: 3b41 pop r11 475a: 3041 ret
Well, that was a lot. But after swapping out the ASCII parts, and the interrupts, a lot of the magic is gone.
Vladivostok Program Structure
Basically, we are:
- Doing… something? at the beginning of the program
- Moving the username prompt into memory and then printing it
- Getting the username from the user, and printf-ing it.
- Moving the password prompt into memory and then printing it.
- Getting the password from the user
- Calling a 0x7E (
0x7C+ 0x2) interrupt to determine if the username and password are valid
- Reporting back to the user how it went
This isn’t anything too different from previous levels. How might we get the program to do what we want?
- There’s no unconditional unlock function so we can’t reroute the return address to go there instead.
- Overwriting the return of the password check will just print up something different but not actually unlock the door.
I think our best bet is to change the type of the interrupt that we’re calling from
0x7F. The way they set up the interrupt is kinda weird… by adding
0x7C. So, we could hypothetically either overwrite
0x3, or we could try to overwrite
0x7D. Because we’re limited to 8 chars, I think we should try the
0x3 option first.
This will look something like:
3 arbitrary chars + 4-byte addr + %n
This was one of the improved-upon strategies in the Novosibirsk level. Upon closer inspection, however, I see that that strategy worked because of this instruction.
4552: 0312 push #0x0
In our case, we’re adding a value to a register, so we’d have to overwrite the instruction, not just a constant. So that won’t work.
Still, the idea of modifying the type of an interrupt is still interesting. Since we’ve got a lot of interrupts in our code (as we saw in the
_aslr_main section), maybe we can make use of this later.
Okay, New Strategy
What if we did the 8-byte strategy from the Novosibirsk level? That was:
[addr to start reading at] [address to write to] %s %n
This would fit within our 8-byte limit. The idea here was that we pick an address near the end of memory (0xff80 range) and read the “garbage” data until the printf function reaches the terminating
00. After reading 0x7F chars, we write that value into the second address.
Unfortunately, even after going through printf line-by-line (more times than I can count) and comparing this printf implementation to that of previous levels, I couldn’t get this to work.
How did I know where to write to for these strategies? I had the original disassembly saved in a text file, and then would find the new locations by hand by control-F and looking for landmark/unique assembly instructions.
I also looked at
rand quite a bit. This was because I had previously done other CTF challenges where a mistake in the
rand setup means that the numbers are predictable. I investigated that for a while and didn’t find any pattern in results of calls to rand.
I also tried following a number of strategies that are used for dealing with ASLR and printf format attacks, like the CTF “console” writeup I linked to earlier. I also watched this:)
However, the syntax that they used to print up large portions of the stack or make use of offsets, etc, didn’t seem to work in Microcorruption. Since we can (mostly) see what’s going on in printf and how it has limited support for special characters, that makes sense. Oh well, I still learned some interesting things. : )
%x %x %x %x
That’s not to say that the string format ideas were entirely fruitless. I typed “%x%x%x%x” to try and leak the contents of nearby memory, and the printf always returned:
0000 [16-bit address that was different every time] 0000 0000
While address itself was different every time, it pointed to the same contents every time. So, it prints up the same point in the program every time, but thanks to ASLR, the actual address of that point varies.
This would prove to be useful later.
After having gone through the
printf functions repeatedly and only making use of the username, I felt kind of stuck.
I had previously verified that the username was limited to 8 chars. I made the assumption that the password length was, too. Of course, that was a mistake on my part (assumptions, asses, etc etc). Extra funny because I had noticed some
address unaligned messages, as mentioned earlier.
Eventually, I tried giving a “too long” (by previous levels’ standards) input, and walked through the code after the
gets call for the password. It prints up “Wrong!” (rude).
Then it tried to return to an unaligned address. Hmmm.
Because of ASLR, the memory addresses are different for each program run (which means they’ll be different for you, too).
If we input
AAAABBBBCCCCDDDDEEEE for our password, we see:
At this point in program execution, the current instruction is
pop sr. After stepping through it a few times, I was able to discern that we had just finished executing this portion of code:
474c: 0e41 mov sp, r14 474e: 2e53 incd r14 4750: 0e12 push r14 4752: 3f41 pop r15 4754: 3152 add #0x8, sp 4756: 3a41 pop r10 4758: 3b41 pop r11 475a: 3041 ret
So, we popped r10, r11, and then we were returning. The problem with the
unaligned address is that we’re returning to 0x4343… or trying to. Since we did these things in sequential order, and the stack pointer is moving up in memory (towards higher addresses), we know that the previous 4 bytes
4242 are now in r10 and r11. (In the photo, the stack pointer is pointing to the next address after our attempted return address of 0x4343).
We can verify that by looking at the register state:
Let’s recap real quick:
- We can leak out some memory info by
printf-ing the username
- We can overwrite the return address at the end of the
_aslr_mainlogic to go somewhere else.
- We’re also able to overwrite r10 and r11.
Earlier, I mentioned a strategy where I tried to overwrite the type of an interrupt so instead of doing a
putchar call it, it did a 0x7f unconditional unlock door interrupt.
Since I went through all of the assembly in both
printf, I had seen many interrupt calls. More specifically, I saw how different interrupts loaded the interrupt type in differently. They’d load the value from r11, 12, r13 or r14 into r15, and then that was passed to the interrupt handler.
We have control over r11, so let’s find an interrupt that makes use of that.
It appears that there are two such interrupts. I chose this one, which prints out the username prompt one char at a time:
4534: 103c jmp #0x4556 <_aslr_main+0xd4> 4536: 1e53 inc r14 4538: 8d11 sxt r13 453a: 0b12 push r11 453c: 0d12 push r13 453e: 0b12 push r11 4540: 0012 push pc 4542: 0212 push sr 4544: 0f4b mov r11, r15 // look here, it's r11!! 4546: 8f10 swpb r15 4548: 024f mov r15, sr 454a: 32d0 0080 bis #0x8000, sr 454e: b012 1000 call #0x10 4552: 3241 pop sr 4554: 3152 add #0x8, sp 4556: 6d4e mov.b @r14, r13 4558: 4d93 tst.b r13 455a: ed23 jnz #0x4536 <_aslr_main+0xb4>
The line with
mov r11, r15 looks good. Let’s grab that address and get working.
We’ll input whatever (for now) for our username. Then, for our password:
[6 bytes of filler] + [7f00 to put into r11] + [address to jump to]
For the filler, I just used
41s (or “A”).
How did I find the address? I looked for instruction
0f4b in the memory view and found the corresponding address. There’s actually two
0f4bs so you have to make sure you’re getting the right one (which you can determine by the instructions before/after).
So, that means our password is something like:
4141414141417f00 + [address that contains 0f4b]
I tried this, and the program got off into the weeds in a bad way. The problem seemed to be due to the
pc having a nonsensical value. So, instead of getting the address of
mov r11, r15 I went back a few lines.
I want to make sure there’s a good
pc value so I’m interested in jumping to this line:
4540: 0012 push pc
Okay, to summarize, our new password strategy is:
4141414141417f00 + [address that contains 0012 (followed by 0212, 0f4b, etc.)]
Cool. I tried this, and it worked. So, I went to type
solve and … … I can’t see ANY of the memory
Really, considering the ASLR, this shouldn’t have been a big surprise. Luckily, we have a way to solve it.
We know that there’s a way to leak memory via the username being
printf'd back to us. And we know that the password “formula” is this:
4141414141417f00 + [address that contains 0012 (followed by 0212, 0f4b, etc.)]
In debug mode, I looked at the address that was being printed out from the username input of
%x%x. This was always in the format of
0000 [address] Since the next two addresses are always 0s, there’s no point in using more chars.
After having the username-printed address, I searched for the address that goes in the password input. Then, using a hex calculator, I found the difference between the two. For the new address I chose (to put in the password), the offset was always 0x22A. I tried this multiple times and the offset was always the same. Awesome!
So, to find the address to add to our password input:
[address for password] = [username printf address result] - 0x22A
At this point, I had Taylor Swift’s “22” stuck in my head.
To solve, type
solve. Type a username of
%x%x and get the first address. Use a hex calculator and subtract 0x22A. Adjust this value for endianness (swap the bytes). Put this at the end of the password:
4141414141417f00 + [address you just calculated]
And that should unlock it!