Escrito por: Mateus Gualberto (Midnight Reverser) - humanamente original
Licença: livre, como todo conhecimento deve ser.

Introdução#

Neste artigo serão tratados temas relacionados a ofuscação de binários através do processo de packing, bem como explicação de técnicas para realizar a desofuscação - unpacking. Esse assunto é de especial interesse para engenheiros reversos e analistas de malware, visto que muitos artefatos utilizam esse tipo de procedimento pelos mais variados motivos.

Devido a densidade do conteúdo, será necessário dividi-lo em 2 partes:

  • este artigo definirá o que é packing, seus tipos, motivações para seu uso, como identificá-los e técnicas automáticas e manuais para a desofuscação - incluindo um exemplo prático para os leitores que preferem aprender com a mão na massa;
  • o segundo irá tratar dos aspectos técnicos relacionados à reconstrução do binário, criando uma pequena ferramenta para dump e rebuild da IAT.

Conhecimento básico de C, Assembly (x86) e experiência com debuggers são desejáveis para melhor entendimento do artigo, embora não sejam um requisito.

Uma pequena motivação…#

Vamos supor que estamos em um engajamento Red Team e gostaríamos de ofuscar um malware que construímos, de forma simples, com objetivo de evadir detecções e principalmente aumentar o tempo de análise de engenharia reversa.

Nesse cenário, poderíamos utilizar um software que recebe como entrada um EXE sem ofuscação, realiza um procedimento nas suas seções de código e dados e ao fim retorna um novo EXE, com mesma funcionalidade mas com suas seções ofuscadas, sem necessidade de alterar manualmente o código escrito.

E esse software também poderia adicionar métodos anti-análise e anti-forense, como identificação de ambiente de análise, sandbox, VM, debugging, entre outros. Ou até mesmo mudar o hash e imphash do binário e possibilidade de criar versões polimórficas! (e, quem sabe, tornar Fully Undetectable - FUD…).

Esse software existe e se chama packer! Ele pode ser um aliado (ou não, como veremos mais à frente) para Red Teamers e escritores de malware.

…e um pequeno exemplo#

Na imagem abaixo há a execução de dois binários PEs que chamam uma Message Box contendo a palavra “Hello”. O funcionamento principal é igual nos dois. O primeiro foi nomeado como testvs-nm.exe, e o segundo, testvs-m.exe. Vamos verificar as principais diferenças entre os dois.

a7a406999dc4ec3debb0949cce336b73.png

O sumário#

No sumário de testvs-nm.exe no Detect it Easy, o arquivo contém cerca de 930KB de tamanho, feito na linguagem C/C++, para Windows/x86 de 32 bits, com o toolkit de desenvolvimento (compilador, linker) da Microsoft.

a8f7864188f39547dc57b7dbbfc7b1f9.png

Já o testvs-m.exe difere enormemente no tamanho, sendo aproximadamente 5x menor que o outro. Outras informações disponíveis incluem que sua plataforma é Windows/x86 de 32 bits, foi escrito em Assembly e está com um “empacotador”, chamado MEW. Além disso, há dados comprimidos/ofuscados (de alta entropia) e seu hash difere do primeiro programa.

669312a5876f92ce5f29ab4180e896b3.png

As seções#

Verificando as seções dos dois binários, no primeiro há mais seções e elas têm entropia e nomes normais.

729ba754038b4f3c9528df104b2e591a.png

No segundo binário, há apenas uma seção comprimida/ofuscada, e nome não legível.

b20242e00de01e5a0e38fbf2ebf1b998.png

A Import Table#

Comparando a Import Table, na qual estão listadas as DLLs e funções importadas pelo binário, o primeiro contém uma lista de funções importadas de tamanho normal (para um binário que apenas chama MessageBox).

2762a55329d15226bc954881d5b1aeb9.png

No segundo binário, há apenas a importação de Kernel32.dll, e apenas as funções LoadLibraryA e GetProcAddress. Isso é algo comum para artefatos que utilizam a técnica de Runtime Linking, em que o leitor pode entender melhor nesse artigo do nosso blog.

6e39d8437d66183194cce8121fe356a2.png

As strings#

