Ir para conteúdo

Fabiano Furtado

Apoiador Nibble
  • Postagens

    54
  • Registro em

  • Última visita

  • Dias Ganhos

    12

Tudo que Fabiano Furtado postou

  1. Oi Frederico, concordo novamente com você. Entretanto, estou "contando" em runtime o número de ítens do array alocado dinamicamente através do uso de um ítem NULL em sua última posição. Desde que não seja permitido que os ítens do array sejam nulos, essa técnica te retorna o número de ítens deste array, com o contra de usar uma posição deste array para isso (o úitimo item NULL). Eu só gostaria de saber se a 2a forma de contagem que descrevi no enunciado é a mais correta ou se há alguma outra forma melhor de se fazer isso. Obrigado!
  2. Sim, eu concordo. Entretanto, conforme informei, o array é dinâmico e terá de ser avaliado em runtime.
  3. Pessoal, tudo bem? Uma dúvida básica sobre qual é a melhor maneira de se fazer uma contagem de ítens dentro de um array. Segue o trecho do código em C como exemplo: ... typedef struct { char * name; unsigned char age; } person_t; ... uint16_t c; person_t p[] = { {"Fulano", 32}, {"Ciclano", 41}, {NULL} ); ... Um detalhe é que esse array é dinâmico e não posso calcular o seu total de ítens usando sizeof(p)/sizeof(person_t) pois preciso fazer isso em tempo de execução. Por isso o uso do "NULL" ao final, marcando o fim do array. Formas: 1a) Mais específica pois faço referência ao campo "name": for ( c = 0 ; p[c].name != NULL ; c++ ); 2a) Mais genérica pois uso apenas aritmética de ponteiros, sem a referência ao campo "name": for( c = 0 ; (void *)(*(uint64_t *)(p+c)) != NULL ; c++ ); Os dois códigos acima geram o mesmo ASM e pergunto: essa segunda forma com aritmética de ponteiros é a melhor maneira de se fazer essa contagem? Queria algo mais genérico, independende de arquitetura e que também não faca referência aos campos da "struct". Talvez esse "uint64_t" não funcione em 32 bits, tornando essa forma dependente da arquitetura 64 bits. Obrigado!
  4. Introdução Escrevi este artigo pois me deparei recentemente, durante um CTF, com uma técnica Return Oriented Programming (ROP) que não conhecia, chamada de ret2dl ou ret2dl_resolve, e precisei entender como esse processo de chamada de uma função externa funciona (pelo menos, o seu início!). Este texto foi baseado nestes excelentes artigos em inglês https://ypl.coffee/dl-resolve/ e https://syst3mfailure.io/ret2dl_resolve, mas complementei o assunto com algumas informações extras que achei relevante. Deixo também como referência um paper de 2015 muito bem escrito da USENIX para estudos. Recomendo a sua leitura. Para alinharmos sobre o que foi usado, este artigo tem como base a arquitetura AMD x86 64 bits, sistema operacional Linux, a função externa puts(), e o programa helloworld.c compilado: $ cat <<'EOF' > helloworld.c # usando o "Here Documents" do bash para criarmos o arquivo // helloworld.c // Compilar com: gcc -Wall -O2 -Wl,-z,lazy helloworld.c -o helloworld // *---> "lazy-binding" #include <stdio.h> int main(void) { puts("Hello World!"); // função externa: o código da "puts()" está localizado na LIBC return 0; } EOF Do ponto de vista da funcionalidade, este código é BEM simples pois "só" imprime "Hello World" na tela. Entretanto, do ponto de vista do sistema operacional, o processo é bem mais complexo (deixo aqui um link interessante sobre o assunto: https://lwn.net/Articles/631631/). Como será visto, ao se chamar a puts(), acontecem várias chamadas de funções externas, com o objetivo de se descobrir qual é o endereço dessa função. O processo de chamada de uma função externa é complexo, mas por que isso acontece? Resumidamente, por causa das proteções que um binário ELF moderno possui. Atualmente, há vários métodos para se proteger binários ELF. Podemos compilar o programa usando o PIE (Executável Independente de Posição), onde todas as suas dependências (normalmente shared objects) são carregadas em locais aleatórios na memória virtual sempre que o aplicativo é executado. Isso acontece graças a uma implementação do kernel chamada ASLR (Address Space Layout Randomization) e, teoricamente, fica difícil de se prever em qual endereço de memória virtual o binário foi carregado, principalmente em programas compilados em 64 bits, devido ao seu grande espaço de endereçamento. Tendo em mente este conceito de randomização de endereços, ao se executar um binário, a LIBC é carregada em memória e o endereço de suas funções são descobertos em tempo de execução. Essa descoberta pode acontecer, basicamente, de duas maneiras, dependendo de como o binário foi compilado (na verdade, essa é uma característica do linker, mais especificamente do GNU ld, e não do compilador GCC): Partial RELRO: O endereço da função externa é descoberto no momento de se chamar a própria função, em um processo chamado de lazy-binding ou, traduzindo, "amarração tardia" (link editado com -Wl,-z,lazy). Neste cenário, a inicialização do binário é mais rápida e a parte não-PLT da seção GOT (.got) é read-only, mas a seção .got.plt é writable. Vou explicar sobre essas seções mais adiante. Full RELRO: Todos os endereços das funções externas são descobertos ao se iniciar o binário (link editado com -Wl,-z,now), levando a um processo de inicialização mais demorado. Todo o GOT (.got e .got.plt) é marcado como somente read-only. Mas o que é RELRO? Relocation Read-Only (RELRO) é uma técnica genérica de mitigação de exploração para proteger as seções de dados de um binário ou processo ELF. Neste processo de descoberta do endereço da função externa, as seções .got e .got.plt são usadas? Sim! Essas duas seções são as mais importantes para o processo e trabalham em conjunto no processo de lazy-binding: o Procedure Linkage Table (PLT) e o Global Offset Table (GOT). A seção .plt é sempre read-only e contém código executável, responsável por iniciar o processo de descoberta do endereço da função externa e fazer a sua chamada. A GOT não contém código executável e é responsável por armazenar os endereços das funções externas. Abaixo mostrarei o dump de cada uma. Cada função externa que foi declarada no binário possui uma área na seção got.plt, para que seu endereço virtual seja armazenado, e uma área na .plt, onde será chamada (através de, literalmente, um jump). A seção <nome da função>@plt é sempre usada quando as funções externas precisam ser chamadas pelo binário (você entenderá isso no dump). Neste exemplo, temos a puts@plt. Nosso binário foi link editado com o modo lazy-binding, ou seja, possui Parcial RELRO. Seguem algumas informações sobre: $ file helloworld helloworld: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ccf36bd5d7298575faf6c5d7612b07306264be7d, for GNU/Linux 4.4.0, not stripped $ ldd helloworld # como podem perceber, nosso binário possui 3 dependências linux-vdso.so.1 (0x00007ffe78cb2000) libc.so.6 => /usr/lib/libc.so.6 (0x00007efd5a156000) /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007efd5a38f000) $ checksec -f helloworld [*] '/tmp/helloworld' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled Iniciando a chamada da "puts()" Por onde começamos? Vamos fazer um objdump em nosso binário, mais especificamente na chamada da puts( de dentro da main(): $ objdump -dM intel helloworld # obs: saída com adaptações ... 0000000000001040 <main>: 0x1040: 48 83 ec 08 sub rsp,0x8 0x1044: 48 8d 3d b9 0f 00 00 lea rdi,[addr of "Hello World!"] # RDI aponta para "Hello World!\n" 0x104b: e8 e0 ff ff ff call 0x1030 # call <puts@plt> ... Obs: Como o binário está compilado com o PIE ativo, a saída do objdump mostra apenas os offsets (deslocamentos) das instruções. Para se calcular o endereço virtual onde a função foi carregada, pega-se o endereço base do binário em execução (que é dinâmico devido ao PIE) e soma-se esse offset que está associado à função. No texto, algumas vezes descrevo endereço, mas, na verdade, estou querendo dizer offset. Enfim... Exemplo: 0x555555554000 (endereço base dinâmico) + 0x1040 (offset da main()) = 0x555555555040 (endereço virtual da main()) Com o dump, verificamos que a instrução call puts()(0x104b) é feita para um código que está localizado na seção .plt, mais especificamente call <puts@plt>, e este código da .plt é responsável por chamar a puts()na LIBC. Para entendimento, seguem os códigos da PLT e da GOT para a função puts(): 0000000000001030 <puts@plt>: # código responsável por chamar a "puts()" na LIBC 0x1030: ff 25 e2 2f 00 00 jmp QWORD PTR [0x4018] # <puts@GLIBC_2.2.5> 0x1036: 68 00 00 00 00 push 0x0 0x103b: e9 e0 ff ff ff jmp 0x1020 <_init+0x20> # jump para o início da ".plt" 0000000000004000 <_GLOBAL_OFFSET_TABLE_>: ... 0x4018: 36 10 00 00 00 00 00 00 # "0x1036" em "little-endian", apontando inicialmente para a "push" da "puts@plt". A call 0x1030 # <puts@plt> feita no endereço 0x104b da main()executa a instrução jmp QWORD PTR [0x4018]. O endereço apontado por [0x4018], resulta em 0x1036 (este código tem o mesmo efeito que jmp 0x1036), que está na própria puts@plt. Como esta é a primeira chamada para a puts(), ainda não sabemos o verdadeiro endereço desta função na LIBC (lazy-binding) e o compilador inicia a GOT (em 0x4018) com o endereço 0x1036, que pertence a puts@plt (veja o comentário feito em 0x4018 acima). O objetivo é descobrir o endereço da puts() dentro do shared object libc.so.6, para que o mesmo seja gravado na GOT no endereço 0x4018, substituindo o atual endereço 0x1036 pelo endereço virtual dessa função externa. Quando o código em 0x1036 é executado, o mesmo dá um push 0x0 para a stack (segundo parâmetro reloc_index da função _dl_runtime_resolve() - explicarei essa função e parâmetro depois), e depois dá um jmp 0x1020 para o início da PLT, onde fica a call <_dl_runtime_resolve@plt>. 0000000000001020 <puts@plt-0x10>: # início da PLT "call <_dl_runtime_resolve@plt>" 0x1020: ff 35 e2 2f 00 00 push QWORD PTR [0x4008] # 0x4008: "link_map" <GOT+0x8> 0x1026: ff 25 e4 2f 00 00 jmp QWORD PTR [0x4010] # 0x4010: "_dl_runtime_resolve()" <GOT+0x10> Mas que função é essa _dl_runtime_resolve()? No meu caso, a PLT chama a função _dl_runtime_resolve_xsavec(), responsável por descobrir, de alguma forma, o endereço da puts() na LIBC (para efeitos didáticos, vou considerar o nome da _dl_runtime_resolve_xsavec() como _dl_runtime_resolve()). A instrução push [0x4008], localizada no endereço 0x1020, coloca o primeiro parâmetro link_map da função _dl_runtime_resolve() na stack. Por curiosidade, este endereço do struct "link_map" pertence ao espaço de endereçamento do dynamic linker/loader /lib64/ld-linux-x86-64.so.2, bem como a função _dl_runtime_resolve()(eu achava que esta _dl_runtime_resolve()era uma função da LIBC, mas descobri que não era usando o GDB durante a depuração, através do comando vmmap. Ele mostra todas as libs carregadas e o mapa de endereçamento virtual. (BEM útil!). Com estes dois parâmetros configurados na stack, podemos chamar a função _dl_runtime_resolve(link_map, reloc_index). gef➤ vmmap [ Legend: Code | Heap | Stack ] Start End Offset Perm Path 0x0000555555554000 0x0000555555555000 0x0000000000000000 r-- /tmp/helloworld 0x0000555555555000 0x0000555555556000 0x0000000000001000 r-x /tmp/helloworld 0x0000555555556000 0x0000555555557000 0x0000000000002000 r-- /tmp/helloworld 0x0000555555557000 0x0000555555558000 0x0000000000002000 r-- /tmp/helloworld 0x0000555555558000 0x0000555555559000 0x0000000000003000 rw- /tmp/helloworld 0x0000555555559000 0x000055555557a000 0x0000000000000000 rw- [heap] 0x00007ffff7d8a000 0x00007ffff7d8c000 0x0000000000000000 rw- 0x00007ffff7d8c000 0x00007ffff7db8000 0x0000000000000000 r-- /usr/lib/libc.so.6 0x00007ffff7db8000 0x00007ffff7f2e000 0x000000000002c000 r-x /usr/lib/libc.so.6 0x00007ffff7f2e000 0x00007ffff7f82000 0x00000000001a2000 r-- /usr/lib/libc.so.6 0x00007ffff7f82000 0x00007ffff7f83000 0x00000000001f6000 --- /usr/lib/libc.so.6 0x00007ffff7f83000 0x00007ffff7f86000 0x00000000001f6000 r-- /usr/lib/libc.so.6 0x00007ffff7f86000 0x00007ffff7f89000 0x00000000001f9000 rw- /usr/lib/libc.so.6 0x00007ffff7f89000 0x00007ffff7f98000 0x0000000000000000 rw- 0x00007ffff7fc0000 0x00007ffff7fc4000 0x0000000000000000 r-- [vvar] 0x00007ffff7fc4000 0x00007ffff7fc6000 0x0000000000000000 r-x [vdso] 0x00007ffff7fc6000 0x00007ffff7fc8000 0x0000000000000000 r-- /usr/lib/ld-linux-x86-64.so.2 0x00007ffff7fc8000 0x00007ffff7fef000 0x0000000000002000 r-x /usr/lib/ld-linux-x86-64.so.2 0x00007ffff7fef000 0x00007ffff7ffa000 0x0000000000029000 r-- /usr/lib/ld-linux-x86-64.so.2 0x00007ffff7ffb000 0x00007ffff7ffd000 0x0000000000034000 r-- /usr/lib/ld-linux-x86-64.so.2 0x00007ffff7ffd000 0x00007ffff7fff000 0x0000000000036000 rw- /usr/lib/ld-linux-x86-64.so.2 0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack] 0xffffffffff600000 0xffffffffff601000 0x0000000000000000 --x [vsyscall] Importante: os parâmetros para a função _dl_runtime_resolve()estão sendo passados pela stack (de acordo com a Linux x86_64 Calling Convention, os registradores rdi, rsi, rdx, rcx, r8 e r9 são reservados para a passagem de parâmetros das funções), "quebrando" esta calling convention da arquitetura Linux x86_64. Por que isso acontece? Os registradores da Calling Convention ficam reservados para a passagem dos parâmetros da função em evidência: a puts()utiliza o registrador rdi para armazenar o endereço do parâmetro Hello World! (0x1044: lea rdi,[addr of Hello World!]). Como os registradores estão "reservados", a passagem de parâmetro da _dl_runtime_resolve() é feita utilizando-se a stack. Achei bem interessante essa "quebra", pois pensava que sempre teríamos de respeitar essa Calling Convention... bem, nem sempre! ? Quando utilizamos o Parcial RELRO, as entradas da .got.plt ficam configuradas assim: offset para a seção .dynamic (calculado durante o processo de link-edição); endereço da link_map (armazenado ao se iniciar o ELF); endereço da função _dl_runtime_resolve() (armazenado ao se iniciar o ELF); endereço da puts()(descoberto ao se chamar a própria função); Vale lembrar que cada entrada da .got.plt possui 8 bytes (64 bits). Estes conceitos podem parecer confusos, mas não são. Entre no GDB e faça o debugging da main(), passo a passo, entrando em cada função, e você entenderá melhor o que foi descrito até aqui. Use comandos simples do GDB como ni, si, vmmap e b*main para fazer a depuração, disas <endereço> para o disassembly e x/6gx <endereço> para verificar a memória. Percebeu que um programa simples esconde processos bem mais complexos que, talvez, você nem desconfiava que existisse? Legal, né? Então, vamos continuar! Por dentro da "_dl_runtime_resolve()" Como descrito anteriormente, antes de chamarmos a _dl_runtime_resolve(), as seguintes estruturas são utilizadas durante processo: .plt e .got.plt Depois que entramos nessa função, as estruturas \.dynamic, .rela.plt, .dynsym e .dynstr são utilizadas para encontrarmos o endereço da puts(). Mas, afinal, o que faz a _dl_runtime_resolve()? De forma geral: Encontra a string puts\0, através das estruturas .dynamic, .rela.plt, .dynsym e .dynstr; Procura a puts()em todas as libs carregadas (shared objects) e encontra o seu endereço dinâmico na LIBC; Atualiza o endereço dinâmico da puts()na 4a entrada da seção .got.plt (0x4018); Faz um jump para puts()(apenas esta vez. Na próxima chamada, a função será chamada diretamente pela PLT - endereço 0x1030); Neste artigo, vamos mostrar em detalhes este primeiro item. Cada entrada da seção .dynamic possui 16 bytes de tamanho, e é composta por dois campos: d_tag (8 bytes) e d_val (8 bytes). O campo d_val armazena os offsets para seções .rela.plt, .dynsym e .dynstr. Veja: $ readelf -d helloworld # saída com modificações Dynamic section at offset 0x2df8 contains 26 entries: d_tag Type Name/Value 0x01 (NEEDED) Shared library: [libc.so.6] ... 0x05 (STRTAB) 0x0488 # d_val = 0x0488 - offset para ".dynstr" 0x06 (SYMTAB) 0x03e0 # d_val = 0x03e0 - offset para ".dynsym" ... 0x17 (JMPREL) 0x0618 # d_val = 0x0618 - offset para ".rela.plt" O campo d_tag armazena um valor inteiro para cada estrutura do ELF. Veja as definições em https://code.woboq.org/userspace/glibc/elf/elf.h.html. Por exemplo: ... #define DT_STRTAB 5 /* 0x05 - Address of string table */ #define DT_SYMTAB 6 /* 0x06 - Address of symbol table */ ... #define DT_JMPREL 23 /* 0x17 - Address of PLT relocs */ Tendo como referência o campo l_info da struct link_map e utilizando a seção .dynamic, a função _dl_runtime_resolve()consegue encontrar a .rela.plt (definida pela struct Elf64_Rela), e obtém um index que está vinculado ao campo r_info (veja mais informações sobre essa struct em https://man7.org/linux/man-pages/man5/elf.5.html), e usa esse index para localizar a seção .dynsym (definada pela struct Elf64_Sym). Nesta seção, há um campo chamado st_name que armazena o offset para a string puts\0, localizada na .dynstr. Repare que está tudo conectado através de "ponteiros", apontando para outros "ponteiros". A partir da .dynamic, a função _dl_runtime_resolve()você consegue localizar em memória todas as estruturas necessárias para fazer corretamente o seu trabalho. Como exemplo, quando a _dl_runtime_resolve()quer descobrir onde está a .dynstr, ela verifica o link_map->l_info[DT_STRTAB] (veja que o #define acima é utilizado aqui!), onde há um ponteiro para a entrada da STRTAB, dentro da .dynamic. Não colocarei aqui a struct "link_map", pois é bem extensa (por exemplo, armazena até o endereço base do binário ELF), mas o único campo dela que interessa pra gente é o l_info, já citado acima. Seguem as 3 structs citadas: typedef struct { /* .dynamic */ Elf64_Sxword d_tag; /* 8 bytes */ union { /* 8 bytes */ Elf64_Xword d_val; Elf64_Addr d_ptr; } d_un; } Elf64_Dyn; /* Total = 16 bytes */ typedef struct { /* .rela.plt */ | typedef struct { /* .dynsym */ Elf64_Addr r_offset; /* 8 bytes */ | uint32_t st_name; /* 4 bytes */ uint64_t r_info; /* 8 bytes */ | unsigned char st_info; /* 1 byte */ int64_t r_addend; /* 8 bytes */ | unsigned char st_other; /* 1 byte */ } Elf64_Rela; /* Total = 24 bytes */ | uint16_t st_shndx; /* 2 bytes */ | Elf64_Addr st_value; /* 8 bytes */ | uint64_t st_size; /* 8 bytes */ | } Elf64_Sym; /* Total = 24 bytes */ Como podemos ver, as structs Elf64_Rela e Elf64_Sym possuem 24 bytes de tamanho em ambiente 64 bits. Em 32 bits, o tamanho é diferente. Bom, vamos entender esse processo com mais detalhes. Descobrindo os "offsets" das estruturas que usaremos: $ readelf -S helloworld # saída com adaptações There are 30 section headers, starting at offset 0x36c0: Section Headers: [Nr] Name Type Address ... [ 6] .dynsym DYNSYM 03e0 [ 7] .dynstr STRTAB 0488 ... [11] .rela.plt RELA 0618 [12] .init PROGBITS 1000 [13] .plt PROGBITS 1020 ... [21] .dynamic DYNAMIC 3df8 [22] .got PROGBITS 3fd8 [23] .got.plt PROGBITS 4000 [27] .symtab SYMTAB 0000 [28] .strtab STRTAB 0000 Obs: Para melhor entender as estruturas abaixo, aconselho importar o binário para o Ghidra, pois ele mostra visualmente cada seção descrita abaixo! Até agora, usamos o binário sem ele estar sendo executado e, devido ao PIE, obtemos somente os offsets das funções. Vamos executá-lo no GDB: # Obtendo o base address do binário: gef➤ deref -l 1 $_base() 0x0000555555554000│+0x0000: 0x00010102464c457f # veja aqui o "magic number" do cabeçalho ELF # Obtendo o conteúdo da .got.plt, somando o endereço base + o offset da .got.plt (obtido via "readelf -S"): gef➤ x/4gx 0x555555554000 + 0x4000 # saída com adaptações 0x555555558000: 0x000000003df8 # "offset" para a seção ".dynamic" 0x555555558008: 0x7ffff7ffe2a0 # "link_map" 0x555555558010: 0x7ffff7fda5d0 # "_dl_runtime_resolve()" 0x555555558018: 0x555555555036 # "puts()", ainda apontando para ".plt" (0x555555554000 + 0x1036) Chamei de slots as posições alinhadas em memória das estruturas que estamos procurando. Em síntese, é onde a informação que estamos procurando está localizada dentro da link_map. # Descobrindo a ".dynstr": "link_map->l_info[DT_STRTAB]", desfazendo os apontamentos: gef➤ x/gx (0x7ffff7ffe2a0 + (8*13)) # "link_map" + 13 "slots" = 0x7ffff7ffe308 0x7ffff7ffe308: 0x555555557e78 # 0x555555557e78 = endereço da seção ".dynamic", que aponta para: # a seção ".dynstr": gef➤ x/2gx 0x555555557e78 0x555555557e78: 0x000000000005 0x555555554488 *-----> d_tag *-----> d_val Obs: 0x555555557e78 aponta para "d_tag". "d_val" = "d_tag" + 8 bytes! d_val = 0x555555554488: 0x555555554000 (endereço base) + 0x488 (que é o offset para .dynstr) Pronto! Através do link_map e a .dynamic, conseguimos descobrir o endereço da .dynstr (0x555555554488)! Usando a mesma estratégia, vamos descobrir o endereço de link_map->l_info[DT_SYMTAB] e link_map->l_info[DT_JMPREL]: # Descobrindo a .dynsym: link_map->l_info[DT_SYMTAB], desfazendo os apontamentos: gef➤ x/gx (0x7ffff7ffe2a0 + (8*14)) # "link_map" + 14 "slots" = 0x7ffff7ffe310 0x7ffff7ffe310: 0x555555557e88 # 0x555555557e88 = endereço da seção ".dynamic", que aponta para: # a seção ".dynsym": gef➤ x/2gx 0x555555557e88 0x555555557e88: 0x000000000006 0x5555555543e0 *-----> d_tag *-----> d_val Obs: 0x555555557e88 aponta para d_tag. d_val = d_tag + 8 bytes! d_val = 0x5555555543e0: 0x555555554000 (endereço base) + 0x3e0 (que é o offset para .dynsym) Pronto! Através do link_map e a .dynamic, conseguimos descobrir o endereço da .dynsym (0x5555555543e0)! # Descobrindo a .rela.plt: "link_map->l_info[DT_JMPREL], desfazendo os apontamentos: gef➤ x/gx (0x7ffff7ffe2a0 + (8*31)) # "link_map" + 31 "slots" = 0x7ffff7ffe398 0x7ffff7ffe398: 0x555555557ef8 # 0x555555557ef8 = endereço da seção ".dynamic", que aponta para: # a seção ".rela.plt": gef➤ x/2gx 0x555555557ef8 0x555555557ef8: 0x000000000017 0x555555554618 *-----> d_tag *-----> d_val Obs: 0x555555557ef8 aponta para d_tag. d_val = d_tag + 8 bytes! d_val = 0x555555554618: 0x555555554000 (endereço base) + 0x618 (que é o offset para .rela.plt) Pronto! Através do link_map e a .dynamic, conseguimos descobrir o endereço da .rela.plt (0x555555554618)! De posse dos endereços das seções .rela.plt, .dynsym e .dynstr, a função _dl_runtime_resolve() tem condições de encontrar a string puts\0. A primeira estrutura a ser utilizada é a .rela.plt, pois possui informações sobre a realocação dos endereços das funções. $ readelf -r helloworld ... Relocation section '.rela.plt' at offset 0x618 contains 1 entry: Offset Info Type Sym. Value Sym. Name + Addend 000000004018 000300000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0 O readelf só nos mostra a função puts(), mas se o binário tivesse mais funções, ele as listaria aqui. Vamos conferir no debugger o conteúdo do endereço de memória da .rela.plt: gef➤ x/3gx 0x555555554618 # saída com modificações 0x555555554618: 0x0000000000004018 # r_offset 0x555555554620: 0x0000000300000007 # r_info 0x555555554628: 0x0000000000000000 # r_addend Vamos explicar o que significa esses campos: r_offset: Contém o offset que determina onde o endereço do símbolo resolvido será armazenado na .got.plt, no caso 0x4018 (veja o objdump que fizemos no começo do artigo); r_info: Contém duas informações em um único campo: tipo de realocação index para a tabela de símbolos: será usado para localizar a estrutura Elf64_Sym correspndente na seção DYNSYM O campo r_info é interpretado através da sua definição no código da LIBC: #define ELF64_R_SYM(i) ((i) >> 32) #define ELF64_R_TYPE(i) ((i) & 0xffffffff) Após resolvermos essa matemática destes #define, encontramos que o tipo de realocação = 7 (R_X86_64_JUMP_SLOT) e o index para a tabela de símbolos = 3. De fato, se pegarmos o endereço inicial da tabela de símbolos .dynsym 0x5555555543e0 e somarmos 3*24 bytes (lembre-se que a estrutura .dynsym em 64 bits possui 24 bytes), encontraremos 0x555555554428, que é a Elf64_Sym da puts(). $ readelf -s helloworld # saída com modificações Symbol table '.dynsym' contains 7 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0x00 0 NOTYPE LOCAL DEFAULT UND 1: 0x00 0 FUNC GLOBAL DEFAULT UND _[...]@GLIBC_2.34 (2) 2: 0x00 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterT[...] ==> 3: 0x00 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (3) 4: 0x00 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 5: 0x00 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMC[...] 6: 0x00 0 FUNC WEAK DEFAULT UND [...]@GLIBC_2.2.5 (3) Pela saída do readelf, verificamos que realmente o index 3 da tabela de símbolos corresponde à puts(). Em relação ao tipo de realocação = 7 (R_X86_64_JUMP_SLOT - veja o readelf -r acima), há uma verificação sobre essa condição no código em ? assert ((reloc->r_info & 0xffffffff) == 0x7); Para resumir, esse assert verifica se reloc->r_info"é um JUMP_SLOT válido. Vejamos no GDB o conteúdo desses 24 bytes de memória da Elf64_Sym: gef➤ x/24xb 0x555555554428 # saída com modificações 0x555555554428: 0x22 0x00 0x00 0x00 # st_name = 0x22 - 4 bytes 0x55555555442c: 0x12 # st_info = 0x12 - 1 byte 0x55555555442d: 0x00 # st_other = 0x00 - 1 byte 0x55555555442e: 0x00 0x00 # st_shndx = 0x00 - 2 bytes 0x555555554430: 0x00 0x00 ... # st_value = 0x00 - 8 bytes 0x555555554438: 0x00 0x00 ... # st_size = 0x00 - 8 bytes A string puts\0 é encontrada através do campo st_name, que representa o offset correspondente ao início da .dynstr (0x555555554488). Se somarmos este endereço com o valor de st_name, encontraremos a string: gef➤ x/2wx 0x555555554488+0x22 0x5555555544aa: 0x73747570 0x62696c00 *----> string "puts" em hex no formato "little endian" Ou... gef➤ x/s 0x555555554488+0x22 0x5555555544aa: "puts" Pronto! Alcançamos o nosso objetivo. A partir de agora, conforme descrito anteriormente, a _dl_runtime_resolve(): Procurará a puts() em todas as libs carregadas (shared objects) e encontrará o seu endereço dinâmico na LIBC; Atualizará o endereço dinâmico da puts() na 4a entrada da seção .got.plt (0x4018); Fará um jump para puts() (apenas esta vez. Na próxima chamada, a função será chamada diretamente pela PLT - endereço 0x1030); Para fecharmos o ciclo deste artigo, segue a .got.plt com o endereço virtual da puts()descoberto e atualizado: gef➤ x/4gx 0x555555554000 + 0x4000 # saída com adaptações 0x555555558000: 0x000000003df8 # "offset" para a seção ".dynamic" 0x555555558008: 0x7ffff7ffe2a0 # "link_map" 0x555555558010: 0x7ffff7fda5d0 # "_dl_runtime_resolve()" 0x555555558018: 0x7ffff7e075a0 # "puts()" ATUALIZADO!! Veja no vmmap que é um endereço da LIBC! Críticas, correções e comentários são bem-vindos. Obrigado!
  5. EXCELENTE!!!! Uma dúvida... Qual seria mais rápido? Usar a sua função int mul_ints(int *r, int a, int b);, descrita em seu artigo sobre "overflows em multiplicações", ou o builtin do compilador bool __builtin_smul_overflow (int a, int b, int *res)? Como se faz para medir este tempo gasto nestas duas funções? Desde já, agradeço.
  6. Pessoal, disponibilizei no meu GitHub o meu primeiro projeto OpenSource, como forma de retornar para a comunidade o que tenho aprendido nestes últimos anos. O projeto se chama LIBCPF (C Plugin Framework - https://github.com/fabianofurtado/libcpf/) e se trata de uma biblioteca/framework pra gerenciamento de Plugins (".so") no Linux, feita em C. Para maiores detalhes, veja a documentação no arquivo README.md. Ela foi lançada utilizando-se a licença GPL v2, uma vez que não entendo muito sobre esse assunto e precisava de uma licença para o projeto. Espero que, de alguma forma, este projeto possa ser útil para você. Mesmo que não exista a necessidade de utilização desta lib em sua aplicação, a mesma pode ser usada para listar os nomes das funções e os offsets que existem dentro de uma shared library, algo que considero bem interessante. Veja no exemplo como isso funciona. Como qualquer programa em C, o desenvolvimento deu muito trabalho pois tentei desenvolvê-la com foco na segurança, tentando evitar possíveis "buffer overflows" ou qualquer outro tipo de vulnerabilidades. Sugestões e críticas serão sempre bem-vindas! Desde já, agradeço.
  7. Pessoal, estou desenvolvendo um pequeno projeto OpenSource em C para o Linux e quero discutir aqui qual é a melhor licença a ser aplicada. Como é o meu primeiro projeto que vou disponibilizar para a comunidade, fico muito perdido quando começo a ler o texto contendo a definição da licença e solicito uma ajudinha para quem tem experiência, no intuito de entender melhor o contexto. Estava pensando na GPL v2, mas a GPL v3 é mais moderna, porém, mais longa e mais difícil de se entender. O que acham? Desde já agradeço.
  8. Excelente artigo! Descreve de forma simples, clara e objetiva o processo de alocação de memória dinâmica. Para quem quiser se aprofundar, recomendo estes 2 links: https://github.com/shellphish/how2heap https://heap-exploitation.dhavalkapil.com/ Valeu!
  9. 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
  10. Texto excelente! Para complementá-lo, segue um video do Ben Eater, onde ele fala sobre o trabalho de Turing e Church, e questiona se o computador de 8 bits que ele contruiu com portas lógicas simples é realmente considerado um computador completo. Bem, sugiro assistirem a esta Playlist fantástica! São mais de 40 videos. Comecei assistindo um e não consegui parar! ? Valeu!
  11. Muito bom os artigos!!!! Realmente fica quase impossível adivinhar esses números diante de tantas possíveis combinações.
  12. Pessoal, desenvolvi recentemente 2 programas em C para gerar e filtrar números/combinações numéricas das loterias Dupla Sena e Lotofácil (eles podem ser adaptados facilmente para outros tipos de concursos, por exemplo). Gostaria de um feedback de vocês pois não sei se fiz da melhor forma possível. Achei a leitura do arquivo texto contendo as combinações um pouco lenta. Enfim... todas as críticas são bem-vindas pois tenho certeza que poderia ter feito ele de outra maneira mais otimizada. https://github.com/fabianofurtado/random-lottery-numbers Há, basicamente, duas formas de vocês o utilizarem: 1) Através de um arquivo texto contendo as combinações (pode ser gerada no random.org) 2) Através do parâmetro -n onde o sistema utilizará as funções srand() e rand() da libc. Valeu!
  13. Oi Fernando... primeiramente, obrigado pelo retorno. Acho que ainda não tenho essa capacidade para fazer tal análise, ainda mais de dentro da ld-linux. ? Quem sabe um video sobre o assunto no seu canal do YouTube? O máximo que consegui foi tirar algumas informações do dump que foi gravado: $ coredumpctl list Hint: You are currently not seeing messages from other users and the system. Users in groups 'adm', 'systemd-journal', 'wheel' can see all messages. Pass -q to turn off this notice. TIME PID UID GID SIG COREFILE EXE Wed 2018-11-14 15:56:57 -02 17737 1000 988 11 present /usr/lib/ld-2.28.so $ coredumpctl info 17737 Hint: You are currently not seeing messages from other users and the system. Users in groups 'adm', 'systemd-journal', 'wheel' can see all messages. Pass -q to turn off this notice. PID: 17737 (ld-linux-x86-64) UID: 1000 (fabianofurtado) GID: 988 (users) Signal: 11 (SEGV) Timestamp: Wed 2018-11-14 15:56:56 -02 (5min ago) Command Line: /lib64/ld-linux-x86-64.so.2 /tmp/hw64 Executable: /usr/lib/ld-2.28.so Control Group: /user.slice/user-1000.slice/session-6.scope Unit: session-6.scope Slice: user-1000.slice Session: 6 Owner UID: 1000 (fabianofurtado) Boot ID: ***** Machine ID: ***** Hostname: PC-107204 Storage: /var/lib/systemd/coredump/core.ld-linux-x86-64.1000.******.*****.lz4 Message: Process 17737 (ld-linux-x86-64) of user 1000 dumped core. Stack trace of thread 17737: #0 0x00007f293c068bc3 _dl_relocate_object (/usr/lib/ld-2.28.so) #1 0x00007f293c061397 dl_main (/usr/lib/ld-2.28.so) #2 0x00007f293c076090 _dl_sysdep_start (/usr/lib/ld-2.28.so) #3 0x00007f293c05f088 _dl_start (/usr/lib/ld-2.28.so) #4 0x00007f293c05e008 _start (/usr/lib/ld-2.28.so) Esse programa foi um "Hello World" feito em NASM.
  14. Fernando, tudo bem? Tenho uma dúvida em relação a esse procedimento de hook relacionada a segurança. Muito básica, diga-se. Para que esse LD_PRELOAD foi implementado na GLIBC se, ao meu ver, só há desvantagens em relação a segurança? Quais os benefícios de se ter implementado isso? Achei muito interessante esse recurso, mas a segurança fica comprometida com ele ativado. Outra... usei o ldd para ver as dependências e a maioria das aplicações Linux linkadas dinamicamente utiliza a ld-linux-x86-64.so.2 para funcionar/carregar o binário ELF. Lendo mais sobre o assunto (1), e também demonstrado por você no artigo ( $ LD_PRELOAD=$PWD/hook.so ./ld-linux-x86-64.so.2 ./ola ), é possível executar um binário sem o bit de execução habilitado. Não entendi o motivo disso. Fiz um teste com o ld-linux-x86-64.so.2 em um binário sem dependências (linkado estaticamente) e, independentemente do estado do bit de execução, o binário não roda. Tenho um Segmentation Fault. Eu só queria uma opinião mesmo sobre essas implementações pois acho que a segurança fica muito comprometida desta maneira. Desde já agradeço. Referências: * (1) https://superuser.com/questions/341439/can-i-execute-a-linux-binary-without-the-execute-permission-bit-being-set
  15. Artigo EXCELENTE! Sempre aprendo algo novo por aqui! Vou postar um comentário, pois estou com algumas dúvidas. Valeu!
  16. Fala Fernando... tudo bem? Estou acompanhando de perto esse curso e deixo aqui meus parabéns! Se você pudesse falar um pouco sobre o IDA, agradeceria. Depois que vi aquele seu video, "Interagindo com o IDA / CTF Shellterlabs / Sorteio Roadsec Rio" - https://www.youtube.com/watch?v=KjQMOByFt9A, fiquei muito empolgado pois trata-se de um software muito poderoso e queria mais informações. No mais, continue com essa didática. Valeu!
  17. Gostei muito do curso, mas acho que poderia abordar mais um pouco da linguagem, através de mais videos. De qualquer maneira, recomendo este curso para todos.
×
×
  • Criar Novo...