Skip to content

Execução simbólica

A execução simbólica é uma técnica muito útil para se ter em sua caixa de ferramentas, especialmente ao lidar com problemas em que você precisa encontrar uma entrada correta para alcançar um determinado bloco de código. Nesta seção, resolveremos um simples crackme para Android usando a estrutura de análise binária Angr como nosso mecanismo de execução simbólica.

Para demonstrar esta técnica, usaremos um crackme chamado Validador de Licença Android. O crackme consiste em um único arquivo executável ELF, que pode ser executado em qualquer dispositivo Android seguindo as instruções abaixo:

$ adb push validate /data/local/tmp
[100%] /data/local/tmp/validate

$ adb shell chmod 755 /data/local/tmp/validate

$ adb shell /data/local/tmp/validate
Uso: ./validate <serial>

$ adb shell /data/local/tmp/validate 12345
Serial incorreto (formato errado).

Até aqui tudo bem, mas não sabemos nada sobre como é uma chave de licença válida. Para começar, abra o executável ELF em um desmontador como iaito. A função principal está localizada no deslocamento 0x00001874 na desmontagem. É importante notar que este binário está habilitado para PIE, e o iaito escolhe carregar o binário em 0x0 como endereço base da imagem.

Os nomes das funções foram removidos do binário, mas felizmente há strings de depuração suficientes para nos fornecer contexto sobre o código. Avançando, começaremos a analisar o binário a partir da função de entrada no deslocamento 0x00001874, e manteremos uma nota de todas as informações facilmente disponíveis para nós. Durante esta análise, também tentaremos identificar as regiões de código que são adequadas para execução simbólica.

strlen é chamado no deslocamento 0x000018a8, e o valor retornado é comparado a 0x10 no deslocamento 0x000018b0. Imediatamente após isso, a string de entrada é passada para uma função de decodificação Base32 no deslocamento 0x00001340. Isso nos fornece informações valiosas de que a chave de licença de entrada é uma string de 16 caracteres codificada em Base32 (que totaliza 10 bytes em bruto). A entrada decodificada é então passada para a função no deslocamento 0x00001760, que valida a chave de licença. A desmontagem desta função é mostrada abaixo.

Agora podemos usar esta informação sobre a entrada esperada para analisar mais a fundo a função de validação em 0x00001760.

╭ (fcn) fcn.00001760 268
│   fcn.00001760 (int32_t arg1);
│           ; var int32_t var_20h @ fp-0x20
│           ; var int32_t var_14h @ fp-0x14
│           ; var int32_t var_10h @ fp-0x10
│           ; arg int32_t arg1 @ r0
│           ; CALL XREF from fcn.00001760 (+0x1c4)
│           0x00001760      push {r4, fp, lr}
│           0x00001764      add fp, sp, 8
│           0x00001768      sub sp, sp, 0x1c
│           0x0000176c      str r0, [var_20h]                          ; 0x20 ; "$!" ; arg1
│           0x00001770      ldr r3, [var_20h]                          ; 0x20 ; "$!" ; entry.preinit0
│           0x00001774      str r3, [var_10h]                          ; str.
│                                                                      ; 0x10
│           0x00001778      mov r3, 0
│           0x0000177c      str r3, [var_14h]                          ; 0x14
│       ╭─< 0x00001780      b 0x17d0
│       │   ; CODE XREF from fcn.00001760 (0x17d8)
│      ╭──> 0x00001784      ldr r3, [var_10h]                          ; str.
│       │                                                              ; 0x10 ; entry.preinit0
│      ╎│   0x00001788      ldrb r2, [r3]
│      ╎│   0x0000178c      ldr r3, [var_10h]                          ; str.
│      ╎│                                                              ; 0x10 ; entry.preinit0
│      ╎│   0x00001790      add r3, r3, 1
│      ╎│   0x00001794      ldrb r3, [r3]
│      ╎│   0x00001798      eor r3, r2, r3
│      ╎│   0x0000179c      and r2, r3, 0xff
│      ╎│   0x000017a0      mvn r3, 0xf
│      ╎│   0x000017a4      ldr r1, [var_14h]                          ; 0x14 ; entry.preinit0
│      ╎│   0x000017a8      sub r0, fp, 0xc
│      ╎│   0x000017ac      add r1, r0, r1
│      ╎│   0x000017b0      add r3, r1, r3
│      ╎│   0x000017b4      strb r2, [r3]
│      ╎│   0x000017b8      ldr r3, [var_10h]                          ; str.
│      ╎│                                                              ; 0x10 ; entry.preinit0
│      ╎│   0x000017bc      add r3, r3, 2                              ; "ELF\x01\x01\x01" ; aav.0x00000001
│      ╎│   0x000017c0      str r3, [var_10h]                          ; str.
│      ╎│                                                              ; 0x10
│      ╎│   0x000017c4      ldr r3, [var_14h]                          ; 0x14 ; entry.preinit0
│      ╎│   0x000017c8      add r3, r3, 1
│      ╎│   0x000017cc      str r3, [var_14h]                          ; 0x14
│      ╎│   ; CODE XREF from fcn.00001760 (0x1780)
│      ╎╰─> 0x000017d0      ldr r3, [var_14h]                          ; 0x14 ; entry.preinit0
│      ╎    0x000017d4      cmp r3, 4                                  ; aav.0x00000004 ; aav.0x00000001 ; aav.0x00000001
│      ╰──< 0x000017d8      ble 0x1784                                 ; provável
│           0x000017dc      ldrb r4, [fp, -0x1c]                       ; "4"
│           0x000017e0      bl fcn.000016f0
│           0x000017e4      mov r3, r0
│           0x000017e8      cmp r4, r3
│       ╭─< 0x000017ec      bne 0x1854                                 ; provável
│       │   0x000017f0      ldrb r4, [fp, -0x1b]
│       │   0x000017f4      bl fcn.0000170c
│       │   0x000017f8      mov r3, r0
│       │   0x000017fc      cmp r4, r3
│      ╭──< 0x00001800      bne 0x1854                                 ; provável
│      ││   0x00001804      ldrb r4, [fp, -0x1a]
│      ││   0x00001808      bl fcn.000016f0
│      ││   0x0000180c      mov r3, r0
│      ││   0x00001810      cmp r4, r3
│     ╭───< 0x00001814      bne 0x1854                                 ; provável
│     │││   0x00001818      ldrb r4, [fp, -0x19]
│     │││   0x0000181c      bl fcn.00001728
│     │││   0x00001820      mov r3, r0
│     │││   0x00001824      cmp r4, r3
│    ╭────< 0x00001828      bne 0x1854                                 ; provável
│    ││││   0x0000182c      ldrb r4, [fp, -0x18]
│    ││││   0x00001830      bl fcn.00001744
│    ││││   0x00001834      mov r3, r0
│    ││││   0x00001838      cmp r4, r3
│   ╭─────< 0x0000183c      bne 0x1854                                 ; provável
│   │││││   0x00001840      ldr r3, [0x0000186c]                       ; [0x186c:4]=0x270 section..hash ; section..hash
│   │││││   0x00001844      add r3, pc, r3                             ; 0x1abc ; "Product activation passed. Congratulations!"
│   │││││   0x00001848      mov r0, r3                                 ; 0x1abc ; "Product activation passed. Congratulations!" ;
│   │││││   0x0000184c      bl sym.imp.puts                            ; int puts(const char *s)
│   │││││                                                              ; int puts("Product activation passed. Congratulations!")
│  ╭──────< 0x00001850      b 0x1864
│  ││││││   ; CODE XREFS from fcn.00001760 (0x17ec, 0x1800, 0x1814, 0x1828, 0x183c)
│  │╰╰╰╰╰─> 0x00001854      ldr r3, aav.0x00000288                     ; [0x1870:4]=0x288 aav.0x00000288
│  │        0x00001858      add r3, pc, r3                             ; 0x1ae8 ; "Incorrect serial." ;
│  │        0x0000185c      mov r0, r3                                 ; 0x1ae8 ; "Incorrect serial." ;
│  │        0x00001860      bl sym.imp.puts                            ; int puts(const char *s)
│  │                                                                   ; int puts("Incorrect serial.")
│  │        ; CODE XREF from fcn.00001760 (0x1850)
│  ╰──────> 0x00001864      sub sp, fp, 8
╰           0x00001868      pop {r4, fp, pc}                           ; entry.preinit0 ; entry.preinit0 ;

Discutir todas as instruções na função está além do escopo deste capítulo, em vez disso, discutiremos apenas os pontos importantes necessários para a análise. Na função de validação, há um loop presente em 0x00001784 que realiza uma operação XOR no deslocamento 0x00001798. O loop é mais claramente visível na visualização de grafo abaixo.

XOR é uma técnica muito comumente usada para criptografar informações onde ofuscação é o objetivo em vez de segurança. XOR não deve ser usado para qualquer criptografia séria, pois pode ser quebrado usando análise de frequência. Portanto, a mera presença de criptografia XOR em tal lógica de validação sempre requer atenção e análise especiais.

Prosseguindo, no deslocamento 0x000017dc, o valor decodificado por XOR obtido acima está sendo comparado com o valor de retorno de uma sub-chamada de função em 0x000017e8.

Claramente, esta função não é complexa e pode ser analisada manualmente, mas ainda assim permanece uma tarefa árdua. Especialmente ao trabalhar em uma grande base de código, o tempo pode ser uma restrição importante, e é desejável automatizar tal análise. A execução simbólica dinâmica é útil exatamente nessas situações. No crackme acima, o mecanismo de execução simbólica pode determinar as restrições em cada byte da string de entrada mapeando um caminho entre a primeira instrução da verificação de licença (em 0x00001760) e o código que imprime a mensagem "Product activation passed" (em 0x00001840).

