Security Is Hard
On Hacker News, user jgrahamc writes:
If you take a narrow focus on a particular cryptographic event (such as your encryption of a string with an RSA public key) then you miss the greater story about encryption: it’s not just the individual cryptographic primitive that needs to be implemented correctly, it’s everything else.
I recently have been involved in putting a demo together of using Arm’s TrustZone for Microcontrollers This is based around putting “secure” operations within a trusted firmware app.
For this demo, we decided to implement the generation of JSON Web Tokens signed with a secret key that is kept on the secure side. Being only really a proof of concept (and naturally ending up short on time for a demo), there were a lot of known things missing from this:
- The secret is hardcoded into the code on the secure side.
- The secure side has no notion of secure time, so it will happily sign tokens with any desired validity.
It turns out that a couple of things missing from the implementation ended up making this call completely insecure, allowing a non-secure app to ask the token signer to give it any memory desired, even from the secure side.
The API
struct tfm_sst_jwt_t {
char *buffer; /* Buffer to write result, in NS memory. */
uint32_t out_size; /* Function will write bytes used here. */
uint32_t buffer_size;/* Available bytes in the buffer. */
int32_t iat; /* The current time. */
int32_t exp; /* The expiration time. */
char *aud; /* Token "aud" value. */
uint32_t aud_len; /* Length of audience string. */
};
The caller passes in a pointer to a buffer, along with some size information, the time stamps, and a field called “audience” that is copied into the “aud” value of the token.
Whenever a call is made to the secure side, the parameters are passed through a structure. Presumably, the call interface verifies that this structure is in valid non-secure memory. However, it doesn’t know that some of the fields are pointers, and also need to be validated to be pointers into non-secure memory.
In this particular case, it doesn’t validate the aud
parameter.
This means it is possible for the non-secure side to set this to point
to a secure address. Since the aud
value is copied directly into
the token, this can be used to copy secure memory directly into
non-secure (well, with some base64 encoding on the way). By setting
this to the address of the secret key in ROM, the token will contain
the secret key. As a short test, I tried just this, and the payload
part of the token that came back decodes to:
00000000 7b 22 61 75 64 22 3a 22 8f e2 47 03 9b 35 ec 6b
00000010 e0 8c 8b cb 53 b6 70 f6 f6 04 14 f2 cd e5 68 59
00000020 29 45 e8 5c 72 01 61 fb 26 5b 53 65 63 20 48 61
00000030 6e 64 6c 65 72 5d 20 25 73 5c 72 5c 6e 22 2c 22
00000040 65 78 70 22 3a 31 35 33 32 31 32 33 36 31 38 2c
00000050 22 69 61 74 22 3a 31 35 33 32 31 32 30 30 31 38
00000060 7d
This translates to {"aud":"...",...}
where the string after aud
contains the bytes of the private key, with the only change being that
a 0x0d was replaced by a "\r"
by the JSON encoder. The private key
is not null-terminated, so the token also grabs the string following
it in the ROM.
There is are a surprising number of things here that need to be fixed, and I only had to take advantage of a few of them to be able to read the key out of secure memory:
- The non-secure/secure interface needs to deeply verify pointers. The v8-m architecture contains an instruction that can be used to verify that an address should be accessible to non-secure memory (while running in secure mode), so this should be fairly straightforward to implement. However, without something like an IDL to define the interface, checking is ad-hoc, and becomes easy to miss.
- The audience is passed in as a pointer and a length. However, internal to the JSON library, it is treated as a null-terminated string. The pointer verification needs to take into consideration the length, but also needs to either verify the null-termination, or the JSON library be modified to allow the length to be specified. In this exploit example, it gave us access to some memory after the string, and could possibly be exploited by using a pointer near the boundary of secure/non-secure memory.
- Note that buffer is also not verified, so this API can also be used to coerce the secure side into writing values to arbitrary addresses. It is a little harder to exploit, because the text must start with a fixed pattern, as well as being base64-encoded. A fun challenge would be to come up with a stack overwrite attack written with code that only uses base64 characters.
- In a real system, the private key will not be in flash, but stored in some type of secure filesystem. However, even on the secure side, it should not be kept in memory until the moment it is needed, and then that memory cleared. In this JWT application, the key should not be retrieved until after the main part of the token is built. This way, any exploit that did allow secure memory to be copied into the token, at least wouldn’t have access to the secure key.
Conclusion
To conclude: security is hard. In some sense, the cryptography is the “easy” part, in that the algorithms are widely understood with implementations available that have undergone analysis, and hopefully have had problems such as side-channel attacks resolved in them.
However, this is only a small part of making a system secure. Maybe we’re properly generating an ECDSA signature (which, BTW, we are not, but that’s another post), but if some part along the whole chain doesn’t check an address, the private key can be leaked, which renders all of the security worthless.