Fortification vs Memcheck

Mark Wielaard

Presenter Notes

Battling memory protectors

gcc/glibc

  • fortification of C library functions

gcc/sanitisers

  • compiled in checks

valgrind/memcheck

  • whole program address space tracking

Presenter Notes

-D_FORTIFY_SOURCE=2

Most distros now build with this enabled

Defining this macro causes some lightweight checks to be performed to detect some buffer overflow errors when employing various string and memory manipulation functions.

for example, memcpy(3), memset(3), stpcpy(3), strcpy(3), strncpy(3), strcat(3), strncat(3), sprintf(3), snprintf(3), vsprintf(3), vsnprintf(3), gets(3), and ... variants thereof.

Presenter Notes

How does fortification work?

One GCC compiler trick

  • __builtin_object_size

Lots of GLIBC C pre-processor include file tricks

  • And some ELF symbol aliasing trickery

Presenter Notes

__builtin_object_size

size_t __builtin_object_size (const void * ptr, int type)

returns a constant number of bytes from ptr to the end of the object ptr pointer points to (if known at compile time)

Depending on type returns the minimum or maximum of the object(s) ptr might point to, the closest surrounding subobject or the outer object and either 0 or -1/SIZE_MAX.

https://gcc.gnu.org/onlinedocs/gcc/Object-Size-Checking.html

warning _FORTIFY_SOURCE requires compiling with optimization (-O)

to detect object sizes across function boundaries or to follow pointer assignments through non-trivial control flow they rely on various optimization passes enabled with -O2

Presenter Notes

Simplifications

glibc include files are almost unreadable, so we'll pretend we are only interested in -D_FORTIFY_SOURCE=2 with the "simplest" semantics.

Always just use type == 1

returns SIZE_MAX if unknown, otherwise largest remaining size sub-object ptr points to.

Example:

struct V { char buf1[10]; int b; char buf2[10]; } var;
char *p = &var.b;
char *q = &var.buf1[1];
char *r = x > 0 ? p : q;

#define bos(ptr) __builtin_object_size (ptr, 1)

bos (p) == sizeof (var.b);         // 4
bos (q) == sizeof (var.buf1) - 1;  // 9
bos (r) == max (bos (p), bos (q)); // 9

See https://gcc.gnu.org/onlinedocs/gcc/Object-Size-Checking.html for all the details. Try type = 0 => r == 27, type = 2 => r == 16, type = 3 => r == 4.

Presenter Notes

glibc example getcwd

char *getcwd (char *buf, size_t size);

getcwd_chk.c

char *
__getcwd_chk (char *buf, size_t size, size_t buflen)
{
  if (size > buflen)
    __chk_fail ();

  return __getcwd (buf, size);
}

unistd.h (actually bits/unistd.h included if FORITFY_LEVEL > 0)

extern char *__getcwd_chk (char *__buf, size_t __size, size_t __buflen);

extern char *__REDIRECT (__getcwd_alias, ( /* ... */ ), getcwd);
extern char *__REDIRECT (__getcwd_chk_warn, ( /* ... */ ), __getcwd_chk)
     __warnattr ("getcwd caller with bigger length than size of "
                 "destination buffer");

Presenter Notes

glibc example getcwd continued...

unistd.h (actually bits/unistd.h included if FORITFY_LEVEL > 0)

__extern_always_inline __attribute_artificial__ char *
getcwd (char *__buf, size_t __size)
{
  if (__bos (__buf) != (size_t) -1)
    {
      if (!__builtin_constant_p (__size))
        return __getcwd_chk (__buf, __size, __bos (__buf));

      if (__size > __bos (__buf))
        return __getcwd_chk_warn (__buf, __size, __bos (__buf));
    }
  return __getcwd_alias (__buf, __size);
}

Presenter Notes

getcwd example with valgrind

Source

char *path = malloc (16);
char *cwd = getcwd (path, PATH_MAX);
printf ("cwd: %s\n", cwd);

Run

$ ./getcwd
cwd: /home/mark

valgrind

$ valgrind -q ./getcwd
==24741== Syscall param getcwd(buf) points to unaddressable byte(s)
==24741==    at 0x4F324CA: getcwd (getcwd.c:78)
==24741==    by 0x4004BA: main (getcwd.c:10)
==24741==  Address 0x5200050 is 0 bytes after a block of size 16 alloc'd
==24741==    at 0x4C2DB9D: malloc (vg_replace_malloc.c:299)
==24741==    by 0x4004AD: main (getcwd.c:9)

Presenter Notes

getcwd example with gcc/glibc

Compile

$ gcc -D_FORTIFY_SOURCE=2 -O2 -g -o getcwd getcwd.c 
In file included from /usr/include/unistd.h:1163:0,
                 from getcwd.c:2:
In function ‘getcwd’,
    inlined from ‘main’ at getcwd.c:10:9:
/usr/include/bits/unistd.h:208:9: warning: call to ‘__getcwd_chk_warn’
declared with attribute warning: getcwd caller with bigger length than
size of destination buffer
  return __getcwd_chk_warn (__buf, __size, __bos (__buf));
         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Presenter Notes

getcwd example with gcc/glibc

Run

