This is an old revision of the document!
For a long time, hardware had a central role in computer security. Take, for example, the CPU's protection rings model (on x86): they realize a privilege separation between a hypervisor / Operating System kernel and the user applications and is enforced at hardware-level for efficiency.
Nowadays, the security requirements of certain applications has led to the implementation of additional access control or cryptographic functions directly into the hardware, e.g., AES-NI / SHA SSE-based instructions, the Trusted Platform Module cryptoprocessor, smart cards or Trusted Execution Environments (ARM TrustZone, Intel SGX, memory encryption etc.).
On a different note, hardware is also susceptible to security bugs: side channel attacks, cryptographic vulnerabilities (e.g., cache or timing attacks or the much recent Spectre / Meltdown speculative execution bugs), hardcoded credentials or even manufacturer-introduced backdoors. These are very difficult (or even impossible) to fix without re-designing the chip and replacing the faulty products.
A side channel attack is … TODO (să nu fim ca Ponta :P).
Trusted execution environments are special CPU areas where programs run with hardware-level protections against other (even privileged) components. The platform ensures memory and CPU context isolation against powerful adversaries, which can be used to provide better security for sensitive applications by minimizing their attack surface (e.g., by regarding the Operating System as insecure / untrusted).
Intel Software Guard Extensions (SGX) is a novel TEE technology aiming secure code execution inside enclaves, available starting with Skylake CPUs. An enclave is a protected region of memory that can only be accessed by the owning application (not even the kernel or other peripherals - e.g. DMA - is allowed to read it). It can provide remote attestation (i.e., proof of trusted execution) to services that require code to be executed on untrusted devices.
SGX applications are divided into two logical components: trusted and untrusted. Intel advises that the trusted component (the enclave) should be as small as possible (smaller attack surface == less probability of bugs), enforced by a hardware limit of 128 MB for the entire protected memory space.
The enclave code runs in user space, and thus can access the entire memory of the process within which it runs. In this way, data can be transmitted between the trusted and untrusted application regions, e.g. for passing function parameters and results. An aspect worth mentioning is that, inside the enclave, one can not directly execute priviledeged operations (such as system calls). For this, one needs to execute an untrusted function.
For the interaction between those two components, the SDK exposes two important concepts:
These functions represent the way the untrusted application part and the enclave interact. They are defined in an EDL file (Enclave Definition Language), in a specific format. A simple EDL file looks like this:
enclave { trusted { /* define ECALLs here. */ public int get_sum(int a, int b); }; untrusted { /* define OCALLs here. */ void ocall_print([in, string]const char* str); }; };
ECALL and OCALL functions parameters that are pointers should be decorated with either a pointer direction attribute in, out or a user_check attribute explicitly.
Observation: Using direction attributes ([in], [out]) implies copying buffers from the application to enclave or the other way around, so usually these are followed by a size attributes that indicates how much data needs to be copied, e.g.:
void foo([in, size=buf_len]const char* buf, size_t buf_len);
Ideally, you should solve this laboratory on openstack. As in the first week, create an m1.small instance from the ISC 2020 image. If you are outside the campus local network, you will need to set up a 2-hop SSH connection via fep. If you have configured your localhost's keypair on openstack (not the one you generated on fep), you can connect directly this way:
$ ssh -J ${LDAP_ID}@fep.grid.pub.ro student@${VM_IP}
NOTE: you can have more than one keypair configured (both for your localhost, and for fep). Select the one you want from the Key Pair tab during the instance creation.
Note: UPB's OpenStack servers currently have very old CPUs (Core 2 Duo!!!) lacking modern cryptographic instructions! We will need to work around it by using qemu user-mode emulation (version >= 4 required, so we need to install it beforehand):
wget http://archive.ubuntu.com/ubuntu/pool/universe/q/qemu/qemu-user-static_4.2-3ubuntu6.21_amd64.deb sudo dpkg -i qemu-user-static_4.2-3ubuntu6.21_amd64.deb # test it? qemu-x86_64-static --version
qemu-x86_64-static
when running them!
Normally, to benefit from most SGX security features, you need two things:
SGX projects can be compiled in both hardware and simulation mode. Since we will be working in simulation mode, we won't require the SGX driver, the BIOS support, or even the ENCLU instructions that implement the SGX functionality. The SDK is already installed on your VM.
In order to be able to use the SDK, you need to import/update some environment variables:
# do this in every shell you open (maybe add it to .bashrc) source /opt/intel/sgxsdk/environment
Use the following Makefile targets for compiling / cleaning the workspace:
$ make SGX_MODE=SIM $ make clean
(expand this if you want to use a local machine)
Since you'll be working inside a headless machine, you will need some vim skills to effortlessly edit the code; don't worry! there are plenty of docs on the Internet ;)
Download the side channel demo archive here.
Implement the TODOs and crack the password via a timing attack ;)
First, download the SGX skeleton here. The provided code archive contains TODOs that will guide you through solving the tasks. Over the course of the lab, make sure to consult the documentation.
The function should have the prototype:
unsigned int generate_random_number(void)
Steps
sgx_status_t sgx_read_rand(unsigned char *rand, size_t length_in_bytes);
Where:
void ocall_write_file(const char* filename, const char* buf, size_t buf_len); void ocall_read_file(const char* filename, char* buf, size_t buf_len);
sgx_status_t sgx_seal_data( const uint32_t additional_MACtext_length, ---> no additional MAC, this parameter should be 0 const uint8_t * p_additional_MACtext, ---> no additional MAC, this parameter should be NULL const uint32_t text2encrypt_length, ---> length of the text to be encrypted const uint8_t * p_text2encrypt, ---> the text to be encrypted (make sure the type of this parameter is uint8_t* - you can use casts) const uint32_t sealed_data_size, ---> length of the output (encrypted text), must be computed using sgx_calc_sealed_data_size (see below) sgx_sealed_data_t * p_sealed_data ---> output of the function; you must dynamically allocate this prior to this function call );
. Use sgx_calc_sealed_data_size to calculate the number of bytes to allocate for the sgx_sealed_data_t structure and to use for the sealed_data_size parameter.
uint32_t sgx_calc_sealed_data_size( const uint32_t add_mac_txt_size, ---> no additional MAC, this parameter should be 0 const uint32_t txt_encrypt_size ---> length of the text to be encrypted );
void seal_secret(void);
sgx_status_t sgx_unseal_data( const sgx_sealed_data_t * p_sealed_data, ---> encrypted data (read from the file); you must cast the buffer read from the file to sgx_sealed_data_t * uint8_t * p_additional_MACtext, ---> no additional MAC, this parameter should be NULL uint32_t * p_additional_MACtext_length, ---> no additional MAC, this parameter should be 0 uint8_t * p_decrypted_text, ---> buffer where the decrypted data will be written; you can allocate it statically, just make sure you provide a large enough buffer uint32_t * p_decrypted_text_length ---> this parameter is both input and output; as input, it represents the length of p_decrypted_text; as output, represents the number of bytes that were decrypted );
void unseal_secret(void);
Please take a minute to fill in the feedback form for this lab.