Challenge Name: diskomatic
Point Value: 200
Description: It slices, it dices, it juliennes fries!
Tags: forensics
The challenge is a binary file with a forensics tags, so I assumed it would be pulling some data out of some other file format.
Step 1: Unpack
$ file diskomatic-98757b0043b8aea77e9014cff8ead7b1
diskomatic-98757b0043b8aea77e9014cff8ead7b1:
gzip compressed data, last modified: Mon Jun 22 00:08:37 2015, max
compression, from Unix
Simple enough.
$ zcat diskomatic-98757b0043b8aea77e9014cff8ead7b1 > diskomatic.dat
$ file diskomatic.dat diskomatic.dat:
DOS/MBR boot sector, code offset 0x58+2, OEM-ID "mkdosfs", FAT 1,
Media descriptor 0xf8, sectors/track 32, heads 64, sectors 250368
(volumes > 32 MB) , FAT (32 bit), sectors/FAT 1941, serial number
0x20550574, label: "DISKOMATIC "
Step 2: Mount Filesystem
So we have a FAT filesystem image. Let's try mounting it and seeing what's inside.
$ sudo mount -t vfat ./diskomatic.dat ~/mnt/dos/
$ ls -la ~/mnt/dos
total 5
drwxr-xr-x 2 root root 512 Dec 31 1969 .
drwxr-xr-x 3 grind grind 4096 Aug 6 12:41 ..
$ df -h | grep dos
/dev/loop0 122M 512 122M 1% /home/grind/mnt/dos
$ sudo umount ~/mnt/dos
Interesting so from the filesystem's perspective there are no valid files, but we have 122MB of something.
Step 3: Analyze the Binary
$ binwalk -e diskomatic.dat
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
17787904 0x10F6C00 PNG image, 1600 x 900, 8-bit/color RGB, non-interlaced
34088942 0x20827EE Minix filesystem, V1, little endian, 30 char names, 23429 zones
binwalk found a PNG file, but was unable to extract it. Interesting... perhaps it is corrupted somehow. I don't know where the Minix FS detection came from, but it's a false positive.
So let's open up the file in a hex editor and take a look at location
0x10F6C00.
Step 4: Hex Editor
The beginning of the file is as `file` advertised, a FAT32 partition table. Let's go to location
0x10F6C00.
Here's the PNG header, with the first IDAT section but it is noticeably
quite short. Searching for IDAT we find many more of these sections.
Each one is exactly 512B, which I believe is the block size for FAT32.
So it seems we have a PNG file which has been fragmented and we need to
piece it back together. Well let's try the naieve approach, and just
concatenate all the chunks. We can do this from within the hex editor
by cropping the file from location
0x10F6C00 onward and then doing a find and replace on all the
\x00\x00\x00\x00 words with nothing.
We'll save this file as
diskomatic.png and open it.
Well that didn't work. But the CRC error is a great hint. PNG is a simple format with chunks split into just 4 fields.
Length |
Chunk type |
Chunk data |
CRC |
4 bytes |
4 bytes |
Length bytes |
4 bytes |
But you don't have to take my word for it, you can read the Wikipedia article.
Looking at the 512B blocks in the hex editor again, we see that the PNG chunks are split in the middle of the data section, leaving some data and the CRC for that data at the front of the next block. So we have a fragmented file, but we have a method of determining in which order the pieces are supposed to fit back together by combining two blocks and then checking whether the CRC for the PNG chunk is correct. So now we have an idea of the problem and a solution, but first we'll need to extract each block of the PNG file.
Step 5: Programmatically Extracting FAT32 Blocks
At first I just selected a few blocks in the hex editor and saved them as new files. But after about 6 of these, with the location not moving much, I realized I'd probably need to automate this.
$ grep -abc IDAT diskomatic.dat
4976
Yep, I definitely don't want to copy'n'paste 5000 times by hand. So I wrote some Python:
FILE = "diskomatic.dat"
def get_parts():
fh = open(FILE, 'rb')
parts = []
# we know the location of the first part from `binwalk`
fh.seek(0x010F6C00)
p_idx = 0
part = ""
try:
while True:
block = fh.read(512)
if len(block) == 0:
break
if block[0:4] != "\x00\x00\x00\x00":
fh2 = open("parts/p"+str(p_idx)+".png.part", "wb")
fh2.write(block)
fh2.close()
p_idx += 1
offset = fh.tell()
if offset % 0x100000 == 0:
print "0x%x" % offset
except EOFError:
fh.close()
This writes all the blocks/parts out, 1 per file, and puts them in a directory called parts. After running this code we have 6238 files.
$ cd parts
$ ls | wc
6238 56144 354468
Step 6: Reassemble the Blocks
First I tested my hypothesis that all the blocks we needed were present,
by programmatically concatenating every block with the first block, and
was successful finding one block.
From that code, I moved on to
checking every block against every other block, and constructing the
correct ordering of blocks in a dictionary. Then writing that string of
blocks out into a single PNG file. The full code, excluding the
get_parts() function is:
#!/usr/bin/env python
import binascii
import struct
FILE = "diskomatic.dat"
NUM_CHUNKS = 6238
FIRST_PART = 0
def tokenize_chunk(data):
length = struct.unpack("!I", data[0:4])[0]
chunk = data[0:4+4+length+4]
return chunk
def png_chunk_verify_crc(chunk):
length = struct.unpack("!I", chunk[0:4])[0]
ctype = chunk[4:8]
data = chunk[8:8+length]
crc = struct.unpack("!I", chunk[8+length:8+length+4])[0]
# The CRC is a network-byte-order CRC-32 computed over the chunk type and
# chunk data, but not the length.
crc_c = (binascii.crc32(chunk[4:-4]) & 0xFFFFFFFF)
valid = (crc == crc_c)
return valid
def read_parts():
parts = []
# XXX: magic number: that's how many chunks were in the original file
for i in xrange(0, NUM_CHUNKS):
fh = open("parts/p"+str(i)+".png.part", "rb")
data = bytearray(fh.read())
parts.append(data)
fh.close()
return parts
def compare_parts(parts):
parts_dict = {}
for i in xrange(1, NUM_CHUNKS):
first_two = parts[0] + parts[i]
# XXX: magic number, for first one, because there's the PNG header
chunk = tokenize_chunk(first_two[0x2E:])
matched = png_chunk_verify_crc(chunk)
if matched:
print "Chunk 0 and %d matched!" % i
parts_dict[0] = i
break
for i in xrange(1, NUM_CHUNKS):
matched = False
for j in xrange(1, NUM_CHUNKS):
if i == j:
continue
two_parts = parts[i] + parts[j]
# XXX: magic number, most other parts seem to have the same offset
offset_to_chunk = 0x2E
chunk = tokenize_chunk(two_parts[offset_to_chunk:])
matched = png_chunk_verify_crc(chunk)
if matched:
if i % 100 == 0:
print "Chunk %d and %d matched!" % (i, j)
parts_dict[i] = j
break
if not matched:
print "Chunk %d had no match!" % (i)
return parts_dict
def write_parts(parts, parts_dict):
fh = open("final.png", "wb")
next_key = 0
for i in parts_dict.iterkeys():
fh.write(parts[next_key])
try:
next_key = parts_dict[next_key]
except KeyError:
print "Hit KeyError"
fh.close()
def main():
parts = read_parts()
parts_dict = compare_parts(parts)
write_parts(parts, parts_dict)
if __name__ == "__main__":
main()
The nested for loops do some unnecessary work, but the whole thing runs in under 5 minutes on my laptop.
Step 7: Collect the Flag
Now we have a (hopefully) valid PNG. Let's open it.