Elastic Security Labs

Investigación de una firma Authenticode misteriosamente malformada

Descubriendo la heurística oculta detrás de las firmas authenticode mal formadas

16 minutos de lecturaOperaciones de seguridad
Investigación de una firma Authenticode misteriosamente malformada

Introducción

Elastic Security Labs encontró recientemente un problema de validación de firma con uno de nuestros binarios de Windows. El ejecutable se firmó empleando signtool.exe como parte de nuestro proceso de integración continua (CI) estándar, pero en esta ocasión, el archivo de salida no pasó la validación de la firma con el siguiente mensaje de error:

La firma digital del objeto está mal formada. Para obtener detalles técnicos, consulte el boletín de seguridad MS13-098.

La documentación de MS13-098 es vaga, pero describe una posible vulnerabilidad relacionada con firmas Authenticode malformadas. Nada obvio cambió de nuestra parte que pudiera explicar este nuevo error, por lo que necesitábamos investigar la causa y resolver el problema.

Si bien identificamos que este problema estaba afectando a uno de nuestros binarios de Windows firmados, podría afectar a cualquier binario. Publicamos esta investigación como referencia para cualquier otra persona que pueda enfrentar al mismo problema en el futuro.

Diagnóstico

Para investigar más a fondo, creamos un programa de prueba básico que llamó a la función WinVerifyTrust de Windows contra el ejecutable problemático para validar manualmente la firma. Esto reveló que estaba fallando con el código de error TRUST_E_MALFORMED_SIGNATURE.

WinVerifyTrust es una función compleja, pero luego de anexar un depurador, descubrimos que el código de error se estaba estableciendo en el siguiente punto:

dwReserved1 = psSipSubjectInfo->dwReserved1;
if(!dwReserved1)
    goto LABEL_58;
v40 = I_GetRelaxedMarkerCheckFlags(a1, v22, (unsigned int *)&pvData);
if(v40 < 0)
    break;
if(!pvData)
    v42 = 0x80096011;    // TRUST_E_MALFORMED_SIGNATURE

Como se muestra arriba, si psSipSubjectInfo->dwReserved1 no es 0, el código llama I_GetRelaxedMarkerCheckFlags. Si esta función no devuelve datos, el código establece el error TRUST_E_MALFORMED_SIGNATURE y sale.

Al recorrer el código con nuestro binario problemático, vimos que dwReserved1 de hecho estaba configurado en 1. Al ejecutar la misma prueba contra un binario firmado correctamente, este valor siempre fue 0, lo que omite la llamada a I_GetRelaxedMarkerCheckFlags.

Al analizar I_GetRelaxedMarkerCheckFlags, vimos que simplemente verifica la presencia de un atributo específico: 1.3.6.1.4.1.311.2.6.1. Una búsqueda rápida en el Internet no arrojó muchos resultados, salvo el hecho de que este identificador de objeto (OID) está etiquetado como SpcRelaxedPEMarkerCheck.

__int64 __fastcall I_GetRelaxedMarkerCheckFlags(struct _CRYPT_PROVIDER_DATA *a1, DWORD a2, unsigned int *a3)
{
    unsigned int v4; // ebx
    CRYPT_PROVIDER_SGNR *ProvSignerFromChain; // rax
    PCRYPT_ATTRIBUTE Attribute; // rax
    signed int LastError; // eax
    DWORD pcbStructInfo; // [rsp+60h] [rbp+18h] BYREF

    pcbStructInfo = 4;
    v4 = 0;
    *a3 = 0;
    ProvSignerFromChain = WTHelperGetProvSignerFromChain(a1, a2, 0, 0);
    if(ProvSignerFromChain)
    {
        Attribute = CertFindAttribute(
            "1.3.6.1.4.1.311.2.6.1",
            ProvSignerFromChain->psSigner->AuthAttrs.cAttr,
            ProvSignerFromChain->psSigner->AuthAttrs.rgAttr);
        if(Attribute)
        {
            if(!CryptDecodeObject(
                a1->dwEncoding,
                (LPCSTR)0x1B,
                Attribute->rgValue->pbData,
                Attribute->rgValue->cbData,
                0,
                a3,
                &pcbStructInfo))
            {
                return HRESULT_FROM_WIN32(GetLastError());
            }
        }
    }

    return v4;
}

Nuestro binario no tenía este atributo, lo que provocó que la función no devolviera datos y activara el error. Los nombres de las funciones nos recordaron un parámetro opcional que vimos previamente en signtool.exe:

/rmc - Especifica la firma de un archivo PE con la semántica de verificación de marcador relajada. La bandera se ignora para los archivos que no son PE. Durante la verificación, ciertas secciones autenticadas de la firma omitirán la verificación de marcadores PE no válidos. Esta opción solo debe emplear luego de una cuidadosa consideración y revisión de los detalles del caso MS12-024 de MSRC para garantizar que no se introduzcan vulnerabilidades.

