Como encontrar vulnerabilidades
Você já se perguntou como profissionais de segurança encontram vulnerabilidades em sistemas? Hoje eu vou matar sua curiosidade, pois vou lhe ensinar como isso é feito.
Esse processo é chamado de análise de vulnerabilidades e, neste post, vou focar em explicar o processo e o raciocínio por trás, não o conhecimento técnico necessário. Estou supondo que o leitor já tenha conhecimento técnico sobre desenvolvimento de software, no mínimo.
Para melhor clareza do texto, vou explicar em ordem: como vulnerabilidades ocorrem, como encontrar vulnerabilidades com acesso ao código fonte do projeto e como encontrar vulnerabilidades sem acesso ao código fonte do projeto (ex.: em um teste de segurança externo).
E para fins de esclarecimento: este post é focado em explicar como encontrar falhas de implementação e não falhas de design (arquitetura de software). Para saber como encontrar falhas de design, estude sobre threat modeling.
Como falhas de implementação ocorrem
Softwares, de um ponto de vista abstrato, funcionam como uma grande função de processamento de dados. Você coloca entradas para o software, ele processa e produz alguma saída.
Mas enxergar um software assim é parecido com diminuir o zoom de uma foto até ela se tornar um pixel só na tela. Quanto mais zoom você dá na imagem, mais pixels você enxerga até conseguir enxergar a imagem completa.
Softwares não devem ser enxergados como uma só entrada e uma só saída, quando o seu objetivo é encontrar vulnerabilidades. Você deve "dar zoom" e enxergá-lo como uma enorme cadeia de entradas e saídas de vários componentes do software se comunicando. Esses componentes podem ser objetos e seus métodos, funções etc., dependendo do paradigma de programação utilizado. Vou utilizar o termo "componente" como uma forma genérica de se referir a isso.
É importante pensar dessa forma porque é exatamente nessa comunicação entre componentes onde a vulnerabilidade ocorre. Todo componente assume as suas entradas como "confiável" (trusted) ou "não confiável" (untrusted). A vulnerabilidade acontece quando um dado untrusted é usado como entrada para um componente que assume o dado como trusted. Para ajudar a ilustrar isso, veja o pseudo-código abaixo:
function main(a: untrusted) {
return doSomething(a)
}
function doSomething(x: trusted) {
...
}
Na ilustração acima, a função doSomething() assume que seu input x é trusted, mas a função main() passa um input untrusted para a função. Bingo! Você acabou de achar uma vulnerabilidade.
Na vida real, as linguagens de programação não terão as palavras-chave trusted e untrusted (pelo menos, não a maioria), mas essas assunções sobre a confiança do dado é feita o tempo todo durante a programação dos componentes do software, mesmo que isso não seja escrito explicitamente no código.
Alguns exemplos da vida real: funções como system() ou eval() assumem que sua entrada é trusted. Funções que executam SQL assumem que o código SQL é trusted, mas bind parameters são assumidos como untrusted.
Toda entrada do usuário para o software é untrusted e, cabe ao software, fazer validação e sanitização para transformar o dado unstruted em dado trusted. Para ilustrar de novo:
function main(a: untrusted) {
userInput: trusted = validateThisShit(a)
...
}
function validateThisShit(x: untrusted): trusted {
...
}
A função main() acima assume que o retorno da função validateThisShit() é trusted, portanto, se a função falhar em validar corretamente o dado, uma vulnerabilidade também ocorre porque ela irá retornar um dado unstruted que foi assumido como trusted.
Se um componente as vezes retorna dado trusted e as vezes untrusted, como é o caso de uma função de validação falha, então podemos assumir que seu retorno é sempre untrusted.
Pensando de forma abstrata, basicamente falhas de implementação ocorrem assim: Quando um dado não confiável (untrusted) é assumido como confiável (trusted).
Impacto
Eu simplifiquei um pouco acima para ajudar a focar no raciocínio, mas cometer um erro de assunção de confiança não causa uma vulnerabilidade por si só. As vezes a única coisa que causa é um comportamento estranho que não compromete a segurança do software ou simplesmente não tem nenhum resultado específico (vulgo "tanto faz").
Então, para ser mais honesto: Um erro de assunção de confiança causa um comportamento inesperado, não uma vulnerabilidade.
Por isso, é importante avaliar o impacto que o comportamento inesperado causa no software. Pois um comportamento inesperado só pode ser considerado uma vulnerabilidade quando esse comportamento viola um dos princípios da CIA triad: confidencialidade, integridade ou disponibilidade.
Análise de vulnerabilidades
Existem dois tipos de análise de vulnerabilidades que você pode fazer em um software:
- Static Application Security Testing (SAST): é o teste de segurança analisando o código do software. Seja manualmente (auditando o código) ou usando ferramentas automatizadas como SonarQube.
- Dynamic Application Security Testing (DAST): é o teste de segurança interagindo com o software, usando ferramentas como depuradores (como o gdb) ou web proxies (como OWASP ZAP ou BurpSuite). Pode ser feito manualmente ou automaticamente usando ferramentas como fuzzers, nuclei, OpenVAS etc.
O raciocínio para encontrar vulnerabilidades em um software que você tem acesso ao código fonte segue basicamente o raciocínio explicado no tópico anterior: você analisa assunções de confiança que os componentes do software fazem (como as funções) e acompanha o fluxo de todo input controlado pelo usuário.
Encontrar vulnerabilidades em softwares compilados que você não tem acesso ao código fonte segue o mesmo raciocínio, com a diferença que você terá que fazer engenharia reversa no software. De resto, é igual.
Mas para testar a segurança de softwares que você não tem acesso ao código fonte e não pode fazer engenharia reversa (como em um pentest black box de um sistema web rodando em um servidor), as coisas são um pouco mais complicadas. Você vai ter que ter um raciocínio analítico mais apurado e isso não pode ser ensinado, apenas praticado.
O raciocínio é um pouco diferente porque você não pode analisar onde o código comete o erro de assumir um dado untrusted como trusted, mas você pode deduzir. O raciocínio dedutivo é muito importante para encontrar falhas de segurança em cenários black box (que você não tem informações sobre como o sistema funciona). Sim, você vai ter que bancar o Sherlock Holmes. :D
Então o raciocínio muda para: primeiro você cria hipóteses de como o sistema lida com as entradas do usuário, como esse dado é usado pelo sistema e onde potencialmente ele comete um erro de assunção de confiança.
Após formular hipóteses, você testa uma por uma afim de validar se alguma hipótese está correta. Falhou todas as hipóteses? Formula mais hipóteses e faz mais testes. Em ciclo... Até cansar. :D
O raciocínio para encontrar uma vulnerabilidade é igual o descrito para quando você tem acesso ao código fonte do software, só que com a complexidade extra de que você terá que formular hipóteses sobre como o sistema funciona pois você não tem essa informação.
Muitas falhas de segurança acontecem justamente porque o desenvolvedor nem sequer cogita a possibilidade do atacante ser capaz de deduzir como o sistema funciona. Mas, sim, isso é possível e ocorre com frequência. Tanto profissionais de segurança ofensiva (como pentesters ou red teamers) como também cibercriminosos têm essa habilidade dedutiva.
Provas de conceito
Uma parte muito importante da análise de vulnerabilidades é provar que a vulnerabilidade existe. Essa prova da existência da vulnerabilidade é chamada de Proof of Concept (PoC), ou em bom Português: Prova de conceito.
Uma PoC é um código ou instrução passo-a-passo que prova a existência da vulnerabilidade. Todo profissional de segurança responsável sempre cria PoCs antes de reportar uma vulnerabilidade, pois como dizem: nem tudo que reluz, é ouro.
Só porque algo parece ser uma vulnerabilidade, não quer dizer que seja. Ter uma PoC é a diferença entre você "achar" que uma vulnerabilidade existe e ela realmente existir.
Provas não são opcionais se você quiser levar segurança à sério. Sempre crie PoCs para as vulnerabilidades que você acredita que encontrou.