>/posts/Sword Of Secrets - 0x1 Palisade $
Estimated reading time: 14 minutes
What is even this task about?
Ok, so last time we ended up with some bunch of weird looking bytes;
00 00 00 00 0e 05 13 07 36 0f 37 69 22 27 3f 65 2e 20 36 69 2f 3b 3f 24 26 61 2c 21 24 3a 7b 65 7d 39 6a 79 7d 79 6a 38 4d

Attempt to SOLVE says 'This is not the right header...' so we can simply search for this string in the repo.

Great, single appearance in a simple condition. The 'Palisade' function has to return something equal to or greater than 0. Let's search for this function now.
int palisade()
{
int err = -1;
char message[128] = { 0 };
flash_read(PALISADE_FLASH_ADDR, message, sizeof(message));
xcryptXor((uint8_t *)message, sizeof(message));
if (memcmp(message, FLAG_BANNER, strlen(FLAG_BANNER)))
{
goto error;
}
printf(message);
printf("\r\n");
err = 0;
error:
return err;
}
It loads the message from the task address (0x10000), XORs it, and compares the message to FLAG_BANNER. What's that?
#define FLAG_BANNER "MAGICLIB"
It looks like a flag prefix, e.g. MAGICLIB{S0M3_T3XT_G035_H3R3}. The last thing we need to search for is PALISADE_FLASH_ADDR, and it brings us to this part of the code;
static void palisadeSetup()
{
char message[] = FLAG_BANNER "{No one can break this! " S(PARAPET_FLASH_ADDR) "}";
size_t len;
printf("Running %s...\r\n", __FUNCTION__);
// Create the mesasage
len = strlen(message) + 1;
xcryptXor((uint8_t *)message, len);
// Oh noes! Something happened... X_X
*((uint32_t *)(message)) = 0x00000000; // random(0xffffffff);
// Write the first flag to its corresponding address
flash_erase_block(PALISADE_FLASH_ADDR);
flash_write(PALISADE_FLASH_ADDR, message, len);
}
Now we can see that the plaintext before XOR was MAGICLIB{No one can break this! 0xXXXXXX}, so we need to decrypt it to obtain the address of the next task. To verify, we can count the characters in this text and compare to our byte array length.

And yes, the length is exactly the same! That's good news. There is yet one more detail we should pay attention to. Before writing our message to flash, something happened, and the first couple of bytes got zeroed out. We have to reconstruct them even though we don't know the XOR key and that's our task.

How to get the key?
We know that FLAG_BANNER length is equal to 8, so that's the only length that matters. Our task is to find
$$ \text{cipher_bytes} \oplus \text{unknown_key} = \text{MAGICLIB} $$
and at this point we still don't know the key that was used to encrypt this. But we have the rest of the ciphertext and the original plaintext. If you pay attention to the XOR function;
void xcryptXor(uint8_t * buf, size_t len)
{
for (unsigned i = 0; i < len; i++)
{
buf[i] ^= xor_key[i % sizeof(xor_key)];
}
}
It repeats the key if the plaintext is longer than the key. In that case, even though XOR isn't a mathematically bijective function, cryptographic XOR with a constant key is, which means we can perform:
$$ \text{cipher_bytes} \oplus \text{known_input} = \text{unknown_key} $$
and recover the key this way!
This script recovers the key:
mem_bytes = "00 00 00 00 0e 05 13 07 36 0f 37 69 22 27 3f 65 2e 20 36 69 2f 3b 3f 24 26 61 2c 21 24 3a 7b 65 7d 39 6a 79 7d 79 6a 38 4d"
flash_data = mem_bytes.split.map { |x| x.to_i(16) }.pack("C*").b
known_plain = "MAGICLIB{No one can break this! 0xFFFFF }"
puts "[*] Flash data (#{flash_data.length} B) : #{flash_data.bytes.map { |b| "%02x" % b }.join(" ")}"
def xor_bytes(data, key)
data = data.b
key = key.b
data.bytes.map.with_index { |byte, i| byte ^ key.bytes[i % key.bytesize] }.pack("C*").b
end
output = xor_bytes(flash_data, known_plain)
puts output
"MAGIMIZEMAXIMIZEMAXIMIZEMAXIMIZEMA,?;?,\x180"
From this we can assume the repeating key is "MAXIMIZE". Now we can take the first 4 bytes of mem_bytes and XOR them with "MAXI" as the key.
Fix the broken data
Once we know the key we can fix the affected bytes. This is one more fun fact about the XOR function. If you XOR two identical bytes, the result is 0x00, and the same applies in reverse 0x00 XOR e.g. 0xA5 equals 0xA5. Why does it matter in our case? Well, MAGI and MAXI differ by only one letter, meaning 3 out of 4 bytes were already 0x00 in the ciphertext even before the memory corruption occurred. Here is the script to obtain the fixed memory value:
mem_bytes = "00 00 00 00 0e 05 13 07 36 0f 37 69 22 27 3f 65 2e 20 36 69 2f 3b 3f 24 26 61 2c 21 24 3a 7b 65 7d 39 6a 79 7d 79 6a 38 4d"
flash_data = mem_bytes.split.map { |x| x.to_i(16) }.pack("C*").b
def xor_bytes(data, key)
data = data.b
key = key.b
data.bytes.map.with_index { |byte, i| byte ^ key.bytes[i % key.bytesize] }.pack("C*").b
end
out = xor_bytes("MAGI", "MAXI")
patched_flash = out + flash_data[4..]
puts "Patched flash data: #{patched_flash.bytes.map { |b| "%02x" % b }.join(" ")}"
And now we just need to upload it to flash memory;

And yeah, as you can see, we got the address of the second task and the flag displayed!
See you in the next post of this series!
X
Comments (0)
Please log in to add a comment.