return address <- PopFromStack()
if DecodeInstruction(return address - call instruction size) = "call thisFunction":
goto return address
else:
CRASH
what attacks does this NOT stop that canaries do?
example: SortList can be called from Innocent,
then return from Dangerous
check that we’re calling some bar method
still allows calling wrong one
control flow graph
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)
#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, ...)
...
}bool StringLessThan(char *a, char *b)bool UploadFileSCP(char *local_file, char *remote_file)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])
two possible fixes:
allow foo to call B
(easier to attack)
scan for multiple labels
(more overhead)
only checks calls via VTables or function pointers
stable implementation requires libraries compiled with support
label information is placed in separate data structure
trick: keep functions in one region of memory
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]:
CRASHclass 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?
(*ptr)(1);(*ptr)(0); if compiler thinks ptr set to g ever, yes; otherwise, noFoo *q = attacker_controlled(); q->f()Quux *q = attacker_controlled(); q->f()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?
(*ptr)(1);(*ptr)(0); if compiler thinks ptr set to g ever, yes; otherwise, noFoo *q = attacker_controlled(); q->f() can only call real f() methods; could call Bar::f() but how to change g’s arg?Quux *q = attacker_controlled(); q->f() can only call real f() methods of Quux and subclasses, so can’t even call Bar::f()Abadi et al’s 2004 paper:
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
0-6% for other indirect call checking
some_function:
authentication_code <- MAC(
secret key,
return address
)
... dangerous function code ...
assert(authenication_code ==
MAC(secret key, return address))
jump to return address
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
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
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
secret key kept in a special register (hard to leak to attacker)
authentication code placed in upper pointer bits
Liljestrand et al, “PAC it up: Towards Pointer Integrity using ARM Pointer Authentication”
processes can have multiple authentication keys active
easy to use separate keys for
authentication keys are in special registers — need OS to read/set
also can ‘‘mix’’ in extra info like stack pointer
CFI and pointer authentication both limit use of address
… but don’t eliminate potential for abuse:
scope of labels/authentication keys+extra info important
https://support.apple.com/en-il/guide/security/sec8b776536b/1/web/1