FCSC 2023 Forensics: Ransomémoire

Solving a memory analysis challenge and reverse engineering a malware.

In my quest to learn forensics, I took on the annual French cybersecurity challenge organized by ANSSI. It was my first time participating in it, so I approached it calmly, focusing on solving the tasks that would help me learn the most.

Being entirely new to the field of forensics, I learned on the fly through this challenge, and I thought it would be good to share the challenge that I enjoyed solving the most.

Challenge Objective

You were looking at your beautiful cat photos when suddenly your super-secret file on your desktop changes its extension and becomes unreadable…

You capture memory to understand what happened in order to recover this precious file.

According to the description, it appears that we are dealing with ransomware that has encrypted a specific file on the desktop.

Using the provided memory capture, we will follow this trail and find a way to recover the file.

Finding the Malware

The first step is to locate the malware in the memory capture. We will analyze it using the volatility3 memory analysis tool.

I started with the more complex part. I analyzed the handles to the desktop using the windows.handles option. In Windows programming, a handle is an object returned by a function that allows interaction with Windows objects (programs, files, registry keys, etc.). When a process interacts with one of these objects, it first obtains a handle to it.

Since we know that the process is acting on the desktop, it is natural to think that a handle to the desktop must be associated with a specific process:

1
2
3
4
5
> python vol.py -f ../fcsc.dmp windows.handles | grep File | grep Desktop

1748	CompatTelRunne	0x818688458e50	0xd90	File	0x120089	\Device\HarddiskVolume2\Windows\System32\CatRoot\{F750E6C3-38EE-11D1-85E5-00C04FC295EE}\Microsoft-Windows-Client-Desktop-Required-Package01~31bf3856ad364e35~amd64~~10.0.19041.2006.cat
1748	CompatTelRunne	0x818689b0fca0	0xfb0	File	0x120089	\Device\HarddiskVolume2\Windows\System32\CatRoot\{F750E6C3-38EE-11D1-85E5-00C04FC295EE}\Microsoft-Windows-Client-Desktop-Required-Package051021~31bf3856ad364e35~amd64~~10.0.19041.2006.cat
5540	svchost.exe	0x818689b8f590	0x218	File	0x100001	\Device\HarddiskVolume2\Users\Admin\Desktop

The only “non-system” process with a handle on the desktop is svchost.exe with a PID of 5540.

Now, the simpler method:

1
2
3
4
5
6
7
> python vol.py -f ../fcsc.dmp windows.pstree --pid 5540

624	548	winlogon.exe	0x818684cd7080	5	-	1	False	2023-04-16 21:46:21.000000 	N/A
* 3892	624	userinit.exe	0x8186813f5340	0	-	1	False	2023-04-16 21:47:17.000000 	2023-04-16 21:47:42.000000
** 3928	3892	explorer.exe	0x818684aa0340	66	-	1	False	2023-04-16 21:47:17.000000 	N/A
*** 6424	3928	VBoxTray.exe	0x81868852e080	13	-	1	False	2023-04-16 21:47:34.000000 	N/A
**** 5540	6424	svchost.exe	0x818687754080	1	-	1	False	2023-04-17 17:21:18.000000 	N/A

It can be noted that svchost.exe was created from a user process (VboxTray) descending from explorer.exe, which is suspicious.

Malware Analysis

Once this process is identified, it should be extracted from memory for analysis. In fact, most running processes during a memory capture keep their executable file inside it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
_BOOL8 __fastcall write_random_bytes_files(PUCHAR pbBuffer, ULONG cbBuffer)
{
  HANDLE FileW; // rax
  void *v6; // r13
  DWORD NumberOfBytesWritten; // [rsp+4Ch] [rbp-22Ch] BYREF
  WCHAR FileName[276]; // [rsp+50h] [rbp-228h] BYREF

  wsprintfW(FileName, L"C:\\Windows\\Temp\\MsCmdRun%d.log");
  if ( BCryptGenRandom(0i64, pbBuffer, cbBuffer, 2u) )
    return 0i64;
  FileW = CreateFileW(FileName, 0x40000000u, 0, 0i64, 2u, 0x80u, 0i64);
  v6 = FileW;
  if ( FileW == -1i64 )
    return 0i64;
  if ( WriteFile(FileW, pbBuffer, cbBuffer, &NumberOfBytesWritten, 0i64) )
  {
    CloseHandle(v6);
    return NumberOfBytesWritten == cbBuffer;
  }
  else
  {
    CloseHandle(v6);
    return 0i64;
  }
}

