AlexCTF 2017 - RE5: Packed Movement (350)

Mon, Feb 6, 2017

Writeups #ctf #alexctf #2017 #reversing

This was the final reversing challenge in AlexCTF 2017. You can download it here (md5: ca6fd408e10d7d358bb10c8124ff7862).

The challenge description was:

Being said that move instruction is enough to build a complete computer, anyway move on while you can.

move

The first thing I did was to put this into my vagrant box, run file on it, and then execute it.

$ file move
move: ELF 32-bit LSB  executable, Intel 80386, version 1 (GNU/Linux), statically linked, stripped
$ ./move
Guess a flag: flag
Wrong Flag!
Well, that was somewhat expected. Let’s look under the hood, shall we? I opened the binary in Binary Ninja and there was not a whole lot of stuff to look at in the assembly. Taking a look at the strings next, I noticed something very helpful: the binary had been packed with UPX Packer.

UPX Packer String

So I took to Google and found the UPX packer version 3.91 that was used to pack this binary (found here). I downloaded it, extracted the archive, and looked at the usage.

$ ./upx-3.91-amd64_linux/upx -h
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2013
UPX 3.91        Markus Oberhumer, Laszlo Molnar & John Reiser   Sep 30th 2013

Usage: upx [-123456789dlthVL] [-qvfk] [-o file] file..

Commands:
  -1     compress faster                   -9    compress better
  --best compress best (can be slow for big files)
  -d     decompress                        -l    list compressed file
  -t     test compressed file              -V    display version number
  -h     give this help                    -L    display software license

Options:
  -q     be quiet                          -v    be verbose
  -oFILE write output to 'FILE'
  -f     force compression of suspicious files
  --no-color, --mono, --color, --no-progress   change look

Compression tuning options:
  --brute             try all available compression methods & filters [slow]
  --ultra-brute       try even more compression variants [very slow]

Backup options:
  -k, --backup        keep backup files
  --no-backup         no backup files [default]

Overlay options:
  --overlay=copy      copy any extra data attached to the file [default]
  --overlay=strip     strip any extra data attached to the file [DANGEROUS]
  --overlay=skip      don't compress a file with an overlay

Options for djgpp2/coff:
  --coff              produce COFF output [default: EXE]

Options for dos/com:
  --8086              make compressed com work on any 8086

Options for dos/exe:
  --8086              make compressed exe work on any 8086
  --no-reloc          put no relocations in to the exe header

Options for dos/sys:
  --8086              make compressed sys work on any 8086

Options for ps1/exe:
  --8-bit             uses 8 bit size compression [default: 32 bit]
  --8mib-ram          8 megabyte memory limit [default: 2 MiB]
  --boot-only         disables client/host transfer compatibility
  --no-align          don't align to 2048 bytes [enables: --console-run]

Options for watcom/le:
  --le                produce LE output [default: EXE]

Options for win32/pe, rtm32/pe & arm/pe:
  --compress-exports=0    do not compress the export section
  --compress-exports=1    compress the export section [default]
  --compress-icons=0      do not compress any icons
  --compress-icons=1      compress all but the first icon
  --compress-icons=2      compress all but the first icon directory [default]
  --compress-icons=3      compress all icons
  --compress-resources=0  do not compress any resources at all
  --keep-resource=list    do not compress resources specified by list
  --strip-relocs=0        do not strip relocations
  --strip-relocs=1        strip relocations [default]

Options for linux/elf:
  --preserve-build-id     copy .gnu.note.build-id to compressed output

file..   executables to (de)compress

This version supports:
    AMD64-darwin.macho               Mach/AMD64
    ARMEL-darwin.macho               Mach/ARMEL
    amd64-linux.elf                  linux/ElfAMD
    amd64-linux.kernel.vmlinux       vmlinux/AMD64
    amd64-win64.pe                   win64/pe
    arm-linux.elf                    linux/armel
    arm-linux.kernel.vmlinux         vmlinux/armel
    arm-wince.pe                     arm/pe
    armeb-linux.elf                  linux/armeb
    armeb-linux.kernel.vmlinux       vmlinux/armeb
    armel-linux.kernel.vmlinuz       vmlinuz/armel
    fat-darwin.macho                 Mach/fat
    i086-dos16.com                   dos/com
    i086-dos16.exe                   dos/exe
    i086-dos16.sys                   dos/sys
    i386-bsd.elf.execve              BSD/386
    i386-darwin.macho                Mach/i386
    i386-dos32.djgpp2.coff           djgpp2/coff
    i386-dos32.tmt.adam              tmt/adam
    i386-dos32.watcom.le             watcom/le
    i386-freebsd.elf                 BSD/elf386
    i386-linux.elf                   linux/elf386
    i386-linux.elf.execve            linux/386
    i386-linux.elf.shell             linux/sh386
    i386-linux.kernel.bvmlinuz       bvmlinuz/386
    i386-linux.kernel.vmlinux        vmlinux/386
    i386-linux.kernel.vmlinuz        vmlinuz/386
    i386-netbsd.elf                  netbsd/elf386
    i386-openbsd.elf                 opnbsd/elf386
    i386-win32.pe                    win32/pe
    m68k-atari.tos                   atari/tos
    mips-linux.elf                   linux/mipseb
    mipsel-linux.elf                 linux/mipsel
    mipsel.r3000-ps1                 ps1/exe
    powerpc-darwin.macho             Mach/ppc32
    powerpc-linux.elf                linux/ElfPPC
    powerpc-linux.kernel.vmlinux     vmlinux/ppc32