Realizando uma pesquisa diretamente por strings, é possível identificar “Hello” dentro do primeiro binário, que mostra que ela está em texto claro e sem ofuscações.

dedee09a6d8167b3793bcd58145d3929.png

No segundo binário, ela sequer é encontrada, podendo estar comprimida ou ofuscada.

14bf41b7ba25adf67e73b0eaf91160d0.png

O código#

Já realizando o decompiling no primeiro binário, a partir do Entry Point (EP), é possível perceber que seu código é bem simples e padrão de um binário compilado pelo toolkit da Microsoft. Facilmente a função main pode ser identificada.

67bfa94dc10f35be643d968ec3d581e2.png

Já no segundo binário, seu Entry Point é confuso e não é possível identificar facilmente a função main.

bbc61aa11707ed5a230ecaf5a973e0ad.png

Em resumo#

Como foi possível inferir com base nas informações dispostas nas seções anteriores, o primeiro binário (testvs-nm.exe) é o binário original, enquanto o segundo (testvs-m.exe) é sua versão ofuscada por um packer. Enquanto a engenharia reversa pode ser facilmente realizada no primeiro, o segundo torna o processo mais longo, cansativo e complexo, inclusive para binários extremamente básicos.

Vamos entender melhor o que é esse processo e como ele é feito nas próximas seções.

O que é packing?#

Podemos definir packing como o procedimento pelo qual um arquivo binário tem sua seção de código e/ou dados ofuscados antes da execução do EntryPoint original, resultando em um novo binário com novo hash e características que dificultam a engenharia reversa e recuperação de código.

Para a maioria dos packers, não é necessário acesso ao código-fonte original, apenas o binário compilado.

Como isso é feito?#

A imagem abaixo representa um procedimento comum realizado por um packer em um arquivo PE (EXE, DLL). Esse é um ponto de partida para entender o que o packer faz, mas cada um tem suas especificidades, podendo modificar outros campos. Em vermelho são os principais elementos modificados durante o packing.

8e49c4c05f83b3502cb612e3327d0d24.png

A princípio, temos o binário original, com seu Entry Point padrão, Import Table e seções de código (.text) e dados (.data). O Entry Point está apontando para o início da .text, comumente local da função que prepara o ambiente para executar a main.

Durante o processo de packing, o software packer:

  • Obterá o conteúdo das seções .text e .data e utilizará um algoritmo de compressão (como LZMA) para comprimi-las junto. Essa versão comprimida é, então, nomeada de .PACKED0, e escrita no binário packeado;
  • Note que, como a seção de código está comprimida, o seu conteúdo original é ilegível para o processador. É necessário desofuscar esse código em runtime. Para isso, o packer irá criar uma nova seção .PACKED1, que não está ofuscada e contém um pequeno código (stub) que tem as seguintes funcionalidades:
    • Em runtime irá obter o conteúdo de .PACKED0;
    • Realizará a descompressão do código e dados originais em memória;
    • Fará os procedimentos necessários para carregamento da Import Table original;
    • Alterará o fluxo de execução para o código original.
  • Para isso, o stub deverá ser o Entry Point do binário, e não mais a .text, alterando o campo Optional Header->AddressOfEntryPoint;
  • Com a alteração das seções, a Section Table e seus cabeçalhos também serão modificados;
  • Por fim, não é necessário que a Import Table se mantenha original, visto que apenas as funções para funcionamento primordial do stub são necessárias. As funções originais importadas podem ser obtidas após a descompressão em runtime através de runtime linking. Logo, o campo Optional Header->DataDirectories->Import Table também sofrerá mudanças.

O stub também pode conter código anti-debugging e para injeção de código, no caso de artefatos maliciosos. Também pode haver criptografia ou virtualização ao invés de uma simples compressão de dados. Tudo depende dos objetivos que o escritor do packer gostaria de atingir: apenas anti-reversing ou também evasão de defesas?

Unpacking#

Como os bytes rodam em “texto claro” na CPU, em algum momento os dados comprimidos/criptografados terão que sofrer a descompressão/descriptografia. Isso dá uma janela importante para a engenharia reversa, em que podemos realizar o processo de unpacking.

Unpacking é o procedimento de desofuscação do binário que sofreu o procedimento de packing, buscando obter o código mais perto possível do código original. Como os dados sem packing são obtidos da memória, muitas vezes haverá bytes extras do runtime que será embutido no binário restaurado, campos originais de cabeçalhos podem ser perdidos e seções podem ser adicionadas/removidas. Dessa forma, o binário reconstruído não terá o mesmo hash do original, e poderá ser um pouco maior que o original.

As vantagens de realizar unpacking são principalmente para a análise de malware, permitindo uma identificação mais facilitada de capacidades maliciosas e resultando na busca de IoCs de forma mais rápida.

Tipos de packer#

Há diversos tipos de packer, que podem ser catalogados de acordo com sua funcionalidade (compressor, crypter, protector) e por sua licença/construção e distribuição. Vamos aos principais:

  • Open Source e Freeware

Packer freeware ou opensource é um tipo de packer que está amplamente distribuído na Internet. Tendem a ser menos eficazes e podem gerar mais detecções, mas são um ótimo ponto de partida para estudos por serem mais simples (e, em caso de opensource, disponibilizarem o código).

Apesar disso, escritores de malware por vezes modificam características desse tipo de packer para evitar detecções e dificultar o unpacking.

Como exemplos há o UPX (opensource), FSG e MEW (freeware e bem antigos!).

6651ead3f770bea7777625409dd5cce4.png
  • Customizado

Packers customizados geralmente são criados especificamente para campanhas de malwares e vendidos na Deep Web. Geralmente seu código não é divulgado publicamente. Crypter é um tipo de packer customizado em que a compressão dá lugar à criptografia das seções, podendo criar malwares polimórficos a partir da mudança da chave de criptografia.

O objetivo, além de dificultar mais a engenharia reversa que um packer freeware/opensource, é de evadir defesas. Como exemplo é possível citar o PureCrypter.

Também existe o conceito de Packer as a Service, em que threat actors vendem acesso a plataformas online para realizar packing.

07cd1940f2d8ec7c30ff3b0656702fc6.png
  • Comercial

São softwares construídos por companhias especializadas em proteção de código. Geralmente conhecidos por protectors, eles utilizam de vários tipos de checagens de ambiente e ofuscações para proteger o código. Muitos emulam uma máquina virtual para proteção de código (instruções do computador real são transformadas em bytecode). Isso torna o procedimento de reversa particularmente difícil.

Por causa dessas características avançadas, atacantes obtêm cópias piratas e as utilizam por serem packers complexos de serem analisados. Exemplos de protectors incluem o VMProtect, PELock e Themida.

Pode não ser legal realizar o unpacking/análise desse tipo de packer (pelo menos não é permitido publicar os detalhes internos de funcionamento do stub ou da VM). Sempre é importante checar a licença do software antes de publicar algo.

d9835f4a9aefa102d008be37dd27a4c7.png

Motivos do uso de packers#

Como já citado, há vários motivos para que se deseje usar um packer. Na época da internet discada, era utilizado para comprimir o executável e reduzir seu tamanho, exigindo menos banda e diminuindo custos.

Hoje esse cenário mudou drasticamente, sendo que o uso legítimo de um packer está ligado mais à proteção do código do que compressão por problemas de internet. Os usos maliciosos têm como objetivo atrasar a análise e resposta a incidentes e evadir defesas ao empregar altos níveis de ofuscação e uso de técnicas anti-sandbox.

Identificando uso de packers#

Agora, como podemos identificar um binário que está packeado? Infelizmente, essa não é uma “ciência muito exata”. Não há uma única característica que determine isso, mas há uma série de fatores que podem corroborar com a hipótese de packing, por exemplo:

  • Entropia do binário/seções >= 7. A entropia é o grau de desordem de um sistema - quanto maior a desordem, maior a entropia. Dados comprimidos tem um grau de desordem muito maior que código ou dados não ofuscados.
  • Seções com nomes de packers já conhecidos ou aleatórios.
  • Poucos imports ou ausência da Import Table.
  • Poucas strings com sentido.
  • Excesso de strings ofuscadas ou tripa de bytes.
  • Diferença considerável entre Raw/Virtual Sizes das seções, indicando que elas podem conter um conteúdo pequeno no binário que pode “se expandir” em memória.

