add exterminated writeup and fix diplodocus typos

Signed-off-by: Julien CLEMENT <julien.clement@epita.fr>
This commit is contained in:
Julien CLEMENT 2022-05-10 18:26:48 +02:00
parent 3e9261a7c8
commit 55075855bb
13 changed files with 476 additions and 14 deletions

@ -0,0 +1,288 @@
---
title: "Decrypting cryptolocked partition | EXTerminated @ FCSC 2022"
date: "2022-05-08 19:00:00"
author: "Juju"
tags: ["Reverse", "Writeup", "fcsc"]
toc: true
---
# Intro
EXTerminated is a malware reversing challenge. You are handed an EXT4 partition
that has been encrypted by a malware, your goal is to recover the original
unencrypted files.
The description of the challenge tells us that the malware does not use any
known cryptosystem to encrypt data, let's find this out.
{{< image src="/EXTerminated/panik.png" style="border-radius: 8px;" >}}
## Challenge description
`reverse` | `472 pts` `12 solves` `:star::star:`
```
Un client a détecté un seveur compromis sur son parc. Ce serveur semble avoir
perdu l'ensemble de ses données suite à une infection. Il nous indique que les
attaquants exigent une rançon et affirment pouvoir récupérer les fichiers
disparus.
Une analyse rapide du virus indique que ce dernier ne disposerait à première
vue d'aucun algorithme cryptographique connu.
On vous demande d'analyser ce disque, et de récupérer les fichiers originaux.
SHA256(disk.img) = a9e7891224868af43e2aa134152beaa2a83f43cde21af8038d138001377157dc.
```
Author: `Nofix`
## Given files
[disk.img](/EXTerminated/disk.img)
# Writeup
## Overview
So we can see we are given an EXT4 partition.
```console
$ file disk.img
disk.img: Linux rev 1.0 ext4 filesystem data,
UUID=c26167b3-e0b9-441d-ab4d-a5f4b5b1fcd0 (extents) (64bit) (large files)
(huge files)
```
Let's try to mount it.
We can see that there are multiple pictures, a pdf and an executable called
`wannaweep`, which is probably our malware.
```console
$ sudo mount -o loop ./disk.img mnt/
$ tree mnt/
mnt/
├── Documents
│   └── anssi-guide-ransomware_attacks_all_concerned-v1.0.pdf
├── Images
│   ├── accident.png
│   ├── disk.jpg
│   ├── flag.jpg
│   ├── martine.jpg
│   ├── smile.png
│   ├── tintin.jpeg
│   └── valide.png
├── lost+found [error opening dir]
└── wannaweep
3 directories, 9 files
```
By further investigating we can see that all data of the files have been erased
except for wannaweep, which definitely is the malware, an x64 stripped and
dinamically linked ELF.
```console
$ file mnt/Images/flag.jpg
mnt/Images/flag.jpg: data
$ xxd mnt/Images/flag.jpg | head
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000 ................
$ file mnt/wannaweep
mnt/wannaweep: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=590a9f68cde37ffceb8e7441db343742e4032f47, for GNU/Linux
3.2.0, stripped
```
We can see that the binary links to `libext2fs.so.2`, a library for EXT file
system parsing and manipulation. Since it is dinamically linked we will still
have the corresponding symbols so I'm guessing I won't have too much trouble
understanding what the malware does even though I do not know the EXT
specification.
```console
$ ldd mnt/wannaweep
linux-vdso.so.1 (0x00007ffccb2b3000)
libext2fs.so.2 => /usr/lib/libext2fs.so.2 (0x00007f8059117000)
libcom_err.so.2 => /usr/lib/libcom_err.so.2 (0x00007f8059111000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f8058f07000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f80591ad000)
```
## Main
It is now the perfect time for me to absolutely not launch this program, I
could have setup a sandboxed environment but it turned out it wasn't necessary,
the code was really straight forward.
So I start my favorite decompiler and I download the source code of libext2fs.
A small fast forward in time as the main function is really straight forward,
The program takes the path to the device to encrypt as argument, calls a bunch
of function that are mostly wrappers around libext2fs functions to initialize
some global variables holding structures of the EXT filesystem. It alsos checks
that some flags are set in the structures of libext2fs, I do not know if these
flags are implementation specific or are standard EXT flags but I did not
bother too much with this.
The intersting stuff is at the end, I can see that it calls a function that
I called `encrypt_folder` after reversing it. It then flushes the filesystem to
disk and write the inode bitmap.
{{< code file="/static/EXTerminated/main.c" language="c" >}}
## Encrypt folder
Alright so let's take a look at the `encrypt_folder` to understand why I called
it this way.
I could clearly see from `main` that function was called with the string `.`
to reference the current directory, and a function pointer that was still
unkown to me at the time but that I renamed `encrypt_file`.
How I knew that the function was encrypting folders and that the parameter was
a callback to encrypt files is really simple, you can clearly see the libc
symbols calling `opendir` on the path given as argument (`.`), reading all the
entries of the directory and calling the callback if the entry is a file.
If it is not a file, it will `chdir` in the said directory, before recursively
calling itself with the same arguments.
{{< code file="/static/EXTerminated/encrypt_folder.c" language="c" >}}
## Encrypt file
Let's now look at the `encrypt_file` we guessed. Again, mostly wrapper
functions so I did not bother to show you why I named them this way, the code
basically reads the content of the file to encrypt, encrypts it in a dedicated
block, puts it in the file system before deleting the original content of the
file.
{{< code file="/static/EXTerminated/encrypt_file.c" language="c" >}}
## Encrypt block
Well, I was scared of custom cryptography but this seems simple enough for me.
To encrypt a plain text block, the malware starts at the last byte of the block
and xor it with the previous one until it reaches the start.
Actually this function does a heap buffer underflow when xoring the first byte
of the block, since it will xor it with the byte right before the block in the
heap.
Reversing the encryption is trivial, the only undefined behaviours is with the
underflow because we cannot know the value of the byte preceding the block.
However, I assumed it would really likely be 0.
{{< code file="/static/EXTerminated/encrypt_block.c" language="c" >}}
## Decrypting
So to decrypt a block, we simply need to know the value of the byte preceding
the buffer. Let's assume it is 0 since it is the most likely.
I will xor this byte with the first byte of cipher text and that will give
me the first byte of plain text, I then repeat the operation, xoring the
first byte of plain text with the second byte of cipher text and I do this
for the whole block to recover the entire block.
## Solve
So know I know how to decrypt a block, let's decrypt the whole filesystem.
I could do something really smart and overengineered to recover a valid
decrypted EXT filesystem that I could mount to recover the original files.
However I'm not familiar with the EXT specification and their are still small
shadow zones in the malware code for me, so I try a naive solution.
I'm guessing that data blocks of files or all alligned on `0x1000`, it would be
kind of weird otherwise for me. So if I just cut the whole filesystem in
`0x1000` sized blocks without any consideration of the semantic of those blocks
in the EXT structure, I could just decrypt each of these blocks individually,
and prey that I recover some files whose blocks were contiguous in the EXT
structure.
Obviously that will corrupt all the EXT metadata but it doesn't really matter
as long as I can recover the files.
{{< code file="/static/EXTerminated/decrypt.py" language="py" >}}
Now let's try to output the result and see if we find any file signatures:
```console
$ ./decrypt.py > decrypted.img
$ binwalk decrypted.img
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
9289728 0x8DC000 PNG image, 473 x 306, 8-bit/color RGB, non-interlaced
9531392 0x917000 JPEG image data, JFIF standard 1.01
9625600 0x92E000 PNG image, 500 x 564, 8-bit/color RGBA, non-interlaced
10125312 0x9A8000 JPEG image data, JFIF standard 1.01
10212833 0x9BD5E1 bix header, header size: 64 bytes, header CRC: 0x6003083D, created: 2004-01-10 13:39:36, image size: 2097152 bytes, Data Address: 0x22120000, Entry Point: 0x1E040003, data CRC: 0xD2100008, CPU: IA64, image name: ""
10231808 0x9C2000 JPEG image data, EXIF standard
10231820 0x9C200C TIFF image data, big-endian, offset of first image directory: 8
10330112 0x9DA000 JPEG image data, EXIF standard
10330124 0x9DA00C TIFF image data, big-endian, offset of first image directory: 8
10403840 0x9EC000 PNG image, 482 x 367, 8-bit/color RGB, non-interlaced
10403902 0x9EC03E Zlib compressed data, default compression
34263365 0x20AD145 Cisco IOS experimental microcode, for ""
46640306 0x2C7ACB2 MySQL ISAM compressed data file Version 9
46691658 0x2C8754A GIF image data 15531 x
```
We do! So let's try to extract them
```console
$ binwalk --dd='.*' decrypted.img
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
9289728 0x8DC000 PNG image, 473 x 306, 8-bit/color RGB, non-interlaced
9531392 0x917000 JPEG image data, JFIF standard 1.01
9625600 0x92E000 PNG image, 500 x 564, 8-bit/color RGBA, non-interlaced
10125312 0x9A8000 JPEG image data, JFIF standard 1.01
10212833 0x9BD5E1 bix header, header size: 64 bytes, header CRC: 0x6003083D, created: 2004-01-10 13:39:36, image size: 2097152 bytes, Data Address: 0x22120000, Entry Point: 0x1E040003, data CRC: 0xD2100008, CPU: IA64, image name: ""
10231808 0x9C2000 JPEG image data, EXIF standard
10231820 0x9C200C TIFF image data, big-endian, offset of first image directory: 8
10330112 0x9DA000 JPEG image data, EXIF standard
10330124 0x9DA00C TIFF image data, big-endian, offset of first image directory: 8
10403840 0x9EC000 PNG image, 482 x 367, 8-bit/color RGB, non-interlaced
10403902 0x9EC03E Zlib compressed data, default compression
34263365 0x20AD145 Cisco IOS experimental microcode, for ""
46640306 0x2C7ACB2 MySQL ISAM compressed data file Version 9
46691658 0x2C8754A GIF image data 15531 x
$ file _decrypted.img.extracted/*
_decrypted.img.extracted/2C7ACB2: MySQL ISAM compressed data file Version 9
_decrypted.img.extracted/2C8754A: GIF image data 15531 x
_decrypted.img.extracted/8DC000: PNG image data, 473 x 306, 8-bit/color RGB, non-interlaced
_decrypted.img.extracted/9A8000: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, progressive, precision 8, 660x424, components 3
_decrypted.img.extracted/9BD5E1: data
_decrypted.img.extracted/9C200C: TIFF image data, big-endian, direntries=0
_decrypted.img.extracted/9C2000: JPEG image data, Exif standard: [TIFF image data, big-endian, direntries=0], comment: "CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), quality = 90", baseline, precision 8, 481x600, components 3
_decrypted.img.extracted/9DA000: JPEG image data, Exif standard: [TIFF image data, big-endian, direntries=0], baseline, precision 8, 683x500, components 3
_decrypted.img.extracted/9DA00C: TIFF image data, big-endian, direntries=0
_decrypted.img.extracted/9EC000: PNG image data, 482 x 367, 8-bit/color RGB, non-interlaced
_decrypted.img.extracted/9EC03E: empty
_decrypted.img.extracted/9EC03E-0: zlib compressed data
_decrypted.img.extracted/20AD145: cisco IOS experimental microcode for ''
_decrypted.img.extracted/92E000: PNG image data, 500 x 564, 8-bit/color RGBA, non-interlaced
_decrypted.img.extracted/917000: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 495x669, components 3
```
Displaying the images we just extracted, we find the following one, giving us
the flag.
{{< image src="/EXTerminated/flag.png" style="border-radius: 8px;" >}}

