Elastic Security Labs

Enquête sur une signature Authenticode mystérieusement malformée

Découverte de l'heuristique cachée derrière les signatures authenticode malformées

16 minutes de lectureInternals, Detection Engineering
Enquête sur une signature Authenticode mystérieusement malformée

Introduction

Elastic Security Labs a récemment rencontré un problème de validation de signature avec l'un de nos binaires Windows. L'exécutable a été signé à l'aide de signtool.exe dans le cadre de notre processus standard d'intégration continue (CI), mais à cette occasion, le fichier de sortie a échoué à la validation de la signature avec le message d'erreur suivant :

La signature numérique de l'objet est malformée. Pour plus de détails techniques, consultez le bulletin de sécurité MS13-098.

La documentation de MS13-098 est vague, mais elle décrit une vulnérabilité potentielle liée à des signatures Authenticode malformées. Rien d'évident n'ayant changé de notre côté pour expliquer cette nouvelle erreur, nous avons dû en rechercher la cause et résoudre le problème.

Bien que nous ayons identifié que ce problème affectait l'un de nos binaires Windows signés, il pourrait affecter n'importe quel binaire. Nous publions cette recherche à titre de référence pour tous ceux qui pourraient rencontrer le même problème à l'avenir.

Diagnostic

Pour approfondir la question, nous avons créé un programme de test de base qui appelle la fonction Windows WinVerifyTrust contre l'exécutable problématique afin de valider manuellement la signature. Cela a révélé qu'il échouait avec le code d'erreur TRUST_E_MALFORMED_SIGNATURE.

WinVerifyTrust est une fonction complexe, mais après avoir installé un débogueur, nous avons découvert que le code d'erreur était défini au point suivant :

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

Comme indiqué ci-dessus, si psSipSubjectInfo->dwReserved1 n'est pas 0, le code appelle I_GetRelaxedMarkerCheckFlags. Si cette fonction ne renvoie aucune donnée, le code indique l'erreur TRUST_E_MALFORMED_SIGNATURE et quitte le système.

En parcourant le code avec notre binaire problématique, nous avons constaté que dwReserved1 était effectivement défini sur 1. En effectuant le même test avec un binaire correctement signé, cette valeur était toujours 0, ce qui permet d'éviter l'appel à I_GetRelaxedMarkerCheckFlags.

En examinant I_GetRelaxedMarkerCheckFlags, nous avons vu qu'il vérifie simplement la présence d'un attribut spécifique : 1.3.6.1.4.1.311.2.6.1. Une rapide recherche en ligne n'a pas donné grand-chose, si ce n'est que cet identifiant d'objet (OID) est étiqueté comme 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;
}

Notre binaire ne possède pas cet attribut, ce qui fait que la fonction ne renvoie aucune donnée et déclenche l'erreur. Les noms des fonctions nous ont rappelé un paramètre optionnel que nous avions déjà vu dans signtool.exe:

/rmc - Spécifie la signature d'un fichier PE avec la sémantique de vérification de marqueur détendue. L'indicateur est ignoré pour les fichiers non PE. Lors de la vérification, certaines sections authentifiées de la signature contournent le contrôle des marqueurs PE non valides. Cette option ne doit être utilisée qu'après avoir examiné attentivement les détails du dossier MS12-024 du CSEM afin de s'assurer qu'aucune vulnérabilité n'est introduite.

Sur la base de notre analyse, nous avons soupçonné que la re-signature de l'exécutable avec le drapeau "relaxed marker check" (/rmc), et comme prévu, la signature était maintenant valide.

Analyse de la cause d’un problème

Si la solution ci-dessus a permis de résoudre notre problème immédiat, elle n'en est pas la cause première. Nous avons dû mener une enquête plus approfondie pour comprendre pourquoi l'indicateur interne dwReserved1 a été activé en premier lieu.

Ce champ fait partie de la structure SIP_SUBJECTINFO, qui est documentée sur MSDN - mais malheureusement, il n'a pas été d'une grande utilité dans ce cas :

Pour savoir où ce champ était activé, nous avons travaillé à rebours et identifié un moment où dwReserved1 était encore 0 - c'est-à-dire avant que le drapeau n'ait été activé. Nous avons placé un point d'arrêt matériel (en écriture) sur le champ dwReserved1 et repris l'exécution. Le point d'arrêt a été atteint dans la fonction 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;
}

Cette fonction appelle l'API ImageGetCertificateDataEx qui est exportée par imagehlp.dll. La valeur renvoyée par le cinquième paramètre de cette fonction est stockée dans dwReserved1. Cette valeur détermine en fin de compte si le PE est considéré comme "malformed" de la manière que nous avons observée.

Malheureusement, ImageGetCertificateDataEx n'est pas documenté sur MSDN. Cependant, une variante antérieure, ImageGetCertificateData, est documentée:

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

Cette fonction extrait le contenu du répertoire IMAGE_DIRECTORY_ENTRY_SECURITY des en-têtes PE. L'analyse manuelle de la fonction ImageGetCertificateDataEx a montré que les quatre premiers paramètres correspondent à ceux de ImageGetCertificateData, mais avec un paramètre de sortie supplémentaire à la fin.

Nous avons écrit un programme de test simple qui nous permet d'appeler cette fonction et d'effectuer des contrôles sur le cinquième paramètre inconnu :

#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;
}

L'exécution de cette opération contre divers exécutables a confirmé nos attentes : la valeur de retour inconnue était 1 pour notre exécutable "cassé", et 0 pour les binaires correctement signés. Cela a confirmé que le problème provenait d'une partie de la fonction ImageGetCertificateDataEx.

Une analyse plus approfondie de cette fonction a révélé que l'indicateur inconnu est activé par une autre fonction interne : IsBufferCleanOfInvalidMarkers.

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

Après avoir nettoyé la fonction IsBufferCleanOfInvalidMarkers, nous avons observé ce qui suit :

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;
}

Cette fonction charge une liste globale de "marqueurs non valides" en utilisant LoadInvalidMarkers, s'ils ne sont pas déjà chargés. imagehlp.dll contient une liste de marqueurs par défaut codée en dur, mais vérifie également le registre pour une liste définie par l'utilisateur au chemin d'accès suivant :

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

Cette valeur de registre ne semble pas exister par défaut.

La fonction effectue ensuite une recherche sur l'ensemble des données de la signature PE, à la recherche de l'un ou l'autre de ces marqueurs. Si une correspondance est trouvée, pdwInvalidMarkerFound est remplacé par 1, qui correspond directement à la valeur psSipSubjectInfo->dwReserved1 mentionnée plus haut.

Vider les marqueurs non valides

Les marqueurs sont stockés dans une structure non documentée à l'intérieur de imagehlp.dll. Après avoir procédé à l'ingénierie inverse de la fonction RabinKarpFindPatternInBuffer mentionnée ci-dessus, nous avons écrit un petit outil pour extraire la liste complète des marqueurs :

#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;
}

Les résultats suivants ont été obtenus :

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-
<#$@@$#>

Comme prévu, il s'agit d'une liste de valeurs magiques relatives aux anciens installateurs et formats d'archives compressées. Cela correspond à la description de MS13-098, qui indique que certains installateurs sont concernés.

Nous avons soupçonné qu'il s'agissait d'un problème lié aux exécutables auto-extractibles. Si un exécutable se lit lui-même à partir du disque et analyse ses propres données à la recherche d'une archive intégrée (par exemple, un fichier ZIP), un pirate pourrait potentiellement ajouter des données malveillantes à la section de signature sans invalider la signature - puisque les données de signature ne peuvent pas se hacher elles-mêmes. L'exécutable vulnérable pourrait ainsi localiser les données malveillantes avant les données d'origine, en particulier s'il analyse le fichier à rebours à partir de la fin.

