Captura de tela da interface do desmontador do IDA ProO IDA Pro é um famoso desmontador que tem sido usado por pesquisadores de segurança da informação em todo o mundo há muitos anos. Nós da Positive Technologies também usamos essa ferramenta. Além disso, pudemos desenvolver nosso próprio
módulo de processador desmontador para a arquitetura do microprocessador NIOS II , o que aumenta a velocidade e a conveniência da análise de código.
Hoje vou contar sobre a história deste projeto e mostrar o que aconteceu no final.
Antecedentes
Tudo começou em 2016, quando tivemos que desenvolver nosso próprio módulo de processador para analisar o firmware em uma tarefa. O desenvolvimento foi conduzido do zero no manual
Nios II Classic Processor Reference Guide , que era o mais relevante. No total, esse trabalho levou cerca de duas semanas.
O módulo do processador foi desenvolvido para a versão IDA 6.9. Por velocidade, o IDA Python foi escolhido. No local em que os módulos do processador residem - o subdiretório procs dentro do diretório de instalação do IDA Pro - existem três módulos Python: msp430, ebc, spu. Neles, você pode ver como o módulo está organizado e como a funcionalidade básica de desmontagem pode ser implementada:
- analisar instruções e operandos,
- sua simplificação e exibição,
- criando compensações, referências cruzadas, bem como o código e os dados aos quais eles se referem
- processamento de construções de interruptores,
- manipulação de manipulações com a pilha e as variáveis da pilha.
Aproximadamente essa funcionalidade foi implementada na época. Felizmente, a ferramenta foi útil no processo de trabalhar em outra tarefa, durante a qual, um ano depois, foi ativamente usada e refinada.
Decidi compartilhar a experiência de criar o módulo do processador com a comunidade no PHDays 8. A apresentação despertou interesse (o relatório em vídeo foi
publicado no site do PHDays), até o criador do IDA Pro Ilfak Gilfanov. Uma de suas perguntas era se o suporte à versão 7 do IDA Pro havia sido implementado. Naquela época, não estava lá, mas após o desempenho prometi fazer um lançamento apropriado do módulo. Foi aqui que a diversão começou.
Agora, o
manual mais recente
da Intel , usado para verificar e verificar se há erros. Revisei significativamente o módulo, adicionei vários novos recursos, incluindo a solução de problemas que não podiam ser derrotados antes. Bem, é claro, eu adicionei suporte para a 7ª versão do IDA Pro. Aqui está o que aconteceu.
Modelo de software NIOS II
O NIOS II é um processador de software desenvolvido para os FPGAs da Altera (agora parte da Intel). Do ponto de vista dos programas, ele possui os seguintes recursos: ordem de bytes do little endian, espaço de endereço de 32 bits, conjunto de instruções de 32 bits, ou seja, 4 bytes, 32 registros gerais e 32 de propósito especial são usados para codificar cada comando.
Desmontagem e referências de código
Então, abrimos um novo arquivo no IDA Pro, com firmware para o processador NIOS II. Após a instalação do módulo, nós o veremos na lista de processadores IDA Pro. A escolha do processador é mostrada na figura.

Suponha que o módulo ainda não tenha implementado uma análise básica de comandos. Dado que cada comando tem 4 bytes, agrupamos os bytes em quatro, então tudo ficará mais ou menos assim.

Após implementar a funcionalidade básica das instruções de decodificação e operandos, exibi-las na tela e analisar as instruções de transferência de controle, o conjunto de bytes definido no exemplo acima é convertido no código a seguir.

Como pode ser visto no exemplo, referências cruzadas também são geradas a partir de comandos de transferência de controle (nesse caso, você pode ver o salto condicional e a chamada de procedimento).
Uma das propriedades úteis que podem ser implementadas nos módulos do processador são os comentários do comando. Se você desabilitar a saída dos valores de bytes e habilitar a saída dos comentários, a mesma seção de código já terá esta aparência.

Aqui, se você encontrou o código assembler de uma nova arquitetura pela primeira vez, usando comentários, você pode entender o que está acontecendo. Além disso, os exemplos de código estarão na mesma forma - com comentários, para não olhar para o manual do NIOS II, mas para entender imediatamente o que está acontecendo na seção de código, que é fornecida como exemplo.
Pseudo-instruções e simplificação de comandos
Alguns comandos do NIOS II são pseudo-instruções. Não existem opcodes separados para essas equipes, e eles mesmos são modelados como casos especiais de outras equipes. No processo de desmontagem, a simplificação das instruções é realizada - a substituição de certas combinações por pseudo-instruções. As pseudo-instruções no NIOS II geralmente podem ser divididas em quatro tipos:
- quando uma das fontes é zero (r0) e pode ser removida da consideração,
- quando a equipe tem um valor negativo e a equipe é substituída pelo oposto,
- quando a condição é revertida,
- quando o deslocamento de 32 bits é inserido em duas equipes em partes (a mais nova e a mais antiga) e isso é substituído por um comando.
Os dois primeiros tipos foram implementados, pois a substituição da condição não oferece nada de especial e as compensações de 32 bits têm mais opções do que as apresentadas no manual.
Por exemplo, para a primeira visualização, considere o código.

É visto que o uso do registro zero nos cálculos é frequentemente encontrado aqui. Se você olhar atentamente para este exemplo, notará que todos os comandos, exceto a transferência de controle, são opções para simplesmente inserir valores em registros específicos.
Após implementar o processamento das pseudo instruções, obtemos a mesma seção de código, mas agora parece mais legível e, em vez de variações dos comandos ou e adicionar, obtemos variações do comando mov.

Variáveis de pilha
A arquitetura do NIOS II suporta a pilha e, além do ponteiro sp da pilha, também há um ponteiro para o quadro da pilha fp. Considere um exemplo de um pequeno procedimento que usa uma pilha.

Obviamente, o espaço é reservado para variáveis locais na pilha. Pode-se supor que o registrador ra seja armazenado na variável da pilha e depois restaurado a partir dele.
Depois de adicionar funcionalidade ao módulo que rastreia alterações no ponteiro da pilha e cria variáveis de pilha, o mesmo exemplo será semelhante a este.

Agora, o código parece um pouco mais claro e você já pode nomear as variáveis da pilha e analisar sua finalidade, seguindo as referências cruzadas. A função no exemplo é do tipo __fastcall e seus argumentos nos registradores r4 e r5 são empurrados para a pilha para chamar um subprocedimento que é do tipo _stdcall.
Números e compensações de 32 bits
A peculiaridade do NIOS II é que em uma operação, ou seja, ao executar um único comando, é possível registrar no máximo um valor direto de 2 bytes (16 bits) de tamanho. Por outro lado, os registros do processador e o espaço de endereço são de 32 bits, ou seja, para endereçamento, 4 bytes devem ser inseridos no registro.
Para resolver esse problema, deslocamentos de duas partes são usados. Um mecanismo semelhante é usado nos processadores do PowerPC: o deslocamento consiste em duas partes, a mais antiga e a mais nova, e é inserido no registro por dois comandos. No PowerPC, é o seguinte.

Nessa abordagem, os links cruzados são formados pelas duas equipes, embora, na verdade, o endereço esteja configurado no segundo comando. Às vezes, isso pode ser um incômodo ao contar o número de referências cruzadas.
As propriedades de deslocamento da parte antiga usam o tipo não-padrão HIGHA16, às vezes o tipo HIGH16 é usado, para a parte mais nova - LOW16.

Não há nada complicado no cálculo de números de duas partes de 32 bits. Dificuldades surgem na formação de operandos como compensações para duas equipes separadas. Todo esse processamento cai no módulo do processador. Não há exemplos de como implementar isso (especialmente em Python) no IDA SDK.
No relatório sobre PHDays, os vieses permaneceram como um problema não resolvido. Para resolver o problema, trapaceamos: deslocamento de 32 bits apenas da parte mais jovem - na base. A base é calculada como a parte mais antiga, deslocada para a esquerda em 16 bits.

