Jump to content
  • Sign in to follow this  

    Comando find e suas miscelâneas

       (2 reviews)

    Leandro Fróes

    Após ver sobre o comando find no nosso canal Papo Binário decidi estudar um pouco mais sobre o mesmo. Revisando estas anotações pensei que seria interessante compartilhá-las, tendo em vista que o find é um comando extremamente poderoso. Alguns dos parâmetros já foram abordados no vídeo, mas vou repassar alguns aqui, não custa nada, não é mesmo?!

    Este comando pode ser útil para diversas tarefas, dentre elas investigação, administração ou mesmo aprendizado sobre o sistema.

    Indo direto ao ponto, o find é um comando para procurar itens no filesystem (arquivos, links, diretórios, etc). O que o difere de outros programas que fazem isto é a quantidade de opções que a ferramenta possui e o fato de não depender da variável $PATH para encontrar um binário. O comando leva como principal parâmetro um path, ou seja, um caminho para procurar algo. Se não passarmos nada ele entenderá que o path é o diretório atual:

    find 
    find /etc

    Se não especificarmos exatamente o que queremos buscar o find simplesmente nos mostra tudo o que achar pois ele varre o filesystem recursivamente na hora de procurar algo, mas não queremos isso tudo, até porque não seria muito útil. 🙄

    Vamos tentar entender alguns filtros interessantes... Imagine que você é um administrador e precisa verificar todos os arquivos que pertencem a um usuário em específico:

    find / -type f -user leandro 

    O que fizemos aqui? Utilizamos 2 tipos de filtros, um deles foi o -user, que busca arquivos que pertencem apenas à aquele usuário. O -type filtra pelo tipo de item no filesystem e suporta os seguintes tipos:

    • d -> diretório
    • f -> arquivo regular
    • l -> link simbólico
    • s -> socket
     
     
    Procurando por arquivos perdidos:
     
    Imagine agora que seu sistema está uma bagunça e você não faz ideia onde está um arquivo em específico, pense que você tem no mínimo 8 subdiretórios lotados de arquivos e você não lembra onde está o que você está procurando, só lembra que existe a palavra "mentebinaria" no nome dele. Além disso, você também sabe que não está nos primeiros 2 subdiretórios. Podemos resolver com:
    find . -mindepth 2 -name "*mentebinaria*" -type f
    A primeira coisa que fizemos foi utilizar a opção -mindepth, que especifica quantos níveis na hierarquia o find deve olhar no mínimo (a opção -maxdepth especifica o máximo). A outra opção foi a -name, que procura por um nome completo ou parte dele como fizemos no exemplo utilizando o wildcard * (asterisco) para bater com qualquer string antes de depois da palavra "mentebinaria".
     

    Executando comandos:

    Na minha opinião uma das opções mais interessantes do find é a -exec, que praticamente executa comandos em cima do que o find encontrar. Não entendeu? Vamos lá... supondo que queiramos ver qual o tipo de arquivo de todos os arquivo que encontrarmos em um diretório em específico com o comando file:

    find . -type f -exec file {} \;

    Temos muita coisa pra entender nesta linha. Primeiro, o -exec trabalha com o conceito de targets (as chaves {} ) e isto significa: coloque tudo o que o find devolver no local da chave. Para cada arquivo que o find achar ele rodará o comando file naquele arquivo. Incrível, não?

    Sim, mas com isto estaremos executanto o mesmo comandos múltiplas vezes, por exemplo:

    leandro@teste:~$ find . -type f | wc -l
    295

    Imagine rodar isto 295 vezes, muita coisa, não? Se notarmos no primeiro exemplo do -exec vemos que no fim da linha tem um ponto de vírgula e este indica o fim do -exec para o find (e não para o shell). Temos que usar a contra barra para escapar e o shell não pensar que é para ele.

    Ok, mas até agora não vimos como melhorar isto. Concordam que o comando file aceita mais de um parâmetro?

    file arq1 arq2 arq3

    E se pudéssemos pegar tudo que o find achar e, ao invés de rodar um comando do -exec por vez passamos tudo um atrás do outro? É exatamente isto o que o + faz e para ele não precisamos escapar:

    find . -type f -exec file {} +

    Este exemplo é a mesma coisa do anterior, mas de forma mais automatizada. Vamos medir a velocidade dos dois comandos:

    root@teste:~# time find / -type l -exec file {} \;
    
    ...
    
    real    0m15,127s
    user    0m0,336s
    sys     0m1,640s
    root@teste:~# time find / -type l -exec file {} +
    
    ...
    
    real    0m1,119s
    user    0m0,212s
    sys     0m0,396s

    Bem mais rápido com o +, não acham? 😉

     

    Investigando o sistema:

    Seu servidor foi atacado, você não sabe exatamente o que aconteceu e como aconteceu, só sabe que nem tudo está funcionando do jeito que deveria. Uma coisa interessante à se fazer é tentar olhar para o que exatamente foi alterado desde o ataque. Imagine que isto ocorreu à 2 dias:
    find / -mtime -2

    Aqui estamos dizendo que a partir da hora que rodarmos o comando olhar para tudo que foi modificado 48 horas atrás. Podemos também verificar se algo foi acessado com -atime.

    E se você não sabe exatamente quando foi o ataque? A única coisa que você sabe é que a última coisa que você fez foi adicionar novas funcionalidades à um script que você tem. Podemos procurar por tudo que foi modificado após este arquivo com a opção -newer:

    find /etc -newer <arquivo_velho>

    Mas como isto? O Linux guarda um tipo de informação chamada MAC no inode de cada arquivo, resumindo é simplesmente a data da última modificação, acesso e criação do arquivo ao qual aquele inode se refere. Apenas como curiosidade, o comando stat lê essas informações também. 😋

     

    Mais algumas informações:

    Ok, agora você não teve nenhum problema, só quer algumas informações sobre os arquivos que o find encontrar. A opção -size <n> pode ajudar a procurar por arquivos maiores (+) ou menores (-) que o especificado:

    find /var -size +20k

    Podemos trabalhar com os seguintes formatos:

    • c -> bytes
    • k -> KB
    • 0 ou -empty -> vazio
    find . -empty

    Não está satisfeito? Ok, a opção -ls ti da muito mais informações (praticamente aplica um ls -lids em cima de tudo que o find achar)

    find . -user leandro -type d -ls 

     

    Facilitando o parsing:

    Achou as opções de informações fracas? De fato a saída fica bem poluída. E se você precisasse todo dia monitorar informações específicas sobre arquivos específicos e criasse um script para isso, como você faria para obter estas informações? O find ti ajuda nisso também!!! Se você está familiarizado com a linguagem C (se não está veja isto) a função printf do C pode imprimir uma saída formatada de acordo com o que você escolher (string, inteiro, inteiro sem sinal, etc).

    Assim como em C, a opção -printf possui uma série de diretivas para formatarmos a saída do find como quisermos, algumas delas são:

    • %f -> nome do arquivo
    • %p -> path completo
    • %i -> inode
    • %M -> permissões
    • %n -> número de hard links
    find / -type f -atime -1 -printf '%p %i %M \n'

    O único detalhe aqui é que por padrão o -printf não coloca um caractere de nova linha, devemos adicionar como no exemplo. Com isto a saída fica bem mais interesante para um script ler, não acham?! Aqui está o exemplo de uma saída:

    file1 262295 -rw-r--r--
    file2 262283 -rw-r--r--
    file3 262296 -rw-r--r--

    Estas foram algumas dicas sobre o comando find. Com certeza informações mais completas podem ser encontradas no manual do comando, este tutorial tem como objetivo simplesmente compartilhar minhas anotações sobre o que acho bem interessante e usual sobre o comando find.

    Qualquer dúvida, crítica ou sugestão, por favor, sinta-se à vontade para comentar e obrigado! 😄

    Sign in to follow this  


    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

    fredericopissarra

       4 of 4 members found this review helpful 4 / 4 members

    Cobre o básico do uso de find muito bem. Eu acrescentaria um detalhe:

    É possível criar expressões mais complexas com o uso de \( e \) e as opções -and (que é implícita) ou -or. Por exemplo:

    $ find ~/videos/ -type f \( -name '*.webm' -or -name '*.avi' \) \
      -exec convert2mp4.sh '{}' \;

    Aqui todos os arquivos encontrador a partir do diretório ~/videos/, nomeados *.webm ou *.avi, serão passados para o script convert2mp4.sh.

    Notar que, assim como o ';' final é "escapado", os parênteses também tém que sê-lo.

    Share this review


    Link to review

  • Similar Content

    • 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á! 😎
    • 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 lucass
      Vou começar agradecendo ao @Fernando Mercês pela oportunidade e por ter sugerido este artigo, que também me motivou bastante a escrevê-lo!
      Introdução
      Não sou conhecido internet a dentro, apenas acompanho alguns canais no Discord (tal como o do Mente Binária). Meu nível de programação e engenharia reversa não é algo admirável ainda. Em um grupo especifico intitulado "Terra do 1337", que é um grupo fechado de amigos com finalidade de estudar engenharia reversa, programação e descontrair, eu surgi com uma idéia de escrever uma ferramenta que iria facilitar a vida de muitos nesta área de engenharia reversa e achei de API Inspector.
      A seguir um spoiler de como foi o início do projeto, para quem se interessar. 😉
      O que é o API Inspector
      É uma ferramenta de código-aberto voltada para área de engenharia reversa, que irá auxiliar na análise de funções correspondentes a certas API's do Windows, retornando informações obtidas dos argumentos caso a função seja chamada pela aplicação.
      O que ele faz
      Ele faz um hook (do Inglês "gancho"), que consiste num desvio na função original da API solicitada para nossa própria função e com isso podemos obter os dados (argumentos/parâmetros) que foram passados para tal função.
      Como ele funciona
      O princípio de um hook é simples: você insere no inicio da função um salto que irá levar para a sua função (que é uma cópia da função original) e depois de efetuar o que quiser, irá retornar para a função original prosseguir.
      Talvez mais fácil visualizar o que expliquei com código:
      //Aqui está a função //ZwWriteVirtualMemory | NtWriteVirtualMemory, originada do binário: ntdll.dll //créditos ao https://undocumented.ntinternals.net/ NTSYSAPI NTSTATUS NTAPI //WINAPI NtWriteVirtualMemory( IN HANDLE ProcessHandle, IN PVOID BaseAddress, IN PVOID Buffer, IN ULONG NumberOfBytesToWrite, OUT PULONG NumberOfBytesWritten OPTIONAL ); //Sua versão assembly 777F2110 mov eax,0x3A 777F2115 mov edx,ntdll.77808D30 777F211A call edx 777F211C ret 0x14 //O que nós vamos fazer é criar uma função similar á ela com o nome que decidirmos //Então vamos inserir um jmp no início da função original para nossa função, ficando assim: 777F2110 jmp api inspector.573523EC 777F2115 mov edx,ntdll.77808D30 777F211A call edx 777F211C ret 0x14 //Usei como exemplo minha próprio ferramenta! //Então quando ocorrer a chamada desta função ela será jogada em nossa função! Depois de nós fazermos que desejar vamos retorna-la, porém para uma região que aloquei onde contém //Um buffer dos bytes que foram sobrescritos da função original: 03610000 mov eax,0x3A 03610005 jmp ntdll.777F2115 //Ela irá retornar depois do jmp que existe na função original e continuar o código.... Vantagens de se utilizar o API Inspector ao invés de um debugger
      Imagine que você está visualizando as chamadas intermodulares (para bibliotecas externas, no caso) que um programa faz, utilizando um debugger (o x64dbg por exemplo) e notou que uma certa função que deseja inspecionar é chamada em diversos pontos do programa. Vejo duas opções neste caso: colocar vários breakpoints, um em cada chamada à função, no código do programa ou colocar um único breakpoint função em si, no código dela, na DLL.
      Em ambos os casos, você vai precisar analisar chamada por chamada, parâmetro por parâmetro. E se a função for chamada 20 vezes consecutivamente? O tempo que você levaria para visualizar apenas o primeiro parâmetro da chamada é o tempo que a ferramenta iria levar para exibir todas as 20 chamadas, com os argumentos formatados bonitinhos ao seu dispor. Entende a vantagem? 🙂
      E as desvantagens?
      Por hora, uma desvantagem é a quantidade de funções e API's suportadas. De fato, a primeira release não possui uma quantidade significativa que vá fazer você utilizar a ferramenta e nem uma quantidade de recursos interessantes na ferramenta. Mas é ai que vem o ponto chave, o fato de deixar ela pública remete ao próprio crescimento da mesma, no primeiro momento é necessário uma orientação da parte de vocês para me ajudar a melhorar o código visual. O segundo passo é eu e vocês começarem a fornecerem mais recursos para ela. Eu irei adicionar todo ou qualquer recurso que seja significativo para a mesma, e para isso eu já tenho mais funcionalidades para implementar na ferramenta que são excelentes.
      Interface gráfica
      Na imagem abaixo, utilizei o API Inspector para hookar a função MessageBoxW() da USER32.DLL. Depois disso, escrevi um texto num novo arquivo no Notepad++ e tentei fechar o programa. Ao fazer isso, o Notepad++ perguntou se eu queria salvar o arquivo e ele faz isso através de uma chamada à MessageBoxW(), que o API Inspector interceptou prontamente.

      Na imagem acima, a janela à esquerda mostra o que está atualmente passando pelas funções hookadas. Na janela a direita, temos um log.
      Como utilizar o API Inspector
      A única coisa que você precisa fazer é anexar a DLL do API Inspector ao processo desejado e para isso existem os softwares chamados "Injetores de DLL" que podem ser achados na internet.
      Você também pode criar o seu próprio injetor. Uma dica é pesquisar sobre injeção com a função LoadLibrary(), mas no exemplo a seguir eu vou mostrar como utilizar o Process Hacker para fazer a injeção.
      1 - Abra o Process Hacker e identifique no mesmo o processo no qual você quer injectar a DLL do API Inspector. No exemplo, usei o processo do Notepad++.

      2 - Clique com o botão direito sobre o processo e escolha Miscellaneous > Inject DLL.

      3 - Selecione a DLL API-Inspector.dll e clique em Abrir.

      4 - Se o Process Hacker possuir privilégios suficientes a ferramenta irá ser carregada, caso contrário, não.

      Após isso você precisa selecionar a API desejada, a função desejada e clicar em GO Hook!
      O step call é uma funcionalidade que vai fazer a ferramenta aguardar o pressionamento da tecla ENTER para retornar para a função original. Pronto, o seu hook está feito e você já poderá inspecionar a função desejada.
      Download e código
      No repositório do API Inspector no Github você pode baixar a versão compilada e ter acesso ao código-fonte também. Contribuições são muito bem vindas!
      Bom, eu nunca tinha escrito um artigo. Se faltou informação ou coloquei informação demais me desculpe. Estou aberto pra ler os comentários. Ah, e participem deste projeto! Eu quero fazer ele crescer muito. Caso precise de referências de como cheguei a este projeto, tem tudo na página inicial do projeto no Github.
      Agradecimentos
      Obrigado novamente ao Fernando Mercês, ao pessoal do Terra 1337 que me incentiva cada vez mais e em especial para o iPower e Luan que são colaboradores do projeto.
      Referências
      Dear ImGui Programming reference for the Win32 API NTAPI Undocumented Functions C++ 3D DirectX Programming
    • By Candeer
      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...