Mais informações sobre indicadores de packing podem ser obtidas nesse artigo do Alexandre Borges.

No DIE, é possível identificar que o binário de exemplo abaixo está packeado pelo UPX, de acordo com alguns indicadores:

  • Nome de seção contém referência a um packer conhecido (.UPX1);
  • Essa seção está com entropia alta (7.8), demonstrando um nível de desordem alta (compressão).
a82544bf2a96833c4037d3ed41556c33.png

Técnicas para unpacking#

Tal qual a diversidade de packers, as técnicas para unpacking também são numerosas. As principais formas estão ilustradas abaixo:

87b34a8f2b2b856367c223682f2b8a8b.png
  • Uso de unpackers

O unpacking é feito de forma simples, rodando um software que reverte o processo de packing (unpacker). Um exemplo desse processo é o packer UPX, em que o próprio software que ofusca também permite desofuscar (com a opção -d). É um método extremamente rápido, eficaz e sem rodar o artefato, mas também específico: é necessário já existir uma ferramenta que realize o unpacking para aquela versão.

Qualquer modificação aplicada pelo criador do artefato tem o potencial de quebrar o processo de unpacking via unpacker e torná-lo inútil.

7f3b6c623b72d0c4c678f1376cc4e3b2.png
  • Análise estática

O unpacking é feito de forma manual e sem rodar o artefato, utilizando de emulações, execução simbólica, disassembly, decompiling, extração de pedaços de código e entendendo a lógica do packer.

Procedimento lento, menos efetivo em cenários de incidente, mas pode ser um aliado para pesquisas de malware e criação de assinaturas, além de auxiliar a análise dinâmica.

3445f0bbb57cdd32ab84f56002e328be.png
  • Análise dinâmica

O unpacking é feito de forma manual rodando o artefato em ambiente controlado, utilizando ferramentas de debugging e instrumentação (x{32,64}dbg, frida etc).

Dessa forma, é possível identificar o momento em que o artefato desempacota o artefato e pula para o EntryPoint original, permitindo salvá-lo nesse estado.

É complexo e desafiador, mas muitas vezes é a opção mais rápida - ou a única opção, juntamente com a análise estática.

57c6a27b0e0162986718930760b53f26.png
  • Automaticamente

O unpacking, nesse cenário, é feito de forma automática através de plataformas terceiras (como unpac.me) ou softwares que focam na varredura de processos, buscando por injeções, hooks e outras técnicas de evasão (como PE Sieve, Hollows Hunter). É rápido, mas pode ser custoso ($) ou não conseguir realizar o unpacking devido às técnicas empregadas pelos artefatos.

50ee8252544003bdf0e945e96d8371a7.png

Unpacking manual#

O unpacking manual se dá, principalmente, pela aplicação de análise dinâmica. Não é uma das tarefas mais fáceis e rápidas de engenharia reversa, mas muitas vezes é a única alternativa - em especial, quando não há unpackers disponíveis. Pode ser necessário diversas execuções e bastante tempo para encontrar o OEP, devido à complexidade e características únicas de cada packer. Abaixo está ilustrado o fluxo das etapas para o unpacking manual.

883643c13120cb6df125df946e3450d1.png

1. Identificação do Stub (EP)#

O ponto inicial da análise é a identificação do stub. Geralmente, ele estará no EntryPoint, ou haverá um jump para a rotina de desofuscação a partir do EP.

2. Realizar debugging do Stub (rodar)#

Essa é a etapa mais longa, pois demanda bastante atenção, análise e experiência. O objetivo principal é identificar o ponto em que há a desofuscação do código original em memória e mudança de fluxo para ele. O Stub também poderá conter funcionalidades anti-debugging, além de executar outros procedimentos necessários à execução (como reconstrução da Import Table via runtime linking).

Nesse ponto, breakpoints em APIs de alocação, proteção e escrita de memória, além de APIs de hardware ou memória, são os melhores amigos do engenheiro reverso:

- BP de software#

