Simplificando, fiquei entediado um dia e pedi ao meu amigo, Maximus Hackerman, para me dar algo para fazer. Prontamente, ele enviou um simples executável chamado "reverseMe.exe" (link Virustustotal) com um comentário simples de "Imprima a mensagem oculta em seu próprio aplicativo". Naturalmente, os arquivos de cabeçalho do CPP não foram fornecidos. Ao executar o executável, ele simplesmente abre como um aplicativo baseado em console, imprime "Enviou todos os pacotes mágicos, saindo em breve, Beep Boop!" e então quase imediatamente sai; Eu acho que "em breve" é subjetivo.
Felizmente, não parece haver anti-fuga, então acho que Hackerman estava se sentindo bem no dia.
Depois de anexar o Windbg e um desmontador, podemos ver que o aplicativo joga, o que inicialmente parece ser, uma divide by zero .
No entanto, em uma inspeção mais detalhada, descobrimos que essa exceção é lançada imediatamente antes da impressão de sua única mensagem visível. É importante observar que dois veículos (manipuladores de exceção vetorados) são registrados um pouco antes deste ponto, por isso é provável que essa exceção seja intencional e usada para gerenciar o fluxo de controle. Truque barato realmente tentar nos jogar realmente!
Esquecendo os vehs no momento, é seguro assumir que, se o aplicativo estiver "enviando" pacotes, ele está usando a função Winsock Enviar para fazê -lo (embora o aplicativo diga claramente pacotes "mágicos", então veremos).
Como esperado, acontece que você não pode enviar pacotes por mágica, então a função send do Winsock é realmente importada.
Ao colocar um ponto de interrupção na função Enviar, podemos dar uma olhada e ver se existem dados relevantes que podemos abstrair no momento do envio. Infelizmente, quando o buffer é carregado na função, os dados já estão criptografados. No entanto, podemos determinar que o buffer tem sempre 3 bytes de comprimento, o soquete está sempre ligado a um valor codificado de 0x69 ( NICE ) e que o ponto de interrupção da função de envio é atingido 14 vezes no total.
Existem algumas maneiras de se locomover por aí criptografia. Um é revertê -lo completamente, o que pode ser muito esforço, outro é localizar os dados desejados antes da criptografia e abstraí -los antes de serem criptografados. O último é significativamente mais fácil que o primeiro, então vamos com isso; Sinta -se à vontade para reverter a criptografia, porém, tive uma breve olhada e não é horrível. Como alternativa, se você estiver se sentindo chique, sempre poderá substituir o valor do identificador de soquete e ter um acordo de inscrição receptor com ele.
Infelizmente, não há seqüências úteis no segmento de dados somente leitura, além da mensagem inicial, portanto, não há indicadores úteis desse aspecto!
Voltando aos vehs, podemos ver que os dois manipuladores registrados são usados para copiar alguns objetos desconhecidos em um buffer de memória alocado. Isso parece ser feito usando o memcpy, usando uma função como o segundo parâmetro, que por sua vez usa um número inteiro codificado como seu segundo parâmetro. Vale a pena notar que esses valores codificados não excedem 14 a qualquer momento. Eu incluí apenas um dos vehs na captura de tela, pois eles são basicamente idênticos ao código, exceto pelos valores codificados.
Ao se separar de uma das funções memcpy e inspecionar a subcunção no segundo parâmetro, podemos ver que o número inteiro codificado (13 / 0dh no exemplo abaixo) é definido como o primeiro byte do parâmetro A1 e A1+1 contém um caractere, logo após o ponteiro ser dereçado.
Se verificarmos algumas das outras 14 chamadas para essa função, podemos encontrar o mesmo comportamento repetindo; Organizando os caracteres de 1 a 14, usando o número correspondente como um indicador de ordem, podemos ver que eles começam a soletrar algumas palavras legíveis. Agora, poderíamos ser super preguiçosos e apenas fazer um aplicativo de console para imprimir a mensagem, uma vez que soubermos o que é, mas isso realmente parece trapacear. Além disso, a mensagem pode mudar em algum momento. Então, vamos escrever uma caverna de código para interceptar os valores antes de serem criptografados.
Desculpe, mas estamos usando C ++ para isso! Se você preferir usar o C#, sinta -se à vontade para passar por todo o processo de invasão de plataforma por 9,7 milhões de funções e volte aqui quando terminar. De qualquer forma, primeiro precisamos decidir onde codificar a caverna. Felizmente, já sabemos! Ao analisar a função acima, por mais brevemente, sabemos que, pelo ponto RDX destacado, contém o índice e RDX+1 contém o caractere correspondente. Abaixo está o código de montagem para a função discutida.
Agora, logicamente falando, o melhor lugar para dar o salto seria no mov [rsp+arg_8], rdx , pois não nos importamos com o terceiro parâmetro, mas queremos interceptar o registro RDX e RDX+1 . Para fazer isso, precisaremos de alguns bytes: 10 bytes para a instrução MOV , para mover o endereço de nossa caverna para um registro (usaremos RAX , mais sobre isso mais tarde durante as 10 horas) e 2 bytes para a instrução JMP , para pular para o registro. Para aqueles de vocês que têm TEPT de mais matemática, esse total em 12 bytes. Agora, antes de irmos a toda a tia Bessie e Spaghettify esse código com a WriteProcessMemory, precisamos considerar que não há local ideal para substituir 12 bytes na montagem acima. Se quisermos pular do deslocamento 0x2905 ( mov [rsp+arg_8], rdx ), e precisamos de 12 bytes para fazê -lo, isso nos leva a compensar 0x2917 , que está batendo entre duas instruções de MOV . Infelizmente, se fôssemos simplesmente escrever nossos bytes para lá, isso trigentaria completamente a montagem e provavelmente causaria alguns efeitos colaterais "interessantes". Como resultado, será mais fácil (talvez um pouco mais hacky, desculpe, não desculpe) adicionar algumas instruções de um byte para preencher e completar o final de uma instrução. Bem -vindo a bordo, 0x90.
De qualquer forma, agora que sabemos qual é o nosso plano, então vamos escrever algum código: sugestão a música intensa hacker man !
Abaixo está como será o salto inicial para a nossa caverna de código assim que os bytes forem gravados na montagem do aplicativo, completos com seu próprio slide nop.
No entanto, antes de realmente escrever e substituir qualquer assembléia, precisamos iniciar o aplicativo em um estado suspenso; Isso interromperá o tempo de execução do aplicativo em um estágio inicial, para que as alterações de memória possam ser feitas antes que o aplicativo chegue à instrução em que estamos interessados. O extrato de código abaixo mostra esse processo, e eu não vou passar por cima, pois você pode visualizá -lo nos arquivos de origem deste repositório, e isso fala principalmente por si.
Agora que o processo é gerado, precisaremos adquirir o endereço base do processo. Normalmente, você pode usar o enumprocessmodules para isso, mas, como suspeitamos imediatamente o encadeamento do processo principal, o PEB não contém uma estrutura peb_ldr_data totalmente povoada, especificamente o InMemoryOrderModuleList , portanto, não podemos obter o endereço base. A propósito, isso não parece estar documentado em nenhum lugar do MSDN. Felizmente, isso é relativamente fácil de contornar; Ao retomar muito rapidamente o processo, consultando os módulos e, em seguida, ressuspensando o processo, podemos obter as informações de que precisamos sem que o processo progrida demais. Como na maioria das coisas no Windows, a Microsoft gosta de reiterar que o sistema operacional é o superior, sem documentar novamente as funções de que precisamos: NtSuspendProcess e NtResumeProcess . Convenientemente, meu pai é amigo de Bill Gates, e ele me diz que essas funções vivem acopladas no ntdll.dll , para que possamos buscá -las usando a classe abaixo que fiz anteriormente:
Agora que temos as duas funções de que precisamos, podemos retomar o processo, consultar os módulos e ressuspensar o processo:
Você pode estar se perguntando, por que o loop está esperando duas descobertas de módulos em oposição a uma? Bem, a Microsoft está procurando marcar um hattrick com uma almôndega na parte de trás da rede de espaguete sem documentos, também não mencionando que o primeiro módulo encontrado pelo EnumProcessModules será ntdll.dll e o segundo será o executável. Embora isso pareça razoável, uma vez que o executável for encontrado, ele trocará índices com ntdll.dll. Aqui está um exemplo:
O resultado depois de consultar apenas o primeiro módulo:
O resultado depois de consultar dois módulos:
Antes de prosseguirmos, precisamos escrever o código da Assembléia que faz o salto inicial para a caverna do código e a própria caverna do código. Essencialmente, substituiremos alguma memória que começamos no deslocamento 0x2905 , dê um salto, fará nosso código escavando espionagem e depois voltará para 0x2911 para continuar o fluxo normal do programa. Inicialmente, declaramos a mudança do endereço para o RAX Register como MOV RAX, 0x0 , pois o endereço que pulamos é dinâmico e ainda não sabemos o que é. RAX é um registro seguro para usar, pois é um registro volátil e é substituído logo depois de qualquer maneira. Curiosidade, é assim que a Nintendo programou o jogo de plataformas Mario Bros original, com muitos saltos (diga -me que sou engraçado)! Abaixo está como o código fica no compilador; Ele pode ser criado de outras maneiras, mas escolhi dissimular as instruções de montagem necessárias no bytecode. Se você deseja fazer isso em casa, use este site.
O código para a caverna de código real é um pouco mais complexo, e a lógica também é comentada neste arquivo, mas aqui está o processo aproximado:
R10RDX , que contém o índice de pacotes, para R11BRDX , que contém o personagem, para R11B+12 ) para R10 (que é 0x2911 )R10 também precisamos reescrever o código de montagem que substituímos (com o slide NOP) em nossa caverna de código para preservar a pilha etc.; Este código é referenciado na região de " Predetermined Assembly ", excluindo o NOPS. Abaixo está a caverna de código em sua glória feia:Parte difícil, principalmente.
Nesse ponto, temos essencialmente tudo o que precisamos para sípitar a mensagem oculta da memória, apenas precisamos implementá -la. Para recapitular, temos uma alça para um processo suspenso que geramos, 2 matrizes de bytes que representam a lógica de montagem, o endereço base do processo suspenso, armazenado em modules[0] e o deslocamento de onde precisamos escrever a lógica do salto. O snippet de código abaixo cria o endereço para pular, o endereço da caverna do código (para saltar), o endereço de nosso armazenamento de memória de 3 bytes, grava os endereços no código de montagem e, em seguida, grava o código da Assembléia no processo suspenso, antes de retomá-lo:
O elenco mágico de estilo Harry Potter para o codeCaveStorageAddr é converter o endereço em bytes, e os valores codificados nos loops são para os posicionamentos de endereço dinâmico nas matrizes de montagem. Obviamente, isso pode ser feito de uma maneira muito mais limpa (não escreva crianças mágicas), eu sou preguiçoso depois de ter que escrever manualmente todos esses bytes. Os últimos bits para escrever são os loops para ler, usando o ReadProcessMemory, a memória, armazenam os caracteres e indexos em uma matriz de bytes ordenados, sinalizam o byte, usando WriteProcessMemory , quando a leitura da memória atual é concluída e imprima a mensagem oculta. Sabemos que a função de envio ocorre apenas 14 vezes, por isso saímos o loop enquanto a matriz de bytes for preenchida; Isso pode ser alterado com mais edições de memória para sinalizar para o nosso aplicativo que o processo está "saindo" e nosso loop pode ser interrompido, em vez de usar um valor codificado de 14, mas isso funciona para este exemplo.
O resultado? Nossa mensagem oculta está impressa em nosso próprio aplicativo de console! Se alguém estiver curioso, o NPT é uma referência a outro pedaço de software Maximums Hacker-Whathame-i-chamado-Him escreveu.