@ -139,7 +139,7 @@ enum describing possible movements in this array. I therefore name them `move`,
Next we notice that we check that the `jth` bit of the `ith` row of the array,
if is not set, it sets the said bit and update the coordinates in the context.
If however it is already set, the program set the error flag that we already
If however it is already set, the program sets the error flag that we already
identified earlier.
Clearly this is some sort of 12 * 12 bitboard implementation, the first thing
@ -175,9 +175,9 @@ to place the said bit on a different bitboard than case 2 but with the same
coordinates. If the bit queue is empty then we output an error.
So I understand here that we must place 24 bits on another board while
simultaneously moving on the first one that tracks the tile we already visited.
simultaneously moving on the first one that tracks the tiles we already visited.
Seems easy enough, however, the instruction does not end here, it call a
Seems easy enough, however, the instruction does not end here, it calls a
function passing the row of the bitboard as parameter, if the return value of
this function is more than `2` we output an error.
@ -267,15 +267,15 @@ traversal of the board, we did not encounter more than 2 bits.
Here I was really scared because I started to think that this was maybe `\J`
trolling me with again a cryptography problem modelised using bitboards are
something. And everyone who checks my results of the FCSC know how bad I am
something. And everyone who checks my results of the FCSC know how bad I am at
cryptography.
{{< code file="/static/diplodocus/check_align.c" language="c" >}}
#### Alignement count
So now I am really unhappy, I was having finding out puzzle and placing bits on
boards but now `\J` is throwing modular arithmetic at me.
So now I am really unhappy, I was having fun finding out puzzle and placing
bits on boards but now `\J` is throwing modular arithmetic at me.
I take a break crying in my bed after these findings before actually thinking.
@ -356,8 +356,8 @@ the point if you remember well.
However the only columns check is in case 0 where it simply checks that there
is an even number of point per column.
Nothing forbid us to put 12 points on the first and last column, which
validates all our constraints
Nothing forbids us to put 12 points on the first and last column, which
validates all our constraints.
```
|X| | | | | | | | | | |X|
@ -374,6 +374,8 @@ validates all our constraints
|X| | | | | | | | | | |X|
```
{{< image src="/diplodocus/stop_the_count.png" style="border-radius: 8px;" >}}
## Solve
We now simply need to write the input that will give instructions to the

