Análisis estático de BIOS / UEFI o cómo obtener un gráfico de dependencia

"Terminé de forjar ayer,
Engañé dos planes ... "
... Canción VS Vysotsky ...

Hace casi 3 años (a principios de 2016), apareció el deseo de un usuario sobre el tema del proyecto UEFITool en GitHub: construir un "Gráfico de dependencia" para módulos ejecutables incluidos en BIOS / UEFI.

Incluso se produjo una pequeña discusión, como resultado de lo cual finalmente quedó claro que esta tarea no es en absoluto trivial, la funcionalidad disponible para su solución no es suficiente, las perspectivas en ese momento son nebulosas ...

Y esta pregunta permaneció en el limbo, con la perspectiva de realizarse en un futuro indefinido (pero el deseo probablemente permaneció, y la esperanza, como saben, ¡muere al final!).

Hay una sugerencia: ¡finalmente, encuentre una solución a este problema!

Define los términos


Se supone además que estamos lidiando con la arquitectura Intel 64 e IA-32.

Para determinar inequívocamente lo que decidimos construir, tendremos que tratar con más detalle el funcionamiento de las fases individuales de la operación BIOS / UEFI.

Si observa detenidamente los tipos de archivo presentados en los volúmenes de firmware de FFS , resulta que la mayoría de los archivos disponibles incluyen una sección con módulos ejecutables.

Incluso si consideramos el nuevo firmware de ASUS o ASRock, en el que puede encontrar sin esfuerzo hasta un millón y medio de archivos del tipo EFI_FV_FILETYPE_FREEFORM que contienen imágenes de diferentes formatos, sin embargo, incluso en estos firmwares hay más archivos ejecutables que archivos de otros tipos.

+--------------------------------------------------------------------------+ | File Types Information | +--------------------------------------------------------------------------+ | EFI_FV_FILETYPE_RAW = 6 | | EFI_FV_FILETYPE_FREEFORM = 83 | | EFI_FV_FILETYPE_SECURITY_CORE = 1 | | EFI_FV_FILETYPE_PEI_CORE = 1 | | EFI_FV_FILETYPE_DXE_CORE = 1 | | EFI_FV_FILETYPE_PEIM = 57 | | EFI_FV_FILETYPE_DRIVER = 196 | | EFI_FV_FILETYPE_APPLICATION = 1 | | EFI_FV_FILETYPE_SMM = 60 | | EFI_FV_FILETYPE_SMM_CORE = 1 | | EFI_FV_FILETYPE_PAD = 4 | +--------------------------------------------------------------------------+ | Total Files : = 411 | +--------------------------------------------------------------------------+ 
Un ejemplo de la composición de algún firmware ordinario (ordinario).

Aunque los archivos que contienen módulos ejecutables no están marcados en esta tabla, sin embargo, (por definición) estarán todos en esta lista, excepto los archivos con los sufijos RAW, FREEFORM y PAD.

Los archivos con el sufijo "CORE" (SECURITY_CORE, PEI_CORE y DXE_CORE) son los "núcleos" correspondientes (módulos principales de la fase correspondiente) que reciben el control de otras fases (o después del inicio), SMM_CORE es una subfase de la fase DXE y se llama durante ella. cumplimiento La APLICACIÓN se puede realizar solo a petición del usuario, no tiene un enlace específico a las fases.

Los tipos de archivos más comunes no estaban listados: PEIM (módulos de fase PEI), DRIVER (módulos de fase DXE) y SMM (módulos de subfase DXE). Los módulos CORE de las fases PEI y DXE incluyen un despachador, que controla la secuencia de los módulos de carga / arranque de la fase correspondiente.

En el ejemplo anterior, no hay opciones combinadas, no las recordaremos: aunque se encuentran en firmware real, es bastante raro. Aquellos que deseen recibir información más detallada y detallada están invitados a consultar los artículos 1 , 2 y 3 de CodeRush . Y también cite su consejo: "Para los fanáticos de la documentación original, la especificación UEFI PI siempre está disponible, todo se describe con mucho más detalle".