Utilizado principalmente em localizações do stub ou em funções chamadas por ele, é o principal aliado para identificar alocações e escritas na memória - como localizações em que o binário original será desofuscado. Algumas APIs importantes estão listadas abaixo:

  • VirtualAlloc: função da Kernel32.dll que permite alocar e reservar memória com permissões específicas - como páginas de memória para leitura, escrita e execução;
  • malloc: alocação de memória através da msvcrt (Microsoft C Runtime Library) para a heap;
  • VirtualAllocEx: semelhante à VirtualAlloc, mas permite alocar memória em outros processos - útil para injeção de código;
  • VirtualProtect: permite alterar as permissões das páginas de memória já alocadas;
  • WriteProcessMemory: essa API permite escrever em uma região de memória alocada em outro processo (como a alocada via VirtualAllocEx);
  • CreateProcessInternalW: API chamada na criação de processos, chamada internamente por várias outras APIs desse propósito;
  • ZwAllocateVirtualMemory: API chamada na alocação de memória, chamada internamente por várias outras APIs desse propósito;

A dica, para os já experientes é rodar até o BP ser acionado, executar até o RET e seguir o valor retornado (por exemplo, alocações de memória no dump do debugger). Monitorar esses valores até encontrar algo interessante à sua análise.

A documentação de APIs Win32 está disponível no portal da MSDN

BP de Hardware#

Ao encontrar uma alocação de memória, pode ser interessante identificar o momento em que essa memória está sendo utilizada. Breakpoints de software são ineficazes visto que podem ser sobrescritos no processo de escrita do código original em memória.

Para esse cenário, BPs de Hardware, em especial na escrita ou execução, são ideais. Eles não são sobrescritos por APIs de escrita de memória por serem registradores físicos do processador, mas são limitados (apenas 4 usáveis para monitorar endereços específicos).

O melhor cenário para unpacking é utilizar um BP de hardware na execução no primeiro byte da memória recém alocada. O de escrita também pode ser utilizado para identificar em que parte do fluxo está a(s) subrotina(s) que realizam a desofuscação.

BP de Memória#

Em alguns cenários, o BP de memória na execução pode ser utilizado, em especial quando o BP de hardware falha, ou não se conhece exatamente onde pode estar sendo feita a mudança de fluxo para o código original. O BP de memória funciona alterando as permissões de uma ou mais páginas da memória, gerando exceções na sua leitura, escrita ou execução. Sua principal desvantagem é sua lentidão comparada aos demais métodos, além de ser mais impreciso comparado aos outros métodos.

3. Identificar OEP#

A identificação do Original Entry Point (OEP) é feita depois da desofuscação e obtenção dos endereços das funções importadas. A mudança de fluxo de execução do stub para o OEP é feita, em boa parte das vezes, por um JMP/Jcc/CALL para endereços de fora da região do stub, ou via JMPs/Jccs/CALLs indiretos (como JMP EAX).

Ou seja, deve haver uma chamada para fora do stub, de alguma forma. O primeiro byte do binário original a ser executado é o OEP.

4. Dump e reconstrução da IAT#

Após identificar o OEP, o engenheiro reverso deve “salvar” seu árduo trabalho através do dump de memória do processo naquele estado. Dessa forma, o EntryPoint estará no OEP no binário em disco.

Entretanto, ao tentar executar o dump, é muito provável que ocorra um crash. Isso é devido à Import Table, que continua sendo apontada como a do binário com packing. Para o binário funcionar, é necessário consertar a Import Table (Fix dump), identificando-a e corrigindo suas entradas (DLLs e funções importadas).

Técnicas para análise dinâmica#

Abaixo está um resumão das técnicas já citadas nesse artigo que podem auxiliar o processo de unpacking manual, independente do tipo/versão do packer:

  • BP de hardware (execução, escrita) no primeiro byte de regiões alocadas;
  • BP de software nas APIs de alocação de memória + análise de modificações nessas regiões;
  • BP de memória (execução) em seções suspeitas;
  • Identificar calls indiretas (como CALL EAX) e JMPs/Jccs para fora da seção que pertencem (stub);

Prática#

Agora vamos por em prática o que aprendemos nesse artigo, através de unpacking manual! Para isso, é recomendável que se utilize uma máquina virtual segura - mesmo que os binários que compilamos sejam seguros, não analisamos o packer inteiramente. Leia nosso artigo sobre a construção de um laboratório seguro caso não saiba como criar um.

