Stage 1

Welcome to my guide for the first stage of the Sword of Secrets CTF / Hardware Escape Room. This stage can be done with a physical Sword of Secrets, or by loading the Virtual Challenge in your preferred web browser.

I’ve tried to split the sections into background, hints, and full-on spoilers. However, it is still recommended that you explore the challenge on your own before reading anything here, as even the review section spoils some of the basic things one must learn to interact with the Sword.


Review


Review: Expectations for Stage 1

Review: Expectations for Stage 1

  • You interact with the Sword via a terminal (USB serial port; virtual challenge has a web-based CLI).
  • It is expected that you will review the source code, and thus find the commands that can be sent via the terminal, and what they do. See prior post for help with that.
  • It is also expected that you will have the datasheet for the SPI flash chip, which shows what commands are supported by that chip.
  • Term definitions:
    • Cleartext - original unencrypted data
    • Plaintext - the padded (or otherwise processed) original unencrypted data that is fed into the encryption algorithm.
    • Ciphertext - the encrypted plaintext data

Review: Verify communication with SPI flash chip

Review: Verify communication with SPI flash chip

First thing to do is very basic connectivity to the SPI flash chip. Let’s try to send the 9Fh command, to get the chip ID from the chip. This requires a sequence of commands:

  1. BEGIN – this enables the SPI controller, preparing it for sending and receiving data transfers to / from the SPI flash chip.
  2. ASSERT – tells the flash chip to pay attention to the data line … essentially enabling the chip (/CS)
  3. DATA 9F – Sends the command bytes to the SPI flash chip. Returned data should be zeros.
  4. DATA 0 0 0 – Sends dummy bytes via SPI, allowing the flash chip to send back the data for the command.
  5. RELEASE – tells the flash chip to stop paying attention to the data line. (/CS)
  6. END – disable the SPI controller

At step 4, you should see the flash chip’s ID.

Example console log from website version:

>> BEGIN

>> ASSERT

>> DATA 9F
00 

>> DATA 0 0 0
ef 40 18 

>> RELEASE

>> END

Review: Source Code

Review: Source Code

Start with parsing of your input. You’ve found the commands, so next is to figure out which code is run when you type SOLVE? Which code is relevant to this first challenge?

Review: Binary Algebra

Review: Binary Algebra

Using a single bit value, XOR is defined simply as true when one or the other inputs is true, but false if both inputs are true. One interesting facet is that XOR is reversible … which allows the xcryptXor() function to perform the same process for both encryption and decryption.

XOR provides a few nice features that are always true:

  • X ^ X === 0
  • A ^ B ^ B === A, for all values of A and B.
  • If A ^ B === C, then A === C ^ B and B === C ^ A

That last one is actually a couple of steps. Let me give one in more detail:

  • A ^ B === C – starting point
  • A ^ B ^ B === C ^ B – XOR both sides by B
  • A ^ 0 === C ^ B – Replace B ^ B with zero
  • A === C ^ B

We will use some of these characteristics of XOR for this challenge.


Hints


Hint: Source Code

Hint: Source Code

We’re focusing on the first challenge, so the relevant code is in palisadeSetup() and palisade() Take a moment to go through those, and also review the function, xcryptXor().

Hint: Problem to be Solved

Hint: Problem to be solved

Your job is to get palisade() to consider the data stored on the flash valid. If it considers the data valid, it will then print the cleartext message, allowing you to continue to the next challenge.

In palisadeSetup(), the cleartext message is defined as:

char message[] = FLAG_BANNER "{No one can break this! " S(PARAPET_FLASH_ADDR) "}";
// which becomes:
char message[] = "MAGICLIB{No one can break this! " S(PARAPET_FLASH_ADDR) "}";
//                ....-....1....-....2....-....3..  ?????????????????????  ..
char message[] = "MAGICLIB{No one can break this! 0x*****}";
//                ....-....1....-....2....-....3....-....4 == 40 bytes (0x38)

If the ciphertext is written to flash, then palisade() would decrypt it, detect the FLAG_BANNER aka "MAGICLIB" at the start of the plaintext, and as a result would print the plaintext and consider the stage successfully completed.

However, the first four bytes of the ciphertext, in order to make this a challenge, get intentionally corrupted just before being written to flash:

// Oh noes! Something happened... X_X
*((uint32_t *)(message)) = 0x00000000; // random(0xffffffff);

Therefore, to solve the challenge, one must fix the ciphertext on the flash by figuring out what should have been in those four byte, and writing the corrected data to that sector of the flash.


Walkthrough (spoilers)


Wrapper of spoilers

What is secret? What is not?

