CTF Writeups

[CTF] P’Hack – RAID dead redemption (forensics)

I took part in the P’Hack CTF with the Arn’Hack Team. It was a beginner friendly CTF, with a lot of easy challenges. I wanted to write about this forensic challenge “RAID dead redemption”. The goal was to recover data from a RAID partition composed of  3 disks, in which one of them was completely deleted. 

The challenge

Vous travaillez à la brigade spéciale du service cyberdéfense de la gendarmerie de Montargis. 
Les disques dur  d'une femme ont été saisis et viennent de vous être transmis. Elle est  suspectée d'avoir téléchargé de nombreux fichiers PNG et JPG dont elle  n'avait pas la propriété intellectuelle. Mais il semblerait qu'elle ait  eu le temps de supprimer une partie des preuves avant l'intervention.  Faites de votre mieux pour en extraire le maximum ! 
Le  manuel d'un logiciel suspect tournant sur l'ordinateur a également été  retrouvé et vous a été transmis pour vous guider dans votre enquête. 
You are working for the gendarmerie cybersecurity division in Montargis.
A woman’s hard drives were seized and were transfered to you. She is suspected of having downloaded a lot of PNG and JPG files of which she hadn’t the intellectual property. It seems she had enough time to delete a part of the evidence before your colleagues grabed her. Do your best to recover as much as possible !
A suspicious software’s user manual was also recoverd on the computer, and was send to your department to help you in your investigation. 

Here are the files that were attached to the challenge :

If we take a look at the three files, DISK1.bin and DISK3.bin are exactly the same size, and DISK2.bin is completely empty. It’s probably the disk that was purged. Let’s take a look at this user manual : 

So the backup system is using RAID 5, in left-asynchronous mode, with the data written from left to right. The chunk size is 1 byte. 

A word on RAID 5

RAID 5 is a backup system using at least 3 disks. It’s made for integrity. Build on a distributed parity system, RAID 5 allows you to recover the entire data from a partial or complete data loss on one single drive. It reaches his limit when more than one drive is corrupted.

Each drive is divided  in fix sized chunks. When considering two drives, the xor of two equally indexed chunks results in the value of the third drives chuck. 

The previous image is very close to the structure offered by the challenge. The parity block starts on the last drive, and form a diagonal. The data is written from left to right (A1 > A2 > A3 > B1 > ...). 
This all means that our job is actually pretty easy. We have to xor the two files to recover the missing drive, and then just put the data back in right order for us to read it.

Solving the challenge

As I was scripting at the same time as I was trying to figure out how this RAID 5 implementation worked, I have to different scripts. A very simple one, to xor the two files (byte per byte), and the second one to put the data back together.

# XOR DISK1 and DISK3 into DISK2

disk1 = bytearray(open("DISK1.bin", "rb").read())
disk2 = open("DISK2.bin", "wb")
disk3 = bytearray(open("DISK3.bin", "rb").read())

toWrite = bytearray()
for i in range(0,len(disk1)):
    toWrite.append(disk1[i] ^ disk3[i])


Now let's check if our xor was successful.

647 KiB   DISK1.bin 
647 KiB   DISK2.bin 
647 KiB   DISK3.bin

The size of our newly created DISK2 is exactly the same as the two others. It's encouraging.

