Trust the Source, Luke

Published on . Tagged with c++, security.
Yoda

It is a common belief that the only documentation a programmer should rely on is the source code. Some experienced engineers argue that one should only trust in the disassembly. However, I'd like to present a case where neither of these beliefs holds true.

Consider this kernel code:

int x;
void kernel_func(int index) {
    if (index < public_bounds) {
        x = x ^ detector[secret[index] * SOME_BIG_NUMBER];
    }
}
void kernel_func(int) PROC
    push    ebp
    mov     ebp, esp
    mov     eax, DWORD PTR _index$[ebp]
    cmp     eax, DWORD PTR int public_bounds
    jge     SHORT $LN1@kernel_fun
    mov     ecx, DWORD PTR _index$[ebp]
    mov     edx, DWORD PTR int * secret[ecx*4]
    imul    edx, DWORD PTR int SOME_BIG_NUMBER
    mov     eax, DWORD PTR int x
    xor     eax, DWORD PTR int * detector[edx*4]
    mov     DWORD PTR int x, eax
$LN1@kernel_fun:
    pop     ebp
    ret     0

Could we obtain the value of the secret[index] for indices outside of public_bounds, if we have only access to index, detector[] and the value of SOME_BIG_NUMBER? According to the source code and disassembly we should not, right?

Right?

Welcome to hardware land! CPU hates idleness. If there is a cache miss in the branch condition index < public_bounds, which would make CPU stall for a while, CPU can calculate x = x ^ detector[secret[index] * ...] doing speculative execution. If the condition, fetched from memory, turns out to be false, the CPU simply doesn't commit the speculative result. So, what's the issue? The problem arises because the CPU can fetch detector[secret[index] * ...] into cache, even if the branch condition is not met!

So, in the user process, we could've evicted whole cache, executed kernel_func making CPU do branch prediction (by running it with a valid index a few times beforehand), and then executed:

auto start = __rdtsc();  // high-precision timer
auto tmp = detector['A' * SOME_BIG_NUMBER];
auto end = __rdtsc();
auto diff = end - start;

In this manner, we could have determined if secret[index] was 'A' simply by measuring the time difference (a small time indicates it was fetched into the cache by speculative execution). We could repeat the entire process for 'B', 'C', 'D', etc., and voilĂ !

This vulnerability, called Spectre/Meltdown, has been described in more detail in "Reading privilaged memory with a side-channel" by Jann Horn.

Be careful where you place your trust.