Ransomware · Ransomworm · Worm
WannaCry - invoice_greenanimals.pdf.exe
- Author
- Moise Medici
- Updated
- 15 Feb 2026 · Completed
- Difficulty
- Medium
- Platform
- Capabilities
- Tags
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:
00402145 68 e8 f4 PUSH s_TaskStart_0040f4e8 = "TaskStart" 40 000040214a 50 PUSH EAX0040214b e8 d4 07 CALL FUN_00402924 undefined FUN_00402924(undefined 00 0000402150 59 POP ECX00402151 3b c3 CMP EAX ,EBX00402153 59 POP ECX00402154 74 04 JZ LAB_0040215a00402156 53 PUSH EBX00402157 53 PUSH EBX00402158 ff d0 CALL EAXThe 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.vbsecho SET om = ow.CreateShortcut("%s%s")>> m.vbsecho om.TargetPath = "%s%s">> m.vbsecho om.Save>> m.vbscscript.exe //nologo m.vbsdel m.vbsChecking 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:
10004d1110004d1d10004d29
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:
10004ceb PUSH s_@WanaDecryptor@.exe_1000d5c4 LPCSTR lpNewFileName for CopyFileA10004cf0 PUSH s_u.wnry_1000d704 LPCSTR lpExistingFileName for CopyFileA10004cf5 CALL dword ptr [-> KERNEL32.DLL::CopyFileA ] = 0000b486It 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.
10004cfb PUSH u_@WanaDecryptor@.exe.lnk_1000cc44 LPCWSTR lpFileName for GetFileAttributesW10004d00 CALL ESI => KERNEL32.DLL::GetFileAttributesW10004d02 CMP EAX ,-0x110004d05 JNZ LAB_10004dd910004d0b PUSH EDI10004d0c MOV ECX ,0x3610004d11 MOV ESI ,s_@echo_off_echo_SET_ow_=_WScript._1000d628 = »@echo offecho SET ow = WScript.CreateObject("WScript.Shell")> m.vbsecho SET om = ow.CreateShortc10004d16 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:
10004d9e LEA EAX => local_6cc ,[ESP + 0x8 ]10004da2 PUSH s_@WanaDecryptor@.exe_1000d5c4 = "@WanaDecryptor@.exe"10004da7 PUSH EAX10004da8 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 ECX10004db9 LEA EAX => local_3e8 ,[ESP + 0x2fc ]10004dc0 PUSH EDX char * _Format for sprintf10004dc1 PUSH EAX char * _Dest for sprintf10004dc2 CALL dword ptr [-> MSVCRT.DLL::sprintf ] = 0000b74csprintf 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 isEAX,local_3e8.format: format-control string. In the code this isEDX(local_4c4).arg1:ECX,local_6ccarg2: the string@WanaDecryptor@.exe.lnkarg3: againlocal_6ccarg4: 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):
10004d0c MOV ECX ,0x3610004d11 MOV ESI ,s_@echo_off_echo_SET_ow_=_WScript._1000d628 = »@echo off10004d16 LEA EDI => local_4c4 ,[ESP + 0x210 ]10004d1d MOVSD.REP ES :EDI ,ESI => s_@echo_off_echo_SET_ow_=_WScript._1000d628 = »@echo offlocal_6cc is the result (lpBuffer) of GetCurrentDirectory 24:
10004d3a LEA ECX => local_6cc ,[ESP + 0x8 ]10004d3e PUSH ECX LPSTR lpBuffer for GetCurrentDirectoryA10004d3f PUSH 0x208 DWORD nBufferLength for GetCurrentDirectoryA10004d44 STOSB ES :EDI10004d45 CALL dword ptr [-> KERNEL32.DLL::GetCurrentDirectoryA ] = 0000b272The 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.SaveThis, plus the deletion of m.vbs, is stored in local_3e8, which is then used in the next call:
10004dc8 LEA ECX => local_3e8 ,[ESP + 0x304 ]10004dcf PUSH ECX10004dd0 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).
_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:
100010b9 MOV EAX ,dword ptr [ESP + param_1 ]100010bd PUSH ESI BOOL bInheritHandles for CreateProcessA100010be PUSH ESI LPSECURITY_ATTRIBUTES lpThreadAttributes for CreateProcessA100010bf PUSH ESI LPSECURITY_ATTRIBUTES lpProcessAttributes for CreateProcessA100010c0 PUSH EAX LPSTR lpCommandLine for CreateProcessA100010c1 PUSH ESI LPCSTR lpApplicationName for CreateProcessA100010c2 MOV dword ptr [ESP + local_54 ],ESI100010c6 MOV dword ptr [ESP + local_18 ],0x1100010ce MOV word ptr [ESP + local_14 ],SI
100010d3 CALL dword ptr [-> KERNEL32.DLL::CreateProcessA ] = 0000b1e2100010d9 TEST EAX ,EAX100010db JZ LAB_10001135100010dd MOV EAX ,dword ptr [ESP + param_2 ]100010e1 CMP EAX ,ESI100010e3 JZ LAB_10001116100010e5 MOV ECX => local_54 ,dword ptr [ESP + 0x8 ]100010e9 PUSH EAX DWORD dwMilliseconds for WaitForSingleObject100010ea PUSH ECX HANDLE hHandle for WaitForSingleObject100010eb CALL dword ptr [-> KERNEL32.DLL::WaitForSingleObject ] = 0000b1ccCreateProcessA 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 vstaskkill.exe /f /im mysqld.exetaskkill.exe /f /im sqlwriter.exetaskkill.exe /f /im sqlserver.exetaskkill.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\MsWinZonesCacheCounterMutexASince 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:
10005b45 CALL dword ptr [-> KERNEL32.DLL::GetModuleFileNameW ] = 0000b59e10005b4b MOV ESI ,dword ptr [-> MSVCRT.DLL::wcsrchr ] = 0000b7f210005b51 LEA EAX => local_214 ,[ESP + 0x10 ]... skipped lines ...10005b70 LEA EDX => local_214 ,[ESP + 0x10 ]10005b74 PUSH EDX LPCWSTR lpPathName for SetCurrentDirectoryW10005b75 CALL dword ptr [-> KERNEL32.DLL::SetCurrentDirectoryW ] = 0000b586They 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:
10001016 PUSH s_c.wnry_1000c010 char * _Filename for fopen1000101b CALL dword ptr [-> MSVCRT.DLL::fopen ] = 0000b73a... skipped lines ...1000103b CALL dword ptr [-> MSVCRT.DLL::fread ] = 0000b73210001041 JMP LAB_1000104e LAB_10001043 XREF[1]: 10001034 (j)10001043 MOV ECX ,dword ptr [ESP + param_1 ]10001047 PUSH ECX void * _Str for fwrite10001048 CALL dword ptr [-> MSVCRT.DLL::fwrite ] = 0000b728This 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:
10001321 CALL dword ptr [-> ADVAPI32.DLL::GetUserNameW ] = 0000b61010001327 LEA EDX => local_258 ,[ESP + 0x4 ]1000132b PUSH u_SYSTEM_1000c068 wchar_t * _Str2 for _wcsicmp10001330 PUSH EDX wchar_t * _Str1 for _wcsicmp10001331 CALL dword ptr [-> MSVCRT.DLL::_wcsicmp ] = 0000bb9eOtherwise 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:
10001303 LEA EDX => local_258 ,[ESP + 0x4 ]10001307 PUSH EDX10001308 PUSH u_S-1-5-18_1000c078 = u"S-1-5-18"1000130d JMP LAB_10001331If 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):
100011d4 LEA EAX => local_8 ,[ESP + 0x8 ]100011da PUSH 0x8 DWORD DesiredAccess for OpenProcessToken100011dc MOV dword ptr [ESP + local_c ],0x0100011e4 CALL dword ptr [-> KERNEL32.DLL::GetCurrentProcess ] = 0000b252100011eb CALL dword ptr [-> ADVAPI32.DLL::OpenProcessToken ] = 0000b5fc100011f1 TEST EAX ,EAX100011f3 JNZ LAB_100011fb100011fa RET10001209 LEA ECX => local_c ,[ESP + 0x8 ]10001214 CALL EDI => ADVAPI32.DLL::GetTokenInformation10001216 TEST EAX ,EAX10001218 JNZ LAB_1000122d1000121a CALL dword ptr [-> KERNEL32.DLL::GetLastError ] = 0000b24210001220 CMP EAX ,0x7a10001223 JZ LAB_1000122d10001226 XOR EAX ,EAX1000122c RET10001234 CALL dword ptr [-> KERNEL32.DLL::GlobalAlloc ] = 0000b23410001244 LEA EDX => local_c ,[ESP + 0x8 ]1000124e CALL EDI => ADVAPI32.DLL::GetTokenInformation10001250 TEST EAX ,EAX10001252 JNZ LAB_1000125a10001259 RET1000125f CALL dword ptr [-> KERNEL32.DLL::LoadLibraryA ] = 0000b22410001265 TEST EAX ,EAX10001267 JNZ LAB_1000126f1000126e RET10001275 CALL dword ptr [-> KERNEL32.DLL::GetProcAddress ] = 0000b2121000127b TEST EAX ,EAX1000127d JNZ LAB_1000128510001284 RET LAB_10001285 XREF[1]: 1000127d (j)10001285 MOV dword ptr [ESP + local_4 ],0x01000128d MOV ECX ,dword ptr [ESI ]1000128f LEA EDX => local_4 ,[ESP + 0x10 ]10001295 CALL EAX10001297 TEST EAX ,EAX10001299 JNZ LAB_100012a1100012a0 RETThis function is renamed get_sid.
Next, FUN_10003410 is called. It dynamically loads all the file operations available in kernel32.dll:
10003458 MOV [DAT_1000d91c ],EAX1000345d CALL EDI => KERNEL32.DLL::GetProcAddress1000345f PUSH s_ReadFile_1000cf0c LPCSTR lpProcName for GetProcAddressThis 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:
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:
10005bb0 PUSH s_%08X.res_1000d8c0 char * _Format for sprintf10005bb5 PUSH _Dest_1000dcf0 char * _Dest for sprintf10005bba CALL ESI => MSVCRT.DLL::sprintfThese 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:
10004604 PUSH s_Global\MsWinZonesCacheCounterMut_1000d520 LPCSTR lpName for OpenMutexA10004609 PUSH 0x1 BOOL bInheritHandle for OpenMutexA1000460b PUSH 0x100000 DWORD dwDesiredAccess for OpenMutexA10004610 CALL dword ptr [-> KERNEL32.DLL::OpenMutexA ] = 0000b464... skipping lines ...10004634 PUSH s_Global\MsWinZonesCacheCounterMut_1000d4fc = "Global\\MsWinZonesCacheCounterMutexA"10004639 PUSH _Format_1000d4f4 char * _Format for sprintf1000463e PUSH ECX char * _Dest for sprintf1000463f CALL dword ptr [-> MSVCRT.DLL::sprintf ] = 0000b74c10004645 ADD ESP ,0x1010004648 LEA EDX => local_64 ,[ESP + 0x4 ]1000464c PUSH EDX LPCSTR lpName for CreateMutexA1000464d PUSH 0x1 BOOL bInitialOwner for CreateMutexA1000464f PUSH 0x0 LPSECURITY_ATTRIBUTES lpMutexAttributes for CreateMutexA10004651 CALL dword ptr [-> KERNEL32.DLL::CreateMutexA ] = 0000b454This function is renamed manage_mutex.
Once the mutex is created, FUN_10004500 is called. This function contains three subfunctions:
FUN_10003a10→ this only callsInitializeCriticalSection33 and is renamedinit_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 callsDeleteCriticalSection34 and is renameddelete_critical_section.FUN_10003d10→ this function is called beforedelete_critical_section. It is called with three parameters: the handle to the critical section object, the_Dest_FilePath.pkystring and the strings_%08X.dky_1000d4e8, which is passed vialocal_40:
1000451c LEA ECX => local_40 ,[ESP + 0x28 ]10004520 PUSH ESI10004521 PUSH EAX10004522 PUSH s_%08X.dky_1000d4e8 char * _Format for sprintf10004527 PUSH ECX char * _Dest for sprintf10004528 CALL dword ptr [-> MSVCRT.DLL::sprintf ] = 0000b74cThis 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:
LAB_10003a87 XREF[1]: 10003aab (j)10003a87 MOV EAX ,ESI10003a89 PUSH 0xf000000010003a8e NEG EAX10003a90 SBB EAX ,EAX10003a92 PUSH 0x1810003a94 AND EAX ,s_Microsoft_Enhanced_RSA_and_AES_C_1000d168 = "Microsoft Enhanced RSA and AES Cryptographic Provider"10003a99 PUSH EAX10003a9a PUSH 0x010003a9c PUSH EDI10003a9d CALL dword ptr [DAT_1000d93c ]10003aa3 TEST EAX ,EAX10003aa5 JNZ LAB_10003ab010003aa7 INC ESI10003aa8 CMP ESI ,0x210003aab JL LAB_10003a8710003aad POP EDI10003aae POP ESIIt calls a DAT_ variable, which indicates a function initialized elsewhere. Double‑clicking on it takes you to its reference:
DAT_1000d93c XREF[4]: FUN_10003a80:10003a9d (R) , FUN_10004440:10004440 (R) , FUN_10004440:1000447b (W) , FUN_10004440:100044b6 (R)1000d93c undefined4 00000000hNote that in function FUN_10004440 it is defined. Double-clicking on the function name shows a series of defined variables:
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:
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 ,EAX10003e55 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 ,EAX10003e78 JNZ LAB_10003e82 LAB_10003e82 XREF[1]: 10003e78 (j)10003e82 LEA EDI => local_228 ,[EBP + 0xfffffddc ]10003e88 OR this ,0xffffffff10003e8b XOR EAX ,EAX10003e8d SCASB.REPNE ES :EDI10003e8f NOT this10003e91 DEC this10003e93 LEA EAX => local_228 ,[EBP + 0xfffffddc ]10003e9a LEA this => local_21c ,[EBP + 0xfffffde8 ]10003ea1 CALL dword ptr [-> MSVCRT.DLL::strncmp ] = 0000b81a10003eaa TEST EAX ,EAX10003eac JNZ LAB_10003ed410003eb0 LEA EDX => local_14 ,[EBP + -0x10 ]10003eb4 CALL MSVCRT.DLL::_local_unwind2 undefined _local_unwind2()10003ebc MOV EAX ,0x110003ec4 MOV dword ptr FS :[0x0 ]=> ExceptionList ,this = 0000000010003ed1 RET 0x8 LAB_10003ed4 XREF[1]: 10003eac (j)10003ed4 MOV dword ptr [EBP + local_8 ],0xffffffff10003edb 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_10003c00which has been renamed todestroy_key_and_load_key_pkyFUN_10004350which has been renamed tocall_CryptGenKeyFUN_10004040since it callsCryptExportKeyand file operations likeCreateFileandWriteFileI will rename itcrypt_and_write_fileFUN_10003c40which callsFUN_10004170. Looking at it, it shows the usual math-like operations, hence it is renamedmath_opsandFUN_10003c40is renamedmath_and_write_filesince it callsCreateFileandWriteFile. 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:
100046e1 PUSH _FilePath.res LPCSTR lpFileName for CreateFileA100046e6 CALL dword ptr [-> KERNEL32.DLL::CreateFileA ] = 0000b416... skipped lines ...10004712 CALL dword ptr [-> KERNEL32.DLL::ReadFile ] = 0000b424The function is renamed read_filepath_res. The file is then deleted outside the function:
10005c61 PUSH _FilePath.res LPCSTR lpFileName for DeleteFileA10005c66 CALL dword ptr [-> KERNEL32.DLL::DeleteFileA ] = 0000b578And 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):
10005c7c PUSH lpBuffer_1000dc68 = 0000000010005c81 MOV ECX ,ESI10005c83 MOV dword ptr [DAT_1000dc70 ],EBX10005c89 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.
10005c9e MOV ESI ,dword ptr [-> KERNEL32.DLL::CreateThread ] = 0000b2b610005ca4 PUSH EBX LPDWORD lpThreadId for CreateThread10005ca5 PUSH EBX DWORD dwCreationFlags for CreateThread10005ca6 PUSH EBX LPVOID lpParameter for CreateThread10005ca7 PUSH lpStartAddress_10004790 LPTHREAD_START_ROUTINE lpStartAddress for CreateThread10005cac PUSH EBX SIZE_T dwStackSize for CreateThread10005cad PUSH EBX LPSECURITY_ATTRIBUTES lpThreadAttributes for CreateThread10005cae CALL ESI => KERNEL32.DLL::CreateThread10005cb0 MOV EBP ,dword ptr [-> KERNEL32.DLL::CloseHandle ] = 0000b19410005cb6 CMP EAX ,EBX10005cb8 JZ LAB_10005cbd10005cba PUSH EAX HANDLE hObject for CloseHandle10005cbb CALL EBP => KERNEL32.DLL::CloseHandle LAB_10005cbd XREF[1]: 10005cb8 (j)10005cbd MOV EDI ,dword ptr [-> KERNEL32.DLL::Sleep ] = 0000b3ce10005cc3 PUSH 0x64 DWORD dwMilliseconds for Sleep10005cc5 CALL EDI => KERNEL32.DLL::SleepAfter 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_10004790this address seems to contain assembly code in the form of hexadecimal. And this is just a guess. These are the first few bytes and53,56and57are the representation ofPUSH EBX,PUSH ESIandPUSH EDI.
10004795 ?? 53h S10004796 ?? 56h V10004797 ?? 57h W10004798 ?? 85hSo 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:
00000000 53 PUSH EBX00000001 56 PUSH ESI00000002 57 PUSH EDI00000003 85C0 TEST EAX,EAX00000005 753E JNE 0000004500000007 8B1D60710010 MOV EBX,DWORD PTR [1000716D]0000000D 8B3D70700010 MOV EDI,DWORD PTR [10007083]00000013 6A00 PUSH 0000000000000015 FFD3 CALL EBX00000017 83C404 ADD ESP,000000040000001A A3DCDC0010 MOV DWORD PTR [1000DCDC],EAX0000001F E877FFFFFF CALL 00000-6500000024 33F6 XOR ESI,ESI00000026 A190DD0010 MOV EAX,DWORD PTR [1000DD90]0000002B 85C0 TEST EAX,EAX0000002D 7516 JNE 000000450000002F 68E8030000 PUSH 000003E800000034 FFD7 CALL EDI00000036 46 INC ESI00000037 83FE19 CMP ESI,000000190000003A 7CEA JL 000000260000003C A190DD0010 MOV EAX,DWORD PTR [1000DD90]00000041 85C0 TEST EAX,EAX00000043 74CE JE 0000001300000045 6A00 PUSH 0000000000000047 FF1564700010 CALL DWORD PTR [100070B1]0000004D 5F POP EDI0000004E 5E POP ESI0000004F 5B POP EBX00000050 90 NOP00000051 90 NOP00000052 90 NOP00000053 90 NOP00000054 90 NOPFor 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:
1000716Dpoints toMSVCRT.dll:wcscat10007083points tokernel32.dll:WriteFile1000DCDCpoints to an empty space, which makes sense, since in this case the address is not called but written into:MOV DWORD PTR [1000DCDC],EAX1000DD90points to an empty space10007083points tokernel32.dll:DeleteFileW
So the address lpStartAddress_10004790 is renamed to write_and_delete_file.
lpStartAddress_100045c0→ callssafe_load_crypt_key; the address is renamed asaddr_safe_load_crypt_key.lpStartAddress_10005730→ this seems like an interesting function, since it starts by callingGetLogicalDrivesto then start a new thread for each loop. This function is analyzed shortly.lpStartAddress_10005300→ callsrun_batwithtaskdl.exeas argument, renamed asaddr_run_taskdl.exelpStartAddress_10004990→ this looks promising in terms of functionalities as well, it mentionsc_wnry_opsandtasksche.exe.
lpStartAddress_10005730 after calling GetLogicalDrives then starts a loop on DAT_1000dd8c:
10005765 MOV EAX ,[DAT_1000dd8c ]1000576a TEST EAX ,EAX1000576c JNZ LAB_100057afWithin 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.
100015b8 XOR EDI ,EDI100015ba LEA param_1 ,[ESI + 0x2c ]100015bd MOV dword ptr [ESP + local_4 ],EDI100015c1 CALL init_critical_section undefined4 * init_critical_section(undefined4 * param_1)100015c6 LEA param_1 ,[ESI + 0x54 ]100015c9 MOV byte ptr [ESP + local_4 ],0x1100015ce CALL FUN_10005d80 undefined FUN_10005d80(undefined4 * param_1)100015d3 MOV AL ,byte ptr [ESP + local_11 ]100015d7 MOV dword ptr [ESI + 0x4c8 ],EDI100015dd MOV dword ptr [ESI + 0x4cc ],EDI100015e3 MOV dword ptr [ESI + 0x4d0 ],EDI100015e9 MOV dword ptr [ESI + 0x4d4 ],EDIFor 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:
100056a4 PUSH DAT_1000dd8c100056a9 PUSH LAB_10005340100056ae PUSH _Dest_FilePath.pky = 00000000100056b3 LEA ECX => local_930 ,[ESP + 0xc ]100056b7 MOV dword ptr [ESP + local_4 ],0x0100056c2 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:
10002ab1 PUSH u_.WNCRYT_1000cbc8 = u".WNCRYT"10002ab6 LEA EDX ,[ESI + 0x504 ]10002abc PUSH EAX10002abd PUSH EDX wchar_t * _Format for swprintf10002abe LEA param_1 ,[EAX + 0x1 ]10002ac1 PUSH u_%s\%d%s_1000cbb8 size_t _Count for swprintf10002ac6 PUSH EBP wchar_t * _String for swprintf10002ac7 MOV dword ptr [ESI + 0x914 ],param_110002acd CALL dword ptr [-> MSVCRT.DLL::swprintf ] = 0000b7c010002ad3 ADD ESP ,0x1410002ad6 PUSH 0x110002ad8 PUSH EBP10002ad9 PUSH EDI10002ada 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.
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 = 0000b99210002a5a MOV EAX ,dword ptr [EAX ]10002a5c ADD EAX ,0x810002a5f MOV EAX ,dword ptr [EAX + 0x4 ]10002a62 CMP EAX ,EBX10002a64 JZ LAB_10002a6810002a66 MOV EDI ,EAX
10002a68 PUSH EBP wchar_t * _Str for wcslen10002a69 CALL dword ptr [-> MSVCRT.DLL::wcslen ] = 0000b78210002a6f ADD ESP ,0x410002a72 TEST EAX ,EAX10002a74 JBE LAB_10002ae410002a76 PUSH 0x1This 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:
10005db0 MOV dword ptr [param_1 ],PTR_FUN_1000acbc = 10005d90The function pointed to is a function not very clear, since it contains:
1000acbc addr FUN_10005d901000acc0 addr DAT_1000acf8and the first function contains:
FUN_10005d90 XREF[1]: 1000acbc (*)10005d90 PUSH ESI10005d91 MOV ESI ,this10005d93 CALL register_func_in_param_1 undefined register_func_in_param_1(undefined4 * param_1)10005d98 TEST byte ptr [ESP + param_1 ],0x110005d9d JZ LAB_10005da810005d9f PUSH ESI void * param_1 for operator_delete10005da0 CALL MSVCRT.DLL::operator_delete void operator_delete(void * param_1)10005da5 ADD ESP ,0x4which 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
LAB_100055f2 XREF[1]: 10005575 (j)100055f2 MOV ESI ,dword ptr [-> KERNEL32.DLL::GetDriveTypeW ] = 0000b50e100055f8 LEA EDX => local_228 ,[ESP + 0x10 ]100055fc PUSH EDX LPCWSTR lpRootPathName for GetDriveTypeW100055fd CALL ESI => KERNEL32.DLL::GetDriveTypeW100055ff CMP EAX ,0x510005602 JZ LAB_10005668and 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:
10005067 MOV ESI ,dword ptr [ESP + param_2 ]1000506e PUSH EDI1000506f PUSH 0x104 UINT uSize for GetWindowsDirectoryW10005074 PUSH ESI LPWSTR lpBuffer for GetWindowsDirectoryW10005075 CALL dword ptr [-> KERNEL32.DLL::GetWindowsDirectoryW ] = 0000b4b6and 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:
LAB_100050d0 XREF[1]: 1000508c (j)100050d0 PUSH u_$RECYCLE_1000d778 = u"$RECYCLE"100050d5 PUSH EDI wchar_t * _Format for swprintf100050d6 PUSH u_%C:\%s_1000d768 size_t _Count for swprintf100050db PUSH ESI wchar_t * _String for swprintf100050dc CALL dword ptr [-> MSVCRT.DLL::swprintf ] = 0000b7c0100050e2 ADD ESP ,0x10100050e5 PUSH 0x0 LPSECURITY_ATTRIBUTES lpSecurityAttributes for CreateDirectoryW100050e7 PUSH ESI LPCWSTR lpPathName for CreateDirectoryW100050e8 CALL dword ptr [-> KERNEL32.DLL::CreateDirectoryW ] = 0000b492100050ee PUSH s_$RECYCLE_1000d75c = "$RECYCLE"100050f3 PUSH EDI100050f4 LEA EDX => local_400 ,[ESP + 0x10 ]100050f8 PUSH s_attrib_+h_+s_%C:\%s_1000d748 char * _Format for sprintf100050fd PUSH EDX char * _Dest for sprintf100050fe CALL dword ptr [-> MSVCRT.DLL::sprintf ] = 0000b74c10005104 PUSH 0x010005106 LEA EAX => local_400 ,[ESP + 0x1c ]1000510a PUSH 0x01000510c PUSH EAX1000510d 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.
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 ESI10001915 MOV ESI ,this10001917 PUSH EDI10001918 PUSH EAX wchar_t * _Source for wcscpy10001919 LEA EDI ,[ESI + 0x504 ]1000191f PUSH EDI wchar_t * _Dest for wcscpy10001920 CALL dword ptr [-> MSVCRT.DLL::wcscpy ] = 0000b76e10001926 MOV EAX ,dword ptr [ESI + 0x914 ]1000192c ADD ESP ,0x81000192f ADD ESI ,0x70c10001935 LEA this ,[EAX + 0x1 ]10001938 PUSH u_.WNCRYT_1000cbc8 = u".WNCRYT"1000193d MOV dword ptr [ESI + 0x208 ],this10001943 PUSH EAX10001944 PUSH EDI wchar_t * _Format for swprintf10001945 PUSH u_%s\%d%s_1000cbb8 size_t _Count for swprintf1000194a PUSH ESI wchar_t * _String for swprintf1000194b CALL dword ptr [-> MSVCRT.DLL::swprintf ] = 0000b7c0However, 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:
1000180d ADD ESI ,0x70c10001813 PUSH ESI wchar_t * _Str for wcslen10001814 CALL dword ptr [-> MSVCRT.DLL::wcslen ] = 0000b7821000181a ADD ESP ,0x41000181d TEST EAX ,EAX1000181f JBE LAB_1000182810001821 PUSH ESI10001822 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).
10002391 LEA EAX => local_a10 ,[ESP + 0x50 ]10002395 LEA this => local_7c0 ,[ESP + 0x2a0 ]1000239e CALL dword ptr [-> KERNEL32.DLL::FindFirstFileW ] = 0000b37e100023a6 CMP EDI ,-0x1100023ad JNZ LAB_10002413100023b3 LEA EDX => local_a34 ,[ESP + 0x2c ]100023b8 MOV byte ptr [ESP + local_4 ],0x0100023c0 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 ],ESI100023e2 MOV dword ptr [ESP + local_a44 ],ESI100023e6 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 ,EAXIf 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
LAB_10002413 XREF[1]: 100023ad (j)10002413 PUSH EBX10002414 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@.bmpand also two earlier_Str2_1000ccb4/_Str2_1000ccacnames (likely ”.” and ”..” or other sentinel names; the control flow suggests “if name equals X, skip” repeated twice).
LAB_10002521 XREF[1]: 1000248f (j)10002521 CMP dword ptr [ESP + local_a28 ],ESI10002525 JZ LAB_1000262a1000252b LEA this => local_9e4 ,[ESP + 0x7c ]1000252f PUSH u_@Please_Read_Me@.txt_1000cc74 wchar_t * _Str2 for wcscmp10002534 PUSH this wchar_t * _Str1 for wcscmp10002535 CALL EBX => MSVCRT.DLL::wcscmp10002537 ADD ESP ,0x81000253a TEST EAX ,EAX1000253c JZ LAB_1000262a10002542 LEA EDX => local_9e4 ,[ESP + 0x7c ]10002546 PUSH u_@WanaDecryptor@.exe.lnk_1000cc44 wchar_t * _Str2 for wcscmp1000254b PUSH EDX wchar_t * _Str1 for wcscmp1000254c CALL EBX => MSVCRT.DLL::wcscmp1000254e ADD ESP ,0x810002551 TEST EAX ,EAX10002553 JZ LAB_1000262a10002559 LEA EAX => local_9e4 ,[ESP + 0x7c ]1000255d PUSH u_@WanaDecryptor@.bmp_1000cc1c wchar_t * _Str2 for wcscmp10002562 PUSH EAX wchar_t * _Str1 for wcscmp10002563 CALL EBX => MSVCRT.DLL::wcscmp10002565 ADD ESP ,0x810002568 TEST EAX ,EAX1000256a JZ LAB_1000262a10002570 MOV this ,0x13810002575 XOR EAX ,EAX10002577 LEA EDI => local_4ee ,[ESP + 0x572 ]1000257e MOV word ptr [ESP + local_4f0 ],SIWhen 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)
10002473 LEA this => local_7c0 ,[ESP + 0x2a8 ]1000247a PUSH u_%s\%s_1000cca0 size_t _Count for swprintf1000247f PUSH this wchar_t * _String for swprintf10002480 CALL dword ptr [-> MSVCRT.DLL::swprintf ] = 0000b7c010002486 MOV AL ,byte ptr [ESP + local_a10 ]1000248a ADD ESP ,0x101000248d TEST AL ,0x10If 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.
10002591 CALL FUN_10002d60 int FUN_10002d60(wchar_t * param_1)10002596 CMP EAX ,0x610002599 MOV dword ptr [ESP + local_10 ],EAX100025a0 JZ LAB_1000262a100025a6 CMP EAX ,0x1100025a9 JZ LAB_1000262a100025ab CMP EAX ,ESI100025ad JNZ LAB_100025bf100025af CMP dword ptr [ESP + local_9f4 ],ESI100025b3 JA LAB_100025bf100025b5 CMP dword ptr [ESP + local_9f0 ],0xc800000100025bd JC LAB_1000262aThe 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 tounknown_nothing_crazy.FUN_100037c0: same as above, renamed tounknown_nothing_crazy2.FUN_10002f70: it callsGetTempFileNameW42 which creates a temporary file - renamed ascall_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:
_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:
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.
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 ?? 00h1000c0f9 ?? 00h1000c0fa ?? 00h1000c0fb ?? 00hIn 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\TempThis folder protects against ransomware. Modifying it will reduce protectionTemporary Internet FilesContent.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.
Looking at the outgoing calls from that 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.