Com essa abordagem, uma referência cruzada é formada apenas com o comando para inserir a parte inferior do deslocamento de 32 bits.
A base é visível nas propriedades de deslocamento e a propriedade é marcada para considerá-lo como um número, de modo que um grande número de referências cruzadas para o endereço em si não seja formado, que tomamos como base.

No código do NIOS II, o seguinte mecanismo é encontrado para inserir números de 32 bits no registro. Primeiro, a parte mais antiga do deslocamento é inserida no registro com o comando movhi. Então a parte mais jovem se junta a ela. Isso pode ser feito de três maneiras (por comandos): adicionando addi, subtraindo subi, OR lógico ORi.
Por exemplo, na próxima seção do código, os registradores são configurados para números de 32 bits, que são inseridos nos registradores - argumentos antes de chamar a função.

Após adicionar o cálculo do deslocamento, obtemos a seguinte representação desse bloco de código.

O deslocamento de 32 bits resultante é exibido ao lado do comando para inserir sua parte inferior. Este exemplo é bastante ilustrativo e poderíamos calcular facilmente todos os números de 32 bits na mente, simplesmente adicionando as partes menores e as mais altas. A julgar pelos valores, eles provavelmente não são tendenciosos.
Considere o caso em que a subtração é usada ao entrar na parte mais jovem. Neste exemplo, não será possível determinar os números finais de 32 bits (deslocamentos) em movimento.

Depois de aplicar o cálculo dos números de 32 bits, obtemos o seguinte formulário.

Aqui vemos que agora, se o endereço estiver no espaço de endereços, um deslocamento é formado nele, e o valor que foi formado como resultado da conexão das partes júnior e sênior não será mais exibido próximo a ele. Aqui eles foram compensados pela linha “22/10/08”. Para que o restante das compensações aponte para endereços válidos, vamos aumentar um pouco o segmento.

Depois de aumentar o segmento, descobrimos que agora todos os números de 32 bits calculados são compensados e indicam endereços válidos.
Foi mencionado acima que existe outra opção para calcular compensações quando um comando OR lógico é usado. Aqui está um código de exemplo no qual duas compensações são calculadas dessa maneira.

O que é avaliado no registro r8 é então empurrado para a pilha.
Após a conversão, fica claro que, neste caso, os registradores são configurados para os endereços do início dos procedimentos, ou seja, o endereço do procedimento é empurrado para a pilha.

Leitura e escrita em relação à base
Antes disso, consideramos casos em que um número de 32 bits digitado usando dois comandos poderia ser apenas um número e também um deslocamento. No exemplo a seguir, a base é inserida na parte superior do registro e, em seguida, a leitura ou gravação ocorre em relação a ele.

Após o processamento de tais situações, obtemos o deslocamento para as variáveis dos próprios comandos de leitura e gravação. Além disso, dependendo da dimensão da operação, o tamanho da variável em si é definido.

Switch construções
As construções de switch encontradas em arquivos binários podem facilitar a análise. Por exemplo, pelo número de casos de seleção dentro da construção do comutador, você pode localizar o comutador responsável pelo processamento de um determinado protocolo ou sistema de comando. Portanto, surge a tarefa de reconhecer o próprio switch e seus parâmetros. Considere a seguinte seção de código.

O encadeamento de execução para na transição do registro jmp r2. Além disso, existem blocos de código para os quais existem links dos dados e, no final de cada bloco, há um salto para o mesmo rótulo. Obviamente, essa é uma construção de switch e esses blocos individuais lidam com casos específicos a partir dela. Acima, você também pode ver a verificação do número de casos e o salto padrão.
Depois de adicionar o processamento do switch, esse código ficará assim.

Agora o salto em si é indicado, o endereço da tabela com compensações, o número de casos e cada caso com o número correspondente.
A tabela em si com deslocamentos para as opções é a seguinte. Para economizar espaço, os cinco primeiros elementos são fornecidos.