Cada módulo de firmware ejecutable es un módulo de formato PE + (Portable Ejecutable) o su derivado (Terse Executable: formato TE). El módulo ejecutable en formato PE + es un conjunto de datos estructurados "ligeramente" empaquetados que contienen la información que necesita el cargador para asignar este módulo a la memoria.

El formato (estructura) PE + en sí mismo no tiene ningún mecanismo de interacción entre los módulos PE + individuales. Cada módulo ejecutable después de cargar e iniciar la ejecución es un proceso autónomo independiente (bueno, ¡debería ser así!) , Es decir el módulo no debe "asumir" nada sobre lo que se está haciendo fuera de él.

La organización de la interacción entre módulos ejecutables separados de una fase UEFI se organiza mediante el módulo CORE de la fase correspondiente. Los módulos ejecutables separados pueden definir (Instalar) protocolos, solicitar (Localizar) y usar protocolos declarados por otros módulos, establecer / declarar eventos, declarar (Notificar) manejadores de eventos.

Por lo tanto, para cada módulo de firmware ejecutable, estamos interesados ​​en la presencia de los siguientes artefactos:

  1. Lista de protocolos que define este módulo. (Cada protocolo se identifica mediante un número único: guid).
  2. Lista de protocolos que usa este módulo (intenta usar).
  3. Lista de eventos que anuncia este módulo. (El evento tiene un número único: guid).
  4. Una lista de controladores de eventos presentes (implementados y se pueden instalar / inicializar) en este módulo.
Un gráfico de dependencia estática para una fase BIOS / UEFI dada se considera definido si, para cada módulo de fase ejecutable, conocemos todos los artefactos enumerados anteriormente en las secciones 1-4. (En otras palabras, si hemos definido toda la información que describe las interdependencias entre los módulos).
Consideraremos solo la opción de análisis estático, esto significa que algunos elementos del código que implementan los elementos 1-4 pueden ser inalcanzables (son fragmentos del código "inactivo") o se podrán lograr solo con ciertas opciones para datos / parámetros de entrada.

Todo lo que hemos considerado hasta ahora se basa solo en la especificación BIOS / UEFI . Y para comprender las "relaciones" de los módulos ejecutables existentes del firmware en cuestión, tendremos que profundizar en su estructura, lo que significa que deberíamos revertirlos al menos parcialmente (restaurar los algoritmos originales).

Como ya se mencionó anteriormente, el módulo ejecutable en formato PE + es solo un conjunto de estructuras para el cargador, que construye en la memoria un objeto al que se transferirá el control, y este objeto por su naturaleza consiste en instrucciones del procesador, así como datos para estas instrucciones.
Diremos que se realizó un desmontaje completo del módulo ejecutable si fuera posible resolver el problema de separar los comandos y los datos presentados en este módulo.
Al mismo tiempo, no impondremos ningún requisito sobre la estructura y los tipos de datos, es suficiente si por cada byte que pertenece a la imagen del módulo ejecutable recibido por el cargador, podemos decir claramente a cuál de las dos categorías pertenece: byte de comando o byte de datos.

La tarea de desmontar completamente el módulo ejecutable en sí mismo generalmente no es trivial, además, en el caso general, no es solucionable algorítmicamente. No entraremos en detalles sobre este tema, tampoco romperemos las lanzas, consideramos esta afirmación como un axioma.

Supongamos:

  1. Ya hemos resuelto el problema del desmontaje completo para un módulo de ejecución BIOS / UEFI específico, es decir. Logramos separar comandos y datos.
  2. Existe el código fuente del módulo en el lenguaje "C" (en el firmware BIOS / UEFI actual, los módulos se desarrollan principalmente en el lenguaje "C").

Incluso en este caso, simplemente comparar los resultados obtenidos (el texto del ensamblador es solo una representación textual de las instrucciones del procesador) con el código fuente en el lenguaje "C" requerirá casi siempre una buena experiencia / calificación, con la excepción de casos absolutamente degenerados.

