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:
- We’re trying to write over the return address and instead direct the program to the
unlock_door
function. - We’re capped at 32 chars for the username and password combined… for real this time.
- Throwing in some 00’s doesn’t help us because
strcpy
stops at 00. - We can’t use the username
cmp.b
to our advantage because thegetsn
function is limited to a 0xff-length username.
At this point, I don’t have any clever ideas for the username section, and instead, decide to compare the username and password section.
I added some spaces between the section so you can see the three main parts:
- Ask for the username/password, get it, print it.
- Count the length and compare
- 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à!