Elastic Security Labs

Untersuchung einer mysteriös falsch formatierten Authenticode-Signatur

Aufdeckung der versteckten Heuristiken hinter fehlerhaften authentischen Signaturen

16 Minuten LesezeitInternals, Detection Engineering
Untersuchung einer mysteriös falsch formatierten Authenticode-Signatur

Einführung

Elastic Security Labs hat kürzlich ein Problem mit der Signaturvalidierung mit einer unserer Windows-Binärdateien festgestellt. Die ausführbare Datei wurde mit signtool.exe als Teil unseres standardmäßigen CI-Prozesses (Continuous Integration) signiert, aber in diesem Fall hat die Signaturüberprüfung der Ausgabedatei mit der folgenden Fehlermeldung fehlgeschlagen:

Die digitale Signatur des Objekts ist falsch formatiert. Ausführliche Informationen finden Sie im Security Bulletin MS13-098.

Die Dokumentation für MS13-098 ist vage, beschreibt jedoch eine potenzielle Sicherheitsanfälligkeit im Zusammenhang mit fehlerhaften Authenticode-Signaturen. Auf unserer Seite hatte sich nichts Offensichtliches geändert, was diesen neuen Fehler erklären könnte, also mussten wir die Ursache untersuchen und das Problem beheben.

Wir haben zwar festgestellt, dass dieses Problem eine unserer signierten Windows-Binärdateien betrifft, aber es kann sich auf jede Binärdatei auswirken. Wir veröffentlichen diese Studie als Referenz für alle anderen, die in Zukunft auf das gleiche Problem stoßen könnten.

Diagnose

Um dies weiter zu untersuchen, haben wir ein grundlegendes Testprogramm erstellt, das die Windows WinVerifyTrust -Funktion für die problematische ausführbare Datei aufgerufen hat, um die Signatur manuell zu validieren. Dies ergab, dass es mit dem Fehlercode TRUST_E_MALFORMED_SIGNATUREfehlschlug.

WinVerifyTrust ist eine komplexe Funktion, aber nach dem Anfügen eines Debuggers haben wir festgestellt, dass der Fehlercode an der folgenden Stelle festgelegt wurde:

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

Wie oben gezeigt, ruft der Code I_GetRelaxedMarkerCheckFlagsauf, wenn psSipSubjectInfo->dwReserved1 nicht 0ist. Wenn diese Funktion keine Daten zurückgibt, legt der Code den TRUST_E_MALFORMED_SIGNATURE Fehler fest und wird beendet.

Als wir den Code mit unserer problematischen Binärdatei durchliefen, sahen wir, dass dwReserved1 tatsächlich auf 1gesetzt war. Beim Ausführen desselben Tests für eine korrekt signierte Binärdatei war dieser Wert immer 0, wodurch der Aufruf von I_GetRelaxedMarkerCheckFlagsübersprungen wird.

Als wir uns I_GetRelaxedMarkerCheckFlagsangesehen haben, haben wir gesehen, dass es einfach nach dem Vorhandensein eines bestimmten Attributs sucht: 1.3.6.1.4.1.311.2.6.1. Eine schnelle Online-Suche ergab sehr wenig außer der Tatsache, dass dieser Objektbezeichner (OID) als SpcRelaxedPEMarkerCheckgekennzeichnet ist.

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

Unsere Binärdatei hatte dieses Attribut nicht, was dazu führte, dass die Funktion keine Daten zurückgab und den Fehler auslöste. Die Funktionsnamen erinnerten uns an einen optionalen Parameter, den wir zuvor in signtool.exegesehen hatten:

/rmc - Gibt an, dass eine PE-Datei mit der Semantik der gelockerten Markierungsprüfung signiert wird. Das Flag wird für Nicht-PE-Dateien ignoriert. Während der Überprüfung umgehen bestimmte authentifizierte Abschnitte der Signatur die Prüfung ungültiger PE-Markierungen. Diese Option sollte nur nach sorgfältiger Abwägung und Überprüfung der Details des MSRC-Falls MS12-024 verwendet werden, um sicherzustellen, dass keine Sicherheitsanfälligkeiten eingeführt werden.

Basierend auf unserer Analyse vermuteten wir, dass das erneute Signieren der ausführbaren Datei mit dem Flag "relaxed marker check" (/rmc) und wie erwartet nun gültig war.

Ursachenanalyse

Die obige Problemumgehung hat zwar unser unmittelbares Problem gelöst, war aber eindeutig nicht die Grundursache. Wir mussten weiter nachforschen, um zu verstehen, warum das interne dwReserved1 -Flag überhaupt gesetzt wurde.

Dieses Feld ist Teil der SIP_SUBJECTINFO Struktur, die auf MSDN dokumentiert ist - aber leider hat es in diesem Fall nicht viel geholfen:

Um herauszufinden, wo dieses Feld gesetzt wurde, arbeiteten wir rückwärts und identifizierten einen Punkt, an dem dwReserved1 noch 0 war - also bevor das Flag gesetzt wurde. Wir haben einen Hardware-Haltepunkt (beim Schreiben) auf das Feld dwReserved1 gesetzt und die Ausführung fortgesetzt. Der Breakpoint wurde in der Funktion SIPObjectPE_::GetMessageFromFile erreicht:

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

Diese Funktion ruft die ImageGetCertificateDataEx -API auf, die von imagehlp.dllexportiert wird. Der Wert, der vom fünften Parameter dieser Funktion zurückgegeben wird, wird in dwReserved1gespeichert. Dieser Wert bestimmt letztendlich, ob das PE in der von uns beobachteten Weise als "missgebildet" angesehen wird.

Leider ist ImageGetCertificateDataEx auf MSDN nicht dokumentiert. Eine frühere Variante, ImageGetCertificateData, ist jedoch dokumentiert:

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

Diese Funktion extrahiert den Inhalt des Verzeichnisses IMAGE_DIRECTORY_ENTRY_SECURITY aus den PE-Headern. Die manuelle Analyse der Funktion ImageGetCertificateDataEx zeigte, dass die ersten vier Parameter mit denen von ImageGetCertificateDataübereinstimmten, jedoch mit einem zusätzlichen Ausgabeparameter am Ende.

Wir haben ein einfaches Testprogramm geschrieben, das es uns ermöglicht, diese Funktion aufzurufen und Überprüfungen gegen den unbekannten fünften Parameter durchzuführen:

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

Das Ausführen für eine Vielzahl von ausführbaren Dateien bestätigte unsere Erwartungen: Der unbekannte Rückgabewert wurde für unsere "defekte" ausführbare Datei 1 und 0 für korrekt signierte Binärdateien. Dies bestätigte, dass das Problem irgendwo in der ImageGetCertificateDataEx Funktion aufgetreten ist.

Eine weitere Analyse dieser Funktion ergab, dass das unbekannte Flag von einer anderen internen Funktion gesetzt wird: IsBufferCleanOfInvalidMarkers.

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

Nach dem Bereinigen der Funktion IsBufferCleanOfInvalidMarkers haben wir Folgendes beobachtet:

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

Diese Funktion lädt mit LoadInvalidMarkerseine globale Liste von "ungültigen Markern", falls diese nicht bereits geladen sind. imagehlp.dll enthält eine hartcodierte Standardliste von Markern, überprüft aber auch die Registrierung auf eine benutzerdefinierte Liste unter dem folgenden Pfad:

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

Dieser Registrierungswert scheint standardmäßig nicht vorhanden zu sein.

Die Funktion führt dann eine Suche in den gesamten PE-Signaturdaten durch, wobei nach einem dieser Marker gesucht wird. Wenn eine Übereinstimmung gefunden wird, wird pdwInvalidMarkerFound auf 1festgelegt, was direkt dem zuvor erwähnten psSipSubjectInfo->dwReserved1 Wert zugeordnet wird.

Dump der ungültigen Marker

Die Marker werden in einer undokumentierten Struktur in imagehlp.dllgespeichert. Nach dem Reverse-Engineering der oben erwähnten RabinKarpFindPatternInBuffer -Funktion haben wir ein kleines Tool geschrieben, um die gesamte Liste der Marker auszugeben:

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

Dies führte zu folgenden Ergebnissen:

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

Wie erwartet, scheint dies eine Liste von magischen Werten zu sein, die sich auf alte Installationsprogramme und komprimierte Archivformate beziehen. Dies stimmt mit der Beschreibung von MS13-098 überein, die darauf hinweist, dass bestimmte Installationsprogramme betroffen sind.

Wir vermuteten, dass dies mit selbstextrahierenden ausführbaren Dateien zusammenhing. Wenn sich eine ausführbare Datei selbst von der Festplatte liest und ihre eigenen Daten nach einem eingebetteten Archiv (z.B. einer ZIP-Datei) durchsucht, könnte ein Angreifer möglicherweise bösartige Daten an den Signaturabschnitt anhängen, ohne die Signatur ungültig zu machen - da Signaturdaten sich nicht selbst hashen können. Dies kann möglicherweise dazu führen, dass die anfällige ausführbare Datei die schädlichen Daten vor den ursprünglichen Daten findet, insbesondere wenn sie vom Ende der Datei aus rückwärts gescannt wird.