Nous avons ensuite trouvé une ancienne conférence RECon ( 2012 ) d'Igor Glücksmann, qui décrit exactement ce scénario et semble confirmer notre hypothèse.

Le correctif de Microsoft consistait à analyser le bloc de signatures PE à la recherche de modèles d'octets connus susceptibles d'indiquer ce type d'abus.

Enquête sur les faux positifs

Après un débogage plus approfondi, nous avons découvert que le binaire était signalé parce que les données de signature contenaient le marqueur EGGA de la liste ci-dessus :

Dans le contexte de la liste de marqueurs ci-dessus, la signature EGGA semble se rapporter à une valeur d'en-tête spécifique utilisée par un format d'archive appelé ALZip. Notre code n'utilise pas ce format de fichier.

L'heuristique de Microsoft considère la présence de EGGA comme la preuve que des données d'archives malveillantes ont été intégrées dans la signature PE. Dans la pratique, rien de tel n'a été constaté. Il se trouve que le bloc de signature lui-même inclut ces quatre octets dans les données hachées.

Les collisions de ce type sont inhabituelles, mais le hachage de pages (/ph) les a rendues plus probables. En augmentant la taille du bloc de signature, le hachage de pages accroît la surface des correspondances coïncidentes et augmente la probabilité de déclencher l'heuristique.

Le binaire ne contenait pas de routines d'auto-extraction, de sorte que le résultat obtenu sur EGGA était un faux positif. Dans ce contexte, l'avertissement n'a aucune incidence sur l'intégrité du dossier. Cela signifie qu'il est possible de re-signer le fichier à l'adresse /rmc pour rétablir la validation attendue.

Conclusion

Il est bien connu que des données supplémentaires peuvent être intégrées dans un fichier PE sans rompre sa signature en les ajoutant au bloc de sécurité. Même certains logiciels légitimes profitent de cette situation pour intégrer des métadonnées spécifiques à l'utilisateur dans des exécutables signés. Cependant, nous ne savions pas que Microsoft avait mis en place une heuristique pour détecter des cas malveillants spécifiques, bien qu'elle ait été introduite en 2012.

Le message d'erreur original était très vague, et nous n'avons pas pu trouver de documentation ou de références en ligne permettant d'expliquer ce comportement. Même la recherche de la valeur de registre associée après l'avoir découverte (PECertInvalidMarkers) n'a donné aucun résultat.

Ce que nous avons découvert, c'est que Microsoft a ajouté l'analyse heuristique des blocs de signatures il y a plus de dix ans pour contrer des cas d'abus spécifiques. Ces heuristiques résident dans une liste codée en dur de "marqueurs invalides", dont beaucoup sont liés à des installateurs et des formats d'archive obsolètes. Notre binaire est entré en collision avec l'un de ces marqueurs lorsqu'il a été signé avec la fonction de hachage de page activée, ce qui a entraîné un échec de validation sans documentation claire et sans références publiques à la clé de registre sous-jacente ou à la logique de détection.

L'absence de discussions en ligne concernant ce mode de défaillance, à part un seul message non résolu de la communauté des développeurs de Visual Studio datant de 2018, a rendu le diagnostic initial difficile. En publiant cette analyse, nous souhaitons fournir un point de référence technique à ceux qui pourraient rencontrer le même problème. Dans notre cas, la résolution du problème a nécessité un dépannage approfondi que peu de personnes extérieures à cet espace auraient normalement besoin d'effectuer. Pour les équipes qui automatisent la signature du code, la principale leçon à tirer est qu'il faut intégrer les contrôles de validation des signatures dès le début et être conscient du fait que la détection heuristique des marqueurs peut conduire à des échecs dans les cas extrêmes.

Références supplémentaires

Vous pouvez trouver l'auteur sur X à l'adresse @x86matthew.

Partager cet article