This first function generates 64 random bytes and writes them to a file each time it is executed. The file containing these bytes will be located at “C:\Windows\Temp\MsCmdRun%d.log” where %d will be replaced by the corresponding execution number.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
if ( nbytes )
{
  op_counter = 0i64;
  do
  {
    *(Heap + op_counter) ^= execution_counter ^ *(bytes + op_counter);
    ++op_counter;
  }
  while ( nbytes != op_counter );
  v12 = NumberOfBytesRead[0];
}
if ( SetFilePointer(FileW, -v12, 0i64, 1u) == -1
  || !WriteFile(FileW, Heap, NumberOfBytesRead[0], &NumberOfBytesWritten, 0i64)
  || NumberOfBytesRead[0] != NumberOfBytesWritten )
{
  goto LABEL_12;
}

This function snippet is responsible for file encryption. This is where we understand the encryption logic of this malware. It is simple: randomly generated bytes are used to XOR the bytes of the original file, along with the execution number.

The program scans the desktop in search of any file with the .fcsc extension. If it doesnt find one, it increments a counter and generates new random bytes.

We know that XOR is a reversible operation. So, having the execution counter, the file with random bytes, and the encrypted file, we can recover the original file.

Now, we just need to recover these files..

File Recovery

Volatility3 contains a command to list files present in memory:

1
2
3
4
> python vol.py -f ../fcsc.dmp windows.filescan | grep MsCmdRun

0x818687921450.0\Windows\Temp\MsCmdRun20.log	216
0x818689b862b0	\Windows\Temp\MsCmdRun19.log	216

These are the random byte files present in memory. However, it’s not enough because we don’t know which one was used to encrypt our file. Additionally, memory doesn’t contain the encrypted file on the desktop.

This is where the Master File Table (MFT) comes into play. In simple terms, in an NTFS file system, there is a file that records information about each file on the disk (size, last modification, address on the disk), making it easier for Windows to find it. This table also stores the content of small files, and much of it is available in memory.

Volatility3 doesn’t provide in-depth analysis of the MFT, so we have to resort to volatility2:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
> ./volatility_2.6_lin64_standalone -f ../fcsc.dmp mftparser>mft.txt

Creation                       Modified                       MFT Altered                    Access Date                    Name/Path
------------------------------ ------------------------------ ------------------------------ ------------------------------ ---------
2023-04-17 17:23:45 UTC+0000 2023-04-17 17:23:50 UTC+0000   2023-04-17 17:23:50 UTC+0000   2023-04-17 17:23:50 UTC+0000   Users\Admin\Desktop\flag.fcsc.enc

$DATA
0000000000: 3b 65 17 19 64 03 71 9f dd 1a 30 ec 37 ba 83 c9   ;e..d.q...0.7...
0000000010: 1b b0 44 c9 8d 05 45 88 ff 41 40 d6 32 e5 61 09   ..D...E..A@.2.a.
0000000020: 5f f2 32 07 44 6a 8d 05 c7 fe 82 2f 22 76 9a 08   _.2.Dj...../"v..
0000000030: 32 28 7a ad ff 90 c8 4d 96 ca 99 54 1c 2c 58 f7   2(z....M...T.,X.
0000000040: 7a 8b e5 c5 5d 51 5a                              z...]QZ

I found the encrypted file in the MFT table. We know that a file with random bytes named MsCmdRun is created before altering the flag file. So, I searched in this table for an MsCmdRun file with the creation/modification date closest to that of the flag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$FILE_NAME
Creation                       Modified                       MFT Altered                    Access Date                    Name/Path
------------------------------ ------------------------------ ------------------------------ ------------------------------ ---------
2023-04-17 17:23:50 UTC+0000 2023-04-17 17:23:50 UTC+0000   2023-04-17 17:23:50 UTC+0000   2023-04-17 17:23:50 UTC+0000   Windows\Temp\MsCmdRun14.log

