El formato de archivo RecentFileCache.bcf, en términos sencillos
El formato BCF es una cabecera, una lista de rutas UTF-16LE con prefijo de longitud, y se acabó. Aquí está lo que significa cada byte y por qué nada ha cambiado desde 2009.
Hay formatos de archivo forenses que te pelean. Logs de CBS, bases de datos ESE, la estructura binaria del log de eventos de Windows. RecentFileCache.bcf no es uno de ellos. Es una cabecera, un contador y una lista de cadenas. Puedes parsearlo en doce líneas de Python y no necesitarás un editor hexadecimal para nada.
Esa simplicidad es la razón por la que el formato no ha cambiado desde que se lanzó Windows 7 SP1. No hay nada que extender. Microsoft pasó al AmCache en lugar de versionar este archivo.
Qué hay en el archivo
C:\Windows\AppCompat\Programs\RecentFileCache.bcf lo escribe la tarea de evaluación de Application Experience en Win7 SP1 y Server 2008 R2. La distribución en disco, en orden:
- Una cabecera fija. Veinte bytes.
- Un contador de entradas de 32 bits little-endian.
- Cero o más entradas, cada una siendo una cadena UTF-16LE con prefijo de longitud que contiene la ruta NT completa de un ejecutable.
Eso es todo el archivo. Sin checksums. Sin CRC. Sin cola. Sin marca de tiempo por entrada. Sin hash por entrada. Si el archivo tiene 18 bytes de largo, tiene una cabecera y nada más. Si tiene dos megabytes de largo, ha estado creciendo durante un tiempo porque el appraiser no se ha ejecutado.
La cabecera
Los primeros veinte bytes son constantes en cada BCF que he sacado de un host Win7. Cinco valores de 32 bits little-endian, todos ellos constantes mágicas. La interesante es la primera: 0xFE 0xFF 0xEE 0xFE. El resto son ceros o relleno. Si ves un valor distinto en los primeros cuatro bytes, no tienes un RecentFileCache.bcf. Tienes otra cosa que casualmente comparte el nombre.
He visto exactamente un caso de cabecera corrupta en el campo, y fue causado por un antivirus que puso el archivo a medias en cuarentena. Los bytes de cabecera habían sido reemplazados por ceros. Vale la pena comprobarlo.
El contador de entradas
Los bytes 20 a 23, little-endian, son el contador de entradas que siguen. Si este número es cero, el appraiser corrió recientemente y vació el conjunto de trabajo. Si es grande, o bien nada ha vaciado el archivo en semanas, o han aparecido muchos binarios nuevos entre pases de evaluación. Ambos son interesantes a su manera.
El contador es autoritativo. Si el contador dice diez y el archivo contiene once cadenas, trata la undécima como basura. En la práctica esto no ocurre porque el archivo lo escribe atómicamente un único proceso, pero los parsers no deberían confiar solo en recorrer cadenas para encontrar el final.
Las entradas
Cada entrada es:
- Una longitud de 32 bits little-endian en bytes (no caracteres).
- Esa cantidad de bytes de datos de cadena UTF-16LE.
Las cadenas son rutas del administrador de objetos NT. Empiezan con \??\ y luego una ruta normal de Windows:
\??\C:\Windows\System32\rundll32.exe
\??\C:\Users\bob\AppData\Local\Temp\setup.exe
Trata el prefijo \??\ como C:\. Es la forma del kernel de decir "espacio de nombres de dispositivo DOS" y no lleva ningún significado forense más allá de "esto es una ruta de archivo normal". Quítalo para mostrar, mantenlo por fidelidad si estás exportando salida legible por máquina.
La longitud está en bytes, lo que hace tropezar a casi todo el mundo en el primer parseo. Una ruta con treinta caracteres Unicode ocupa sesenta bytes más lo que el archivo use para el nulo final, que típicamente son dos bytes pero no deberías depender de ello. Lee la longitud, lee esa cantidad de bytes, decodifica como UTF-16LE, quita los nulos finales. Hecho.
No hay separador entre entradas. El campo de longitud de la siguiente entrada empieza inmediatamente después de los datos de cadena de la entrada anterior. Si tu parser se desplaza un byte, obtendrás basura para el resto del archivo. Por eso importa respetar el contador de entradas: te da una condición de parada que no depende del contenido de la cadena.
Doce líneas de Python
import struct
def parse_bcf(path):
with open(path, "rb") as f:
data = f.read()
if data[:4] != b"\xfe\xff\xee\xfe":
raise ValueError("not a RecentFileCache.bcf")
count = struct.unpack_from("<I", data, 20)[0]
offset = 24
entries = []
for _ in range(count):
length = struct.unpack_from("<I", data, offset)[0]
offset += 4
s = data[offset:offset + length].decode("utf-16-le").rstrip("\x00")
entries.append(s)
offset += length
return entries
Eso es suficiente para parsear cualquier BCF con la que te encuentres. Sin dependencias. Sin casos límite que haya visto en producción. Si haces esto en un entorno corporativo donde no puedes ejecutar Python arbitrario en la máquina de evidencia, el parser en este sitio hace lo mismo en el navegador sin subir nada.
Qué no te dice el formato
Esta es la parte donde la simplicidad del formato se vuelve una limitación. El BCF no registra:
- Tiempo de ejecución. Sabes que el binario corrió desde el último pase de evaluación, y eso es todo.
- Conteo de ejecuciones. Una ejecución y cien ejecuciones se ven idénticas.
- El usuario que lo ejecutó. El archivo es por máquina.
- Un hash. No puedes pivotar a threat intel desde el contenido del archivo.
- Una lista de carga. A diferencia de Prefetch, no hay registro de las DLLs cargadas en el proceso.
- Argumentos de línea de comandos. Ninguno. Ni siquiera la existencia de argumentos.
Si necesitas cualquiera de esos, empareja el BCF con Prefetch, la Master File Table, el USN journal y cualquier EVTX que tengas. El BCF te dice el qué; todo lo demás te dice el cuándo, cómo y quién.
Por qué el formato no ha cambiado
Microsoft reemplazó este archivo con el AmCache en Windows 8 y nunca volvió a añadir campos al BCF. El AmCache es un hive estilo ESE con docenas de campos por entrada: hashes, metadatos de archivo, tiempo de instalación, información del editor. El BCF nunca iba a crecer para parecerse a eso. Existió como archivo de trabajo para una pipeline de evaluación que desde entonces ha sido rediseñada en torno a primitivas diferentes.
La consecuencia práctica es que un parser de 2014 y un parser de 2026 producen la misma salida con la misma entrada. No hay deriva de versión por la que preocuparse. Las herramientas escritas antes de que el formato fuera documentado públicamente todavía funcionan. El análisis público original de Willi Ballenthin en Mandiant en 2014 sigue siendo preciso hoy; no he visto un solo byte desviarse de lo que él documentó.
Esto es raro para los artefactos de Windows. La mayoría mutan a través de las builds. Shimcache cambió cinco veces entre Win7 y Win10. El esquema del AmCache ganó y perdió columnas. Prefetch se comprimió en Windows 10. El BCF simplemente está ahí, congelado, porque nadie tuvo razón para tocarlo después de Win7.
Una nota sobre el truncamiento
Después de que el appraiser procesa el archivo, el SO lo trunca a "cabecera más cero entradas". El archivo no se borra. Los metadatos del sistema de archivos (tiempo de creación, entrada del directorio padre) sobreviven. Esto importa por dos razones:
- El tiempo de creación de
RecentFileCache.bcfmismo puede ser anterior a cualquier ruta dentro de él por meses o años. No uses las marcas de tiempo del propio archivo para acotar las entradas. - Un binario que corrió hace dos días y fue procesado por el appraiser ayer está fuera del archivo, pero el archivo sigue ahí, luciendo inocente y pequeño. Vacío no es lo mismo que nunca usado.
Lecturas adicionales
- Willi Ballenthin, RecentFileCache.bcf. El análisis público original del formato. Todavía correcto.
- Microsoft, Application Experience y Programa de Inventario. Contexto del fabricante sobre por qué existe el archivo.
- Yogesh Khatri, AmCache and friends. Para cuando necesites saber qué vino después.
Si has parseado algún formato forense binario antes, el BCF se va a sentir como un ejercicio de calentamiento. Esa es la reacción correcta. Gasta tu esfuerzo en triar la lista de rutas, no en el formato del archivo.