Jump to content
  • ​Alterando Binários ELF manualmente

       (3 reviews)

    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:

    1586991512.png

    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!

    • Curtir 4


    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

    Fabiano Furtado

       2 of 2 members found this review helpful 2 / 2 members

    Primeiramente, parabéns pelo artigo! Aguardo a parte 2! 🙂

    Olha... através de caminhos diferentes, cheguei nesse mesmo problema dos "0s" abusivos no meio do binário.

    No meu caso, estava tentando fazer o menor "hello world" possível em NASM 64bits com a .text sem null chars, mas os "0s" sempre apareciam. Se quiser tornar o seu binário menor, sem esses "0s", utilize a opção "-z noseparate-code" no ld. Isso fará com que ele não coloque esses "0s" entre as sessões do ELF. Dizem que esses "0s" servem para aumentar a segurança... eu queria entender o porque! Sinceramente, uma sessão .text onde há um oceano de null chars sobrando me parece bem mais suscetível a receber um shellcode que um binário mais exuto.

    Bom... voltando para o "hello world", também fiz algumas alterações na mão nos headers, e coloquei a string do programa lá (incrível como não deu pau!).Se quiser analisar, segue o base64 do binário com apenas 141 bytes! Valeu!

    f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAcABAAAAAAABAAAAAAAAAAEhlbGxvIFdvcmxkCkAAOAABAEAAAAAAAAEAAAAFAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAjQAAAAAAAACNAAAAAAAAAEgxwP7ASInHSI01qf///7IMDwVIMcAEPEgx/w8F

    MD5: fd8ac0bdd57939705e1900dfaa63c74e

    Share this review


    Link to review
    Felipe.Silva

       2 of 3 members found this review helpful 2 / 3 members

    Tenho uma ferramenta em Bash no meu GitHub chamada 'new' que pode ser útil para gerar executáveis assim, sem precisar de modificar "na mão". No repositório tu pode olhar a pasta "templates" que um deles é o bin-elf64.

    A sequência de comandos ficaria assim:

    $ nasm tst.asm -o tst.bin
    $ new bin-elf64 file=tst.bin out=tst
    $ chmod +x tst
    $ ./tst

    Um "Hello World!" fica com 171 bytes.

    Mas um detalhe: Menor não necessariamente significa melhor. Isso aí é só de brincadeira, o executável não vai ficar mais eficiente porque você "capou" ele.

    Mas como passa-tempo é válido.

    Share this review


    Link to review

  • Similar Content

    • By 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:
       

      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.
       

      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:
      ELF Interruptions Breakpoints Interrupt Descriptor Table Qualquer problema/erro por favor me chame 🙂
    • 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á! 😎
    • By julio neves
      Livro do Julio Cezar Neves com dicas importantes (e raras de serem encontradas) sobre shell, incluindo sincronismo de processos, novidades do Bash 4.0, uso do ImageMagik e YAD (o melhor da categoria dos dialog da vida). Vale ler cada palavra. 🙂
    • By Fabiano Furtado
      Pessoal...
      Ontem achei um artigo na Internet bem escrito, interessante e detalhado sobre Engenharia Reversa em ELF.
      É um reversing básico, mas não tããããão básico assim. Acho que vale a pena conferir.
      http://manoharvanga.com/hackme/
      Valeu!
    • By anderson_leite
      Olá, já faz um bom tempo desde do ultimo artigo sobre a construção de debuggers mas, sem mais delongas, vamos dar continuidade a esta série! 😀 
      Neste artigo iremos falar um pouco sobre uma chamada de sistema que é capaz de controlar quase todos os aspectos de um processo: a syscall PTRACE (process trace). Antes de continuarmos, vale ressaltar que todo o código utilizado neste artigo está disponível no repositório do Github.
      De acordo com o manual do Linux (man ptrace), a syscall ptrace é definida assim:
      "A syscall ptrace provê meios para que um processo (denominado "tracer") possa observar, controlar a execução de um outro processo (denominado "tracee"), examinar e modificar a memória e registradores do "tracee". É primariamente utilizado para a implementação de 'breakpoint debugging' e para rastreamento de syscalls".
      Em outras palavras, podemos utilizar a ptrace para controlar um outro processo sobre o qual termos permissões sobre!
      Por exemplo, execute:
      strace /bin/ls O comando "strace" acima, é utilizado para que se possa rastrear todas as syscalls que um programa realiza. Vale lembrar que toda a técnica utilizada para o rastreamento de syscalls envolve o conteúdo abordado nos artigos anteriores, então é de suma importância que você tenha lido (ou saiba) o primeiro artigo sobre Sinais e o segundo sobre Forks.
      Antes de começar a rastrear um dado comando, o strace precisa ter controle total sobre a execução do processo alvo, para isso é feito um fork do processo em questão e o mesmo é "traceado". Voltaremos neste assunto em breve.
      A wrapper da ptrace é definida em <sys/ptrace.h> e tem o seguinte protótipo:
      #include <sys/ptrace.h> long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data); Onde o primeiro argumento request é um enum onde cada valor define uma ação em cima do "tracee", tais como TRACEME, GETEREGS, SETREGS e etc. O segundo argumento, pid, é o PID (Process Identification) do processo que queremos "tracear", o terceiro argumento addr é um endereço para alguma interação que a ser realizada da memória do processo "traceado" e o quarto e último argumento data é algum tipo de dado passado para o processo.
      Agora que você ja conhece o formato desta syscall, vamos fazer um pequeno breakdown do comando "strace".
      Execute:
      strace strace /bin/ls 2>&1 | grep -A2 clone Por mais bizarro que o comando acima pareça, o que vamos fazer aqui é rastrear todas as syscalls que o strace faz usando o próprio strace! Como a saída padrão do strace não é o stdout (dê uma lida em standart streams, caso esteja confuso) então é primeiro redirecionar a saída de erro para a saída padrão, para que seja possível rodar o grep no que queremos.
      Estamos buscando aqui, alguma chamada a syscall clone, que é sempre chamada quando é feito um fork. A chamada à ptrace vem logo em seguida:
      clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c4aa8ea10) = 16203 ptrace(PTRACE_SEIZE, 16203, NULL, 0) = 0 Nesse caso, o strace cria um processo filho e em seguida usa o ptrace com o argumento SEIZE para iniciar o rastreamento (tracing) de um processo sem interrompê-lo, como analisaremos em seguida. Dessa maneira o strace é capaz de interceptar cada chamada de sistema feita pelo processo!
      Dê uma olhada no comando ltrace, que diferente do strace, rastreia todas as chamadas à bibliotecas (libraries trace) e tente fazer o mesmo que fizemos acima!
      Algumas ações notáveis que podemos fazer com a ptrace:
      PTRACE_PEEKTEXT, PTRACE_PEEKDATA Ler uma word em um dado endereço. PTRACE_POKETEXT, PTRACE_POKEDATA Copiar uma word para um determinado endereço (injete dados na memória). PTRACE_GETREGS Ler os registradores de um processo, que será guardado na struct user_regs_struct em <sys/user.h>. PTRACE_SETREGS Escrever nos registradores de um processo (também no formato da struct acima). Execute "man ptrace" para uma abordagem mais detalhadas de todos os valores disponíveis. 👍
       
      Implementando um simples tracer
      Agora que já temos uma base de forks e uma ideia de como o ptrace funciona, podemos unificar os dois e tenho certeza que o ptrace irá ficar mais claro. A partir de agora ele é fundamental para a implementação do nosso debugger.
      O primeiro passo é definir o escopo de como será feito o nosso "tracer": vamos rastrear um processo que já esta sendo executado ou vamos criar um novo? Para o nosso debugger, iremos apenas criar um fork e trocar sua imagem de execução para a do programa que queremos debugar, usando uma das funções da família exec.
      Primeiro vamos usar a função execl, que faz parte do leque de funções exec (man 3 exec) que trocam a imagem do nosso processo por outra, ou seja, o nosso programa é realmente trocado por outro em uma execução.
      A função execl é definida como:
      #include <unistd.h> int execl(const char *pathname, const char *arg, ... /* (char *) NULL */); Onde o primeiro argumento pathname é caminho completo do nosso executável alvo e os demais argumentos, que podem ser vários, são os argumentos para o programa que será executado.
      Para seguir um padrão, o primeiro argumento que geralmente colocamos é o caminho do programa em questão (lembrem que no array argv a posição 0 guarda o nome do programa em si), o resto dos argumentos são opcionais e seguem no modelo de lista de argumentos que são delimitados por um argumento NULL, que geralmente usamos para finalizar a lista.
      Agora considere o seguinte exemplo:
      #include <unistd.h> #include <stdio.h> int main(int argc, char* const* argv) { if (argc < 3) { printf("Usage: %s <command> <args>\n", argv[0]); return 1; } const char* command = argv[1]; char* const* args = &argv[1]; printf("First arg => %s\n", args[0]); execv(command, args); puts("Continua?\n"); return 0; } Compile com
      $ gcc -o exec exec.c $ ./exec /bin/ls -lah Este programa bem simples demonstra como a exec funciona.
      O que acabamos de criar aqui foi uma espécie de wrapper para qualquer comando: ele irá pegar o nome do comando e os seus respectivos argumentos e trocar sua execução atual pela a que você especificou.
      Note também a string "Continue?" que deveria ser impressa na tela. Esta nunca será impressa pois o nosso programa virou de fato, outro.
      Interessante, não? Usando um pouco de criatividade, podemos criar novos processos filhos combinando forks + exec, ou seja, criamos um fork do nosso processo e trocamos sua imagem por outra! Dessa maneira, por exemplo, temos total controle sobre o comando ls.
      Modificando um pouco o código acima e seguindo a ideia de forks, temos:
      #include <stdio.h> #include <sys/types.h> #include <sys/ptrace.h> #include <unistd.h> int main(int argc, char* const* argv) { if (argc < 3) { printf("Usage: %s <command> <args>\n", argv[0]); return 1; } const char* command = argv[1]; char* const* args = &argv[1]; pid_t child_pid = fork(); // Neste ponto, todas as variaveis sao copiadas para o nosso fork // o fork NAO recebe as mesmas variaveis, apenas uma cópia ;) if (!child_pid) { // Hora de transformar nosso fork em outro programa ptrace(PTRACE_TRACEME, NULL, NULL, NULL); execv(command, args); } char in; do { puts("Iniciar processo ? [y/n]: "); in = getchar(); } while (in != 'y'); ptrace(PTRACE_CONT, child_pid, NULL, NULL); return 0; } Compile
      $ gcc -o fork_exec fork_exec. $ ./fork_exec /bin/ls O programa acima realiza os primeiros passos do nosso tracer: é passado o caminho de um programa e os argumentos para o mesmo. Com isso criamos um fork e usamos o ptrace no própio fork com o argumento TRACEME. Este parâmetro indica que o este processo será "traced" pelo seu processo pai. Em seguida trocamos a nossa execução para o nosso programa alvo. Neste momento temos total controle sobre a execução, no exemplo acima, do comando ls.
      Quando um processo inicia sua execução com TRACEME + exec, o mesmo recebe um sinal de interrupção (SIGTRAP) até que o seu processo pai indique que ele deve continuar sua execução. Por isso, o nosso processo pai, que retém o PID do processo filho, usa o ptrace com o argumento CONT para que seja enviado o signal para dar continuidade de execução.
      E depois?
      Agora toda a comunicação entre os processos pai e o filho se dará via sinais e usaremos a syscall wait constantemente.
      Lembra que definimos acima algumas funções que podemos usar em conjunto com a ptrace? Para já irmos adiantando alguns artigos, vamos fazer um programa que mostra o estado dos registradores para um processo, passo a passo. Vamos usar dois parâmetros para a ptrace: GETREGS e STEP. Segue o código:
      #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/ptrace.h> #include <sys/user.h> #include <sys/wait.h> void display_regs(struct user_regs_struct* regs) {     printf("RIP: 0x%x\n", regs->rip);     printf("RBP: 0x%x\n", regs->rbp);     printf("RSP: 0x%x\n", regs->rsp); } int main(int argc, char* const* argv) {     if (argc < 2) {         fprintf(stderr, "Usage: %s <program_path>\n", argv[0]);         return 1;     }     const char* progName = argv[1];          pid_t child = fork();     if (!child) {         ptrace(PTRACE_TRACEME, NULL, NULL, NULL);         execl(progName, progName, NULL);     }          int status;     int options = 0;     int signal;     // Estrutura que mantem os registradores     struct user_regs_struct regs;     /// Capta primeiro sinal de parada do filho     waitpid(child, &status, 0);     signal = WSTOPSIG(status);     if (signal == SIGTRAP) {         printf("Processo alvo %s esperando pronto para iniciar\n\n", progName);     }          printf("Executando 10 instruções\n");     for (int i = 0; i < 10; ++i) {         printf("Passo: %d\n", i+1);         // Executa uma instrução         ptrace(PTRACE_SINGLESTEP, child, NULL, NULL);         // Espera sinal do filho         waitpid(child, &status, 0);         // Copia o estado atual dos registradores         ptrace(PTRACE_GETREGS, child, NULL, &regs);         // Função local para imprimir os principais registradores         display_regs(&regs);         puts("\n\n");     }     puts("Continuando...\n");     /// Continua execução     ptrace(PTRACE_CONT, child, NULL, NULL);     waitpid(child, &status, 0);     printf("Filho saiu com %d\n", WIFEXITED(status));     return 0; }  
      Compile:
      $ gcc -o tracer tracer.c $ ./tracer /bin/ls O código acima, além de criar e rastrear o processo, executa as primeiras 10 instruções e copia os estados dos registradores em cada passo. Logo após, continua a execução do programa normalmente.
      A estrutura user_reg_struct, definida em <sys/user.h>, contém todos os registradores que estão disponíveis na sua arquitetura. O código foi escrito considerando um ambiente x86-64.
      Com o estudo da ptrace, fechamos toda a introdução para construirmos o nosso debugger de fato, que vamos começar a desenvolver no próximo artigo, incialmente com capacidade de por breakpoints, imprimir o atual estado dos registrados e executar instrução por instrução do processo.
      Qualquer dúvida ou correção sinta-se livre de por nos comentários!  😁
      Links úteis:
      Process control Process relationship Code injection with ptrace Sinais Fork Até a próxima!
×
×
  • Create New...