Ginseng: keeping secrets in registers when you distrust the operating system Yun & Zhong et al., NDSS’19
Suppose you did go to the extreme length of establishing an unconditional root of trust for your system, even then, unless every subsequent piece of code you load is also fully trusted (e.g., formally verified) then you’re open to post-boot attacks. This is especially true in a context where lots of third-party application code (e.g. apps on a mobile phone) gets loaded.
Many mobile and IoT apps nowadays contain sensitive data, or secrets, such as passwords, learned models, and health information. Such secrets are often protected by encryption in the storage. However, to use a secret, an app must decrypt it and usually store it as cleartext in memory. In doing so, the app assumes that the operating system (OS) is trustworthy. OSes are complex software and have a large attack surface… Increasingly abundant evidence suggests that prudent apps should not trust the OS with their secrets.
Instead of trying to protect absolutely everything, Ginseng assumes that some data matters more than others. It arranges things such that this sensitive data is only ever in the clear in registers (never memory), and is saved in an encrypted memory region called the secure stack on context switches. Using a small trusted service that runs in a Trusted Execution Environment (TEE), for example, Intel SGX or as in this paper, ARM’s TrustZone, Ginseng can bypass the operating system for secure data transport to and from registers, ensure code integrity for functions handling sensitive data, and provide control-flow integrity for sensitive data itself.
The evaluation use cases give a good sense for how Ginseng might be used:
- In a onetime password generator (OTP) app for two-factor authentication, the shared secret key can be protected.
wpa_supplicant, an OSS library commonly used to connect to wireless networks using WPA , reads a cleartext password from a configuration file or network manager and stores in in memory. Ginseng can keep that password protected.
- In Nginx, the master key for TLS is kept in memory by OpenSSL. With Ginseng the master key can be protected, and session keys derived from it, without ever storing the master key in memory.
- In a decision-tree based learned classifier, the criteria at each node of the decision tree can be protected.
How Ginseng works
Developers annotate variables and parameters to be protected as
sensitive, and the Ginseng compiler then ensures these variables stay protected either in registers or in the secure stack. The code injected by the compiler talks directly to the Ginseng Service running in a trusted execution environment.
In addition to the availability of a TEE, Ginseng requires that direct calls from an app to the higher privilege mode (Ginseng Service) bypass the OS, and that writes to virtual memory control registers can be trapped into the higher privilege mode. Both x86 and ARM meet those requirements.
Everything starts with the programmer determining the location of sensitive variables and call parameters in the code and annotating them.
The Ginseng compiler performs static taint analysis to figure out all variables that may carry sensitive information (e.g. the return value of a function taking a sensitive parameter). This analysis is conservative, so with knowledge of the code the programmer can also stop taint propagation using the
The compiler ensures that all sensitive variables are kept in registers. On a function call, the compiler emits instructions to send a request to the GService which encrypts the sensitive data and save it in the secure stack. After the function call, similar code is injected to restore the sensitive registers. It’s smart enough to only do this when the register actually contains sensitive data, versus being used for regular data at other points in the program. Registers for sensitive data are allocated before those for non-sensitive data, to avoid unnecessary spills of registers containing sensitive data (which incur higher overheads) when there aren’t enough registers available.
The static protection offered by the the Ginseng compiler is necessary but not sufficient to protect sensitive variables in the Normal world…
Ginseng also adds runtime protections to ensure code integrity, data confidentiality, and control-flow integrity.
Calls from the application to GService must bypass the OS (which is untrusted remember). On the ARM architecture this is achieved by triggering security violations through attempted writes to Secure world memory. GService allocates a unique Secure world memory address for each secure API, and configures an external abort (EA) handler to trap the EA in the secure world.
To ensure code integrity the kernel page table is made read-only at boot time. The kernel is modified to send a request to GService whenever it needs to modify the page table, GService honours this request only when doing so would not result in mapping the code pages of a sensitive function. The kernel is also prevented from overwriting its page table base register so that it can’t swap the table with a compromised one.
On first entry to a sensitive function, GService walks the kernel page table to ensure no mapping to the function’s code pages. Then it hashes the function code and compares it with the hash supplied and signed by the compiler. If both checks past, a function control block (fCB) is allocated inside GService so that future invocations can proceed immediately.
For data confidentiality, in addition to the call stack management described previously, Ginseng must also intercept all exceptions to save sensitive registers to the callstack before the exception can be handled by the OS. Ginseng intercepts exceptions using dynamic trapping. A NOP instruction is inserted at the beginning of the exception vector code in the kernel, and replaced by a call to the secure monitor at runtime when sensitive data enters registers. (Once the registers are clear of sensitive data, the NOP is restored). Once the OS serves the exception, control is handed back to the app. GService manages the return address to ensure we resume at the correct point in the sensitive function.
When sensitive data is in registers, Ginseng also ensure control-flow integrity. Function pointers are treated as sensitive and the integrity of the functions they point to is also checked.
Implementation and evaluation
The prototype implementation is built for ARM with the Ginseng compiler based on LLVM 6.0. Most of the implementation is written in Rust, with statically allocated memory for the heap.
… we miminize the unsafe part of GService to a small amount of assembly code, 190 lines in our implementation, which is amenable to formal verification by existing tools, e.g. Vale
Although we prototype Ginseng in an ARM-based device, we envision that adopting a different architecture could make the implementation simpler and lead to less overhead. With the hypervisor level of x86, for example, the secure APIs can be implemented with hypercalls, instead of security violation. Moreover, there is no need for forwarding the kernel page table modiﬁcation trapped in the hypervisor to a higher level, i.e., the Secure world. Finally, dynamic exception trapping will not be necessary because the hypervisor can directly intercept exceptions.