Tokyo Westerns CTF 2016 - Recovery 1: deadnas

Wed, Sep 7, 2016

Writeups #ctf #tokyowesterns #2016 #forensics

The situation was given as such:

Today, our 3-disk NAS has failed. Please recover flag.

deadnas.7z

Upon extraction, the file list was as such

root@kali:~# 7z x deadnas.7z 

7-Zip [64] 9.20  Copyright (c) 1999-2010 Igor Pavlov  2010-11-18
p7zip Version 9.20 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,2 CPUs)

Processing archive: deadnas.7z

Extracting  deadnas/disk0
Extracting  deadnas/disk1
Extracting  deadnas/disk2
Extracting  deadnas

Everything is Ok

Folders: 1
Files: 3
Size:       1048588
Compressed: 499418
root@kali:~# ls -la deadnas
total 1036
drwxr-xr-x  2 root root   4096 Sep  2 19:24 .
drwxr-xr-x 17 root root   4096 Sep  7 16:50 ..
-rw-r--r--  1 root root 524288 Jun 16 13:56 disk0
-rw-r--r--  1 root root     12 Jun 16 14:01 disk1
-rw-r--r--  1 root root 524288 Jun 16 13:56 disk2

and running file on all the files showed a FAT32 file, a text file, and a data file

root@kali:~# cd deadnas/
root@kali:~/deadnas# file *
disk0: DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "mkfs.fat", sectors/cluster 4, root entries 512, sectors 2048 (volumes <=32 MB) , Media descriptor 0xf8, sectors/FAT 2, sectors/track 32, heads 64, serial number 0x867314a9, unlabeled, FAT (12 bit)
disk1: ASCII text
disk2: data

Now, we know that this is a NAS device and that there are 3 drives. The first thing I thought to do was open the first disk (disk0) in dff GUI and see what it shows.

dff screenshot

Of cource, it couldn’t be that simple. We can see here that there are a bunch of C files for some kind of source code, but nothing that looks like a flag. In addition to that, if we look at the contents of some of these files, such as configure, we can see that they appear to be broken in some parts, plaintext in others, and it only goes up to 0x200, or 512.

dff screenshot 2

Now, if you know much about NAS’s, more often than not you put your drives into a RAID configuration. The most common RAID types that are used are 0, 1, 5, 6, and 10 and each has it’s own advantages and disadvantages. Due to how we can see the data is not sequantial and consistent, it is safe to assume that these drives are likely in RAID and it is most likely not RAID 0, 1, and 10, especially with only 3 drives in the array.

That leaves RAID 5 and 6. Now, to understand how these are especially good choices based on that this is a challenge and we are assuming it can be solved, we must first understand how RAID 5 and 6 behave. Since RAID 6 is based on RAID 5, I will briefly cover how RAID 5 works in a simplified form.

In RAID 5, you must have at least 3 drives in your array. When data is written to the drives, it is striped into sections based on your configuration. Every drive gets part of the data every stripe, sequentially, and one of the drives is used for parity. When data is written to the drives, it is XOR’d and that result is written to the block on the parity drive (in addition, the drive used for parity rotates forwards or backwards each block). The unique nature of the XOR bitwise function is that it is totally reversible, as long as you have the other factors in the equation.

RAID 5 diagram image courtesy Wikipedia

For example,

A XOR B = C
C XOR B = A
C XOR A = B

As you can see, this is very advantagous because it allows us to recover data across stripes, not just for a single drive. RAID 6 is very similar to RAID 5 in that it does the same thing, however it adds another parity block for additonal redundancy and the computation of the second parity block is more complicated than a simple XOR operation.

I took the liberty to assume that it was likely RAID 5 (which was later confirmed with hints provided to the challenge) and wrote out a simple script in python that would be able to rebuild our missing drive and combine the 3 drives into a single disk image.

with open('disk0', 'rb') as f:
    disk0 = f.read()

with open('disk2', 'rb') as f:
    disk2 = f.read()

#XOR data in disk0 and disk2 to rebuild disk1
disk1 = ''.join([chr(ord(d0) ^ ord(d2)) for d0, d2 in zip(disk0, disk2)])

#create a RAID 5 "array"
raidArray = [disk0, disk1, disk2]

#our block size is 512 (remember from earlier how it cut off at 0x200?)
BS = 512

#open our output disk file
with open('disk', 'wb') as f:
    #iterate over the blocks (disk length / 512 bytes per block)
    for blockIndex in xrange(len(disk0) / BS):
        #calculate our parity drive index rotating backwards, starting with the last drive
        parityIndex = (2 - blockIndex) % 3
	
        #iterate over the 3 drives in the array
        for driveIndex in xrange(3):
            #make sure not to pull data from the parity drive, we only want actual data
            if driveIndex != parityIndex:
                #calculate our starting byte position
                blockStart = blockIndex * BS
		
                #write the data from the starting byte to 512 bytes after it from the target drive
                f.write(raidArray[driveIndex][blockStart:blockStart + BS])

After execution, we are left with a file, disk, with a filesize of 1024kb and it’s file output is

root@kali:~/deadnas# file disk
disk: DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "mkfs.fat", sectors/cluster 4, root entries 512, sectors 2048 (volumes <=32 MB) , Media descriptor 0xf8, sectors/FAT 2, sectors/track 32, heads 64, serial number 0x867314a9, unlabeled, FAT (12 bit)

Looks promising, let’s see if it’s valid…

We mount the image using losetup

root@kali:~/deadnas# losetup /dev/loop0 disk

And we are left with a file, flag.jpg, in our mounted disk! deadnas flag

P.S. Remember those C files we saw in dff? That was the source code to which-2.21, just to throw us off…

~Joel