What is secret? What is not?

For stage 1, the address where stage 1 (palisade) data is stored is NOT a secret. In fact, the opening screen flat out tells you that you are at [spoiler]0x10000[/spoiler].

In addition, FLAG_BANNER is not a secret … it’s defined as the eight-byte value "MAGICLIB" in the source code.

Reviewing palisadeSetup(), the message in is converted to ciphertext by xcryptXor(), and the ciphertext is then written to the stage 1 flash address 0x10000.

Read the stage 1 ciphertext

Read the stage 1 ciphertext

Console log showing the 0x29 bytes of ciphertext generated by palisadeSetup(). There may be more bytes, as one cannot differentiate between data of 0xFF vs. unused flash.

That’s Ok, we actually need quite a bit less data to solve this challenge.

>> BEGIN

>> ASSERT

>> REM send read (03h) for 0x010000

>> DATA 03 01 00 00
00 00 00 00 

>> REM following lines read 16 bytes each

>> DATA 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
00 00 00 00 0e 05 13 07 36 0f 37 69 22 27 3f 65 

>> DATA 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
2e 20 36 69 2f 3b 3f 24 26 61 2c 21 24 3a 7b 65 

>> DATA 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
7d 39 6a 79 7d 79 6a 38 4d ff ff ff ff ff ff ff 

>> RELEASE

>> END

Exploiting weakness to get xor_key

Exploiting weakness to get `xor_key`

Let:

  • P stands for plaintext
  • C stands for ciphertext
  • K stands for the key

The xorcrypt() function simply XOR’s each byte with the next byte of the key, looping back to the first byte of the key after using the last byte of the key.

In other words, P ^ K === C. Using the algebra trick for XOR describe earlier, this means P ^ C === K.

The plaintext, P is nearly completely known. Out of 0x29 values, only five bytes near the end (as the flag) have unknown values. I’ll use underscores to indicate unknown values:

00000000 4D 41 47 49 43 4C 49 42 7B 4E 6F 20 6F 6E 65 20  MAGICLIB{No one
00000010 63 61 6E 20 62 72 65 61 6B 20 74 68 69 73 21 20  can break this!
00000020 30 78 __ __ __ __ __ 7D 00                       0x?????}

Similarly, the ciphertext C is nearly completely known, except for the first four bytes that were intentionally corrupted.

00000000 __ __ __ __ 0E 05 13 07 36 0F 37 69 22 27 3F 65
00000010 2E 20 36 69 2F 3B 3F 24 26 61 2C 21 24 3A 7B 65
00000020 7D 39 6A 79 7D 79 6A 38 4D

Using the knowledge that P ^ C === K, let’s XOR the plaintext and ciphertext, tracking the unknown / corrupted spots with underscores, and see if we can discover the repeated key that’s in use:

C 00000000 __ __ __ __ 0E 05 13 07 36 0F 37 69 22 27 3F 65
P 00000000 4D 41 47 49 43 4C 49 42 7B 4E 6F 20 6F 6E 65 20  MAGICLIB{No one
K 00000000 __ __ __ __ 4D 49 5A 45 4D 41 58 49 4D 49 5A 45  ????MIZEMAXIMIZE
                                   ^^^^^^^^^^^^^^^^^^^^^^^          \______/

C 00000010 2E 20 36 69 2F 3B 3F 24 26 61 2C 21 24 3A 7B 65
P 00000010 63 61 6E 20 62 72 65 61 6B 20 74 68 69 73 21 20  can break this!
K 00000010 4D 41 58 49 4D 49 5A 45 4D 41 58 49 4D 49 5A 45  MAXIMIZEMAXIMIZE
           ^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^  \______/\______/

C 00000020 7D 39 6A 79 7D 79 6A 38 4D
P 00000020 30 78 __ __ __ __ __ 7D 00                       0x?????}
K 00000020 4D 41 __ __ __ __ __ 45 4D                       MA?????EM

Because we had 30 consecutive bytes where we knew both the plaintext and the ciphertext, it was easy to recover the secret xor_key.

Generating the corrected ciphertext

Generating the corrected ciphertext

As you recall, palisadeSetup() overwrote the first four bytes of ciphertext with zero bytes. To solve this challenge, one needs to fix the first four bytes on the flash.

Now that we know the key and the plaintext of those four bytes, and we know that P ^ K = C, this is trivial:

