IR [Reverse]

IR

We found this snippet of code on our employee's laptop. It looks really scary. Can you figure out what it does?

Recon

We are provided with a file chall.e which appears to contain LLVM IR code. The syntax looks quite expressive, but perhaps its easier to work with a binary we can decompile? ;-)

After fetching a recent copy of LLVM (9.0.0) and trying to compile the IR into an object file using llc -filetype=obj chall.ll we get errors like these:

llc: error: llc: chal.ll:12:13: error: use of undefined value '@llvm.dbg.declare'

30 seconds of googling did not learn us why these dbg statements are not understood by llc out of the box. So we opted to (painstakingly, manually) get rid of all it. This mean removing all call instructions to llvm.dbg.declare as well as nuking any !dbg suffix to other instructions.

Afterwards the IR code compiles/links cleanly:

$ llc -filetype=obj chal.ll
$ file chal.o
chal.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

Decompiling this code gives us (roughly):

#define MAX_SIZE 0x40

unsigned char check[] = {
  0x03, 0x12, 0x1a, 0x17, 0x0a, 0xec, 0xf2, 0x14, 0x0e, 0x05, 0x03, 0x1d,
  0x19, 0x0e, 0x02, 0x0a, 0x1f, 0x07, 0x0c, 0x01, 0x17, 0x06, 0x0c, 0x0a,
  0x19, 0x13, 0x0a, 0x16, 0x1c, 0x18, 0x08, 0x07, 0x1a, 0x03, 0x1d, 0x1c,
  0x11, 0x0b, 0xf3, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x05
};

int reverse(unsigned char *a1) {
  int k;
  int j;
  int i;

  for ( i = 0; i < MAX_SIZE; ++i )
    a1[i] += 5;
  for ( j = 0; j < MAX_SIZE - 1; ++j )
    a1[j] ^= a1[j + 1];
  for ( k = 0; ; ++k )
  {
    if ( k >= MAX_SIZE )
      return 1;
    if ( check[k] != a1[k] )
      break;
  }
  return 0LL;
}

Lazy solution

When all you have is a hammer, every problem looks like a nail. We opted to (quickly) solve this using Angr + symbolic execution.

Code

#define MAX_SIZE 0x40
#include <stdio.h>
#include <stdlib.h>

unsigned char check[] = {
  0x03, 0x12, 0x1a, 0x17, 0x0a, 0xec, 0xf2, 0x14, 0x0e, 0x05, 0x03, 0x1d,
  0x19, 0x0e, 0x02, 0x0a, 0x1f, 0x07, 0x0c, 0x01, 0x17, 0x06, 0x0c, 0x0a,
  0x19, 0x13, 0x0a, 0x16, 0x1c, 0x18, 0x08, 0x07, 0x1a, 0x03, 0x1d, 0x1c,
  0x11, 0x0b, 0xf3, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x05
};


int reverse(unsigned char *a1) {
  int k; // [rsp+0h] [rbp-18h]
  int j; // [rsp+4h] [rbp-14h]
  int i; // [rsp+8h] [rbp-10h]

  for ( i = 0; i < MAX_SIZE; ++i )
    a1[i] += 5;
  for ( j = 0; j < MAX_SIZE - 1; ++j )
    a1[j] ^= a1[j + 1];
  for ( k = 0; ; ++k )
  {
    if ( k >= MAX_SIZE )
      return 1;
    if ( check[k] != a1[k] )
      break;
  }
  return 0LL;
}


int main(int argc, char *argv[]) {
        char flaggie[0x50];

        read(0, flaggie, 0x40);
    return reverse(flaggie);
}

Compile to a binary (even lazier, no pie) with

gcc source.c -no-pie -o flag

Then find the instruction addresses for: 1. The "good case", i.e. return 1 2. The "bad case", i.e. return 0

Build our solver with angr (pip install angr)

import angr

p = angr.Project('./flag')
pg = p.factory.simulation_manager()
# find is the addr for the "good case" we want to reach
# avoid is the "bad case"
e = pg.explore(find=0x0040120d, avoid=0x0040123f)

if len(e.found) > 0:
    print(e.found[0].posix.dumps(0))

Flag

$ python angr_solve.py 
/home/user/.virtualevs/ctf/lib/python3.7/site-packages/pysmt/walkers/generic.py:43: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3,and in 3.9 it will stop working
  if len(nodetypes) == 1 and isinstance(nodetypes[0], collections.Iterable):
b'utflag{machine_agnostic_ir_is_wonderful}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'