← 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 17, Question 13 and Question 14

A common first step when dealing with .dll files is to see which functions they export via pestudio. In this case, on pestudio’s export page, only one function is exported and has the name TaskStart. In the tasksche.exe decompiled code this function is used:

tasksche.exe
00402145 68 e8 f4 PUSH s_TaskStart_0040f4e8 = "TaskStart"
40 00
0040214a 50 PUSH EAX
0040214b e8 d4 07 CALL FUN_00402924 undefined FUN_00402924(undefined
00 00
00402150 59 POP ECX
00402151 3b c3 CMP EAX ,EBX
00402153 59 POP ECX
00402154 74 04 JZ LAB_0040215a
00402156 53 PUSH EBX
00402157 53 PUSH EBX
00402158 ff d0 CALL EAX

The usual approach continues with FLOSS output filtered to a 12-character minimum. A few interesting strings appear.

Firstly, the strings below answer one of the earlier questions: what m.vbs is. It appeared when the analysis started but no clear traces were found until now.

echo SET ow = WScript.CreateObject("WScript.Shell")> m.vbs
echo SET om = ow.CreateShortcut("%s%s")>> m.vbs
echo om.TargetPath = "%s%s">> m.vbs
echo om.Save>> m.vbs
cscript.exe //nologo m.vbs
del m.vbs

Checking references of the address that stores the string above (the entire block is a single string in a single variable; searching for WScript in the “Defined Strings” view in Ghidra is sufficient) shows three addresses where it is used:

  • 10004d11
  • 10004d1d
  • 10004d29

Given how close they are to each other, any of them leads to the same function, just a few lines apart. In any case the navigation reaches FUN_10004cd0. Analysing it briefly, it starts by copying u.wnry into WanaDecryptor.exe:

kbdlv.dll
10004ceb PUSH s_@WanaDecryptor@.exe_1000d5c4 LPCSTR lpNewFileName for CopyFileA
10004cf0 PUSH s_u.wnry_1000d704 LPCSTR lpExistingFileName for CopyFileA
10004cf5 CALL dword ptr [-> KERNEL32.DLL::CopyFileA ] = 0000b486

It then proceeds with checking if the file exists using GetFileAttributesW 23, which fails if the file is not present. This is checked by comparing EAX (the return value of the function) and -0x1 (-1). If they are the same, execution continues to reading the VBS script.

kbdlv.dll
10004cfb PUSH u_@WanaDecryptor@.exe.lnk_1000cc44 LPCWSTR lpFileName for GetFileAttributesW
10004d00 CALL ESI => KERNEL32.DLL::GetFileAttributesW
10004d02 CMP EAX ,-0x1
10004d05 JNZ LAB_10004dd9
10004d0b PUSH EDI
10004d0c MOV ECX ,0x36
10004d11 MOV ESI ,s_@echo_off_echo_SET_ow_=_WScript._1000d628 = »@echo off
echo SET ow = WScript.CreateObject("WScript.Shell")> m.vbs
echo SET om = ow.CreateShortc
10004d16 LEA EDI => local_4c4 ,[ESP + 0x210 ]

After that, up to the end, there are several operations that are not immediately obvious. The sprintf at the end is easier to interpret:

kbdlv.dll
10004d9e LEA EAX => local_6cc ,[ESP + 0x8 ]
10004da2 PUSH s_@WanaDecryptor@.exe_1000d5c4 = "@WanaDecryptor@.exe"
10004da7 PUSH EAX
10004da8 LEA ECX => local_6cc ,[ESP + 0x10 ]
10004dac PUSH s_@WanaDecryptor@.exe.lnk_1000d60c = "@WanaDecryptor@.exe.lnk"
10004db1 LEA EDX => local_4c4 ,[ESP + 0x21c ]
10004db8 PUSH ECX
10004db9 LEA EAX => local_3e8 ,[ESP + 0x2fc ]
10004dc0 PUSH EDX char * _Format for sprintf
10004dc1 PUSH EAX char * _Dest for sprintf
10004dc2 CALL dword ptr [-> MSVCRT.DLL::sprintf ] = 0000b74c

sprintf has the following parameters:

int sprintf(
char *buffer,
const char *format [,
argument] ...
);

The arguments are the placeholders for the format string. In the code the function is called with the following arguments:

  • buffer: (destination) storage location for output. In the code this is EAX, local_3e8.
  • format: format-control string. In the code this is EDX (local_4c4).
  • arg1: ECX, local_6cc
  • arg2: the string @WanaDecryptor@.exe.lnk
  • arg3: again local_6cc
  • arg4: the string @WanaDecryptor@.exe

To understand the variable values it is easier to work backwards from the first one. local_3e8 has not been previously set and will be filled by sprintf.

local_4c4 is the template string, and its value can be derived from the following code, where the pointer to the string is copied into ESI and then from ESI into local_4c4 (EDI):

kbdlv.dll
10004d0c MOV ECX ,0x36
10004d11 MOV ESI ,s_@echo_off_echo_SET_ow_=_WScript._1000d628 = »@echo off
10004d16 LEA EDI => local_4c4 ,[ESP + 0x210 ]
10004d1d MOVSD.REP ES :EDI ,ESI => s_@echo_off_echo_SET_ow_=_WScript._1000d628 = »@echo off

local_6cc is the result (lpBuffer) of GetCurrentDirectory 24:

kbdlv.dll
10004d3a LEA ECX => local_6cc ,[ESP + 0x8 ]
10004d3e PUSH ECX LPSTR lpBuffer for GetCurrentDirectoryA
10004d3f PUSH 0x208 DWORD nBufferLength for GetCurrentDirectoryA
10004d44 STOSB ES :EDI
10004d45 CALL dword ptr [-> KERNEL32.DLL::GetCurrentDirectoryA ] = 0000b272

The final script, reconstructed from these pieces, is:

echo SET ow = WScript.CreateObject("WScript.Shell")
echo SET om = ow.CreateShortcut("<currentDirectory>/@WanaDecryptor@.exe.lnk")
echo om.TargetPath = "<currentDirectory>/@WanaDecryptor@.exe"
echo om.Save

This, plus the deletion of m.vbs, is stored in local_3e8, which is then used in the next call:

kbdlv.dll
10004dc8 LEA ECX => local_3e8 ,[ESP + 0x304 ]
10004dcf PUSH ECX
10004dd0 CALL FUN_10001140 undefined FUN_10001140(undefined4 param_1)

FUN_10001140 is straightforward. It starts by generating a random number to use as the filename for a .bat file, then saves the content of param_1 (the VBS script just analysed) to that file. It then calls FUN_10001080 with the name of the file as first parameter (local_104 is populated with sprintf).

kbdlv.dll
_Seed = GetTickCount();
srand(_Seed);
tVar3 = time((time_t *)0x0);
uVar1 = (undefined4)tVar3;
iVar2 = rand();
sprintf(local_104,s_%d%d.bat_1000c034,iVar2,uVar1);
_File = fopen(local_104,(char *)&_Mode_1000c030);
if (_File == (FILE *)0x0) {
return;
}
fprintf(_File,s_%s_del_/a_%%0_1000c020,param_1);
fclose(_File);
FUN_10001080(local_104,0,(LPDWORD)0x0);

FUN_10001080 is the last call needed to understand this flow. The expectation is that it executes the file, which is confirmed by the code:

kbdlv.dll
100010b9 MOV EAX ,dword ptr [ESP + param_1 ]
100010bd PUSH ESI BOOL bInheritHandles for CreateProcessA
100010be PUSH ESI LPSECURITY_ATTRIBUTES lpThreadAttributes for CreateProcessA
100010bf PUSH ESI LPSECURITY_ATTRIBUTES lpProcessAttributes for CreateProcessA
100010c0 PUSH EAX LPSTR lpCommandLine for CreateProcessA
100010c1 PUSH ESI LPCSTR lpApplicationName for CreateProcessA
100010c2 MOV dword ptr [ESP + local_54 ],ESI
100010c6 MOV dword ptr [ESP + local_18 ],0x1
100010ce MOV word ptr [ESP + local_14 ],SI
100010d3 CALL dword ptr [-> KERNEL32.DLL::CreateProcessA ] = 0000b1e2
100010d9 TEST EAX ,EAX
100010db JZ LAB_10001135
100010dd MOV EAX ,dword ptr [ESP + param_2 ]
100010e1 CMP EAX ,ESI
100010e3 JZ LAB_10001116
100010e5 MOV ECX => local_54 ,dword ptr [ESP + 0x8 ]
100010e9 PUSH EAX DWORD dwMilliseconds for WaitForSingleObject
100010ea PUSH ECX HANDLE hHandle for WaitForSingleObject
100010eb CALL dword ptr [-> KERNEL32.DLL::WaitForSingleObject ] = 0000b1cc

CreateProcessA 13 executes the lpCommandLine argument, which is assigned from param_1 via EAX. Then WaitForSingleObject 25 pauses the main thread until the new process is completed.

There are two other notable string blocks in kbdlv.dll: the first for killing SQL and Exchange processes:

md.exe /c start /b %s vs
taskkill.exe /f /im mysqld.exe
taskkill.exe /f /im sqlwriter.exe
taskkill.exe /f /im sqlserver.exe
taskkill.exe /f /im MSExchange*
taskkill.exe /f /im Microsoft.Exchange.*

and the second mentioning a Mutex, which is probably created by the sample (to learn more about Mutexes, see Mutex):

Global\MsWinZonesCacheCounterMutexA

Since the decompiled code is already under review, it is reasonable to skip further basic static and dynamic phases and proceed directly through the code. The purpose here is to understand at a high level what the TaskStart function is doing, not to detail every implementation nuance. If some behaviours prove interesting, they can be revisited in more depth. For now, the focus is on the various Microsoft API calls, opening each function, scanning the CALL statements, and renaming accordingly.

Starting with the first two Windows API calls:

kbdlv.dll
10005b45 CALL dword ptr [-> KERNEL32.DLL::GetModuleFileNameW ] = 0000b59e
10005b4b MOV ESI ,dword ptr [-> MSVCRT.DLL::wcsrchr ] = 0000b7f2
10005b51 LEA EAX => local_214 ,[ESP + 0x10 ]
... skipped lines ...
10005b70 LEA EDX => local_214 ,[ESP + 0x10 ]
10005b74 PUSH EDX LPCWSTR lpPathName for SetCurrentDirectoryW
10005b75 CALL dword ptr [-> KERNEL32.DLL::SetCurrentDirectoryW ] = 0000b586

They respectively retrieve the full path of the module (DLL) and set the current working directory to that path, saved in local_214 using GetModuleFileNameW 26 and SetCurrentDirectoryW 27.

FUN_10001000 is then called, which opens, reads and writes to c.wnry:

kbdlv.dll
10001016 PUSH s_c.wnry_1000c010 char * _Filename for fopen
1000101b CALL dword ptr [-> MSVCRT.DLL::fopen ] = 0000b73a
... skipped lines ...
1000103b CALL dword ptr [-> MSVCRT.DLL::fread ] = 0000b732
10001041 JMP LAB_1000104e
LAB_10001043 XREF[1]: 10001034 (j)
10001043 MOV ECX ,dword ptr [ESP + param_1 ]
10001047 PUSH ECX void * _Str for fwrite
10001048 CALL dword ptr [-> MSVCRT.DLL::fwrite ] = 0000b728

This function is renamed c_wnry_ops.

FUN_100012d0 is then executed; it is a simple if/else statement controlled by the result of FUN_100011d0. If FUN_100011d0 returns 0, it takes the current user via GetUserName 28 and compares it with SYSTEM:

kbdlv.dll
10001321 CALL dword ptr [-> ADVAPI32.DLL::GetUserNameW ] = 0000b610
10001327 LEA EDX => local_258 ,[ESP + 0x4 ]
1000132b PUSH u_SYSTEM_1000c068 wchar_t * _Str2 for _wcsicmp
10001330 PUSH EDX wchar_t * _Str1 for _wcsicmp
10001331 CALL dword ptr [-> MSVCRT.DLL::_wcsicmp ] = 0000bb9e

Otherwise it compares the SID S-1-5-18, the SECURITY_LOCAL_SYSTEM_RID ID 29 with the variable local_258, which is defined dynamically at the beginning of the function:

kbdlv.dll
10001303 LEA EDX => local_258 ,[ESP + 0x4 ]
10001307 PUSH EDX
10001308 PUSH u_S-1-5-18_1000c078 = u"S-1-5-18"
1000130d JMP LAB_10001331

If either comparison is true, the function returns 1, otherwise 0. This function is renamed running_as_admin. The internal function FUN_100011d0 is essentially trying to obtain the SID of the user running the current process. To do so it loads the process using GetCurrentProcess 30, then checks the access token for the associated user using GetTokenInformation 31 and converts it to a string using the s_ConvertSidToStringSidW_1000c040 string, which represents the ConvertSidToStringSid module 32 dynamically loaded via LoadLibrary. For more about dynamic loading, see the Load Library page.

GetTokenInformation is called twice, which is typical for some Windows APIs (see the learn section Intro To Windows API):

kbdlv.dll
100011d4 LEA EAX => local_8 ,[ESP + 0x8 ]
100011da PUSH 0x8 DWORD DesiredAccess for OpenProcessToken
100011dc MOV dword ptr [ESP + local_c ],0x0
100011e4 CALL dword ptr [-> KERNEL32.DLL::GetCurrentProcess ] = 0000b252
100011eb CALL dword ptr [-> ADVAPI32.DLL::OpenProcessToken ] = 0000b5fc
100011f1 TEST EAX ,EAX
100011f3 JNZ LAB_100011fb
100011fa RET
10001209 LEA ECX => local_c ,[ESP + 0x8 ]
10001214 CALL EDI => ADVAPI32.DLL::GetTokenInformation
10001216 TEST EAX ,EAX
10001218 JNZ LAB_1000122d
1000121a CALL dword ptr [-> KERNEL32.DLL::GetLastError ] = 0000b242
10001220 CMP EAX ,0x7a
10001223 JZ LAB_1000122d
10001226 XOR EAX ,EAX
1000122c RET
10001234 CALL dword ptr [-> KERNEL32.DLL::GlobalAlloc ] = 0000b234
10001244 LEA EDX => local_c ,[ESP + 0x8 ]
1000124e CALL EDI => ADVAPI32.DLL::GetTokenInformation
10001250 TEST EAX ,EAX
10001252 JNZ LAB_1000125a
10001259 RET
1000125f CALL dword ptr [-> KERNEL32.DLL::LoadLibraryA ] = 0000b224
10001265 TEST EAX ,EAX
10001267 JNZ LAB_1000126f
1000126e RET
10001275 CALL dword ptr [-> KERNEL32.DLL::GetProcAddress ] = 0000b212
1000127b TEST EAX ,EAX
1000127d JNZ LAB_10001285
10001284 RET
LAB_10001285 XREF[1]: 1000127d (j)
10001285 MOV dword ptr [ESP + local_4 ],0x0
1000128d MOV ECX ,dword ptr [ESI ]
1000128f LEA EDX => local_4 ,[ESP + 0x10 ]
10001295 CALL EAX
10001297 TEST EAX ,EAX
10001299 JNZ LAB_100012a1
100012a0 RET

This function is renamed get_sid.

Next, FUN_10003410 is called. It dynamically loads all the file operations available in kernel32.dll:

kbdlv.dll
10003458 MOV [DAT_1000d91c ],EAX
1000345d CALL EDI => KERNEL32.DLL::GetProcAddress
1000345f PUSH s_ReadFile_1000cf0c LPCSTR lpProcName for GetProcAddress

This is done for read, write, move, create and delete file operations. The function is renamed file_api_loads. All the DAT_ entries are also renamed to the corresponding operations:

kbdlv.dll
DAT_CreateFile = GetProcAddress(hModule,s_CreateFileW_1000cf24);
DAT_WriteFile = GetProcAddress(hModule,s_WriteFile_1000cf18);
DAT_ReadFile = GetProcAddress(hModule,s_ReadFile_1000cf0c);
DAT_MoveFile = GetProcAddress(hModule,s_MoveFileW_1000cf00);
DAT_MoveFileEx = GetProcAddress(hModule,s_MoveFileExW_1000cef4);
DAT_DeleteFile = GetProcAddress(hModule,s_DeleteFileW_1000cee8);
DAT_CloseHandle = GetProcAddress(hModule,s_CloseHandle_1000cedc);

The DAT_ prefix is kept to indicate that these were loaded dynamically.

If loading the above functions succeeds, the names of three files are prepared:

kbdlv.dll
10005bb0 PUSH s_%08X.res_1000d8c0 char * _Format for sprintf
10005bb5 PUSH _Dest_1000dcf0 char * _Dest for sprintf
10005bba CALL ESI => MSVCRT.DLL::sprintf

These files have been seen before but their exact use was not yet clear:

  • 00000000.eky
  • 00000000.pky
  • 00000000.res

The _Dest variables are renamed to _Dest_FilePath.eky and similar, to make later usage easier to track.

FUN_10004600 is called next, which simply opens or creates the global mutex s_Global\MsWinZonesCacheCounterMut_1000d520:

kbdlv.dll
10004604 PUSH s_Global\MsWinZonesCacheCounterMut_1000d520 LPCSTR lpName for OpenMutexA
10004609 PUSH 0x1 BOOL bInheritHandle for OpenMutexA
1000460b PUSH 0x100000 DWORD dwDesiredAccess for OpenMutexA
10004610 CALL dword ptr [-> KERNEL32.DLL::OpenMutexA ] = 0000b464
... skipping lines ...
10004634 PUSH s_Global\MsWinZonesCacheCounterMut_1000d4fc = "Global\\MsWinZonesCacheCounterMutexA"
10004639 PUSH _Format_1000d4f4 char * _Format for sprintf
1000463e PUSH ECX char * _Dest for sprintf
1000463f CALL dword ptr [-> MSVCRT.DLL::sprintf ] = 0000b74c
10004645 ADD ESP ,0x10
10004648 LEA EDX => local_64 ,[ESP + 0x4 ]
1000464c PUSH EDX LPCSTR lpName for CreateMutexA
1000464d PUSH 0x1 BOOL bInitialOwner for CreateMutexA
1000464f PUSH 0x0 LPSECURITY_ATTRIBUTES lpMutexAttributes for CreateMutexA
10004651 CALL dword ptr [-> KERNEL32.DLL::CreateMutexA ] = 0000b454

This function is renamed manage_mutex.

Once the mutex is created, FUN_10004500 is called. This function contains three subfunctions:

  • FUN_10003a10 → this only calls InitializeCriticalSection 33 and is renamed init_critical_section. A critical section is a section where more than one thread might need access and is protected by a critical section object that behaves like a mutex.
  • FUN_10003a60 → this only calls DeleteCriticalSection 34 and is renamed delete_critical_section.
  • FUN_10003d10 → this function is called before delete_critical_section. It is called with three parameters: the handle to the critical section object, the _Dest_FilePath.pky string and the string s_%08X.dky_1000d4e8, which is passed via local_40:
kbdlv.dll
1000451c LEA ECX => local_40 ,[ESP + 0x28 ]
10004520 PUSH ESI
10004521 PUSH EAX
10004522 PUSH s_%08X.dky_1000d4e8 char * _Format for sprintf
10004527 PUSH ECX char * _Dest for sprintf
10004528 CALL dword ptr [-> MSVCRT.DLL::sprintf ] = 0000b74c

This function is doing a lot of work, given the number of functions it calls. The first call is FUN_10003a80, which is short but initially opaque:

kbdlv.dll
LAB_10003a87 XREF[1]: 10003aab (j)
10003a87 MOV EAX ,ESI
10003a89 PUSH 0xf0000000
10003a8e NEG EAX
10003a90 SBB EAX ,EAX
10003a92 PUSH 0x18
10003a94 AND EAX ,s_Microsoft_Enhanced_RSA_and_AES_C_1000d168 = "Microsoft Enhanced RSA and AES Cryptographic Provider"
10003a99 PUSH EAX
10003a9a PUSH 0x0
10003a9c PUSH EDI
10003a9d CALL dword ptr [DAT_1000d93c ]
10003aa3 TEST EAX ,EAX
10003aa5 JNZ LAB_10003ab0
10003aa7 INC ESI
10003aa8 CMP ESI ,0x2
10003aab JL LAB_10003a87
10003aad POP EDI
10003aae POP ESI

It calls a DAT_ variable, which indicates a function initialized elsewhere. Double‑clicking on it takes you to its reference:

kbdlv.dll
DAT_1000d93c XREF[4]: FUN_10003a80:10003a9d (R) ,
FUN_10004440:10004440 (R) ,
FUN_10004440:1000447b (W) ,
FUN_10004440:100044b6 (R)
1000d93c undefined4 00000000h

Note that in function FUN_10004440 it is defined. Double-clicking on the function name shows a series of defined variables:

kbdlv.dll
DAT_1000d93c = GetProcAddress(hModule,s_CryptAcquireContextA_1000d1f8);
DAT_1000d940 = GetProcAddress(hModule,s_CryptImportKey_1000d1e8);
DAT_1000d944 = GetProcAddress(hModule,s_CryptDestroyKey_1000d1d8);
DAT_1000d948 = GetProcAddress(hModule,s_CryptEncrypt_1000d1c8);
DAT_1000d94c = GetProcAddress(hModule,s_CryptDecrypt_1000d1b8);
DAT_1000d950 = GetProcAddress(hModule,s_CryptGenKey_1000d1ac);

I have renamed the DAT_ variables to DAT_<apicallname> and the function to init_crypt_funcs. And so, FUN_10003a80 calls DAT_CryptAcquireContext and so I renamed it call_crypt_acquirecontext.

After this, function FUN_10003f00 is called, with first argument _Dest_FilePath.pky. This function opens, reads and uses it with DAT_CryptImportKey. Googling what a .pky file extension is, it seems to be a security certificate for PockeTTY 37. The function can then be renamed to load_cryptkey_from_pky.

This function is called twice, first with the .pky and the second time, if the first fails, using the .dky file. Once the key has been loaded it is tested, using CryptEncrypt followed by CrytpDecrypt. If the encryption or decryption fails, the function returns 0. Otherwise the decrypted value is compared with local_228 which is TESTDATA:

kbdlv.dll
10003e13 LEA this => local_22c ,[EBP + 0xfffffdd8 ]
10003e1a LEA EDX => local_21c ,[EBP + 0xfffffde8 ]
10003e27 MOV EAX ,dword ptr [EBX + 0x8 ]
10003e2b CALL dword ptr [DAT_CryptEncrypt ]
10003e31 TEST EAX ,EAX
LAB_10003e43 XREF[2]: 10003da0 (j) , 10003ee0 (j)
10003e43 XOR EAX ,EAX
10003e55 RET 0x8
LAB_10003e58 XREF[1]: 10003e33 (j)
10003e58 LEA EDX => local_22c ,[EBP + 0xfffffdd8 ]
10003e5f LEA EAX => local_21c ,[EBP + 0xfffffde8 ]
10003e6c MOV this ,dword ptr [EBX + 0xc ]
10003e70 CALL dword ptr [DAT_CryptDecrypt ]
10003e76 TEST EAX ,EAX
10003e78 JNZ LAB_10003e82
LAB_10003e82 XREF[1]: 10003e78 (j)
10003e82 LEA EDI => local_228 ,[EBP + 0xfffffddc ]
10003e88 OR this ,0xffffffff
10003e8b XOR EAX ,EAX
10003e8d SCASB.REPNE ES :EDI
10003e8f NOT this
10003e91 DEC this
10003e93 LEA EAX => local_228 ,[EBP + 0xfffffddc ]
10003e9a LEA this => local_21c ,[EBP + 0xfffffde8 ]
10003ea1 CALL dword ptr [-> MSVCRT.DLL::strncmp ] = 0000b81a
10003eaa TEST EAX ,EAX
10003eac JNZ LAB_10003ed4
10003eb0 LEA EDX => local_14 ,[EBP + -0x10 ]
10003eb4 CALL MSVCRT.DLL::_local_unwind2 undefined _local_unwind2()
10003ebc MOV EAX ,0x1
10003ec4 MOV dword ptr FS :[0x0 ]=> ExceptionList ,this = 00000000
10003ed1 RET 0x8
LAB_10003ed4 XREF[1]: 10003eac (j)
10003ed4 MOV dword ptr [EBP + local_8 ],0xffffffff
10003edb CALL FUN_10003ef6 undefined FUN_10003ef6(void)

I have then renamed the function FUN_10003d10 as load_crypt_key, and FUN_10004500 as safe_load_crypt_key to explain that it is the wrapper around load_crypt_key with the added safety of InitializeCriticalSection.

The next function that is called is FUN_10003ac0, which calls a couple of other functions:

  • FUN_10003c00 which has been renamed to destroy_key_and_load_key_pky
  • FUN_10004350 which has been renamed to call_CryptGenKey
  • FUN_10004040 since it calls CryptExportKey and file operations like CreateFile and WriteFile I will rename it crypt_and_write_file
  • FUN_10003c40 which calls FUN_10004170. Looking at it, it shows the usual math-like operations, hence it is renamed math_ops and FUN_10003c40 is renamed math_and_write_file since it calls CreateFile and WriteFile. If analysis later needs to dive into those functions and they require deeper work, it can be done then.

As a summary, FUN_10003ac0 is crypt_and_file_ops.

Moving on in TaskStart, the function FUN_100046d0 is called which opens and reads _FilePath.res:

kbdlv.dll
100046e1 PUSH _FilePath.res LPCSTR lpFileName for CreateFileA
100046e6 CALL dword ptr [-> KERNEL32.DLL::CreateFileA ] = 0000b416
... skipped lines ...
10004712 CALL dword ptr [-> KERNEL32.DLL::ReadFile ] = 0000b424

The function is renamed read_filepath_res. The file is then deleted outside the function:

kbdlv.dll
10005c61 PUSH _FilePath.res LPCSTR lpFileName for DeleteFileA
10005c66 CALL dword ptr [-> KERNEL32.DLL::DeleteFileA ] = 0000b578

And the function FUN_10004420 is called, which only calls CryptGenRandom 35, which fills a buffer of randomly generated bytes. The buffer is the second argument of the function, which is lpBuffer_1000dc68 (which is renamed to crypt_random_data):

kbdlv.dll
10005c7c PUSH lpBuffer_1000dc68 = 00000000
10005c81 MOV ECX ,ESI
10005c83 MOV dword ptr [DAT_1000dc70 ],EBX
10005c89 CALL fun_CryptGenRandom undefined fun_CryptGenRandom(void * this, BYTE * param_1, DWORD param_2)

The reason why there is no PUSH for the first argument is because this is a this call, a convention used in C++ in which the first argument (the this argument) is passed via ECX. See the Intro To Windows API to learn more.

FUN_10003bb0 calls CryptReleaseContext 36, which is a function used to free up all the cryptographic-related resources that are no longer needed. The function is renamed to fun_cryptreleasecontext.

Lastly, the function repeatedly calls CreateThread which executes the function defined at the pointer of the third argument lpStartAddress.

kbdlv.dll
10005c9e MOV ESI ,dword ptr [-> KERNEL32.DLL::CreateThread ] = 0000b2b6
10005ca4 PUSH EBX LPDWORD lpThreadId for CreateThread
10005ca5 PUSH EBX DWORD dwCreationFlags for CreateThread
10005ca6 PUSH EBX LPVOID lpParameter for CreateThread
10005ca7 PUSH lpStartAddress_10004790 LPTHREAD_START_ROUTINE lpStartAddress for CreateThread
10005cac PUSH EBX SIZE_T dwStackSize for CreateThread
10005cad PUSH EBX LPSECURITY_ATTRIBUTES lpThreadAttributes for CreateThread
10005cae CALL ESI => KERNEL32.DLL::CreateThread
10005cb0 MOV EBP ,dword ptr [-> KERNEL32.DLL::CloseHandle ] = 0000b194
10005cb6 CMP EAX ,EBX
10005cb8 JZ LAB_10005cbd
10005cba PUSH EAX HANDLE hObject for CloseHandle
10005cbb CALL EBP => KERNEL32.DLL::CloseHandle
LAB_10005cbd XREF[1]: 10005cb8 (j)
10005cbd MOV EDI ,dword ptr [-> KERNEL32.DLL::Sleep ] = 0000b3ce
10005cc3 PUSH 0x64 DWORD dwMilliseconds for Sleep
10005cc5 CALL EDI => KERNEL32.DLL::Sleep

After that there is a sleep of 100 ms to wait for the thread to run. Each call to CreateThread uses a different address; below they are reported in order:

  • lpStartAddress_10004790 this address seems to contain assembly code in the form of hexadecimal. And this is just a guess. These are the first few bytes and 53, 56 and 57 are the representation of PUSH EBX, PUSH ESI and PUSH EDI.
kbdlv.dll
10004795 ?? 53h S
10004796 ?? 56h V
10004797 ?? 57h W
10004798 ?? 85h

So what can be done is to take all the hexadecimal, load them in CyberChef with the operation “Disassemble x86” with the default parameters except for “Bit mode” set to 32, and the result is:

kbdlv.dll
00000000 53 PUSH EBX
00000001 56 PUSH ESI
00000002 57 PUSH EDI
00000003 85C0 TEST EAX,EAX
00000005 753E JNE 00000045
00000007 8B1D60710010 MOV EBX,DWORD PTR [1000716D]
0000000D 8B3D70700010 MOV EDI,DWORD PTR [10007083]
00000013 6A00 PUSH 00000000
00000015 FFD3 CALL EBX
00000017 83C404 ADD ESP,00000004
0000001A A3DCDC0010 MOV DWORD PTR [1000DCDC],EAX
0000001F E877FFFFFF CALL 00000-65
00000024 33F6 XOR ESI,ESI
00000026 A190DD0010 MOV EAX,DWORD PTR [1000DD90]
0000002B 85C0 TEST EAX,EAX
0000002D 7516 JNE 00000045
0000002F 68E8030000 PUSH 000003E8
00000034 FFD7 CALL EDI
00000036 46 INC ESI
00000037 83FE19 CMP ESI,00000019
0000003A 7CEA JL 00000026
0000003C A190DD0010 MOV EAX,DWORD PTR [1000DD90]
00000041 85C0 TEST EAX,EAX
00000043 74CE JE 00000013
00000045 6A00 PUSH 00000000
00000047 FF1564700010 CALL DWORD PTR [100070B1]
0000004D 5F POP EDI
0000004E 5E POP ESI
0000004F 5B POP EBX
00000050 90 NOP
00000051 90 NOP
00000052 90 NOP
00000053 90 NOP
00000054 90 NOP

For each of the CALL instructions the pointer can be checked; it should point to a function. In Ghidra, clicking anywhere on the screen and pressing “g” opens the “Go To” window; typing the address and going to the related address shows:

  • 1000716D points to MSVCRT.dll:wcscat
  • 10007083 points to kernel32.dll:WriteFile
  • 1000DCDC points to an empty space, which makes sense, since in this case the address is not called but written into: MOV DWORD PTR [1000DCDC],EAX
  • 1000DD90 points to an empty space
  • 10007083 points to kernel32.dll:DeleteFileW

So the address lpStartAddress_10004790 is renamed to write_and_delete_file.

  • lpStartAddress_100045c0 → calls safe_load_crypt_key; the address is renamed as addr_safe_load_crypt_key.
  • lpStartAddress_10005730 → this seems like an interesting function, since it starts by calling GetLogicalDrives to then start a new thread for each loop. This function is analyzed shortly.
  • lpStartAddress_10005300 → calls run_bat with taskdl.exe as argument, renamed as addr_run_taskdl.exe
  • lpStartAddress_10004990 → this looks promising in terms of functionalities as well, it mentions c_wnry_ops and tasksche.exe.

lpStartAddress_10005730 after calling GetLogicalDrives then starts a loop on DAT_1000dd8c:

kbdlv.dll
10005765 MOV EAX ,[DAT_1000dd8c ]
1000576a TEST EAX ,EAX
1000576c JNZ LAB_100057af

Within the loop it runs a new thread at each iteration with target lpStartAddress_10005680. This function calls a series of other functions. First FUN_10001590, which seems to be initializing a lot of variables like the code below, where EDI is set to 0 and then each index of ESI is set to EDI.

kbdlv.dll
100015b8 XOR EDI ,EDI
100015ba LEA param_1 ,[ESI + 0x2c ]
100015bd MOV dword ptr [ESP + local_4 ],EDI
100015c1 CALL init_critical_section undefined4 * init_critical_section(undefined4 * param_1)
100015c6 LEA param_1 ,[ESI + 0x54 ]
100015c9 MOV byte ptr [ESP + local_4 ],0x1
100015ce CALL FUN_10005d80 undefined FUN_10005d80(undefined4 * param_1)
100015d3 MOV AL ,byte ptr [ESP + local_11 ]
100015d7 MOV dword ptr [ESI + 0x4c8 ],EDI
100015dd MOV dword ptr [ESI + 0x4cc ],EDI
100015e3 MOV dword ptr [ESI + 0x4d0 ],EDI
100015e9 MOV dword ptr [ESI + 0x4d4 ],EDI

For now it is renamed as initializer. One thing to note though is that this function is setting a lot of components of param_1, which looks like a struct for some object. This is used a lot in the next function; however, the composition is not clear. From this point, some details might be lost, since it becomes harder to track what is going on in certain places.

Then the function FUN_10001830 is called:

kbdlv.dll
100056a4 PUSH DAT_1000dd8c
100056a9 PUSH LAB_10005340
100056ae PUSH _Dest_FilePath.pky = 00000000
100056b3 LEA ECX => local_930 ,[ESP + 0xc ]
100056b7 MOV dword ptr [ESP + local_4 ],0x0
100056c2 CALL FUN_10001830 undefined4 FUN_10001830(void * this, LPCSTR param_1, undefined4 param_2, undefined4 param_3)

Note the local_930 variable, which is assigned to ECX just before the call to the function, meaning it will be the this value. The function creates a new thread as well, this time pointing at lpStartAddress_100029e0. This address calls FUN_100029f0, still with the above-mentioned structure as parameter. This function seems to rename files:

kbdlv.dll
10002ab1 PUSH u_.WNCRYT_1000cbc8 = u".WNCRYT"
10002ab6 LEA EDX ,[ESI + 0x504 ]
10002abc PUSH EAX
10002abd PUSH EDX wchar_t * _Format for swprintf
10002abe LEA param_1 ,[EAX + 0x1 ]
10002ac1 PUSH u_%s\%d%s_1000cbb8 size_t _Count for swprintf
10002ac6 PUSH EBP wchar_t * _String for swprintf
10002ac7 MOV dword ptr [ESI + 0x914 ],param_1
10002acd CALL dword ptr [-> MSVCRT.DLL::swprintf ] = 0000b7c0
10002ad3 ADD ESP ,0x14
10002ad6 PUSH 0x1
10002ad8 PUSH EBP
10002ad9 PUSH EDI
10002ada CALL dword ptr [DAT_MoveFileEx ]

It renames the file only if param_1 + 0x70c is not empty. This can be seen below, where param_1 + 0x70c (ESI) is moved to EBP and then EBP is pushed on the stack before the strlen call. If the return value is less than or equal to 0 (JBE) then the MoveFile is triggered, otherwise it tries to delete the file.

kbdlv.dll
10002a48 LEA EBP ,[ESI + 0x70c ]
10002a4e MOV EAX ,dword ptr [ESI + 0x4e4 ]
10002a54 MOV EDI ,dword ptr [-> MSVCP60.DLL::`private:_static_unsigned_short_const*___cdecl_std = 0000b992
10002a5a MOV EAX ,dword ptr [EAX ]
10002a5c ADD EAX ,0x8
10002a5f MOV EAX ,dword ptr [EAX + 0x4 ]
10002a62 CMP EAX ,EBX
10002a64 JZ LAB_10002a68
10002a66 MOV EDI ,EAX
10002a68 PUSH EBP wchar_t * _Str for wcslen
10002a69 CALL dword ptr [-> MSVCRT.DLL::wcslen ] = 0000b782
10002a6f ADD ESP ,0x4
10002a72 TEST EAX ,EAX
10002a74 JBE LAB_10002ae4
10002a76 PUSH 0x1

This function is renamed to wncryt_rename_or_delete, the address lpStartAddress_10005680 is renamed to addr_wncryt_rename_or_delete and the function FUN_10001830 (which is the caller of the address just renamed) is renamed crypt_and_wncryt.

Next is the call to FUN_10001680. It starts by calling FUN_10001760, which is renamed destructor as it calls only GlobalFree and CryptReleaseContext to free up resources. At the end it calls FUN_10005db0 which is renamed to register_func_in_param_1, since that appears to be the only thing that it does:

kbdlv.dll
10005db0 MOV dword ptr [param_1 ],PTR_FUN_1000acbc = 10005d90

The function pointed to is a function not very clear, since it contains:

kbdlv.dll
1000acbc addr FUN_10005d90
1000acc0 addr DAT_1000acf8

and the first function contains:

kbdlv.dll
FUN_10005d90 XREF[1]: 1000acbc (*)
10005d90 PUSH ESI
10005d91 MOV ESI ,this
10005d93 CALL register_func_in_param_1 undefined register_func_in_param_1(undefined4 * param_1)
10005d98 TEST byte ptr [ESP + param_1 ],0x1
10005d9d JZ LAB_10005da8
10005d9f PUSH ESI void * param_1 for operator_delete
10005da0 CALL MSVCRT.DLL::operator_delete void operator_delete(void * param_1)
10005da5 ADD ESP ,0x4

which seems like it calls itself. Because of that, FUN_10001680 is renamed to cleanup_maybe. The “maybe” indicates the uncertainty over its functionalities.

The next function called in lpStartAddress_10005680 is FUN_10005540, which starts by identifying the drive type using GetDriveTypeW 38

kbdlv.dll
LAB_100055f2 XREF[1]: 10005575 (j)
100055f2 MOV ESI ,dword ptr [-> KERNEL32.DLL::GetDriveTypeW ] = 0000b50e
100055f8 LEA EDX => local_228 ,[ESP + 0x10 ]
100055fc PUSH EDX LPCWSTR lpRootPathName for GetDriveTypeW
100055fd CALL ESI => KERNEL32.DLL::GetDriveTypeW
100055ff CMP EAX ,0x5
10005602 JZ LAB_10005668

and skips CD-ROM (drive type ID 5). Otherwise, if it is a DRIVE_FIXED (hard drive or flash drive) it executes functions FUN_10005060 and FUN_10001910. The first one gets the Windows directory path:

kbdlv.dll
10005067 MOV ESI ,dword ptr [ESP + param_2 ]
1000506e PUSH EDI
1000506f PUSH 0x104 UINT uSize for GetWindowsDirectoryW
10005074 PUSH ESI LPWSTR lpBuffer for GetWindowsDirectoryW
10005075 CALL dword ptr [-> KERNEL32.DLL::GetWindowsDirectoryW ] = 0000b4b6

and assigns the value to param_2. It then compares the drive of the Windows path; if it matches, it takes the temporary path with GetTempPathW 39 (like C:\TEMP), otherwise if it is in a secondary disk, it creates a hidden recycle bin directory:

kbdlv.dll
LAB_100050d0 XREF[1]: 1000508c (j)
100050d0 PUSH u_$RECYCLE_1000d778 = u"$RECYCLE"
100050d5 PUSH EDI wchar_t * _Format for swprintf
100050d6 PUSH u_%C:\%s_1000d768 size_t _Count for swprintf
100050db PUSH ESI wchar_t * _String for swprintf
100050dc CALL dword ptr [-> MSVCRT.DLL::swprintf ] = 0000b7c0
100050e2 ADD ESP ,0x10
100050e5 PUSH 0x0 LPSECURITY_ATTRIBUTES lpSecurityAttributes for CreateDirectoryW
100050e7 PUSH ESI LPCWSTR lpPathName for CreateDirectoryW
100050e8 CALL dword ptr [-> KERNEL32.DLL::CreateDirectoryW ] = 0000b492
100050ee PUSH s_$RECYCLE_1000d75c = "$RECYCLE"
100050f3 PUSH EDI
100050f4 LEA EDX => local_400 ,[ESP + 0x10 ]
100050f8 PUSH s_attrib_+h_+s_%C:\%s_1000d748 char * _Format for sprintf
100050fd PUSH EDX char * _Dest for sprintf
100050fe CALL dword ptr [-> MSVCRT.DLL::sprintf ] = 0000b74c
10005104 PUSH 0x0
10005106 LEA EAX => local_400 ,[ESP + 0x1c ]
1000510a PUSH 0x0
1000510c PUSH EAX
1000510d CALL run_bat undefined4 run_bat(LPSTR param_1, DWORD param_2, LPDWORD param_3)

hiding it with the attrib command. It returns the path to this new directory or the temp path, which is stored in the second argument that was passed to the function. This function is renamed as return_temp_path.

The next function is FUN_10001910, which is interesting because its second argument is the return value of the previous one, and it looks like, based on the formatting string of the swprintf, u_%s\%d%s_1000cbb8, that it is building a string like <path>\<number>.WNCRYT.

kbdlv.dll
undefined __thiscall FUN_10001910 (void * this , wchar_t)
undefined AL:1 <RETURN>
void * ECX:4 (auto) this
wchar_t * Stack[0x4]:4 param_1 XREF[1]: 10001910 (R)
FUN_10001910 XREF[1]: FUN_10005540:1000564e (c)
10001910 MOV EAX ,dword ptr [ESP + param_1 ]
10001914 PUSH ESI
10001915 MOV ESI ,this
10001917 PUSH EDI
10001918 PUSH EAX wchar_t * _Source for wcscpy
10001919 LEA EDI ,[ESI + 0x504 ]
1000191f PUSH EDI wchar_t * _Dest for wcscpy
10001920 CALL dword ptr [-> MSVCRT.DLL::wcscpy ] = 0000b76e
10001926 MOV EAX ,dword ptr [ESI + 0x914 ]
1000192c ADD ESP ,0x8
1000192f ADD ESI ,0x70c
10001935 LEA this ,[EAX + 0x1 ]
10001938 PUSH u_.WNCRYT_1000cbc8 = u".WNCRYT"
1000193d MOV dword ptr [ESI + 0x208 ],this
10001943 PUSH EAX
10001944 PUSH EDI wchar_t * _Format for swprintf
10001945 PUSH u_%s\%d%s_1000cbb8 size_t _Count for swprintf
1000194a PUSH ESI wchar_t * _String for swprintf
1000194b CALL dword ptr [-> MSVCRT.DLL::swprintf ] = 0000b7c0

However, note that it has as destination this + 0x70c, which is ESI, which was given the value of this at line 3. This location (this + 0x70c) is actually deleted in the destructor which is called shortly after this function. The code from destructor that shows the delete:

kbdlv.dll
1000180d ADD ESI ,0x70c
10001813 PUSH ESI wchar_t * _Str for wcslen
10001814 CALL dword ptr [-> MSVCRT.DLL::wcslen ] = 0000b782
1000181a ADD ESP ,0x4
1000181d TEST EAX ,EAX
1000181f JBE LAB_10001828
10001821 PUSH ESI
10001822 CALL dword ptr [DAT_DeleteFile ]

This would explain why files with extension WNCRYT are never seen. I have renamed FUN_10001910 as counter_wncryt.

The last call of FUN_10005540 is FUN_100027f0. The first function it calls is FUN_10002300 which executes FindFirstFileW and, if it returns INVALID_HANDLE_VALUE (-1), the function takes a potential error/cleanup path: it calls internal helper functions (FUN_100036a0, FUN_100037c0 which I will check later).

kbdlv.dll
10002391 LEA EAX => local_a10 ,[ESP + 0x50 ]
10002395 LEA this => local_7c0 ,[ESP + 0x2a0 ]
1000239e CALL dword ptr [-> KERNEL32.DLL::FindFirstFileW ] = 0000b37e
100023a6 CMP EDI ,-0x1
100023ad JNZ LAB_10002413
100023b3 LEA EDX => local_a34 ,[ESP + 0x2c ]
100023b8 MOV byte ptr [ESP + local_4 ],0x0
100023c0 MOV this ,dword ptr [EAX ]
100023c4 LEA this => local_a4c ,[ESP + 0x20 ]
100023c8 CALL FUN_100036a0 undefined FUN_100036a0(void * this, int * * param_1, int * param_2, int * param_3)
100023d2 CALL MSVCRT.DLL::operator_delete void operator_delete(void * param_1)
100023de MOV dword ptr [ESP + local_a48 ],ESI
100023e2 MOV dword ptr [ESP + local_a44 ],ESI
100023e6 MOV this ,dword ptr [EAX ]
100023ea LEA this => local_a34 ,[ESP + 0x34 ]
100023ef LEA this => local_a40 ,[ESP + 0x2c ]
100023fa CALL FUN_100037c0 undefined FUN_100037c0(void * this, int * * param_1, int * param_2, int * param_3)
10002404 CALL MSVCRT.DLL::operator_delete void operator_delete(void * param_1)
1000240c XOR EAX ,EAX

If enumeration succeeds, the JNZ jump instruction points to LAB_10002413 which calls FUN_10002f70(param_1) once and saves the result in local_a28. After that, the main loop repeatedly inspects each WIN32_FIND_DATAW entry and decides whether to skip it or process it. WIN32_FIND_DATAW is a struct about the file found with the FindFirstFile and FindNextFile functions. More information 40

kbdlv.dll
LAB_10002413 XREF[1]: 100023ad (j)
10002413 PUSH EBX
10002414 CALL FUN_10002f70 undefined4 FUN_10002f70(LPCWSTR param_1)

The skip logic is mostly string comparisons against the current filename (stored inside local_9e4). It uses wcscmp to ignore specific names. The visible constants show it explicitly skipping:

  • @Please_Read_Me@.txt
  • @WanaDecryptor@.exe.lnk
  • @WanaDecryptor@.bmp and also two earlier _Str2_1000ccb4 / _Str2_1000ccac names (likely ”.” and ”..” or other sentinel names; the control flow suggests “if name equals X, skip” repeated twice).
kbdlv.dll
LAB_10002521 XREF[1]: 1000248f (j)
10002521 CMP dword ptr [ESP + local_a28 ],ESI
10002525 JZ LAB_1000262a
1000252b LEA this => local_9e4 ,[ESP + 0x7c ]
1000252f PUSH u_@Please_Read_Me@.txt_1000cc74 wchar_t * _Str2 for wcscmp
10002534 PUSH this wchar_t * _Str1 for wcscmp
10002535 CALL EBX => MSVCRT.DLL::wcscmp
10002537 ADD ESP ,0x8
1000253a TEST EAX ,EAX
1000253c JZ LAB_1000262a
10002542 LEA EDX => local_9e4 ,[ESP + 0x7c ]
10002546 PUSH u_@WanaDecryptor@.exe.lnk_1000cc44 wchar_t * _Str2 for wcscmp
1000254b PUSH EDX wchar_t * _Str1 for wcscmp
1000254c CALL EBX => MSVCRT.DLL::wcscmp
1000254e ADD ESP ,0x8
10002551 TEST EAX ,EAX
10002553 JZ LAB_1000262a
10002559 LEA EAX => local_9e4 ,[ESP + 0x7c ]
1000255d PUSH u_@WanaDecryptor@.bmp_1000cc1c wchar_t * _Str2 for wcscmp
10002562 PUSH EAX wchar_t * _Str1 for wcscmp
10002563 CALL EBX => MSVCRT.DLL::wcscmp
10002565 ADD ESP ,0x8
10002568 TEST EAX ,EAX
1000256a JZ LAB_1000262a
10002570 MOV this ,0x138
10002575 XOR EAX ,EAX
10002577 LEA EDI => local_4ee ,[ESP + 0x572 ]
1000257e MOV word ptr [ESP + local_4f0 ],SI

When it decides an entry is a candidate, it formats a full path using swprintf with "%s\\%s" (base directory param_1 + filename). It then checks the dwFileAttributes field from WIN32_FIND_DATAW (the TEST AL, 0x10 corresponds to testing the directory attribute bit, since 0x10 corresponds to FILE_ATTRIBUTE_DIRECTORY 41)

kbdlv.dll
10002473 LEA this => local_7c0 ,[ESP + 0x2a8 ]
1000247a PUSH u_%s\%s_1000cca0 size_t _Count for swprintf
1000247f PUSH this wchar_t * _String for swprintf
10002480 CALL dword ptr [-> MSVCRT.DLL::swprintf ] = 0000b7c0
10002486 MOV AL ,byte ptr [ESP + local_a10 ]
1000248a ADD ESP ,0x10
1000248d TEST AL ,0x10

If it is a directory, it calls an internal FUN_100032c0(fullPath, fileName) and if that returns true it skips.

If it is not a directory, it only proceeds if the earlier FUN_10002f70 flag (local_a28) is nonzero. It then performs the explicit “WanaDecryptor”/readme name exclusions above, and calls another internal function FUN_10002d60(filename). The return value is compared on a number of different values: if it is different from 6, 1, 0, and the variables local_9f4 and local_9f0 are respectively different to 0 and greater than 0xc800000 it executes FUN_10003760.

kbdlv.dll
10002591 CALL FUN_10002d60 int FUN_10002d60(wchar_t * param_1)
10002596 CMP EAX ,0x6
10002599 MOV dword ptr [ESP + local_10 ],EAX
100025a0 JZ LAB_1000262a
100025a6 CMP EAX ,0x1
100025a9 JZ LAB_1000262a
100025ab CMP EAX ,ESI
100025ad JNZ LAB_100025bf
100025af CMP dword ptr [ESP + local_9f4 ],ESI
100025b3 JA LAB_100025bf
100025b5 CMP dword ptr [ESP + local_9f0 ],0xc800000
100025bd JC LAB_1000262a

The enumeration loop advances using FindNextFileW with the same WIN32_FIND_DATAW buffer, and terminates when FindNextFileW returns 0.

After enumeration a few other calls are invoked. Before looking into them, the functions called before are:

  • FUN_100036a0: which is not clear in what it execute, but it does not call anything specific. I rename it to unknown_nothing_crazy.
  • FUN_100037c0: same as above, renamed to unknown_nothing_crazy2.
  • FUN_10002f70: it calls GetTempFileNameW 42 which creates a temporary file - renamed as call_gettempfilename.
  • FUN_10002d60: this is very interesting. Remember how after this call, the return value is compared to a couple of different numbers? These numbers are explained in the function code. Below the decompiled code to make it a bit easier to see the pattern:
kbdlv.dll
_Str1 = wcsrchr(param_1,L'.');
if (_Str1 == (wchar_t *)0x0) {
return 0;
}
iVar3 = _wcsicmp(_Str1,u_.exe_1000ccd0);
if ((iVar3 == 0) || (iVar3 = _wcsicmp(_Str1,u_.dll_1000ccc4), iVar3 == 0)) {
return 1;
}
iVar3 = _wcsicmp(_Str1,u_.WNCRY_1000cbf4);
if (iVar3 == 0) {
return 6;
}
ppuVar4 = &PTR_u_.doc_1000c098;
pwVar2 = (wchar_t *)PTR_u_.doc_1000c098;
while (pwVar2 != (wchar_t *)0x0) {
iVar3 = _wcsicmp((wchar_t *)*ppuVar4,_Str1);
if (iVar3 == 0) {
return 2;
}
ppwVar1 = (wchar_t **)ppuVar4 + 1;
ppuVar4 = (undefined **)((wchar_t **)ppuVar4 + 1);
pwVar2 = *ppwVar1;
}
ppuVar4 = &PTR_u_.docb_1000c0fc;
pwVar2 = (wchar_t *)PTR_u_.docb_1000c0fc;
while (pwVar2 != (wchar_t *)0x0) {
iVar3 = _wcsicmp((wchar_t *)*ppuVar4,_Str1);
if (iVar3 == 0) {
return 3;
}
ppwVar1 = (wchar_t **)ppuVar4 + 1;
ppuVar4 = (undefined **)((wchar_t **)ppuVar4 + 1);
pwVar2 = *ppwVar1;
}
iVar3 = _wcsicmp(_Str1,u_.WNCRYT_1000cbc8);
if (iVar3 != 0) {
iVar3 = _wcsicmp(_Str1,u_.WNCYR_1000cc04);
return (-(uint)(iVar3 != 0) & 0xfffffffb) + 5;
}
return 4;
}

Above is highlighted only what seems like an extension-to-return-value relationship. But there is more. Look at the following:

kbdlv.dll
ppuVar4 = &PTR_u_.doc_1000c098;
pwVar2 = (wchar_t *)PTR_u_.doc_1000c098;
while (pwVar2 != (wchar_t *)0x0) {
iVar3 = _wcsicmp((wchar_t *)*ppuVar4,_Str1);
if (iVar3 == 0) {
return 2;
}
ppwVar1 = (wchar_t **)ppuVar4 + 1;
ppuVar4 = (undefined **)((wchar_t **)ppuVar4 + 1);
pwVar2 = *ppwVar1;
}

In pwVar2 there is a pointer to a string, and then, at lines 8 and 10, the code is increasing that pointer, effectively moving to the next string. If in Ghidra that pointer is double-clicked, a bigger list of file extensions appears, reported below. The while loop will terminate at 1000c0f8 because it is null and will match the 0x0 value in the while condition. So the return code of 2 matches for all of these files. The return code of 3 follows the same pattern.

kbdlv.dll
PTR_u_.doc_1000c098 XREF[3]: FUN_10002d60:10002dc5 (R) ,
FUN_10002d60:10002dca (*) ,
FUN_10002d60:10002dd3 (R)
1000c098 addr u_.doc_1000cbac = u".doc"
PTR_u_.docx_1000c09c XREF[2]: FUN_10002d60:10002dd3 (R) ,
FUN_10002d60:10002de0 (R)
1000c09c addr u_.docx_1000cba0 = u".docx"
1000c0a0 addr u_.xls_1000cb94 = u".xls"
1000c0a4 addr u_.xlsx_1000cb88 = u".xlsx"
1000c0a8 addr u_.ppt_1000cb7c = u".ppt"
1000c0ac addr u_.pptx_1000cb70 = u".pptx"
1000c0b0 addr u_.pst_1000cb64 = u".pst"
1000c0b4 addr u_.ost_1000cb58 = u".ost"
1000c0b8 addr u_.msg_1000cb4c = u".msg"
1000c0bc addr u_.eml_1000cb40 = u".eml"
1000c0c0 addr u_.vsd_1000cb34 = u".vsd"
1000c0c4 addr u_.vsdx_1000cb28 = u".vsdx"
1000c0c8 addr u_.txt_1000cb1c = u".txt"
1000c0cc addr u_.csv_1000cb10 = u".csv"
1000c0d0 addr u_.rtf_1000cb04 = u".rtf"
1000c0d4 addr u_.123_1000caf8 = u".123"
1000c0d8 addr u_.wks_1000caec = u".wks"
1000c0dc addr u_.wk1_1000cae0 = u".wk1"
1000c0e0 addr u_.pdf_1000cad4 = u".pdf"
1000c0e4 addr u_.dwg_1000cac8 = u".dwg"
1000c0e8 addr u_.onetoc2_1000cab4 = u".onetoc2"
1000c0ec addr u_.snt_1000caa8 = u".snt"
1000c0f0 addr u_.jpeg_1000ca9c = u".jpeg"
1000c0f4 addr u_.jpg_1000ca90 = u".jpg"
1000c0f8 ?? 00h
1000c0f9 ?? 00h
1000c0fa ?? 00h
1000c0fb ?? 00h

In summary, return value per file type:

0: no dot/extension found (or “unknown” as used by caller)
1: .exe or .dll
2: .doc, .docx, .xls, .xlsx, .ppt, .pptx, .pst, .ost, .msg, .eml, .vsd, .vsdx, .txt, .csv, .rtf, .123, .wks, .wk1, .pdf, .dwg, .onetoc2, .snt, .jpeg, .jpg
3: .docb, .docm, .dot, .dotm, .dotx, .xlsm, .xlsb, .xlw, .xlt, .xlm, .xlc, .xltx, .xltm, .pptm, .pot, .pps, .ppsm, .ppsx, .ppam, .potx, .potm, .edb, .hwp, .602, .sxi, .sti, .sldx, .sldm, .sldm, .vdi, .vmdk, .vmx, .gpg, .aes, .ARC, .PAQ, .bz2, .tbk, .bak, .tar, .tgz, .gz, .7z, .rar, .zip, .backup, .iso, .vcd, .bmp, .png, .gif, .raw, .cgm, .tif, .tiff, .nef, .psd, .ai, .svg, .djvu, .m4u, .m3u, .mid, .wma, .flv, .3g2, .mkv, .3gp, .mp4, .mov, .avi, .asf, .mpeg, .vob, .mpg, .wmv, .fla, .swf, .wav, .mp3, .sh, .class, .jar, .java, .rb, .asp, .php, .jsp, .brd, .sch, .dch, .dip, .pl, .vb, .vbs, .ps1, .bat, .cmd, .js, .asm, .h, .pas, .cpp, .c, .cs, .suo, .sln, .ldf, .mdf, .ibd, .myi, .myd, .frm, .odb, .dbf, .db, .mdb, .accdb, .sql, .sqlitedb, .sqlite3, .asc, .lay6, .lay, .mml, .sxm, .otg, .odg, .uop, .std, .sxd, .otp, .odp, .wb2, .slk, .dif, .stc, .sxc, .ots, .ods, .3dm, .max, .3ds, .uot, .stw, .sxw, .ott, .odt, .pem, .p12, .csr, .crt, .key, .pfx, .der,
4: .WNCRYT
5: .WNCYR
6: .WNCRY or if nothing matches.

This function is renamed to file_ext_identifier.

Now if the file is not .WNCRY or an .exe or a .dll, it executes FUN_10003760 which is unclear in behavior, but is assumed to save the names of these files somewhere. The function is renamed to maybe_bookkeeping_non_exe_files.

If the original path being enumerated is a directory, it executes FUN_100032c0 which returns true if the directory name is or includes

  • \Intel
  • \ProgramData
  • \WINDOWS
  • \Program Files
  • \Program Files (x86)
  • \AppData\Local\Temp
  • \Local Settings\Temp
  • This folder protects against ransomware. Modifying it will reduce protection
  • Temporary Internet Files
  • Content.IE5

FUN_100032c0 is rename asdir_name_check.

It then calls FUN_100035c0 which seems to be directory bookkeeping, but that is not entirely clear. So it is renamed maybe_dir_bookkeeping.

At this point the overall direction is visible. The whole DLL is basically the core of the malware. The expectation is that soon the CrytEncrypt function will be called, very likely within this thread. This can be confirmed by first renaming FUN_10002300 to currently_analyzing_function so that it can be easily recognized later. Then under Windows → Symbol Reference, filtering for CryptEncrypt shows there is only one function that has not been renamed that calls it.

Ghidra's Symbol References for DAT_CryptEncrypt, five in total, of which only FUN_10004370 has not been renamed.
Fig. 26: Ghidra's Symbol References for DAT_CryptEncrypt, five in total, of which only FUN_10004370 has not been renamed.

Looking at the outgoing calls from that function:

The call tree for FUN_10004370, reached through FUN_10002940 from currently_analyzing_function.
Fig. 27: The call tree for FUN_10004370, reached through FUN_10002940 from currently_analyzing_function.

It can be seen that in function FUN_10002940, which is inside the function being analyzed, the encryption function is called.

This was definitely a different way to approach analysis of a sample that, given the complexity and depth of it, would probably have been easier to dynamically run on its own first and then go in Ghidra as needed; nonetheless it was an interesting deep dive. This allows answering confidently that this is the core of WanaCry.