Part 1/3
Challenge
Lors d’une de ses investigations, le ministère est tombĂ© sur une mystĂ©rieuse clĂ© USB. Les donnĂ©es disposĂ©es sur ce pĂ©riphĂ©rique sont de forme inconnue.
PersuadĂ© que ce système est disponible en open-source, il vous est demandĂ© Ă vous, expert du renseignement en sources ouvertes et de l’analyse forensique, de mener une investigation.
Seriez-vous capable de retrouver le dépôt associé à ce système et de retrouver une première information confidentielle sur le périphérique ?
SHA256(dump.bin) 6d1476c3bd00856491455f3ab9eeed6da37383fc7f73a4343ce812e98f25a650 : 104857600 octets
Format du flag: SHLK{[a-zA-Z0-9_]}
Solution
dump.bin file identification
In this challenge we are given a dump.bin
file. Our first goal is to analyze and identify its format.
$ file dump.bin
dump.bin: data
Nothing interesting comes out of the file utility. Let’s examine the strings contained in the dump.
$ strings dump.bin
sfkcolrehs
Watson's old device - CONFIDENTIAL
...
Interestingly, sfkcolrehs
happens to be the mirror of sherlockfs
. A quick search leads us to the following GitHub repo. As the name of the repo and the challenge description suggest it, dump.bin
is a dump of an encrypted filesystem.
Identifying the vulnerability
Going through the GitHub repo teaches us that there is a utility named shlkfs.mount
based on FUSE, whose purpose is to mount the filesystem.
SherlockFS v1 - Mounting a SherlockFS filesystem Usage: ./build/shlkfs.mount [-k|--key <PRIVATE KEY PATH>] [-v|--verbose] <DEVICE> [FUSE OPTIONS] <MOUNTPOINT>
In order to mount the encrypted filesystem we need a private RSA key that will be used to decrypt the filesystem content. However no key is provided… Let’s dive in the code.
After exploring the code we end up finding the structure of our filesystem.
// file: include/cryptfs.h
#define CRYPTFS_BLOCK_SIZE_BYTES 4096
/* ... */
struct CryptFS
{
struct CryptFS_Header header; // BLOCK 0: Header
struct CryptFS_KeySlot keys_storage[NB_ENCRYPTION_KEYS]; // BLOCK 1-64: Keys
struct CryptFS_FAT first_fat; // BLOCK 65: First FAT
struct CryptFS_Entry root_entry; // BLOCK 66: Root directory entry
uint8_t
padding[CRYPTFS_BLOCK_SIZE_BYTES
- sizeof(struct CryptFS_Entry)]; // CryptFS_Entry unused space
struct CryptFS_Directory
root_directory; // BLOCK 67: Root directory directory
} __attribute__((packed, aligned(CRYPTFS_BLOCK_SIZE_BYTES)));
It seems that the filesystem is split into 4096-byte blocks.
Blocks 1 to 64 are used to stored encryption keys, let’s look at their format.
// file: include/cryptfs.h
struct CryptFS_KeySlot
{
uint8_t occupied; // 1 if the slot is occupied, 0 if free
uint8_t aes_key_ciphered[RSA_KEY_SIZE_BYTES]; // AES key ciphered with RSA
uint8_t rsa_n[RSA_KEY_SIZE_BYTES]; // RSA public modulus 'n'
uint32_t rsa_e; // RSA public exponent 'e'
} __attribute__((packed, aligned(CRYPTFS_BLOCK_SIZE_BYTES)));
So it look like the filesystem is encrypted with AES and that the AES key is itself encrypted with the user’s RSA private key.
After digging a bit in the commit history of the repo, looking for leaked RSA private key, we end up finding a script used to generate private keys.
Looking at the code we quickly see that the private key generation is flawed.
if __name__ == "__main__":
key_size = 2048
while True:
p = generate_prime(key_size - 20)
q = generate_prime(20)
n = p * q
if n.bit_length() == key_size:
break
The private key is built using a factor of 20 bits only, because of that we can easily factor the RSA public modulus and thus recover the private key.
Private key recovery & mounting the filesystem
We recover the private key by factoring the RSA public modulus using the following python script.
from sympy import factorint
from Crypto.PublicKey import RSA
with open('dump.bin', 'rb') as f:
f.seek(0x1000 + 0x100 + 1) # Seek at the 'rsa_n' member of the first CryptFS_KeySlot struct
n_modulus = int.from_bytes(f.read(0x100), 'big')
e = int.from_bytes(f.read(4), 'little')
p, q = factorint(n_modulus).keys()
d = pow(e, -1, (p-1)*(q-1))
private_key = RSA.construct((n_modulus, e, d))
with open('private.pem', 'wb') as f:
f.write(private_key.export_key())
The next step is to build the shlkfs.mount
utility from sources. Gracefully authors of the challenge left a Dockerfile
in the repository so that we can compile the project with ease.
Now that we compiled shlkfs.mount
let’s mount the filesystem.
$ sudo mkdir /mnt/sherlock
$ sudo ./shlkfs.mount -k private.pem dump.bin /mnt/sherlock -f -s
[INFO]: Checking if the device 'dump.bin' is a SherlockFS device...
[ERROR]: Implementation not supported
[ERROR]: The device 'dump.bin' is not formatted. Please format it first.
Weird error, let’s look at the code again to understand why this error is raised.
// file: fs/filesystem/format.c
bool is_already_formatted(const char *device_path)
/* ... */
else if (header.version != CRYPTFS_VERSION)
{
print_error("Implementation not supported\n");
return false;
}
/* ... */
}
So it seems that the code is looking at the version field in the filesystem dump header. We remember that the header is stored in the first block of 4096 bytes and is defined by the following structure.
// file: include/cryptfs.h
#define CRYPTFS_BOOT_SECTION_SIZE_BYTES 2048
#define CRYPTFS_MAGIC "sfkcolrehs" // reverse("sherlockfs")
#define CRYPTFS_MAGIC_SIZE (sizeof(CRYPTFS_MAGIC) - 1) // exclude '\0'
#define CRYPTFS_LABEL_SIZE 128 // reverse("sherlockfs")
#define CRYPTFS_VERSION 1
/* ... */
struct CryptFS_Header
{
uint8_t boot[CRYPTFS_BOOT_SECTION_SIZE_BYTES]; // Reserved for boot code
// (bootloader, etc.)
uint8_t magic[CRYPTFS_MAGIC_SIZE]; // Magic number
uint16_t version; // CRYPTFS_VERSION
uint32_t blocksize; // in bytes
uint8_t label[CRYPTFS_LABEL_SIZE]; // Filesystem label
uint64_t last_fat_block; // Last FAT block index
} __attribute__((packed, aligned(CRYPTFS_BLOCK_SIZE_BYTES)));
Let’s take a look at our dump version.
$ xxd -s 0x80a -l 0x2 -p dump.bin
dead
Look like authors wanted to trick us with a fake version value :) Let’s set this value back to its expected value (1).
$ printf '\x01\x00' | dd of=dump.bin bs=1 seek=$((0x80a)) count=2 conv=notrunc
We should now be able to mount the filesystem.
$ sudo ./shlkfs.mount -k private.pem dump.bin /mnt/sherlock -f -s
[INFO]: Checking if the device 'dump.bin' is a SherlockFS device...
[INFO]: Loading private key 'private.pem' from disk...
[INFO]: Extracting master key from the device...
[INFO]: Mounting a SherlockFS filesystem instance...
[SUCCESS]: SherlockFS filesystem mounted successfully!
Perfect ! We can now extract the flag from the mounted filesystem.
$ sudo ls /mnt/sherlock
flag1.txt
$ sudo cat /mnt/sherlock/flag1.txt
***************************************************************
* *
* WARNING - CONFIDENTIAL *
* *
* This device is classified CONFIDENTIAL by the UK *
* Intelligence Service. Unauthorized access to this device *
* is strictly prohibited and will result in severe legal *
* consequences, including potential fines and imprisonment. *
* Access is limited to authorized personnel only. *
* *
***************************************************************
The flag is: SHLK{m0un71n94cu570mf1135y573m15qu173fun}
Flag: SHLK{m0un71n94cu570mf1135y573m15qu173fun}
Part 2/3
Challenge
Vous, ancien agent double, avez eu dans le passĂ© accès au pĂ©riphĂ©rique de stockage dump2.bin d’un service de renseignement Ă©tranger. Or, les services secrets concernĂ©s s’en sont rendus compte et ont supprimĂ© vos accès.
Vous vous souvenez cependant avoir effacé (proprement) un document PDF important sur ce même périphérique.
Trouvez un moyen, Ă l’aide du fichier dump2.bin ainsi que de votre ancienne clĂ© privĂ©e my_private.pem, de retrouver vos accès au pĂ©riphĂ©rique de stockage et de rĂ©cupĂ©rer le document PDF effacĂ©.
SHA256(dump2.bin) 146e74e46dc75385124a19f3a0013b05ef97687f245e18c66808f80d9bce1e7e : 104857600 octets
SHA256(my_private.pem) 0aeab3d6ade1c3488fceda108e205ff949dd58d32c5ab50697a722bcb213b4d3 : 1704 octets
Format du flag: SHLK{[a-z0-9]+}
Solution
Again we are given a dump of an encrypted filesystem, but this time we also have a private key. Let’s mount the filesystem.
$ sudo mkdir /mnt/sherlock
$ sudo ./shlkfs.mount -k my_private.pem dump2.bin /mnt/sherlock -f -s
[INFO]: Checking if the device 'dump2.bin' is a SherlockFS device...
[INFO]: Loading private key 'my_private.pem' from disk...
[ERROR]: The user with the private key 'my_private.pem' is not registred in the keys storage of the device 'dump2.bin'
Alright, so it look like the agent private key was deleted from the filesystem registered keys. But was it really …
Regaining access to the filesystem
First let’s recall how keys are stored.
// file: include/cryptfs.h
struct CryptFS_KeySlot
{
uint8_t occupied; // 1 if the slot is occupied, 0 if free
uint8_t aes_key_ciphered[RSA_KEY_SIZE_BYTES]; // AES key ciphered with RSA
uint8_t rsa_n[RSA_KEY_SIZE_BYTES]; // RSA public modulus 'n'
uint32_t rsa_e; // RSA public exponent 'e'
} __attribute__((packed, aligned(CRYPTFS_BLOCK_SIZE_BYTES)));
If we take a look at the second key slot block (offset 0x2000) we clearly see that a key was left there with theoccupied
field set to 0.
$ xxd -s 0x2000 -l 0x201 dump2.bin
00002000: 00ab 4494 502d 1fb4 57bd b855 e663 c8a4 ..D.P-..W..U.c..
[...]
000021f0: cf5b 747b 4f91 8aad 1cd7 573f 3faf bf51 .[t{O.....W??..Q
00002200: f1
Let’s set this field back to 1 and mount the filesystem.
$ printf '\x01' | dd of=dump2.bin bs=1 seek=$((0x2000)) count=1 conv=notrunc
$ sudo ./shlkfs.mount -k my_private.pem dump2.bin /mnt/sherlock -f -s
[INFO]: Checking if the device 'dump2.bin' is a SherlockFS device...
[INFO]: Loading private key 'my_private.pem' from disk...
[INFO]: Extracting master key from the device...
[SUCCESS]: SherlockFS filesystem mounted successfully!
Nice, now we can explore the filesystem.
$ sudo ls /mnt/sherlock -la
total 4
drwxrwxrwx 1 root root 2 Jun 5 01:02 .
drwxr-xr-x 8 root root 4096 Jul 7 09:47 ..
-rwxrwxrwx 1 root root 304 Jun 5 01:02 .bash_history.bak
-rwxrwxrwx 1 root root 466 Jun 5 01:02 notice.txt
$ sudo cat /mnt/sherlock/.bash_history.bak
~/shlkfs_mount /dev/sdb -f -s /mnt
nano /mnt/notice.txt
vi /mnt/notice1.txt
vi /mnt/notice2.txt
vi /mnt/notice3.txt
vi /mnt/notice4.txt
vi /mnt/notice5.txt
rm /mnt/notice2.txt
rm /mnt/notice3.txt
cp ~/flag2.pdf /mnt/flag2.pdf
rm /mnt/flag2.pdf
rm /mnt/notice1.txt
rm /mnt/notice4.txt
rm /mnt/notice5.txt
The bash history reveals that the agent copied a flag2.pdf
file to the filesystem prior deleting it. But was it really deleted …
Carving the “deleted” .pdf file
What we are going to do is a technique called file carving. In essence, this technique exploits the fact that certain filesystems do not truly delete data when a file is removed; instead, they merely remove the metadata, which renders the data inaccessible to the system.
// file: include/cryptfs.h
#define CRYPTFS_BLOCK_SIZE_BYTES 4096
/* ... */
struct CryptFS
{
struct CryptFS_Header header; // BLOCK 0: Header
struct CryptFS_KeySlot keys_storage[NB_ENCRYPTION_KEYS]; // BLOCK 1-64: Keys
struct CryptFS_FAT first_fat; // BLOCK 65: First FAT
struct CryptFS_Entry root_entry; // BLOCK 66: Root directory entry
uint8_t
padding[CRYPTFS_BLOCK_SIZE_BYTES
- sizeof(struct CryptFS_Entry)]; // CryptFS_Entry unused space
struct CryptFS_Directory
root_directory; // BLOCK 67: Root directory directory
} __attribute__((packed, aligned(CRYPTFS_BLOCK_SIZE_BYTES)));
Looking at the source code we understand that files metadatas and their content are stored after the 64th block (offset 0x41000). By opening the dump in a hex editor we can see that there is encrypted data from 0x41000 to 0x54000 (19 blocks).
Before carving the pdf file we will need to decrypt the filesystem content. Looking at the code of the read_blocks_with_decryption
function in fs/filesystem/block.c
we understand that each block of 4096 bytes is decrypted by the following function.
// file: fs/security/crypto.c
const unsigned char iv[] = "SherlockFScrypto";
/* ... */
unsigned char *aes_decrypt_data(const unsigned char *aes_key,
const void *encrypted_data,
size_t encrypted_data_size,
size_t *decrypted_data_size)
{
if (encrypted_data_size > INT_MAX)
return NULL;
// Setting up AES CTX
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (ctx == NULL
|| EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, aes_key, iv) != 1)
internal_error_exit("Failed to initialize AES decryption\n",
EXIT_FAILURE);
if (EVP_CIPHER_CTX_set_padding(ctx, 0) != 1)
internal_error_exit("Failed to disable padding\n", EXIT_FAILURE);
unsigned char *decrypted_data = xcalloc(1, encrypted_data_size);
int decrypted_data_size_int;
if (EVP_DecryptUpdate(ctx, decrypted_data, &decrypted_data_size_int,
encrypted_data, encrypted_data_size)
!= 1)
internal_error_exit("Failed to decrypt data\n", EXIT_FAILURE);
*decrypted_data_size = decrypted_data_size_int;
EVP_CIPHER_CTX_free(ctx);
return decrypted_data;
}
Each block is decrypted using AES 256-CBC with the IV SherlockFScrypto
.
Now that we figured out how blocks were decrypted all we need to do is to figure out the AES key used by the filesystem, decrypt the 19 blocks and carve the .pdf file in the decrypted result.
Let’s first extract the AES key. To do so i did modify the source code to print the key after it was extracted.
// file: fs/security/crypto_device.c
unsigned char *extract_aes_key(const char *device_path,
const char *private_key_path, char *passphrase)
{
/* ... */
unsigned char *aes_key = rsa_decrypt_data(
rsa_keypair, shlkfs->keys_storage[index].aes_key_ciphered,
RSA_KEY_SIZE_BYTES, &extraction_size);
printf("Decrypted aes key:\n");
for (int i=0; i < AES_KEY_SIZE_BYTES; i++) {
printf("%02x ", aes_key[i]);
}
/* ... */
}
After recompiling the project and running shlkfs.mount
again, the AES key is displayed in the console log.
$ sudo ./shlkfs.mount -k my_private.pem dumptest.bin /mnt/sherlock -f -s
[...]
Decrypted aes key:
14 e9 b0 2c 8d ed 21 2e 70 9a 31 c4 bd c1 a8 ec 92 db 31 c6 98 a3 cb 33 d9 31 49 69 c4 3e 4d 18
[INFO]: Mounting a SherlockFS filesystem instance...
Then i wrote a python script that decrypt the 19 blocks of encrypted data.
from Crypto.Cipher import AES
iv = b'SherlockFScrypto'
key = bytes.fromhex('14 e9 b0 2c 8d ed 21 2e 70 9a 31 c4 bd c1 a8 ec 92 db 31 c6 98 a3 cb 33 d9 31 49 69 c4 3e 4d 18')
decrypted_output = b''
with open('dump2.bin', 'rb') as f:
f.seek(65*0x1000)
for _ in range(19):
decoder = AES.new(key, AES.MODE_CBC, iv)
cipher = f.read(0x1000)
decrypted = decoder.decrypt(cipher)
decrypted_output += decrypted
with open('decrypted_blocks.bin', 'wb') as f:
f.write(decrypted_output)
Finally, by analyzing the decrypted blocks content with a hex editor i came up with the following python script that reassemble part of the .pdf file.
with open('decrypted_blocks.bin', 'rb') as f:
data = f.read()
pdf = b''
pdf += data[0x7000:0x7000 + 0x1000]
pdf += data[0x8000:0x8000 + 0x1000]
pdf += data[0xB000:0xB000 + 0x1000]
pdf += data[0xC000:0xC000 + 0x1000]
pdf += data[0xD000:0xD000 + 0x1000]
pdf += data[0xE000:0xE000 + 0x1000]
pdf += data[0xF000:0xF000 + 0x1000]
pdf += data[0x10000:0x10000 + 0x1000]
pdf += data[0x11000:0x11000 + 0x1000]
pdf += data[0x12000:0x12000 + 0x282]
with open('flag2.pdf', 'wb') as f:
f.write(pdf)
We can now open the carved .pdf and get the flag at the bottom of the document.
Flag: SHLK{f113c42v1n9c4n83p2377yh42d70d0m4nu411y}
Part 3/3
Challenge
Un incident rĂ©cent a compromis un dispositif utilisĂ© par un service de renseignement Ă©tranger, vous ayant permis de rĂ©cupĂ©rer deux Ă©lĂ©ments essentiels : un dump du pĂ©riphĂ©rique dump3.bin, ainsi qu’un dump de la mĂ©moire du programme faisant tourner le système de ce pĂ©riphĂ©rique.
Votre mission est d’analyser ces renseignements et de rĂ©cupĂ©rer une notice sur le pĂ©riphĂ©rique.
SHA256(dump3.bin) 384933be55464a40c869399bc1425ff022ab963c3f71cc991a7d02638ed836d5 : 104857600 octets
SHA256(mem.dmp) 5467688b27577ef028300cb6de4793330bba2471f1e45522f83537e996e49d2b : 2445528 octets
Solution
In this last challenge we are given another dump as well as a memory dump of a running shlkfs.mount
instance.
$ file mem.dmp
mem.dmp: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from '/workspace/SherlockFS/build/shlkfs_mount', real uid: 0, effective uid: 0, real gid: 0, effective gid: 0, execfn: '/workspace/SherlockFS/build/shlkfs_mount', platform: 'x86_64'
A quick look at the filesystem dump shows that only one key slot is used and that the RSA modulus cannot be factored easily. We thus come to the following conclusion: we will need to recover decryption keys from the memory dump.
Recovering decryption key from the memory dump
After searching for some basics strings related to RSA keys (such as -----BEGIN PRIVATE KEY-----
) in the memory dump we did not find any interesting results. We will have to search for the AES key instead.
By opening the filesystem dump in a hex editor we notice that there are 7 blocks of encrypted data from 0x41000 to 0x48000.
The first idea that came to my mind was to try every possible consecutive block of 32 bytes of the memory dump as a key to decrypt theses 7 blocks. In order to validate if the key was the right one we simply decrypt the 7 blocks with it and search for 6 consecutive null bytes in the result. In fact first decrypted blocks contain a bunch of consecutive null bytes as they are made of file metadatas and padded with null bytes to fill the total 4096 bytes.
Here is the script.
from Crypto.Cipher import AES
KEY_SIZE = 32
IV = b'SherlockFScrypto'
with open('dump3.bin', 'rb') as f:
f.seek(65*4096)
encrypted_blocks = []
for _ in range(7):
encrypted_blocks.append(f.read(4096))
with open('mem.dmp', 'rb') as f:
memdmp = f.read()
for key_index in range(len(memdmp) - KEY_SIZE):
candidate_key = memdmp[key_index:key_index+KEY_SIZE]
decrypted = b''.join([AES.new(candidate_key, AES.MODE_CBC, IV).decrypt(blocks) for blocks in encrypted_blocks])
if bytes(6) in decrypted:
print(f'[*] Found possible key at offset {key_index}, key={candidate_key.hex()}')
After waiting a few minutes it seems that the script does not find any possible encryption key. Look like we are missing something…
Decryption key obfuscation
By looking at the following snippet we understand that the AES key is not directly stored in the memory.
// file: fuse/fuse_ps_info.c
struct fs_ps_info
{
bool master_key_set; // Whether the master key has been set
unsigned char master_key[AES_KEY_SIZE_BYTES]; // XORed AES master key
unsigned char xor_key[AES_KEY_SIZE_BYTES]; // master key XOR key
unsigned char decoded_key[AES_KEY_SIZE_BYTES]; // Decoded key (must be
// zeroed after use)
} __attribute__((packed));
It’s rather the xored version of the key that is stored in memory. However the xor key is accessible right next to it so we can easily unxor it. Let’s adapt our previous script.
from Crypto.Cipher import AES
KEY_SIZE = 32
IV = b'SherlockFScrypto'
with open('dump3.bin', 'rb') as f:
f.seek(65*4096)
encrypted_blocks = []
for _ in range(7):
encrypted_blocks.append(f.read(4096))
with open('mem.dmp', 'rb') as f:
memdmp = f.read()
for key_index in range(len(memdmp) - KEY_SIZE):
candidate_key = bytes(memdmp[key_index + i] ^ memdmp[key_index + KEY_SIZE + i] for i in range(KEY_SIZE))
decrypted = b''.join([AES.new(candidate_key, AES.MODE_CBC, IV).decrypt(blocks) for blocks in encrypted_blocks])
if bytes(6) in decrypted:
print(f'[*] Found possible key at offset {key_index}, key={candidate_key.hex()}')
$ python find_key.py
[*] Found possible key at offset 18985, key=7bef55bbd20d72584fecbc35d7e82ad14a39d5e25aef55bc82ea2f752b5da2c9
Bingo ! We found the decryption key. We now adapt the part 2 script to extract the 7 blocks with this key and search for the flag in the decrypted result.
$ python decrypt_blocks.py
$ strings decrypted_blocks.bin | grep SHLK{
SHLK{7h32315n0345y1007w17hm3m02y134k49353cu217y}
Flag: SHLK{7h32315n0345y1007w17hm3m02y134k49353cu217y}