O link direto para o binário de exemplo está aqui. O repositório com todos os artefatos, incluindo o packer em si, está disponível neste link.

Análise inicial#

Executando o binário para entender seu comportamento, é possível verificar que ele é de linha de comando e pede uma senha, dando uma resposta ao usuário - se a senha está correta ou não.

9786f7efff0dd56bb6f279d02fb3f52d.png

Analisando no DIE, é possível verificar que:

  • O binário é bem pequeno (28Kb);
  • PE/EXE de 32 bits;
  • Carimbo de data e hora falso/modificado;
  • Apenas duas seções, sendo uma contendo caracteres ilegíveis.
    • Essa seção está com 7.9 ~ 8.0 de entropia, indicando uma alta probabilidade de uso de packing;
    • A outra está nomeada apenas como “PE Header”, possivelmente um erro de parsing ou o DIE não conseguiu encontrar essa infomação.
  • Assinatura indicando empacotador “MEW”.

Ao leitor, quais são os outros indicativos de uso de packing nesse binário?

172e20d2252b14796c3cc453845cb437.pnge184c821f0b8853137e82e71175810d5.png

Identificando OEP#

Vamos ao debugging! Ao carregarmos o binário no x32dbg, é possível identificar 3 regiões de memória mapeadas com a flag “E”, de Executable - além de leitura (R) e escrita (W). Os bytes dessas regiões podem ser lidos, escritos e executados (interpretados como instruções pelo processador). Dentre as regiões:

  • crackme-mod.exe, que contém o binário e seus cabeçalhos mapeados;
  • Região/seção “MEW”, que está vazia (ver imagens abaixo);
  • Seção com nome ilegível, que contém código executável e dados - possivelmente o código principal do nosso stub.
4a4b09eeb0d73d657d17121b7ca9f0c3.png

Abaixo, parte do hexdump da região crackme-mod.exe mapeado em memória.

1d06e05ad5a1b0bff2d5485d5b6a5943.png

Abaixo região “MEW” vazia em memória.

3d8e9b96be09d951d2a8d1a49981f50d.png

Abaixo o disassembly e parte do hexdump da seção com nome ilegível em memória.

12d3459ae7b00ea65de145b9d7a266c9.png

Com base no que aprendemos até aqui, vamos levantar a hipótese (engenheiros reversos e analistas de malware vivem disso!) de que o stub escreverá o código e dados originais na seção “MEW”, que está vazia. Afinal, qual seria o uso de uma seção vazia de um binário ofuscado?

Para isso, configuramos um BP de hardware na escrita no endereço 401000 - primeiro byte da seção MEW - e rodamos.

O breakpoint irá pausar na seguinte subrotina:

2d7132ca99a4ca42feeca3ae845037ad.png

É possível perceber que um C3 foi escrito exatamente no primeiro byte da seção MEW. Se seguirmos executando essa função, uma boa parte dos bytes subsequentes também será escrita, no loop de desofuscação.

Verificando os endereços mais altos da seção MEW, também é possível notar que strings relacionadas a nomes de DLLs e funções já foram escritas. Essas informações são recuperadas pelo stub para montar a Import Table original do binário, e, durante o runtime, são utilizadas via Runtime Linking para executar o código original sem problemas de dependências externas.

8dd2a4894595fc5107e0a69fcd4adfaa.png

Verificando a stack trace/call stack, é possível identificar o endereço 4001D4, que chamou a função que bateu no BP de hardware que criamos. É o endereço de retorno para o qual o fluxo irá mudar quando o RET for executado.

f775477cb7b76e648f2cedf64e4cbe28.png

Seguindo esse endereço, e usando o Run until return ou equivalente, é possível perceber que, após a desofuscação, há instruções relacionadas a carregamento de strings (LODSD) e chamadas indiretas de funções (CALL DWORD PTR DS:[EBX-0x10]). Vamos analisar um pouco mais isso.

1e5e9479811c8648d4fd284d99167c83.png

