Comecei a escrever notas sobre os vídeos relacionados à segurança que assisto (como uma maneira de recall rápido).
Isso pode ser mais útil para iniciantes.
A ordem das notas aqui não está em ordem de dificuldade, mas na ordem cronológica reversa de como eu as escrevo (ou seja, mais recente primeiro).
Este trabalho é licenciado sob uma Licença Internacional Creative Commons Attribution-NonCommercial-Sharealike 4.0.
Escrito em 12 de agosto de 2017
Influenciado pela confiança de Gynvael CTF 2017 LiveSteres aqui e aqui; E por seu Google CTF Quals 2017 LiveStre Dream aqui
Às vezes, um desafio pode implementar uma tarefa complicada implementando uma VM. Nem sempre é necessário engenhar completamente a VM e trabalhar para resolver o desafio. Às vezes, você pode ser um pouco e, depois de saber o que está acontecendo, pode conectar -se à VM e ter acesso a coisas necessárias. Além disso, os ataques de canal lateral baseados em tempo tornam-se mais fáceis nas VMs (principalmente devido ao maior número de instruções "reais" executadas.
Funções criptograficamente interessantes em binários podem ser reconhecidas e rapidamente, apenas procurando as constantes e pesquisando -as on -line. Para funções criptográficas padrão, essas constantes são suficientes para adivinhar rapidamente uma função. As funções criptográficas mais simples podem ser reconhecidas ainda mais facilmente. Se você vir muitos Xors e coisas assim acontecendo, e nenhuma constante facilmente identificável, provavelmente é criptografia enrolada à mão (e também possivelmente quebrada).
Às vezes, ao usar o IDA com hexágios, a visão de desmontagem pode ser melhor do que a visão de descompilação. Isso é especialmente verdadeiro se você perceber que parece haver muita complicação acontecendo na visão de descompilação, mas você percebe padrões repetitivos na visão de desmontagem. (Você pode alternar rapidamente B/W os dois usando a barra de espaço). Por exemplo, se houver uma biblioteca de grande integração (tamanho fixo) implementado, a visão de decompilação é terrível, mas a visão de desmontagem é fácil de entender coisas (e facilmente reconhecíveis devido às instruções repetitivas "com transporte", como adc ). Além disso, ao analisar como esse, o uso do recurso "Grupo" na visualização do gráfico da Ida é extremamente útil para reduzir rapidamente a complexidade do seu gráfico, como você entende o que cada nó faz.
Para arquiteturas estranhas, ter um bom emulador é extremamente útil. Especialmente, um emulador que pode lhe dar um despejo da memória pode ser usado para descobrir rapidamente o que está acontecendo e reconhecer partes interessantes, depois de ter a memória fora do emulador. Além disso, o uso de um emulador implementado em um idioma confortável (como o Python) significa que você pode executar as coisas exatamente como gosta. Por exemplo, se houver uma parte interessante do código, você poderá executar várias vezes (por exemplo, para força bruta ou algo assim), usando o emulador, você pode codificar rapidamente algo que faz apenas essa parte do código, em vez de ter que executar o programa completo.
Ser preguiçoso é bom, quando Reing. Não perca tempo a engenharia reversa de tudo, mas gaste tempo suficiente fazendo Recon (mesmo em um desafio!), Para poder reduzir o tempo gasto na verdade realizando a tarefa mais difícil de REING. O que o Recon, nessa situação significa, é apenas olhar rapidamente para diferentes funções, sem gastar muito tempo na análise de cada função minuciosamente. Você apenas avalia rapidamente o que a função pode ser (por exemplo, "parece uma coisa de criptografia" ou "parece uma coisa de gerenciamento de memória" etc.)
Para hardware ou arquitetura desconhecidos, gaste tempo suficiente no Google, você pode ter sorte com um monte de ferramentas ou documentos úteis que podem ajudá -lo a criar ferramentas mais rapidamente. Muitas vezes, você encontrará implementações de emulador de brinquedos etc que podem ser úteis como um ponto rápido para começar. Como alternativa, você pode obter algumas informações interessantes (como como os bitmaps são armazenados ou como as strings são armazenadas ou algo assim) com as quais você pode escrever um script rápido "Fix" e, em seguida, usar ferramentas normais para ver se as coisas interessantes estão lá.
O GIMP (a ferramenta de manipulação de imagem) possui uma funcionalidade de abertura/carga muito legal para ver dados brutos de pixels. Você pode usar isso para procurar rapidamente ativos ou estruturas repetitivas em dados binários brutos. Gaste tempo brincando com as configurações para ver se mais informações podem ser obtidas.
Escrito em 2 de julho de 2017
Influenciado por uma discussão com @p4n74 e @h3rcul35 no bate -papo infoseciitr #bin. Estávamos discutindo sobre como às vezes os iniciantes lutam para começar com um desafio maior binário, especialmente quando é despojado.
Para resolver o desafio de ER ou para poder colocá -lo, é preciso primeiro analisar o binário fornecido, a fim de poder explorá -lo efetivamente. Como o binário pode ser despojado etc (encontrado usando file ), é preciso saber onde iniciar a análise, para obter um ponto de apoio para se acumular.
Existem alguns estilos de análise, ao procurar vulnerabilidades em binários (e pelo que reuni, diferentes equipes de CTF têm preferências diferentes):
1.1. Transpilando código completo para C
Esse tipo de análise é meio raro, mas é bastante útil para binários menores. A idéia é ir em um engenheiro reverso a totalidade do código. Toda e qualquer função é aberta no IDA (usando a visualização do decompilador) e renomear (atalho: n) e retipo (atalho: y) são usados para tornar rapidamente o código decompilado muito mais legível. Em seguida, todo o código é copiado/exportado para um arquivo .c separado, que pode ser compilado para obter um binário equivalente (mas não o mesmo) ao original. Em seguida, a análise do nível do código -fonte pode ser feita, para encontrar vulns etc. Uma vez que o ponto de vulnerabilidade for encontrado, o exploração é construído no binário original, seguindo a fonte bem descompilada em IDA, lado a lado com a desmontagem da visualização (use a guia para alternar rapidamente entre os dois;
1.2. Análise mínima de descompilação
Isso é feito com bastante frequência, pois a maior parte do binário é relativamente inútil (da perspectiva do invasor). Você só precisa analisar as funções suspeitas ou levá -lo ao vuln. Para fazer isso, existem algumas abordagens para começar:
1.2.1. Comece de Main
Agora, geralmente, para um binário despojado, mesmo o principal não é rotulado (o IDA 6.9 em diante marque para você), mas com o tempo você aprende a reconhecer como alcançar o principal do ponto de entrada (onde a IDA abre por padrão). Você pula para isso e começa a analisar a partir daí.
1.2.2. Encontre seqüências relevantes
Às vezes, você conhece algumas seqüências específicas que podem ser emitidas etc, que você sabe que pode ser útil (por exemplo, "Parabéns, sua bandeira é %s" para um desafio de re). Você pode pular para a exibição Strings (atalho: Shift+F12), encontre a string e trabalhe para trás usando o XREFS (atalho: x). Os XREFs permitem encontrar o caminho das funções nessa string, usando o XREFS em todas as funções nessa cadeia, até chegar a Main (ou algum ponto que você conhece).
1.2.3. De alguma função aleatória
Às vezes, a string não específica pode ser útil e você não deseja começar do Main. Então, em vez disso, você passa rapidamente por toda a lista de funções, procurando funções que parecem suspeitas (como ter muitas constantes, ou muitos Xors, etc.) ou chamam funções importantes (xrefs de MaiC, gratuitamente, etc.) e você começa a partir daí e vá para a frente (seguintes funções que ele chama) e para trás (xrefs das funções)
1.3. Análise de desmontagem pura
Às vezes, você não pode usar a visão de descompilação (devido à arquitetura estranha, às técnicas de anti-decompilação, ou montagem escrita à mão, ou descompilação parecendo desnecessariamente complexa). Nesse caso, é perfeitamente válido examinar puramente para a visão de desmontagem. É extremamente útil (para novas arquiteturas) ativar os comentários automáticos, o que mostra um comentário explicando cada instrução. Além disso, as funcionalidades da colorização e do grupo do nó são imensamente úteis. Mesmo se você não usar nada disso, marcando regularmente comentários na desmontagem ajuda muito. Se estou fazendo isso pessoalmente, prefiro escrever comentários semelhantes ao Python, para que eu possa rapidamente transpilar manualmente em Python (especialmente útil para desafios de ER, onde você pode ter que usar o Z3 etc).
1.4. Usando plataformas como bap, etc.
Esse tipo de análise é (semi-) automatizado e geralmente é mais útil para software muito maior e raramente é usado diretamente nos CTFs.
A fuzzing pode ser uma técnica eficaz para chegar rapidamente ao vuln, sem ter que realmente entendê -la inicialmente. Usando um Fuzzher, é possível obter muito estilo de vulns, que precisa ser analisado e triia para chegar ao Vuln real. Veja minhas anotações sobre o básico de fuzzing e fuzzing genético para obter mais informações.
A análise dinâmica pode ser usada após encontrar um vuln usando análise estática, para ajudar a criar explorações rapidamente. Como alternativa, pode ser usado para encontrar o próprio vuln. Geralmente, inicia -se o executável dentro de um depurador e tenta acompanhar os caminhos de código que acionam o bug. Ao colocar pontos de interrupção nos locais certos e analisar o estado dos registros/heap/pilha/etc, pode -se ter uma boa idéia do que está acontecendo. Pode -se também usar os depuradores para identificar rapidamente funções interessantes. Isso pode ser feito, por exemplo, definindo pontos de interrupção temporários em todas as funções inicialmente; Em seguida, prosseguir para fazer 2 caminhadas - uma em todos os caminhos de código desinteressantes; e um a apenas um único caminho interessante. A primeira caminhada viaja todas as funções desinteressantes e desativa esses pontos de interrupção, deixando assim os interessantes aparecendo como pontos de interrupção durante a segunda caminhada.
Meu estilo pessoal para análise é começar com a análise estática, geralmente a partir de aplicativos principais (ou para não console, de strings) e trabalhar para encontrar rapidamente uma função que parece estranha. Passo tempo e ramo para frente e para trás a partir daqui, escrevendo regularmente comentários e renomeando e renomeando continuamente as variáveis para melhorar a descompilação. Como outros, eu uso nomes como Apple, banana, cenoura, etc. para aparentemente útil, mas, ainda assim, ainda desconhecidos funções/variáveis/etc, para facilitar a análise (acompanhar o estilo FUNC_123456 de nomes é muito difícil para mim). Também uso regularmente a exibição de estruturas em IDA para definir estruturas (e enum) para tornar a descompilação ainda mais agradável. Depois de encontrar o vuln, geralmente passo a escrever um script com pwntools (e uso isso para chamar um gdb.attach() ). Dessa forma, posso obter muito controle sobre o que está acontecendo. Dentro do GDB, geralmente uso o GDB simples, embora tenha adicionado um comando peda que carrega PEDA instantaneamente, se necessário.
Meu estilo está definitivamente evoluindo, pois me senti mais confortável com minhas ferramentas e também com ferramentas personalizadas que escrevi para acelerar as coisas. Ficaria feliz em saber de outros estilos de análise, bem como possíveis mudanças no meu estilo que podem me ajudar a ficar mais rápido. Para quaisquer comentários/críticas/elogios que você tenha, como sempre, posso ser contatado no Twitter @Jay_f0xtr0t.
Escrito em 4 de junho de 2017
Influenciado por esta incrível transmissão ao vivo por Gynvael Coldwind, onde ele discute o básico do ROP, e dá algumas dicas e truques
A programação orientada para o retorno (ROP) é uma das técnicas clássicas de exploração, que é usada para ignorar a proteção NX (memória não executável). A Microsoft incorporou o NX como DEP (prevenção de execução de dados). Mesmo o Linux etc., tenha eficaz, o que significa que, com essa proteção, você não pode mais colocar o código de shell na pilha/pilha e executá -lo apenas pulando para ele. Então, agora, para poder executar o código, você entra no código pré-existente (binário principal ou suas bibliotecas-libc, ldd etc no linux; kernel32, ntdll etc no Windows). A ROP surge reutilizando fragmentos desse código que já existe e descobrindo uma maneira de combinar esses fragmentos para fazer o que você deseja fazer (o que é obviamente, hackear o planeta !!!).
Originalmente, o ROP começou com o RET2LIBC e depois se tornou mais avançado ao longo do tempo usando muitos outros pequenos pedaços de código. Alguns podem dizer que o ROP agora está "morto", devido a proteções adicionais para mitigá -lo, mas ainda pode ser explorado em muitos cenários (e definitivamente necessário para muitos CTFs).
A parte mais importante da ROP são os gadgets. Os gadgets são "peças de código utilizáveis para ROP". Isso geralmente significa peças de código que terminam com um ret (mas outros tipos de gadgets também podem ser úteis; como aqueles que terminam com pop eax; jmp eax etc). Correntamos esses gadgets para formar a exploração, conhecida como cadeia ROP .
Uma das suposições mais importantes do ROP é que você tem controle sobre a pilha (ou seja, o ponteiro da pilha aponta para um buffer que você controla). Se isso não for verdade, você precisará aplicar outros truques (como o pivô da pilha) para obter esse controle antes de construir uma cadeia ROP.
Como você extrai gadgets? Use ferramentas para downloads (como ropgadget) ou ferramenta on -line (como ROPSHELL) ou escreva suas próprias ferramentas (pode ser mais útil para desafios mais difíceis às vezes, pois você pode ajustá -lo ao desafio específico, se necessário). Basicamente, precisamos apenas dos endereços para os quais podemos pular para esses gadgets. É aqui que pode haver um problema com o ASLR etc (nesse caso, você obtém um vazamento do endereço, antes de passar para realmente fazer o ROP).
Então agora, como usamos esses gadgets para fazer um Ropchain? Primeiro, procuramos "gadgets básicos". São gadgets que podem realizar tarefas simples para nós (como pop ecx; ret , que podem ser usadas para carregar um valor no ECX, colocando o gadget, seguido pelo valor a ser carregado, seguido pelo restante da cadeia, que é retornado após o carregamento do valor). Os gadgets básicos mais úteis são geralmente "definidos um registro", "o valor do registro da loja no endereço apontado pelo registro", etc.
Podemos construir a partir dessas funções primitivas para obter funcionalidade de nível mais alto (semelhante ao meu post intitulado Abstração de Exploração). Por exemplo, usando os aparelhos de registro do conjunto e da armazenagem de valor de armazenamento em endereço, podemos criar uma função "POKE", que nos permite definir qualquer endereço específico com um valor específico. Usando isso, podemos construir uma função "string de puxão" que nos permite armazenar qualquer string específica em qualquer local específico na memória. Agora que temos uma corda, estamos basicamente quase concluídos, pois podemos criar qualquer estrutura que queremos na memória e também podemos chamar quaisquer funções que desejarem com os parâmetros que queremos (já que podemos definir o registro e colocar valores na pilha).
Uma das razões mais importantes para construir a partir dessas primitivas de ordem inferior para funções maiores que fazem coisas mais complexas é reduzir as chances de cometer erros (o que é comum no ROP de outra forma).
Existem idéias, técnicas e dicas mais complexas para o ROP, mas isso é possivelmente um tópico para uma nota separada, por um tempo diferente :)
PS: GYN tem um post do blog sobre exploração orientada para o retorno que pode valer a pena ler.
Escrito em 27 de maio de 2017; estendido em 29 de maio de 2017
Influenciado por esta incrível transmissão ao vivo por Gynvael Coldwind, onde ele fala sobre a teoria básica por trás da fuzzing genética, e começa a construir um fuzger genético básico. Ele então passa a concluir a implementação nesta transmissão ao vivo.
"Avançado" Fuzzing (em comparação com um fuzir cego, descrito no meu "básico de fuzzing"). Ele também modifica/muta bytes etc., mas faz um pouco mais inteligente que o fuzger "idiota" cego.
Por que precisamos de um Fuzzer genético?
Alguns programas podem ser "desagradáveis" em relação a Fuzzhers idiotas, pois é possível que uma vulnerabilidade exige que várias condições sejam satisfeitas para serem alcançadas. Em um docer idiota, temos uma probabilidade muito baixa de isso acontecer, pois não tem idéia se estiver fazendo algum progresso ou não. Como exemplo específico, se tivermos o código if a: if b: if c: if d: crash! (Vamos chamá -lo de código de crise); nesse caso, precisamos de quatro condições para ficar satisfeitas para travar o programa. No entanto, um docer idiota pode não conseguir superar a condição a , apenas porque há uma chance muito baixa de que todas as 4 mutações a , b , c , d aconteçam ao mesmo tempo. De fato, mesmo que progrida fazendo apenas a , a próxima mutação pode voltar !a apenas porque não sabe nada sobre o programa.
Espere, quando aparece esse tipo de programa de "caso ruim"?
É bastante comum nos analisadores de formato de arquivo, para dar um exemplo. Para alcançar alguns caminhos de código específicos, pode -se precisar de várias verificações "esse valor deve ser esse, e esse valor deve ser isso, e algum outro valor deve ser algo de outra coisa" e assim por diante. Além disso, quase nenhum software do mundo real é "não complicado", e a maioria dos softwares tem muitos muitos caminhos de código possíveis, alguns dos quais só podem ser acessados depois que muitas coisas no estado são configuradas corretamente. Desse modo, muitos dos caminhos de código desses programas são basicamente inacessíveis a Fuzzhers idiotas. Além disso, às vezes, alguns caminhos podem ser completamente inacessíveis (em vez de apenas improváveis loucamente) devido a mutações insuficientes feitas. Se algum desses caminhos tivesse bugs, um fuzger idiota nunca seria capaz de encontrá -los.
Então, como nos fazemos melhor do que fuzzhers idiotas?
Considere o Gráfico de Fluxo de Controle (CFG) do código Crasher acima mencionado. Se, por acaso, um fuzger idiota de repente fosse a , também não reconheceria que atingisse um novo nó, mas continuaria ignorando isso, descartando a amostra. Por outro lado, o que AFL (e outros detectores genéticos ou "inteligentes") é que eles reconhecem isso como uma nova informação ("um caminho recém -alcançado") e armazenam essa amostra como um novo ponto inicial no corpus. O que isso significa é que agora o Fuzzher pode começar a partir do bloco a e se mover mais. Obviamente, às vezes, pode voltar ao !a da amostra a , mas na maioria das vezes, não será capaz de atingir o bloco b Novamente, este é um novo nó alcançado, então adiciona uma nova amostra ao corpus. Isso continua, permitindo que mais e mais caminhos possíveis sejam verificados e, finalmente, atinge o crash! .
Por que isso funciona?
Ao adicionar amostras mutadas ao corpus, que exploram mais o gráfico (ou seja, alcance as peças não exploradas antes), podemos alcançar áreas anteriormente inacessíveis e, portanto, podemos confundir essas áreas. Como podemos confundir essas áreas, podemos descobrir bugs nessas regiões.
Por que é chamado de fuzzing genético?
Esse tipo de fuzzing "inteligente" é como algoritmos genéticos. A mutação e o cruzamento das amostras causam novas amostras. Mantemos espécimes que são mais adequados às condições que são testadas. Nesse caso, a condição é "quantos nós no gráfico alcançaram?". Os que atravessam mais podem ser mantidos. Isso não é exatamente como algos genéticos, mas é uma variação (já que mantemos todos os espécimes que atravessam o território inexplorado, e não fazemos crossover), mas é suficientemente semelhante para obter o mesmo nome. Basicamente, a escolha da população pré-existente, seguida de mutação, seguida de testes de condicionamento físico (se ela viu novas áreas) e repetir.
Espere, então apenas acompanhamos nós não alcançados?
Não, na verdade não. A AFL acompanha os travessos de borda no gráfico, em vez de nós. Além disso, não diz apenas "Edge viajou ou não", mantém o controle de quantas vezes uma vantagem foi atravessada. Se uma borda for atravessada 0, 1, 2, 4, 8, 16, ... vezes, é considerada um "novo caminho" e leva à adição ao corpus. Isso é feito porque olhar para as bordas, em vez de nós, é uma maneira melhor de distinguir entre os estados de aplicação e o uso de uma contagem exponencialmente crescente das travessias de borda fornece mais informações (uma aresta atravessada é bastante diferente de atravessar duas vezes, mas atravessou 10 não é muito diferente de 11 vezes).
Então, o que e tudo você precisa em um fuzger genético?
Precisamos de duas coisas, a primeira parte é chamada de rastreador (ou instrumentação de rastreamento). Basicamente, diz quais instruções foram executadas no aplicativo. A AFL faz isso de uma maneira simples, pulando entre os estágios de compilação. Após a geração da montagem, mas antes de montar o programa, ele procura blocos básicos (procurando terminações, verificando o tipo de instruções de salto/ramificação) e adiciona código a cada bloco que marca o bloco/borda, conforme executado (provavelmente em alguma memória de sombra ou algo assim). Se não tivermos código -fonte, podemos usar outras técnicas para rastrear (como PIN, depurador, etc.). Acontece que mesmo a ASAN pode fornecer informações de cobertura (consulte os documentos para isso).
Para a segunda parte, usamos as informações de cobertura fornecidas pelo rastreador para acompanhar novos caminhos à medida que aparecem e adicionamos as amostras geradas ao corpus para seleção aleatória no futuro.
Existem vários mecanismos para fazer o traçador. Eles podem ser baseados em software ou baseados em hardware. Para baseados em hardware, existem, por exemplo, existem alguns recursos da Intel CPU onde, em relação a um buffer na memória, ele registra informações de todos os blocos básicos atravessados nesse buffer. É um recurso do kernel, portanto, o kernel deve apoiá -lo e fornece -o como uma API (o que o Linux faz). Para baseado em software, podemos fazê -lo adicionando código ou usando um depurador (usando pontos de interrupção temporários, ou através de um passo único), ou usar as habilidades de rastreamento do Sinitizador de endereço, ou usar ganchos, ou emuladores ou várias outras maneiras.
Outra maneira de diferenciar os mecanismos é o rastreamento de caixa preta (onde você pode usar apenas o binário não modificado) ou o rastreamento de caixa branca de software (onde você tem acesso ao código-fonte e modifique o próprio código para adicionar o código de rastreamento).
A AFL usa a instrumentação de software durante a compilação como método para rastrear (ou através da emulação de Qemu). Honggfuzz suporta métodos de rastreamento baseados em software e hardware. Outros difusos inteligentes podem ser diferentes. O que Gyn Builds usa o rastreamento/cobertura fornecida pelo Sanitizer de endereço (ASAN).
Alguns fuzzhers usam "speedhacks" (ou seja, aumentam a velocidade de fuzzing), como fazer um garfraser ou outras idéias. Pode valer a pena investigar isso em algum momento :)
Escrito em 20 de abril de 2017
Influenciado por esta incrível transmissão ao vivo por Gynvael Coldwind, onde ele fala sobre o que se trata o Fuzzing, e também constrói um Fuzzher básico do zero!
O que é um Fuzzher, em primeiro lugar? E por que usamos isso?
Considere que temos uma biblioteca/programa que pega dados de entrada. A entrada pode ser estruturada de alguma forma (digamos um pdf, ou png, ou xml, etc; mas não precisa ser nenhum formato "padrão"). Do ponto de vista da segurança, é interessante se houver um limite de segurança entre a entrada e o processo / biblioteca / programa, e podemos passar por alguma "entrada especial" que causa comportamento não intencional além desse limite. Um docer é uma maneira de fazer isso. Faz isso "mutando" as coisas na entrada ( possivelmente corrompendo -as), a fim de levar a uma execução normal (incluindo erros manipulados com segurança) ou uma falha. Isso pode acontecer devido à lógica do caso de borda não ser bem tratada.
A travamento é a maneira mais fácil para as condições de erro. Pode haver outros também. Por exemplo, o uso do ASAN (Significador de endereço) etc também pode levar à detecção de mais coisas, o que pode ser problemas de segurança. Por exemplo, um único transbordamento de bytes de um buffer pode não causar uma falha por conta própria, mas usando ASAN, podemos capturar isso com um fuzfer.
Outro uso possível para um Fuzzher é que as entradas geradas por um programa também podem ser usadas em outra biblioteca/programa e ver se existem diferenças. Por exemplo, alguns erros da biblioteca matemática de alta precisão foram notados assim. Isso geralmente não leva a problemas de segurança, por isso não nos concentraremos muito nisso.
Como funciona um docer?
Um docer é basicamente um loop de repetição extra-decantual que explora o espaço de estado do aplicativo para tentar "aleatoriamente" encontrar estados de um vuln de falha / segurança. Não encontra uma exploração, apenas um vuln. A parte principal do Fuzzfer é o próprio mutador. Mais sobre isso mais tarde.
Saídas de um Fuzzher?
No Fuzzfer, um depurador está (às vezes) anexado ao aplicativo para obter algum tipo de relatório do acidente, para poder analisá -lo posteriormente como falha de segurança vuln versus benigno (mas possivelmente importante).
Como determinar quais áreas dos programas são melhores para se prejudicar primeiro?
Ao prender, queremos geralmente nos concentrar em uma única peça ou em um pequeno conjunto de pedaços do programa. Isso geralmente é feito principalmente para reduzir a quantidade de execução a ser feita. Geralmente, nos concentramos apenas na análise e processamento. Novamente, o limite de segurança importa muito para decidir quais peças são importantes para nós.
Tipos de Fuzzhers?
As amostras de entrada dadas ao Fuzzher são chamadas de corpus . Em Fuzzhers Oldschool (também conhecidos como "cegos"/"idiotas", havia uma necessidade de um grande corpus. Os mais novos (também conhecidos como fuzzhers "genéticos", por exemplo, AFL) não precisam necessariamente de um corpus tão grande, pois exploram o estado por conta própria.
Como os fuzzhers são úteis?
Fuzzhers são principalmente úteis para "frutas baixas". Ele não encontrará bugs lógicos complicados, mas pode achar fácil encontrar bugs (que às vezes são fáceis de perder durante a análise manual). Embora eu possa dizer a entrada ao longo desta nota e geralmente me referir a um arquivo de entrada , não precisa ser exatamente isso. Os fuzzhers podem lidar com entradas que podem ser Stdin ou Arquivo de entrada ou soquete de rede ou muitos outros. Sem muita perda de generalidade, podemos pensar nisso como apenas um arquivo por enquanto.
Como escrever um fuzfer (básico)?
Novamente, ele só precisa ser um loop de repetição de muta. Precisamos ser capazes de chamar o alvo com frequência ( subprocess.Popen ). Também precisamos poder passar a entrada para o programa (por exemplo: arquivos) e detectar falhas ( SIGSEGV etc, causando exceções que podem ser capturadas). Agora, precisamos apenas escrever um mutador para o arquivo de entrada e continuar ligando para o alvo nos arquivos mutados.
Mutadores? O que?!?
Pode haver vários mutadores possíveis. Fácil (ou seja, simples de implementar) pode ser mutar bits, mutar bytes ou muta a valores de "mágica". Para aumentar a chance de falha, em vez de alterar apenas 1 bit ou algo assim, podemos alterar vários (talvez alguma porcentagem parametrizada deles?). Também podemos (em vez de mutações aleatórias), alterar bytes/words/dwords/etc para alguns valores de "mágica". Os valores mágicos podem ser 0 , 0xff , 0xffff , 0xffffffff , 0x80000000 ( INT_MIN ), 0x7fffffff ( INT_MAX de 32 bits) etc. Basicamente, escolha aqueles que são comuns a causar problemas de segurança (porque eles podem desencadear alguns casos de arestas). Podemos escrever mutadores mais inteligentes se soubermos mais informações sobre o programa (por exemplo, para números inteiros à base de string, podemos escrever algo que altere uma string inteira para "65536" ou -1 etc). Os mutadores baseados em pedaços podem mover peças (basicamente, reorganizando a entrada). Os mutadores aditivos/anexos também funcionam (por exemplo, causando uma entrada maior no buffer). Os truncadores também podem funcionar (por exemplo, às vezes o EOF pode não ser bem tratado). Basicamente, tente um monte de maneiras criativas de gerenciar coisas. Quanto mais experiência em relação ao programa (e exploração em geral), mais mutadores podem ser possíveis.
Mas o que é esse fuzzing "genético"?
Isso provavelmente é uma discussão para mais tarde. No entanto, alguns links para alguns detectores modernos (de código aberto) são AFL e Honggfuzz.
Escrito em 7 de abril de 2017
Influenciado de um bom desafio em Picoctf 2017 (nome do desafio retido, já que o concurso ainda está em andamento)
Aviso: esta nota pode parecer simples/óbvia para alguns leitores, mas exige dizer, já que a camada não era clara para mim até muito recentemente.
Obviamente, ao programar, todos nós usamos abstrações, sejam eles classes e objetos, funções, meta-funções, polimorfismo, ou mônadas, ou funções ou todo esse jazz. No entanto, podemos realmente ter isso durante a exploração? Obviamente, podemos explorar erros que são cometidos na implementação das abstrações acima mencionadas, mas aqui estou falando de algo diferente.
Em vários CTFs, sempre que eu escrevi uma exploração anteriormente, tem sido um script de exploração ad-hoc que solta um shell. Eu uso os incríveis pwntools como uma estrutura (para conectar -se ao serviço e converter coisas, dynefl, etc.), mas é isso. Cada exploração tendia a ser uma maneira ad-hoc de trabalhar em direção ao objetivo da execução do código arbitrário. No entanto, esse desafio atual, além de pensar sobre minha nota anterior sobre a exploração de string de formato "avançada", me fez perceber que poderia colocar minhas façanhas de maneira consistente e passar por diferentes camadas de abstração para finalmente atingir o objetivo necessário.
Como exemplo, vamos considerar a vulnerabilidade como um erro lógico, que nos permite fazer uma leitura/gravação de 4 bytes, em algum lugar de uma pequena faixa após um buffer. Queremos abusar disso até a execução do código e, finalmente, a bandeira.
Nesse cenário, eu consideraria essa abstração um primitivo short-distance-write-anything . Com isso por conta própria, obviamente não podemos fazer muito. No entanto, faço uma pequena função de python vuln(offset, val) . No entanto, desde que logo após o buffer, pode haver alguns dados/metadados que podem ser úteis, podemos abusar isso para construir os dois primitivos read-anywhere e write-anything-anywhere . Isso significa que escrevo funções curtas do Python que chamam a função vuln() definida anteriormente. Essas funções get_mem(addr) e set_mem(addr, val) são feitas simplesmente (neste exemplo atual) simplesmente usando a função vuln() para substituir um ponteiro, que pode ser desferenciado em outras partes do binário.
Agora, depois de termos abstrações get_mem() e set_mem() , construo uma abstração anti-aslr, basicamente vazando 2 endereços do Get através get_mem() e comparando com um banco de dados da LIBC (obrigado @niklasb por fazer o banco de dados). As compensações destes me dão um libc_base de maneira confiável, o que me permite substituir qualquer função na obtenção de outra da libc.
Isso me deu essencialmente controle sobre o EIP (no momento em que posso "acionar" uma dessas funções exatamente quando eu quiser). Agora, tudo o que resta é chamar o gatilho com os parâmetros corretos. Por isso, configurei os parâmetros como uma abstração separada e, em seguida, ligo para trigger() e tenho acesso ao shell no sistema.
Tl; dr: pode -se construir pequenas primitivas de exploração (que não têm muita energia) e, combinando -as e construindo uma hierarquia de primitivas mais fortes, podemos obter uma execução completa.
Escrito em 6 de abril de 2017
Influenciado por esta incrível transmissão ao vivo por Gynvael Coldwind, onde ele fala sobre a exploração de string de formato
Explorações de string de formato simples:
Você pode usar o %p para ver o que está na pilha. Se a sequência do formato estiver na pilha, pode -se colocar um endereço (digamos Foo ) na pilha e, em seguida, procurá -la usando o especificador de posição n$ (por exemplo, AAAA %7$p pode retornar AAAA 0x41414141 , se 7 for a posição na pilha). We can then use this to build a read-where primitive, using the %s format specifier instead (for example, AAAA %7$s would return the value at the address 0x41414141, continuing the previous example). We can also use the %n format specifier to make it into a write-what-where primitive. Usually instead, we use %hhn (a glibc extension, iirc), which lets us write one byte at a time.
We use the above primitives to initially beat ASLR (if any) and then overwrite an entry in the GOT (say exit() or fflush() or ...) to then raise it to an arbitrary-eip-control primitive, which basically gives us arbitrary-code-execution .
Possible difficulties (that make it "advanced" exploitation):
If we have partial ASLR , then we can still use format strings and beat it, but this becomes much harder if we only have one-shot exploit (ie, our exploit needs to run instantaneously, and the addresses are randomized on each run, say). The way we would beat this is to use addresses that are already in the memory, and overwrite them partially (since ASLR affects only higher order bits). This way, we can gain reliability during execution.
If we have a read only .GOT section, then the "standard" attack of overwriting the GOT will not work. In this case, we look for alternative areas that can be overwritten (preferably function pointers). Some such areas are: __malloc_hook (see man page for the same), stdin 's vtable pointer to write or flush , etc. In such a scenario, having access to the libc sources is extremely useful. As for overwriting the __malloc_hook , it works even if the application doesn't call malloc , since it is calling printf (or similar), and internally, if we pass a width specifier greater than 64k (say %70000c ), then it will call malloc, and thus whatever address was specified at the global variable __malloc_hook .
If we have our format string buffer not on the stack , then we can still gain a write-what-where primitive, though it is a little more complex. First off, we need to stop using the position specifiers n$ , since if this is used, then printf internally copies the stack (which we will be modifying as we go along). Now, we find two pointers that point ahead into the stack itself, and use those to overwrite the lower order bytes of two further ahead pointing pointers on the stack, so that they now point to x+0 and x+2 where x is some location further ahead on the stack. Using these two overwrites, we are able to completely control the 4 bytes at x , and this becomes our where in the primitive. Now we just have to ignore more positions on the format string until we come to this point, and we have a write-what-where primitive.
Written on 1st April 2017
Influenced by this amazing live stream by Gynvael Coldwind, where he explains about race conditions
If a memory region (or file or any other resource) is accessed twice with the assumption that it would remain same, but due to switching of threads, we are able to change the value, we have a race condition.
Most common kind is a TOCTTOU (Time-of-check to Time-of-use), where a variable (or file or any other resource) is first checked for some value, and if a certain condition for it passes, then it is used. In this case, we can attack it by continuously "spamming" this check in one thread, and in another thread, continuously "flipping" it so that due to randomness, we might be able to get a flip in the middle of the "window-of-opportunity" which is the (short) timeframe between the check and the use.
Usually the window-of-opportunity might be very small. We can use multiple tricks in order to increase this window of opportunity by a factor of 3x or even up to ~100x. We do this by controlling how the value is being cached, or paged. If a value (let's say a long int ) is not aligned to a cache line, then 2 cache lines might need to be accessed and this causes a delay for the same instruction to execute. Alternatively, breaking alignment on a page, (ie, placing it across a page boundary) can cause a much larger time to access. This might give us higher chance of the race condition being triggered.
Smarter ways exist to improve this race condition situation (such as clearing TLB etc, but these might not even be necessary sometimes).
Race conditions can be used, in (possibly) their extreme case, to get ring0 code execution (which is "higher than root", since it is kernel mode execution).
It is possible to find race conditions "automatically" by building tools/plugins on top of architecture emulators. For further details, http://vexillium.org/pub/005.html
Written on 31st Mar 2017
Influenced by this amazing live stream by Gynvael Coldwind, where he is experimenting on the heap
Use-after-free:
Let us say we have a bunch of pointers to a place in heap, and it is freed without making sure that all of those pointers are updated. This would leave a few dangling pointers into free'd space. This is exploitable by usually making another allocation of different type into the same region, such that you control different areas, and then you can abuse this to gain (possibly) arbitrary code execution.
Double-free:
Free up a memory region, and the free it again. If you can do this, you can take control by controlling the internal structures used by malloc. This can get complicated, compared to use-after-free, so preferably use that one if possible.
Classic buffer overflow on the heap (heap-overflow):
If you can write beyond the allocated memory, then you can start to write into the malloc's internal structures of the next malloc'd block, and by controlling what internal values get overwritten, you can usually gain a read-what-where primitive, that can usually be abused to gain higher levels of access (usually arbitrary code execution, via the GOT PLT , or __fini_array__ or similar).