UPX comes with ABSOLUTELY NO WARRANTY; for details visit http://upx.sf.net

So the argument I needed to use was -d to decompress.

$ ./upx-3.91-amd64_linux/upx -d move -o move_unpacked
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2013
UPX 3.91        Markus Oberhumer, Laszlo Molnar & John Reiser   Sep 30th 2013

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
  10308504 <-   2619128   25.41%  netbsd/elf386  move_unpacked

Unpacked 1 file.

Now I had an unpacked binary. Turns out that even though this was the correct thing to do, this actually was even worse to look at! Literally every instruction was a MOV…?! mov instructions

I did some intense googling and found out that this binary was created using a tool called movfuscator. I tried to find a deobfuscator for it, however the only one that I could find did not do any good. So I kept looking and came across an interesting writeup on movfuscator that had a tracer program that would look at 1-byte memory writes using Intel PIN. So, I downloaded Intel PIN, compiled the program as instructed, and tried to use it. I also came across this other CTF writeup that used the same tool I was using in a CTF context. The way it was using it was to look at the patterns in the 1-byte writes, and see what was happening in response to changes in the input.

I applied this same type of thinking to the output that I was receiving. Since I knew that the flag started with ALEXCTF{, I used this to understand the output from the tracer program.

First, I tested using an empty string.

$ (echo "" | ../../../pin -t obj-ia32/tracer.so -- ../../../../move_unpacked); xxd trace-1byte-writes.bin
Guess a flag: Wrong Flag!
0000000: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000010: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000020: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000030: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000040: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000050: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000060: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000070: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000080: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000090: 0101 0000                                ....

Then, I started adding the flag format characters in one at a time and observed the changes.

$ (echo "A" | ../../../pin -t obj-ia32/tracer.so -- ../../../../move_unpacked); xxd trace-1byte-writes.bin
Guess a flag: Wrong Flag!
0000000: 0000 0100 0101 0000 0101 0000 0101 0000  ................
0000010: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000020: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000030: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000040: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000050: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000060: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000070: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000080: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000090: 0101 0000                                ....

$ (echo "AL" | ../../../pin -t obj-ia32/tracer.so -- ../../../../move_unpacked); xxd trace-1byte-writes.bin
Guess a flag: Wrong Flag!
0000000: 0000 0100 0000 0100 0101 0000 0101 0000  ................
0000010: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000020: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000030: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000040: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000050: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000060: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000070: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000080: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000090: 0101 0000                                ....

I noticed that there seemed to be 1 byte for each character in the flag. 0101 0000 indicated an incorrect character, and 0000 0100 indicated a correct character. I then started to write a script that would do this for me automagically, but realized I had an issue that would make things take a lot longer. My idea was to simply loop over all characters and check the binary output and see if I had put in a correct character. But what about lowercase vs. uppercase characters? I had done some preliminary scripting and realized that if I had to check 52 letters + symbols + digits, this would take a lot longer than if I could just check lowercase.

Luckily, there was also an output for this!

$ (echo "Al" | ../../../pin -t obj-ia32/tracer.so -- ../../../../move_unpacked); xxd trace-1byte-writes.bin
Guess a flag: Wrong Flag!
0000000: 0000 0100 0000 0000 0101 0000 0101 0000  ................
0000010: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000020: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000030: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000040: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000050: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000060: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000070: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000080: 0101 0000 0101 0000 0101 0000 0101 0000  ................
0000090: 0101 0000                                ....

An output of 0000 0000 indicated the right letter but wrong case. So I then finished my script so that I checked for these cases and brute forced the flag. Here is my full solution code

from string import ascii_lowercase, digits
import os

allChars = digits + '_}' + ascii_lowercase

flag = 'ALEXCTF{'
wrong = '\x01\x01\x00\x00'
right = '\x00\x00\x01\x00'
case = '\x00\x00\x00\x00'

def tryFlag(f):
    os.system('(echo "{}" | ../../../pin -t obj-ia32/tracer.so -- ../../../../move) > /dev/null'.format(f))
    data = open('trace-1byte-writes.bin', 'rb').read()
    offset = len(f) * 4
    return data[offset - 4:offset]

while flag[:-1] != '}':
    for c in allChars:
        result = tryFlag(flag + c)
        if result == case:
            c = c.upper()
            result = tryFlag(flag + c)
            
        if result == right:
            flag += c
            print flag
            break

The resulting flag was ALEXCTF{M0Vfusc4t0r_w0rk5_l1ke_m4g1c}

~Joel