De fato, o processamento do comutador consiste em voltar ao código e procurar todos os seus componentes. Ou seja, é descrito algum esquema de organização do switch. Às vezes, pode haver exceções nos esquemas. Esse pode ser o motivo dos casos em que os comutadores aparentemente limpos não são reconhecidos nos módulos de processador existentes. Acontece que o comutador real simplesmente não se enquadra no esquema definido dentro do módulo do processador. Ainda existem opções possíveis quando o circuito parece estar lá, mas há outras equipes dentro dele que não estão envolvidas no circuito, ou as equipes principais são reorganizadas ou são interrompidas pelas transições.
O módulo do processador NIOS II reconhece um switch com essas instruções "estranhas" entre os comandos principais, bem como com os locais reorganizados dos comandos principais e com interrupções no circuito. Um caminho de retorno é usado ao longo do caminho de execução, levando em consideração possíveis transições que interrompem o circuito, com a instalação de variáveis internas que sinalizam diferentes estados do reconhecedor. Como resultado, são reconhecidas cerca de 10 opções diferentes de organização de switches encontradas no firmware.
Instrução personalizada
Há um recurso interessante na arquitetura do NIOS II - a instrução personalizada. Dá acesso a 256 instruções definidas pelo usuário que são possíveis na arquitetura NIOS II. Em seu trabalho, além dos registros de uso geral, a instrução personalizada pode acessar um conjunto especial de 32 registros personalizados. Depois de implementar a lógica para analisar o comando personalizado, obtemos o seguinte formulário.

Você pode perceber que as duas últimas instruções têm o mesmo número de instruções e parecem executar as mesmas ações.
De acordo com as instruções personalizadas, há um
manual separado . Segundo ele, uma das opções mais abrangentes e atualizadas para o conjunto de instruções personalizadas é o conjunto de instruções do NIOS II Floating Point Hardware 2 Component (FPH2) para trabalhar com o ponto flutuante. Após implementar a análise dos comandos do FPH2, o exemplo será semelhante a este.

A partir das mnemônicas das duas últimas equipes, garantimos que elas realmente executem a mesma ação - o comando fadds.
Transições por valor do registro
No firmware sob investigação, geralmente é encontrada uma situação quando um salto é realizado de acordo com o valor do registro, no qual um deslocamento de 32 bits, que determina o local do salto, é inserido anteriormente.
Considere um pedaço de código.

Na última linha, há um salto no valor do registro, enquanto fica claro que o endereço do procedimento que começa na primeira linha do exemplo é inserido primeiro no registro. Nesse caso, é óbvio que o salto é feito desde o início.
Depois de adicionar a funcionalidade de reconhecimento de saltos, é obtido o seguinte formulário.

Ao lado do comando jmp r8, o endereço onde o salto ocorre, se foi possível calcular, é exibido. Também é formada uma referência cruzada entre a equipe e o endereço onde o salto ocorre. Nesse caso, o link é visível na primeira linha, o salto em si é realizado a partir da última linha.
Valor do registro Gp (ponteiro global), salvar e carregar
É comum usar um ponteiro global configurado para algum endereço e as variáveis são endereçadas em relação a ele. O NIOS II usa o registro gp (ponteiro global) para armazenar o ponteiro global. Em algum momento, como regra, nos procedimentos de inicialização do firmware, o valor do endereço é inserido no registro gp. O módulo do processador lida com essa situação; Para ilustrar isso, a seguir estão exemplos de código e a janela de saída do IDA Pro quando as mensagens de depuração são ativadas no módulo do processador.
Neste exemplo, o módulo processador encontra e calcula o valor do registro gp no novo banco de dados. Ao fechar o banco de dados idb, o valor de gp é armazenado no banco de dados.

Ao carregar um banco de dados idb existente e se o valor gp já foi encontrado, ele é carregado a partir do banco de dados, conforme mostrado na mensagem de depuração no exemplo a seguir.

Leitura e escrita sobre GP
As operações comuns são de leitura e gravação com um deslocamento relativo ao registro gp. Por exemplo, no exemplo a seguir, são realizadas três leituras e um registro desse tipo.

Como já obtivemos o valor do endereço armazenado no registrador gp, podemos resolver esse tipo de leitura e gravação.
Após adicionar o processamento para situações de leitura e gravação em relação ao registro gp, obtemos uma imagem mais conveniente.

