VolgaCTF 2015 interstellar

@mrexcessive WHA
& Tom O'Brien WHA

The problem

Just a snall binary from a far-far galaxy
interstellar - binary for download

The solution

Started with readelf -a ./interstellar

Then straced it
Nothing particularly weird

objdump -d ./interstellar >asm.asm

and read through

gdb --args ./interstellar abcd
(gdb) b\*0x400c46            ; hits here first
(gdb) b\*0x400c02
(gdb) b\*0x400af4
(gdb) b\*0x400ab7

There's a sleep(1) call... in the code, just after fork()
Implies perhaps bruting is possible.
But the main thread just checks for debugger (ptrace), checks that no LD_PRELOAD
The work is probably happening in the other thread

Tried various things, patching around the debugger checks thread...
... stopping it for longer with a huge sleep.
Doesn't seem to object to me patching the binary (with bvi!) and then rerunnning

So I have various versions at this point, with some strange patches.

We have a strcmp() just after this

  400e7a:    48 89 d6                mov    %rdx,%rsi                 
  400e7d:    48 89 c7                mov    %rax,%rdi

Which seems to verify flag.

After a while work out it demands exactly 36 chars.
gdb --args ./jhacked 012345678901234567890123456789012345

Try again with that.
OK better !

This then gets to 0x400dff once... then exits

Process is exiting at 0x400b93
OK... this can maybe be patched..

(gdb) hb *0x400b8e
;when it hits that... 
(gdb) set $pc = 0x400b98
(gdb) b *0x400e6f
;OK reached the strcmp()
;        012345678901234567890123456789012345
$rdx = "From a seed a mighty trunk may grow.\n"
$rax = "0234b32db9ecd2fde7b4fc03240ab6fbb0844525db477f13251a7ed7134773db8e589f2da7"

But it's a calculated flag... so not much joy.

(gdb) x/36bx $rax
0x7fffffffe2d0:    0x5b    0xe6    0x53    0xf3    0x11    0x29    0xec    0x36
0x7fffffffe2d8:    0xc0    0xd2    0xa2    0x25    0x24    0x36    0x41    0xf1
0x7fffffffe2e0:    0x2b    0x58    0x1a    0xf1    0x23    0x15    0x6d    0xdd
0x7fffffffe2e8:    0x7c    0x6e    0x9c    0xaa    0xde    0xac    0x91    0x79
0x7fffffffe2f0:    0x93    0x60    0x47    0xfa

We need correct input to the algo.
It's using strange floatingpoint math routines, but only as integers.
Don't these people know about Python!

I'm writing it in Python...

def FirstMangle(s):
   v = 0
   for i in xrange(0,len(s)):
      v *= 307
      v += ord(s[i])
   return v

sInput = "012345678901234567890123456789012345"
   assert(len(sInput) == 36)
   v1 = FirstMangle(sInput)
   print bin(v1)     

Check we get same binary output as from the program itself in GDB...


There is then a Binary XNOR...
The key is found here

(gdb) x/1s0x400f70
0x400f70:     "01111101001000101000000111101001001011111110010011100111010011000010101101110110100001101011100101001110000000001101000110001011011010101001000000010010001100011001100011001011010101111011110110001100101100101000110011101111101101000110110010101001100100110100010101101111101111011001100011111101"

It is fixed position and unchanging content between runs... Hurrah!

Now we have a Python program which can run it... had a few attempts at bruting it backwards from LSB to MSB

   sStartWith = "x12345678901234567890123456789012345"
   sTarget =    "From a seed a mighty trunk may grow."
   charsin = []
   for ch in sStartWith:
   # brute it one char at a time, backwards
   # get 3 chars right
   valids = []
   useables = ""
   for i in xrange(0,len(sStartWith)):    # start all sets off as whole set
   posnA = len(sStartWith) - 3            # initial pos for left hand side
   while posnA >= 0:
      posnB = posnA + 1
      posnC = posnA + 2
      validB = {}
      validC = {}
      for chA in valids[posnA]:
         for chB in valids[posnB]:
            for chC in valids[posnC]:
               charsin[posnA] = chA
               charsin[posnB] = chB
               charsin[posnC] = chC
               (isGood,result) = ComputeFor(''.join(charsin))
               if isGood:
                  result = result[0:36]         # this is what the assembler does...  (drop final byte)
                  if result[posnC] == sTarget[posnC]:      # got one
                     validB[chB] = 1
                     validC[chC] = 1      
      print "All valid B at %i [%s]" % (posnB,validB.keys())
      print "All valid C at %i [%s]" % (posnC,validC.keys())
      # modify the posnB keys... but not the A or C
      valids[posnB] = ''.join(validB.keys())
      # go for next
      posnA -= 1
#      if posnA < len(sStartWith) - 6:      #XXX for debug
#         break      # skip to next part
# Same thing... but using the valids... and only accepting two chars correct as ok ?
   posnA = len(sStartWith) - 4            # initial pos for left hand side
   while posnA >= 0:
      posnB = posnA + 1
      posnC = posnA + 2
      posnD = posnA + 3
      validB = {}
      validC = {}
      validD = {}
      for chA in valids[posnA]:
         sys.stdout.write(".")         # for comfort / sanity
         for chB in valids[posnB]:
            for chC in valids[posnC]:
               for chD in valids[posnD]:
                  charsin[posnA] = chA
                  charsin[posnB] = chB
                  charsin[posnC] = chC
                  charsin[posnD] = chD
                  (isGood,result) = ComputeFor(''.join(charsin))
                  if isGood:
                     result = result[0:36]         # this is what the assembler does...  (drop final byte)
                     if result[posnC] == sTarget[posnC] and result[posnD] == sTarget[posnD]:      # got one
                        validB[chB] = 1
                        validC[chC] = 1
                        validD[chD] = 1      
      print "All valid B at %i [%s]" % (posnB,validB.keys())
      print "All valid C at %i [%s]" % (posnC,validC.keys())
      print "All valid D at %i [%s]" % (posnD,validD.keys())
      # modify the posnC,D keys... but not the A or B
      valids[posnC] = ''.join(validC.keys())
      valids[posnD] = ''.join(validD.keys())
      # go for next
#      exit()      # XXX debug stop after one
      posnA -= 1
   f.write("NEW RUN OF VALIDS---------------------\n")
   f.write("%s" % valids)

>>> Then overnight sleep for 3 hours (3am to 6am... ) <<<

Saturday morning dawns, well grumpily drags itself from a bed,
... remembers there is a CTF and drinks coffee... back at computer...

So... LUCKILY Tom O'Brien is now online, fellow WHA ... !
Tom knows maths... so while I'm forcing in a second coffee... he quickly understands the maths and reverses things from the desired output back to the XNOR and thence to the input required.

So my main contribution was understanding the problem, and failing to brute it.
Tom actually solved it.

and quickly coded up:

def TomWasHere():
    # First python code, don't hate please!
    # We're going to moonwalk through the code...

    # From our initial input we need to reach the target_string.
    target_string = "From a seed a mighty trunk may grow."

    # The step before the final assertion we perform an XNOR on whatever string we have with the hardcoded xnorbits (at the top of this program).
    # As a result, we need the target_string in binary representation.
    target_binary = "010001100111001001101111011011010010000001100001001000000111001101100101011001010110010000100000011000010010000001101101011010010110011101101000011101000111100100100000011101000111001001110101011011100110101100100000011011010110000101111001001000000110011101110010011011110111011100101110"

    # Problem is, the binary taken to form the string is the first 36 bytes of a 37 byte value, so we're missing the bottom 8 bits.
    # We need to guess the bottom 8 bits, (probably by brute force), but lets start off with an initial guess of 00000000
    guessed_bits = "00000000"

    # The 37 byte string to XNOR with the xnorbits
    string_to_xnor = target_binary + guessed_bits

    # XNOR our target_string + guessed 8 bits. StringAnd is Peter Goode's attempt at an XNOR function, so blame him if stuff goes wrong.
    string_after_xnor = StringAnd(string_to_xnor, xnorbits)

    # Ok, so we now have a 37 byte value that the program would calculate when doing some weird additive and multiplicative function.
    # The gist of it is start at 0, add the value of the first byte (most significant), then for every next byte you multiply by 307
    # and add the byte's value. This means that after every iteration we have a value in the form 307*k + x, where 0 <= x < 256. It also
    # means that given a 36 byte input string we would end up calculating a 37 or 38 byte value (37 if the 36 byte value is less than about
    # half its max value).
    # The plan is to turn our reverse-engineered 37 byte value (technically still a string) into its decimal/hex representation,
    # calculate the largest multiple of 307 below it, and the difference should be 'x', 0 <= x < 256, the value of the last byte of the input
    # string. We should be able to repeat this process for all the remaining bytes to recover the initial input string required.

    # Calculate the value to start unwinding.
    value = int(string_after_xnor, 2)

    initial_string = ""

    while value > 0:
        # We want the difference between the value we calculated and the largest multiple of 307 below it, so take our value modulo 307.
        byte_found = value % 307

        # Make sure it's a valid byte, otherwise bad stuff might happen...
        assert(byte_found < 256)

        # Add the byte onto the string
        initial_string = chr(byte_found) + initial_string

        # Should be numerical!
        value = (value - byte_found) / 307

    return initial_string

result = "@keup@nds0lv3an0ther_ch@113nge"

And we have to fiddle a bit to get the first and last characters...

Flag !!