Como todos os programadores, você ama código. Você e ele são melhores amigos. Mas, mais cedo ou mais tarde na vida, chegará um momento em que não haverá código com você. Sim, é difícil de acreditar, mas haverá uma enorme lacuna entre vocês: você está do lado de fora e ele está bem no fundo. Do desespero, você, como todos, terá que ir para o outro lado. Ao lado da engenharia reversa.
Usando o exemplo da tarefa nº 2 da fase online do
NeoQUEST-2019, analisaremos o princípio geral do driver reverso do Windows. Obviamente, o exemplo é bastante simplificado, mas a essência do processo não muda com isso - a única questão é a quantidade de código que precisa ser visualizada. Armado com experiência e sorte, vamos começar!
Dado
Segundo a lenda, recebemos dois arquivos: um despejo de tráfego e um arquivo binário que gerou o mesmo tráfego. Primeiro, dê uma olhada no despejo usando o Wireshark:
O dump contém um fluxo de pacotes UDP, cada um dos quais contém 6 bytes de dados. Esses dados, à primeira vista, são algum tipo de conjunto aleatório de bytes - não é possível obter nada do tráfego. Portanto, voltamos nossa atenção para o binário, que deve lhe dizer como descriptografar tudo.
Abra-o na IDA:
Parece que estamos enfrentando algum tipo de motorista. As funções com o prefixo WSK referem-se ao Winsock Kernel, a interface de programação de rede no modo kernel do Windows. No MSDN, você pode
ver uma descrição das estruturas e funções usadas no WSK.
Por conveniência, você pode carregar a biblioteca do Windows Driver Kit 8 (modo kernel) - wdk8_km (ou qualquer mais recente) no IDA para usar os tipos definidos aqui:
Cuidado, inverta!
Como sempre, comece pelo ponto de entrada:
Vamos em ordem. Primeiro, o Wsk é inicializado, um soquete é criado e vinculado - não descreveremos essas funções em detalhes, elas não carregam nenhuma informação útil para nós.
A função sub_140001608 define 4 variáveis globais. Vamos chamá-lo de InitVars. Em um deles, um valor é gravado no endereço 0xFFFFF78000000320. Pesquisando um pouco esse endereço, podemos assumir que ele registra o número de ticks do timer do sistema a partir do momento em que o sistema é inicializado. Por enquanto, vamos nomear a variável TickCount.
O EntryPoint configura funções para processar pacotes IRP (pacote de solicitação de E / S). Você pode
ler mais sobre eles no MSDN. Para todos os tipos de solicitações, é definida uma função que simplesmente passa o pacote para o próximo driver na pilha.
Mas para o tipo IRP_MJ_READ (3) uma função separada é definida; vamos chamá-lo de IrpRead.
Por sua vez, está instalado o CompletionRoutine.
A CompletionRoutine preenche a estrutura desconhecida com os dados recebidos do IRP e os coloca na lista. Até o momento, não sabemos o que está dentro do pacote - retornaremos a essa função mais tarde.
Analisamos mais detalhadamente o EntryPoint. Após definir os manipuladores de IRP, a função sub_1400012F8 é chamada. Vamos olhar para dentro e perceber imediatamente que um dispositivo (IoCreateDevice) é criado nele.
Chame a função AddDevice. Se os tipos estiverem corretos, veremos que o nome do dispositivo é "\\ Device \\ KeyboardClass0". Portanto, nosso driver interage com o teclado. Pesquisando sobre o IRP_MJ_READ no contexto do teclado, você pode
descobrir que a estrutura KEYBOARD_INPUT_DATA é transmitida em pacotes. Vamos voltar à CompletionRoutine e ver que tipo de dados eles passam.
O IDA aqui não analisa bem a estrutura, mas você pode entender pelas compensações e chamadas adicionais que consiste em ListEntry, KeyData (o código de verificação da chave é armazenado aqui) e KeyFlags.
Após AddDevice, a função sub_140001274 é chamada no EntryPoint. Ela cria um novo fluxo.
Vamos ver o que acontece no ThreadFunc.
Ela obtém o valor da lista e os processa. Preste imediatamente atenção à função sub_140001A18.
Ele passa os dados processados para a entrada da função sub_140001A68, juntamente com um ponteiro para WskSocket e o número 0x89E0FEA928230002. Depois de analisar o número do parâmetro por bytes (0x89 = 137, 0xE0 = 224, 0xFE = 243, 0xA9 = 169, 0x2328 = 9000), obtemos exatamente o mesmo endereço e porta do despejo de tráfego: 169.243.224.137:9000. É lógico supor que esta função envie um pacote de rede para o endereço e porta especificados - não o consideraremos em detalhes.
Vamos ver como os dados são processados antes do envio.
Para os dois primeiros elementos, um equivalente é executado com o valor gerado. Como o número de ticks é usado para calcular, pode-se supor que estamos diante da geração de um número pseudo-aleatório.
Depois de gerar o número, ele substitui o valor da variável que chamamos anteriormente de TickCount. Variáveis para a fórmula são definidas em InitVars. Se retornarmos à chamada para essa função, descobriremos os valores para essas variáveis e, como resultado, obteremos a seguinte fórmula:
(54773 + 7141 * prev_value)% 259200Este é um
gerador linear de
números pseudoaleatórios congruentes. É inicializado no InitVars usando TickCount. Para cada número subseqüente, o anterior atua como o valor inicial (o gerador retorna um valor de dois bytes e o mesmo é usado para a geração subseqüente).
Após o equivalente a um número aleatório de dois valores transmitidos pelo teclado, é chamada uma função que forma os dois bytes restantes da mensagem. Simplesmente produz
xor de dois parâmetros já criptografados e algum valor constante. É improvável que isso decodifique os dados, de modo que os dois últimos bytes da mensagem para nós não carregam nenhuma informação útil e não podem ser considerados. Mas o que fazer com dados criptografados?
Vamos dar uma olhada no que exatamente está criptografado. KeyData é um código de verificação que pode assumir uma gama bastante ampla de valores; acho que não é fácil. Mas
KeyFlags é um campo de bits:
Se você olhar para a
tabela de códigos
de varredura, notará que na maioria das vezes o sinalizador será 0 (a tecla está pressionada) ou 1 (a tecla está levantada). KEY_E0 será exposto muito raramente, mas pode aparecer, mas para atender a KEY_E1 as chances são muito pequenas. Portanto, você pode tentar fazer o seguinte: examinamos os dados do dump, selecionamos um valor criptografado KeyFlags, fazemos um equivalente com 0, geramos dois PSCs sucessivos. Em primeiro lugar, KeyData é um byte único e podemos verificar a correção do MSS gerado por byte alto. E segundo, os próximos KeyFlags criptografados, ao executar um equivalente com o PSC correto, terão os mesmos valores de bits. Se isso estiver errado, assumimos que o KeyFlags que analisamos originalmente era 1 etc.
Vamos tentar implementar nosso algoritmo. Vamos usar python para isso:
Implementação de algoritmo Execute nosso script nos dados recebidos do dump:
E no tráfego descriptografado, encontramos nossa linha mais desejável!
NQ2019DABE17518674F97DBA393415E9727982FC52C202549E6C1740BC0933C694B3DEEm breve haverá artigos com análise das demais tarefas, não perca!
PS E lembramos que todos que concluíram pelo menos uma tarefa no NeoQUEST-2019 têm direito a um prêmio! Verifique se há uma carta no seu e-mail e, se não chegou, escreva para
support@neoquest.ru !