social



BSIDES CTF 2017 - ximage writeup

Well this was a fun one.

So what we've got here is this archive of seven BMP images: ximage.zip, and we also know from the description that every one of them should contain the same flag, starting with 'FLAG:'. Thumbing through them, we can see at a glance that they all are peppered with a seemingly random arrangement of colored dots, most of them having 0x90 for the red byte, some 0x80 for the blue one. Also, one of the images, neoncow.bmp, is the clear winner for starting a more thorough examination - it only has six colors in it besides the dots. So let's write a script to see what colors the dots in neoncow.bmp are of and how often they are occuring:

from PIL import Image
import operator
import pprint 

img = Image.open("neoncow.bmp")

colors = {}

pixels = img.load()
w,h = img.size

for y in range(h):
    for x in range(w):
        if pixels[x,y] not in [
                (0xfd, 0xfd, 0xfd),
                (0x07, 0x90, 0x06),
                (0xfd, 0x00, 0xeb),
                (0xfd, 0xff, 0x24),
                (0x96, 0x3c, 0xfd),
                (0x24, 0x2c, 0x03),
            ]:

            pixels_repr = ''.join(["%02x" % _ for _ in pixels[x,y]])
            if pixels_repr not in colors:
                colors[pixels_repr] = 0
            colors[pixels_repr] += 1

pprint.pprint(sorted(colors.items(), key=operator.itemgetter(1))[::-1])

Giving us this output:

(ctf) tr@karabut.com:~/work/ctf/bsides2017/ximage$ python count_neoncow.py 
[('90c031', 47),
 ('9080cd', 47),
 ('9004b0', 46),
 ('900000', 3),
 ('022980', 3),
 ('012980', 3),
 ('310180', 2),
 ('90db31', 2),
 ('032980', 2),
 ('020180', 2),
 ('202980', 2),
 ('060180', 2),
 ('322980', 1),
 ('340180', 1),
 ('909059', 1),
 ('030180', 1),
 ('070180', 1),
 ('320180', 1),
 ('362980', 1),
 ('90c0fe', 1),
 ('3c0180', 1),
 ('2c0180', 1),
 ('312980', 1),
 ('290180', 1),
 ('200180', 1),
 ('2e2980', 1),
 ('2c2980', 1),
 ('2e0180', 1),
 ('2a0180', 1),
 ('040180', 1),
 ('302980', 1),
 ('90d231', 1),
 ('5a2980', 1),
 ('2d0180', 1),
 ('0b2980', 1),
 ('052980', 1),
 ('2f2980', 1),
 ('010180', 1),
 ('0d2980', 1),
 ('0000e8', 1),
 ('0001c6', 1),
 ('0001bb', 1),
 ('0001ba', 1),
 ('050180', 1)]

So our initial assessment is pretty much right: we have here mostly colors with red 0x90 or blue 0x80, with the exception of four bytes each occuring once: 0000e8, 0001c6, 0001bb and 0001ba. Also, pixels 90c031, 9080cd and 9004b0 are the definite majority, and xxxx80 ones all seem to be having a form of either xx0180 or xx2980. But where are those 00 exceptions in the image? Well, they can easily be detected at the bottom of it, placed right next to one of those 900000 pixels each, making pairs like 0001bb900000.

Looking at the other images again, we can confirm that those pairs are found at the bottom of each of them. Again, 90c031, 9080cd and 9004b0 occur throughout every picture. There's no visible correlation between these pixels' coordinates, with the exception of the pairs. Let's get back to neoncow.bmp and try to understand their meaning.

There's no pixels that look particularly like ASCII, but at this point of solving the challenge I started to really wonder about specifically 9080cd groups and those pairs along with them. They definitely seemed to remind me of something. So, to make more sense of pairs, I tried to interpret them as BGR rather than RGB, which made them look like bb0100000090 or ba0100000090. Also, it made 9080cd into cd8090, and then it all clicked, because at this point (especially after completing the nibbler challenge -- seriously, read that) I was pretty used to seeing cd80 here and there. To clarify, using the Defuse online assembler,