Aqui você pode ver quais variáveis estão sendo acessadas, rastrear seu uso e identificar sua finalidade.
Endereçamento relativo ao GP
Há outro uso do registro gp para endereçar variáveis.

Por exemplo, aqui vemos que os registradores são configurados em relação ao registrador gp para algumas variáveis ou áreas de dados.
Depois de adicionar a funcionalidade que reconhece essas situações, converte em compensações e adiciona referências cruzadas, obtemos o seguinte formulário.

Aqui você já pode ver quais áreas relativas aos registros gp estão configuradas e fica mais claro o que está acontecendo.
Endereçamento relativo a sp
Da mesma forma, no exemplo a seguir, os registradores são ajustados para algumas áreas da memória, desta vez em relação ao apontador sp do registrador para a pilha.

Obviamente, os registros são ajustados para algumas variáveis locais. Tais situações - configurando argumentos para buffers locais antes das chamadas de procedimento - são bastante comuns.
Após adicionar o processamento (converter valores diretos em compensações), obtemos o seguinte formulário.

Agora, fica claro que, após a chamada do procedimento, os valores são carregados dessas variáveis cujos endereços foram passados como parâmetros antes da chamada da função.
Referências cruzadas do código para os campos da estrutura
Definir estruturas e usá-las no IDA Pro pode facilitar a análise de código.

Observando esta parte do código, podemos entender que o campo field_8 está aumentando e, possivelmente, é um contador da ocorrência de um evento. Se os campos de leitura e gravação estiverem separados no código a uma grande distância, a referência cruzada poderá ajudar.
Considere a própria estrutura.
Embora o acesso aos campos de estruturas seja, como vemos, não há referências cruzadas do código para os elementos das estruturas.Depois que essas situações são processadas, para o nosso caso, tudo ficará da seguinte maneira.
Agora, existem referências cruzadas para estruturar campos de equipes específicas que trabalham com esses campos. As referências cruzadas para frente e para trás são criadas e é possível rastrear por diferentes procedimentos onde os valores dos campos da estrutura são lidos e onde são inseridos.Discrepâncias entre manual e realidade
No manual, ao decodificar alguns comandos, certos bits devem assumir valores estritamente definidos. Por exemplo, para um comando de retorno de uma exceção eret, os bits 22–26 devem ser 0x1E.
Aqui está um exemplo deste comando de um firmware.
Abrindo outro firmware em um local com um contexto semelhante, encontramos uma situação diferente.
Esses bytes não foram convertidos automaticamente em um comando, embora haja processamento de todos os comandos. A julgar pelo ambiente e até por um endereço semelhante, deve ser a mesma equipe. Vamos olhar atentamente para os bytes. Este é o mesmo comando eret, com a exceção de que os bits 22–26 não são iguais a 0x1E, mas iguais a zero.Temos que corrigir a análise deste comando um pouco. Agora, isso não corresponde exatamente ao manual, mas corresponde à realidade.
Suporte da AID 7
A partir do IDA 7.0, a API fornecida pelo Python IDA para scripts regulares mudou bastante. Quanto aos módulos do processador, as mudanças são colossais. Apesar disso, o módulo do processador NIOS II pôde ser refeito para a versão 7 e funcionou com êxito.
O único momento incompreensível: ao carregar um novo arquivo binário no NIOS II no IDA 7, a análise automática inicial presente no IDA 6.9 não ocorre.Conclusão
Além da funcionalidade básica de desmontagem, cujos exemplos estão no SDK, o módulo do processador implementa muitos recursos diferentes que facilitam o trabalho do explorador de código. Está claro que tudo isso pode ser feito manualmente, mas, por exemplo, quando existem milhares e dezenas de milhares de compensações de tipos diferentes em um arquivo binário com firmware de alguns megabytes, por que gastar tempo com isso? Deixe o módulo do processador fazer isso por nós. Afinal, como estão os recursos agradáveis da navegação rápida pelo código estudado usando referências cruzadas! Isso torna a IDA uma ferramenta conveniente e agradável como a conhecemos.Postado por Anton Dorfman, Positive Technologies