Un estudio completo de ejemplos que muestran dificultades para identificar o comparar los resultados del desensamblaje con el código fuente no forma parte de nuestros planes actuales.
Consideremos solo un ejemplo cuando en la lista de ensambladores nos encontramos con el comando "Llamada indirecta" , una llamada a procedimiento implícito.

Este es un ejemplo de una llamada a procedimiento referenciada en una tabla. Una tabla que contiene enlaces a varios procedimientos es un caso típico de implementación de la presentación de interfaces de un protocolo arbitrario.

Dicha tabla no tiene que consistir únicamente en referencias a procedimientos; nadie prohíbe almacenar datos arbitrarios en esta estructura (y este es un ejemplo de una estructura típica "C").

Aquí hay una forma de dicha llamada (en lugar del registro ecx, son posibles casi todas las variantes de los registros del procesador de 32 bits):
FF 51 18 call dword ptr [ecx + 18h]
Después de obtener, después del análisis, un comando similar, es posible averiguar qué tipo de procedimiento se llama, una lista de sus parámetros, el tipo y el valor del resultado devuelto, es posible solo si conocemos el tipo de objeto (protocolo) cuya interfaz se llama mediante este comando.

Si sabemos que en el ejemplo anterior el registro "ecx" contiene un puntero (la dirección del comienzo de la tabla EFI_PEI_SERVICES), podemos recibir (presentar) este comando de la siguiente manera más comprensible y "agradable":
Llamada FF 51 18 [exx + EFI_PEI_SERVICES.InstallPpi]
La obtención de información sobre el contenido del registro que participa en el comando "Llamada indirecta" con frecuencia va más allá de las capacidades de un desensamblador "típico", cuya tarea es simplemente analizar y convertir el código binario (binario) del procesador en una forma legible para los humanos: una representación textual del comando del procesador correspondiente.

Para resolver este problema, a menudo se requiere el uso de información adicional (Meta) que no está disponible en el módulo ejecutable binario (perdido como resultado de la compilación y la vinculación; se usa en transformaciones de una representación del algoritmo a otra, pero el procesador ya no necesita ejecutar los comandos recibidos).

Si estos Metadatos aún están disponibles para nosotros desde fuentes adicionales, usándolos y realizando análisis adicionales, obtenemos una representación más comprensible (y más precisa) del comando "Llamada indirecta" .

De hecho, este análisis avanzado ya recuerda más el proceso de "descompilación", aunque el resultado no se parece al código fuente del módulo en el lenguaje "C", sin embargo, en el futuro nos referiremos a este proceso como la descompilación de comandos que son "Llamada indirecta" o " descompilación parcial

Entonces, estamos listos para determinar las condiciones suficientes para construir el gráfico de la interdependencia de los módulos de firmware ejecutables para la fase BIOS / UEFI dada:
Para obtener un gráfico de dependencia estática (cualquiera de las fases: PEI o DXE), es suficiente desmontar completamente todos los módulos ejecutables de la fase correspondiente (al menos separar todos los comandos) y descompilar los comandos de "Llamada indirecta" presentes en los módulos desmontados.
Inmediatamente hay muchas preguntas sobre cómo nuestro conocimiento de los equipos de "Llamada indirecta" está conectado con las interacciones entre módulos.
Como se mencionó anteriormente, todo el servicio de gestión de interacción es provisto por el módulo "CORE" de la fase correspondiente, y los servicios en las fases están diseñados como tablas de servicios "básicos".

Dado que los modelos de interacción entre los módulos en las fases PEI y DXE, aunque son ideológicamente (estructuralmente) similares, son técnicamente diferentes, se propone pasar de algunas consideraciones formales a considerar una construcción directa específica de un Gráfico de Dependencia Estática para la fase PEI.

Incluso podremos determinar y formular las condiciones necesarias y suficientes para la posibilidad de construir un Gráfico de Dependencia Estática para la fase PEI.

Creación de un gráfico de dependencia estática para la fase PEI


