cfi

motivation: beyond stack canaries

  • stack canaries: try to make sure we return to genuine place
  • should make return-oriented programming hard
  • problem: defeated with information leak

  • alternate idea: make sure we aren’t returning to gadget
  • …by checking that it actually called our function

a simple way to check returns?

  • observation: places we return to usually after call instructions
    • exception: ‘tail calls’ — we’ll ignore this for now
  • we could check for one
  • replace return with:
return address <- PopFromStack()
if DecodeInstruction(return address - call instruction size) = "call thisFunction":
    goto return address
else:
    CRASH

a simple way to check returns?

  • more practical: label $ID instruction with encoding:
    • TWO-BYTE-OPCODE FOUR-BYTE-CONSTANT
    • (real version: can reuse some sufficiently nop-like instruction)
...
    call foo
    label $0xf19279bb // random ID for function foo
...
...
foo:
    pop %rdx    // %rdx <- return address
    cmpl $0xf19279bb, 2(%rdx)
    jne CRASH
    jmp *%rdx
...

looks like canaries? (1)

  • what attacks does this stop that canaries don’t?
  • ID does not need to be secret!
    • (assuming non-executable writeable memory)
    • attacker can’t write new places for return to go
  • avoids “stack pivoting” attacks
    • attacker can’t make stack pointer point to wrong part of stack
    • and expect it to return differently

looks like canaries? (2)

  • what attacks does this NOT stop that canaries do?

  • example: SortList can be called from Innocent,
    then return from Dangerous

    • assumption: attacker can overwrite return address at right time (running on another core? problem with sortFunc1?)
void Innocent() {
  ...
  SortList(someList1,
           sortFunc1);
  Use(someList1);
  ...
}
void Dangerous() {
  ...
  SortList(someList2,
           sortFunc2);
  UseDangerously(someList2);
  ...
}

checking a VTable call

class A { public:
  virtual void bar() { ... }
};
class B : public A { public:
  void bar() { ... }
};
void example(A *obj) {
  obj->bar();
}
A::bar():
  label $0xe0c5df0b
  ...
B::bar():
  label $0xe0c5df0b
  ...
example:
  // %rax <- vtable address
  movq (%rdi), %rax
  // %rdx <- first vtable entry
  movq (%rax), %rax
  // call using vtable entry
  call *%rax
  ...
example:
  movq (%rdi), %rax
  movq (%rax), %rax
  cmpl $0xe0c5df0b, 2(%rax)
  jne CRASH
  call *%rax

check that we’re calling some bar method
still allows calling wrong one

checking a VTable return

...
  // %rdi = A or B object
  movq (%rdi), %rax
  movq (%rax), %rax
  cmpq $0xe0c5df0b, 2(%rax)
  jne CRASH
  call *%rax
  label $0x64a0cfe3
  ret
A::bar():
  label $0xe0c5df0b
  ...
  pop %rdx // RDX <- return address
  cmp $0x64a0cfe3, 2(%rdx)
  jne CRASH
  jmp *%rdx
B::bar():
  label $0xe0c5df0b
  ...
  pop %rdx // RDX <- return address
  cmp $0x64a0cfe3, 2(%rdx)
  jne CRASH
  jmp *%rdx
  • if we want to use this label-checking on the return
    need to choose the same label for A::bar and B::bar return, too

calls through function pointers

typedef int (*CompareFnType)(const char*, const char*)
void SortFunction(const char **items, CompareFnType compare) {
    ...
    (*compare)(a, b);
    ...
}
  • here: call through explicitly passed function pointer
  • want to do the same thing we did for VTable calls
    • all the compare functions have the same label
    • all the returns form compare functions have the same label
  • requires somehow finding all compare functions?

finding compare functions?

  • seems really tricky to track all compare functions
  • practically speaking: going to be a place where we make approximations

concept: labels and control flow graph

Figure of Abadi paper showing two comparison functions 'lt' and 'gt', and a function 'sort2(int a[], int b[], int len) { sort(a, len, lt); sort (b, len, gt ); }' and figure representing the assembly code for sort2(), sort(), lt(), and gt(), showing lines with arrowheads representing function calls and dotted lines with arorowheads representing returns. The call instruction in sort is represented 'call 17,R' and points to instructions 'label 17' at the beginning of 'lt' and 'gt'. The return instructions from lt and gt is 'ret 23', and goes do a line in sort() that starts 'label 23'. The return instruction from sort is 'ret 55' and points to instructions 'label 55' in sort2().
  • control flow graph

    • nodes = blocks of code
    • edges = potential jump/call
  • assigning labels: every in-edge needs to check same label at source

figure from Abadi et al, ‘‘Control-Flow Integrity: Principles, Implementations and Applications’’ (CCS 2005)

library-crossing CFGs

#include <png.h>
void ReadImageFromNetwork(
    png_structp libpng_handle,
    unsigned char *bytes,
    size_t size
) { ...  }

