← Reports

Ransomware · Ransomworm · Worm

WannaCry - invoice_greenanimals.pdf.exe

Author
Moise Medici
Updated
15 Feb 2026 · Completed
Difficulty
Medium
Platform
Windows
Capabilities
Command and Control C2 CommunicationCommand Execution via Powershell Cmd BashFile EncryptionPersistence Mechanisms
Tags
C++WannaCryptor

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.

The desktop background written from b.wnry, telling the victim to run @WanaDecryptor@.exe.
Fig. 20: The desktop background written from b.wnry, telling the victim to run @WanaDecryptor@.exe.

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.

The Wana Decrypt0r 2.0 payment window spawned from u.wnry, with the two countdown timers and the bitcoin address.
Fig. 21: The Wana Decrypt0r 2.0 payment window spawned from u.wnry, with the two countdown timers and the bitcoin address.

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:

Terminal window
$ 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.zip

t.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:

tasksche.exe
00402125 PUSH s_t.wnry_0040f4f4 ; = "t.wnry"
0040212a MOV dword ptr [EBP + local_8], EBX
0040212d 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:

tasksche.exe
004014fe PUSH EBX ; HANDLE hTemplateFile for CreateFileA
004014ff PUSH EBX ; DWORD dwFlagsAndAttributes for CreateFileA
00401500 PUSH 0x3 ; DWORD dwCreationDisposition for CreateFileA
00401502 PUSH EBX ; LPSECURITY_ATTRIBUTES lpSecurityAttributes for
; CreateFileA
00401503 PUSH 0x1 ; DWORD dwShareMode for CreateFileA
00401505 PUSH 0x80000000 ; DWORD dwDesiredAccess for CreateFileA
0040150a PUSH dword ptr [EBP + t_wnry] ; LPCSTR lpFileName for CreateFileA
0040150d CALL dword ptr [->KERNEL32.DLL::CreateFileA] ; = 0000d922
00401513 MOV EDI, EAX

Note 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.