Las descripciones de la solución al problema del desmontaje completo de los módulos ejecutables de la fase PEI y la descompilación de los comandos de Llamada indirecta presentes en estos módulos están más allá del alcance de nuestra historia y no se darán en él: la presentación de este material en volumen puede exceder el tamaño de esta obra.

Es posible que con el tiempo esto suceda como un material separado, pero por ahora, sepa cómo.

Solo notamos que el uso de Metadatos, más la presencia de una determinada estructura para construir código binario, hace posible en la práctica desmontar completamente los módulos ejecutables BIOS / UEFI. La prueba formal de este hecho no se supone ahora ni en el futuro. Al menos en el análisis / procesamiento de más de cien (100) BIOS / UEFI de varios fabricantes, no hubo ejemplos en los que no fuera posible el desmontaje completo .

Además, solo resultados específicos (con explicaciones: qué, cómo y cuánto ...).

La estructura EFI_PEI_SERVICES es la estructura básica de la fase PEI, que se pasa como parámetro al punto de entrada de cada módulo PEI y contiene enlaces a los servicios básicos necesarios para que funcionen los módulos PEI.

Solo nos interesarán los campos ubicados al comienzo de la estructura:



Un fragmento de una estructura real de tipo EFI_PEI_SERVICES en el desensamblador IDA Pro.