As restrições obtidas das etapas acima são passadas para um mecanismo solver, que encontra uma entrada que as satisfaça - uma chave de licença válida.

Você precisa realizar várias etapas para inicializar o mecanismo de execução simbólica do Angr:

  • Carregar o binário em um Project, que é o ponto de partida para qualquer tipo de análise no Angr.

  • Passar o endereço a partir do qual a análise deve começar. Neste caso, inicializaremos o estado com a primeira instrução da função de validação de serial. Isso torna o problema significativamente mais fácil de resolver porque você evita executar simbolicamente a implementação Base32.

  • Passar o endereço do bloco de código que a análise deve alcançar. Neste caso, é o deslocamento 0x00001840, onde está localizado o código responsável por imprimir a mensagem "Product activation passed".

  • Além disso, especificar os endereços que a análise não deve alcançar. Neste caso, o bloco de código que imprime a mensagem "Incorrect serial" em 0x00001854 não é interessante.

Observe que o carregador do Angr carregará o executável PIE com um endereço base de 0x400000, que precisa ser adicionado aos deslocamentos do iaito antes de passá-lo para o Angr.

O script de solução final é apresentado abaixo:

import angr # Versão: 9.2.2
import base64

load_options = {}

b = angr.Project("./validate", load_options = load_options)
# A função de validação da chave começa em 0x401760, então é onde criamos o estado inicial.
# Isso acelera muito as coisas porque estamos contornando o codificador Base32.

options = {
    angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
    angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS,
}

state = b.factory.blank_state(addr=0x401760, add_options=options)

simgr = b.factory.simulation_manager(state)
simgr.explore(find=0x401840, avoid=0x401854)

# 0x401840 = Product activation passed
# 0x401854 = Incorrect serial
found = simgr.found[0]

# Obtém a string solução de *(R11 - 0x20).

addr = found.memory.load(found.regs.r11 - 0x20, 1, endness="Iend_LE")
concrete_addr = found.solver.eval(addr)
solution = found.solver.eval(found.memory.load(concrete_addr,10), cast_to=bytes)
print(base64.b32encode(solution))

Como discutido anteriormente na seção "Instrumentação Binária Dinâmica))", o mecanismo de execução simbólica constrói uma árvore binária das operações para a entrada do programa fornecida e gera uma equação matemática para cada caminho possível que pode ser tomado. Internamente, o Angr explora todos os caminhos entre os dois pontos especificados por nós, e passa as equações matemáticas correspondentes para o solver retornar resultados concretos significativos. Podemos acessar essas soluções via lista simulation_manager.found, que contém todos os caminhos possíveis explorados pelo Angr que satisfazem nossos critérios de busca especificados.

Observe mais de perto a parte posterior do script onde a string de solução final está sendo recuperada. O endereço da string é obtido do endereço r11 - 0x20. Isso pode parecer mágico à primeira vista, mas uma análise cuidadosa da função em 0x00001760 contém a pista, pois determina se a string de entrada fornecida é uma chave de licença válida ou não. Na desmontagem acima, você pode ver como a string de entrada para a função (no registrador R0) é armazenada em uma variável local da stack 0x0000176c str r0, [var_20h]. Portanto, decidimos usar este valor para recuperar a solução final no script. Usando found.solver.eval você pode fazer perguntas ao solver como "dada a saída desta sequência de operações (o estado atual em found), qual deve ter sido a entrada (em addr)?".

Em ARMv7, R11 é chamado fp (function pointer), portanto R11 - 0x20 é equivalente a fp-0x20: var int32_t var_20h @ fp-0x20

Em seguida, o parâmetro endness no script especifica que os dados são armazenados no formato "little-endian", que é o caso para quase todos os dispositivos Android.

Além disso, pode parecer que o script está simplesmente lendo a string solução da memória do script. No entanto, está lendo da memória simbólica. Nem a string nem o ponteiro para a string realmente existem. O solver garante que a solução que fornece é a mesma como se o programa fosse executado até aquele ponto.

Executar este script deve retornar a seguinte saída:

$ python3 solve.py
WARNING | ... | cle.loader | O binário principal é um executável independente de posição. Está sendo carregado com um endereço base de 0x400000.

b'JACE6ACIARNAAIIA'

Agora você pode executar o binário validate em seu dispositivo Android para verificar a solução (consulte Validador de Licença Android).

Você pode obter soluções diferentes usando o script, pois existem várias chaves de licença válidas possíveis.

Para concluir, aprender execução simbólica pode parecer um pouco intimidador no início, pois requer compreensão profunda e prática extensiva. No entanto, o esforço é justificado considerando o tempo valioso que pode economizar em contraste com a análise manual de instruções desmontadas complexas. Normalmente, você usaria técnicas híbridas, como no exemplo acima, onde realizamos análise manual do código desmontado para fornecer os critérios corretos para o mecanismo de execução simbólica. Consulte o capítulo iOS para mais exemplos de uso do Angr.