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 ;)