Ransomware · Ransomworm · Worm
WannaCry - invoice_greenanimals.pdf.exe
- Author
- Moise Medici
- Updated
- 15 Feb 2026 · Completed
- Difficulty
- Medium
- Platform
- Capabilities
- Tags
Question 16
The .wnry files use a custom extension, so they might be anything. One approach is to load them in tools that understand the file header and MIME type, or simply inspect their content. They were opened with Exeinfo, pestudio, notepad, and the file command from Linux.
b.wnry: opened with Exeinfo it shows as an image, so changing the extension to .png and opening it with the default image viewer shows the background image used once the infection is triggered.
s.wnry: identified as an archive, and when extracted it contains the Tor and Data directories.
u.wnry: is another executable. Looking at the strings and resources sections of pestudio, there are clear indications that this binary spawns the payment interface. There are images in the resources section and strings like Check Payment. This is also confirmed later on in question 17, where the assembly lines showing the copy from u.wnry to WanaDecryptor in kbdlv.dll are identified.
r.wnry: is the ransom note; opening it with notepad displays the following:
Q: What's wrong with my files?
A: Ooops, your important files are encrypted. It means you will not be able to access them anymore until they are decrypted. If you follow our instructions, we guarantee that you can decrypt all your files quickly and safely! Let's start decrypting!
Q: What do I do?
A: First, you need to pay service fees for the decryption. Please send %s to this bitcoin address: %s
Next, please find an application file named "%s". It is the decrypt software. Run and follow the instructions! (You may need to disable your antivirus for a while.)
Q: How can I trust?
A: Don't worry about decryption. We will decrypt your files surely because nobody will trust us if we cheat users.
* If you need our assistance, send a message by clicking <Contact Us> on the decryptor window.c.wnry: recognized by file as “DATA”. A simple cat shows various .onion websites that might be used for some of the activities:
$ cat c.wnry�Cgx7ekbenv2riucmf.onion;57g7spgrzlojinas.onion;xxlvbrloxvriy2c5.onion;76jdd2ir2embyv47.onion;cwwnhwhlz52maqm7.onion;https://dist.torproject.org/torbrowser/6.5.1/tor-win32-0.2.9.10.zipt.wnry: recognized by file as “DATA”, but opening it with cat or Notepad shows only incomprehensible data. Dragging the file to CFF Explorer and checking the hex representation shows the first few bytes as 57 41 4E 41 43 52 59, which in ASCII is WANACRY.
To better understand this data, it is useful to determine which executable uses it and how. To see how it is used, the code of tasksche.exe is reviewed where the t.wnry string appears. Looking at the address reference, the relevant code is:
00402125 PUSH s_t.wnry_0040f4f4 ; = "t.wnry"0040212a MOV dword ptr [EBP + local_8], EBX0040212d CALL FUN_004014a6 ; undefined FUN_004014a6(undefined4 param_1, undefined4 param_2);The first step is to look at the FUN_004014a6 function and rename param_1 to t_wnry, leaving the type as undefined4. (It could be changed to a STRING type, but that sometimes creates confusion in Ghidra later on.) The first occurrence of t_wnry is in CreateFileA:
004014fe PUSH EBX ; HANDLE hTemplateFile for CreateFileA004014ff PUSH EBX ; DWORD dwFlagsAndAttributes for CreateFileA00401500 PUSH 0x3 ; DWORD dwCreationDisposition for CreateFileA00401502 PUSH EBX ; LPSECURITY_ATTRIBUTES lpSecurityAttributes for ; CreateFileA00401503 PUSH 0x1 ; DWORD dwShareMode for CreateFileA00401505 PUSH 0x80000000 ; DWORD dwDesiredAccess for CreateFileA0040150a PUSH dword ptr [EBP + t_wnry] ; LPCSTR lpFileName for CreateFileA0040150d CALL dword ptr [->KERNEL32.DLL::CreateFileA] ; = 0000d92200401513 MOV EDI, EAXNote that CreateFileA takes lpFileName as an argument, which here is assigned to t_wnry. The returned file handle is moved from EAX to EDI.
From this point, to understand what else is done with the file, EDI needs to be tracked throughout the function, until it is overwritten by another value.
At line 1 it gets saved in local_24c, which will be used at line 114. In between, there are seven more times where it gets used.
00401515 MOV dword ptr [EBP + local_24c], EDI0040151b CMP EDI, -0x10040151e JZ LAB_004016d000401524 LEA EAX=>local_28, [EBP + -0x24]00401527 PUSH EAX ; PLARGE_INTEGER lpFileSize for GetFileSizeEx00401528 PUSH EDI ; HANDLE hFile for GetFileSizeEx00401529 CALL dword ptr [->KERNEL32.DLL::GetFileSizeEx] ; = 0000d9120040152f CMP dword ptr [EBP + local_24], EBX00401532 JG LAB_004016d000401538 JL LAB_004015470040153a CMP dword ptr [EBP + local_28], 0x640000000401541 JA LAB_004016d0 LAB_00401547 XREF[1]: 00401538(j)00401547 PUSH EBX00401548 LEA EAX=>local_20, [EBP + -0x1c]0040154b PUSH EAX0040154c PUSH 0x80040154e LEA EAX=>local_240, [EBP + 0xfffffdc4]00401554 PUSH EAX00401555 PUSH EDI00401556 CALL dword ptr [DAT_0040f880]0040155c TEST EAX, EAX0040155e JZ LAB_004016d000401564 PUSH 0x8 ; size_t _Size for memcmp00401566 PUSH s_WANACRY!_0040eb7c ; void * _Buf2 for memcmp0040156b LEA EAX=>local_240, [EBP + 0xfffffdc4]00401571 PUSH EAX ; void * _Buf1 for memcmp00401572 CALL MSVCRT.DLL::memcmp ; int memcmp(void * _Buf1, void * _Buf2, size_t ; _Size)00401577 ADD ESP, 0xc0040157a TEST EAX, EAX0040157c JNZ LAB_004016d000401582 PUSH EBX00401583 LEA EAX=>local_20, [EBP + -0x1c]00401586 PUSH EAX00401587 PUSH 0x400401589 LEA EAX=>local_248, [EBP + 0xfffffdbc]0040158f PUSH EAX00401590 PUSH EDI00401591 CALL dword ptr [DAT_0040f880]00401597 TEST EAX, EAX00401599 JZ LAB_004016d00040159f MOV EAX, 0x100004015a4 CMP dword ptr [EBP + local_248], EAX004015aa JNZ LAB_004016d0004015b0 PUSH EBX004015b1 LEA ECX=>local_20, [EBP + -0x1c]004015b4 PUSH ECX004015b5 PUSH EAX004015b6 PUSH dword ptr [ESI + 0x4c8]004015bc PUSH EDI004015bd CALL dword ptr [DAT_0040f880]004015c3 TEST EAX, EAX004015c5 JZ LAB_004016d0004015cb PUSH EBX004015cc LEA EAX=>local_20, [EBP + -0x1c]004015cf PUSH EAX004015d0 PUSH 0x4004015d2 LEA EAX=>local_244, [EBP + 0xfffffdc0]004015d8 PUSH EAX004015d9 PUSH EDI004015da CALL dword ptr [DAT_0040f880]004015e0 TEST EAX, EAX004015e2 JZ LAB_004016d0004015e8 PUSH EBX004015e9 LEA EAX=>local_20, [EBP + -0x1c]004015ec PUSH EAX004015ed PUSH 0x8004015ef LEA EAX=>local_238, [EBP + 0xfffffdcc]004015f5 PUSH EAX004015f6 PUSH EDI004015f7 CALL dword ptr [DAT_0040f880]004015fd TEST EAX, EAX004015ff JZ LAB_004016d000401605 CMP dword ptr [EBP + local_234], EBX0040160b JG LAB_004016d000401611 JL LAB_0040162300401613 CMP dword ptr [EBP + local_238], 0x64000000040161d JA LAB_004016d0 LAB_00401623 XREF[1]: 00401611(j)00401623 LEA EAX=>local_30, [EBP + -0x2c]00401626 PUSH EAX00401627 LEA EAX=>local_230, [EBP + 0xfffffdd4]0040162d PUSH EAX0040162e PUSH dword ptr [EBP + local_248]00401634 PUSH dword ptr [ESI + 0x4c8]0040163a LEA ECX, [ESI + 0x4]0040163d CALL FUN_004019e1 ; undefined FUN_004019e1(undefined4 param_1, ; undefined4 param_2, undefined4 param_3, undefined4 ; param_4)00401642 TEST EAX, EAX00401644 JZ LAB_004016d00040164a LEA EDI, [ESI + 0x54]0040164d PUSH 0x100040164f PUSH dword ptr [EBP + local_30]00401652 PUSH dword ptr [PTR_DAT_0040f578] ; = 0040f91400401658 LEA EAX=>local_230, [EBP + 0xfffffdd4]0040165e PUSH EAX0040165f MOV ECX, EDI00401661 CALL FUN_00402a76 ; undefined FUN_00402a76(undefined4 param_1, ; undefined4 param_2, undefined4 param_3, undefined4 ; param_4)00401666 PUSH dword ptr [EBP + local_238] ; SIZE_T dwBytes for GlobalAlloc0040166c PUSH EBX ; UINT uFlags for GlobalAlloc0040166d CALL dword ptr [->KERNEL32.DLL::GlobalAlloc] ; = 0000d87400401673 MOV dword ptr [EBP + local_2c], EAX00401676 CMP EAX, EBX00401678 JZ LAB_004016d00040167a PUSH EBX0040167b LEA EAX=>local_20, [EBP + -0x1c]0040167e PUSH EAX0040167f PUSH dword ptr [EBP + local_28]00401682 PUSH dword ptr [ESI + 0x4c8]00401688 PUSH dword ptr [EBP + local_24c]0040168e CALL dword ptr [DAT_0040f880]00401694 TEST EAX, EAX00401696 JZ LAB_004016d000401698 MOV EAX, dword ptr [EBP + local_20]0040169b CMP EAX, EBX0040169d JZ LAB_004016d00040169f CMP EBX, dword ptr [EBP + local_234]004016a5 JG LAB_004016b1004016a7 JL LAB_004016d0004016a9 CMP EAX, dword ptr [EBP + local_238]004016af JC LAB_004016d0 LAB_004016b1 XREF[1]: 004016a5(j)004016b1 PUSH 0x1004016b3 PUSH EAX004016b4 MOV EBX, dword ptr [EBP + local_2c]004016b7 PUSH EBX004016b8 PUSH dword ptr [ESI + 0x4c8]004016be MOV ECX, EDI004016c0 CALL FUN_00403a77 ; undefined FUN_00403a77(undefined4 param_1, ; undefined4 param_2, undefined4 param_3, undefined4 ; param_4)004016c5 MOV EAX, dword ptr [EBP + param_2]004016c8 MOV ECX, dword ptr [EBP + local_238]004016ce MOV dword ptr [EAX], ECX LAB_004016d0 XREF[18]: 0040151e(j), 00401532(j), 00401541(j), 0040155e(j), 0040157c(j), 00401599(j), 004015aa(j), 004015c5(j), 004015e2(j), 004015ff(j), 0040160b(j), 0040161d(j), 00401644(j), 00401678(j), 00401696(j), 0040169d(j), 004016a7(j), 004016af(j)004016d0 PUSH -0x1004016d2 LEA EAX=>local_14, [EBP + -0x10]004016d5 PUSH EAX004016d6 CALL MSVCRT.DLL::_local_unwind2 ; undefined _local_unwind2()004016db POP ECX004016dc POP ECX004016dd MOV EAX, EBX004016df JMP LAB_004016f9Currently the problem is what those DAT_xxxx strings are, for example:
00401554 PUSH EAX00401555 PUSH EDI00401556 CALL dword ptr [DAT_0040f880]And this is actually pretty easy to find. Double‑clicking on any of the DAT_xxxx strings, for example DAT_0040f880, leads to where this object is referenced:
DAT_0040f880 XREF[8]: FUN_004014a6:00401556(R), FUN_004014a6:00401591(R), FUN_004014a6:004015bd(R), FUN_004014a6:004015da(R), FUN_004014a6:004015f7(R), FUN_004014a6:0040168e(R), FUN_0040170a:0040176b(W), FUN_0040170a:004017af(R)0040f880 undefi 00000000hNote at line 7 that one of the functions (FUN_0040170a) writes to it (W), rather than reading (R). Double‑clicking on the function shows its code, specifically where the DAT_ objects are initialized:
00401727 PUSH s_kernel32.dll_0040ebe8 ; LPCSTR lpLibFileName for LoadLibraryA0040172c CALL dword ptr [->KERNEL32.DLL::LoadLibraryA] ; = 0000d86400401732 MOV EDI, EAX00401734 CMP EDI, EBX00401736 JZ LAB_004017d80040173c PUSH ESI0040173d MOV ESI, dword ptr [->KERNEL32.DLL::GetProcAddress] ; = 0000d85200401743 PUSH s_CreateFileW_0040ebdc ; LPCSTR lpProcName for GetProcAddress00401748 PUSH EDI ; HMODULE hModule for GetProcAddress00401749 CALL ESI=>KERNEL32.DLL::GetProcAddress0040174b PUSH s_WriteFile_0040ebd0 ; LPCSTR lpProcName for GetProcAddress00401750 PUSH EDI ; HMODULE hModule for GetProcAddress00401751 MOV [DAT_0040f878], EAX00401756 CALL ESI=>KERNEL32.DLL::GetProcAddress00401758 PUSH s_ReadFile_0040ebc4 ; LPCSTR lpProcName for GetProcAddress0040175d PUSH EDI ; HMODULE hModule for GetProcAddress0040175e MOV [DAT_0040f87c], EAX00401763 CALL ESI=>KERNEL32.DLL::GetProcAddress00401765 PUSH s_MoveFileW_0040ebb8 ; LPCSTR lpProcName for GetProcAddress0040176a PUSH EDI ; HMODULE hModule for GetProcAddress0040176b MOV [DAT_0040f880], EAX00401770 CALL ESI=>KERNEL32.DLL::GetProcAddress00401772 PUSH s_MoveFileExW_0040ebac ; LPCSTR lpProcName for GetProcAddress00401777 PUSH EDI ; HMODULE hModule for GetProcAddress00401778 MOV [DAT_0040f884], EAX0040177d CALL ESI=>KERNEL32.DLL::GetProcAddress0040177f PUSH s_DeleteFileW_0040eba0 ; LPCSTR lpProcName for GetProcAddress00401784 PUSH EDI ; HMODULE hModule for GetProcAddress00401785 MOV [DAT_0040f888], EAX0040178a CALL ESI=>KERNEL32.DLL::GetProcAddress0040178c PUSH s_CloseHandle_0040eb94 ; LPCSTR lpProcName for GetProcAddress00401791 PUSH EDI ; HMODULE hModule for GetProcAddress00401792 MOV [DAT_0040f88c], EAX00401797 CALL ESI=>KERNEL32.DLL::GetProcAddress00401799 CMP dword ptr [DAT_0040f878], EBX0040179f MOV [DAT_0040f890], EAXStarting from the top, LoadLibrary is called. This syscall loads a module so that the process can use its functionality.
This is commonly used to avoid loading the functionalities directly and hide from tools like pestudio, see the Load Library page for more info on this. In this case the module loaded is kernel32.dll.
At this point, at line 3, EDI contains the loaded module.
Following that, GetProcAddress is called:
00401743 PUSH s_CreateFileW_0040ebdc ; LPCSTR lpProcName for GetProcAddress00401748 PUSH EDI ; HMODULE hModule for GetProcAddress00401749 CALL ESI=>KERNEL32.DLL::GetProcAddress0040174b PUSH s_WriteFile_0040ebd0 ; LPCSTR lpProcName for GetProcAddress00401750 PUSH EDI ; HMODULE hModule for GetProcAddress00401751 MOV [DAT_0040f878], EAXGetProcAddress 14 requires a handle to a module as first argument (EDI) and the name of the process to get the address of (s_CreateFileW):
FARPROC GetProcAddress( [in] HMODULE hModule, [in] LPCSTR lpProcName);Once called it returns the address, which is saved in DAT_. Looking back at the original function, this adds up since the DAT_ is called by dereferencing the pointer:
0040168e CALL dword ptr [DAT_0040f880]So all the DAT_ in this function are renamed from DAT_xxxx to DAT_<syscall_name>, for example DAT_WriteFile. The function has also been renamed from FUN_0040170a to assign_dat_fileop.
With a clearer picture of what function FUN_004014a6 calls, its purpose becomes easier to approach. It starts by calling the CreateFile call which in this case does not create the file. This can be understood based on two factors:
- Intuition: it does not make sense to have a
CreateFilefollowed by aGetFileSizeand aReadFile, mostly because the file has not been written yet. So the first one will return0as well as the second. CreateFile, based on the Microsoft documentation 15 has one parameter calleddwCreationDispositionwhich in this case is set to3. This is translated toOPEN_EXISTING, which is described by Microsoft as: “Opens a file or device, only if it exists. If the specified file or device does not exist, the function fails and the last-error code is set to ERROR_FILE_NOT_FOUND (2).” So in order for the program to continue, the file must exist already.
The first thing done while reading the file is to compare the first bytes with the WANCRY! bytes mentioned earlier.
At this point the analysis becomes less straightforward, mostly due to the three functions that are called while reading the file. The functions are:
FUN_004019e1FUN_00402a76FUN_00403a77
The last two are the “easiest” to guess; they likely implement encryption/decryption routines. The reason for this assumption is that they are full of loops with modulo operators (%), byte shifts (<<) and generally more mathematical operations.
The first one is probably the best place to start:
******************************************************* * FUNCTION * ******************************************************* undefined FUN_004019e1(undefined4 param_1, undefi assume FS_OFFSET = 0xffdff000 undefined AL:1 <RETURN> undefined4 Stack[0x4]:4 param_1 XREF[2]: 004019fc(R), 00401a26(R) undefined4 Stack[0x8]:4 param_2 XREF[3]: 004019f8(*), 00401a23(R), 00401a34(R) undefined4 Stack[0xc]:4 param_3 XREF[1]: 00401a29(R) undefined4 Stack[0x10] param_4 XREF[1]: 00401a31(R) FUN_004019e1 XREF[1]: FUN_004014a6:0040163d(c)004019e1 PUSH EBP004019e2 MOV EBP, ESP004019e4 PUSH ESI004019e5 MOV ESI, ECX004019e7 PUSH EDI004019e8 CMP dword ptr [ESI + 0x8], 0x0004019ec JZ LAB_00401a19004019ee LEA EDI, [ESI + 0x10]004019f1 PUSH EDI ; LPCRITICAL_SECTION lpCriticalSection for ; EnterCriticalSection004019f2 CALL dword ptr [->KERNEL32.DLL::EnterCriticalSectio ; = 0000d9a2004019f8 LEA EAX=>param_2, [EBP + 0xc]004019fb PUSH EAX004019fc PUSH dword ptr [EBP + param_1]004019ff PUSH 0x000401a01 PUSH 0x100401a03 PUSH 0x000401a05 PUSH dword ptr [ESI + 0x8]00401a08 CALL dword ptr [DAT_0040f8a4]00401a0e TEST EAX, EAX00401a10 PUSH EDI ; LPCRITICAL_SECTION lpCriticalSection for ; LeaveCriticalSection00401a11 JNZ LAB_00401a1d00401a13 CALL dword ptr [->KERNEL32.DLL::LeaveCriticalSectio ; = 0000d98a LAB_00401a19 XREF[1]: 004019ec(j)00401a19 XOR EAX, EAX00401a1b JMP LAB_00401a3f LAB_00401a1d XREF[1]: 00401a11(j)00401a1d CALL dword ptr [->KERNEL32.DLL::LeaveCriticalSectio ; = 0000d98a00401a23 PUSH dword ptr [EBP + param_2] ; size_t _Size for memcpy00401a26 PUSH dword ptr [EBP + param_1] ; void * _Src for memcpy00401a29 PUSH dword ptr [EBP + param_3] ; void * _Dst for memcpy00401a2c CALL MSVCRT.DLL::memcpy ; void * memcpy(void * _Dst, void * _Src, size_t ; _Size)What this function does is use EnterCriticalSection 16 and LeaveCriticalSection to get and release a critical section object, which is an object that can be held by a single thread to ensure that two different threads are not able to access the same shared memory area.
Once this access is granted, another DAT_ object is called. Using the same method as before shows that DAT_0040f8a4 is written in FUN_00401a45, along with other variables. These are renamed as follows:
- DAT_CryptAcquireContext
- DAT_CryptImportKey
- DAT_CryptDestroyKey
- DAT_CryptEncrypt
- DAT_CryptDecrypt
- DAT_CryptGenKey
And in the function, DAT_CryptDecrypt is called on the critical section object retrieved previously:
004019f2 CALL dword ptr [->KERNEL32.DLL::EnterCriticalSectio ; = 0000d9a2004019f8 LEA EAX=>param_2, [EBP + 0xc]004019fb PUSH EAX004019fc PUSH dword ptr [EBP + param_1]004019ff PUSH 0x000401a01 PUSH 0x100401a03 PUSH 0x000401a05 PUSH dword ptr [ESI + 0x8]00401a08 CALL dword ptr [DAT_CryptDecrypt]The current guess is that since it is decrypting a shared memory area, the area is t.wnry, which is an encrypted file. The reason for this guess is the proximity of the calls. The file being read is t.wnry, which has a custom signature and no recognizable content.
In Ghidra the renaming of functions starts to give them names based on what they seem to be doing. The process followed is:
- Finding the reference to the string
t_wnry. - Finding the first function that calls it, previously identified as
FUN_004014a6, then opening it and examining each function one by one.
The full rename is:
FUN_004014a6→t_wnry_opsFUN_004019e1→UseCryptDecryptFUN_00402a76→MathThings→ this and all the below ones are purposely vague, mostly because their behaviour is not immediately clearFUN_00403a77→LotOfMathFUN_00403797→MathThings2FUN_00403a28→RaiseExceptionFUN_0040350f→MathThings3
This might seem like a pointless renaming, but looking at the decompiled code now, some steps make more sense:
iVar2 = UseCryptDecrypt(*(in_ECX + 0x4c8), local_248, local_230, &local_30);if (iVar2 != 0) { MathThings(local_230, PTR_DAT_0040f578, local_30, 0x10); local_2c = GlobalAlloc(0, local_238);This means that if the return value of UseCryptDecrypt is different from 0, some calculations are performed with two of the values used by the first function (local_230 and local_30). What UseCryptDecrypt returns does not really matter, since it is always used as a control variable for returns of operations.
The first parameter of the function is a pointer to the current value of ESI + 0x4c8, which is populated in one of the ReadFile calls just above, since it is the second argument of the call, lpBuffer 17:
004015b6 PUSH dword ptr [ESI + 0x4c8]004015bc PUSH EDI004015bd CALL dword ptr [DAT_ReadFile]The content of ESI + 0x4c8 is built progressively. The very first ReadFile is used to compare the first 8 bytes (third argument, nNumberOfBytesToRead of ReadFile) with WANACRY!, the header that was found previously. This is likely an “integrity” check:
0040154c PUSH 0x80040154e LEA EAX=>local_240, [EBP + 0xfffffdc4]00401554 PUSH EAX00401555 PUSH EDI00401556 CALL dword ptr [DAT_ReadFile]0040155c TEST EAX, EAX0040155e JZ end00401564 PUSH 0x8 ; size_t _Size for memcmp00401566 PUSH s_WANACRY!_0040eb7c ; void * _Buf2 for memcmp0040156b LEA EAX=>local_240, [EBP + 0xfffffdc4]Next it reads 4 more bytes:
00401582 PUSH EBX00401583 LEA EAX=>local_20, [EBP + -0x1c]00401586 PUSH EAX00401587 PUSH 0x400401589 LEA EAX=>local_248, [EBP + 0xfffffdbc]0040158f PUSH EAX00401590 PUSH EDI00401591 CALL dword ptr [DAT_ReadFile]After the ReadFile it is compared to 0x100 (256 decimal):
00401597 TEST EAX, EAX00401599 JZ end0040159f MOV EAX, 0x100004015a4 CMP dword ptr [EBP + local_248], EAX004015aa JNZ endIf it is successful, this value (0x100) is passed to UseCryptDecrypt as the second parameter.
Lastly, it reads 256 bytes which will be saved in ESI + 0x4c8:
0040159f MOV EAX, 0x100004015a4 CMP dword ptr [EBP + local_248], EAX004015aa JNZ end004015b0 PUSH EBX004015b1 LEA ECX=>local_20, [EBP + -0x1c]004015b4 PUSH ECX004015b5 PUSH EAX004015b6 PUSH dword ptr [ESI + 0x4c8]004015bc PUSH EDI004015bd CALL dword ptr [DAT_ReadFile]To see the content described above, t.wnry can be opened in HxD:
So the first argument to UseCryptDecrypt is the encrypted content of t.wnry, or part of it. The second argument is 256.
The third and fourth arguments of UseCryptDecrypt are only initialized but never used before the call:
undefined local_230 [512];undefined4 local_30;UseCryptDecrypt calls CryptDecrypt 18 , which has the following signature:
BOOL CryptDecrypt( [in] HCRYPTKEY hKey, [in] HCRYPTHASH hHash, [in] BOOL Final, [in] DWORD dwFlags, [in, out] BYTE *pbData, [in, out] DWORD *pdwDataLen);It is called as follow:
004019f8 LEA EAX=>param_2, [EBP + 0xc]004019fb PUSH EAX004019fc PUSH dword ptr [EBP + param_1]004019ff PUSH 0x000401a01 PUSH 0x100401a03 PUSH 0x000401a05 PUSH dword ptr [ESI + 0x8]00401a08 CALL dword ptr [DAT_CryptDecrypt]So param_2 (256) is pdwDataLen, while param_1 is pbData. Respectively, param_2 will contain the length of the decrypted data and param_1 the decrypted data. The content of param_1 is then copied into param_3 (which is local_230), using memcpy 19.
00401a23 PUSH dword ptr [EBP + param_2] ; size_t _Size for memcpy00401a26 PUSH dword ptr [EBP + param_1] ; void * _Src for memcpy00401a29 PUSH dword ptr [EBP + param_3] ; void * _Dst for memcpy00401a2c CALL MSVCRT.DLL::memcpy ; void * memcpy(void * _Dst, void * _Src, size_tlocal_230 is now the decrypted content and it is used in MathThings.
local_230 is therefore renamed to decrypted_content.
param_4, which is local_30, gets the value of param_2 at the end of the function:
00401a31 MOV EAX, dword ptr [EBP + param_4]00401a34 MOV ECX, dword ptr [EBP + param_2]00401a37 ADD ESP, 0xc00401a3a MOV dword ptr [EAX], ECXBecause of that, local_30 is renamed to sizeof_decrypted_content.
Stepping back for a second, the analysis so far shows that the first 256 bytes of the file are decrypted, which is a very small portion of the file.
The decrypted content is used in a math-related function. After that, the file is read till the end:
0040167b LEA EAX=>local_20, [EBP + -0x1c]0040167e PUSH EAX0040167f PUSH dword ptr [EBP + local_28]00401682 PUSH dword ptr [ESI + 0x4c8]00401688 PUSH dword ptr [EBP + local_24c]0040168e CALL dword ptr [DAT_ReadFile]This happens because local_28, which defines the size of bytes to read, is initialized in GetFileSizeEx:
00401524 LEA EAX=>local_28, [EBP + -0x24]00401527 PUSH EAX ; PLARGE_INTEGER lpFileSize for GetFileSizeEx00401528 PUSH EDI ; HANDLE hFile for GetFileSizeEx00401529 CALL dword ptr [->KERNEL32.DLL::GetFileSizeEx] ; = 0000d912The content is saved in ESI + 0x4c8, which is then sent to the LotOfMath function. A plausible working hypothesis is that the first 256 bytes read are an encryption key. In that case, this call:
0040164d PUSH 0x100040164f PUSH dword ptr [EBP + sizeof_decrypted_content]00401652 PUSH dword ptr [PTR_DAT_0040f578] ; = 0040f91400401658 LEA EAX=>decrypted_content, [EBP + 0xfffffdd4]0040165e PUSH EAX0040165f MOV ECX, EDI00401661 CALL MathThings ; undefined MathThings(undefined4 param_1,is consistent with key-related processing.
One thing that might give it away is that MathThings is very likely using AES. The clue is in the only clearly recognizable element in MathThings. Scrolling through the function shows a call to DAT_004089fc, which, when double‑clicked in Ghidra, looks like this:
DAT_004089fc XREF[28]: MathThings:00402cca(R), MathThings:00402cdf(R), MathThings:00402ce6(R), ... FUN_00402e7e:0040318a(R), FUN_00402e7e:0040319f(R), [more]004089fc ?? 63h c004089fd ?? 7Ch |004089fe ?? 77h w004089ff ?? 7Bh {< a lot of other bytes here>Searching online for the first few bytes (63, 7C, 77) leads directly to the Rijndael S-box used in AES 20. This is renamed to rijndael_1 to make it easier to spot in other calls; note that it is referenced in at least one other function in the XREF array in the definition section.
MathThings can be renamed to AESDecryptMaybeKey.
Similarly, MathThings2 contains some variables:
DAT_0040a3fcDAT_0040a7fcDAT_00409ffcDAT_00409bfc
These look like different permutations of the same set of bytes:
DAT_0040a3fc→F4 51 50 A7DAT_0040a7fc→51 50 A7 F4DAT_00409ffc→A7 F4 51 50DAT_00409bfc→50 A7 F4 51
Searching for a larger part of the first sequence, F4 51 50 A7 41 7E 53 65 17 1A C3 A4 27 3A 96 5E, leads to 21, which defines them as reverse tables (decryption lookup tables). At this point MathThings2 is renamed to AESDecryptData and the various DAT above as decrypt_table.
Lastly, in MathThings3 there are four more lookup tables similar to AESDecryptMaybeKey. They are renamed as follows:
DAT_004093fc→rijndael_2DAT_004097fc→rijndael_3DAT_00408ffc→rijndael_4DAT_00408bfc→rijndael_5
The function is then likely going to encrypt something, so it is renamed to AESEncryptSomething.
LotOfMath is renamed as well, to AESFunc.
The function’s behaviour at this point is reasonably clear; the remaining task is to check what is returned. There are two returned values, one via EAX:
004016dd 8b c3 MOV EAX ,EBX004016df eb 18 JMP LAB_004016f9and the other by changing the value of the pointer given as the second argument of the function. Looking at the disassembled code:
local_2c = GlobalAlloc(0, local_238);
if (local_2c != (HGLOBAL)0x0) { iVar2 = (*DAT_ReadFile)( hFile, *(undefined4 *)(in_ECX + 0x4c8), local_28, local_20, 0 );
pvVar1 = local_2c;
if ( ((iVar2 != 0) && (local_20[0] != 0)) && ( (0x7fffffff < local_234) || ( ((int)local_234 < 1) && (local_238 <= local_20[0]) ) ) ) { AESFunc( *(undefined4 *)(in_ECX + 0x4c8), local_2c, local_20[0], 1 );
*param_2 = local_238; pvVar3 = pvVar1; ... skipped to reduce the code snip
return pvVar3;param_2 is returned as the value of local_238, which is used earlier in GlobalAlloc as the second argument. According to the documentation, this is the size in bytes to allocate in memory 22. What is returned by GlobalAlloc is a handle to the space that has been allocated. This is saved in local_2c and then moved into pvVar1, then into pvVar3, which is returned.
At this stage, it is useful to decide where to place the breakpoints and why everything done so far is important.
A breakpoint after the t_wnry_ops function call will allow capture of:
- The address of the decrypted payload (in
EAX) - The size in bytes of the payload
The breakpoints are set at:
0040163D, which is the first parameter of theGlobalAllocfunction and represents the hexadecimal size in bytes. Once the line is executed the stack shows the amount of space reserved.00402132, which is the end of thet_wnry_opsfunction.
When the executable reaches the first breakpoint, stepping over to the next instruction adds to the stack the value 0x10000, which converted to decimal is 65536 bytes.
At the second breakpoint, the value of EAX is set to the address where the file has been decrypted and copied: 028A1490 (this value can differ). Following this address in a dump window shows the memory area filled with data. The first bytes are 4D 5A, which is the magic value of a Windows executable.
The last step is to highlight the 10000 bytes of data from the beginning and dump them to a file. To help determine how much to highlight, the dump view shows something like:
Dump: 028A1490 → 028A1545 (0x000000B6 bytes)Highlighting continues until the value in parentheses reaches 0x00010000, then the highlighted area can be saved via Binary → Save to file.
This is yet another payload that needs to be analyzed. Opening it with pestudio shows that it is a DLL with the original name kbdlv.dll, which is the name of the Latvian Keyboard Layout driver. However, the analysis of kbdlv.dll is deferred; at this point the role of t.wnry is understood.
The last file from the original list of files is f.wnry. Opening it with Notepad shows a path: C:\Users\REM\AppData\Local\Programs\Microsoft VS Code\resources\app\licenses\LICENSE-fra.rtf.WNCRY. Looking at the directory, there are a few files with the same extension, just different languages:
LICENSE-bul.rtf.WNCRYLICENSE-chs.rtf.WNCRYLICENSE-cht.rtf.WNCRYLICENSE-deu.rtf.WNCRYLICENSE-fra.rtf.WNCRYLICENSE-esp.rtf.WNCRYLICENSE-ita.rtf.WNCRYLICENSE-hun.rtf.WNCRYLICENSE-jpn.rtf.WNCRYLICENSE-kor.rtf.WNCRYLICENSE-ptb.rtf.WNCRYLICENSE-rus.rtf.WNCRYLICENSE-trk.rtf.WNCRY
In a clean installation of Windows with VS Code, these files are present without the WNCRY extension. They therefore do not seem to provide any direct purpose to the sample itself.