Introdução
O Elastic Security Labs encontrou recentemente um problema de validação de assinatura com um de nossos binários do Windows. O executável foi assinado usando signtool.exe como parte do nosso processo padrão de integração contínua (CI), mas nesta ocasião, o arquivo de saída falhou na validação da assinatura com a seguinte mensagem de erro:
A assinatura digital do objeto está malformada. Para detalhes técnicos, consulte o boletim de segurança MS13-098.
A documentação do MS13-098 é vaga, mas descreve uma vulnerabilidade potencial relacionada a assinaturas Authenticode malformadas. Nada óbvio havia mudado da nossa parte que pudesse explicar esse novo erro, então precisávamos investigar a causa e resolver o problema.
Embora tenhamos identificado que esse problema estava afetando um dos nossos binários assinados do Windows, ele poderia afetar qualquer binário. Estamos publicando esta pesquisa como referência para qualquer outra pessoa que possa encontrar o mesmo problema no futuro.
Diagnóstico
Para investigar mais a fundo, criamos um programa de teste básico que chamou a função WinVerifyTrust do Windows no executável problemático para validar manualmente a assinatura. Isso revelou que estava falhando com o código de erro TRUST_E_MALFORMED_SIGNATURE.
WinVerifyTrust é uma função complexa, mas após anexar um depurador, descobrimos que o código de erro estava sendo definido no seguinte ponto:
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
Conforme mostrado acima, se psSipSubjectInfo->dwReserved1 não for 0, o código chama I_GetRelaxedMarkerCheckFlags. Se esta função não retornar dados, o código definirá o erro TRUST_E_MALFORMED_SIGNATURE e sairá.
Ao percorrer o código com nosso binário problemático, vimos que dwReserved1 estava de fato definido como 1. Executando o mesmo teste em um binário assinado corretamente, esse valor sempre foi 0, o que ignora a chamada para I_GetRelaxedMarkerCheckFlags.
Observando I_GetRelaxedMarkerCheckFlags, vimos que ele simplesmente verifica a presença de um atributo específico: 1.3.6.1.4.1.311.2.6.1. Uma rápida pesquisa on-line revelou muito pouco além do fato de que esse identificador de objeto (OID) é rotulado 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;
}
Nosso binário não tinha esse atributo, o que fez com que a função não retornasse dados e acionasse o erro. Os nomes das funções nos lembraram de um parâmetro opcional que vimos anteriormente em signtool.exe:
/rmc- Especifica a assinatura de um arquivo PE com a semântica de verificação de marcador relaxada. O sinalizador é ignorado para arquivos não-PE. Durante a verificação, certas seções autenticadas da assinatura ignorarão a verificação de marcadores PE inválidos. Esta opção só deve ser usada após consideração cuidadosa e revisão dos detalhes do caso MSRC MS12-024 para garantir que nenhuma vulnerabilidade seja introduzida.
Com base em nossa análise, suspeitamos que assinar novamente o executável com o sinalizador “verificação de marcador relaxada” (/rmc) e, como esperado, a assinatura agora era válida.
Análise das causas principais
Embora a solução alternativa acima tenha resolvido nosso problema imediato, claramente não era a causa raiz. Precisávamos investigar mais para entender por que o sinalizador interno dwReserved1 foi definido em primeiro lugar.
Este campo faz parte da estrutura SIP_SUBJECTINFO , que está documentada no MSDN - mas, infelizmente, não ajudou muito neste caso:
Para descobrir onde esse campo estava sendo definido, trabalhamos de trás para frente e identificamos um ponto onde dwReserved1 ainda era 0 - ou seja, antes do sinalizador ter sido definido. Colocamos um ponto de interrupção de hardware (na gravação) no campo dwReserved1 e retomamos a execução. O ponto de interrupção foi atingido na função 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 função chama a API ImageGetCertificateDataEx que é exportada por imagehlp.dll. O valor retornado pelo quinto parâmetro desta função é armazenado em dwReserved1. Esse valor determina, em última análise, se o PE é considerado "malformado" da maneira que temos observado.
Infelizmente, ImageGetCertificateDataEx não está documentado no MSDN. Entretanto, uma variante anterior, ImageGetCertificateData, está documentada:
BOOL IMAGEAPI ImageGetCertificateData(
[in] HANDLE FileHandle,
[in] DWORD CertificateIndex,
[out] LPWIN_CERTIFICATE Certificate,
[in, out] PDWORD RequiredLength
);
Esta função extrai o conteúdo do diretório IMAGE_DIRECTORY_ENTRY_SECURITY dos cabeçalhos PE. A análise manual da função ImageGetCertificateDataEx mostrou que os quatro primeiros parâmetros correspondem aos de ImageGetCertificateData, mas com um parâmetro de saída adicional no final.
Escrevemos um programa de teste simples que nos permite chamar esta função e realizar verificações em relação ao quinto parâmetro desconhecido:
#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;
}
Executar isso em uma variedade de executáveis confirmou nossas expectativas: o valor de retorno desconhecido foi 1 para nosso executável “quebrado” e 0 para binários assinados corretamente. Isso confirmou que o problema se originou em algum lugar dentro da função ImageGetCertificateDataEx .
Uma análise mais aprofundada desta função revelou que o sinalizador desconhecido está sendo definido por outra função interna: IsBufferCleanOfInvalidMarkers.
...
if(!IsBufferCleanOfInvalidMarkers(v25, v15, pdwUnknown))
{
LastError = GetLastError();
if(!pdwUnknown)
goto LABEL_34;
}
...
Após limpar a função IsBufferCleanOfInvalidMarkers , observamos o seguinte:
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 função carrega uma lista global de "marcadores inválidos" usando LoadInvalidMarkers, se eles ainda não estiverem carregados. imagehlp.dll contém uma lista padrão codificada de marcadores, mas também verifica o registro para uma lista definida pelo usuário no seguinte caminho:
HKEY_LOCAL_MACHINE\Software\Microsoft\Cryptography\Wintrust\Config\PECertInvalidMarkers
Este valor de registro parece não existir por padrão.
A função então executa uma pesquisa em todos os dados de assinatura do PE, procurando por qualquer um desses marcadores. Se uma correspondência for encontrada, pdwInvalidMarkerFound será definido como 1, que mapeia diretamente para o valor psSipSubjectInfo->dwReserved1 mencionado anteriormente.
Despejando os marcadores inválidos
Os marcadores são armazenados em uma estrutura não documentada dentro de imagehlp.dll. Após a engenharia reversa da função RabinKarpFindPatternInBuffer mencionada acima, escrevemos uma pequena ferramenta para despejar toda a lista 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;
}
Isso produziu os seguintes 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 esperado, esta parece ser uma lista de valores mágicos pertencentes a instaladores antigos e formatos de arquivo compactados. Isso está alinhado com a descrição do MS13-098, que sugere que certos instaladores podem ser afetados.
Suspeitamos que isso estava relacionado a executáveis autoextraíveis. Se um executável lê a si mesmo do disco e verifica seus próprios dados em busca de um arquivo incorporado (por exemplo, um arquivo ZIP), um invasor pode potencialmente anexar dados maliciosos à seção de assinatura sem invalidar a assinatura, já que os dados da assinatura não podem fazer hash de si mesmos. Isso poderia fazer com que o executável vulnerável localizasse os dados maliciosos antes dos dados originais, especialmente se ele escanear de trás para frente a partir do final do arquivo.
Mais tarde, encontramos uma antiga palestra do RECon de 2012 por Igor Glücksmann, que descreve esse cenário exato e parece confirmar nossa hipótese.
A correção da Microsoft envolveu a varredura do bloco de assinatura do PE em busca de padrões de bytes conhecidos que pudessem indicar esse tipo de abuso.
Investigando o falso positivo
Após mais depuração, descobrimos que o binário estava sendo sinalizado devido aos dados de assinatura contendo o marcador EGGA da lista acima:
No contexto da lista de marcadores acima, a assinatura EGGA parece estar relacionada a um valor de cabeçalho específico usado por um formato de arquivo chamado ALZip. Nosso código não faz uso desse formato de arquivo.
A heurística da Microsoft tratou a presença de EGGA como evidência de que dados de arquivo maliciosos haviam sido incorporados na assinatura PE. Na prática, nada disso estava presente. O próprio bloco de assinatura incluía esses quatro bytes como parte dos dados com hash.
Colisões como essa são incomuns, mas o hash de página (/ph) as tornou mais prováveis. Ao expandir o tamanho do bloco de assinatura, o hash de página aumenta a área de superfície para correspondências coincidentes e aumenta a probabilidade de acionar a heurística.
O binário não continha nenhuma rotina de autoextração, então o resultado em EGGA foi um falso positivo. Nesse contexto, o aviso não teve relação com a integridade do arquivo. Isso significava que era seguro assinar novamente o arquivo com /rmc para restaurar a validação esperada.
Conclusão
É bem sabido que dados adicionais podem ser incorporados em um arquivo PE sem quebrar sua assinatura, anexando-os ao bloco de segurança. Até mesmo alguns produtos de software legítimos aproveitam isso para incorporar metadados específicos do usuário em executáveis assinados. No entanto, não sabíamos que a Microsoft havia implementado heurísticas para detectar casos maliciosos específicos disso, embora elas tenham sido introduzidas em 2012.
A mensagem de erro original era muito vaga e não conseguimos encontrar nenhuma documentação ou referência on-line que ajudasse a explicar o comportamento. Mesmo a busca pelo valor de registro associado após descobri-lo (PECertInvalidMarkers) não produziu nenhum resultado.
O que descobrimos é que a Microsoft adicionou a varredura heurística de blocos de assinatura há mais de uma década para combater casos específicos de abuso. Essas heurísticas residem em uma lista codificada de “marcadores inválidos”, muitos dos quais estão vinculados a instaladores e formatos de arquivo desatualizados. Nosso binário colidiu com um desses marcadores quando assinado com o hash de página habilitado, criando uma falha de validação sem documentação clara e sem referências públicas à chave de registro subjacente ou lógica de detecção.
A ausência de discussões on-line sobre esse modo de falha, além de uma única postagem não resolvida na Comunidade de Desenvolvedores do Visual Studio de 2018, dificultou o diagnóstico inicial. Ao publicar esta análise, queremos fornecer um ponto de referência técnico para outras pessoas que possam encontrar o mesmo problema. No nosso caso, resolver o problema exigiu uma solução de problemas profunda que poucas pessoas fora desse setor normalmente precisariam realizar. Para equipes que automatizam a assinatura de código, a lição principal é integrar verificações de validação de assinatura antecipadamente e estar ciente de que a detecção de marcadores heurísticos pode levar a falhas em casos extremos.
Referências adicionais
O autor pode ser encontrado no X em @x86matthew.