Em especial, as calls indiretas, em [EBX-10] e [EBX-C], chamam LoadLibrary e GetProcAddress, respectivamente. Analisando superficialmente, podemos formular a hipótese de que essas informações (como nome da DLL) estão sendo obtidas das informações desofuscadas mencionadas anteriormente.

cb20d79223f2679a1d1ee7e70797464a.png

Abaixo a call para GetProcAddress. Esse é o procedimento de Runtime Linking para funcionamento do código original, que necessita dessas DLLs e funções.

e08038cfc2d6ce2f4f45a990602def64.png

Seguindo o fluxo, após resolver todas as APIs, somos levados ao RET da função atual. Ao executar o RET, verificamos que chegamos a uma região de código pertencente à seção “MEW” (40140A), que anteriormente estava vazia. Não é o primeiro byte dessa região, por isso um BP de hardware na execução do primeiro byte da MEW falharia.

443758d548c62baed4ca530ae8f6f16e.png

Podemos estar no OEP! Mas podemos continuar a análise para facilitar as análises futuras.

Seguindo o JMP, somos levados a um código com a estrutura semelhante à preparação para chamada da main, contendo, inclusive, uma chamada para __p___initenv. Possivelmente foi gerado pelo compilador original.

f7a36dfbecbf167f019dd5c6df28a337.png

Podemos definir o início desse código como OEP, mas para facilitar a análise, podemos defini-lo na main.

A main pode ser encontrada na CALL imediatamente após a chamada de __p___initenv, ou seja, está no endereço 401450.

773b378e722852f3512913869ae463a4.png

É possível confirmar que é a main através da chamada de funções comumente utilizadas por programadores C em Windows: GetStdHandle, memset, WriteConsoleA, conforme imagem abaixo.

Esse código não foi gerado automaticamente pelo compilador, nem pelo packer, e podemos configurar o OEP para o início dessa função (401450).

cb576bd807dab257cb7ed1bef0803fbf.pngf9e7e77547486eef1d423fb1d4fa5c63.png

Salvando binário desofuscado#

Com o EIP em 401450 e utilizando o plugin Scylla, disponível por padrão no x32dbg, podemos realizar o dump do executável e reconstruir sua IAT (da memória para disco). Dessa forma, o binário final se parecerá mais com o original do que o packeado, além de poder ser executado normalmente.

Para criar o dump e realizar a reconstrução da IAT, é só seguir o fluxo: IAT Autosearch -> Get Imports -> Dump -> Fix Dump (apontando o binário criado por Dump).

É possível que a pesquisa da IAT encontre duas IATs devido ao uso de um algoritmo de busca “avançado”. É importante testar qual funciona corretamente com seu binário, não há resposta fixa para todos os casos. Nesse caso, seguiremos com o algoritmo padrão.

Os imports também podem estar inválidos, e devem ser analisados caso a caso - incluindo a reconstrução manual da IAT em certos casos.

959524c3a445101f914cba2e4f85bbed.png

Reconstruindo Imports e resolvendo o Crackme#

Rodando o Scylla conforme citado, é possível identificar 3 DLLs principais encontradas e que serão usadas na reconstrução da IAT.

ab5b72ad449fb1ee7092365c95d96189.png

Verificando a IAT do binário gerado pelo “Fix dump”, há todas as APIs utilizadas pelo binário. Nesse ponto, é possível inferir parte do funcionamento do binário devido a desofuscação da IAT. Executando o binário sem packer, ele funciona corretamente, sem problemas.

8e73f56a23de487d1bd04afdffe7eb6c.png

Verificando as strings, finalmente encontramos coisas com sentido e uma string que parece a senha do desafio.

5e92e3dfe4c6c62cfe70ef8a9beeef23.png

Por fim, testando essa senha, resolvemos o desafio através do unpacking manual!

ab6039fa28dde2674cd004cf98a1ee3f.png

Conclusão#

Nesse artigo, foi possível ter as primeiras noções de unpacking de binários, com foco em técnicas para unpacking manual. No próximo artigo, trataremos da parte mais técnica relacionada à reconstrução da IAT.

Referências e inspirações#

Esse artigo não seria possível senão pelos conhecimentos trazidos por gigantes da área. Abaixo estão as principais inspirações para a construção desse artigo.