$DATA
0000000000: 73 28 4a 54 11 3a 48 a7 b5 26 0b 86 01 85 bc a5   s(JT.:H..&......
0000000010: 73 87 2b a4 b1 3d 79 b7 c5 7c 2a e8 5a da 09 66   s.+..=y..|*.Z..f
0000000020: 68 cb 5f 3a 72 56 e5 3c ab c5 b5 16 1e 49 a6 37   h._:rV.<.....I.7
0000000030: 5e 15 43 c7 c1 ad f0 72 fb f3 f3 6d 73 46 67 9b   ^.C....r...msFg.
0000000040: 46 b2 db fc 6a 22 5e 89 8e 58 7d 0b 5c e5 4a d8   F...j"^..X}.\.J.
0000000050: 62 58 72 87 ee 36 f5 44 49 55 0f bd c0 00 e1 58   bXr..6.DIU.....X
0000000060: 60 5f 1e 0f                                       `_..

With these two files, all that remained was to create a script to decrypt the flag.

At first, I thought the execution counter incremented with each encrypted file. Since it was the only encrypted file, I assumed the counter corresponded to 0. XORing by 0 is trivial, so I omitted this operation:

1
HM]Mu998h<;j6??lh7om<8<?:=j>h?ho79m=6<h9l;79<?<?l=9j>=8?m9j9oj?l<9>97s

However, the flag was still encrypted. I noticed that the beginning of the flag was still recognizable, as the flags in this competition started with FCSC. I saw the presence of 2 M’s corresponding to the two C’s in the flag header. So, I needed to find the value that, when XORed with M multiple times, would yield C. This is the weakness of XOR algorithms when part of the initial string is known; it can be brute-forced to obtain the final string.

The value 14 allowed me to reveal the flag. This is how I realized the importance of the execution counter, and that it was recorded in the random byte file (MsCmdRun14):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ms14 = [0x73, 0x28, 0x4a, 0x54, 0x11, 0x3a, 0x48, 0xa7, 0xb5, 0x26, 0x0b, 0x86, 0x01, 0x85, 0xbc, 0xa5,
0x73, 0x87, 0x2b, 0xa4, 0xb1, 0x3d, 0x79, 0xb7, 0xc5, 0x7c, 0x2a, 0xe8, 0x5a, 0xda, 0x09, 0x66,
0x68, 0xcb, 0x5f, 0x3a, 0x72, 0x56, 0xe5, 0x3c, 0xab, 0xc5, 0xb5, 0x16, 0x1e, 0x49, 0xa6, 0x37,
0x5e, 0x15, 0x43, 0xc7, 0xc1, 0xad, 0xf0, 0x72, 0xfb, 0xf3, 0xf3, 0x6d, 0x73, 0x46, 0x67, 0x9b,
0x46, 0xb2, 0xdb, 0xfc, 0x6a, 0x22, 0x5e, 0x89, 0x8e, 0x58, 0x7d, 0x0b, 0x5c, 0xe5, 0x4a, 0xd8,
0x62, 0x58, 0x72, 0x87, 0xee, 0x36, 0xf5, 0x44, 0x49, 0x55, 0x0f, 0xbd, 0xc0, 0x00, 0xe1, 0x58,
0x60, 0x5f, 0x1e, 0x0f, 0x73, 0x28, 0x4a]

fcsc = [0x3b, 0x65, 0x17, 0x19, 0x64, 0x03, 0x71, 0x9f, 0xdd, 0x1a, 0x30, 0xec, 0x37, 0xba, 0x83, 0xc9, 
0x1b, 0xb0, 0x44, 0xc9, 0x8d, 0x05, 0x45, 0x88, 0xff, 0x41, 0x40, 0xd6, 0x32, 0xe5, 0x61, 0x09, 
0x5f, 0xf2, 0x32, 0x07, 0x44, 0x6a, 0x8d, 0x05, 0xc7, 0xfe, 0x82, 0x2f, 0x22, 0x76, 0x9a, 0x08, 
0x32, 0x28, 0x7a, 0xad, 0xff, 0x90, 0xc8, 0x4d, 0x96, 0xca, 0x99, 0x54, 0x1c, 0x2c, 0x58, 0xf7, 
0x7a, 0x8b, 0xe5, 0xc5, 0x5d, 0x51, 0x5a]


flaglist = []
i = 0


while i < len(fcsc):
    flaglist.append((fcsc[i] ^ ms14[i]) ^ 14)
    i += 1
    
print(''.join(chr(i) for i in flaglist))

FCSC{776f25d811bf9ac262143d0f1fa97c382f7b5972121b37d0361c7d7ad1b27079}

Conclusion

And so, this forensics challenge comes to an end. It was very interesting and taught me a lot about memory analysis and other Windows internals. My reverse engineering skills allowed me to easily pass the malware analysis task, where others may have failed due to a lack of expertise.

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy