![BP-14](/sites/crosscon/files/public/styles/large_800/public/content-images/media/2024/BP-14.png?itok=7vJpxp1I)
Author: Alberto Tacchella, Post-doctoral at Uni. Trento
Improving the resilience of trusted applications with control flow integrity
One of the targets of the CROSSCON project is the development of foundational trusted services whose goal is to support the various security-relevant activities to be performed on IoT devices. In this blog post we analyze in detail one of these services, implementing a control flow integrity facility for applications running on the CROSSCON bare-metal Trusted Execution Environment.
We begin with a brief introduction to the general notion of control flow, followed by a discussion of the security hazards stemming from the possibility of altering the regular control flow of a running application. Finally, we discuss the CROSSCON approach to mitigate this security risk in the case of IoT applications running on hardware-constrained devices.
Control flow
In general, the term control flow refers to the order in which the individual instructions of a program are executed. Each application has its own control flow structure, which is dictated by the underlying domain logic and the way in which the application implements its functionalities. This control flow structure can be represented by a graph, which is called the application control flow graph (CFG).
Each run of the application determines a particular path in the control flow graph. It is important to note that this path cannot be determined ahead of time in general, since the sequence of instructions to be executed typically depends on data which is only available at runtime.
At the lowest level, namely the level of machine language, only two kinds of control flow instructions are usually available: conditional or unconditional branch instructions (also known as jumps), and function call/return instructions. Function calls are managed using a call stack, a data structure in memory that stores information about all the subroutines in execution at each point in time.
Subverting the regular control flow
A malicious actor can attack an application by trying to alter its regular flow of execution. A typical instance is seen in stack-based buffer overflows, also known as stack-smashing attacks. In this kind of exploit, the attacker tries to overwrite the return address of a procedure call stored in the stack to point to a piece of code provided by the attacker. Once the targeted function returns, the execution will resume not at the site of the original call, but at the location selected by the attacker.
An evolution of stack-smashing attacks, which remains viable even in the presence of advanced security features like the no-execute bit, has been called Return Oriented Programming (ROP). In a ROP attack the malicious code is not injected directly by the attacker (which would trigger the no-execute policy on the stack contents); instead, it is built using instruction sequences terminating with a return instruction (called “gadgets”) that are already present in the executable part of the memory, chained together by manipulating the return addresses stored in the stack.
To mitigate these kinds of attack, the notion of Control Flow Integrity (CFI) has been introduced in an influential paper [1] by Martin Abadi and co-workers. In this paper two mechanisms are proposed in order to protect both forward (function call) and backward (function return) control flow transfers.
- Forward edge protection is achieved using a statically-determined lookup table. The control flow graph of the application is precomputed and stored in the form of a jump table. Each indirect jump or call is then instrumented with runtime checks to enforce that the destination address is present among the possible targets in the table.
- Backward edge protection is achieved using a shadow stack. On each function call, the return address is saved in a copy of the call stack, called a shadow stack, which is stored in a safe memory region. On executing a return instruction, the return address found on the stack is compared with the corresponding value on the shadow stack to ensure that it has not been altered.
The CROSSCON solution
The CROSSCON project is particularly concerned with small and low-end devices running on resource-constrained hardware. In this kind of devices, applications typically run on the “bare‐metal” hardware, i.e. without the presence of an underlying kernel/OS.
To address the security needs of these applications, a Trusted Execution Environment specifically designed for bare-metal devices (BareTEE) has been developed. A first version (BareTEE-noMPU) is suitable for very basic devices lacking even a Memory Protection Unit (MPU), whereas a second version (BareTEE-MPU) can rely on the presence of such a unit.
The BareTEE comes equipped with a Control Flow Integrity service. For both versions, the service is based on a software implementation of the shadow stack concept, which provides backward edge protection for control flow transfers, preventing ROP-style attacks. In addition, the MPU version of the service also provides forward edge protection, thereby guaranteeing a greater coverage of possible attack vectors.
Conclusions
Securing IoT devices is a complex task that requires multiple approaches to be deployed at the same time, in order to block all the possible threats to which Trusted Applications are exposed. Control Flow Integrity is a time-honoured technique to prevent a whole class of attacks from compromising the correct execution of TAs. The implementation of a CFI primitive in the CROSSCON stack will definitely have a positive impact on the security of applications running on the resource-constrained devices which are typical in the IoT world.