 | "Eu terminei de forjar ontem, Eu enganei dois planos ... " ... Canção VS Vysotsky ... |
Há quase três anos (no início de 2016), o desejo de um usuário apareceu na
edição do projeto UEFITool no GitHub: criar um "Gráfico de Dependência" para módulos executáveis incluídos no BIOS / UEFI.
Mesmo uma pequena discussão se seguiu, como resultado do qual finalmente ficou claro que essa tarefa não é de modo algum trivial, a funcionalidade disponível para sua solução não é suficiente, as perspectivas naquele momento são nebulosas ...
E essa questão permaneceu no limbo, com a perspectiva de realização em um futuro indefinido (mas o desejo provavelmente permaneceu e a esperança, como você sabe, morre por último!).
Há uma sugestão: finalmente, encontre uma solução para este problema!
Defina os termos
Supõe-se ainda que estamos lidando com as arquiteturas Intel 64 e IA-32.
Para determinar inequivocamente o que decidimos construir, teremos que lidar com mais detalhes com o funcionamento das fases individuais da operação BIOS / UEFI.
Se você observar cuidadosamente os tipos de arquivo apresentados nos volumes de firmware do
FFS , a maioria dos arquivos existentes inclui uma seção com módulos executáveis.
Mesmo se considerarmos o novo firmware da ASUS ou ASRock, no qual você pode encontrar facilmente até meia centena de arquivos do tipo EFI_FV_FILETYPE_FREEFORM contendo imagens de diferentes formatos, no entanto, mesmo nesses firmwares, existem mais arquivos executáveis do que arquivos de outros 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 | +--------------------------------------------------------------------------+
Um exemplo da composição de algum firmware comum (comum).Embora os arquivos que contêm módulos executáveis não estejam marcados nesta tabela, eles (por definição) estarão todos nesta lista, exceto os arquivos com os sufixos RAW, FREEFORM e PAD.
Arquivos com o sufixo "CORE" (SECURITY_CORE, PEI_CORE e DXE_CORE) são os "kernels" correspondentes (módulos principais da fase correspondente) que recebem o controle de outras fases (ou após o início), SMM_CORE é uma sub-fase da fase DXE e é chamada durante ela. realização. O APLICATIVO pode ser realizado apenas a pedido do usuário, não possui uma ligação específica às fases.
Os tipos de arquivos mais comuns não foram listados: PEIM (módulos de fase PEI), DRIVER (módulos de fase DXE) e SMM (módulos de sub-fase DXE). Os módulos CORE das fases PEI e DXE incluem um despachante, que controla a sequência dos módulos de carregamento / partida da fase correspondente.
No exemplo acima, não há opções combinadas, não nos lembraremos delas: embora elas sejam encontradas em firmware real, é bastante raro. Aqueles que desejam receber informações mais detalhadas e detalhadas são convidados a consultar os artigos
1 ,
2 ,
3 do
CodeRush . E também cite seu conselho: "Para os fãs da documentação original,
a especificação UEFI PI está sempre disponível, tudo é descrito com muito mais detalhes".
Cada módulo de firmware executável é um módulo de formato PE + (Portable Executable) ou seu derivado (Terse Executable: formato TE). O módulo executável no formato PE + é um conjunto de dados estruturados "levemente", contendo as informações necessárias ao carregador para mapear esse módulo na memória.
O próprio formato PE + (estrutura) não possui nenhum mecanismo de interação entre os módulos individuais PE +. Cada módulo executável após o carregamento e o início da execução é um processo autônomo independente
(bem, deve ser assim!) , Ou seja, o módulo não deve "assumir" nada sobre o que está sendo feito fora dele.
A organização da interação entre módulos executáveis separados de uma fase UEFI é organizada por meio do módulo CORE da fase correspondente. Módulos executáveis individuais podem definir (Instalar) protocolos, solicitar (Localizar) e usar protocolos declarados por outros módulos, definir / declarar eventos e declarar (Notificar) manipuladores de eventos.
Portanto, para cada módulo de firmware executável, estamos interessados na presença dos seguintes artefatos:
- Lista de protocolos que este módulo define. (Cada protocolo é identificado por um número único - guia).
- Lista de protocolos que este módulo usa (tenta usar).
- Lista de eventos que este módulo anuncia. (O evento possui um número único - guia).
- Uma lista de manipuladores de eventos presentes (implementados e podem ser instalados / inicializados) neste módulo.
Um gráfico de dependência estática para uma determinada fase BIOS / UEFI é considerado definido se, para cada módulo de fase executável, conhecermos todos os artefatos listados acima nas seções 1-4. (Em outras palavras, se tivermos definido todas as informações que descrevem as interdependências entre os módulos).
Consideraremos apenas a opção de análise estática, isso significa que alguns elementos do código que implementam os itens 1-4 podem ser inatingíveis (são fragmentos do código "morto") ou serão alcançáveis apenas com determinadas opções para dados / parâmetros de entrada.
Tudo o que consideramos até agora é baseado apenas na especificação
BIOS / UEFI . E, para entender os “relacionamentos” dos módulos executáveis existentes do firmware em questão, teremos que nos aprofundar em sua estrutura, o que significa que devemos revertê-los pelo menos parcialmente (restaurar os algoritmos originais).
Como já mencionado acima, o módulo executável no formato PE + é apenas um conjunto de estruturas para o carregador, construindo na memória um objeto para o qual o controle será transferido, e esse objeto, por sua natureza, consiste em instruções do processador e dados para essas instruções.
Diremos que uma desmontagem completa do módulo executável foi feita se fosse possível resolver o problema de separar os comandos e dados apresentados neste módulo.
Ao mesmo tempo, não impomos nenhum requisito à estrutura e aos tipos de dados, basta que, para cada byte pertencente à imagem do módulo executável recebido pelo carregador, possamos dizer claramente a qual das duas categorias pertence: byte de comando ou byte de dados.
A tarefa de
desmontar completamente o próprio módulo executável geralmente não é trivial; além disso, no caso geral, é algoritmicamente insolúvel. Não entraremos em detalhes desta questão, nem romperemos as lanças, consideramos essa afirmação como um axioma.
Suponha:
- Já resolvemos o problema da desmontagem completa de um módulo de execução BIOS / UEFI específico, ou seja, conseguimos separar comandos e dados.
- Existe o código fonte do módulo na linguagem "C" (no firmware atual BIOS / UEFI, os módulos são desenvolvidos principalmente apenas na linguagem "C").
Mesmo neste caso, simplesmente comparar os resultados obtidos (o texto do assembler é apenas uma representação textual das instruções do processador) com o código-fonte na linguagem “C” exigirá quase sempre boa experiência / qualificação, com exceção de casos absolutamente degenerados.
Um estudo completo de exemplos mostrando dificuldades na identificação ou comparação dos resultados da desmontagem com o código fonte não faz parte dos nossos planos atuais.
Vamos considerar apenas um exemplo quando, na lista do assembler, encontramos o comando
"Indirect Call" - uma chamada de procedimento implícita.
Este é um exemplo de uma chamada de procedimento referenciada em uma tabela. Uma tabela contendo links para vários procedimentos é um caso típico de implementação da apresentação de interfaces de um protocolo arbitrário.
Essa tabela não precisa consistir apenas de referências a procedimentos; ninguém proíbe o armazenamento de dados arbitrários nessa estrutura (e este é um exemplo de uma estrutura "C" típica).
Aqui está uma forma de chamada (em vez do registro ecx, quase todas as variantes dos registradores de processador de 32 bits são possíveis):
FF 51 18 chama o dword ptr [ecx + 18h]
Tendo obtido, após análise, um comando semelhante, é possível descobrir que tipo de procedimento está sendo chamado, uma lista de seus parâmetros, o tipo e o valor do resultado retornado, só é possível se conhecermos o tipo de objeto (protocolo) cuja interface é chamada por este comando.
Se sabemos que no exemplo anterior o registro "ecx" contém um ponteiro (o endereço do início da tabela EFI_PEI_SERVICES), podemos receber (apresentar) esse comando da seguinte maneira mais compreensível e "agradável":
Chamada FF 51 18 [exx + EFI_PEI_SERVICES.InstallPpi]
A obtenção de informações sobre o conteúdo do registro que participa do comando
"Chamada indireta" costuma ir além dos recursos de um desmontador "típico", cuja tarefa é simplesmente analisar e converter o código binário do processador em uma forma legível por humanos - uma representação textual do comando do processador correspondente.
Para resolver esse problema, geralmente é necessário o uso de informações adicionais (Meta) que não estão disponíveis no módulo executável binário (perdidas como resultado da compilação e vinculação - é usado nas transformações de uma representação do algoritmo para outra, mas o processador não precisa mais executar os comandos recebidos).
Se esses metadados ainda estiverem disponíveis a partir de fontes adicionais, usando-os e realizando análises adicionais, obteremos uma representação mais compreensível (e mais precisa) do comando
"Chamada indireta" .
De fato, essa análise avançada já lembra mais o processo de "descompilação", embora o resultado não se pareça com o código-fonte do módulo na linguagem "C", no entanto, no futuro, nos referiremos a esse processo como
descompilação de comandos que são "chamada indireta" ou
" descompilação parcial .
Portanto, estamos prontos para determinar as condições suficientes para construir o gráfico da interdependência dos módulos de firmware executável para a fase BIOS / UEFI especificada:
Para obter um gráfico de dependência estática (qualquer uma das fases - PEI ou DXE), é suficiente desmontar completamente todos os módulos executáveis da fase correspondente (pelo menos separar todos os comandos) e descompilar os comandos "Chamada indireta" presentes nos módulos desmontados.
Há imediatamente muitas perguntas sobre como o nosso conhecimento das equipes de
"Chamada indireta" está conectado às interações entre módulos.
Como mencionado acima, todo o serviço de gerenciamento de interação é fornecido pelo módulo "CORE" da fase correspondente, e os serviços nas fases são projetados como tabelas de serviço "básicas".
Como os modelos de interação entre os módulos nas fases PEI e DXE, embora ideologicamente (estruturalmente) semelhantes, ainda sejam tecnicamente diferentes, propõe-se passar de algumas considerações formais para considerar uma construção direta específica de um
Gráfico de Dependência Estática para a fase PEI.
Poderemos até determinar e formular as condições
necessárias e suficientes para a possibilidade de construir um
gráfico de dependência estática para a fase PEI.
Construindo um gráfico de dependência estática para a fase PEI
Descrições da solução para o problema da
desmontagem completa de módulos executáveis da fase PEI e descompilação dos comandos de
chamada indireta presentes nesses módulos estão além do escopo de nossa história e não serão divulgadas - a apresentação desse material em volume pode exceder o tamanho dessa obra.
É possível que com o tempo isso aconteça como um material separado, mas por enquanto - saiba como.
Observamos apenas que o uso de metadados, além da presença de uma certa estrutura para a construção de código binário, possibilita na prática
desmontar completamente os módulos executáveis BIOS / UEFI. Prova formal desse fato não é suposta agora ou no futuro. Pelo menos na análise / processamento de mais de cem (100) BIOS / UEFI de vários fabricantes, não havia exemplos em que
a desmontagem completa não
fosse possível.
Além disso, apenas resultados específicos (com explicações: o que, como e quanto ...).
A estrutura EFI_PEI_SERVICES é a estrutura básica da fase PEI, que é passada como parâmetro para o ponto de entrada de cada módulo PEI e contém links para os serviços básicos necessários para o funcionamento dos módulos PEI.
Estaremos interessados apenas nos campos localizados no início da estrutura:
Um fragmento de uma estrutura real do tipo EFI_PEI_SERVICES no desmontador do IDA Pro.E é assim que aparece no código-fonte na linguagem "C" (lembre-se, isso é apenas um fragmento da estrutura):
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;
No início da estrutura EFI_PEI_SERVICES, como em todas as tabelas de serviço "básicas" (tabelas de serviços), está a estrutura EFI_TABLE_HEADER. Os valores apresentados nesta estrutura de cabeçalho nos permitem afirmar inequivocamente que, se a própria estrutura EFI_PEI_SERVICES está realmente presente no fragmento do desmontador (consulte o campo "Hdr.Signature"), pelo menos o modelo dessa estrutura!
struct EFI_TABLE_HEADER { UINT64 Signature; UINT32 Revision; UINT32 HeaderSize; UINT32 CRC32; UINT32 Reserved; };
Ao longo do caminho, podemos estabelecer que o firmware estava sendo desenvolvido no momento em que a versão da especificação UEFI PI era 1.2, cujo período de relevância foi de 2009 a 2013, mas, no momento (início de 2019), a versão atual da especificação já havia crescido (literalmente aumentado no outro dia). para a versão 1.7.
No campo "Hdr.HeaderSize", pode-se determinar que o comprimento total da estrutura é 78h (e esse não é o comprimento do cabeçalho, como o nome indica, mas o comprimento de toda a estrutura de EFI_PEI_SERVICES).
As interfaces EFI_PEI_SERVICES são divididas em 7 categorias / classes. Apenas as listamos:
- Serviços PPI.
- Serviços do modo de inicialização.
- Serviços HOB.
- Serviços de volume de firmware.
- Serviços de memória PEI.
- Serviços de código de status.
- Redefinir serviços.
Toda a narração adicional estará diretamente relacionada aos procedimentos pertencentes à categoria / classe de Serviços PPI, destinados à organização da interação entre módulos dos módulos executáveis da fase PEI.
E há apenas quatro para a fase PEI.
Em geral, não há necessidade de adivinhar o objetivo de cada uma das interfaces: a funcionalidade é completamente determinada pelo nome da interface, todos os detalhes estão na
especificação .
A seguir, estão os protótipos desses procedimentos:
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);
Observamos apenas que, além dos comandos
"Chamada indireta" que invocam os procedimentos / interfaces da classe "Serviços PPI", é possível uma chamada explícita (direta - não tabular) a esses procedimentos, o que às vezes acontece nos módulos executivos, onde a estrutura EFI_PEI_SERVICES é definida / criada.
Vou lhe contar um pequeno segredo: estranhamente, embora essa seja a tabela de serviços "básica" para a fase PEI, no entanto, como mostra a prática, ela pode ser definida não apenas no módulo PEI_CORE.
Na natureza real, existem firmwares nos quais a estrutura EFI_PEI_SERVICES foi definida / formada e usada em vários módulos, e essas não foram de forma alguma cópias do módulo PEI_CORE.
Assim, as seguintes opções de código são possíveis:
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
Um exemplo de uma chamada explícita para o procedimento "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]
Um exemplo de uma chamada implícita para a interface 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]
Exemplos de chamadas implícitas da interface antes e depois da autenticação.Observamos um recurso característico: no caso da fase PEI para a arquitetura IA-32, as interfaces da classe Serviços PPI têm deslocamentos de 18h, 1Ch, 20h e 24h.
E agora declaramos a seguinte declaração:
Para criar um gráfico de dependência estática da fase PEI, é necessário e suficiente desmontar completamente todos os módulos executáveis da fase (pelo menos separar todos os comandos) e descompilar os comandos "Chamada Indireta" com os desvios 18h, 1Ch, 20h, 24h nos módulos desmontados.
De fato, formulamos completamente um algoritmo para resolver o problema e, assim que conseguimos isolar todas as chamadas para as interfaces / procedimentos da classe PPI Services, resta apenas determinar quais parâmetros são passados para essas chamadas. A tarefa pode não ser a mais trivial, mas, como a prática demonstrou, é completamente solucionável, temos todos os dados para isso.
E agora exemplos reais de dados reais para módulos reais da fase PEI. Não indicamos conscientemente quais resultados de BIOS / UEFI da empresa foram obtidos, apenas dê exemplos de como eles se parecem.
Dois exemplos de descrições de módulos PEIM com informações completas sobre o uso das interfaces de Serviços PPI nesses 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
Lista de protocolos por tipos de interfaces nas quais eles foram usados
Abaixo, nos spoilers, há exemplos abreviados de listas de protocolos PPIM para cada uma das interfaces da classe Serviços PPI.
O formato das listas é o seguinte:
| número de série | name_PPI | guid_PPI | executable_name: nome de usuário |
***** Instale 99 ppi no "Firmware" ***** Localize 194 Ppi em "Firmware" ***** Reinstale o 5 Ppi no "Firmware" ***** Notifique 29 Ppi em "Firmware" A lista final de todos os guias dos protocolos mencionados em um BIOS / UEFI específico com uma legenda indicando em quais "Serviços PPI" esses protocolos são encontrados
Abaixo está uma lista de spoilers de 97 PPi-guids encontrados e usados explicitamente em um firmware específico, cujos dados foram fornecidos anteriormente.
Cada item da lista é precedido por uma legenda, que reflete todos os tipos de uso de um protocolo específico.
"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
***** Listar Ppi em "Firmware" Os seguintes intervalos da lista de protocolos são dignos de nota neste BIOS / UEFI:
- No. 38-50.
Definindo protocolos / eventos (InstallPpi) que não são usados por nenhum módulo. - 87-95.
Tente solicitar protocolos que não foram instalados por nenhum módulo deste firmware.
- No. 96-97.
Dois eventos “Notify”, para os quais nenhum módulo se preocupou em declarar a interface correspondente, respectivamente, embora esses procedimentos sejam declarados em módulos executáveis, eles nunca funcionarão.
Conclusão
- Resultados semelhantes aos acima foram obtidos para BIOS / UEFI de vários fabricantes, razão pela qual todos os exemplos são anônimos.
- De fato, foram resolvidas tarefas mais gerais de reverter os algoritmos dos módulos executáveis BIOS / UEFI, e o gráfico resultante é um resultado colateral, uma espécie de bônus adicional.
A solução correta da tarefa "Obtendo gráfico de dependência estática" para módulos executáveis BIOS / UEFI requer uma análise estática do código binário, que inclui a desmontagem completa dos módulos executáveis e a descompilação parcial dos comandos de chamada indireta desses módulos.