0:  cd 80                   int    0x80
2:  90                      nop
3:  bb 01 00 00 00          mov    ebx,0x1
8:  90                      nop
9:  ba 01 00 00 00          mov    edx,0x1
e:  90                      nop

So what about other, more prevalent colors in the neoncow.bmp picture? Well...

0:  fd                      std
1:  fd                      std
2:  fd                      std
3:  06                      push   es
4:  90                      nop
5:  07                      pop    es
6:  eb 00                   jmp    0x8
8:  fd                      std
9:  24 ff                   and    al,0xff
b:  fd                      std
c:  fd                      std
d:  3c 96                   cmp    al,0x96
f:  03 2c 24                add    ebp,DWORD PTR [esp]

Yep, all essentially no-ops, and the other images are sure to be comprised mostly of no-op gadgets as well. So it seems we have our task cut out for us: again, skipping all the no-op colors of neoncow.png, let's see what happens if we disassemble everything else. And let's use capstone this time:

from capstone import *

from PIL import Image

md = Cs(CS_ARCH_X86, CS_MODE_32)

img = Image.open("neoncow.bmp")

pixels = img.load()
w,h = img.size

s = ""
for y in range(h):
    for x in range(w):
        if pixels[x,y] not in [
                (0xfd, 0xfd, 0xfd),
                (0x07, 0x90, 0x06),
                (0xfd, 0x00, 0xeb),
                (0xfd, 0xff, 0x24),
                (0x96, 0x3c, 0xfd),
                (0x24, 0x2c, 0x03),
            ]:
                s += ''.join([chr(_) for _ in pixels[x,y][::-1]])

for inst in md.disasm(s, 0):
    print "0x%x:\t%s\t%s" % (inst.address, inst.mnemonic, inst.op_str)

Executing the script, we get the listing available here in full. But it doesn't look right! In fact, it looks reversed -- indeed, if we just follow it from the bottom up, we'll see really familiar stuff like

0x23a:  call    0x23f           certainly broken by skipping the no-ops
0x240:  xor ebx, ebx
0x234:  mov ebx, 1
0x231:  pop ecx                 ecx should now point at 0x23a
0x22e:  mov byte ptr [ecx], 0
0x22b:  xor edx, edx
0x225:  mov edx, 1
0x222:  add byte ptr [ecx], 0x2a 
0x21f:  xor eax, eax
0x21c:  mov al, 4
0x219:  int 0x80                write(stdout, "\x2a", 1)

Right, it's write calls all the way up, putting out one byte at a time, starting with 0x2a (*) and continuing from there by adding to or substracting from ecx (all those xx0180 and xx2980 pixels). We can actually just follow it manually and see the letters "FLAG:" forming, but let's do it right!

We don't really have to execute the program, just see what happens with ecx and how many times int 0x80 is seen between those occurences, so:

(ctf) tr@karabut.com:~/work/ctf/bsides2017/ximage$ python process_neoncow.py > disas.out
f = open("disas.out")

lines = f.readlines()

s = ""
ecx = 0

for l in lines[::-1]:  # reverse the instructions
    if 'byte ptr [ecx]' in l:
        tmp = l.strip().split(', ')[1]
        if tmp.startswith("0x"):
            tmp = int(tmp, 16)
        else:
            tmp = int(tmp)

        if 'add' in l:
            ecx += tmp
        elif 'sub' in l:
            ecx -= tmp

    elif 'int\t0x80' in l:
        s += chr(ecx)

print s

And...

(ctf) tr@karabut.com:~/work/ctf/bsides2017/ximage$ python reverse.py 
***
LLAGc33dbbf0298eceb3edcd6d250ffd8d30d
**

Hmm, this doesn't seem right! What about some other images? We can just skip any pixels that don't have 0x90 in the red or 0x80 in the blue byte:

happycow.bmp:

***
FLAG:c3dbbf0288ceebeddcd6d2505fddd00d
**

alcatraz.bmp:

***
FLAG:33dbff0298eceb3eccd6d250ffd8d30d
***

fireescape.bmp:

***
FLAG:cddbbf229eece33dcc66d2505fd8d30

***

Looks like each of these has some errors! How could that be though? Looking through the listings manually, we can see that sometimes syscalls seem to be out of place. Maybe the author provided us with that many images for us to be able to correct the deliberately placed errors? What if, say, we try to rely on mov al, 4 and not int 0x80 for printing the characters?

neoncow.bmp:

***
FLAGc33dbbf0298eceb3edcd6d250ffd8d30d
***

happycow.bmp:

***
FLAG:c3dbbf0298eceb3ddcd6d2505fddd30d
***

alcatraz.bmp:

***
FLAG:c3dbbf0298eceb3edcddd2505fd8d30d
***

fireescape.bmp:

***
FLAG:c3dbbf0298eceb3edcd6d2505fd8d30d
***

This definitely seems tidier. There are still errors, though, but this time, comparing the outputs for correction, we can tell that the flag is probably FLAG:c3dbbf0298eceb3edcd6d2505fd8d30d. And in fact it is!

But still, what's with all those errors? Weren't the images meant to be executed as they are?

I pondered this question for some time after getting the flag, when it suddenly struck me: how were I able to reverse the listings if there are clearly some instructions taking up two pixels (those pairs of mov ebx, 1 for example)? They definitely must be preserved in that order for the code to work!

And then I remembered how BMPs are stored in memory: left to right, but bottom to top; so maybe try reversing the scanlines while reading the images to obtain the listings, and process them like that? I'm skipping the 2017_logo_small2.bmp here, because it evidently uses some different means of printing out the flag, judging by lots of paired pixels in it:

from capstone import *
import glob
from PIL import Image

md = Cs(CS_ARCH_X86, CS_MODE_32)

for fn in glob.glob("*.bmp"):

    img = Image.open(fn)
    pixels = img.load()
    w,h = img.size

    s = ""
    for y in range(h-1, -1, -1):
        for x in range(w):
            if pixels[x,y][0] == 0x90 or pixels[x,y][2] == 0x80:   
                s += ''.join([chr(_) for _ in pixels[x,y][::-1]])

    lines = [] 
    for inst in md.disasm(s, 0):
        lines += ["0x%x:\t%s\t%s" % (inst.address, inst.mnemonic, inst.op_str)]

    print fn

    ecx = 0
    out = ""
    for l in lines:
        if 'byte ptr [ecx]' in l:
            tmp = l.strip().split(', ')[1]
            if tmp.startswith("0x"):
                tmp = int(tmp, 16)
            else:
                tmp = int(tmp)

            if 'add' in l:
                ecx += tmp
            elif 'sub' in l:
                ecx -= tmp

        elif 'int\t0x80' in l:
            out += chr(ecx)

    print out
(ctf) tr@karabut.com:~/work/ctf/bsides2017/ximage$ python extract_flags.py 
fireescape.bmp
***
FLAG:c3dbbf0298eceb3edcd6d2505fd8d30d
***


alcatraz.bmp
***
FLAG:c3dbbf0298eceb3edcd6d2505fd8d30d
***


happycow.bmp
***
FLAG:c3dbbf0298eceb3edcd6d2505fd8d30d
***


angry_cow.bmp
***
FLAG:c3dbbf0298eceb3edcd6d2505fd8d30d
***


660px-San_Francisco_districts_map.bmp
***
FLAG:c3dbbf0298eceb3edcd6d2505fd8d30d
***


neoncow.bmp
***
FLAG:c3dbbf0298eceb3edcd6d2505fd8d30d
***

All better now!

All in all, this was a very entertaining challenge; sadly, after mostly skipping the second day, I managed to get the first listing just a few minutes before the deadline and wasn't able to guess the correct flag in that time. I certainly appreciate the imagination and work put into building a generator for these executable images by Ron Bowes (@iagox86), what's with all the beautiful no-op pixels in there, and look forward for more great puzzles made by him ;)