Imersão no driver: o princípio geral de reversão usando o exemplo da tarefa NeoQUEST-2019


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)% 259200

Este é 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
#  -   keymap = […] # ,   Wireshark traffic_dump = […] #  def bxnor(a, b): return ((~a & 0xffff) | b) & (a | (~b & 0xffff)) #   def brgen(a): return ((7141 * a + 54773) % 259200) & 0xffff def decode(): #     for i in range(0, len(traffic_dump) - 1): #   KeyFlags probe = traffic_dump[i][1] #   - scancode = traffic_dump[i+1][0] #    KeyFlags tester = traffic_dump[i+1][1] fail = True #     (  KEY_E1) for flag in range(4): rnd_flag = bxnor(flag, probe) rnd_sc = brgen(rnd_flag) next_flag = bxnor(tester, brgen(rnd_sc)) #   KeyFlags if next_flag in range(4): sc = bxnor(rnd_sc, scancode) if sc < len(keymap): sym = keymap[sc] if next_flag % 2 == 0: print(sym, end='') fail = False break #   -      KeyFlags   if fail: print('Something went wrong on {} pair'.format(i)) return print() if __name__ == "__main__": decode() 


Execute nosso script nos dados recebidos do dump:


E no tráfego descriptografado, encontramos nossa linha mais desejável!

NQ2019DABE17518674F97DBA393415E9727982FC52C202549E6C1740BC0933C694B3DE


Em 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 !

Source: https://habr.com/ru/post/pt446462/


All Articles