Plaintext  4D 41 47 49 43 4C 49 42 7B 4E 6F 20 6F 6E 65 20  MAGICLIB{No one
xor_key    4D 41 58 49 4D 49 5A 45 4D 41 58 49 4D 49 5A 45  MAXIMIZEMAXIMIZE
RESULT     00 00 1F 00 0E 05 13 07 36 0F 37 69 22 27 3F 65

Great! We replace the first four bytes of 00 00 00 00 with the corrected value 00 00 1F 00.

Thus, if we write the following data to the flash chip at 0x10000, palisade() should be happy!

00000000  00 00 1F 00 0E 05 13 07 36 0F 37 69 22 27 3F 65
00000010  2E 20 36 69 2F 3B 3F 24 26 61 2C 21 24 3A 7B 65
00000020  7D 39 6A 79 7D 79 6A 38 4D FF FF FF FF FF FF FF

Writing to SPI Flash

Writing to SPI Flash

Read through the table listing all the commands the W25Q supports. Notice the write command (02h = PROGRAM PAGE).

That won’t work (yet) because flash can only shift bits from 1 -> 0. Therefore, to write the updated data, we’ll first have to erase that area of the flash, which will reset it to 0xFF.

If you’ve not played with flash before, note that while any bit can be changed from 1 -> 0 at any time, erase operations operate on larger sections at a time.

In this case, we will be erasing 4k (0x010000 bytes) at a time. Thus, erasing the first four bytes will also erase all the other ciphertext for this challenge. In other words, you’ll want to keep a copy of the data before erasing it.

First try erasing the sector

First try erasing the sector

Let’s send the Sector Erase command, and then re-read the data. Here’s a console log:

>> BEGIN

>> REM send SECTOR ERASE (20h)

>> ASSERT

>> DATA 20 01 00 00
00 00 00 00 

>> RELEASE

>> REM send READ DATA (03h)

>> ASSERT

>> DATA 03 01 00 00
00 00 00 00 

>> DATA 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
00 00 00 00 0e 05 13 07 36 0f 37 69 22 27 3f 65 

>> DATA 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
2e 20 36 69 2f 3b 3f 24 26 61 2c 21 24 3a 7b 65 

>> DATA 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
7d 39 6a 79 7d 79 6a 38 4d ff ff ff ff ff ff ff 

>> RELEASE

>> END

Bummer, the erase didn’t work, since the data is still there. What went wrong?

Erasing the sector, take two

Erasing the sector, take two

Looking at the datasheet, are there other commands in that table that might need to be looked at?

Status registers allow various ways to lock the pages so they cannot be written … but SR1/SR2/SR3 read as 00h/02h/00h, which means the flash isn’t write protected.

Read the descriptions PAGE PROGRAM 02h command. Aha! Check out the [spoiler]ENABLE WRITE (06h)[/spoiler] command. Let’s try that. Here’s another console log showing the commands:

>> BEGIN

>> REM send that command

>> ASSERT

>> DATA 06
00 

>> RELEASE

>> REM immediately send SECTOR ERASE (20h)

>> ASSERT

>> DATA 20 01 00 00
00 00 00 00 

>> RELEASE

>> REM and verify the data is gone

>> REM send READ DATA (03h)

>> ASSERT

>> DATA 03 01 00 00
00 00 00 00 

>> DATA 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF

>> DATA 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF

>> DATA 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF

>> RELEASE

Success! That sector is now erased, and all bytes are 0xff.

Writing the updated data

Writing the updated data

So the flash is successfully erased. By now, you should have this well in hand. Note that the ENABLE WRITE command has to be sent prior to EVERY write … because it auto-disables after each such write (at least on the real physical product … the virtual challenge may have a bug in this area.)

Here’s a console log writing the updated data:

>> ASSERT

>> DATA 02 01 00 00
00 00 00 00 

>> DATA 00 00 1F 00 0E 05 13 07
00 00 00 00 00 00 00 00 

>> DATA 36 0F 37 69 22 27 3F 65
00 00 00 00 00 00 00 00 

>> DATA 2E 20 36 69 2F 3B 3F 24
00 00 00 00 00 00 00 00 

>> DATA 26 61 2C 21 24 3A 7B 65
00 00 00 00 00 00 00 00 

>> DATA 7D 39 6A 79 7D 79 6A 38
00 00 00 00 00 00 00 00 

>> DATA 4D FF FF FF FF FF FF FF
00 00 00 00 00 00 00 00 

>> RELEASE

>> END

Solve!

Solve!

Use the SOLVE command to see if palisade() considers your data a correct solution.



FIN

If you enjoyed this first stage, and were using the virtual challenge website, consider picking up the physical product … the remaining challenges are more difficult, while each builds upon the prior challenges. Along the way, you’ll learn about historical mistakes in cryptography (and how to exploit those mistakes), how to exploit bugs in script interpreters, and more!