Con base en nuestro análisis, sospechamos que al volver a firmar el ejecutable con el indicador “verificación de marcador relajado” (/rmc), y como se esperaba, la firma ahora era válida.

Análisis de la causa raíz

Si bien la solución alternativa anterior resolvió nuestro problema inmediato, claramente no fue la causa raíz. Necesitábamos investigar más a fondo para comprender por qué se configuró el indicador interno dwReserved1 en primer lugar.

Este campo es parte de la estructura SIP_SUBJECTINFO , que está documentada en MSDN , pero desafortunadamente, no ayudó mucho en este caso:

Para encontrar dónde se estaba configurando este campo, trabajamos hacia atrás e identificamos un punto donde dwReserved1 todavía era 0 , es decir, antes de que se configuró la bandera. Colocamos un punto de interrupción de hardware (en escritura) en el campo dwReserved1 y reanudamos la ejecución. Se alcanzó el punto de interrupción en la función SIPObjectPE_::GetMessageFromFile :

__int64 __fastcall SIPObjectPE_::GetMessageFromFile(
    SIPObjectPE_ *this,
    struct SIP_SUBJECTINFO_ *a2,
    struct _WIN_CERTIFICATE *a3,
    unsigned int a4,
    unsigned int *a5)
{
    __int64 v5; // rcx
    __int64 result; // rax
    DWORD v8; // [rsp+40h] [rbp+8h] BYREF

    v5 = *((_QWORD*)this + 1);
    v8 = 0;
    result = ImageGetCertificateDataEx(v5, a4, a3, a5, &v8);
    if((_DWORD)result)
        a2->dwReserved1 = v8;

    return result;
}

Esta función llama a la API ImageGetCertificateDataEx que es exportada por imagehlp.dll. El valor devuelto por el quinto parámetro de esta función se almacena en dwReserved1. Este valor determina en última instancia si el PE se considera "malformado" en la forma que estuvimos observando.

Lamentablemente, ImageGetCertificateDataEx no está documentado en MSDN. Sin embargo, está documentada una variante anterior, ImageGetCertificateData:

BOOL IMAGEAPI ImageGetCertificateData(
  [in]      HANDLE            FileHandle,
  [in]      DWORD             CertificateIndex,
  [out]     LPWIN_CERTIFICATE Certificate,
  [in, out] PDWORD            RequiredLength
);

Esta función extrae el contenido del directorio IMAGE_DIRECTORY_ENTRY_SECURITY de los encabezados PE. El análisis manual de la función ImageGetCertificateDataEx mostró que los primeros cuatro parámetros coinciden con los de ImageGetCertificateData, pero con un parámetro de salida adicional al final.

Escribimos un programa de prueba simple que nos permite llamar a esta función y realizar comprobaciones contra el quinto parámetro desconocido:

#include <stdio.h>
#include <windows.h>
#include <imagehlp.h>