Y así es como aparece en el código fuente en el lenguaje "C" (recuerde, esto es solo un fragmento de la estructura):

 struct EFI_PEI_SERVICES { EFI_TABLE_HEADER Hdr; EFI_PEI_INSTALL_PPI InstallPpi; EFI_PEI_REINSTALL_PPI ReInstallPpi; EFI_PEI_LOCATE_PPI LocatePpi; EFI_PEI_NOTIFY_PPI NotifyPpi; //...      ... }; 

Al comienzo de la estructura EFI_PEI_SERVICES, como en todas las tablas de servicio "básicas" (Tablas de servicios), se encuentra la estructura EFI_TABLE_HEADER. Los valores presentados en esta estructura de encabezado nos permiten afirmar inequívocamente que si la estructura EFI_PEI_SERVICES en sí misma está realmente presente en el fragmento del desensamblador (consulte el campo "Firma HDR"), ¡al menos la plantilla de esta estructura!

 struct EFI_TABLE_HEADER { UINT64 Signature; UINT32 Revision; UINT32 HeaderSize; UINT32 CRC32; UINT32 Reserved; }; 

En el camino, podemos establecer que el firmware se estaba desarrollando en un momento en que la versión de la especificación UEFI PI era 1.2, cuyo período de relevancia era de 2009 a 2013, pero en este momento (principios de 2019), la versión actual de la especificación ya ha crecido (literalmente creció el otro día) a la versión 1.7.

Desde el campo "Hdr.HeaderSize", puede determinar que la longitud total de la estructura es 78 h (y esta no es la longitud del encabezado, como su nombre lo indica, sino la longitud de toda la estructura de EFI_PEI_SERVICES).

Las interfaces EFI_PEI_SERVICES se dividen en 7 categorías / clases. Solo los enumeramos:

  1. Servicios PPI.
  2. Servicios de modo de arranque.
  3. Servicios HOB.
  4. Servicios de volumen de firmware.
  5. Servicios de memoria PEI.
  6. Servicios de código de estado.
  7. Restablecer servicios.

Toda narración adicional estará directamente relacionada con los procedimientos que pertenecen a la categoría / clase de Servicios PPI, destinados a la organización de la interacción entre módulos de los módulos ejecutables de la fase PEI.

Y solo hay cuatro para la fase PEI.

En general, no es necesario adivinar el propósito de cada una de las interfaces: la funcionalidad está completamente determinada por el nombre de la interfaz, todos los detalles están en la especificación .

Los siguientes son prototipos de estos procedimientos:

 typedef EFI_STATUS (__cdecl *EFI_PEI_INSTALL_PPI)( const EFI_PEI_SERVICES **PeiServices, const EFI_PEI_PPI_DESCRIPTOR *PpiList); typedef EFI_STATUS (__cdecl *EFI_PEI_REINSTALL_PPI)( const EFI_PEI_SERVICES **PeiServices, const EFI_PEI_PPI_DESCRIPTOR *OldPpi, const EFI_PEI_PPI_DESCRIPTOR *NewPpi); typedef EFI_STATUS (__cdecl *EFI_PEI_LOCATE_PPI)( const EFI_PEI_SERVICES **PeiServices, const EFI_GUID *Guid, UINTN Instance, EFI_PEI_PPI_DESCRIPTOR **PpiDescriptor, void **Ppi); typedef EFI_STATUS (__cdecl *EFI_PEI_NOTIFY_PPI)( const EFI_PEI_SERVICES **PeiServices, const EFI_PEI_NOTIFY_DESCRIPTOR *NotifyList); 

Solo notamos que además de los comandos de "Llamada indirecta" que invocan los procedimientos / interfaces de la clase "Servicios PPI", es posible una llamada explícita (directa - no tabular) a estos procedimientos, lo que a veces ocurre en los módulos ejecutivos, donde se define / crea la estructura EFI_PEI_SERVICES.

Te diré un pequeño secreto: curiosamente, aunque esta es la tabla de servicios "básica" para la fase PEI, sin embargo, como muestra la práctica, se puede definir no solo en el módulo PEI_CORE.

En la naturaleza real, hay firmwares en los que la estructura EFI_PEI_SERVICES se definió / formó y se usó en varios módulos, y de ninguna manera fueron copias del módulo PEI_CORE.

Por lo tanto, las siguientes opciones de código son posibles:

 seg000:00785F0D B8 8C A6 78+ mov eax, offset ppiList_78A68C seg000:00785F12 50 push eax ; PpiList seg000:00785F13 57 push edi ; PeiServices seg000:00785F14 89 86 40 0E+ mov [esi+0E40h], eax seg000:00785F1A E8 70 FC FF+ call InstallPpi 

Un ejemplo de una llamada explícita al procedimiento "InstallPpi".

 seg000:00787CBB 8B 4D FC mov ecx, [ebp+PeiServices] seg000:00787CBE 50 push eax ; PpiList seg000:00787CBF C7 00 10 00+ mov dword ptr [eax], 80000010h seg000:00787CC5 C7 43 3C A8+ mov dword ptr [ebx+3Ch], offset guid_78A9A8 seg000:00787CCC 8B 11 mov edx, [ecx] seg000:00787CCE 51 push ecx ; PeiServices seg000:00787CCF FF 52 18 call [edx+EFI_PEI_SERVICES.InstallPpi] 

Un ejemplo de una llamada implícita a la interfaz InstallPpi.

 FF 51 18 call dword ptr [ecx+18h] FF 51 18 call [ex+EFI_PEI_SERVICES.InstallPpi] FF 51 1 call dword ptr [ecx+1Ch] FF 51 1C call [ex+EFI_PEI_SERVICES.ReInstallPpi] FF 51 20 call dword ptr [ecx+20h] FF 51 20 call [ex+EFI_PEI_SERVICES.LocatePpi] FF 51 24 call dword ptr [ecx+24h] FF 51 24 call [ex+EFI_PEI_SERVICES.NotifyPpi] 
Ejemplos de llamadas de interfaz implícitas antes y después de la autenticación.

Observamos una característica: en el caso de la fase PEI para la arquitectura IA-32, las interfaces de la clase de servicios PPI tienen compensaciones de 18h, 1Ch, 20h y 24h.

Y ahora declaramos la siguiente declaración:
Para construir un Gráfico de Dependencia Estática de la fase PEI, es necesario y suficiente desmontar completamente todos los módulos ejecutables de la fase (al menos separar todos los comandos) y descompilar los comandos de "Llamada indirecta" con compensaciones 18h, 1Ch, 20h, 24h en módulos desmontados.
De hecho, hemos formulado completamente un algoritmo para resolver el problema, y ​​tan pronto como logramos aislar todas las llamadas a las interfaces / procedimientos de la clase de Servicios PPI, solo queda determinar qué parámetros se pasan a estas llamadas. La tarea puede no ser la más trivial, pero, como lo ha demostrado la práctica, es completamente solucionable, tenemos todos los datos para esto.

Y ahora ejemplos reales de datos reales para módulos de fase PEI reales. No indicamos a sabiendas qué resultados de BIOS / UEFI de la empresa se obtuvieron, solo damos ejemplos de cómo se ven.

Dos ejemplos de descripciones de módulos PEIM con información completa sobre el uso de las interfaces de servicios PPI en estos módulos


  -- File 04-047/0x02F/: "TcgPlatformSetupPeiPolicy" : [007CCAF0 - 007CD144] DEPENDENCY_START EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI DEPENDENCY_END Install Protocols: [1] TCG_PLATFORM_SETUP_PEI_POLICY Locate Protocols: [2] EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI 
 -- File 04-048/0x030/: "TcgPei" : [007CD160 - 007CF5DE] DEPENDENCY_START EFI_PEI_MASTER_BOOT_MODE_PEIM_PPI EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI AND DEPENDENCY_END Install Protocols: [1] AMI_TCG_PLATFORM_PPI [2] EFI_PEI_TCG_PPI [2] PEI_TPM_PPI Locate Protocols: [1] EFI_PEI_TCG_PPI [1] EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI [1] TCG_PLATFORM_SETUP_PEI_POLICY [5] PEI_TPM_PPI Notify Events: [1] AMI_TCM_CALLBACK ReInstall Protocols: [1] PEI_TPM_PPI 

Listas de protocolos por tipos de interfaces en las que se utilizaron.


A continuación, debajo de los spoilers, hay ejemplos abreviados de listas de protocolos PPIM para cada una de las interfaces de la clase de Servicios PPI.

El formato de las listas es el siguiente:
 El |  número de serie |  name_PPI |  guid_PPI |  nombre_ejecutable: nombre de usuario |

***** Instale 99 Ppi en "Firmware"


***** Localice 194 Ppi en "Firmware"


***** Vuelva a instalar 5 Ppi en "Firmware"


***** Notifique 29 Ppi en "Firmware"


La lista final de todas las guías de los protocolos a los que se hace referencia en un BIOS / UEFI particular con una leyenda que indica en qué "Servicios PPI" se encuentran estos protocolos


A continuación se muestra una lista de spoilers de 97 PPi-guías encontradas y utilizadas explícitamente en un firmware específico, cuyos datos se proporcionaron anteriormente.

Cada elemento de la lista está precedido por una leyenda, que refleja todos los tipos de uso de un protocolo en particular.

 "D" - in DEPENDENCY section used "I" - in "InstallPpi" functions used "L" - in "LocatePpi" functions used "R" - in "ReInstallPpi" functions used "N" - in "NotifyPpi" functions used 

***** Lista Ppi en "Firmware"




Los siguientes intervalos de la lista de protocolos son notables en este BIOS / UEFI:

  1. No. 38-50.
    Definición de protocolos / eventos (InstallPpi) que ningún módulo utiliza.
  2. No. 87-95.
    Intente solicitar protocolos que no fueron instalados por ningún módulo de este firmware.
  3. No. 96-97.
    Dos eventos "Notificar", para los cuales ningún módulo se molestó en declarar la interfaz correspondiente, respectivamente, aunque estos procedimientos se declaran en módulos ejecutables, nunca funcionarán.

Conclusión


  • Se obtuvieron resultados similares a los anteriores para BIOS / UEFI de varios fabricantes, por lo que todos los ejemplos son anónimos.
  • De hecho, se resolvieron tareas más generales de revertir los algoritmos de los módulos ejecutables BIOS / UEFI, y el gráfico resultante es un resultado secundario, una especie de bonificación adicional.
  • La solución correcta de la tarea "Obtención del gráfico de dependencia estática" para los módulos ejecutables BIOS / UEFI requiere un análisis estático del código binario, que incluye un desmontaje completo de los módulos ejecutables y la descompilación parcial de los comandos de llamada indirecta de estos módulos.

Source: https://habr.com/ru/post/440052/


All Articles