@ -0,0 +1,19 @@
#!/usr/bin/env python3
import sys
import os
with open('./disk.img', 'rb') as f:
data = f.read()
blocks = [data[i * 0x1000:(i * 0x1000) + 0x1000] for i in range(len(data) // 0x1000)]
clear = bytearray()
for block in blocks:
x = 0
for b in block:
c = x ^ b
clear.append(c)
x = c
os.write(1, clear)

Binary file not shown.

@ -0,0 +1,9 @@
int64_t encrypt_block(char* block, int64_t len)
{
int64_t i;
for (i = 0; i < len; i = (i + 1))
{
block[((len - i) - 1)] = (block[((len - i) - 1)] ^ block[((len - i) - 2)]);
}
return i;
}

@ -0,0 +1,73 @@
int64_t encrypt_file(char* name)
{
return encrypt_file2(name);
}
int64_t encrypt_file2(char* name)
{
int32_t inode = 0;
int32_t len_read = 0;
uint64_t blocksize = ((uint64_t)fs->blocksize);
void* block = xcalloc(1, blocksize); // Allocate data for encrypted block
int64_t fd = get_size(name); // Get the file size
if (fd != 0)
{
fd = open(name, 2); // Open the file
int32_t fd = fd;
if (fd != 0xffffffff)
{
if (inode == 0)
// Create an inode for the encrypted file
ext2fs_new_inode(fs, 0, 0x41ed, 0, &inode);
ext2fs_inode_alloc_stats2(fs, ((uint64_t)inode), 1, 1);
char stop = 0;
int64_t nb_blocks = 1;
uint64_t* blocks = xcalloc(1, 0x10);
do
{
// Fill the block with file data
if (fill_block(fd, block, blocksize, &len_read) == 0)
break;
// Determine if another block is needed to encrypt the file
int32_t n_blocks = len_read / 0x1000;
int32_t len_read;
if (len_read % 0x1000 != 0)
stop = 1;
// Encrypt the block
encrypt_block(block, ((int64_t)len_read));
// Write encrypted block to the filesystem
int64_t new_block = write_block(block, ((uint64_t)fs->blocksize), inode);
// Some stuff I did not bother to understand
// probably setting up recursive blocks for big files
// or preparing new empty blocks for the file that must be
// emptied
// but I do not know nor does it really matter for now
if (blocks[((nb_blocks - 1) * 2)] == 0)
{
blocks[((nb_blocks - 1) * 2)] = new_block;
blocks[(((nb_blocks - 1) * 2) + 1)] = new_block;
}
else if ((blocks[(((nb_blocks - 1) * 2) + 1)] + 1) != new_block)
{
nb_blocks = (nb_blocks + 1);
realloc(blocks, (nb_blocks << 4));
blocks[((nb_blocks - 1) * 2)] = new_block;
blocks[(((nb_blocks - 1) * 2) + 1)] = new_block;
}
else
blocks[(((nb_blocks - 1) * 2) + 1)] = new_block;
memset(block, 0, ((uint64_t)fs->blocksize));
} while ((stop & 1) == 0);
// Remove data from the original file
fd = delete_data(name, nb_blocks, blocks);
}
}
return fd;
}

@ -0,0 +1,28 @@
DIR* encrypt_folder(char* path, void* callback)
{
DIR* dir = opendir(path);
if (dir != 0)
{
while (true)
{
struct dirent64* dirent = readdir(dir);
if (dirent == 0)
{
break;
}
if (((uint32_t)dirent->d_type) != 4)
{
callback(&dirent->d_name);
}
else if ((strcmp(&dirent->d_name, ".") != 0 && strcmp(&dirent->d_name, "..") != 0))
{
chdir(&dirent->d_name);
encrypt_folder(".", callback);
}
}
chdir("..");
dir = closedir(dir);
}
return dir;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 MiB

@ -0,0 +1,43 @@
int32_t main(int32_t argc, char** argv, char** envp)
{
int32_t res;
if (argc != 2)
{
char** rax;
rax = 0;
printf("Usage %s <device> \n", *(int64_t*)argv);
res = 1;
}
else if (check_fs_opened() != 0)
{
res = 1;
}
else
{
open_and_read(argv[1]);
if (fs == 0)
{
perror("Could not get handle on FS");
exit(1);
/* no return */
}
if (check_flag_clear() != 0)
{
res = 1;
}
else
{
if (fs->blocksize != 0x1000)
{
perror("FS blocksize is invalid");
exit(1);
/* no return */
}
encrypt_folder(".", encrypt_file);
ext2fs_flush(fs);
write_inode_bitmap();
res = 0;
}
}
return res;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

@ -16,7 +16,7 @@ case 0:
zmm0 = (zmm0 & _mm_bsrli_si128(zmm0, 4));
int32_t all_set = (zmm0 & 0xfff) != 0xfff;// Check that context.third_board
err = all_set | err; // Is all set to 1
err = all_set | err; // is all set to 1
context.err = err;
uint32_t flag = 1; // Set error flag

@ -1,7 +1,7 @@
uint64_t sub_1570(int32_t j, int32_t i, struct context context)
{
int32_t res = 0;
int32_t *board = context.board;;
int32_t *board = context.board;
int32_t res_flag = 0;
if ((j <= 0xb && i <= 0xb))
{
@ -15,15 +15,15 @@
{
int32_t r8 = r12_1; // ignore for now
int32_t x = -0xb; // Second increment
int32_t neg = (y >> 0x1f); // Probably interesting
int32_t neg = (y >> 0x1f); // Probably interesting
int32_t r11_2 = ((y_cpy * -0xb) + rbp_1); // but I can't seem
int32_t y_cpy_cpy = ((neg ^ y) - osef); // to care enough
int32_t y_cpy_cpy = ((neg ^ y) - neg); // to care enough
while (true)
{
if (x != 0)
{
int32_t neg = (x >> 0x1f); // Abs value again or
int32_t x_cpy = ((neg ^ x) - osef); // something
int32_t x_cpy = ((neg ^ x) - neg); // something
int32_t y_cpy_cpy_cpy = y_cpy_cpy;
int32_t count;
while (true) // Euclidean algorithm
@ -63,7 +63,7 @@
// STOP THE COUNT
int32_t too_much = count > 2;
res_flag = (res_flag | count>2);
res_flag = (res_flag | too_much);
}
if (x == 0xb) // End the while true loops and iterate
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB