Target

Our target for this lesson will be Assault Cube 1.2.0.2.

Overview

In previous lessons, we used x64dbg to debug and reverse games. After attaching x64dbg to these games, we were able to set breakpoints on game instructions. When the game executed these instructions, our breakpoints would pop and program execution would pause. We could then observe the values of all the registers and step through individual instructions.

In this lesson, we will explore how to create a debugger for Windows utilizing the Windows API. We will confirm that this debugger is working by using Assault Cube as an example. In the No Recoil lesson, we identified that the mov instruction at 0x0046366C was only executed when the player was firing. After we create our debugger, we will place a breakpoint on this instruction and verify that it is only hit when we fire.

Windows Debugger API’s

Windows has a collection of API’s that allow for a process to attach to and debug another process. These are detailed in several short articles available on MSDN.

For our purposes, we mainly care about the following API’s:

  • DebugActiveProcess, which is used to attach to a target process
  • WaitForDebugEvent, which is used to wait for debugging events, as described in this MSDN article
  • ContinueDebugEvent, which is used to continue execution after a debug event is triggered

When using these API’s, we are attaching to a process and waiting for it to trigger one of several debug events, such as creating a thread or encountering an exception. However, when debugging a target we do not have the source code to, this will limit us to only breaking on thread and process creation events.

To be able to trigger a breakpoint on an address, we will need to use an interrupt instruction. Interrupt instructions are a special set of software instructions that invoke a special interrupt handler on the CPU. One of these instructions, int 3, will trigger a breakpoint when executed. Its opcode is 0xCC.

We can utilize this behavior to set a breakpoint on any instruction. Before we attach a debugger to a process, we will use WriteProcessMemory to write 0xCC to the instruction we wish to break on. We will then listen for debug events like normal. When we get a breakpoint event, we will restore the instruction to its original form and continue execution. By doing this, we can set breakpoints on any instruction in targets that we do not have the source control to.

The full source code for the debugger discussed in this lesson is available on github.

Writing the Int 3 Instruction

To write our int 3 instruction into the target, we will use an approach covered in previous lessons. First, we will iterate over all processes in the system using CreateToolhelp32Snapshot and locate the Assault Cube process (ac_client.exe). Then, we will open a handle to the process, and use that handle to write 0xCC (the opcode for int 3) over the instruction at 0x0046366C:

HANDLE process_snapshot = NULL;
HANDLE process_handle = NULL;

DWORD pid;
DWORD bytes_written = 0;

BYTE instruction_break = 0xcc;

PROCESSENTRY32 pe32 = { 0 };

pe32.dwSize = sizeof(PROCESSENTRY32);

process_snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
Process32First(process_snapshot, &pe32);

do {
  if (wcscmp(pe32.szExeFile, L"ac_client.exe") == 0) {
    pid = pe32.th32ProcessID;

    process_handle = OpenProcess(PROCESS_ALL_ACCESS, true, pe32.th32ProcessID);
    WriteProcessMemory(process_handle, (void*)0x0046366C, &instruction_break, 1, &bytes_written);
  }
} while (Process32Next(process_snapshot, &pe32));

Since we will need the process identifier (or pid) of Assault Cube for the DebugActiveProcess API, we also store the pid for later use.

Main Debugger Loop

Next, we can use an identical model discussed on MSDN to attach to the target and handle debugger events. The code provided on MSDN enters a permanent loop that checks for debugging events and then continues execution when encountering an event.

DEBUG_EVENT debugEvent = { 0 };

DWORD continueStatus = DBG_CONTINUE;

DebugActiveProcess(pid);

for (;;) {
  continueStatus = DBG_CONTINUE;

  if (!WaitForDebugEvent(&debugEvent, INFINITE))
    return 0;

  switch (debugEvent.dwDebugEventCode) {
    case EXCEPTION_DEBUG_EVENT:
      switch (debugEvent.u.Exception.ExceptionRecord.ExceptionCode)
      {
        case EXCEPTION_BREAKPOINT:
          continueStatus = DBG_CONTINUE;
          break;
        default:
          continueStatus = DBG_EXCEPTION_NOT_HANDLED;
          break;
      }
      break;
    default:
      continueStatus = DBG_EXCEPTION_NOT_HANDLED;
      break;
  }

  ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, continueStatus);
}

CloseHandle(process_handle);

Handling the Breakpoint

With this structure setup, we can now begin handling debugger events. First, let’s verify that our int 3 breakpoint actually worked with a print statement:

case EXCEPTION_BREAKPOINT:
  printf("Breakpoint hit");

  continueStatus = DBG_CONTINUE;
  break;

Make sure Assault Cube is running and run the debugger we have built so far. It should immediately print out Breakpoint hit. If you then fire, it will print out Breakpoint hit again before the game crashes. This indicates that our breakpoint was set successfully.

However, crashing the target is not ideal. To fix this, we will need to adjust two things:

  1. Only trigger our breakpoint when the instruction is executed and not when we first run our program.
  2. Restore the original instruction after our breakpoint is executed.

When we first attach to a process, a breakpoint exception is triggered. Since we only want to handle our breakpoint on the instruction, we will ignore this first exception:

bool first_break_has_occurred = false;
case EXCEPTION_BREAKPOINT:       
  if (first_break_has_occurred) {
    //only handle breakpoint events after the first exception
  }

  first_break_has_occurred = true;

Next, we can handle the crash that occurs after our breakpoint is triggered. This crash occurs because we have replaced the original mov opcode (0x8b) with our interrupt. After executing our interrupt and our handling of the debug event, the game tries to execute the next opcode, which is not valid. To resolve this, we need to restore the mov instruction after handling our debug event.

The EIP (extended instruction pointer) register is used to track the current instruction executing. Each time an instruction is executed, it is changed to reflect the next instruction address to execute. When we execute our int 3 instruction, it is increased by 1. To restore the mov instruction, we need to first decrease it.

We can do this by opening the thread responsible for triggering the breakpoint and retrieving the context (registers) of the thread. We can then decrease the EIP register and set the thread’s context to our new values:

HANDLE thread_handle = NULL;
CONTEXT context = { 0 };

thread_handle = OpenThread(THREAD_ALL_ACCESS, true, debugEvent.dwThreadId);
if (thread_handle != NULL) {
  context.ContextFlags = CONTEXT_ALL;
  GetThreadContext(thread_handle, &context);

  context.Eip--;

  SetThreadContext(thread_handle, &context);
  CloseHandle(thread_handle);

EIP will now point to the original mov instruction address again (0x0046366C). However, the instruction at this location will still be int 3. To fix this, we can use WriteProcessMemory to write the original opcode back to the address:

WriteProcessMemory(process_handle, (void*)0x0046366C, &instruction_normal, 1, &bytes_written);

With this change, Assault Cube will no longer crash when our breakpoint is triggered. In addition, we can set a breakpoint on the context.Eip– line of code and verify that we can view the contents of all registers when our breakpoint is triggered:

Disassembly

The same approach used to modify EIP can be used to modify other registers as well.

 

results matching ""

    No results matching ""