$ ./getcwd 
*** buffer overflow detected ***: ./getcwd terminated
======= Backtrace: =========
/lib64/libc.so.6(+0x791fb)[0x7fa990cc01fb]
/lib64/libc.so.6(__fortify_fail+0x37)[0x7fa990d61187]
/lib64/libc.so.6(+0x118120)[0x7fa990d5f120]
/lib64/libc.so.6(+0x1186c3)[0x7fa990d5f6c3]
./getcwd[0x400500]
/lib64/libc.so.6(__libc_start_main+0xf1)[0x7fa990c67401]
./getcwd[0x40054a]
======= Memory map: ========
00400000-00401000 r-xp 00000000 b3:04 1185378    /home/mark/getcwd
00600000-00601000 r--p 00000000 b3:04 1185378    /home/mark/getcwd
[...]
7ffd50dbc000-7ffd50ddd000 rw-p 00000000 00:00 0  [stack]
7ffd50df7000-7ffd50df9000 r--p 00000000 00:00 0  [vvar]
7ffd50df9000-7ffd50dfb000 r-xp 00000000 00:00 0  [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000  [vsyscall]
Aborted (core dumped)

Presenter Notes

getcwd example with gcc/glibc

Valgrind

$ valgrind -q ./getcwd 
*** buffer overflow detected ***: ./getcwd terminated
======= Backtrace: =========
[...]
======= Memory map: ========
[...]
==29012== Process terminating with default action of signal 6 (SIGABRT)
==29012==    at 0x4E6F91F: raise (raise.c:58)
==29012==    by 0x4E71519: abort (abort.c:89)
==29012==    by 0x4EB31FF: __libc_message (libc_fatal.c:175)
==29012==    by 0x4F54186: __fortify_fail (fortify_fail.c:30)
==29012==    by 0x4F5211F: __chk_fail (chk_fail.c:28)
==29012==    by 0x4F526C2: __getcwd_chk (getcwd_chk.c:27)
==29012==    by 0x4004FF: getcwd (unistd.h:208)
==29012==    by 0x4004FF: main (getcwd.c:10)

Presenter Notes

fortification good or bad?

Good

  • Very low overhead.
  • No (or very, very low) false positives.
  • Can be used in production/always on.
  • Aborts before something bad happens.
  • Might see things valgrind/memcheck misses. Because compiler knows object type.

Bad

  • Not as good/expressive warnings.
  • Only works when compiler can staticly deduce buffer bounds.
  • Blinds other memory protectors because bad usage is prevented. So no better diagnostics can be created.
  • Might obscure tracking memory usage other verifiers depend on because standard functions get (not) called by other names (valgrind mostly immume).

Presenter Notes

Can I have the good without the bad?

Pass bad address to __check_fail

char *
__getcwd_chk (char *buf, size_t size, size_t buflen)
{
  if (size > buflen)
    __chk_fail (buf + buflen);

  return __getcwd (buf, size);
}

Add ptr and valgrind client requests to __check_fail

#include <valgrind/memcheck.h>

void
__attribute__ ((noreturn))
__chk_fail (char *ptr)
{
  VALGRIND_MAKE_MEM_NOACCESS(ptr,1);
  VALGRIND_CHECK_MEM_IS_ADDRESSABLE(ptr,1);
  __fortify_fail ("buffer overflow detected");
}
libc_hidden_def (__chk_fail)

Presenter Notes

The good without the bad

$ valgrind -q ./getcwd
==30124== Unaddressable byte(s) found during client check request
==30124==    by 0x4F5211F: __chk_fail (chk_fail.c:30)
==30124==    by 0x4F526C2: __getcwd_chk (getcwd_chk.c:27)
==30124==    by 0x4004FF: getcwd (unistd.h:208)
==30124==    by 0x4004FF: main (getcwd.c:10)
==30124==  Address 0x5200050 is 0 bytes after a block of size 16 alloc'd
==30124==    at 0x4C2DB9D: malloc (vg_replace_malloc.c:299)
==30124==    by 0x4004FD: main (getcwd.c:30)
*** buffer overflow detected ***: ./getcwd terminated
======= Backtrace: =========
[...]
======= Memory map: ========
[...]

Presenter Notes

Alternatively...

Against adding a ptr argument to __chk_fail

  • Code bloat! Every extra instruction is one too many!
  • gcc/glibc hackers really like it small and fast (so it is always on).
  • You cannot always point at an (exact) address.
  • select fd_sets for example.
  • With a bit more work (outside glibc!) valgrind could give even better messages/hints.
  • True, but actually not that often...

Just implement all ~75 __foobar_chk functions as valgrind overrides.

  • We already do (for e.g. extra overlap checking) a couple
  • see memmove_chk, stpcpy_chk, strcpy_chk and memcpy_chk in vg_replace_strmem.c
  • Most are really as simple as getcwd.

Presenter Notes

Issues

I haven't done most of the work

  • Even though for ~50 out of the ~75 overrides it would be just a day work.

The snprintf_chk family seems slightly tricky

  • Sets an internal flag that gets checked in libio.

Newer gcc have builtins that get completely inlined:

__builtin___memcpy_chk  __builtin___memmove_chk __builtin___mempcpy_chk
__builtin___memset_chk  __builtin___memmove_chk __builtin___memset_chk 
__builtin___strcpy_chk  __builtin___stpcpy_chk  __builtin___strncpy_chk
__builtin___strcat_chk  __builtin___strncat_chk
  • Still call __chk_fail so we can override that for better backtrace.
    • But without address? Might still ask for one...

Presenter Notes

Conclusion

fortification is awesome and low cost

  • If you have a C library that takes buffers and length separately please add fortified functions.

fortification finds memory issues early

  • But blinds memcheck because the illegal access doesn't happen.

memcheck can provide more information if it knows the address

  • Either by having a check/failure function that takes the address.
  • Or by overriding all _chk functions and taking the address ourselves.

Presenter Notes