tasksche.exe
00401515 MOV dword ptr [EBP + local_24c], EDI
0040151b CMP EDI, -0x1
0040151e JZ LAB_004016d0
00401524 LEA EAX=>local_28, [EBP + -0x24]
00401527 PUSH EAX ; PLARGE_INTEGER lpFileSize for GetFileSizeEx
00401528 PUSH EDI ; HANDLE hFile for GetFileSizeEx
00401529 CALL dword ptr [->KERNEL32.DLL::GetFileSizeEx] ; = 0000d912
0040152f CMP dword ptr [EBP + local_24], EBX
00401532 JG LAB_004016d0
00401538 JL LAB_00401547
0040153a CMP dword ptr [EBP + local_28], 0x6400000
00401541 JA LAB_004016d0
LAB_00401547 XREF[1]: 00401538(j)
00401547 PUSH EBX
00401548 LEA EAX=>local_20, [EBP + -0x1c]
0040154b PUSH EAX
0040154c PUSH 0x8
0040154e LEA EAX=>local_240, [EBP + 0xfffffdc4]
00401554 PUSH EAX
00401555 PUSH EDI
00401556 CALL dword ptr [DAT_0040f880]
0040155c TEST EAX, EAX
0040155e JZ LAB_004016d0
00401564 PUSH 0x8 ; size_t _Size for memcmp
00401566 PUSH s_WANACRY!_0040eb7c ; void * _Buf2 for memcmp
0040156b LEA EAX=>local_240, [EBP + 0xfffffdc4]
00401571 PUSH EAX ; void * _Buf1 for memcmp
00401572 CALL MSVCRT.DLL::memcmp ; int memcmp(void * _Buf1, void * _Buf2, size_t
; _Size)
00401577 ADD ESP, 0xc
0040157a TEST EAX, EAX
0040157c JNZ LAB_004016d0
00401582 PUSH EBX
00401583 LEA EAX=>local_20, [EBP + -0x1c]
00401586 PUSH EAX
00401587 PUSH 0x4
00401589 LEA EAX=>local_248, [EBP + 0xfffffdbc]
0040158f PUSH EAX
00401590 PUSH EDI
00401591 CALL dword ptr [DAT_0040f880]
00401597 TEST EAX, EAX
00401599 JZ LAB_004016d0
0040159f MOV EAX, 0x100
004015a4 CMP dword ptr [EBP + local_248], EAX
004015aa JNZ LAB_004016d0
004015b0 PUSH EBX
004015b1 LEA ECX=>local_20, [EBP + -0x1c]
004015b4 PUSH ECX
004015b5 PUSH EAX
004015b6 PUSH dword ptr [ESI + 0x4c8]
004015bc PUSH EDI
004015bd CALL dword ptr [DAT_0040f880]
004015c3 TEST EAX, EAX
004015c5 JZ LAB_004016d0
004015cb PUSH EBX
004015cc LEA EAX=>local_20, [EBP + -0x1c]
004015cf PUSH EAX
004015d0 PUSH 0x4
004015d2 LEA EAX=>local_244, [EBP + 0xfffffdc0]
004015d8 PUSH EAX
004015d9 PUSH EDI
004015da CALL dword ptr [DAT_0040f880]
004015e0 TEST EAX, EAX
004015e2 JZ LAB_004016d0
004015e8 PUSH EBX
004015e9 LEA EAX=>local_20, [EBP + -0x1c]
004015ec PUSH EAX
004015ed PUSH 0x8
004015ef LEA EAX=>local_238, [EBP + 0xfffffdcc]
004015f5 PUSH EAX
004015f6 PUSH EDI
004015f7 CALL dword ptr [DAT_0040f880]
004015fd TEST EAX, EAX
004015ff JZ LAB_004016d0
00401605 CMP dword ptr [EBP + local_234], EBX
0040160b JG LAB_004016d0
00401611 JL LAB_00401623
00401613 CMP dword ptr [EBP + local_238], 0x6400000
0040161d JA LAB_004016d0
LAB_00401623 XREF[1]: 00401611(j)
00401623 LEA EAX=>local_30, [EBP + -0x2c]
00401626 PUSH EAX
00401627 LEA EAX=>local_230, [EBP + 0xfffffdd4]
0040162d PUSH EAX
0040162e 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, EAX
00401644 JZ LAB_004016d0
0040164a LEA EDI, [ESI + 0x54]
0040164d PUSH 0x10
0040164f PUSH dword ptr [EBP + local_30]
00401652 PUSH dword ptr [PTR_DAT_0040f578] ; = 0040f914
00401658 LEA EAX=>local_230, [EBP + 0xfffffdd4]
0040165e PUSH EAX
0040165f MOV ECX, EDI
00401661 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 GlobalAlloc
0040166c PUSH EBX ; UINT uFlags for GlobalAlloc
0040166d CALL dword ptr [->KERNEL32.DLL::GlobalAlloc] ; = 0000d874
00401673 MOV dword ptr [EBP + local_2c], EAX
00401676 CMP EAX, EBX
00401678 JZ LAB_004016d0
0040167a PUSH EBX
0040167b LEA EAX=>local_20, [EBP + -0x1c]
0040167e PUSH EAX
0040167f 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, EAX
00401696 JZ LAB_004016d0
00401698 MOV EAX, dword ptr [EBP + local_20]
0040169b CMP EAX, EBX
0040169d JZ LAB_004016d0
0040169f CMP EBX, dword ptr [EBP + local_234]
004016a5 JG LAB_004016b1
004016a7 JL LAB_004016d0
004016a9 CMP EAX, dword ptr [EBP + local_238]
004016af JC LAB_004016d0
LAB_004016b1 XREF[1]: 004016a5(j)
004016b1 PUSH 0x1
004016b3 PUSH EAX
004016b4 MOV EBX, dword ptr [EBP + local_2c]
004016b7 PUSH EBX
004016b8 PUSH dword ptr [ESI + 0x4c8]
004016be MOV ECX, EDI
004016c0 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 -0x1
004016d2 LEA EAX=>local_14, [EBP + -0x10]
004016d5 PUSH EAX
004016d6 CALL MSVCRT.DLL::_local_unwind2 ; undefined _local_unwind2()
004016db POP ECX
004016dc POP ECX
004016dd MOV EAX, EBX
004016df JMP LAB_004016f9

Currently the problem is what those DAT_xxxx strings are, for example:

tasksche.exe
00401554 PUSH EAX
00401555 PUSH EDI
00401556 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:

tasksche.exe
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 00000000h

Note 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:

tasksche.exe
00401727 PUSH s_kernel32.dll_0040ebe8 ; LPCSTR lpLibFileName for LoadLibraryA
0040172c CALL dword ptr [->KERNEL32.DLL::LoadLibraryA] ; = 0000d864
00401732 MOV EDI, EAX
00401734 CMP EDI, EBX
00401736 JZ LAB_004017d8
0040173c PUSH ESI
0040173d MOV ESI, dword ptr [->KERNEL32.DLL::GetProcAddress] ; = 0000d852
00401743 PUSH s_CreateFileW_0040ebdc ; LPCSTR lpProcName for GetProcAddress
00401748 PUSH EDI ; HMODULE hModule for GetProcAddress
00401749 CALL ESI=>KERNEL32.DLL::GetProcAddress
0040174b PUSH s_WriteFile_0040ebd0 ; LPCSTR lpProcName for GetProcAddress
00401750 PUSH EDI ; HMODULE hModule for GetProcAddress
00401751 MOV [DAT_0040f878], EAX
00401756 CALL ESI=>KERNEL32.DLL::GetProcAddress
00401758 PUSH s_ReadFile_0040ebc4 ; LPCSTR lpProcName for GetProcAddress
0040175d PUSH EDI ; HMODULE hModule for GetProcAddress
0040175e MOV [DAT_0040f87c], EAX
00401763 CALL ESI=>KERNEL32.DLL::GetProcAddress
00401765 PUSH s_MoveFileW_0040ebb8 ; LPCSTR lpProcName for GetProcAddress
0040176a PUSH EDI ; HMODULE hModule for GetProcAddress
0040176b MOV [DAT_0040f880], EAX
00401770 CALL ESI=>KERNEL32.DLL::GetProcAddress
00401772 PUSH s_MoveFileExW_0040ebac ; LPCSTR lpProcName for GetProcAddress
00401777 PUSH EDI ; HMODULE hModule for GetProcAddress
00401778 MOV [DAT_0040f884], EAX
0040177d CALL ESI=>KERNEL32.DLL::GetProcAddress
0040177f PUSH s_DeleteFileW_0040eba0 ; LPCSTR lpProcName for GetProcAddress
00401784 PUSH EDI ; HMODULE hModule for GetProcAddress
00401785 MOV [DAT_0040f888], EAX
0040178a CALL ESI=>KERNEL32.DLL::GetProcAddress
0040178c PUSH s_CloseHandle_0040eb94 ; LPCSTR lpProcName for GetProcAddress
00401791 PUSH EDI ; HMODULE hModule for GetProcAddress
00401792 MOV [DAT_0040f88c], EAX
00401797 CALL ESI=>KERNEL32.DLL::GetProcAddress
00401799 CMP dword ptr [DAT_0040f878], EBX
0040179f MOV [DAT_0040f890], EAX

Starting 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:

tasksche.exe
00401743 PUSH s_CreateFileW_0040ebdc ; LPCSTR lpProcName for GetProcAddress
00401748 PUSH EDI ; HMODULE hModule for GetProcAddress
00401749 CALL ESI=>KERNEL32.DLL::GetProcAddress
0040174b PUSH s_WriteFile_0040ebd0 ; LPCSTR lpProcName for GetProcAddress
00401750 PUSH EDI ; HMODULE hModule for GetProcAddress
00401751 MOV [DAT_0040f878], EAX

GetProcAddress 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:

tasksche.exe
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:

  1. Intuition: it does not make sense to have a CreateFile followed by a GetFileSize and a ReadFile, mostly because the file has not been written yet. So the first one will return 0 as well as the second.
  2. CreateFile, based on the Microsoft documentation 15 has one parameter called dwCreationDisposition which in this case is set to 3. This is translated to OPEN_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_004019e1
  • FUN_00402a76
  • FUN_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:

tasksche.exe
*******************************************************
* 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 EBP
004019e2 MOV EBP, ESP
004019e4 PUSH ESI
004019e5 MOV ESI, ECX
004019e7 PUSH EDI
004019e8 CMP dword ptr [ESI + 0x8], 0x0
004019ec JZ LAB_00401a19
004019ee LEA EDI, [ESI + 0x10]
004019f1 PUSH EDI ; LPCRITICAL_SECTION lpCriticalSection for
; EnterCriticalSection
004019f2 CALL dword ptr [->KERNEL32.DLL::EnterCriticalSectio ; = 0000d9a2
004019f8 LEA EAX=>param_2, [EBP + 0xc]
004019fb PUSH EAX
004019fc PUSH dword ptr [EBP + param_1]
004019ff PUSH 0x0
00401a01 PUSH 0x1
00401a03 PUSH 0x0
00401a05 PUSH dword ptr [ESI + 0x8]
00401a08 CALL dword ptr [DAT_0040f8a4]
00401a0e TEST EAX, EAX
00401a10 PUSH EDI ; LPCRITICAL_SECTION lpCriticalSection for
; LeaveCriticalSection
00401a11 JNZ LAB_00401a1d
00401a13 CALL dword ptr [->KERNEL32.DLL::LeaveCriticalSectio ; = 0000d98a
LAB_00401a19 XREF[1]: 004019ec(j)
00401a19 XOR EAX, EAX
00401a1b JMP LAB_00401a3f
LAB_00401a1d XREF[1]: 00401a11(j)
00401a1d CALL dword ptr [->KERNEL32.DLL::LeaveCriticalSectio ; = 0000d98a
00401a23 PUSH dword ptr [EBP + param_2] ; size_t _Size for memcpy
00401a26 PUSH dword ptr [EBP + param_1] ; void * _Src for memcpy
00401a29 PUSH dword ptr [EBP + param_3] ; void * _Dst for memcpy
00401a2c 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:

tasksche.exe
004019f2 CALL dword ptr [->KERNEL32.DLL::EnterCriticalSectio ; = 0000d9a2
004019f8 LEA EAX=>param_2, [EBP + 0xc]
004019fb PUSH EAX
004019fc PUSH dword ptr [EBP + param_1]
004019ff PUSH 0x0
00401a01 PUSH 0x1
00401a03 PUSH 0x0
00401a05 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:

  1. Finding the reference to the string t_wnry.
  2. 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_004014a6t_wnry_ops
  • FUN_004019e1UseCryptDecrypt
  • FUN_00402a76MathThings → this and all the below ones are purposely vague, mostly because their behaviour is not immediately clear
  • FUN_00403a77LotOfMath
  • FUN_00403797MathThings2
  • FUN_00403a28RaiseException
  • FUN_0040350fMathThings3

This might seem like a pointless renaming, but looking at the decompiled code now, some steps make more sense:

tasksche.exe
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:

tasksche.exe
004015b6 PUSH dword ptr [ESI + 0x4c8]
004015bc PUSH EDI
004015bd 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:

tasksche.exe
0040154c PUSH 0x8
0040154e LEA EAX=>local_240, [EBP + 0xfffffdc4]
00401554 PUSH EAX
00401555 PUSH EDI
00401556 CALL dword ptr [DAT_ReadFile]
0040155c TEST EAX, EAX
0040155e JZ end
00401564 PUSH 0x8 ; size_t _Size for memcmp
00401566 PUSH s_WANACRY!_0040eb7c ; void * _Buf2 for memcmp
0040156b LEA EAX=>local_240, [EBP + 0xfffffdc4]

Next it reads 4 more bytes:

tasksche.exe
00401582 PUSH EBX
00401583 LEA EAX=>local_20, [EBP + -0x1c]
00401586 PUSH EAX
00401587 PUSH 0x4
00401589 LEA EAX=>local_248, [EBP + 0xfffffdbc]
0040158f PUSH EAX
00401590 PUSH EDI
00401591 CALL dword ptr [DAT_ReadFile]

After the ReadFile it is compared to 0x100 (256 decimal):

tasksche.exe
00401597 TEST EAX, EAX
00401599 JZ end
0040159f MOV EAX, 0x100
004015a4 CMP dword ptr [EBP + local_248], EAX
004015aa JNZ end

If 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:

tasksche.exe
0040159f MOV EAX, 0x100
004015a4 CMP dword ptr [EBP + local_248], EAX
004015aa JNZ end
004015b0 PUSH EBX
004015b1 LEA ECX=>local_20, [EBP + -0x1c]
004015b4 PUSH ECX
004015b5 PUSH EAX
004015b6 PUSH dword ptr [ESI + 0x4c8]
004015bc PUSH EDI
004015bd CALL dword ptr [DAT_ReadFile]

To see the content described above, t.wnry can be opened in HxD:

t.wnry in HxD, opening with the WANACRY! magic value followed by 00 01 00 00.
Fig. 22: t.wnry in HxD, opening with the WANACRY! magic value followed by 00 01 00 00.

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:

tasksche.exe
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:

tasksche.exe
004019f8 LEA EAX=>param_2, [EBP + 0xc]
004019fb PUSH EAX
004019fc PUSH dword ptr [EBP + param_1]
004019ff PUSH 0x0
00401a01 PUSH 0x1
00401a03 PUSH 0x0
00401a05 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.

tasksche.exe
00401a23 PUSH dword ptr [EBP + param_2] ; size_t _Size for memcpy
00401a26 PUSH dword ptr [EBP + param_1] ; void * _Src for memcpy
00401a29 PUSH dword ptr [EBP + param_3] ; void * _Dst for memcpy
00401a2c CALL MSVCRT.DLL::memcpy ; void * memcpy(void * _Dst, void * _Src, size_t

local_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:

tasksche.exe
00401a31 MOV EAX, dword ptr [EBP + param_4]
00401a34 MOV ECX, dword ptr [EBP + param_2]
00401a37 ADD ESP, 0xc
00401a3a MOV dword ptr [EAX], ECX

Because 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 first 256 bytes of t.wnry selected in HxD, a small fraction of the whole file.
Fig. 23: The first 256 bytes of t.wnry selected in HxD, a small fraction of the whole file.

The decrypted content is used in a math-related function. After that, the file is read till the end:

tasksche.exe
0040167b LEA EAX=>local_20, [EBP + -0x1c]
0040167e PUSH EAX
0040167f 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:

tasksche.exe
00401524 LEA EAX=>local_28, [EBP + -0x24]
00401527 PUSH EAX ; PLARGE_INTEGER lpFileSize for GetFileSizeEx
00401528 PUSH EDI ; HANDLE hFile for GetFileSizeEx
00401529 CALL dword ptr [->KERNEL32.DLL::GetFileSizeEx] ; = 0000d912

The 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:

tasksche.exe
0040164d PUSH 0x10
0040164f PUSH dword ptr [EBP + sizeof_decrypted_content]
00401652 PUSH dword ptr [PTR_DAT_0040f578] ; = 0040f914
00401658 LEA EAX=>decrypted_content, [EBP + 0xfffffdd4]
0040165e PUSH EAX
0040165f MOV ECX, EDI
00401661 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:

tasksche.exe
DAT_004089fc XREF[28]: MathThings:00402cca(R),
MathThings:00402cdf(R),
MathThings:00402ce6(R),
...
FUN_00402e7e:0040318a(R),
FUN_00402e7e:0040319f(R),
[more]
004089fc ?? 63h c
004089fd ?? 7Ch |
004089fe ?? 77h w
004089ff ?? 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_0040a3fc
  • DAT_0040a7fc
  • DAT_00409ffc
  • DAT_00409bfc

These look like different permutations of the same set of bytes:

  • DAT_0040a3fcF4 51 50 A7
  • DAT_0040a7fc51 50 A7 F4
  • DAT_00409ffcA7 F4 51 50
  • DAT_00409bfc50 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_004093fcrijndael_2
  • DAT_004097fcrijndael_3
  • DAT_00408ffcrijndael_4
  • DAT_00408bfcrijndael_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:

tasksche.exe
004016dd 8b c3 MOV EAX ,EBX
004016df eb 18 JMP LAB_004016f9

and the other by changing the value of the pointer given as the second argument of the function. Looking at the disassembled code:

tasksche.exe
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:

  1. The address of the decrypted payload (in EAX)
  2. The size in bytes of the payload

The breakpoints are set at:

  • 0040163D, which is the first parameter of the GlobalAlloc function 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 the t_wnry_ops function.

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.

The breakpoint before GlobalAlloc, where the stack now holds 0x10000, which is 65536 bytes.
Fig. 24: The breakpoint before GlobalAlloc, where the stack now holds 0x10000, which 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.

At the second breakpoint EAX points to 028A1490, where the dump shows the decrypted content starting with 4D 5A.
Fig. 25: At the second breakpoint EAX points to 028A1490, where the dump shows the decrypted content starting with 4D 5A.

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.WNCRY
  • LICENSE-chs.rtf.WNCRY
  • LICENSE-cht.rtf.WNCRY
  • LICENSE-deu.rtf.WNCRY
  • LICENSE-fra.rtf.WNCRY
  • LICENSE-esp.rtf.WNCRY
  • LICENSE-ita.rtf.WNCRY
  • LICENSE-hun.rtf.WNCRY
  • LICENSE-jpn.rtf.WNCRY
  • LICENSE-kor.rtf.WNCRY
  • LICENSE-ptb.rtf.WNCRY
  • LICENSE-rus.rtf.WNCRY
  • LICENSE-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.