int main() {
    /* init libpng */
    png_structp libpng_handle = ...;
    /* tell libpng how to read image data */
    png_set_read_fn(
        libpng_handle, ...,
        ReadImageFromNetwork
    )
    ...
    /* extract "header" 
       information from image */
    png_get_IHDR(libpng_handle, ...)
    ...
}

control flow graph, showing flow from main to png_set_read_fn, then back to main, then to png_get_IHDR, then to ReadImageFromNetwork. All of these control flow transfers are shown to cross the boundary between executable and library.

CFGs will be imprecise

FunctionPtr p = functionA;
Example() {
  while (true) {
    ...
    if (SomethingComplicated()) {
      (*p)();
    } else if (SomethngElseComplicated()) {
      foo();
    }
    ...
  }
}
foo() {
  ...
  if (AnotherComplexThing()) {
    p = functionB;
  }
}
  • can Example() call functionB()? probably not practical to tell
    • need to make conservative ‘yes’ guess

finding possible function pointer values? (1)

  • given call using function pointers
    how do we find the legitimate possible values?
  • simple approximation:
    • every function a pointer is taken to with same type
  • but maybe situations like…
    • bool StringLessThan(char *a, char *b)
    • bool UploadFileSCP(char *local_file, char *remote_file)

finding possible function pointer values? (2)

  • given call using function pointers
    how do we find the legitimate possible values?
  • more precise idea:
for each fptr constant X:
    PossibleValues[X] = {X}
for each fptr variable X:
    PossibleValues[X] = empty set
until PossibleValues stops changing:
    for each fptr assignment LHS=RHS:
        for each fptr variable/constant Y that RHS could evaluate to:
            PossibleValues[LHS] = Union(PossibleValues[LHS], PossibleValues[Y])

labels aren’t enough?

control-flow-graph diagram showing calls from foo and bar through function pointers. foo can call A and B; bar can call B and C. A check for a label is listed before each call and a label is listed at the beginning of A, B, C, but the label is left as question marks.

two possible fixes:

  • allow foo to call B
    (easier to attack)

  • scan for multiple labels
    (more overhead)

clang’s CFI implementation

clang idea for CFI indirect calls

start_funcs_with_two_string_args:
.align 8
compare_alpha:
  jmp real_compare_alpha
.align 8
run_command_with_arg:
  jmp real_run_command_with_arg
.align 8
print_two_strings:
  jmp real_print_two_strings
.align 8
move_file:
  jmp real_move_file
.align 8
compare_reverse_alpha:
  jmp real_compare_reverse_alpha
end_funcs_with_two_string_args:

check psuedocode for compare:

# which functions are valid for compare
COMPARE_BITMASK = [True, False, False, False, True]
...
if fptr % 8 != 0:
    CRASH
if fptr < start_funcs_with_two_string_args:
    CRASH
if fptr >= end_funcs_with_two_string_args:
    CRASH
index = (fptr - start_funcs_with_two_string_args)/8
if not COMAPRE_BITMASK[index]:
    CRASH

clang idea for VTables

  • check VTable element address instead of function address
    • VTables at small number of fixed locations, anyways
  • otherwise
    • place all VTables for related classes together
    • check start/end address for VTables
    • bit mask indicating which VTable entries are okay for call
      • need to handle unusual case of pointers to member functions

CFI prevents?

class Foo { public: virtual void f() { } };
class Bar : public Foo { public: virtual void f() { g(1); } };
class Quux : public Foo { public: virtual void f() { } };
void g(int x) { if (x == 0) { danger(); } }
int h(int x) { return 0; }
int (*ptr)(int) = &h;
  • with clang’s CFI, which likely can end up calling danger() if an attacker can first write to arbitrary memory locations?

    • A. (*ptr)(1);
    • B. (*ptr)(0); if compiler thinks ptr set to g ever, yes; otherwise, no
    • C. Foo *q = attacker_controlled(); q->f()
    • D. Quux *q = attacker_controlled(); q->f()
    • E. none of these

CFI prevents?

class Foo { public: virtual void f() { } };
class Bar : public Foo { public: virtual void f() { g(1); } };
class Quux : public Foo { public: virtual void f() { } };
void g(int x) { if (x == 0) { danger(); } }
int h(int x) { return 0; }
int (*ptr)(int) = &h;
...
  • with clang’s CFI, which likely can end up calling danger() if an attacker can first write to arbitrary memory locations?

    • A. (*ptr)(1);
    • B. (*ptr)(0); if compiler thinks ptr set to g ever, yes; otherwise, no
    • C. Foo *q = attacker_controlled(); q->f() can only call real f() methods; could call Bar::f() but how to change g’s arg?
    • D. Quux *q = attacker_controlled(); q->f() can only call real f() methods of Quux and subclasses, so can’t even call Bar::f()
    • E. none of these

CFI: dynamically loaded libraries?

  • what about dynamically loaded libraries
  • problem: precomputed control flow graph now invalid

Intel hardware CFI support

  • Intel adds ‘endbr64’ instruction
  • special NOP instruction that acts as a label
  • means: only one label for everything
    • prevents gadgets from existing
  • “Control Flow Enforcement”: if enabled
    computed jump to non-endbr64 triggers segfault-like error
  • ARM has similar feature called Branch Target Identification

CFI overhead

  • Abadi et al’s 2004 paper:

    • used label-based approach
    • 0-45% time overhead on SPECcpu2000 benchmarks
    • best: compression program
    • worst: chess engine
  • Tice et al’s 2014 paper (clang-style impl, sometimes in GCC, sometimes in Clang)

    • could seperately enable different parts

    • in tests on SPECcpu 2006 benchmarks:

    • 0–10% slowdown for VTable dereference checks

      • but 20% without tuning
    • 0-6% for other indirect call checking

authenticated pointers (1)

some_function:
    authentication_code <- MAC(
        secret key, 
        return address
    )
    ... dangerous function code ...
    assert(authenication_code ==
        MAC(secret key, return address))
    jump to return address
  • compute MAC (message authentication code) for return address
  • verify MAC before using it

authenticated return addresses (1)

  • instead of return address
    store {return address, MAC(secret key, return address)}
  • check MAC before every return
  • preview: will have hardware help for keeping secret key secret
  • exercise: what can attacker still do with stack overflow?

authenticated pointers (2)

some_function:
    authentication_code <- MAC(
        secret key,
        stack pointer, 
        return address
    )
    ... dangerous function code ...
    assert(authenication_code ==
        MAC(secret key, stack pointer, return address))
    jump to return address

authenticated return addresses (2)

  • instead of return address
    store {return address, MAC(secret key, stack address, return address)}
  • check MAC before every return
  • prevented returning to wrong function…
  • exercise: what can attacker still do?

storing MACs

  • scheme so far: store extra information alongside address
  • inconvenient: need to allocate extra space for address
    • pretty easy to do for return addresses…
    • … but we’d like to apply this to other things
  • alternate idea:
    • encode MAC + pointer together in pointer-sized thing
    • decode to get out original pointer
    • (probably something that looks more like symmetric encryption)

authenticated pointers (3)

some_function:
    return address <- encode(
        secret key,
        stack pointer, 
        return address
    )
    ... dangerous function code ...
    return address <- decode_or_crash(
        secret key,
        stack pointer, 
        return address
    )
    jump to return address

authenticated pointers (4)

some_vtable[index] <- encrypt(
    secret key,
    label,
    address of some of function
)
... dangerous code ...
function pointer <- decrypt(
    secret key,
    label,
    object->vtable[index]
)
call function pointer

ARM authenticated pointers

  • ARM64 implements this idea with:

  • secret key kept in a special register (hard to leak to attacker)

  • authentication code placed in upper pointer bits

    • makes pointer temporarily invalid
    • can’t ‘‘accidentally’’ use authenticated pointer without verifying authentication code first

authenticated pointer layout

Figure from a research paper labeleds 'Figure 1: The PAC is created using key-specific PA instructions (pacia) and is a keyed MAC calculated over the pointer address and a modifier. The figure shows a pointer pointer represented as an empty upper part and an address. THe pacia instruction transforms it using a keyed MAC taking in that address, a modifier, and the PA-key, to produce a PAC (pointer authentication code) that is stored in the ('reserved') upper bits of the pointer, except for one bit which is used as the upper bit of the address.

Liljestrand et al, “PAC it up: Towards Pointer Integrity using ARM Pointer Authentication”

authentication keys

  • processes can have multiple authentication keys active

  • easy to use separate keys for

    • return address pointers
    • function pointers (in VTables, for example)
    • any pointers to data
  • authentication keys are in special registers — need OS to read/set

  • also can ‘‘mix’’ in extra info like stack pointer

    • similar to separate key for each stack address

different limits

  • CFI and pointer authentication both limit use of address

  • … but don’t eliminate potential for abuse:

    • can still call wrong function in CFI
    • can still use wrong signed pointer
  • scope of labels/authentication keys+extra info important

pointer authentication use

  • commonly enabled on ARM64 for return addreses
    • can “just” enable in compiler, like stack canaries
  • otherwise:
  • on ARM64 OS X:
    • two different compiler ‘architectures’: arm64, arm64e
    • can’t use arm64e libraries in arm64 program or vice-versa
  • proposals to use on Linux in global offset table, etc.
    • GNU Linux linker option -z pac-plt; unclear if used ‘for real’

macOS PAC for apple code

https://support.apple.com/en-il/guide/security/sec8b776536b/1/web/1

macOS PAC

  • ‘new’ ‘preview’ architecture in compiter
  • seems to be used internally by Apple
  • doesn’t appear easily available/supported for others
  • incompatible with ‘old’ libraries

PAC bypasses

  • have been memory exploits in spite of PAC in iOS kernel
  • only certain code pointers authenticated
    • too expensive to authenticate all pointers
  • changing unsigned pointer values just before signed
    • get context switch to occur at just the right time
    • modify pointer value in saved context
  • code that signs pointers they shouldn’t
    • changing pointer without crashing if old pointer invalid
  • ‘gadgets’ that allow brute-forcing MAC tag
    • not enough space in pointers for non-brute-forceable tag