>/posts/Sword Of Secrets - 0x3 Postern $

Estimated reading time: 12 minutes


Third task - Postern

Now it's time for the 3rd task, and this one was much more challenging than the previous one. From the previous task, we learned that the data for this challenge is located at memory address 0x30000, and just like in both previous tasks we also get an error message displayed.

ss1

So similarly to the previous tasks, we're searching for the error message in the code repository to find the function responsible for generating it.

ss2

Our third task is named "Postern", and here is its code:

int postern()
{
    int err = -1;
    struct AES_ctx ctx;
    uint8_t iv[AES_BLOCKLEN] = { 0 };
    char message[128];
    char response[128];
    size_t len;

    flash_read(POSTERN_FLASH_ADDR, &len, sizeof(len));

    len = MIN(len, sizeof(message));

    flash_read(POSTERN_FLASH_ADDR + sizeof(len), message, len);
    flash_read(POSTERN_FLASH_ADDR + sizeof(len) + len, response, sizeof(FINAL_PASSWORD) - 1);;

    AES_init_ctx_iv(&ctx, aes_key, iv);
    AES_CBC_decrypt_buffer(ctx&, (uint8_t *)message, len);

    if (checkPKCS7Pad((uint8_t *)message, len) < 0)
    {
        err = 1;

        goto error;
    }

    message[len - message[len - 1]] = '\0';

    // Check everything's OK!
    if (memcmp(response, FINAL_PASSWORD, strlen(FINAL_PASSWORD)))
    {
        goto error;
    }

    printf(message);
    printf("\r\n");

    err = 0;
error:
    return err;
}

And the last thing we got is the data from the mentioned 0x30000 address:

20 00 00 00 f7 60 4a 1f 5e 96 39 7e 96 f5 9e 31 72 0b d9 00 d7 6b ed c8 d1 d1 47 34 81 46 9a 24 bf aa 90 22

Here, 0x20 means the size of the data, and we got exactly 32 bytes. Let's look at the task setup:

static void posternSetup()
{
    struct AES_ctx ctx;
    uint8_t iv[AES_BLOCKLEN] = { 0 };
    char message[128] = FLAG_BANNER "{Passwd: " FINAL_PASSWORD "}";
    size_t len;

    printf("Running %s...\r\n", __FUNCTION__);

    // Initialize AES context
    AES_init_ctx_iv(&ctx, aes_key, iv);

    len = PKCS7Pad((uint8_t *)message, strlen(message));

    // Encrypt
    AES_CBC_encrypt_buffer(&ctx, (uint8_t *)message, len);

    // Oops... Something bad happened...
    message[len - AES_BLOCKLEN - 1] = '\0';

    // Write buffer to flash
    flash_erase_block(POSTERN_FLASH_ADDR);
    flash_write(POSTERN_FLASH_ADDR, &len, sizeof(len));
    flash_write(POSTERN_FLASH_ADDR + sizeof(len), message, len);
}

Now we know the complete task setup and flow. To solve this, we need to restore the original message, but there is a problem: one byte was zeroed and we do not know its value. On top of that, we also do not know the AES key. One more thing worth mentioning is that the Postern challenge adds a null byte after the end of the message, which causes printf() to stop reading there, and that's good.

What can we do?

This time the used cipher was AES-CBC, which means blocks are chained and the next block can be correctly decrypted only if the previous one was correct, and we already lost one byte. The next interesting thing worth mentioning is that blocks are required to be exactly 16 bytes in size. So what happens if one of them is smaller than 16? Well, it gets padded up to this value. Obviously we could pad this using 0x00 bytes, but it is not a good solution in our case. PKCS#7 padding was used, which is much better, and here is how it works:

If we have a block with length 15, in such a case the used pad will be 0x01. For a block length of 14 this would be 0x02, 0x02 and so on. In general, the formula for this is N = (16 - blocklen) added to the plaintext N times. What does it mean for us in this case? Well, we have an oracle that informs us whether the padding is correct or not. Since we control the ciphertext we can try to force the last byte to be decrypted to 0x01, which would suggest padding of length 1. You can read how it works exactly on Wikipedia.

While trying all 256 values (0x00 - 0xff), we expect to see a message different than "Invalid padding", therefore we can obtain the correct value immediately. Then by XORing the expected value (0x01 in the first case) with the byte that we passed to decryption we recover the original plaintext byte. Once we have it we can switch to testing the previous byte, trying all 256 values and forcing the last byte to produce padding 0x02, continuing this process up to the beginning of the block.

Once we have it we can control the whole output, except the first block as it relies on the initialization vector. However in our case we know that the initialization vector was 0! So we can basically control the whole outcome of the decryption and align the response with FINAL_PASSWORD!

Oracle padding attack

I added automation for this particular attack to my pseudo-terminal, and it was constantly erasing memory, writing new values, testing them, and checking the response. Here is a screenshot from the beginning:

ss3

One cycle (erase + write + check) took 13 seconds. In the worst case this would take 13 * 16 * 256, which is statistically very unlikely, but it would be around 14 hours. In my case it took around 9 hours. And there's the end of our attack;

ss4

Takeouts

This challenge is a great example of how dangerous padding oracles can be in practice. Even though strong cryptography like AES was used, improper handling of padding errors completely broke the security of the system. The key issue here was the clear distinction between valid and invalid padding, which effectively exposed an oracle. This allowed us to iteratively recover the plaintext without knowing the encryption key. Another important takeaway is that even a single corrupted byte does not necessarily make the data unrecoverable. With the right attack strategy, we were able to fully reconstruct the message and control the decryption output. Finally, this challenge highlights that implementation details matter just as much as the cryptographic primitives themselves. Secure algorithms can still lead to vulnerable systems if they are used incorrectly. Always remember: never expose padding validation results directly, and prefer authenticated encryption modes such as AES-GCM over raw CBC.

Thanks for reading!

Comments (0)

There is no comments yet, add first!

Please log in to add a comment.

X