FileId de Amcache explicado: el formato de hash SHA-1 que almacena Windows

FileId en Root\InventoryApplicationFile es uno de los campos más útiles de toda la hive, y uno de los más malentendidos. Es un hash de contenido. No es del todo un SHA-1 estándar. No hashea del todo el archivo entero. Esta página es la referencia completa.

Para el contexto más amplio ve la Referencia completa de Amcache. Para la estructura del Registro que rodea esto dentro de la hive, ve Estructura del Registro de Amcache.

Cómo se ve el valor#

Un FileId típico de una hive real:

0000da39a3ee5e6b4b0d3255bfef95601890afd80709

41 caracteres en total:

  • Los cuatro primeros siempre son "0000". Tag de tipo fijo.
  • Los 40 restantes son el digest hex del SHA-1.

El prefijo "0000" es un artefacto histórico. Los builds tempranos del appraiser preveían múltiples algoritmos de hash con prefijos distintos. Solo llegó a usarse SHA-1. El prefijo lleva años siendo constante.

AmcacheParser separa esto en dos columnas CSV:

Columna Valor
FileId String completo de 41 caracteres con prefijo.
Hash 40 caracteres hex, SHA-1 solo.

Usa siempre Hash (o quita el prefijo tú) al hacer join contra feeds externos. VirusTotal, feeds de TI, bases de datos de allowlist todos quieren SHA-1 de 40 caracteres. Te van a fallar el match silenciosamente si incluyes el "0000".

Qué hashea de verdad#

La trampa que pilla a casi cualquier analista nuevo:

El SHA-1 hashea los primeros 31 MiB del archivo, no el archivo entero.

Para archivos más pequeños de 31 MiB (que es la mayoría de EXEs y DLLs), el hash de prefijo coincide con el SHA-1 del archivo completo. Indistinguibles.

Para archivos mayores, el valor de Amcache es un hash de prefijo. Sigue siendo lo bastante distintivo para identificar un build concreto de un binario concreto. Pero no es lo que sha1sum te daría sobre el archivo entero.

Por qué importa#

  • Matches de VirusTotal. Por debajo de 31 MiB, el SHA-1 de Amcache coincide con el SHA-1 que VT indexa. Archivos más grandes (instaladores, algunos binarios de juegos, software empresarial grande) a menudo no coinciden, y una respuesta "no hay registro" de VT no significa nada útil.
  • Bases de datos de hashes propias. Si mantienes una allowlist interna, guarda el mismo tipo de hash contra el que vas a comparar. O guarda SHA-1 de contenido completo (y acepta mismatches en binarios grandes) o mantén una columna paralela de hash de prefijo.
  • Verificación. Si tienes el binario original y quieres verificar, hashea solo los primeros 31 MiB:
import hashlib
PREFIX_BYTES = 31 * 1024 * 1024
 
def amcache_sha1(path: str) -> str:
    h = hashlib.sha1()
    with open(path, 'rb') as f:
        h.update(f.read(PREFIX_BYTES))
    return h.hexdigest()

Trampas del mundo real#

Un puñado de tropezones que aparecen en casos reales.

No incluyas el prefijo en los lookups#

# Mal
search_virustotal('0000da39a3ee5e6b4b0d3255bfef95601890afd80709')
 
# Bien
search_virustotal('da39a3ee5e6b4b0d3255bfef95601890afd80709')

La API de VirusTotal espera el hash pelado. Incluir el prefijo devuelve un resultado vacío silenciosamente, que parece idéntico a "este archivo es desconocido". Eso desperdicia tiempo de triage y produce hallazgos equivocados.

Las colisiones de SHA-1 son teóricas pero no imposibles#

Existen ataques reales de colisión de SHA-1. En contexto no adversarial es irrelevante. Encontrar una colisión contra una entrada concreta de Amcache es absurdamente desproporcionado para lo que el atacante gana. Pero para matching de alta confianza en una investigación de alto riesgo, no trates un match de SHA-1 como identidad criptográfica. Combina con tamaño de archivo, link date y al menos un campo más.

No te fíes del FileId de una fila no-PE#

Amcache registra ocasionalmente valores FileId para archivos no-PE que el appraiser vio. El hash sigue siendo real, pero las herramientas posteriores que asumen contexto PE (búsquedas PE-aware de VirusTotal, reglas Yara contra bytes PE) devuelven resultados menos útiles.

Varias filas, mismo hash#

Si encuentras el mismo Hash en varias filas de *_UnassociatedFileEntries.csv en el mismo host, eso es significativo. El mismo contenido binario se inventarió en varias rutas. Razones habituales:

  • El atacante copió la herramienta a varias ubicaciones para probar cuál se ejecuta.
  • Un instalador legítimo dejó la misma DLL en varios directorios de producto.
  • Un usuario copió un archivo a mano.

Agrupa por Hash, luego mira FullPath y KeyLastWriteTimestamp de cada instancia. Los timestamps cuentan la secuencia. Las rutas cuentan la intención.

Varios hashes, mismo FullPath#

El patrón opuesto. Misma ruta, distinto Hash en varias filas significa que el binario en esa ruta cambió entre inventarios. Señal fuerte de reemplazo:

  • Legítimo: actualización de software sobrescribió el archivo.
  • Sospechoso: el atacante intercambió un binario del sistema o una herramienta de usuario habitual por una copia troyanizada.

Ordena las filas por KeyLastWriteTimestamp para ver cuándo apareció cada hash nuevo. Correla con eventos de parche o eventos Sysmon File Create alrededor de esos momentos.

Pivotes que se ganan su sitio#

Caza de hash entre hosts#

# Pivotar un SHA-1 conocido-malo entre las CSVs de Amcache de cada host
$badHash = 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
Get-ChildItem -Recurse -Filter *_UnassociatedFileEntries.csv |
  ForEach-Object {
    Import-Csv $_.FullName |
      Where-Object { $_.Hash -eq $badHash } |
      Select @{n='Host';e={$_.PSChildName.Split('_')[0]}},
             FullPath, KeyLastWriteTimestamp, Size
  } |
  Sort-Object Host

Así pasas de "encontramos este hash en un host" a "todos los hosts del parque que han tenido alguna vez este binario".

Enriquecimiento con VirusTotal#

import csv, requests, time
 
API = 'https://www.virustotal.com/api/v3/files/'
HEADERS = {'x-apikey': '<your-key>'}
 
seen = set()
with open('HOST_amcache_UnassociatedFileEntries.csv', newline='') as f:
    for row in csv.DictReader(f):
        h = row['Hash']
        if not h or h in seen:
            continue
        seen.add(h)
        r = requests.get(API + h, headers=HEADERS)
        if r.status_code == 200:
            stats = r.json()['data']['attributes']['last_analysis_stats']
            if stats.get('malicious', 0) > 0:
                print(h, stats, row['FullPath'])
        time.sleep(15)  # rate-limit de la API pública de VT

Incluso a bajo volumen contra la API pública saca una lista compacta de hashes confirmados-malos en un host infectado típico.

Correlación con Sysmon Image Loaded#

El Event ID 7 de Sysmon registra el SHA-1 de cada DLL cargada por cada proceso. Unir el Hash de Amcache contra el campo Hashes de Sysmon 7 te dice exactamente qué procesos cargaron una DLL del atacante concreta, y cuándo.

Lecturas adicionales#

Relacionados#

Artículos relacionados

Volver a todos los artículos