Saarsec

saarsec

Schwenk and pwn

RealWorldCTF 5th - SealUnseal

SealUnseal

Thanks

Shoutout to Linus S. (aka PistonMiner) from ENOFLAG for collaboration on this writeup! He wrote the thorough description on CPUSVN!

Background

We’ve been playing with Sauercloud in the RealWorldCTF 5th Qualifiers, a CTF famous for its difficulty. It’s the evening of the second day, and Sauercloud is still battling with Blue Water and PPP for the crown. During all that commotion I feel frustrated because I haven’t been able to contribute anything yet. But then the organizers drop the last challenge: SealUnseal.

The project demonstrates sealing/unsealing data between two enclaves.

I can’t believe it. It’s an Intel-SGX challenge, which is something I spend the last 2 months researching on. Let’s do this.

Preliminaries

The challenge provides us with 4 files:

We are dealing with an Intel(R) Software Guard Extensions(SGX) Enclave. Enclave are areas of memory for secure computation. If setup properly, even a user with administrator privileges cannot access enclave memory. In case of the challenge we are dealing with Sealing: The ./app used the enclave libenclave_seal.signed.so to store some secret data into sealed_data_blob.txt with a key unique to that enclave. This key can only be generated by that enclave on that particular platform. More information: https://sgx101.gitbook.io/sgx101/sgx-bootstrap/sealing

Approach

We assume the goal is to use the libenclave_unseal.signed.so to unseal the flag.

We follow the steps on the github repository https://github.com/intel/linux-sgx to install SGX. Important to note is that only a few Intel CPU’s actually have SGX hardware support (a list can be found on https://github.com/ayeks/SGX-hardware). Inside the linux-sgx directory we find the interesteng example enclave SampleCode/SealUnseal. After building this enclave with make we find the files app, libenclave_seal.signed.so, libenclave_unseal.signed.so, and sealed_data_blob.txt inside this directory. The filenames are the same as in the challenge! Running the ./app yields:

Sealing data succeeded.
Unseal succeeded.

It does things! But we would like to see what has been sealed, so we can get the flag later. For that we need Ocalls.

Ocalls and Ecalls

The secret data is completely processed in Enclave memory. Only encrypted data makes it into sealed_data_blob.txt. While it’s possible to read enclave memory if the enclave is built in DEBUG mode, we would like to just print enclave memory. The problem with enclaves is that they don’t have syscalls to use printf, puts, write and similar to print out enclave memory.

Solution: An OCALL is an API for interaction between an untrused userspace application and the secure Enclave. We can just define an OCALL that takes a string and prints it out. For that we need to do three things.

  1. Implement the Ocall in App/app.cpp:
  2. Write down the function definition in Enclave_Unseal/Enclave_Unseal.edl
  3. Call the Ocall in Enclave_Unseal/Enclave_Unseal.cpp

1. Implementation of Ocall in app.cpp

1
2
3
4
void ocall_print_string(const char *str) {
    //Writes to stdout, requires 

    write(1, str, strlen(str));
}

We also need to remember to #include <unistd.h> and comment out the part that does the sealing in main().

2. Function definition in Enclave_Unseal.edl

The Enclave Definition Language EDL, is a language to prototype OCalls. The Edger8r tool will build the headers required so the enclave can call these functions. Luckily the Makefile handles all that so we only need to write the EDL file

1
2
3
4
    untrusted {
        /* define OCALLs here. */
        void ocall_print_string([in, string] unsigned char *str);
    };

We just tried out a few things until we got the syntax right.

3. Call Ocall in Enclave_unseal.cpp

At the end of the function unseal, the secret data should be inside decrypt_data. Therefore, we just call our ocall:

1
2
3
4
5
6
7
8
sgx_status_t unseal_data(const uint8_t *sealed_blob, size_t data_size){
    ...
    ocall_print_string(decrypt_data); // Print out secret data!

    free(de_mac_text);
    free(decrypt_data);
    return ret;
}
    

Using make we can see the unsealed data: "Data to encrypt" (how original!)

Now we copy the sealed_data_blob.txt that contains the flag into our folder and …

Error: Invalid CPUSVN.
Failed to unseal the data blob.

CPUSVN

Deciphering this error, an SVN is apparently a security version number. The general idea seems to be to prevent vulnerabilities in prior versions from compromising higher versions. Intel describes this as

The caller may not request a key for an SVN beyond the current CPU, ISV or enclave configuration’s SVN, respectively. https://www.felixcloutier.com/x86/egetkey

The exact details of how this works are unclear as this is largely undocumented by Intel and they treat it as an opaque 16 byte blob. Contained within there seem be six one-byte version numbers, with higher numbers being newer. These apparently correspond to version numbers of individual components of the platform, such as the microcode version. 0xff and 0x80 seem to be reserved as special values, however their exact meaning remains unclear. The key factor here is that a platform with a lower, older CPUSVN (so a potentially compromised version) cannot request a key from a platform with a higher, newer CPUSVN. What exactly “lower” and “higher” means here is not exactly clear, but from experiments the answer seems to be that all six numbers in the CPUSVN requested when passed to EGETKEY must be less than or equal to the current platform’s CPUSVN. As a consequence, we can only run this code on a platform which has a CPUSVN that has a matching or newer CPUSVN than the platform the flag was encrypted on. This turns out to be rather hard.

The flag blob’s CPUSVN seems to be 5, 5, 8, 9, 255, and 255. On all devices we tested, we could not find any with a CPUSVN that would match or exceed this. An additional complication is rendered by the fact that SGX was deprecated and is no longer available on the newest Intel chips: it seems to have been removed around 10th generation processors. So we need an Intel chip that is new, but not too new. Furthermore, the only way to tell what CPUSVN a particular platform has seems to be going through all the prerequisite steps necessary to run SGX code (which is rather complicated and involves compiling a bunch of things) and then checking. All of this combined meant that we could not find a single platform that could request the key to this blob.

Note

As we found out afterwards, in general, chip (as in, the physical piece of silicon you own) specific secrets seem to be used in the key derivation process, which means even if we found a suitable platform, it’s possible that we would have been unable to decrypt the secret. Having said that, the organizers deployed two different VMs in two different regions but with the same sealed blob file, which would be a strong indicator that different dies can derive the same keys without chip specific secrets involved; however this may also be an artifact of the virtialization setup involved. We’re honestly not sure, and it doesn’t help that Intel’s documentation on these things is difficult to read at best and simply nonexistent at worst.

Final solve

After teams complained, the CTF-organizers provided their own infrastructure with the correct CPU, so we could solve it. The steps were:

  1. Pack our SealUnseal folder into a tarball
  2. Upload it to transfer.sh so we can easily access it from their server
  3. Download our tarball, rebuild the Enclave and win!

We actually got first blood for this challenge and it has been solved by only 3 out of 632 teams participating.

1
2
3
4
5
6
7
8
9
10
# Download our Enclave project that we created earlier

curl https://transfer.sh/rcjbXt/SealUnseal.tar.gz > SealUnseal.tar.gz
tar -xvf SealUnseal.tar.gz
# Copy the sealed flag into the folder

cp *txt -t ./SealUnseal
cd SealUnseal
# rebuild the enclave

make clean
make
./app

rwctf{N1ce_to_m33t_confidentail_computing_lol}