Jump to content

Search the Community

Showing results for tags 'debuggers'.



More search options

  • Search By Tags

    Type tags separated by commas.
  • Search By Author

Content Type


Forums

  • Supporter area
    • Tools of the Trade
    • Finance transparency
  • MBConf
    • MBConf v1
    • MBConf v2
    • MBConf v3
  • Mente Binária
    • General
    • Arquitetura de Computadores
    • Certifications
    • Quantum computing
    • Cryptography
    • Challenges and CTF
    • Hardware Hacking
    • Electronics
    • Conferences
    • Forensics
    • Games
    • Data privacy and laws
    • Code breaking
    • Networking
    • Pentest
    • Speak to us!
  • Career
    • Study and profession
    • Oportunidades
  • Reverse Engineering
    • General
    • Malware Analysis
    • Firmware
    • Linux and UNIX-like
    • Windows
  • Programming
    • Assembly
    • C/C++
    • Python
    • Other languages
  • Operating Systems
    • GNU/Linux and UNIX-like
    • Windows
  • Segurança na Internet's Discussão

Categories

  • Crackmes
  • Documentação
  • Debuggers
  • PE tools
  • Books
  • Util
  • Packers
  • Unpackers

Find results in...

Find results that contain...


Date Created

  • Start

    End


Last Updated

  • Start

    End


Filter by number of...

Joined

  • Start

    End


Group


Full name


Como veio parar aqui?


Website


Github/Gitlab


LinkedIn

Found 1 result

  1. 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 🙂
×
×
  • Create New...