Uno de los módulos en el kernel de Windows proporciona soporte para combinar un conjunto de operaciones de archivo en una entidad conocida como transacción . Al igual que en las bases de datos, estas entidades son aisladas y atómicas . Puede realizar algunos cambios en el sistema de archivos que no serán visibles hasta que los confirme . O, como alternativa, siempre puede revertir todo. En cualquier caso, usted actúa sobre el grupo de operaciones en su conjunto. Precisamente, ¿qué se necesitaba para preservar la coherencia al instalar software o actualizar nuestros sistemas, ¿verdad? Si algo sale mal, el instalador o incluso todo el sistema falla, la transacción se revierte automáticamente.
Desde la primera vez que vi un artículo sobre este increíble mecanismo, siempre me pregunté cómo sería el mundo desde adentro. ¿Y sabes que? Acabo de descubrir un enfoque verdaderamente maravilloso para forzar a cualquier proceso a operar dentro de una transacción predefinida, que este margen es demasiado estrecho para contener . Además, la mayoría de las veces, ni siquiera requiere privilegios administrativos.
Luego hablemos sobre los elementos internos de Windows, pruebe una nueva herramienta y responda una pregunta: ¿qué tiene que ver con los sandboxes?
Repositorio
Aquellos que quieran comenzar a experimentar de inmediato son bienvenidos en la página del proyecto en GitHub: TransactionMaster .
Teoría
La introducción de Transactional NTFS, también conocido como TxF , en Windows Vista fue un paso revolucionario para mantener la consistencia del sistema y, por lo tanto, la estabilidad. Al exponer esta funcionalidad directamente a los desarrolladores, Microsoft hizo posible simplificar dramáticamente el manejo de errores en todos los componentes responsables de instalar y actualizar el software. La tarea de mantener un plan de respaldo para todas las posibles fallas del sistema de archivos se convirtió en un trabajo del sistema operativo en sí, que comenzó a proporcionar una semántica ACID con todas las funciones a pedido.
Para proporcionar este nuevo instrumento, Microsoft introdujo un conjunto de funciones API que duplicaban la funcionalidad existente, pero dentro de un contexto de transacciones. La transacción en sí se convirtió en un nuevo objeto del núcleo, junto con los existentes, como archivos, procesos y primitivas de sincronización. En el escenario más simple, la aplicación crea una nueva transacción usando CreateTransaction
, realiza las operaciones requeridas ( CreateFileTransacted
, MoveFileTransacted
, DeleteFileTransacted
, etc.), y luego la confirma o CommitTransaction
con CommitTransaction
/ RollbackTransaction
.
Echemos un vistazo a la arquitectura de estas nuevas funciones. Sabemos que la capa API oficial de bibliotecas como kernel32.dll
no invoca el kernel directamente, sino que convierte los parámetros y reenvía la llamada a ntdll.dll
. Que luego, emite un syscall. Sorprendentemente, no hay signos de ninguna función adicional -Transacted tanto en el lado ntdll como en el kernel de la llamada.
Las definiciones de estas funciones de API nativas no han cambiado en décadas, por lo que no hay ningún parámetro adicional para especificar una transacción. ¿Cómo sabe el núcleo cuál usar entonces? La respuesta es simple pero prometedora: cada subproceso tiene un campo designado, donde almacena un identificador para la transacción actual. Esta variable reside en una región específica de la memoria llamada TEB - Thread Environmental Block. En cuanto a otros campos conocidos ubicados aquí también, puedo nombrar el último código de error y la ID del hilo .
Por lo tanto, todas las funciones con el sufijo -Transacted establecen el campo de transacción actual en TEB, llaman a la API no transaccionada correspondiente y restauran el valor anterior. Para lograr este objetivo, utilizan un par de rutinas bastante sencillas llamadas RtlGetCurrentTransaction
/ RtlSetCurrentTransaction
de ntdll
. Proporcionan un nivel suficiente de abstracción, que resulta útil en el caso de WoW64 , más sobre eso más adelante.
¿Qué significa para nosotros? Al cambiar una variable en la memoria, podemos controlar, en un contexto de qué transacción el proceso accede al sistema de archivos. No es necesario instalar ningún gancho o devolución de llamada en modo kernel, todo lo que necesitamos es entregar el identificador al proceso de destino y modificar un par de bytes de memoria por cada subproceso. Suena sorprendentemente fácil, ¡pero el resultado debe ser sorprendente!
Trampas
El primer concepto de trabajo reveló muchos detalles peculiares. Para mi gran deleite, Far Manager , que uso como reemplazo para el Explorador de Windows, está perfectamente bien con el cambio activo de transacciones. Pero también vi un par de programas, en los cuales mi método no tenía efecto esperado ya que crean nuevos hilos para las operaciones de archivos. Un ejemplo del representante de segunda clase es WinFile . Solo como recordatorio, la transacción actual es una función por subproceso. Inicialmente, fue un agujero en la trama, ya que el seguimiento de la creación de subprocesos fuera de proceso es bastante difícil, teniendo en cuenta la sensibilidad temporal de esta operación.
Dll de seguimiento de hilos
Afortunadamente, recibir notificaciones sincrónicas sobre la creación de subprocesos es extremadamente simple dentro del contexto del proceso de destino. Todo lo que necesitamos es crear una DLL, que propague la transacción actual a nuevos subprocesos, el cargador de ntdll
de ntdll
se encargará del resto. Cada vez que llega un nuevo hilo al proceso, activará nuestro punto de entrada con el parámetro DLL_THREAD_ATTACH
. Al implementar esta funcionalidad, arreglé la compatibilidad con un montón de programas diferentes.
* Estrictamente hablando, esta devolución de llamada no ocurre en todas las condiciones posibles. De vez en cuando, verá uno o dos hilos auxiliares dando vueltas sin una transacción. La mayoría de las veces, estos son los subprocesos del grupo de trabajo del cargador de módulos en sí. La razón es que las notificaciones de DLL ocurren bajo el bloqueo del cargador , lo que implica una variedad de limitaciones, incluida la capacidad de cargar más módulos. Y eso es, de hecho, lo que esos hilos necesitan lograr, mientras que paralelizan el acceso a los archivos mientras tanto. Por lo tanto, existe una excepción para evitar puntos muertos: si la persona que llama especifica el indicador THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH
mientras crea un hilo con la ayuda de NtCreateThreadEx
, las devoluciones de llamada de notificación DLL no se activan.
Iniciando el Explorador de Windows
Desafortunadamente, todavía quedan algunos programas que no pueden manejar bien las transacciones de cambio en caliente, y Windows Explorer es uno de ellos. No puedo diagnosticar el problema de manera confiable. Es una aplicación compleja que generalmente tiene muchos identificadores abiertos, y si el contexto de una transacción invalida algunos de ellos, podría provocar un bloqueo. De todos modos, la solución universal a tales problemas es asegurarse de que el proceso se ejecute dentro de un contexto consistente desde la primera instrucción que ejecuta.
Por lo tanto, implementé una opción para realizar la inyección de DLL de inmediato al crear un nuevo proceso. Y resultó ser suficiente para arreglar el bloqueo. Sin embargo, dado que Explorer utiliza de manera intensiva COM fuera de proceso, la vista previa y algunas otras características aún no funcionan en archivos modificados.
¿Qué pasa con WoW64?
Los beneficios de compatibilidad que proporciona el subsistema Windows-on-Windows de 64 bits son sinceramente notables. Sin embargo, tener en cuenta sus detalles a menudo se vuelve tedioso durante la programación del sistema. Anteriormente mencioné que el comportamiento de Rtl[Get/Set]CurrentTransaction
vuelve un poco más complejo en este caso. Dado que dichos procesos funcionan con un tamaño distintivo de punteros que el resto del sistema, cada hilo WoW64 mantiene dos TEB asociados con él: el sistema operativo en sí mismo espera que tenga uno de 64 bits, y la aplicación requiere uno de 32 bits como bien para funcionar correctamente. Y aunque, desde la perspectiva del kernel, el TEB nativo tiene prioridad, hay un código adicional en estas funciones para garantizar que los valores correspondientes siempre coincidan. De todos modos, es esencial tener en cuenta todas estas peculiaridades al implementar una nueva funcionalidad .
Problemas sin resolver
Por triste que sea, el primer escenario de uso que se nos viene a la mente: instalar aplicaciones en este modo, no funciona bien por ahora. En primer lugar, los instaladores suelen crear procesos complementarios, y todavía no he implementado la captura de procesos secundarios en la misma transacción. Veo varias formas de hacerlo, pero podría llevar un tiempo. Otro problema importante surge cuando intentamos ejecutar archivos binarios que se desempaquetan durante la instalación y, por lo tanto, no existen en ningún otro lugar. Teniendo en cuenta que NtCreateUserProcess
y, por lo tanto, CreateProcess
, ignoran la transacción actual por alguna razón, la solución de este problema probablemente requerirá algo de creatividad, combinada con un montón de trucos sofisticados. Por supuesto, siempre podemos confiar en NtCreateProcessEx
como último recurso, pero arreglar la compatibilidad puede convertirse en una pesadilla en este caso.
Por cierto, me recuerda una historia que leí sobre un malware que logró engañar a un par de ingenuos antivirus aplicando un enfoque similar. Solía soltar una carga útil en una transacción, iniciarla y revertir los cambios para cubrir las pistas, permitiendo que el proceso se ejecute sin una imagen en el disco. Incluso si la lógica de "sin archivo - sin amenaza" suena tonta, podría no haber sido el caso para algunos AV, al menos hace unos años.
¿Qué pasa con el sandboxing?
Echa un vistazo a esta captura de pantalla a continuación. Puede ver tres programas completamente en desacuerdo sobre el contenido de la misma carpeta. Todos trabajan dentro de tres transacciones diferentes. Ese es el poder del aislamiento en la semántica de ACID.

Mi programa no es un sandbox en absoluto; carece de una pieza crucial: un límite de seguridad . Sé que algunas compañías todavía logran vender productos similares, presentándolos como verdaderos sandboxes, qué vergüenza, qué puedo decir. Y podría pensar: ¿cómo puede convertirlo en un sandbox? Incluso si es un depurador, no puede evitar de manera confiable que un proceso modifique una variable que controla la transacción, después de todo, reside en su memoria. Es bastante justo, por eso tengo que tener otro truco maravilloso en mi manga, que eventualmente me ayudará a terminar este proyecto y que no revelaré por ahora. Sí, estoy planeando crear una caja de arena completamente en modo de usuario con virtualización del sistema de archivos. Mientras tanto, usa Sandboxie y sigue experimentando con AppContainers . Estén atentos.
Repositorio del proyecto en GitHub: TransactionMaster .