Runtime Monitoring of C Programs for Security and Correctness Suan Hsi Yong University of Wisconsin – Madison Ph.D. Committee: Susan Horwitz (advisor), Thomas Reps, Charles Fischer, Somesh Jha, James Smith _1 Errors in Software • Software errors are undesirable – may produce incorrect results – may crash system – may corrupt data – may be vulnerable to attack • Software errors are difficult to detect – may be infrequently exercised – may not noticeably alter observable output _2 Memory and Type Safety • Memory safety: each dereference can only access ‘intended target’ – spatial access errors (e.g., out-of-bounds array access) – temporal access errors (e.g., stale pointer dereference) • Type safety: operations can only be applied to values of certain types _3 Memory and Type Safety • Useful for improving quality of software • Tradeoff between efficiency and flexibility – If too general, incurs a high runtime overhead to enforce – If too restrictive, limits expressiveness and utility of language • C language mandates but does not enforce memory and type safety – programmer’s responsibility, error prone _4 Approaches for Finding Errors • Static Approaches – imprecise, not scalable • Dynamic Approaches – incomplete coverage, high runtime overhead • This thesis: Dynamic approach, but use static analysis to improve overhead – for testing/debugging, and for use in deployed software _5 This Thesis Explores… • Runtime checking of memory and type safety in C programs • Three manifestations – Memory-Safety Enforcer (MSE): detects invalid dereferences – Sensitive Location Checker (SLC): detects invalid writes to security-sensitive locations – Runtime Type Checker (RTC): detects bugs manifested as type errors _6 Common Features • Tagged memory – each byte of memory is tagged at runtime with information used to detect errors • Source-level instrumentation – approach is portable, compatible with uninstrumented libraries • Static analysis – identifies and eliminates unnecessary instrumentation _7 Architecture C source file Instrumenter instrumented C source file runtime system/ libraries C Compiler Static Analysis classifications instrumented executable _8 Outline • Introduction • Memory-Safety Enforcer (MSE) • Sensitive-Location Checker (SLC) • Runtime Type Checker (RTC) • Related Work • Conclusion _9 Memory Safety p = &a p’s valid target is a *(p+i) valid only if it accesses a • Spatial access error: out of bounds – e.g., if i is negative or is too large • Temporal access error: stale pointer – e.g., if a had been freed prior to *(p+i) 10 _ Memory Safety Enforcer (MSE) • Debugging Tool – invalid access programming error – e.g., buffer overrun, stale pointer dereference • Security Tool – most attacks require invalid write to succeed (e.g., stack smashing attacks) – halt execution when violation detected 11 _ Control Transfer Attacks • Idea: overwrite sensitive location with address of malicious code • Sensitive locations include – return address (stack smashing) – global offset table – function pointers – longjmp buffer – exec call argument – others… 12 _ Stack Smashing char buf[12]; char *p = &buf[0]; do { *p = getchar(); } while(*p++ != ‘\0’); p buf return address 13 _ Detecting Invalid Access • Fat Pointer – Record information about what each pointer should point to – Safe-C, CCured, Cyclone • Tagged Memory (our approach) – Record information about which locations may be valid targets of some pointer dereference – also used by Purify 14 _ Fat Pointers • associate information with pointer: p address and size of referent char buf[12]; char *p = &buf[0]; do { *p = getchar(); } while(*p++ != ‘\0’); buf 12 buf return address 15 _ Tagged Memory • associates information with target rather than pointer char buf[12]; char *p = &buf[0]; do { *p = getchar(); } while(*p++ != ‘\0’); p return address = valid target of unsafe dereference buf 16 _ Fat Pointer vs. Tagged Memory • Fat Pointers + Guaranteed to catch all spatial errors – Difficult to catch temporal errors efficiently • e.g., CCured uses garbage collection • Tagged Memory + Can detect both spatial and temporal errors efficiently – Guaranteed only to catch invalid accesses to non-user memory • But can improve with static analysis 17 _ Improving MSE • Which dereferences to check? – if static analysis can guarantee that *p is always valid, then *p need not be instrumented. – classify dereferences into checked/unchecked • Which locations to tag valid at runtime? – if x can only be accessed directly or via unchecked dereference, then x need not be tagged valid – classify locations into tracked/untracked 18 _ Checked Derefs/Tracked Locs • naively: all dereferences are checked; all user-defined locations are tracked char buf[12]; char *p = &buf[0]; do { *p = getchar(); } while(*p++ != ‘\0’); p buf return address 19 _ Checked Derefs/Tracked Locs FN_PTR fp = &foo; char buf[12]; char *p = &buf[0]; do { *p = getchar(); } while(*p++ != ‘\0’); (*fp)(); • Static Analysis: identify fewer checked derefs and tracked locations p buf fp 20 _ Checked Dereferences • Writes Only vs. Read/Write – write-only checks catches most attacks; significantly improving overhead • Flow-insensitive analysis: a[i] *(a+i) – *p is checked if: • p is assigned a non-pointer value, or • p is the result of pointer arithmetic, or • p may point to stack or freed heap location 21 _ Example: Checked Dereferences FN_PTR fp = &foo; char buf[12]; char *p = &buf[0]; do { *p = getchar(); } while(*p++ != ‘\0’); (*fp)(); dereferences: *p checked *fp unchecked 22 _ Tracked Locations • locations that may be validly accessed via checked dereference • fewer tracked locations means better performance and coverage – less overhead to mark and clear valid tag – increase likelihood of catching invalid access • identify with points-to analysis [Das’00] – for each checked dereference *p, all locations in p’s points-to set are tracked 23 _ Example: Tracked Locations FN_PTR fp = &foo; char buf[12]; char *p = &buf[0]; do { *p = getchar(); } while(*p++ != ‘\0’); (*fp)(); points-to locations: dereferences: graph: p untracked p fp *p unsafe fp untracked *fp safe buf foo buf tracked 24 _ Example: Tracked Locations FN_PTR fp = &foo; char buf[12]; p char *p = &buf[0]; buf do { *p = getchar(); } while(*p++ != ‘\0’); (*fp)(); fp points-to locations: dereferences: graph: p untracked p fp *p unsafe fp untracked *fp safe buf foo buf tracked 25 _ Summary of MSE Coverage MSE Unoptimized MSE + Static Analysis Return address yes yes Function pointer no likely exec argument no maybe Attack target 26 _ Evaluation: Runtime Overhead 27 _ Flow-Sensitive Analyses • Redundant Checks Analysis *(p+i) = ...; *(p+i) = ...; //- don’t instrument • Pointer Range Analysis – track range of possible values for each pointer *p = ...; p a:char[10], [3,7] – if *p is definitely in-bounds, don’t instrument 28 _ Flow-Sensitive Analyses 29 _ Flow-Sensitive Analyses 30 _ Analysis Time Slowdown 31 _ MSE Static Analysis Summary • Unoptimized MSE – high runtime overhead – only catches invalid access to non-user memory • Flow-insensitive (Extended Points-To Analysis) – low runtime overhead, scalable analysis • Flow-sensitive Analyses – 20% improvement, but analysis not scalable • Write-only faster than read-write checking 32 _ Comparison with Other Tools (runtime overhead) 33 _ Summary of MSE • Tool for detecting invalid pointer dereferences that + has low runtime overhead + does not report false positives + is portable, and does not require programmer changes to source code + protects against a wide range of vulnerabilities, including stack smashing and erroneous free 34 _ Outline • Introduction • Memory-Safety Enforcer (MSE) • Sensitive-Location Checker (SLC) • Runtime Type Checker (RTC) • Related Work • Conclusion 35 _ Two Approaches to Security • MSE: Try to detect all invalid accesses – including invalid accesses that are not vulnerable to attack • SLC: Detect only invalid accesses to sensitive locations – return address, function pointers, longjmp buffers, exec call arguments – related work: StackGuard – protects only return address on the activation record 36 _ SLC vs MSE: classification MSE: identify unsafe dereferences, then compute tracked locations dereferences *p locations w x *q y *r z (points-to edges) 37 _ SLC vs MSE: classification SLC: identify sensitive locations, then compute unchecked dereferences dereferences *p locations w x *q y *r z (points-to edges) 38 _ Example char safe_buf[8]; not sensitive safe _buf char vuln_buf[8]; sensitive strcpy(vuln_buf, “ls”); unchecked gets(safe_buf); checked system(vuln_buf); return address vuln _buf 39 _ SLC vs MSE: instrumentation • SLC must set/clear tag of sensitive locations, while MSE must set/clear tag of tracked locations – In general, much fewer sensitive locations that tracked locations, so SLC is faster • SLC must set/clear tag of return address on activation record – may slow down SLC compared to MSE 40 _ Runtime Overhead: SLC vs MSE Average: SLC=37.7%, MSE=54.1% 41 _ SLC: The Bad News • In some of the benchmarks (ijpeg, li, perl, gap), over 90% of the dereferences were not checked – i.e., they may point to a sensitive location – due to imprecision of points-to analysis • could be improved with better points-to analysis • Good news: can tell from static analysis whether SLC will be effective for a given program 42 _ SLC vs MSE • Memory Safety Enforcer (MSE) – detects invalid accesses that may not be vulnerable to attack – may prevent new as-yet-undiscovered methods of attack • Sensitive Location Checker (SLC) – targets specific locations known (a priori) to be vulnerable to attack – better runtime overhead because of limited scope 43 _ Outline • Introduction • Memory-Safety Enforcer (MSE) • Sensitive-Location Checker (SLC) • Runtime Type Checker (RTC) • Related Work • Conclusion 44 _ Runtime Type Checking • Idea is to detect runtime type violations – value of one type is used in context of incompatible type – Scalar types only (structs and arrays broken down into components) • Debugging tool, for use during development/testing – Higher overhead acceptable (~20x) – Related tools: Purify, Insure++, Valgrind 45 _ Error Example 1: Union union U { int u1; int *u2; } u; int *p; u.u1 = 20; // write int into u.u1 p = u.u2; // copy int from u.u2 -- suspicious! *p = 0; // bad pointer deref -- error! 46 _ Example 2: Bad Pointer Access int *intArray = (int *) malloc(15 * sizeof(int)); int **ptrArray = (int **) malloc(15 * sizeof(int *)); intArray ptrArray User memory 47 _ Example 2: Bad Pointer Access int *i, sumEven = 0; PURIFY ORIGINAL for(i = intArray; ...; i += 2) sumEven += *i; intArray intArray i ptrArray i User memory ptrArray padding User memory48_ Example 3: Custom Allocator char * myMalloc(size_t size) { static char *myMemory, *current; ... if(first_time){ myMemory = (char *) malloc(BLKSIZE); } ... return &myMemory[current += size]; } 49 _ Example 3: Custom Allocator ORIGINAL int *intArray = (int *) myMalloc(10 * sizeof(int)); int **ptrArray = (int **) myMalloc(10 * sizeof(int *)); intArray i User memory myMemory i PURIFY ptrArray myMemory User memory 50 _ Example 4: Simulated Inheritance struct Base { int a1; int a2; } base; struct Sub { int b1; int b2; char b3; } sub; void f(struct Base *s) { s->a1 = ... s->a2 = ... } : f(&base); f(&sub); : 51 _ Example 4: Simulated Inheritance struct Base { int a1; int a2; } base; struct Sub { int b1; float f1; int b2; char b3; } sub; : void f(struct Base *s) { f(&base); s->a1 = ... s->a2 = ... f(&sub); } : 52 _ Runtime Type-Checking • Track type of values in memory – store in mirror of memory: 4 bits for each byte – unalloc, uninit, integral, real, pointer, zero • Warning when type of value assigned does not match expected type • Error when bad runtime type is used 53 _ Runtime Type Checking Tool (memory) (mirror) int k; float f; int *p; k ... unalloc f = 2.3; f ... unalloc p ... unalloc p = (int *)&f; k = *p; print_int( k ); 54 _ Runtime Type Checking Tool (memory) (mirror) int k; float f; int *p; k ... unalloc uninit f = 2.3; f ... unalloc p ... unalloc p = (int *)&f; k = *p; print_int( k ); instrument a declaration 55 _ Runtime Type Checking Tool (memory) (mirror) int k; float f; int *p; k ... unalloc uninit f = 2.3; f ... unalloc uninit p ... unalloc uninit p = (int *)&f; k = *p; print_int( k ); instrument a declaration 56 _ Runtime Type Checking Tool (memory) (mirror) int k; float f; int *p; k ... unalloc uninit f = 2.3; f ... 2.3 unalloc uninit float p ... unalloc uninit p = (int *)&f; k = *p; print_int( k ); instrument an assignment 57 _ Runtime Type Checking Tool (memory) (mirror) int k; float f; int *p; k ... unalloc uninit f = 2.3; f ... 2.3 unalloc uninit float p ... ) ( &f unalloc pointer uninit p = (int *)&f; k = *p; print_int( k ); instrument an assignment 58 _ Runtime Type Checking Tool (memory) (mirror) int k; float f; int *p; k f = 2.3; f ... 2.3 unalloc uninit float OK p ... ) ( &f p = (int *)&f; k = *p; ... unalloc uninit unalloc pointer uninit print_int( k ); instrument a use (of a pointer) 59 _ Runtime Type Checking Tool (memory) (mirror) int k; float f; int *p; k f = 2.3; f ... 2.3 unalloc uninit float p ... ) ( &f unalloc pointer uninit p = (int *)&f; k = *p; ... OK unalloc uninit print_int( k ); instrument a pointer dereference 60 _ Runtime Type Checking Tool (memory) (mirror) int k; warning! float f; int *p; k ... 2.3 unalloc uninit float f = 2.3; f ... 2.3 unalloc uninit float p ... ) ( &f unalloc pointer uninit p = (int *)&f; k = *p; print_int( k ); instrument an assignment 61 _ Runtime Type Checking Tool (memory) (mirror) int k; error! float f; int *p; k ... 2.3 unalloc uninit float f = 2.3; f ... 2.3 unalloc uninit float p ... ) ( &f unalloc pointer uninit p = (int *)&f; k = *p; print_int( k ); instrument a use (of an int) 62 _ Runtime Type Checking • Found errors in SPEC 95, SPEC 2000, and Solaris utilities • But, overhead is high – 4x-170x slowdown, average 43.5x • Use Static Analysis – Flow insensitive: Type Flow Analysis – Flow sensitive: redundant checks, may-be-uninitialized 63 _ Type Flow Analysis • Classify lvalue expressions into: – safe: runtime type always equals static type • no instrumentation needed – type-unsafe: always in-bounds, but runtime type may not equal static type • must be instrumented to check runtime type – mem-unsafe: may violate memory safety • must be fully instrumented (including check that dereference does not access unalloc memory) 64 _ Type Flow Analysis • Classify locations into: – untracked: runtime type always equals static type • no instrumentation needed – tracked: runtime type always equals static type, but may be accessed by unsafe dereference • initialize mirror to static type, but type doesn’t change – unsafe: may cause type-safety error at runtime • initialize mirror to uninit; type may change at runtime 65 _ Type-Flow Analysis • Points-to Analysis • Build Assignment Graph • Compute Possible Types • Classify Expressions and Locations > 66 _ Type-Flow Analysis • Points-to Analysis • Build Assignment Graph • Compute Possible Types Compute points-to set for each pointer p pt {f} • Classify Expressions and Locations 67 _ Type-Flow Analysis • Points-to Analysis x x • Build Assignment Graph 5.0 VALUEfloat • Compute Possible Types 0 VALUEinit • Classify Expressions and Locations &i int valid-ptr VALUE nodes 68 _ Type-Flow Analysis • Points-to Analysis *p • Build Assignment Graph y+z VALUEint • Compute Possible Types p+i VALUEptr • Classify Expressions and Locations &i *p VALUEvalid-ptr nodes 69 _ Type-Flow Analysis x = y; = x y • Points-to Analysis • Build Assignment Graph • Compute Possible Types • Classify Expressions and Locations *p = 5.0; = *p VALUEfloat x = y + z; x = VALUEint edges 70 _ Type-Flow Analysis T = “uninitialized” • Points-to Analysis • Build Assignment Graph • Compute Possible Types • Classify Expressions and Locations valid-ptr pointer init int char float … | = “multiply-typed” 71 _ Type-Flow Analysis • Points-to Analysis • Build Assignment Graph • Compute Possible Types float VALUEfloat T(x) T(y) x = T(y) y • Classify Expressions and Locations 72 _ Type-Flow Analysis • Points-to Analysis • Build Assignment Graph • Compute Possible Types • Classify Expressions and Locations T(*p) T(k) p *p *p k = pt y p {k} pt {k} T(k) T(y) 73 _ Type-Flow Analysis • Points-to Analysis • Build Assignment Graph • Compute Possible Types • Classify Expressions and Locations 1. poss-type(x) static-type(x) x type-unsafe int k; float f = 2.3; int *p = &f; k = *p; } poss-type(k) = float static-type(k) = int k type-unsafe 74 _ Type-Flow Analysis • Points-to Analysis • Build Assignment Graph • Compute Possible Types • Classify Expressions and Locations 1. poss-type(x) static-type(x) x type-unsafe int k; float f = 2.3; int *p = &f; k = *p; } poss-type(*p) = float static-type(*p) = int *p type-unsafe 75 _ Type-Flow Analysis • Points-to Analysis • Build Assignment Graph • Compute Possible Types • Classify Expressions and Locations 2. poss-type(p) ≠ valid-ptr *p mem-unsafe int *p = &k; p = p + 1; *p = 5; *p mem-unsafe 76 _ Type-Flow Analysis • Points-to Analysis • Build Assignment Graph • Compute Possible Types • Classify Expressions and Locations 3.*p unsafe, pt p {x} x tracked int k; float f = 2.3; int *p = &f; k = *p; *p unsafe p may-point-to f } f tracked 77 _ Type-Flow Analysis: Example int k; float f; int *p; f = 2.3; p = (int *)&f; k = *p; print_int( k ); f tracked p safe k *p type-unsafe type-unsafe Instrumentation: w/o using type-flow analysis 78 _ Type-Flow Analysis: Example int k; float f; int *p; f = 2.3; p = (int *)&f; k = *p; print_int( k ); f tracked p safe k *p type-unsafe type-unsafe Instrumentation: using type-flow analysis 79 _ RTC Runtime Overhead 80 _ Flow-sensitive Refinements • May-be-uninitialized analysis > – needed to detect uses of uninitialized data – analysis and runtime overhead both slow • not worthwhile: better to use unoptimized • Redundant checks analysis – for each use of x, if error is reported, tag of x is corrected to prevent cascading errors y = x + 1; z = x + 2; //check of x is redundant 81 _ RTC Runtime Overhead 82 _ Analysis Time 83 _ Outline • Introduction • Memory-Safety Enforcer (MSE) • Sensitive-Location Checker (SLC) • Runtime Type Checker (RTC) • Related Work • Conclusion 84 _ Related Work • Run-time Error Detection – Purify, Insure++, Valgrind – Safe C, CCured, Cyclone – Safe languages (Java), dynamically-typed languages (Lisp) • Reducing Instrumentation – Java (remove array-bounds checks) – Lisp and Scheme (remove tag-handling operations, e.g., Henglein) – CCured (remove pointer checks: Necula et al) 85 _ Conclusion • Finding errors and vulnerabilities is difficult • Three related runtime monitoring approaches – Sensitive location checker: efficient, security-oriented – Memory-safety enforcer: efficient, security or debugging – Runtime type-checker: slow, for debugging • Effective in detecting errors • Static analysis to improve runtime overhead – Flow-insensitive: scalable, significant improvements – Flow-sensitive: modest improvements, not scalable 86 _ Future Work • Better static analysis: fewer unsafe and tracked locations – escape analysis / scope analysis – better points-to analysis • Different mechanism for tagging – e.g. hash lookup • Apply ideas to other languages and environments – e.g. absorb overhead on multiprocessor 87 _ Runtime Monitoring of C Programs for Security and Correctness END Suan Hsi Yong University of Wisconsin – Madison Ph.D. Committee: Susan Horwitz (advisor), Thomas Reps, Charles Fischer, Somesh Jha, James Smith 88 _ Index • Start / Mem & Type Safety / Architecture • MSE / Ctl-xfer Attack / Fat Pointers / Classification / Flow-sensitive / Results • SLC / Example / Results • RTC / Union / MyMalloc / Inheritance / Example / Type-Flow / Results • Related / Conclusion / Future • MSE example / CCured wild / Type-Flow example / MBU 89 _ Example: Allocation FN_PTR fp = &foo; char buf[12]; char *p = &buf[0]; do { *p = getchar(); } while(*p++ != ‘\0’); (*fp)(); locations: pointers: p untracked p unsafe fp untracked fp safe buf tracked 90 _ Example: Allocation FN_PTR fp = &foo; char buf[12]; char *p = &buf[0]; do { *p = getchar(); } while(*p++ != ‘\0’); (*fp)(); locations: pointers: p untracked p unsafe fp untracked fp safe buf tracked fp 91 _ Example: Allocation FN_PTR fp = &foo; char buf[12]; char *p = &buf[0]; do { *p = getchar(); } while(*p++ != ‘\0’); (*fp)(); locations: pointers: p untracked p unsafe fp untracked fp safe buf tracked buf fp 92 _ Example: Allocation FN_PTR fp = &foo; char buf[12]; char *p = &buf[0]; do { *p = getchar(); } while(*p++ != ‘\0’); (*fp)(); locations: pointers: p untracked p unsafe fp untracked fp safe buf tracked p buf fp 93 _ Example: Checking Writes FN_PTR fp = &foo; char buf[12]; char *p = &buf[0]; do { *p = getchar(); } while(*p++ != ‘\0’); (*fp)(); locations: pointers: p untracked p unsafe fp untracked fp safe buf tracked p buf fp 94 _ Example: Checking Writes FN_PTR fp = &foo; char buf[12]; char *p = &buf[0]; do { *p = getchar(); } while(*p++ != ‘\0’); (*fp)(); locations: pointers: p untracked p unsafe fp untracked fp safe buf tracked p buf fp 95 _ Example: Checking Writes FN_PTR fp = &foo; char buf[12]; char *p = &buf[0]; do { *p = getchar(); } while(*p++ != ‘\0’); (*fp)(); locations: pointers: p untracked p unsafe fp untracked fp safe buf tracked p buf fp 96 _ Example: Checking Writes FN_PTR fp = &foo; char buf[12]; char *p = &buf[0]; do { *p = getchar(); } while(*p++ != ‘\0’); (*fp)(); locations: pointers: p untracked p unsafe fp untracked fp safe buf tracked p buf fp 97 _ Back 98 _ CCured WILD Pointers int i; int *p; int *q1, *q2; q1 = i; q1 = (int)&p; q2 = &i; } q1 WILD => q2 WILD 99 _ Back 100 _ Type-Flow Analysis: Example int k; float f; int *p; f = 2.3; p = (int *)&f; k = *p; print_int( k ); 1. Points-to analysis p pt {f} 101 _ Type-Flow Analysis: Example int k; float f; int *p; f = 2.3; p = (int *)&f; k = *p; print_int( k ); 2. Assignment graph = f = p = k p pt VALUEfloat VALUEvalid-ptr *p {f} 102 _ Type-Flow Analysis: Example int k; float f; int *p; f = 2.3; p = (int *)&f; k = *p; print_int( k ); 3. Runtime types float f valid-ptr p float k = = = float VALUEfloat valid-ptr VALUEvalid-ptr float *p T(*p) T(f) p pt {f} 103 _ Type-Flow Analysis: Example int k; float f; int *p; float tracked f valid-ptr safe p f = 2.3; p = (int *)&f; k = *p; print_int( k ); 4. Type-safety levels float type-unsafe k float type-unsafe *p p pt {f} 104 _ Back 105 _ May-be-uninitialized Analysis 106 _ May-be-uninitialized Analysis 107 _ May-be-uninitialized Analysis 108 _ Back 109 _