╭─leo@precision ~/Downloads 
 ╰─$ xxd DISK1.bin| head 
 00000000: 894e 071a 000d 4944 0002 00f3 0800 ff59  .N….ID…….Y
 00000010: 5c03 004c baff 84a5 6eb4 b370 978b 6f00  ..L….n..p..o.
 00000020: 0100 84b1 718d b16e 981e 048f 754f ffb0  ….q..n….uO..
 00000030: 5a86 a76b 8a86 3793 af39 958c 7096 846e  Z..k..7..9..p..n
 00000040: 8db0 7031 0c5c 8340 7835 c771 783d 6172  ..p1..@x5.qx=ar
 00000050: 463a 9029 4964 3451 fab3 5596 3437 3e51  F:.)Id4Q..U.47>Q
 00000060: 4541 e469 840a 3573 187e 8474 3ab7 2809  EA.i..5s.~.t:.(.
 00000070: 861b 3329 f43a 6f31 728e 7168 4221 5d2e  ..3).:o1r.qhB!].
 00000080: 5068 2eb6 e98e 6c77 cba0 6f6a d16a 8b63  Ph….lw..oj.j.c
 00000090: 5146 ec6c 612d 7f92 d451 907f 6e66 c96c…
 ╭─leo@precision ~/Downloads 
 ╰─$ xxd DISK2.bin| head
 00000000: 5009 0d0a 0000 4816 00a9 0001 0300 00a7  P…..H………
 00000010: 5c00 5018 45ff ea37 1583 2016 e518 1800  .P.E..7.. …..
 00000020: 0002 eb34 35ff 3637 eb1a 06fb 3769 00fe  …45.67….7i..
 00000030: 6cf4 361a fa37 1ffd 1d23 e418 1be6 1619  l.6..7…#……
 00000040: fd38 3a0f 596b e836 6977 5d17 036d 6f07  .8:.Yk.6iw]
 00000050: 6855 fc15 630d 726b 00f9 69e7 1d53 7068  hU..c.rk..i..Sph
 00000060: 5f0c 643b e83b 3405 6370 e842 3780 210a  .d;.;4.cp.B7.!.
 00000070: eb3d 4f12 5a22 1d5e 64e1 1458 116c 711e  .=O.Z".^d..X.lq.
 00000080: 2f42 6e5f e9e1 1a76 00cb 6107 5c18 fb33  /Bn…v..a...3
 00000090: 5210 6e32 0456 6c00 9264 e11c 780f 5c1f  R.n2.Vl..d..x..
 ╭─leo@precision ~/Downloads 
 ╰─$ xxd DISK3.bin| head
 00000000: d947 0a10 000d 0152 00ab 00f2 0b00 fffe  .G…..R……..
 00000010: 0003 5054 ff00 6e92 7b37 9366 7293 7700  ..PT..n.{
 00000020: 0102 6f85 4472 8759 7304 0274 4226 ff4e  ..o.Dr.Ys..tB&.N
 00000030: 3672 9171 70b1 286e b21a 7194 6b70 9277  6r.qp.(
 00000040: 7088 4a3e 5537 6b76 1142 9a66 7b50 0e75  p.J>U7kv.B.f{P.u
 00000050: 2e6f 6c3c 2a69 463a fa4a 3c71 2964 4e39  .oliF:.J@..ov..k.m.rpP
 00000090: 0356 825e 657b 1392 4635 7163 1669 9573  .V.^e{..F5qc.i.s

Here I noticed the letters P, N and G, so I looked for the PNG header :

89 50 4E 47 0D 0A 1A 0A

If we look closely, we can recover this header by reading the DISKS in this order : D1, D2, p_D3, D1, p_D2, D3, p_D1, D2, D3, ...
With D1-D3 being DISK1-DISK3 and the p_ prefix meaning the byte is ignored since it's parity. If you compare the reading order to get a PNG header with this illustration, it's a match.

All we have to do, is reconstruct the data :

disk1 = bytearray(open("DISK1.bin", "rb").read())
disk2 = bytearray(open("DISK2.bin", "rb").read())
disk3 = bytearray(open("DISK3.bin", "rb").read())

out = open("reconstructed.png", "wb")

def shift_partity(p):
    p = p - 1
    if p == 0:
        p = 3
    return p

toWrite = bytearray()
p = 0
for i in range(0,len(disk1)):
    p = shift_partity(p)
    if p == 3:
    elif p == 2:


Here we go but... were is the flag ?

On last step is required before claiming our points. Let's upload our image to, to discover hidden data. If you download the binwalk result, and extract it you'll get a bunch of images.

Here we go 😉