Später fanden wir einen alten RECon-Vortrag aus dem 2012 von Igor Glücksmann, der genau dieses Szenario beschreibt und unsere Hypothese zu bestätigen scheint.

Die Lösung von Microsoft bestand darin, den PE-Signaturblock nach bekannten Bytemustern zu durchsuchen, die auf diese Art von Missbrauch hinweisen könnten.

Untersuchen des falsch positiven Ergebnisses

Beim weiteren Debuggen stellten wir fest, dass die Binärdatei aufgrund der Signaturdaten gekennzeichnet wurde, die die EGGA Markierung aus der obigen Liste enthielten:

Im Kontext der obigen Liste von Markierungen scheint sich die EGGA Signatur auf einen bestimmten Header-Wert zu beziehen, der von einem Archivformat namens ALZip verwendet wird. Unser Code verwendet dieses Dateiformat nicht.

Die Heuristik von Microsoft behandelte das Vorhandensein von EGGA als Beweis dafür, dass bösartige Archivdaten in die PE-Signatur eingebettet waren. In der Praxis war nichts dergleichen vorhanden. Der Signaturblock selbst enthielt zufällig diese vier Bytes als Teil der gehashten Daten.

Kollisionen wie diese sind ungewöhnlich, aber Page Hashing (/ph) hat es wahrscheinlicher gemacht. Durch das Vergrößern des Signaturblocks vergrößert das Seitenhashing die Oberfläche für zufällige Übereinstimmungen und erhöht die Wahrscheinlichkeit, dass die Heuristik ausgelöst wird.

Die Binärdatei enthielt keine selbstextrahierenden Routinen, so dass der Treffer auf EGGA ein Fehlalarm war. In diesem Zusammenhang hatte die Warnung keinen Einfluss auf die Integrität der Datei. Dies bedeutete, dass es sicher war, die Datei erneut mit /rmc zu signieren, um die erwartete Validierung wiederherzustellen.

Fazit

Es ist allgemein bekannt, dass zusätzliche Daten in eine PE-Datei eingebettet werden können, ohne ihre Signatur zu beschädigen, indem sie an den Sicherheitsblock angehängt werden. Sogar einige legitime Softwareprodukte nutzen dies, um benutzerspezifische Metadaten in signierte ausführbare Dateien einzubetten. Uns war jedoch nicht bekannt, dass Microsoft Heuristiken implementiert hat, um bestimmte bösartige Fälle davon zu erkennen, obwohl sie bereits 2012 eingeführt wurden.

Die ursprüngliche Fehlermeldung war sehr vage, und wir konnten online keine Dokumentation oder Referenzen finden, die das Verhalten erklären konnten. Selbst die Suche nach dem zugeordneten Registrierungswert nach dessen Entdeckung (PECertInvalidMarkers) ergab keine Ergebnisse.

Was wir herausgefunden haben, ist, dass Microsoft vor mehr als einem Jahrzehnt heuristische Scans von Signaturblöcken hinzugefügt hat, um bestimmten Missbrauchsfällen entgegenzuwirken. Diese Heuristiken befinden sich in einer fest codierten Liste von "ungültigen Markern", von denen viele an veraltete Installationsprogramme und Archivformate gebunden sind. Unsere Binärdatei kollidierte zufällig mit einer dieser Markierungen, als sie mit aktiviertem Seiten-Hashing signiert wurde, was zu einem Validierungsfehler ohne klare Dokumentation und ohne öffentliche Verweise auf den zugrunde liegenden Registrierungsschlüssel oder die Erkennungslogik führte.

Das Fehlen von Onlinediskussionen zu diesem Fehlermodus, abgesehen von einem einzigen ungelösten Beitrag der Visual Studio Developer Community aus dem Jahr 2018, erschwerte die erste Diagnose. Mit der Veröffentlichung dieser Analyse möchten wir einen technischen Anhaltspunkt für andere bieten, die auf das gleiche Problem stoßen könnten. In unserem Fall erforderte die Lösung des Problems eine gründliche Fehlerbehebung, die normalerweise nur wenige außerhalb dieses Bereichs durchführen müssten. Für Teams, die die Code-Signierung automatisieren, besteht die wichtigste Lektion darin, Signaturvalidierungsprüfungen frühzeitig zu integrieren und sich darüber im Klaren zu sein, dass die Erkennung heuristischer Marker zu Fehlern in Grenzfällen führen kann.

Weitere Verweise

Den Autor finden Sie auf X bei @x86matthew.