int main()
{
    HANDLE hFile = NULL;
    DWORD dwCertLength = 0;
    WIN_CERTIFICATE *pCertData = NULL;
    DWORD dwUnknown = 0;
    BOOL (WINAPI *pImageGetCertificateDataEx)(HANDLE FileHandle, DWORD CertificateIndex, LPWIN_CERTIFICATE Certificate, PDWORD RequiredLength, DWORD *pdwUnknown);

    // open target executable
    hFile = CreateFileA("C:\\users\\matthew\\sample-executable.exe", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
    if(hFile == INVALID_HANDLE_VALUE)
    {
        printf("Failed to open input file\n");
        return 1;
    }

    // locate ImageGetCertificateDataEx export in imagehlp.dll
    pImageGetCertificateDataEx = (BOOL(WINAPI*)(HANDLE,DWORD,LPWIN_CERTIFICATE,PDWORD,DWORD*))GetProcAddress(LoadLibraryA("imagehlp.dll"), "ImageGetCertificateDataEx");
    if(pImageGetCertificateDataEx == NULL)
    {
        printf("Failed to locate ImageGetCertificateDataEx\n");
        return 1;
    }

    // get required length
    dwCertLength = 0;
    if(pImageGetCertificateDataEx(hFile, 0, NULL, &dwCertLength, &dwUnknown) == 0)
    {
        if(GetLastError() != ERROR_INSUFFICIENT_BUFFER)
        {
            printf("ImageGetCertificateDataEx error (1)\n");
            return 1;
        }
    }

    // allocate data
    printf("Allocating %u bytes for certificate...\n", dwCertLength);
    pCertData = (WIN_CERTIFICATE*)malloc(dwCertLength);
    if(pCertData == NULL)
    {
        printf("Failed to allocate memory\n");
        return 1;
    }

    // read certificate data and dwUnknown flag
    if(pImageGetCertificateDataEx(hFile, 0, pCertData, &dwCertLength, &dwUnknown) == 0)
    {
        printf("ImageGetCertificateDataEx error (2)\n");
        return 1;
    }

    printf("Finished - dwUnknown: %u\n", dwUnknown);

    return 0;
}

Al ejecutar esto contra una variedad de ejecutables se confirmaron nuestras expectativas: el valor de retorno desconocido fue 1 para nuestro ejecutable “roto” y 0 para los binarios firmados correctamente. Esto confirmó que el problema se originó en algún lugar dentro de la función ImageGetCertificateDataEx .

Un análisis más detallado de esta función reveló que la bandera desconocida está siendo establecida por otra función interna: IsBufferCleanOfInvalidMarkers.

...
if(!IsBufferCleanOfInvalidMarkers(v25, v15, pdwUnknown))
{
    LastError = GetLastError();
    if(!pdwUnknown)
        goto LABEL_34;
}
...

Luego de limpiar la función IsBufferCleanOfInvalidMarkers , observamos lo siguiente:

DWORD IsBufferCleanOfInvalidMarkers(BYTE *pData, DWORD dwLength, DWORD *pdwInvalidMarkerFound)
{
    if(!_InterlockedCompareExchange64(&global_InvalidMarkerList, 0, 0))
        LoadInvalidMarkers();

    if(!RabinKarpFindPatternInBuffer(pData, dwLength, pdwInvalidMarkerFound))
        return 1;

    SetLastError(0x80096011); // TRUST_E_MALFORMED_SIGNATURE

    return 0;
}

Esta función carga una lista global de "marcadores inválidos" usando LoadInvalidMarkers, si aún no están cargados. imagehlp.dll contiene una lista predeterminada codificada de marcadores, pero también verifica el registro en busca de una lista definida por el usuario en la siguiente ruta:

HKEY_LOCAL_MACHINE\Software\Microsoft\Cryptography\Wintrust\Config\PECertInvalidMarkers

Este valor de registro no parece existir de forma predeterminada.

Luego, la función realiza una búsqueda en todos los datos de la firma PE, buscando cualquiera de estos marcadores. Si se encuentra una coincidencia, pdwInvalidMarkerFound se establece en 1, que se asigna directamente al valor psSipSubjectInfo->dwReserved1 mencionado anteriormente.

Descartando los marcadores no válidos

Los marcadores se almacenan en una estructura no documentada dentro de imagehlp.dll. Luego de realizar ingeniería inversa de la función RabinKarpFindPatternInBuffer mencionada anteriormente, escribimos una pequeña herramienta para volcar la lista completa de marcadores:

#include <stdio.h>
#include <windows.h>

int main()
{
    HMODULE hModule = LoadLibraryA("imagehlp.dll");

    // hardcoded address - imagehlp.dll version:
    // 509ef25f9bac59ebf1c19ec141cb882e5c1a8cb61ac74a10a9f2bd43ed1f0585
    BYTE *pInvalidMarkerData = (BYTE*)hModule + 0xC4D8;

    BYTE *pEntryList = (BYTE*)*(DWORD64*)(pInvalidMarkerData + 20);
    DWORD dwEntryCount = *(DWORD*)pInvalidMarkerData;
    for(DWORD i = 0; i < dwEntryCount; i++)
    {
        BYTE *pCurrEntry = pEntryList + (i * 18);
        BYTE bLength = *(BYTE*)(pCurrEntry + 9);
        BYTE *pString = (BYTE*)*(DWORD64*)(pCurrEntry + 10);
        for(DWORD ii = 0; ii < bLength; ii++)
        {
            if(isprint(pString[ii]))
            {
                // printable character
                printf("%c", pString[ii]);
            }
            else
            {
                // non-printable character
                printf("\\x%02X", pString[ii]);
            }
        }
        printf("\n");
    }

    return 0;
}

Esto produjo los siguientes resultados:

PK\x01\x02
PK\x05\x06
PK\x03\x04
PK\x07\x08
Rar!\x1A\x07\x00
z\xBC\xAF'\x1C
**ACE**
!<arch>\x0A
MSCF\x00\x00\x00\x00
\xEF\xBE\xAD\xDENull
Initializing Wise Installation Wizard
zlb\x1A
KGB_arch
KGB2\x00
KGB2\x01
ENC\x00
disk%i.pak
>-\x1C\x0BxV4\x12
ISc(
Smart Install Maker
\xAE\x01NanoZip
;!@Install@
EGGA
ArC\x01
StuffIt!
-sqx-
PK\x09\x0A
"\x0B\x01\x0B
-lh0-
-lh1-
-lh2-
-lh3-
-lh4-
-lh5-
-lh6-
-lh7-
-lh8-
-lh9-
-lha-
-lhb-
-lhc-
-lhd-
-lhe-
-lzs-
-lz2-
-lz3-
-lz4-
-lz5-
-lz7-
-lz8-
<#$@@$#>

Como era de esperar, esta parece ser una lista de valores mágicos relacionados con instaladores antiguos y formatos de archivos comprimidos. Esto coincide con la descripción de MS13-098, que sugiere que ciertos instaladores podrían ver afectados.

Sospechamos que esto estaba relacionado con ejecutables autoextraíbles. Si un ejecutable se lee a sí mismo desde el disco y escanea sus propios datos en busca de un archivo incrustado (por ejemplo, un archivo ZIP), un atacante podría potencialmente agregar datos maliciosos a la sección de firma sin invalidar la firma, ya que los datos de la firma no pueden codificar por sí mismos. Potencialmente, esto podría provocar que el ejecutable vulnerable localice los datos maliciosos antes que los datos originales, especialmente si escanea hacia atrás desde el final del archivo.

Más tarde encontramos una antigua charla RECon de 2012 de Igor Glücksmann, que describe exactamente este escenario y parece confirmar nuestra hipótesis.

La solución de Microsoft implicó escanear el bloque de firma PE en busca de patrones de bytes conocidos que pudieran indicar este tipo de abuso.

Investigando el falso positivo

Tras una depuración más exhaustiva, descubrimos que el binario estaba marcado debido a los datos de firma que contenían el marcador EGGA de la lista anterior:

En el contexto de la lista de marcadores anterior, la firma EGGA parece estar relacionada con un valor de encabezado específico empleado por un formato de archivo llamado ALZip. Nuestro código no hace uso de este formato de archivo.

La heurística de Microsoft trató la presencia de EGGA como evidencia de que se incrustaron datos de archivo maliciosos en la firma PE. En la práctica, no ocurrió nada parecido. El propio bloque de firma incluía esos cuatro bytes como parte de los datos hash.

Colisiones como ésta son inusuales, pero el hash de página (/ph) las hace más probables. Al ampliar el tamaño del bloque de firma, el hash de página aumenta la superficie para coincidencias coincidentes y aumenta la probabilidad de activar la heurística.

El binario no contenía ninguna rutina autoextraíble, por lo que el resultado en EGGA fue un falso positivo. En ese contexto, la advertencia no tenía ninguna incidencia sobre la integridad del expediente. Esto significaba que era seguro volver a firmar el archivo con /rmc para restaurar la validación esperada.

Conclusión

Es bien sabido que se pueden incorporar datos adicionales a un archivo PE sin romper su firma agregándolos al bloque de seguridad. Incluso algunos productos de software legítimos aprovechan esto para incorporar metadatos específicos del usuario en ejecutables firmados. Sin embargo, no sabíamos que Microsoft implementó heurísticas para detectar casos maliciosos específicos de esto, a pesar de que se introdujeron en 2012.

El mensaje de error original era muy vago y no pudimos encontrar ninguna documentación o referencia en línea que ayudara a explicar el comportamiento. Incluso buscar el valor de registro asociado luego de descubrirlo (PECertInvalidMarkers) no arrojó ningún resultado.

Lo que descubrimos es que Microsoft agregó el escaneo heurístico de bloques de firmas hace más de una década para contrarrestar casos de abuso específicos. Esas heurísticas residen en una lista codificada de “marcadores no válidos”, muchos de los cuales están vinculados a instaladores y formatos de archivo obsoletos. Nuestro binario colisionó con uno de esos marcadores cuando se firmó con el hash de página habilitado, lo que creó una falla de validación sin documentación clara y sin referencias públicas a la clave de registro subyacente o la lógica de detección.

La ausencia de debates en línea sobre este modo de falla, aparte de una única publicación sin resolver de la Comunidad de desarrolladores de Visual Studio de 2018, dificultó el diagnóstico inicial. Al publicar este análisis, queremos proporcionar un punto de referencia técnica para otros que puedan enfrentar el mismo problema. En nuestro caso, resolver el problema requirió una resolución de problemas profunda que pocas personas fuera de este ámbito normalmente necesitarían ejercer. Para los equipos que automatizan la firma de código, la lección clave es integrar verificaciones de validación de firma de manera temprana y tener en cuenta que la detección de marcadores heurísticos puede generar fallas en casos extremos.

Referencias adicionales

Puedes encontrar al autor en X en @x86matthew.