Jump to content
  • Construindo seu debugger - Parte 4 (final): Implementação

       (1 review)

    anderson_leite

    Já faz um bom tempo (quase 1 ano!) desde o último artigo da série de desenvolvimento de debuggers. Este é o último artigo da série e iremos finalmente criar nosso primeiro prototipo de debugger.

    A ideia aqui, é compilar tudo que foi ensinado nos artigos anteriores sobre Sinais, Forks e ptrace . Com isso, criaremos um simples tracer em C que irá receber um endereço como argumento e colocar um breakpoint no mesmo.

    Diagrama

    Antes vamos definir um pouco o escopo do nosso software:

     

    image.png.dfff5b52f31853b9445fff07335e5284.png

    O nosso tracer irá criar um fork e nesse fork será feita a chamada para a execv, que por sua vez irá trocar a imagem do atual processo (seu conteúdo) pela de outro processo, fazendo com que de fato vire outro processo. Já o nosso debugger, dentro de um loop, irá se comunicar via sinais com o processo filho.

    Breakpoints

    Existem dois tipos de breakpoints: software breakpoints e hardware breakpoints. Ambos servem para interromper a execução do nosso software em determinada instrução. Para que isso seja possível é necessário que a execução do processo seja interrompida na nossa CPU.

    Interrupções

    Quando ocorre algum evento no computador que precisa de um tratamento imediato, a CPU invoca uma interrupção. Cada evento desse contém uma ação especifica que nosso kernel irá lidar de alguma maneira e a estrutura responsável por salvar os valores e significados das mesmas é a Interrupt Descriptor Table.

     

    image.thumb.png.e98d3bb41f9a61d7ce737625572a41a1.png

    A imagem acima representa visualmente uma implementação desse vetor, onde cada posição (offset) contém uma struct associada e nela os valores necessários para lidar com isso. Você pode ter uma explicação mais detalhada aqui.

    Mas por que eu estou falando de tudo isso? Porque breakpoints nada mais são do que uma interrupção em um dado endereço que faz com que o processador pare a execução do seu programa.

    O valor que interrompe a CPU para um breakpoint é o 0x03. Vamos testar isto nesse pequeno bloco de código:

    main() {
        int x = 4; // Iniciando qualquer coisa
        __asm__(
            "int $0x03"
        );
    }

    A macro __asm__ permite que seja colocado o código direto em assembly, nesse caso, foi colocado o mnémonico INT, que cuida das interrupções com o valor 3 (offset comentado acima na IDT). Se você compilar e executar esse programa:

    ~ ./code                                                                  
    zsh: trace trap (core dumped)  ./code

    Nesse momento o trabalho de fazer o handle dessa interrupção é do nosso software. O que fizemos aqui foi implementar um software breakpoint. Agora vamos executar esse programa no gdb e não por breakpoint algum (dentro do gdb) e só executar:

    (gdb) r
    Starting program: /home/zlad/code
    
    Program received signal SIGTRAP, Trace/breakpoint trap.
    0x000055555555515f in main ()
    (gdb) disas
    Dump of assembler code for function main:
       0x0000555555555139 <+0>: push   %rbp
       0x000055555555513a <+1>: mov    %rsp,%rbp
       0x000055555555513d <+4>: sub    $0x10,%rsp
       0x0000555555555141 <+8>: movl   $0x2,-0x4(%rbp)
       0x0000555555555148 <+15>:    mov    -0x4(%rbp),%eax
       0x000055555555514b <+18>:    mov    %eax,%esi
       0x000055555555514d <+20>:    lea    0xeb0(%rip),%rdi
       0x0000555555555154 <+27>:    mov    $0x0,%eax
       0x0000555555555159 <+32>:    callq  0x555555555030 <printf@plt>
       0x000055555555515e <+37>:    int3  
    => 0x000055555555515f <+38>:    mov    $0x0,%eax
       0x0000555555555164 <+43>:    leaveq
       0x0000555555555165 <+44>:    retq  
    End of assembler dump.
    (gdb) 

    Veja que a nossa interrupção foi capturada pelo GDB, pois ele detectou um breakpoint trap e é exatamente isso que iremos fazer. Nosso tracer será capaz de detectar quando irá ocorrer um SIGTRAP, ou seja, um sinal que deve ser tratado por nosso sistema operacional.

    Finalmente implementando

    Vamos finalmente começar o nosso pequeno tracer, que será capaz colocar breakpoints, executar instrução por instrução e imprimir os registradores na tela!

    Para inserir a interrupção de breakpoint (int 3) não precisamos de muito, pois já existe um mnemónico para isso que é o int3 e que tem como valor 0xCC. Para inserir breakpoints precisamos de um endereço (que vá ser executado) e uma maneira de escrever nesse local na memória virtual do nosso processo.

    Já vimos anteriormente o ptracer e nele sabemos que temos alguns enums que podem ser passados como seu primeiro argumento. São eles o PEEK_DATA e o POKE_DATA, que servem para buscar algo na memória de um processo e escrever algo na memória de um processo, respectivamente. Segue a função que vamos usar para adicionar breakpoints no nosso tracer:

    uint64_t add_breakpoint(pid_t pid, uint64_t address)
    {
        uint64_t break_opcode = 0xCC;
        uint64_t code_at = ptrace(PTRACE_PEEKDATA, pid, address, NULL);
        uint64_t breakpoint_code = (code_at & ~0xFF) | break_opcode;
           
        ptrace(PTRACE_POKEDATA, pid, address, breakpoint_code);
       
        return code_at;
    }

    Respire fundo e vamos em partes, a ideia aqui é a seguinte:

    Dado o pid do nosso processo filho e um endereço de memória, vamos buscar o código que estava naquele local (code_at), salvar esse código (não só queremos adicionar um novo opcode, mas podemos futuramente querer executá-lo) e então vamos adicionar nossa instrução nos bytes menos significativos, ou seja, vamos executar ela primeiro.

    Usamos aqui uma variável de 64 bits por conta da arquitetura do meu sistema. Se você quiser tornar isto portável, é possível criar uma variável genérica baseada na arquitetura:

    #ifdef __i386__
    #define myvar uint32_t
    #else
    #define myvar uint64_t
    #endif

    Isso é opcional, mas caso você queira criar algo mais genérico, esse é o caminho.

    A operação bitwise que fizemos aqui também pode ser “nebulosa” para alguns, mas segue o equivalente de maneira mais “verbosa” e em python:

    >>> hex(0xdeadbeef & ~0xFF) # Mascarando byte menos significativo
    '0xdeadbe00'
    >>> hex(0xdeadbeef & ~0xFF | 0xCC) # Mascarando byte e adicionado opcode int3(0xCC)
    '0xdeadbecc'

    O que é feito aqui é uma jogada lógica. Vamos quebrar isso em passos:

    • Fazemos um AND com algo negado (0xFFFFFF00);
    • Fazemos um OR com o resultado que irá "preencher" o espaço vazio, visto que um valor OR 0 será sempre o valor com conteúdo;
    • No final mascaramos o último byte e colocamos nosso opcode;

    O nosso loop precisa executar enquanto nosso processo filho estiver sendo debugado. Em termos de estrutura de códigos vamos usar um laço que irá receber uma flag para sua execução:

    while (!WIFEXITED(status)) {
        // Our code
    }

    Caso você esteja perdido nessa função WIFEXITED, vale a pena dar uma olhada no artigo desta série sobre Forks. Agora é puramente uma questão de jogar com sinais e estruturar nosso código da maneira mais coesa possível, resumindo, pura programação 🙂

    Após nosso breakpoint ser definido em memória precisamos fazer o handling disso. Para isso usamos a função WSTOPSIG, que irá receber o status do nosso processo (que é atribuído na função wait) e irá nos dizer qual tipo de interrupção ocorreu:

    while (!WIFEXITED(status)) {
        wait(&status);
        signal = WSTPOPSIG(status);
    
        switch(signal) {
            case SIGTRAP:
                puts("We just hit a breakpoint!\n");
                display_process_info(pid);
                break;
        }
    }

    No momento que uma sigtrap for enviada para a gente podemos considerar que caímos no nosso breakpoint. Nesse momento, nosso processo filho está block (pois sofreu uma interrupção), esperando algum tipo de ação para continuar.

    A função display_process_info(pid) irá mostrar o atual estado dos nossos registrados, usando o enum PTRACE_GETREGS que recebe a struct regs (também já visto no artigo passado):

    void display_process_info(pid_t pid)
    {
        struct user_regs_struct regs;
        ptrace(PTRACE_GETREGS, pid, NULL, &regs);
       
        printf("Child %d Registers:\n", pid);
        printf("R15: 0x%x\n", regs.r15);
        printf("R14: 0x%x\n", regs.r14);
        printf("R12: 0x%x\n", regs.r12);
        printf("R11: 0x%x\n", regs.r11);
        printf("R10: 0x%x\n", regs.r10);
        printf("RBP: 0x%x\n", regs.rbp);
        printf("RAX: 0x%x\n", regs.rax);
        printf("RCX: 0x%x\n", regs.rcx);
        printf("RDX: 0x%x\n", regs.rdx);
        printf("RSI: 0x%x\n", regs.rsi);
        printf("RDI: 0x%x\n", regs.rdi);
        printf("RIP: 0x%x\n", regs.rip);
        printf("CS:  0x%x\n", regs.cs);
        printf("EFGLAS: 0x%x\n", regs.eflags);
    }

    O código do nosso loop final fica da seguinte forma:

    while (!WIFEXITED(status)) {
        signal = WSTOPSIG(status);
    
        switch(signal) {
            case SIGTRAP:
                puts("We just hit a breakpoint!\n");
                break;
        }
       
        printf("> ");
        fgets(input, 100, stdin);
        if (!strcmp(input, "infor\n")) {
            display_process_info(pid);
        } else if (!strcmp(input, "continue\n")) {
            ptrace(PTRACE_CONT, pid, NULL, NULL);
            wait(&status);
        }
    
    }
    
    printf("Child %d finished...\n", pid);
    return 0;
    }

    Não iremos focar em implementação pela parte da interação do úsuario pois não é o foco dessa série de artigos. Tentei ser o mais “verboso” possível no quesito UX 😃. No projeto original usei a lib linenoise para criar uma shell interativa, mas isso fica para sua imaginação.

    Vamos executar:

    ~/.../mentebinaria/artigos >>> ./tracer hello 0x401122 #<== Endereco da main                  [130]
    Forking...
    
    Adding breakpoint on 0x401122
    We just hit a breakpoint!
    
    > infor
    Child 705594 Registers:
    R15: 0x0
    R14: 0x0
    R12: 0x401050
    R11: 0x2
    R10: 0x7
    RBP: 0x0
    RAX: 0x401122
    RCX: 0x225d7578
    RDX: 0x19a402c8
    RSI: 0x19a402b8
    RDI: 0x1
    RIP: 0x401123
    CS:  0x33
    EFGLAS: 0x246
    We just hit a breakpoint!
    
    > continue
    Hello world
    Child 705594 finished...

    A ideia aqui não é criar tudo para você. A partir de agora, com o conhecimento básico dessa série de artigos, é possível criar o seu próprio debugger ou ferramenta semelhante. Deixo aqui o meu projeto, sdebugger, que foi fruto do meu estudo sobre este tema. Todo conhecimento base que eu passei aqui foi o necessário para criar este projetinho.

    Agradeço a toda turma do Mente Binária pelo apoio e desculpa à todos pela demora para finalizar essa série de artigos. Tenho várias ideias para artigos futuros, então vamos nos ver em breve!

    Links úteis:

    Qualquer problema/erro por favor me chame 🙂



    User Feedback

    Join the conversation

    You can post now and register later. If you have an account, sign in now to post with your account.

    Guest

    Josicler Leme Silva

      

    Muito excelente artigo parabéns 

    Share this review


    Link to review

  • Similar Content

    • By l0gan
      Em todos os sistemas operacionais existem arquivos estruturados. Imagine um bloco segmentado em diversas partes e cada uma sendo uma área que armazena um tipo de dado específico (ex.: cabeçalho, área de código, área de dado inicializado, área de dado estático, área de dado não inicializado, área de referência de definições externas/outros objetos) servindo de referência para resguardar determinada classe de dado do respectivo arquivo binário para serem usados durante a execução do software ou até mesmo para fornecer informações que ajudam no processo de debugging. O conceito dessa formatação do arquivo (file format) é presente em todos os sistemas operacionais populares como Windows e Unix-like – isso inclui o macOS.
      Sabendo que o macOS é um sistema operacional do Unix é de se esperar que seus arquivos binários também tenham um “formato”, e estes são conhecidos como “arquivos de objeto do Mac” ou simplesmente Mach-O. Com esse entendimento o propósito deste artigo é dar uma visão técnica geral sobre a estrutura de arquivos construídos com este formato.
       
      Por que é importante conhecer o formato Mach-O?
      Algumas pessoas acreditam que o sistema operacional macOS (atualmente na versão denominada Catalina) é mais seguro que outros sistemas operacionais existentes pelo fato de não ser afetado por malware. Grande engano! Atualmente, vemos muitas publicações de vulnerabilidades relacionadas ao macOS, o que demonstra que este sistema operacional é, sim, um alvo em potencial.
      A grande pergunta que sempre faço é: “O que é mais interessante para um criminoso?”. Neste contexto, por “criminoso” me refiro à qualquer indivíduo que se utiliza dos meios eletrônicos para cometer fraudes. Deixando dispositivos móveis de lado, minha opinião é que hajam duas alternativas principais:
      Infectar o maior número de hosts possível (Windows ou Linux); Infectar um número mais restrito de hosts, porém algo mais direcionado a usuários, em geral, de cargos executivos, por exemplo: Diretores, CSO, etc. ou usuários domésticos, que muitas vezes permitem que softwares de fonte desconhecida sejam executados livremente em seu sistema operacional, ao desativar mecanismos de segurança como o gatekeeper; Se eu fosse um criminoso, optaria pela segunda opção; pois, atualmente o MacBook está se tornando cada vez mais popular.
      A imagem abaixo nos mostra a grande quantidade de arquivos Mach-O que foram analisados no VirusTotal nos últimos 7 dias desde a escrita deste artigo:

      Estes são os tipos de arquivos submetidos ao VirusTotal nos últimos 7 dias, obtidos em 25/julho/2020.
      Repare que a imagem não reporta arquivos infectados, mas sim os binários de cada tipo analisados. Bom, é perceptível que Mach-O está ganhando uma certa predominância hoje em dia, embora ainda seja bem inferior ao número do arquivo executável do Windows (Win32.exe).
      Apenas a título de curiosidade, o Mach-O tem um formato multi arquitetura, também conhecido como “fat binary” (conforme podemos ver na imagem abaixo)  aonde ele suporta 3 tipos de arquiteturas diferentes: x86_64, i386 e ppc7400:


      Aqui temos uma tabela com todos os “Magic Number” (valor numérico de texto usado para identificar um formato de arquivo) referentes à binários do tipo Mach-O:

      Ainda nesta linha de pesquisa, a técnica utilizada para gerar um binário suportado com várias plataformas (cross-compiling) é demonstrada na imagem  abaixo utilizando o compilador gcc:

      Usando o comando file do macOS vemos o tipo do arquivo e a arquitetura da plataforma que é suportado:

      O formato Mach-O de 64-bits
      Conforme observado anteriormente os binários Mach-O tem três regiões principais: Cabeçalho (Header); Comandos de carregamento (Load Commands); e, Dados (Data). A imagem abaixo representa a estrutura básica dos arquivos Mach-O 64-bit:

      No Header, encontram-se especificações gerais do binário, como seu magic number e a arquitetura alvo. Podemos encontrar este header em /usr/include/mach-o/loader.h:

      Conhecendo um pouco mais a estrutura do mach header podemos notar que ela é composta por 8 membros, cada um possuindo 4 bytes, ou seja: 4 * 8 = 32. Podemos ver os primeiros 32 bytes do binário, isto é, os valores do header abaixo:


      A região Load Commands especifica a estrutura lógica do arquivo e informações para que o binário possa ser carregado em memória e utilizado pelo sistema. Ela é composta por uma sequência de diversos modelos de commands numa tupla, por exemplo: “[load_command, specific_command_headers]” -- definindo as diferentes “seções lógicas” (commands) do binário. Cada command necessita de um ou mais cabeçalhos específicos, por isso, o segundo membro da tupla (specific_command_headers) pode variar de acordo com o tipo de command da mesma em questão:

      A título de exemplo, podemos ver também o command LC_SEGMENT_64  do cabeçalho do binário Mach-O:

      Neste mesmo contexto, podemos ver que as bibliotecas dinâmicas (dylib) "libncurses" e "libSystem" foram carregadas nos commands 12 e 13, que pertencem ao cabeçalho LC_LOAD_DYLIB.
      Deste jeito, o kernel consegue mapear as informações do executável para um espaço de memória que pode ser acessado simultaneamente por múltiplos programas na finalidade de prover comunicação entre eles ou para evitar compartilhamento de dados supérfluos – tal conceito é conhecido como memória compartilhada:

      Podemos ver também que a section __text contém o segmento __TEXT:

      E por fim temos a Data, onde temos instruções armazenadas logo após a região LOAD_Commands. Na região Data é que são definidas as permissões de leitura e gravação. Dependendo do tipo de Mach-O a maneira como essa região é usada varia.
      Quando analisamos um binário um dos primeiros pontos para o início dos testes é a inspeção do binário em um debugger a partir de seu entrypoint. No caso do deste Mach-O que estamos analisando percebemos que o código é colocado na seção __TEXT, as bibliotecas são carregadas no cabeçalho LC_LOAD_DYLIB e o LC_MAIN é o cabeçalho que aponta para o ponto de entrada (entrypoint) :


      Por enquanto já temos uma noção básica da estrutura dos binários Mach-O. Em um próximo artigo, iremos detalhar melhor este binário com foco em engenharia reversa para identificar ações de software malicioso.

      Para ajudar, recomendo a você artigos da H2HC Magazine sobre pilhas, registradores etc., dos colegas Fernando Mercês, Ygor da Rocha Parreira, Gabriel Negreiros, Filipe Balestra e Raphael Campos nas edições 7, 8, 9, 10 e 11. Outra referência para auxiliar nesta análise é o artigo "Montando sua máquina virtual para engenharia reversa em macOS"[11].

      Até lá!

      Referências
      Palestra H2HC University Vídeo Demo Malware Keranger Mach-O Vídeo Demo Crackme Mach-O Calling Conventions OS X ABI Mach-O File Format Revista H2HC ed7 Revista H2HC ed8 Revista H2HC ed9 Revista H2HC ed10 Revista H2HC ed11 Montando sua máquina virtual para engenharia reversa em macOS
    • By Marioh
      Cá estava eu programando com o nasm, tentando (apenas tentando mesmo) reproduzir os wrappers de systemcall que existem na glibc, quando me deparei com o tamanho de um bináriozinho em assembly que só retorna um valor, um "hello world" no nasm, ali no canto do diretório. O binário tinha 4.2K, nada realmente muito pesado, mas para um programa que não utiliza nenhuma biblioteca e só retorna um valor me pareceu muito estranho.
      Código do programa:
      BITS 32 global _start _start: mov eax, 1 mov ebx, 10 int 0x80 Para compilar e testar:
      [mario@zrmt rivendell]$ nasm -f elf32 elrond.asm [mario@zrmt rivendell]$ ld -m elf_i386 -s elrond.o -o elrond [mario@zrmt rivendell]$ ./elrond [mario@zrmt rivendell]$ echo $? 10 Aqui vai o hexdump do binário:
      [mario@zrmt rivendell]$ hexdump -C elrond 00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 00000010 02 00 03 00 01 00 00 00 00 90 04 08 34 00 00 00 |............4...| 00000020 20 10 00 00 00 00 00 00 34 00 20 00 02 00 28 00 | .......4. ...(.| 00000030 03 00 02 00 01 00 00 00 00 00 00 00 00 80 04 08 |................| 00000040 00 80 04 08 74 00 00 00 74 00 00 00 04 00 00 00 |....t...t.......| 00000050 00 10 00 00 01 00 00 00 00 10 00 00 00 90 04 08 |................| 00000060 00 90 04 08 0c 00 00 00 0c 00 00 00 05 00 00 00 |................| 00000070 00 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00001000 b8 01 00 00 00 bb 2a 00 00 00 cd 80 00 2e 73 68 |......*.......sh| 00001010 73 74 72 74 61 62 00 2e 74 65 78 74 00 00 00 00 |strtab..text....| 00001020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00001040 00 00 00 00 00 00 00 00 0b 00 00 00 01 00 00 00 |................| 00001050 06 00 00 00 00 90 04 08 00 10 00 00 0c 00 00 00 |................| 00001060 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00 |................| 00001070 01 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 |................| 00001080 0c 10 00 00 11 00 00 00 00 00 00 00 00 00 00 00 |................| 00001090 01 00 00 00 00 00 00 00 |........| 00001098 Da pra perceber que de 0x72 à 0xfff todos os bytes são 0. Humm... suspeito. Não sou especialista e posso estar terrívelmente errado, mas não lembro dessa quantidade de zeros no manual do formato ELF. Se abrirmos o binário com o readelf veremos o seguinte:
      [mario@zrmt rivendell]$ readelf elrond -h ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x8049000 Start of program headers: 52 (bytes into file) Start of section headers: 4128 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 2 Size of section headers: 40 (bytes) Number of section headers: 3 Section header string table index: 2 Três Section Headers, dois Program Headers e mais um bando de coisa. Como não precisamos das seções para executar o programa irei ignorá-las por agora. Não precisamos das seções para executar o programa devido ao fato de que elas são feitas para auxiliar o linker no momento de construção do binário. Como o binário já está construído e nenhuma das seções representa objetos dinâmicos, elas podem ser ignoradas.
      Então vamos diminuir esse programa aí. Primeiramente, devemos descobrir o endereço base do programa, para isto, basta pegar o entrypoint (0x8049000) e diminuir o offset do Program Header que tem a flag de executável (que vai conter o devido código do programa). Lembrando que o entrypoint é composto pelo endereço base do programa (para ser mapeado em memória) + “endereço” (no arquivo) do primeiro byte que corresponde ao código executável. O que vamos fazer aqui é achar esse primeiro byte, que pode ser encontrado no Program Header, onde se tem a flag de executável que recebe o nome de p_offset. Vejamos o readelf -l:
      [mario@zrmt rivendell]$ readelf -l elrond Elf file type is EXEC (Executable file) Entry point 0x8049000 There are 2 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x08048000 0x08048000 0x00074 0x00074 R 0x1000 LOAD 0x001000 0x08049000 0x08049000 0x0000c 0x0000c R E 0x1000 Section to Segment mapping: Segment Sections... 00 01 .text Para ajudar: de acordo com o manual o campo p_offset é “O offset do início do arquivo onde o primeiro byte do segmento se encontra”. Como estamos lidando com um segmento executável esse primeiro byte vai ser o início do nosso código.
      Então dá para ver que o segundo Program Header (que possui a flag de executável) tem offset 0x001000! Então o endereço base é 0x08048000 (0x08049000 - 0x00001000) ! Já que temos o endereço base podemos excluir os zeros (caso contrário o programa ficaria quebrado e não iríamos conseguir analisá-lo com o readelf), alto lá! Apenas os inúteis! Mas quais são os inúteis ? Todos os que os Program Headers apontam, pois esses serão os  bytes do programa mapeados em memória, então vamos deixar eles lá. Vou usar o hyx como editor hexa, mas o hte também funciona.
      Após excluirmos todos os zeros entre 0x74 e 0x1000:
      [mario@zrmt rivendell]$ hyx elrond 0000> 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 0010: 02 00 03 00 01 00 00 00 00 90 04 08 34 00 00 00 |............4...| 0020: 20 10 00 00 00 00 00 00 34 00 20 00 02 00 28 00 | .......4. ...(.| 0030: 03 00 02 00 01 00 00 00 00 00 00 00 00 80 04 08 |................| 0040: 00 80 04 08 74 00 00 00 74 00 00 00 04 00 00 00 |....t...t.......| 0050: 00 10 00 00 01 00 00 00 00 10 00 00 00 90 04 08 |................| 0060: 00 90 04 08 0c 00 00 00 0c 00 00 00 05 00 00 00 |................| 0070: 00 10 00 00 00 b8 01 00 00 00 bb 2a 00 00 00 cd |...........*....| 0080: 80 00 2e 73 68 73 74 72 74 61 62 00 2e 74 65 78 |...shstrtab..tex| 0090: 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |t...............| 00a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 0b 00 00 |................| 00c0: 00 01 00 00 00 06 00 00 00 00 90 04 08 00 10 00 |................| 00d0: 00 0c 00 00 00 00 00 00 00 00 00 00 00 10 00 00 |................| 00e0: 00 00 00 00 00 01 00 00 00 03 00 00 00 00 00 00 |................| 00f0: 00 00 00 00 00 0c 10 00 00 11 00 00 00 00 00 00 |................| 0100: 00 00 00 00 00 01 00 00 00 00 00 00 00 |.............| Ahh muito mais enxuto! Porém o bicho tá todo quebrado. Se executarmos:
      [mario@zrmt rivendell]$ ./elrond Bus error (core dumped) Um “Bus error” não é nada mais que uma tentativa de read ou write em um espaço de memória desalinhado. Como citado no manual os mapeamentos tem que ser alinhados com as páginas de memória, ou seja, 4KB.
      Vamos consertá-lo! Vamos ter que consertar: o entrypoint e o mapeamento do segundo Program Header, ou seja, seu endereço virtual, físico e seu offset. Como estamos alterando as posições dos segmentos (isto é, o nome oficial para o que um Program Header mapeia)  teremos que alterar seu mapeamento no arquivo junto com o entrypoint (que aponta para o primeiro byte de um segmento executável). Na verdade, o endereço físico pode ser ignorado, o manual cita que os “System V” ignoram endereços físicos de aplicações, mas iremos adicioná-los em prol da completude.
      Revisando... o entrypoint vai ser o endereço base mais o offset do segundo Program Header, e esse offset vai ser 0x75 (lembre-se que era 0x1000, mas com a retirada dos zeros entre 0x74 e 0x1000 efetivamente reduzimos o entrypoint em 0xFFF - 0x74 = 0xF8B,  logo, o entrypoint vai ser 0x1000 - 0xF8B = 0x75) então nosso entrypoint vai ser 0x08048075. Esse também vai ser o endereço virtual e o endereço físico do header.
      Então troquemos:
      O entrypoint no Header ELF por 0x08048075 O offset do section header por 0x00000075 Os endereços virtuais e físicos do segundo Program Header por 0x08048075 Agora mais do que nunca teremos que ter atenção. Saque seu editor de hexa preferido e lembre-se que estamos lidando com little endian. Vou usar o hyx, que é um editor hexa um pouco parecido com o vi:

      No terminal de cima temos o arquivo original sem os zeros, já no de baixo temos o arquivo já alterado.
      Para ajudar:
      Vermelho: Entrypoint Amarelo: Offset do Header Verde: Endereço Virtual do Header Azul: Endereço Físico do Header Agora se executarmos:
      [mario@zrmt rivendell]$ ./elrond [mario@zrmt rivendell]$ echo $? 10 Como disse lá em cima, não alterei as seções e nesse caso (binário já linkado e sem bibliotecas dinâmicas) elas não são importantes. Tente ler elas pra ver o que acontece.
      No fim passamos de 4.2k para ...
      [mario@zrmt rivendell]$ ls -lh elrond -rwxr-xr-x 1 mario mario 269 --- -- --:-- elrond 269!
      Achei que a galera poderia gostar dessa pequena aventura, acho bem interessante principalmente para aprender bem sobre o formato. Se gostarem tenho planos pra parte dois!
    • By Fernando Mercês
      Ano passado eu assisti à uma palestra sobre esse novo utilitário da suíte GNU chamado poke. Ele é um editor de dados binários de linha de comando bem diferente dos que costumo usar (HT Editor, Hiew, etc). Hoje decidi testá-lo e curti bastante. Tá em mega beta, então não tá nem perto de ter pacote disponível nos repositórios oficiais das distros Linux, mas consegui compilar e neste artigo vou dar as instruções, que podem variar em cada ambiente, até porque o poke está em constante desenvolvimento. Usei um ambiente Debian testing aqui.
      Instalando as dependências
      A dependência mais chatinha de instalar foi a gettext, porque o pacote pronto dela não foi suficiente. Então tive que clonar e compilar:
      $ sudo apt install perf fp-compiler fp-units-fcl groff build-essential git $ git clone https://git.savannah.gnu.org/git/gettext.git $ cd gettext $ ./gitsub.sh pull $ ./autogen.sh $ ./configure $ make $ sudo make install Com a gettext instalada, agora podemos partir para as demais dependências do poke:
      $ sudo apt install build-essential libgc-dev libreadline-dev flex libnbd-dev help2man texinfo Só então podemos seguir para a compilação do poke.
      Compilando o poke
      $ git clone git://git.savannah.gnu.org/poke.git $ cd poke $ ./bootstrap $ ./configure $ make $ sudo make install Criando links para as bibliotecas
      Como instalei as bibliotecas do poke em /usr/local e o meu sistema não tinha este diretório configurado para que o loader busque as bibliotecas, precisei criar dois links para elas em /usr/lib:
      $ sudo ln -s /usr/local/lib/libpoke.so.0 /usr/lib/libpoke.so.0 $ sudo ln -s /usr/local/lib/libtextstyle.so.0 /usr/lib/libtextstyle.so.0 Sei que há outras maneiras de resolver isso, mas fiz assim pra acelerar, afinal eu queria mexer no poke logo! 🤪
      Abrindo um binário PE no poke
      Baixei o executável do PuTTY para brincar um pouco e abri assim:
      $ poke putty.exe _____ ---' __\_______ ______) GNU poke 0.1-beta __) __) ---._______) Copyright (C) 2019, 2020 Jose E. Marchesi. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Powered by Jitter 0.9.212. Perpetrated by Jose E. Marchesi. hserver listening in port 47209. For help, type ".help". Type ".exit" to leave the program. (poke) Gerenciando os arquivos abertos
      O poke permite trabalhar com múltiplos arquivos de uma vez. Você pode ver a lista de arquivos abertos com o seguinte comando:
      (poke) .info ios Id Mode Size Name * #0 rw 0x0010b990#B ./putty.exe ios signifca "IO Spaces". Não tem nada a ver com o SO da Cisco ou com o da Apple. hehe
      Se quiser abrir outro arquivo, pode usar o comando .file <arquivo> e aí pode selecionar em qual você quer trabalhar com o comando .ios #n onde n é o número que identifica o arquivo, mas vou seguir o artigo com somente um arquivo aberto mesmo, então só teremos a tag #0.
      Dumpando dados
      Um dos principais comandos do poke é o dump (perceba este não começa com um ponto) que basicamente visualiza o conteúdo do arquivo, mas este tem várias opções. Vamos à mais básica:

      A primeira linha na saída acima é só uma régua pra te ajudar a encontrar os bytes.
      Fiz questão de colar uma captura de tela aí acima pra você ver que o poke colore a saída, mas nos exemplos seguintes vou colar a saída em texto pelo bem da sua largura de banda. 🙂
      Por padrão, o dump exibe 128 bytes do arquivo, começando do seu primeiro byte. O número de bytes pode ser alterado na própria linha de comando:
      (poke) dump :size 64#B 76543210 0011 2233 4455 6677 8899 aabb ccdd eeff 0123456789ABCDEF 00000000: 4d5a 7800 0100 0000 0400 0000 0000 0000 MZx............. 00000010: 0000 0000 0000 0000 4000 0000 0000 0000 ........@....... 00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000030: 0000 0000 0000 0000 0000 0000 7800 0000 ............x... A sintaxe pode parecer um pouco estranha no início, mas você acostuma rápido. O sufixo #B diz que a unidade usada é bytes. Você pode testar outros valores como 2#KB ou 1#MB por exemplo.  😉
      Dumpando a partir de posições específicas
      Para dumpar a partir de uma posição específica, podemos usar a opção :from do comando dump:
      (poke) dump :from 0x30#B :size 32#B 76543210 0011 2233 4455 6677 8899 aabb ccdd eeff 0123456789ABCDEF 00000030: 0000 0000 0000 0000 0000 0000 7800 0000 ............x... 00000040: 0e1f ba0e 00b4 09cd 21b8 014c cd21 7468 ........!..L.!th No comando acima eu pedi para o poke me mostrar 32 bytes a partir da posição 0x30. Seria o equivalente a fazer hd -n 32 -s 0x30 <arquivo>.
      O poke mantém um ponteiro de leitura no arquivo, por isso se você comandar somente dump novamente, o dump ocorrerá a partir da última posição lida (no caso, 0x30). Se quiser voltar o ponteiro para a posição zero, é a mesma sintaxe: dump :from 0#B.
      Interpretando dados
      O dump sempre te entrega uma saída em hexadecimal, mas e se quisermos interpretar os dados e exibi-los de maneiras diferentes? Para  isso a gente larga de mão o comando dump e começa a operar com o jeito do poke de ler e interpretar especificamente, assim:
      (poke) byte @ 0#B 77UB O sufixo UB significa Unsigned Byte.
      Se eu quiser a saída em hexa por exemplo, basta eu setar a variável obase (output base):
      (poke) .set obase 16 (poke) byte @ 0#B 0x4dUB Eu poderia querer ler 2 bytes. Tranquilo:
      (poke) byte[2] @ 0#B [0x4dUB,0x5aUB] Posso interpretar o conteúdo como número também:
      (poke) uint16 @ 0#B 0x4d5aUH O prefixo UH significa Unsigned Half (Integer). Perceba que o poke sabe que um uint16 tem 2 bytes e por isso os lê sem a necessidade que especifiquemos o número de bytes a serem lidos.
      À essa altura você já sacou que equivalentes aos tipos padrão da linguagem C (da inttypes.h na real) estão disponíveis para uso né? Fique à vontade pra testar off64, int64, int32, etc.
      Lendo strings
      Além dos tipos numéricos, o poke tem o tipo string, onde ele lê até encontrar um nullbyte:
      (poke) dump 76543210 0011 2233 4455 6677 8899 aabb ccdd eeff 0123456789ABCDEF 00000000: 4d5a 7800 0100 0000 0400 0000 0000 0000 MZx............. 00000010: 0000 0000 0000 0000 4000 0000 0000 0000 ........@....... 00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000030: 0000 0000 0000 0000 0000 0000 7800 0000 ............x... 00000040: 0e1f ba0e 00b4 09cd 21b8 014c cd21 5468 ........!..L.!Th 00000050: 6973 2070 726f 6772 616d 2063 616e 6e6f is program canno 00000060: 7420 6265 2072 756e 2069 6e20 444f 5320 t be run in DOS 00000070: 6d6f 6465 2e24 0000 5045 0000 4c01 0700 mode.$..PE..L... (poke) string @ 0x4d#B "!This program cannot be run in DOS mode.$" Patch simples
      Vamos fazer um patch simples: alterar o "T" desta string acima de maiúsculo para minúsculo. Basicamente é só colocar à esquerda o jeito que acessamos uma determinada posição do arquivo e igualar ao que a gente quer. Sabendo que para converter maiúsculo para minúsculo na tabela ASCII basta somar 32 (0x20), podemos fazer:
      (poke) byte @ 0x4e#B = 0x74 Perceba que fui na posição 0x4e, porque na 0x4d temos o '!' e não o 'T'. Só pra checar se funcionou:
      (poke) string @ 0x4d#B "!this program cannot be run in DOS mode.$" (poke) Legal né? Mas dá pra ficar melhor. O poke suporta char, então podemos meter direto:
      (poke) char @ 0x4e#B = 't' (poke) string @ 0x4d#B "!this program cannot be run in DOS mode.$" Por hora é só. Fica ligado aí que postarei a parte 2 em breve, onde vou mostrar mais recursos do poke que tô achando bem úteis para engenharia reversa. Até lá! 😎
×
×
  • Create New...