Jump to content
  • Vitor Mob
    Introdução
    Olá, neste artigo irei abordar a emulação do Intel 8080, com tópicos sobre lógica, instruções e rotinas. O projeto completo do código está no meu Github. 😉
    Para emular o Intel 8080, usei C++, linguagem com a qual estou familiarizado com a sintaxe. Neste artigo irei demonstrar trechos de códigos com instruções usadas pelo Intel 8080 e explicar a lógica por trás. Requisitos mínimos para compreender o artigo incluem C++ e conhecimentos sobre o funcionamento de processadores (flags de estado, registradores, instruções).
    História
    Estudando sobre processadores, me veio a dúvida: Como a lógica de um processador é implementada?. Para compreender isso, precisei estudar um pouco sobre Verilog, porém meu intuito era fazer a emulação de um processador, então decidi emular o Intel 8080, que foi o segundo microprocessador de 8 bits da Intel, tendo como antecessor i8008 e sucessor o i8085.
    O Intel 8080 possui 65536 bytes de memória disponível e 7 registradores de 8 bits (A, B, C, D, E, H e L). Ele obtém as operações que vai executar a partir de pares de registradores de 16 bits (HL, BC e DE) e utiliza 5 flags de estado (PF, SF, AC, CF e ZF). Vamos ver agora como avancei nesse desenvolvimento. 🙂
    O que é emulação?
    Da Wikipedia: "Na computação, um emulador é um software que reproduz as funções de um determinado ambiente, a fim de permitir a execução de outros softwares sobre ele. Pode ser pela transcrição de instruções de um processador alvo para o processador no qual ele está rodando, ou pela interpretação de chamadas para simular o comportamento de um hardware específico. O emulador também é responsável pela simulação dos circuitos integrados ou chips do sistema de hardware em um software. Basicamente, um emulador expõe as funções de um sistema para reproduzir seu comportamento, permitindo que um software criado para uma plataforma funcione em outra. Também são disponíveis emuladores de consoles de vídeo games".
    Para construir um emulador, então, precisamos reproduzir o funcionamento de algo. No caso, do Intel 8080. Este tem várias partes que precisam ser reproduzidas (registradores, pilha de memória, flags, etc), mas precisamos começar de algum lugar, né? Vamos em frente!
    Desenvolvimento
    Memória / Stack
    Vamos começar pela pilha (stack). O processador precisa colocar dados nela com a instrução PUSH e remover/recuperar com a instrução POP. Abaixo minha implementação:
    #define MAX_MEMORY 0x10000L  inline byte_t memory[MAX_MEMORY]; void Instructions::push(word_t data16) { SP -= 2; memory::write_memory_word(SP, data16); } word_t Instructions::pop() { word_t mem = memory::read_memory_word(SP); SP += 2; return mem;  } Podemos notar, quando efetuamos o push(), que o registrador SP (Stack Pointer) é decrementado em duas unidades. Analogamente, SP é incrementado em duas unidades quando a função pop() é chamada.
    Dois registradores que são essenciais para o uso da stack, o SP, e o PC :
    (SP)Stack Pointer Operações de empilhamento são realizadas por várias das instruções, e facilitar a execução de sub-rotinas e tratamento de interrupções de programa. Especifica quais endereços as operações de stack irão operar por meio de um registro especial acessível de 16 bits chamado Ponteiro da Pilha(SP), (PC) Program Counter  É um registrador de 16 bits que é aceito acessível ao programador e cujo conteúdo indique o endereço da próxima instrução a ser executada. Nota: O Intel 8080 endereça a memória em 16-bits através do registrador de Stack Pointer (SP). O tipo word_t é uma palavra-chave para o tipo uint16_t. O mesmo vale para byte_t e uint8_t.
    Flags
    As flags de estado indicam resultados de operações. No Intel 8080, são elas:
    Sign (S), definida quando o resultado é negativo.  Zero (Z), definida quando a operação tem como resultado zero.  Parity (P), definida quando o número de bits 1 em uma operação é par. Carry (C), definida se a última operação de adição resultou em um transbordo ou se a última operação de subtração exigiu um empréstimo. Auxiliary Carry (AC ou H), usada para aritmética com binary-coded decimal (BCD).  Para implementar as flags, utilizei operadores de bitwise. Por exemplo, segue a implementação da instrução ADD, que faz uma operação de adição:
    void Instructions::add(word_t data16) { word_t work16 = A + data16; A = work16 & 0xff; CF = (work16 & 0x100) != 0; AC = ((A >> 7) & 0x1) != 1; SF = (A & 0x80) != 0; ZF = (A == 0);  PF = parity(A); } Nota: Utilizei o registrador A (Acumulador) para somar o argumento da instrução ADD. Falarei mais sobre os registradores mais a frente no texto.
    Vamos à explicação da configuração das flags no contexto da instrução ADD:
    CF recebe o resultado de uma operação de bitwise, pegando os 8 bits menos significativos e verificando se o resultado é 1.   AC recebe o resultado de uma operação de bitwise movendo 7 bits a direita e verifica se é 0.  SF pega os bits mais significativos e verificar se o resultado é 1. ZF verifica se o registrador A é 0, ou seja, se a nossa adição resultou em um valor 0. PF depende do resultado da função parity(), explicada abaixo. Flag de Paridade
    Para implementar a flag de paridade, escrevi uma função que percorre os 8 bits mais significativos do meu registrador e faz a contagem de quantos bits 1 foram encontrados.
    Caso essas operações de bitwise retornem verdadeiro, a flag é setada para 1. Caso contrário, 0. Segue a função:
    inline static bool parity(byte_t n) { word_t p_count = 0; for (int i = 0; i <= 8; i++) p_count += (n >> i) & 1; return !(p_count&1); } Registradores
    O Intel 8080 possui sete registradores de 8-bits, sendo A o registrador principal, na sequência temos B, C, D, E, H e L.
    Para trabalhar com números de 16-bits, o Intel 8080 acessa os pares de registradores HL, DE e BC. O par HL é tipicamente usado para endereçar a memória (H = high e L = low). Algumas instruções também permitem que o par de registradores HL seja usado como um acumulador de 16 bits.
    Implementação do par HL:
    void Instructions::set_hl(word_t data16) { L = data16 & 0xff; H = data16 >> 8; } Instruções
    Sem dúvidas meu maior desafio foi na implementação das instruções do processador. De acordo com o manual Intel 8080 Programmers, este processador possui 78 instruções de categorias variadas: aritméticas com binários e decimais, lógicas, de operação com um byte de dados, saltos, etc. Segue alguns exemplos de conjuntos de instruções:
    Adição
    ADD (Add Register or Memory to Accumulator) ADC (Add Register or Memory to Accumulator With Carry) ACI (Add Immediate to Accumulator With Carry)  DAD (Double Add) Subtração  
    SUB (Subtract Register or Memory) SBB (Subtract Register or Memory From Accumulator With Borrow)  SUI (Subtract Immediate From Accumulator) SBI (Subtract Immediate From Accumulator With Borrow) CMP (Compare Register or Memory With Accumulator)  CPI (Compare Immediate With Accumulator Direct Addressing Instructions) Rotação
    RLC (Rotate Accumulator Left) RRC (Rotate Accumulator Right) RAL (Rotate Accumulator Left Through Carry) RAR (Rotate Accumulator Right Through Carry)  Lógicas
    ANA (Logical and Register or Memory With Accumulator) ORA (Logical or Register or Memory With Accumulator) XRA (Logical Exclusive-Or Register or Memory With Accumulator (Zero Accumulator)) ANI (And Immediate With Accumulator) ORI(Or Immediate With Accumulator)  XRI (Exclusive-Or Immediate With Accumulator)  Eu utilizei uma construção switch/case em C++ para implementar o reconhecimento das instruções do Intel 8080, no método i8080:execute_opcode(). Nele, fiz um grande switch para testar os opcodes de cada instrução que decidi emular, utilizando esta página como referência.
    Vejamos algumas implementações. Nos exemplos abaixo, data16 é um valor imediato.
    A instrução SUB deve fazer A = A - data16. Flags afetadas: CF, AC, SF, ZF e PF. Segue a implementação:
    void Instructions::sub(word_t data16) { word_t work16 = A - data16; A = work16 & 0xff; AC = ((A >> 7) & 0x1) != 1; CF = (work16 & 0x100) != 0; ZF = (A == 0); SF = (A & 0x80) != 0; PF = parity(A); } Já a instrução RAL deve rotacionar os bits de um número uma única vez para a esquerda. Apenas a flag CF é afetada:
    void Instructions::ral() { bool flag = CF; CF = A >> 0x07; A = ( A << 0x01 ) | flag; } A instrução ANA faz um AND bit-a-bit entre A e o byte passado como argumento. Flags afetadas: CF, AC, ZF, SF e PF. Ficou assim:
    void Instructions::ana(word_t data16) { byte_t work16 = A & data16; CF = work16 >> 8; AC = ((A | data16) & 0x08) != 0; ZF = (work16 & 0xff) == 0; SF = (work16 & 0x80) != 0; PF = parity(work16 & 0xff); A = work16; } Essas são algumas das implementações de instruções que fiz. No total, foram 19 instruções emuladas, além de operações na stack. Para mais detalhes, testes e acesso completo ao código fonte, basta acessar o repositório do projeto no Github. Contribuições e feedbacks são muito bem vindos! Valeu! 😉

    Tempest Security Intelligence
    A crescente digitalização que vem ocorrendo nos últimos anos em todas as esferas do cotidiano tem afetado diretamente tanto pessoas quanto empresas. Cada vez mais, consumidores compram online, funcionários trabalham remotamente, empresas migram serviços e dados para ambientes cloud, adotam ferramentas de analytics - sem falar de outras tecnologias que já estão no horizonte ou em processo de adoção como IoT, 5G, I.A., etc. 
    Tudo isso vem acompanhado de novos desafios e riscos que exigem uma postura mais proativa na proteção das informações e ativos de pessoas e empresas. 
    Adotar essa postura envolve uma jornada que deve considerar as particularidades do negócio e sua relação com toda essa tecnologia, especialmente em uma realidade altamente complexa, com pessoas, unidades de negócios, terceirizados e fornecedores acessando ativos e dados valiosos através de inúmeras redes, dispositivos e aplicativos.  
    É nesse cenário que CIOs, CISOs e os times encarregados de proteger o negócio precisam identificar e corrigir vulnerabilidades dentro das empresas, estejam elas onde estiverem .
     
    Um cenário complexo que pede uma postura de segurança proativa 
    Uma postura proativa na segurança da informação pode ser resumida na habilidade de antecipar as táticas, técnicas e procedimentos usados por atacantes. 
    Para isso é preciso, antes de tudo, pensar como ele, bem como ter acesso às mesmas ferramentas e habilidades que eles teriam. 
    Isso demanda acesso a uma solução que envolva uma equipe especializada, que seja capaz de simular o atacante, identificando e simulando a exploração de falhas de segurança em profundidade, evidenciando os principais impactos ao negócio. E o Pentest é o que melhor representa essa postura. 
    Pentests, ou testes de penetração, nada mais são do que uma série de testes executados para identificar os pontos mais vulneráveis a um incidente cibernético, avaliar a exposição da organização e sua capacidade de lidar com esses incidentes. Eles podem ser aplicados a qualquer dispositivo ou ambiente - redes, aplicativos, dispositivos pessoais, sistemas de controle industrial, dispositivos IoT ou mesmo carros e aviões. 

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

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

    A importância de contar com o parceiro correto para proteger seu negócio
    É  fundamental contar com parceiros com expertise técnica que ofereçam produtos, serviços e conhecimento para identificar possíveis brechas de segurança na organização.
    A Tempest Security Intelligence é a maior empresa brasileira especializada em  cibersegurança e prevenção a fraudes digitais.
    Hoje contamos com um time de mais de 450 profissionais e escritórios em Recife, São Paulo e Londres; nos últimos anos a Tempest ajudou a proteger mais de 500 empresas de todos os portes de setores como serviços financeiros, varejo e e-commerce.
    Pesquisando  e criando novas soluções de proteção digital, a Tempest alia expertise técnica, sólida metodologia e alta tecnologia para entregar um portfólio com mais de 70 soluções, envolvendo Consultorias, Digital Identity, Managed Security Services e Integração.
     

    paulosgf
    Introdução
    Reunir informações sobre técnica de "Anti- Engenharia  Reversa" é a proposta dessa série de artigos que serão publicados aqui no Mente Binária. É um tema em que tenho pesquisado e criado pequenos tutoriais práticos para mim mesmo durante o último ano, e resolvi compartilhá-los. 
    Em cada um deles darei uma passada nas principais técnicas encontradas em todas as classes de defesa dos criadores de malware, além de mostrar como desabilitá-las. Tudo com exemplos de código para se entender na prática mesmo. 
    *Para a implementação estarei utilizando o Visual Studio Community, pois este tem o suporte ao ambiente C Runtime (CRT) necessário.
    A grande maioria das técnicas que serão apresentadas é para o ambiente Windows. Alguns poucos casos, que serão informados, são para Linux.
    Como pré-requisito, é necessário algum conhecimento de linguagem C\C++, um pouco de Assembly e principalmente Engenharia Reversa. Todos estes tópicos são abordados nos cursos gratuitos no canal do Mente Binária no Youtube.
    *No caso, não apresentarei a técnica mais simples/comum que utiliza a função IsDebuggerPresent(), pois esta técnica é explicada na última aula do curso do CERO aqui do Mente Binária.
    Classes de Anti-Engenharia Reversa
    Essas classes são uma forma de categorizar os métodos de "Anti-Engenharia Reversa", agrupando métodos de evasão semelhantes num mesmo grupo ou “classe”. Como não existe uma classificação oficial, estas classes estão baseadas na divisão apresentada nesta referência, com algumas adaptações.
    TLS Callback
    Windows:
    A Thread Local Storage (TLS) é um recurso do Windows  para definir objetos (variáveis) na memória que possam ter valores diferentes para cada thread criada por um processo.
    Por exemplo, ao imprimir um documento, uma thread se encarrega de mostrar o documento na tela, enquanto outra acessa esta mesma informação simultaneamente para lidar com a impressão. A thread de impressão pode ter uma variável que armazena a quantidade de páginas impressas, porém, esta variável não se faz necessária na thread que apresenta o documento na tela.
    Esta informação é armazenada (daí o nome "Thread Local Storage") numa região definida no cabeçalho dos binários PE (Portable Executable), e o acesso só é permitido para sua respectiva thread.
    As funções TLS Callback executam antes do binário alcançar seu "ponto de início", chamando e inicializando estes objetos TLS através de métodos construtores e os removendo da memória por métodos destrutores após seu uso. Com isso em mente, as TLS Callbacks também podem ser utilizadas pelos desenvolvedores de malware para permitir que se execute código antes que o binário chegue à região tradicional de início do programa, conhecido como Entrypoint, que irá levar em seguida à função main(). Isto cria diversas oportunidades como por exemplo executar código malicioso antes que o depurador possa detectá-lo (levar o malware a encerrar o processo antes mesmo de executar as rotinas maliciosas ao perceber que está sendo depurado, por exemplo).
    Alguns exemplos de malwares que empregam TLS Callbacks no Windows são:
    Nadnadzzz botnet de 2009; Grum botnet de 2008, através do Grum rootkit; Ursnif (mais recente); Implementação
    A biblioteca C Runtime (CRT) do Visual Studio provê suporte para fácil criação de TLS Callbacks (como comentado aqui) graças ao código em “C:\Program Files (x86)\Microsoft Visual Studio xx.0\VC\crt\src\tlssup.c”, que cria um diretório de dados TLS baseado na seguinte estrutura:
    typedef struct _IMAGE_TLS_DIRECTORY32 { DWORD StartAddressOfRawData; /* Início da seção TLS AddressOfRawData*/ DWORD EndAddressOfRawData; /* Endereço final na seção TLS */ DWORD AddressOfIndex; /* Índice da seção TLS */ DWORD AddressOfCallBacks; /* Ponteiro para o array de funções callback */ DWORD SizeOfZeroFill; DWORD Characteristics; } __tls_used;  
     Para criarmos a nossa própria callback precisamos primeiro defini-la no seguinte formato:
    VOID WINAPI tls_callback1( PVOID DllHandle, DWORD Reason, PVOID Reserved) { codigo_funcao; } As constantes para DWORD Reason podem ser:
    DLL_PROCESS_DETACH = 0 DLL_PROCESS_ATTACH = 1 DLL_THREAD_ATTACH = 2 DLL_THREAD_DETACH = 3 Depois a callback precisa ser alocada da seguinte forma:
    PIMAGE_TLS_CALLBACK ponteiro_tls_callback = tls_callback1; Após esta contextualização do mecanismo de funcionamento, segue um exemplo de código onde a função de callback detecta o ambiente de depuração através da função IsDebuggerPresent() e sai informando que o mesmo foi descoberto. Caso contrário, informa que o programa está executando normalmente:
    #include <iostream> #include <windows.h> using namespace std; // Declara uma variável global requerida para a chamada TLS Callback static int v1 = 0; // Declara a callback VOID WINAPI tls_callback1( PVOID DllHandle, DWORD Reason, PVOID Reserved) { if (Reason == DLL_PROCESS_ATTACH) { v1 = 1; // dentro da Callback altera o valor da variável if (IsDebuggerPresent()) { cout << "Stop debugging program!" << endl; TerminateProcess(GetCurrentProcess(), 0x1); exit(1); } } } // Cria objeto conforme a arquitetura através de #pragmas, que são instruções específicas do compilador #ifdef _M_AMD64 // para arquitetura x86_64 #pragma comment (linker, "/INCLUDE:__tls_used") // instrui linker usar o diretório TLS #pragma comment (linker, "/INCLUDE:p_tls_callback1") // instrui linker usar ponteiro do mesmo tipo da callback tls_callback1 declarada antes #pragma const_seg(push) // carrega o ponteiro na stack do compilador para uso da callback no segmento de dados const (.rdata) #pragma const_seg(".CRT$XLA") // cria nova seção TLS EXTERN_C const PIMAGE_TLS_CALLBACK p_tls_callback1 = tls_callback1; // atribui tls_callback1 ao ponteiro p_tls_callback1 #pragma const_seg(pop) // remove o ponteiro da stack após o uso #endif // fim deste bloco #ifdef _M_IX86 // para a arquitetura x86. as instruções tem as mesmas finalidades do bloco anterior #pragma comment (linker, "/INCLUDE:__tls_used") #pragma comment (linker, "/INCLUDE:_p_tls_callback1") #pragma data_seg(push) #pragma data_seg(".CRT$XLA") EXTERN_C PIMAGE_TLS_CALLBACK p_tls_callback1 = tls_callback1; #pragma data_seg(pop) #endif // main() só será executada depois, quando um depurador não for detectado int main(int argc, char* argv[]) { cout << "Normal execution!" << endl; printf("test value from tls callback is: tls = %d\n", v1); return 0; } Abaixo estão algumas dicas de como lidar com TLS Callbacks em diversas ferramentas:
    x64dbg: Options → Preferencias → Eventos → TLS Callbacks OllyDbg: Options -> Debug Options -> Events -> Make first pause at -> System breakpoint (para em TLS Callbacks) OllyDbg: Plugin Olly Advanced → Break on TLS Callback IDA Pro: Ctrl+E → Choose an entry point IDA Pro (Debugger): Debugger -> Debugger options -> Events -> Stop on (marcar todas as opções) Linux:
    O Linux também suporta o Thread Local Storage (TLS), conforme descrito nestes 2 excelentes artigos:
    https://maskray.me/blog/2021-02-14-all-about-thread-local-storage https://chao-tic.github.io/blog/2018/12/25/tls No entanto, o recurso de TLS no Linux aparentemente só permite a inicialização de variáveis ou objetos, e não chamada de função (como é o caso no Windows). Isto somente seria possível de dentro da main(), como nesse pequeno exemplo que usa o recurso de suporte a threads da Glibc:
     
    #include <stdio.h> // TLS que define e inicializa a variável __thread int main_tls_var = 2; int main() { printf("%d\n", main_tls_var); return 0; }  
    $ ./tls 2 No entanto, ainda é possível executar funções antes ou depois da main() no Linux através das funções construtoras e destrutoras devido ao suporte do GCC. Observem que uma função construtora em C++ instancia uma variável, definindo seu valor, enquanto que a destrutora a remove da memória após a conclusão da execução (normalmente em retorno de funções):
     
    #include<stdio.h> /* atributo construtor em myStartupFun() para executar antes de main() */ void myStartupFun (void) __attribute__ ((constructor)); /* atributo destrutor em myCleanupFun() para executar depois de main() */ void myCleanupFun (void) __attribute__ ((destructor)); /* implementacao de myStartupFun */ void myStartupFun (void) { printf ("startup code before main()\n"); } /* implementacao de myCleanupFun */ void myCleanupFun (void) { printf ("cleanup code after main()\n"); } int main (void) { printf ("hello\n"); return 0; } *Por padrão a __libc_start_main(*main(), argc, **argv, __libc_csu_init(), __libc_csu_fini()) é a função da glibc que chama a main(), onde __libc_csu_init é o construtor e __libc_csu_fini é o destrutor.
    No Linux também é possível evadir o depurador através da função ptrace, mas isso é um assunto que abordarei mais para frente, inclusive com exemplo prático.
    Por hora convido vocês a continuarem sintonizados, pois no próximo artigo começarei a tratar de técnicas da classe que utiliza flags de depuração para detectar o debugger.
    Forte abraço & até lá!
    Referências:
    https://www.codeproject.com/Articles/1090943/Anti-Debug-Protection-Techniques-Implementation-an https://anti-debug.checkpoint.com/ https://rayanfam.com/topics/defeating-malware-anti-vm-techniques-cpuid-based-instructions/ https://evasions.checkpoint.com/ https://anti-reversing.com/Downloads/Anti-Reversing/The_Ultimate_Anti-Reversing_Reference.pdf

    Caue Obici
    Nesse artigo vamos abordar sobre segurança de smart contracts, que afetam diretamente dApps e o universo das criptomoedas, uma vez que os contratos são a base da Web3. Veremos as principais vulnerabilidades nesse ecossistema e uma vulnerabilidade real crítica que foi encontrada. O problema dessas vulnerabilidades é que, como o universo blockchain tem uma forte relação com dinheiro, elas são capazes de causar danos financeiros de bilhões de dólares.
     O que exatamente é Web3?
    Web3 é o nome que se dá a "nova era" da internet que está surgindo com a tecnologia blockchain e Smart Contracts. Na Web3 temos a descentralização como a principal característica, "descentralized Applications" (dApps) é o nome que se dá às aplicações dessa nova categoria, onde não existe um servidor centralizado, tudo está registrado na blockchain, que é uma tecnologia descentralizada por design. Nessas aplicações podemos encontrar governanças descentralizadas (DAOs), tokens sendo a moeda principal da aplicação ao invés de real ou dólar e, o principal, códigos sendo executados na blockchain por meio de Smart Contracts.
    E o que são Smart Contracts?
    Smart Contracts são códigos com funções definidas que, quando são chamadas e executadas, se a execução for bem sucedida, tudo é registrado na blockchain, e a partir daí nada mais altera o que foi executado e nem retira da blockchain. Smart Contracts podem ser escritos em diversas linguagens atualmente, mas quando estavam surgindo, a linguagem Solidity foi criada para essa finalidade.  
    O que executa os Smart Contracts é a Ethereum Virtual Machine (EVM) que é uma máquina de estado mantida por vários computadores rodando um "node" do Ethereum. Aqui vamos falar da rede Ethereum que foi a que introduziu o conceito de Smart Contracts, porém muitas outras redes agora também usam essa tecnologia, como a Binance Smart Chain (BSC), Solana, Polygon etc.
    Rede Ethereum
    A rede Ethereum é uma das redes mais consolidadas e famosas do universo crypto, foi pioneira no modelo de Smart Contracts e possui como criptomoeda nativa o Ether (ETH).  
    Qualquer um é livre para criar uma aplicação descentralizada na rede Ethereum, o que significa que qualquer um pode escrever um Smart Contract e fazer "deploy" dele na rede. Quando o deploy de um Smart Contract é feito, é atribuído um endereço a ele (address), assim como carteiras de pessoas também possuem um endereço.  
    Para chamar uma função de um Smart Contract é necessário passar exatamente o que é para ser chamado no campo data de uma transação e assiná-la com sua chave privada (hoje existem várias bibliotecas que fazem isso automaticamente para você), além disso, também é necessário pagar uma taxa de transação, chamada de "gas fee" (pago em ETH). 
    Levando em conta que qualquer pessoa, a qualquer hora pode executar funções de outros Smart Contracts é necessário que o código destes esteja bem seguro, se não podem haver danos desastrosos.
    Vulnerabilidades em Smart Contracts
    Vamos percorrer algumas vulnerabilidades listadas no TOP 10 da DASP (Descentralized Application Security Project) - https://dasp.co/  
    Esse é o último TOP 10 vulnerabilidades em Smart Contracts que foram divulgadas pela DASP em 2018:
    1. Reentrancy
    2. Access Control
    3. Arithmetic
    4. Unchecked Low Level Calls
    5. DoS
    6. Bad Randomness
    7. Front Running
    8. Time manipulation
    9. Short Addresses
    10. Unknown Unknowns
    1. Reentrancy
    Reentrancy é provavelmente a vulnerabilidade mais famosa dos Smart Contracts, que já foi encontrada em diversos contratos de grandes dApps e podia causar milhões de dólares de prejuízo.  
    A vulnerabilidade é causada quando uma função de um contrato chama uma função do controle do atacante, a partir daí é possível chamar a mesma função novamente e, dependendo de como essa função está programada, é possível induzir erros na lógica da aplicação.
    Exemplo
    function withdraw(uint _amount) external{      require(balances[msg.sender] >= _amount);     msg.sender.transfer(_amount);      balances[msg.sender] -= _amount;  } Essa função, parte de um Smart Contract programado em solidity, tem o objetivo de sacar ETH que estava armazenado nesse contrato. 
    Antes de ver a vulnerabilidade, precisamos saber um pouco sobre algumas coisas do solidity para melhor entendimento da função. A variável balances é um mapping(address => uint), isso significa que é um "dicionário" que usa o endereço de uma carteira (address) como a chave e usa um valor inteiro positivo (uint) como valor dessa chave, nesse caso, o valor inteiro está representando a quantidade de ETH que aquele endereço possui (solidity não utiliza valores decimais, ele utiliza wei - 1 ETH = 10^18 wei ). Outra coisa que gera confusão é o msg.sender, que para o solidity é quem chamou essa função, podendo ter sido uma carteira comum (EOA - Externally Owned Account) ou um Smart Contract. Isso significa que esses 2 tipos de carteira podem chamar essa função, porém os Smart Contracts têm funcionalidades a mais que podem ser usadas para explorar a vulnerabilidade.
    O fluxo dessa função é o seguinte:
     - Requer que o valor requisitado para sacar (_amount) seja menor do que o valor que o address realmente tem armazenado nesse contrato, caso contrário, a transação é revertida e não altera nada no estado da blockchain.
     - Envia os ETH requisitados para quem chamou a função (msg.sender) e chama a função receive(), se definida, no msg.sender. (para isso, o msg.sender deve ser um Smart Contract e não um EOA)
     - Atualiza o valor do balances de acordo com o quanto foi sacado.
    Agora vamos para a vulnerabilidade em si. Essa função, ao transferir ETH para quem a chamou, executa a função receive() por padrão (se existir no contrato que a chamou). Portanto, nessa situação é possível escrever um Smart Contract em que a função receive() chama a função withdraw() desse contrato novamente. Assim, é possível sacar diversas vezes o valor que você possui pelo simples fato do valor de balances[msg.sender] ser atualizado apenas no final da função, que não é alcançado enquanto o receive() chamar withdraw() novamente. Como o valor de balances[msg.sender] não é alterado, o require() no começo da função sempre vai ser verdadeiro, sendo possível roubar mais valor do que você depositou no contrato.


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

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

    Casos reais
    As vulnerabilidades citadas aqui não foram consideradas top 10 por acaso. Elas foram encontradas em dApps reais e podiam causar um prejuízo milionário. Aqui estão alguns write-ups, sobre as vulnerabilidades citadas, encontradas em Smart Contracts nas principais mainnets (redes principais, não as de teste - testnets😞
    Reentrancy - The DAO Bad Randomness + Time Manipulation - Casino Access Control - Multi-sig Unchecked Return Values for Low-Level Calls - King of the Ether Denial of Service - GovernMental Front-Running - Sandwich Attacks Unknown Unknows - $BURG token Hoje em dia, a preocupação é dobrada em relação à segurança em Smart Contracts devido ao crescimento do ecossistema descentralizado blockchain. Os grandes projetos, além de fazerem um "audit", também possuem programas de Bug Bounty que recompensam pessoas que acharem vulnerabilidades em seus contratos.  
    A principal plataforma de Bug Bounty de Smart Contracts é o Immunefi, onde tem programas que oferecem até $10.000.000 por uma falha crítica.
    CTFs
    Para aprender na prática, nada melhor que CTFs (desafios de segurança) de Smart Contracts. Aqui estão alguns sites para aprender segurança de contratos inteligentes na prática:
    Capture the Ether Damn Vulnerable DeFi Lista de de CTFs sobre blockchain Referências
    EVM: https://ethereum.org/en/developers/docs/evm/  
    DASP: https://dasp.co/  
    OpenZeppelin: https://openzeppelin.com/  
    OpenZeppelin Docs: https://docs.openzeppelin.com/  
    Integer Overflow: https://solidity-by-example.org/hacks/overflow/  
    Ethereum Gas Security: https://medium.com/consensys-diligence/silent-but-vulnerable-ethereum-gas-security-concerns-adadf8bfb180  
    Ethereum blockchain explorer: https://etherscan.io/  

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

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

    Anchises
    Desde os últimos meses de 2021 estamos presenciando um cenário de redução da pandemia do novo coronavírus, graças a queda no número de casos e de vítimas. Apesar do surgimento de novas variantes que prometem estender o período de pandemia, aos poucos estamos retomando a vida normal, com a flexibilização das medidas de quarentena e isolamento social. Nesse processo de retomada, diversas empresas que foram obrigadas a adotar o regime de trabalho remoto estão voltando a utilizar seus escritórios, retornando ao trabalho presencial ou optando pelo modelo híbrido, onde os colaboradores podem trabalhar de forma remota ou presencial, dependendo de uma escala ou conveniência acordada com a empresa.
     
    Nesse retorno aos escritórios é importante relembrar alguns cuidados básicos de segurança que devemos ter no ambiente de trabalho, para proteger nossas informações pessoais e corporativas. Afinal, após tanto tempo isolados em casa, nos esquecemos de como conviver em um ambiente com trânsito de muitas pessoas e onde constantemente circulamos informações confidenciais. São cuidados simples e muito importantes, que já exercitávamos antes. Porém, como ficamos muito tempo fora do escritório, vale a pena relembrá-los:
     
    -Sempre bloqueie o computador ao sair da sua mesa: mesmo que seja por um curto período. Assim você evita que alguma pessoa mal intencionada acesse seus dados, suas informações pessoais e até mesmo os sistemas corporativos em seu nome. Essa dica também vale quando você está em uma sala de reunião ou em qualquer lugar externo, como um cibercafé, um coworking ou no escritório de um parceiro comercial;
    - Mesa limpa: não deixe documentos pessoais e confidenciais em cima da sua mesa. Ao sair do escritório, no intervalo do almoço ou no final do dia, não deixe tais informações expostas na mesa – nem mesmo objetos de valor. Como várias empresas adotaram o uso de posições de trabalho rotativas, lembre-se também de guardar ou levar consigo seus objetos pessoais no final do expediente;
    - Não deixe documentos na impressora: se você precisa imprimir algum documento, vá buscar imediatamente, para não correr o risco de esquecer. Se o documento não estiver bom e precisar imprimir novamente, tome o cuidado de descartá-lo adequadamente (veja a próxima dica!). A propósito, evite desperdícios, não imprima documentos desnecessariamente e tome cuidado redobrado ao imprimir documentos pessoais ou confidenciais;
    -Descarte seus documentos de forma segura: precisou descartar um documento impresso, use uma fragmentadora de papel. O ideal é que ela seja posicionada próximo as impressoras. Caso sua empresa não tenha fragmentadoras, então tome o cuidado de você mesmo picar o documento antes de descartá-lo;
    -Não anote suas senhas: você pode não acreditar, mas cedo ou tarde alguém vai encontrar aquela senha escrita no seu caderno de notas ou naquele post-it guardado embaixo do teclado ou na sua caixinha de tranqueiras em cima da mesa. Por isso mesmo, nunca anote as suas senhas – memorize ou use uma ferramenta para guarda segura de senhas;
    -Cuidado nas reuniões com pessoas externas: como um fornecedor, por exemplo. Ao receber uma visita no escritório, é importante certificar que não há nenhum documento sensível esquecido na sala de reunião ou alguma informação confidencial anotada nas lousas. Verifique isso antes do início e após o final de cada reunião e lembre-se de apagar as anotações nos quadros das salas após acabar sua reunião.
     
    Na verdade, a maioria dessas dicas são muito úteis em qualquer lugar, não apenas no escritório: em casa, no hotel, num evento, num ambiente de coworking ou quando você for participar de uma reunião no escritório de terceiros. Faça delas um hábito e automaticamente você protegerá as suas informações. Também convém que essas dicas sejam compartilhadas e reforçadas pelos times de segurança das empresas, colaborando na educação dos usuários finais.
     

    R3tr074
    Durante o estudo de exploração de binários executáveis, uma das primeiras e mais importantes coisas que vemos são as proteções como NX, PIE, ASLR, de entre outras. Hoje vamos ver um pouco mais de perto cada uma delas.
     
    NX
    No eXecute (NX) é uma das proteções mais simples e mais lógica, implementada por volta de 2004. Durante a execução de um binário sem essa proteção ativada, a sua stack, heap e outras regiões possuíam poder de execução, simples assim.
    Os processadores da família x86 tardaram muito para implementar o bit NX, que basicamente é uma forma de sinalizar uma página em memória como executável ou não. Isso gerou projetos para tentar emular tal proteção via software, como o Exec Shield, um projeto da Red Hat que continha um patch no kernel do Linux para emular o funcionamento do NX.

    No kernel Linux, o suporte para o NX foi adicionado oficialmente em 2004. Patches como o mencionado acima, do Exec Shield, já existiam desde 2002. No Windows, a proteção está presente desde 2003 com o nome de DEP (Data Execution Prevention), porém a DEP emulada via software funciona de uma forma diferente: ela não se baseia no bit NX, mas sim checando sempre que uma exceção é realizada se o endereço da execução bate com a tabela de funções do binário.
    Essa proteção é ativada ou não durante o carregamento do binário na memória. No Linux isso é controlado com uma flag exec no segmento PT_GNU_STACK do binário. Já no Windows, as configurações ficam no arquivo Boot.ini, podendo ter até quatro opções diferentes:
    OptIn: Apenas em binários do sistema a proteção é ativada. OptOut: A DEP é ativada por padrão para todos os processos. AlwaysOn: Todos os processos têm a DEP ativada e não existe a possibilidade de desativar para um processo específico. AlwaysOff: A proteção não é habilitada para nenhum processo.  
    PIE/PIC
    O Position-Independent Executable (PIE) ou Position-Independent code (PIC) não é em si uma proteção, mas uma técnica para criar código, assim como o nome diz, independente de posição. Isso significa que, onde o código for alocado, ele conseguirá executar sem problemas, diferente de binários comuns, os quais precisam ser carregados em lugares específicos para fazerem referências de forma correta. Essa técnica é usada principalmente para bibliotecas (shared libraries).

    O problema disso durante a exploração é que os endereços do binário também são randomizados com a ASLR, proteção que vai ser explicada a seguir, impossibilitando, assim, conhecer os endereços do binário e consequentemente utilizar Return Oriented Programming (ROP) para explorar uma vulnerabilidade deixa de ser viável.
    A solução para fazer bypass dessa proteção é conseguir um vazamento (leak) de memória, pois é possível calcular a base já que os offsets continuam os mesmos.
    Como o PIE é uma forma de gerar código, não é possível "desabilitá-lor", porém se a ASLR estiver desligada, o endereço base se manterá o mesmo, não tento efeito prático para mitigar uma ROP, por exemplo.
     
    ASLR
    A Address Space Layout Randomization (ASLR), assim como o nome diz, tem a função de randomizar endereços como os da heap, stack e bibliotecas. Como uma proteção em nível de sistema, cada sistema operacional mantém uma implementação própria.

    No Linux, a ALSR já está presente desde a sua versão 2.6.12, lançada em 2005. Executáveis PIE também possuem um endereço base randômico desde 2003, porém ambos com uma implementação fraca e com pouca aleatoriedade. O PaX e o Exec Shield (mencionado acima) foram responsáveis por patches com uma implementação mais complexa e segura. O endereço da stack com essas alterações podem ter até 524.288 variações diferentes.
    Por padrão, a ASLR é configurada no Linux através do arquivo /proc/sys/kernel/randomize_va_space e pode possuir três valores diferentes: 0 para desabilitar, 1 para randomizar stack, bibliotecas e executáveis PIE e 2, o valor mais alto, para randomizar também a base da heap. É possível, também, ativar ou desativar para processos isoladamente usando um recurso chamado personality .
    O Windows teve sua implementação da ASLR lançada em 2007 com uma abordagem diferente: a ASLR só é ativada em executáveis e DLLs que possuírem uma "flag" no processo de linkagem e todos os membros precisam possuir essa "flag". Por exemplo, suponha que A.exe dependa de B.dll e C.dll. Caso C.dll não tenha ASLR habilitada, A.exe não terá a proteção ligada. Hoje em dia essa é uma situação rara, pois apenas programas mais antigos não possuem suporte para ASLR.
     
    Canary
    O canary é uma proteção muito interessante. Ao compilar algum código com essa proteção, o compilador adiciona um fragmento de código no começo e no final de cada função. No prólogo da função, se cria uma variável local na stack com um valor aleatório e no final se acessa essa mesma variável para verificar a integridade do valor, validando se continua o mesmo. Caso tenha mudado, uma função de callback é chamada para gerar um abort, assim evitando transbordamentos de memória. O GCC, LLVM, Intel compiler entre outros compiladores possuem implementações do canary.

    O valor aleatório citado acima é o chamado canary ou stack cookie. Esse cookie pode ser classificado de três formas:
    terminator
    O terminator Canary se baseia na observação de que a maioria das vulnerabilidades de buffer overflow acontecem em cenários de manuseio de strings que finalizam com um null byte (\0). Em contrapartida, o cookie é construído com null byte (\0), CR (\r), LF (\n) e FF (\f). Como resultado, isso mitiga um buffer overflow em funções como strcpy(), pois, mesmo sabendo o valor do cookie, não é possível copiá-lo com esse tipo de funções, já que ela ira retornar assim que ler o nullbyte.
    random
    Neste tipo, Random Canaries são gerados randomicamente a cada execução a partir de alguma fonte de entropia. Além disso, esse valor não é facilmente lido, pois no carregamento do binário esse valor é armazenado em uma variável global, que é alocada em uma região não mapeada de memória para dificultar ainda mais seu acesso.
    As formas mais viáveis de ler o cookie são com um vazamento da stack ou sabendo o endereço exato da variável global.
    random XOR
    O random XOR Canary é o tipo mais complexo. Seu valor é basicamente determinado a partir de operações XOR de vários valores de controle, como stack pointer, endereço de retorno, etc. Dessa forma, se o Canary ou os dados usados no cálculo forem corrompidos, o programa irá gerar um abort. Para conseguir enganar essa checagem de integridade, é necessário vazar não apenas o cookie, mas também os dados usados no cálculo e o próprio algoritmo para gerar o mesmo.
     
    RELRO
    A RELocation Read-Only (RELRO) é uma proteção exclusiva do Linux, ou melhor, do formato ELF, que tem como intenção deixar as seções relacionadas a realocação como somente leitura (read-only). As seções de realocação, de forma resumida, são onde são armazenados os endereços de funções externas, como as da libc. Por padrão, essas regiões de memória possuem permissão de leitura e escrita (read/write), pois normalmente é utilizada lazy bind, uma forma de resolver esses endereços não em tempo de carregamento, mas durante a execução, logo antes da chamada.

    Essa proteção foca exatamente nessa região de meória. A realocação dos endereços é feita antes de passar o controle para o programa e a região é marcada como somente leitura, impossibilitando, assim, que um atacante a sobescreva com endereços maliciosos.
    Existem dois tipos de RELRO: partial e full, onde a partial marca como somente leitura a maioria das seções, exceto a Global Offset Table (GOT), na seção .got.plt. Já a full, como se deve imaginar, força que todos os símbolos e endereços sejam resolvidos durante o carregamento do binário, permitindo que a GOT seja completamente read-only.
     
    Referências
    - A hardware-enforced BOF protection:
      - http://index-of.es/EBooks/NX-bit.pdf
    - Windows ISV Software Security Defenses
      - https://docs.microsoft.com/en-us/previous-versions/bb430720(v=msdn.10)
    - Position-Independent Code with GCC for ARM Cortex-M
      - https://mcuoneclipse.com/2021/06/05/position-independent-code-with-gcc-for-arm-cortex-m/
    - Effect of ASLR to Memory Deduplication
      - https://www.semanticscholar.org/paper/Effect-of-ASLR-to-Memory-Deduplication-Ratio-in-)-Piao-Sung/3f74d25c72322315ec3b6552e6c3d4413af95022
    - Exploit Protection Mechanisms
      - https://ocw.cs.pub.ro/courses/cns/labs/lab-06
    - Hardening ELF binaries using Relocation Read-Only (RELRO)
      - https://www.redhat.com/en/blog/hardening-elf-binaries-using-relocation-read-only-relro
     

    Fernando Mercês
    Matrix é um sucesso absoluto para os amantes da computação. Sua história torna o submundo, o underground, algo tangível, ainda que ilusório. Parece que alguém finalmente conseguiu relacionar a teoria de que o universo é uma ilusão, tão explorada em textos filosóficos e religiosos, com os computadores. O sonho de todo nerd.
    O primeiro filme, lançado em 1999, tem nota 8.7 no IMDb – talvez o maior banco de dados com informações sobre filmes, séries e outras produções audiovisuais do mundo – e a nota de Matrix não é para menos: trouxe inovações em efeitos, som e imagem, um elenco incrível, enredo amarrado, personagens marcantes (você provavelmente lembra da Switch com seu “nevou”, do traidor Cypher e da bebida do Dozer, isso sem falar nos protagonistas).
    A espera para o Matrix 4 (5.7 no IMDb) foi grande e a comunidade amante de computação comemorou muito. Comigo não foi diferente. Apesar de ter ouvido algumas críticas, eu fui ao cinema na última terça-feira, desafiando a Ômicron e a H3N2, só para ver o que a diretora Lana Wachowski preparou. Vou contar aqui minhas percepções na ordem em que apareceram. Alerta de spoiler.
    História
    Achei o roteiro do filme maneiro. Colocar o Neo de volta na Matrix e fazer parecer que tudo foi um jogo que ele programou foi genial. Já o objetivo de acordar a Trinity achei bobo. Acorda e aí?
    De qualquer forma, aprendi que é possível reamarrar bem uma história já terminada.
    Inclusão
    Há muitos pretos no filme e muitas mulheres, incluindo uma espécie de protagonista (a Bugs – é, o nome é marmomeno). Curti muito isso e para mim é um exemplo incrível.
    Velhice
    Ver Neo e Trinity mais velhos, mais experientes, foi muito maneiro. Infelizmente isso não foi muito explorado.
    Ninguém precisa morrer
    Audacioso fazer um filme em que nenhum dos mocinhos sequer se fere gravemente. Vai contra quase todas as receitas de sucesso dos filmes de ação. Tudo no Matrix 4 parece ser bem fácil. Interessante.
    Fidelidade
    Os generais de Ion são bem fiéis à Niobe. Um deles diz “Se essa missão é importante pra você, nenhum de nós vai ficar em casa”, quando ela pede voluntários para ajudar no resgate da Trinity. Gostei. 🙂
    Efeitos
    As porradas no analista têm uns efeitos muito maneiros, mas Neo e Trinity abobam as cenas com um estalar de dedos, infelizmente. De qualquer forma, curti a inovação.
    Transcendência
    Neo e Trinity sabem exatamente o que é a Matrix e por isso não temem nada e controlam tudo. Por um lado é legal, porque realmente se sabemos que esse mundo não é real, nem nos preocupamos.
    Vilões
    Honestamente, não há vilões. Tem o analista que mantém o Neo na Matrix, mas é isso.
    Morpheus
    Enquanto no Matrix 1 o Morpheus é super sério, sabe da verdade e tal, no Matrix 4 o Morpheus é um malandrops que bebe drinks. E totalmente coadjuvante.
    Cenas de Matrix 1
    Muitas cenas do primeiro filme foram refeitas com menos capricho. Um exemplo é a do helicóptero, as cápsulas caindo e a aeronave finalmente batendo no prédio. Pareceu uma e uma tentativa pouco criativa de requentar o que deu certo.
    Trilha sonora
    Saudades do Rage Against The Machine…
    Hacking
    Tem uma única menção e mais nada. Bobo, bobo...
    Miscelânea
    O Neo não se vê como ele realmente é, mas isso é falado uma vez só no filme e ninguém nunca mais volta no assunto. A Niobe envelhecida me lembrou o Gugu no táxi do Gugu. Aprendi bastante coisa do que fazer e do que não fazer, em tudo. Normalmente meu trabalho aqui no Mente Binária envolve criar conteúdo e gerenciar a equipe. Aprendi com Matrix 4 algumas coisas que posso fazer e algumas coisas pra eu não fazer. Por exemplo, tentar "requentar" o que já foi feito não é legal, mas com certeza é possível pegar um curso que foi terminado e encaixar um novo que tenha o primeiro como o ponto de partida. Mas tem que ser realmente novo. Nada de tentar ressucitar o que já morreu. 🙂
     

    c4v0k

    Introdução ao RSA

    By c4v0k, in Articles,

    Continuando a série de artigos com foco em criptografia, hoje veremos um dos primeiros algoritmos de chave assimétrica, o RSA, que permite estabelecer uma conexão segura entre duas entidades sem contato prévio entre elas.
    Antes de prosseguir, vale relembrar boas práticas apresentadas no artigo do Chinchila: nunca implemente um esquema criptográfico, utilize bibliotecas reconhecidas pela comunidade, preferencialmente de código aberto.
    Outro ponto importante é que o RSA parece simples em termos matemáticos, mas é fácil de ser utilizado de maneira incorreta na construção de protocolos e/ou implementado de forma a inserir várias vulnerabilidades. O algoritmo básico apresentado aqui não garante proteção contra a maioria de tais vulnerabilidades. Por fim, serão apresentadas simplificações no algoritmo e código em Python para auxiliar a compreensão.
     
    Criptografia de chave assimétrica
    Como apresentado no primeiro artigo de criptografia, algoritmos de chave assimétrica possuem 2 chaves: uma chave pública e uma chave privada (secreta). O uso correto de ambas as chaves permite que duas pessoas estabeleçam um canal de comunicação seguro sem a necessidade de terem combinado chaves anteriormente.
    O funcionamento do algoritmo é análogo a uma caixa de correio como a da foto abaixo. Vamos considerar que a comunicação segura é estabelecida entre a pessoa que envia uma carta e o correio. Quando a pessoa vai enviar a carta, basta deixá-la na caixa. Para manter a analogia mais precisa, vamos considerar que existe uma chave disponível junto à caixa de correio e que ela deve ser utilizada para depositar a carta na caixa. Qualquer usuário pode ter acesso a chave para depositar cartas na caixa, por isso tal chave é denominada chave pública.

    Caixa de correio - Fonte: Wikipedia
    Apenas o Correio possui a chave necessária para abrir a caixa, portanto sabemos que somente ele pode receber as cartas depositadas na caixa. Esta chave é denominada chave privada.
    Dessa forma, o usuário pode enviar mensagens que somente o correio pode ler. Isso ocorre porque todas as mensagens deixadas na caixa só podem ser recebidas pelo detentor da chave privada, ou seja, o próprio correio.
    Um dos primeiros algoritmos capazes de proporcionar a funcionalidade descrita é o RSA.
     
    Aritmética modular
    A aritmética modular é feita apenas com números inteiros. Utilizando mais uma analogia, a aritmética modular é próxima ao funcionamento de um relógio. O ponteiro das horas de um relógio tem funcionamento cíclico, nunca é maior que 11 e menor que 0 (considerando que 12h = 0h). Sempre que o relógio ultrapassa 11h, o relógio volta a contar a partir de 0h. O mesmo vale para somar horas: somando 10h com 4h, o resultado será 2h. Nesse caso, diz-se que as operações são feitas "módulo 12", também representadas por "8 + 6 = 2 mod 12". 
    Computar o valor de a módulo b pode ser interpretada também como calcular o resto da divisão de a por b, por exemplo 23 = 2 mod 7, porque 23 = 7\*3 + 2, 16 mod 4 = 0, porque 16 = 4\*4 + 0
    Em Python, o operador "%" é utilizado para computar a operação modular:
    print(23%7) Além da soma, também existem as operações de multiplicação e potenciação modular. Essas operações são computadas assim como na aritmética regular. Em seguida, é calculado o resto da divisão pelo valor do módulo, por exemplo:
    3\*7 mod 5 -> 3\*7 = 21 = 4\*5+1 = 1 mod 5 
    3^5 mod 7 -> 3^5 = 243 = 34\*7 + 5 = 5 mod 7
    print(3*7%5, pow(3,5,7))  # no Python, pow(a,b,c) = (a**b)%c  
    O RSA
    O algoritmo RSA utiliza a potenciação modular, onde os expoentes são as chaves. Conforme a analogia da caixa de correio, existe uma chave pública, representada por e, e uma chave privada (secreta), representada por d. O valor do módulo é representado por N e é calculado como o produto de dois inteiros primos, representados por p e q. A segurança do algoritmo depende, entre outros fatores que não serão apresentados aqui, do tamanho de p e q, sendo recomendado usar primos de no mínimo 1024 bits.
    from random import randint from sympy import isprime def random_prime(lower_bound, upper_bound):     while True:         r = randint(lower_bound, upper_bound)         if isprime(r):             return r  p = random_prime(2**512, 2**513) q = random_prime(2**512, 2**513) N = p*q print(p, q, N) As chaves pública e privada devem ser calculadas de forma que uma mensagem encriptada pela chave pública pode ser desencriptada somente com o conhecimento da chave privada. Em outro artigo será apresentado o porque de calcular as chaves conforme o código abaixo.
    phi = (p-1)*(q-1) e = 17 d = pow(e, -1, phi) print(e, d) Consideramos que a mensagem a ser enviada seja "artigo_RSA_mente_binaria". Ela deve ser convertida para uma representação em números inteiros com a função bytes_to_long da biblioteca Pycryptodome:
    from Crypto.Util.number import long_to_bytes, bytes_to_long msg = b"artigo_RSA_mente_binaria" m = bytes_to_long(msg) print(m) A encriptação da mensagem consiste em elevar o valor inteiro da mensagem à chave pública e:
    ciphertext = pow(m, e, N) print(ciphertext) print(long_to_bytes(ciphertext)) É possível ver que, após a encriptação, é difícil ver qualquer relação entre a mensagem original e a mensagem encriptada.
    Para que o destinatário recupere a mensagem, basta que ele faça o mesmo procedimento da encriptação, mas utilizando o expoente secreto d:
    plaintext = pow(ciphertext, d, N) print(plaintext) print(long_to_bytes(plaintext))  
    Importante
    Esse artigo apresenta apenas uma introdução ao RSA, diversas partes importantes foram omitidas para facilitar a compreensão. Existem várias vulnerabilidades inerentes a uma implementação básica como essa. Uma boa forma de estudar e compreender melhor esse algoritmo e suas vulnerabilidades é analisar e implementar estes ataques. No ASIS CTF Quals 2021 havia um chall que envolvia uma versão modificada do RSA, mas suscetível às mesmas vulnerabilidades. O write-up do ELT pode ser encontrado aqui. Para saber mais, recomendo os challs do Cryptohack e as próximas postagens aqui no site.
    Bom estudo, se tiver dúvidas e/ou sugestões deixe nos comentário abaixo, até a próxima!

    Código
    from random import randint from sympy import isprime from Crypto.Util.number import long_to_bytes, bytes_to_long def random_prime(lower_bound, upper_bound):     while True:         r = randint(lower_bound, upper_bound)         if isprime(r):             return r  p = random_prime(2**512, 2**513) q = random_prime(2**512, 2**513) N = p*q print(p, q, N) phi = (p-1)*(q-1) e = 17 d = pow(e, -1, phi) print(e, d) msg = b"artigo_RSA_mente_binaria" m = bytes_to_long(msg) print(m) ciphertext = pow(m, e, N) print(ciphertext) print(long_to_bytes(ciphertext)) plaintext = pow(ciphertext, d, N) print(plaintext) print(long_to_bytes(plaintext))

    Saullo Carvalho Castelo Branco
    Este artigo aborda superficialmente tokens JWT e o seu uso no gerenciamento de sessão. Ele aponta uma razão para esse uso, os seus riscos e os ataques já sofridos por conta do uso de tokens JWT. Por fim, ele aponta uma abordagem que permite usar tokens JWT em combinação com tokens opacos para mitigar os riscos de segurança previamente listados.
    1. O que é JSON Web Token?
    JSON Web Token é um padrão aberto, especificado na RFC-7519, que define uma forma compacta e autocontida de transmitir com segurança informações entre interlocutores de comunicação (serviços e APIs, por exemplo) como objeto JSON. Neste artigo, apenas o token JWT estruturado será considerado.
    2. Estrutura do token JWT
    O token JWT é composto por três partes codificadas em base64 separadas por pontos. O trecho em cor vermelha é o cabeçalho do token. O trecho em cor roxa é o conteúdo do token. Por fim, o trecho em cor azul é a assinatura do token.
     

    * Cabeçalho
    O cabeçalho contém informações sobre o tipo de token, typ, e o algoritmo usado para assinar os seus cabeçalho e o conteúdo, alg. No caso da figura abaixo, o token é do tipo JWT e o algoritmo usado para assiná-lo combina o algoritmo de assinatura baseada em chave secreta HMAC com a função de hash criptográfico SHA256.

    * Conteúdo
    O conteúdo contém as informações a compartilhar transmitidas por meio do token JWT. Essas informações são associadas a atributos, chamados claims, do objeto JSON. Esses atributos podem ser reservados, públicos ou privados. No caso da figura abaixo, o identificador da entidade sobre a qual JWT trata, sub, é 1234567890, o nome dessa entidade, name, é John Doe e o instante no qual o token foi gerado, iat, é representado por 1516239022, segundos desde o epoch time.

     
    * Assinatura
    A assinatura é usada para validar a confiabilidade do token. Ela permite checar a sua integridade (se o token foi ou não foi alterado) e, no caso de tokens assinados usando chave privada, também permite verificar se o emissor é quem afirma ser. No caso da foto abaixo, a assinatura é calculada a partir da concatenação do cabeçalho e do conteúdo codificados em base64 separados por um ponto ‘.’ e de uma chave secreta.

    3. Uso do token JWT
    O token JWT é usado na troca de informação entre serviços, no processo de autenticação de usuário e no gerenciamento de sessão.
    O fato de o token ser assinado permite ao seu destinatário ter certeza de que as informações não foram corrompidas ou alteradas durante o seu trânsito e o token foi gerado pelo remetente da comunicação. Portanto, o uso de token JWT é uma boa alternativa para transmitir informações.
    Quando um usuário é autenticado com sucesso usando a sua credencial, um token ID é retornado. Segundo a especificação do OpenID Connect (OIDC), o token ID sempre é um token JWT.
    O seu uso mais comum e mais debatido é o uso no gerenciamento de sessão de usuário. A sua aplicação nesse caso de uso tenta reduzir o tempo de resposta de requisição ao eliminar uma consulta ao banco de dados de sessão.
    4. Por que o token JWT é usado no gerenciamento de sessão?

    No caso tradicional de gerenciamento de sessão, após se autenticar, o cliente recebe um token de sessão opaco, identificador de sessão sem significado. A partir desse momento, ele é enviado ao servidor a cada requisição feita a ele.

    Ao receber a requisição, o servidor usa o token de sessão para consultar as informações do usuário no banco de dados. Desse modo, o servidor será capaz de verificar se o token é válido, de validar se o usuário tem permissão para realizar a ação e, em caso positivo, realizá-la. Como essa consulta deve ser feita a cada requisição enviada pelo cliente e ela é lenta, o tempo de resposta às requisições acaba sendo alongado.

    Por outro lado, usando um token JWT estruturado, contendo as informações do usuário como conteúdo, não há mais necessidade de se realizar a consulta ao banco de dados por essas informações a cada requisição enviada pelo cliente. Essa redução do tempo de resposta dá vantagem ao uso de JWT no gerenciamento de sessão.
    5. Será que tudo são flores?
    Apesar dessa redução do consumo de recursos do banco de dados e da consequente redução do tempo de resposta, o uso de JWT no gerenciamento de sessão traz alguns riscos. O uso de tokens JWT adiciona complexidade e cria oportunidade para exploração de falhas de segurança no seu código de implementação. Alguns ataques bem sucedidos contra o JWT exploraram falhas no seu algoritmo de manipulação, a falta de validação da assinatura e o vazamento de informações sensíveis.
    Como os tokens JWT só se tornam inválidos após o seu vencimento, o token continua válido, mesmo que o usuário feche a sua sessão na aplicação ou tenha o seu acesso bloqueado. A confiança nas informações do usuário contidas no token permanece, mesmo que elas tenham sido atualizadas no respectivo banco de dados, o que pode representar um risco de segurança. Caso a permissão de acesso de um usuário seja reduzida da permissão de administração para a permissão de usuário comum, o token garantirá a primeira permissão, enquanto ele for válido, caso essa alteração tenha ocorrido durante o seu período de validade.
    6. Qual é a recomendação da OWASP sobre token de sessão
    Além dos problemas enumerados previamente, o uso de token JWT com informações do usuário vai de encontro à recomendação da OWASP. Essa organização sugere que os identificadores de sessão não tenham sentido para evitar ataques de vazamento de informação. Caso alguma informação sensível faça parte das informações contidas no conteúdo do token, o envio dele para o cliente após a autenticação permitirá o abuso da informação sensível por atores maliciosos.
    7. O "token fantasma"
    Felizmente, não é necessário escolher apenas entre usar token opaco, sem sentido, apesar do prejuízo no tempo de resposta às requisições, ou usar token JWT com informações do usuário para ter melhor tempo de resposta, mas assumir os riscos enumerados previamente.
    A abordagem do "token fantasma" combina a segurança do token opaco com a conveniência do token JWT. A ideia central é inserir um middleware (proxy reverso, por exemplo) com mecanismo de cache entre o cliente e o servidor. Nesse cenário, o cliente recebe um token opaco a ser enviado ao servidor a cada requisição e o middleware intermedeia a comunicação entre o cliente e o servidor. Ele passa a ser responsável por substituir o token opaco pelo token JWT estruturado, antes de encaminhar a requisição ao servidor destino.

    Como o cliente recebe um token opaco, os riscos de vazamento de informações sensíveis e de exploração de falhas de implementação do JWT são mitigados. Como o servidor recebe o token JWT, ele não precisará consultar os dados do usuário a cada requisição e terá melhor tempo de resposta.
    8. Conclusão
    O artigo citou dois tipos de tokens e apontou as suas vantagens e desvantagens. Ele também apresentou uma abordagem combinada que tenta se aproveitar das vantagens desses tipos.
    Apesar de a abordagem do “token fantasma” parecer bem efetiva, não existe "bala de prata". Portanto, no momento de escolher entre usar tokens opacos, tokens JWT, abordagem combinada ou abordagem diferente das elencadas no artigo, avalie os ganhos e os riscos de cada decisão. Por fim, decida pela que lhe oferecer maior ganho com riscos aceitáveis para a sua situação.
    Boa sorte na sua jornada e desejo que a trilhe sempre levando em conta os riscos associados a cada opção disponível!
    Seja atento e vá em segurança.
    Fontes:
    https://jwt.io/introduction 
    https://auth0.com/docs/security/tokens/json-web-tokens/
    https://datatracker.ietf.org/doc/html/rfc7519
    https://apisecurity.io/issue-56-common-jwt-attacks-owasp-api-security-top-10-cheatsheet/
    https://insomniasec.com/blog/auth0-jwt-validation-bypass
    https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
    https://redis.com/blog/json-web-tokens-jwt-are-dangerous-for-user-sessions/
    http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/
    https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
    https://curity.io/resources/learn/phantom-token-pattern/

×
×
  • Create New...