Vencendo campo minado hackeando 2
Este é um artigo complementar ao artigo anterior, onde mostrei como fiz engenharia reversa para o jogo Campo Minado Link
Neste artigo, escreveremos um código que resolverá o jogo para nós, o objetivo do artigo é ensinar como um depurador funciona nos bastidores, explicar o que é um loop de depurador e como ele se parece break point.
O código do artigo pode ser baixado do meu GitHub no seguinte link:
No próximo vídeo, vou mostrar a vocês o uso de um debbuger loop e a vitória no jogo campo minado usando código:
Como funciona debugger?
Em princípio, o depurador funciona em duas etapas:
- Devemos primeiro informar o sistema operacional para nos fornecer informações sobre o processo que queremos testar usando um dos seguintes sinalizadores DEBUG_ONLY_THIS_PROCESS ou DEBUG_PROCESS.
- Crie um loop de depurador que irá lidar com eventos que podem acontecer durante a-debugging.
Antes de começar, devemos examinar as regras do mundo de bugging:
- Debugger - Este é o software que basicamente executa toda a lógica do loop do depurador e com ele pode-se explorar o-debuggee.
- Debuggee - Este é o software no qual a operação de depuração é realizada e que estamos investigando.
- Apenas um depurador pode ser conectado a cada software que pode explorá-lo e todos os seus threads.
- Apenas o thread que abriu o depurado pode depurar o processo, então CreateProcess deve estar no mesmo thread que o Debugger-loop.
- Durante o evento de depuração, todos os threads no debugee estão em estado suspended.
Executando software sob debugger:
Como mencionei anteriormente, para realizar a depuração, precisamos iniciar o processo com o dwCreateionFlags – DEBUG_ONLY_THIS_PROCESS que será assim:
Então, basicamente, iniciamos o processo no modo de depuração e instruímos o sistema operacional a fornecer informações ao nosso processo sobre todos os eventos de depuração, como:
- Process creation/termination
- Thread creation/termination
- Runtime exceptions
- E mais...
Além disso, o sinalizador DEBUG_ONLY_THIS_PROCESS indica que apenas depuramos o processo atual e ignoramos todos os processos filhos que ele cria.
No entanto, no final desta etapa, um novo processo aparece no Gerenciador de Tarefas, mas está em modo de suspensão e agora precisamos criar o loop de depuração de que falamos anteriormente.
debugger loop
debugger loop Ele é o cérebro do depurador, ele executa em loop e a cada vez retorna o evento que o sistema operacional transmitiu para ele via WaitForDebugEvent. Assim:
O comando ContinueDebugEvent basicamente diz ao sistema operacional para continuar executando o debugee. Também obtemos todos os valores necessários em debug_event a partir do comando WaitForDebugEvent.
O último parâmetro indica se deve continuar ou não a ação e só é referido quando o evento é um exception-event. Além disso, você também pode especificar DBG_EXCEPTION_NOT_HANDLED para permitir que o software de depuração manipule o evento.
Não há do que temer de que o processo se desligue antes de realizarmos a leitura da memória ou a gravação na memória e, em seguida, nosso código irá travar, porque ao executar a depuração, o processo depurado está em suspensão total e nada pode pará-lo.
Nem o task manager, process explorer e etc.'... Uma operação de kill fará com que o sistema operacional agende EXIT_PROCESS_DEBUG_EVENT como o próximo comando que nosso depurador recebe em-debugging loop.
Handling debugging events
Existem 9 tipos diferentes de eventos principais e outros 20 subeventos para eventos de exceção, vamos nos concentrar neles mais tarde e agora vamos olhar para a estrutura de DEBUG_EVENT que é assim:
O comando WaitForDebugEvent espera até que recebamos um evento e, em seguida, preenche todos os dados nessa estrutura. O dwDebugEventCode indica qual evento realmente ocorreu e com base no número do evento, saberemos o que fazer de acordo com o evento em- union.
debugger events
O evento OUTPUT_DEBUG_STRING_EVENT ocorre quando queremos criar um texto de depuração que será exibido na janela Saída do Depurador. Quando recebemos este tipo de evento, trabalhamos com uma estrutura chamada OUTPUT_DEBUG_STRING_INFO que é assim:
A variável nDebugStringLength é o tamanho do texto junto com ‘\0’ ( null terminiating ).
A variável fUnicode especifica o formato do texto em 0 (unicode) Ou em 1 (ANSI).
A variável lpDebugStringData de acordo com fUnicde contém o texto.
Deve-se notar que o endereço do lpDebugStringData não está no espaço da memória do depurador e, portanto, o endereço deve ser lido do espaço da memória do processo sob investigação.-debuggee.
Para ler a memória de outro processo, usamos ReadProcessMemory. Claro que o processo de chamada deve ter as permissões apropriadas, o que não é um problema porque nós criamos o processo a partir do depurador, então temos as permissões para fazer tais leituras.
CREATE_PROCESS_DEBUG_EVENT É o primeiro evento que o depurador obtém quando o é criado o processo-debuggee.
Dentro do DEBUG_EVENT usaremos CreateProcessInfo para preencher a estrutura CREATE_PROCESS_DEBUG_INFO
Esta estrutura contém uma grande quantidade de informações sobre o processo resultante que podemos usar mais tarde.
Imediatamente após o final da instrução CREATE_PROCESS_DEBUG_EVENT, uma dll é carregada na memória do depurado, o que faz com que a instrução LOAD_DLL_DEBUG_EVENT seja executada para cada dll e dll ou quando o processo de depuração executa LoadLibrary explicitamente.
Para acessar uma estrutura que contém informações sobre a dll usamos LoadDll que nos preenche com a estrutura LOAD_DLL_DEBUG_INFO assim:
Quando um novo thread é criado no debuggee, o evento CREATE_THREAD_PROCESS_DEBUG_EVENT é chamado. Este evento é chamado antes que o thread realmente comece a funcionar e, portanto, permite que o depurador decida o que fazer com ele. Para acessar os dados relacionados ao evento, recorremos a CreateThread, que preenche o CREATE_THREAD_DEBUG_INFO assim:
Claro, este também é o endereço do-debuggee.
Quando o thread termina, o evento EXIT_THREAD_DEBUG_EVENT é chamado. Também aqui, como em seus predecessores, o evento pode ser acessado por meio de uma variável de DEBUG_EVENT chamada dwThreadId. Dessa forma, saberemos qual thread realmente concluiu sua operação. Para obter todas as informações sobre o tópico, veremos a estrutura EXIT_THREAD_DEBUG_INFO:
Assim como trabalhar com threads quando alguma dll executa um evento de descarregamento UNLOAD_DLL_DEBUG_EVENT ocorre. Para responder adequadamente a este evento, usamos o UnloadDll que preenche o UNLOAD_DLL_DEBUG_INFO:
Como você pode ver, temos apenas o ponteiro para o endereço-base da mesma DLL, então temos que levar em consideração ao criar nosso depurador que o lpBaseOfDll de qualquer DLL carregado em LOAD_DLL_DEBUG_EVENT deve ser salvo, caso contrário não saberemos quem o descarregou!
Quando o software fecha corretamente ou como resultado de um erro, ocorre o evento EXIT_PROCESS_DEBUG_EVENT que é o evento mais simples. Para obter suas informações usaremos o ExitProcess que preenche o EXIT_PROCESS_DEBUG_INFO:
Quando esse evento acontece, precisamos parar nosso loop de depuração e, assim, encerrar nosso processo de depuração.
Quando ocorre um erro, o evento é denominado EXCEPTION_DEBUG_EVENT que é na verdade o evento principal do depurador e no qual desejo me concentrar.
EXCEPTION_DEBUG_EVENT Como mencionei anteriormente, este é o evento mais importante no loop do depurador, que é responsável por lidar com quaisquer erros que ocorram durante o processo de depuração, como:
- Acesse locais de memória fora do nosso domínio
- Breakpoint
- Divida por 0
- SEH
- Etc'...
Além disso, existe uma estrutura que é preenchida pelo sistema operacional chamada EXCEPTION_DEBUG_INFO e acessível via Exception:
Dentro desta estrutura está outra estrutura chamada EXCEPTION_RECORD que contém informações sobre o próprio erro:
Quando realizamos a depuração, deve-se levar em consideração que o depurador obtém os erros antes do-debuggee.
- Quando o depurador obtém o erro, ele é chamado de erro First Chance
- Quando o debuggee obtém o erro, ele é chamado de erro Second Chance
Existem tipos de erros que não são realmente erros, como break point, que estão apenas relacionados ao depurador e não relacionados ao debuggee e, portanto, o depurador obtém a capacidade de manipulá-los primeiro.
Se tivermos um evento de erro, devemos passar para ContinueDebugEvent uma das seguintes opções:
- DBG_CONTINUE – Quando o erro for tratado de maneira ordenada e não há nenhuma ação que o debuggee precisa executar para continuar trabalhando corretamente.
- DBG_EXCEPTION_NOT_HANDLED – Quando o depurador não consegue corrigir o erro e o software precisa tratá-lo.
Se não sabemos o que fazer com o erro é melhor transferi-lo para o software usando DBG_EXCEPTION_NOT_HANDLED a menos que tenhamos um ponto de interrupção que nomeamos e precisemos corrigi-lo é claro.
Exceptions codes
- EXCEPTION_ACCESS_VIOLATION
- EXCEPTION_ARRAY_BOUNDS_EXCEEDED
- EXCEPTION_BREAKPOINT
- EXCEPTION_DATATYPE_MISALIGNMENT
- EXCEPTION_FLT_DENORMAL_OPERAND
- EXCEPTION_FLT_DIVIDE_BY_ZERO
- EXCEPTION_FLT_INEXACT_RESULT
- EXCEPTION_FLT_INVALID_OPERATION
- EXCEPTION_FLT_OVERFLOW
- EXCEPTION_FLT_STACK_CHECK
- EXCEPTION_FLT_UNDERFLOW
- EXCEPTION_ILLEGAL_INSTRUCTION
- EXCEPTION_IN_PAGE_ERROR
- EXCEPTION_INT_DIVIDE_BY_ZERO
- EXCEPTION_INT_OVERFLOW
- EXCEPTION_INVALID_DISPOSITION
- EXCEPTION_NONCONTINUABLE_EXCEPTION
- EXCEPTION_PRIV_INSTRUCTION
- EXCEPTION_SINGLE_STEP
- EXCEPTION_STACK_OVERFLOW
Não vamos examiná-los agora, vamos nos concentrar apenas no EXCEPTION_BREAKPOINT que se parece é assim.:
Debugger Main Engine
Para que nosso depurador funcione corretamente, precisamos adicionar alguns recursos básicos a ele, como:
1. Obtenção de dados básicos sobre nosso Debuggee por meio do evento CREATE_PROCESS_DEBUG_EVENT.
Quando um programa começa a executar o evento CREATE_PROCESS_DEBUG_EVENT lê e preenche a estrutura CREATE_PROCESS_DEBUG_INFO contendo lpStartAddress e outros dados, como o handle para o process e o handle para o thread que usaremos posteriormente para trabalhar perante o-debuggee:
`g_hProcess = DebugEv->u.CreateProcessInfo.hProcess; g_hThread = DebugEv->u.CreateProcessInfo.hThread;`
Também para fazer um breakpoint precisamos substituir um byte no início da instrução para CC que indica INT 3. Então, quando tratarmos de um evento em nosso código, teremos que retornar o byte que estava lá. É assim que o breakpoint realmente funciona nos bastidores, como expliquei no vídeo no início do artigo.
2. Faça um break point em um lugar que nos interessa.
Para fazer um break point, devemos realizar as seguintes etapas:
- Leia um byte do endereço que queremos fazer um nome de ponto de interrupção e salvá-lo em alguma estrutura de dados.
- Escreva o opcode-0xCC em seu lugar e, assim, alterar a instrução.
3. Lidar com o evento de breakpoint corretamente.
Como mencionei anteriormente, EXCEPTION_BREAKPOINT é um evento EXCEPTON_DEBUG_EVENT que preenche a estruturaEXCEPTION_DEBUG_INFO .
É importante observar que, ao construir um depurador, o sistema operacional nos enviarábreakpoint instruction, mesmo antes de o processo de debuggee aumentar para indicar que o processo está sob o depurador. Portanto, é aconselhável pular o primeiro ponto de interrupção obtido por meu depurador, conforme recomendo. Assim:
`case EXCEPTION_BREAKPOINT: if (m_bBreakpointOnceHit) { // actual breakpoint event } else { m_bBreakpointOnceHit = true; } break;`
Então, basicamente, vamos pular o primeiro ponto de interrupção do sistema operacional e não cuidar dele porque o sistema operacional cuida dele sozinho e não temos nenhuma informação que precisamos recuperar.
Depois que o processador pára por causa do nosso breakpoint, temos que recuperá-lo repetindo uma instrução para trás, o que significa que temos que voltar um byte no EIP antes de corrigir o-breakpoint.
Para fazer isso, devemos usar os dois comandos:
- GetThreadContext – Obter acesso ao contexto do thread, ou seja, todos os registros de processador.
- SetThreadContext – Escrever para o contexto significa mudar os registros.
Para obter o contexto, iremos realizar a seguinte ação:
`CONTEXT lcContext; lcContext.ContextFlags = CONTEXT_ALL; GetThreadContext(m_cProcessInfo.hThread, &lcContext);`
E assim obtemos todos os registros, agora vamos ao EIP e colocamos de volta um byte.
`lcContext.Eip--; // Move back one byte SetThreadContext(m_cProcessInfo.hThread, &lcContext);`
Para realizar essas ações, precisamos das seguintes permissões, que obviamente temos porque debugger.
- THREAD_SET_CONTEXT
- THREAD_GET_CONTEXT
Essas ações realizam uma troca de contexto e retornam uma instrução atrás, agora vamos restaurar o byte que salvamos e corrigir a instrução para que o software continue a funcionar corretamente.
`DWORD dwWriteSize; WriteProcessMemory(g_hProcess, (void*)dwAddress, &temp->cInstruction, 1, &dwWriteSize); FlushInstructionCache(g_hProcess, (void*)dwAddress, 1);`
Esta é basicamente a lógica principal que é necessario saber ao trabalhar com um depurador, eu recomendo que você tente implementar mais algumas instruções em seu depurador e assim entender em profundidade todas as ações do depurador. Também recomendo que você leia como fazer um ponto de interrupção de maneira diferente usando trap flag.
Boa sorte!