API call logging is a mechanism of logging API call made by an application.

In this series, we are going to learn about how to develop an API call logger using Windows API.

Windows provides a feature for instrumenting applications known as Windows debugging API. These are certain calls which provide an interface for instrumentation.

To instrument an application, we need to define a debug loop.

A primitive instrumentation code is as follows:

void EnterDebugLoop(const LPDEBUG_EVENT DebugEv)
{
   DWORD dwContinueStatus = DBG_CONTINUE; // exception continuation 
 
   for(;;) 
   { 
cates
    
 
      WaitForDebugEvent(DebugEv, INFINITE); 

   // Process the debugging event code. 
 
      switch (DebugEv->dwDebugEventCode) 
      { 
         case EXCEPTION_DEBUG_EVENT: 

 
            switch(DebugEv->u.Exception.ExceptionRecord.ExceptionCode)
            { 
               case EXCEPTION_ACCESS_VIOLATION: 

                  break;
 
               case EXCEPTION_BREAKPOINT: 
            
                  break;
 
               case EXCEPTION_DATATYPE_MISALIGNMENT: 
               // First chance: Pass this on to the system. 
               // Last chance: Display an appropriate error. 
                  break;
 
               case EXCEPTION_SINGLE_STEP: 
               // First chance: Update the display of the 
               // current instruction and register values. 
                  break;
 
               case DBG_CONTROL_C: 
               // First chance: Pass this on to the system. 
               // Last chance: Display an appropriate error. 
                  break;
 
               default:
               // Handle other exceptions. 
                  break;
            } 

            break;
 
         case CREATE_THREAD_DEBUG_EVENT: 
        
            dwContinueStatus = OnCreateThreadDebugEvent(DebugEv);
            break;

         case CREATE_PROCESS_DEBUG_EVENT: 
         
            dwContinueStatus = OnCreateProcessDebugEvent(DebugEv);
            break;
 
         case EXIT_THREAD_DEBUG_EVENT: 
         // Display the thread's exit code. 

            dwContinueStatus = OnExitThreadDebugEvent(DebugEv);
            break;
 
         case EXIT_PROCESS_DEBUG_EVENT: 
         // Display the process's exit code. 

            dwContinueStatus = OnExitProcessDebugEvent(DebugEv);
            break;
 
         case LOAD_DLL_DEBUG_EVENT: 
         // Read the debugging information included in the newly 
         // loaded DLL. Be sure to close the handle to the loaded DLL 
         // with CloseHandle.

            dwContinueStatus = OnLoadDllDebugEvent(DebugEv);
            break;
 
         case UNLOAD_DLL_DEBUG_EVENT: 
         // Display a message that the DLL has been unloaded. 

            dwContinueStatus = OnUnloadDllDebugEvent(DebugEv);
            break;
 
         case OUTPUT_DEBUG_STRING_EVENT: 
         // Display the output debugging string. 

            dwContinueStatus = OnOutputDebugStringEvent(DebugEv);
            break;

         case RIP_EVENT:
            dwContinueStatus = OnRipEvent(DebugEv);
            break;
      } 
 
   // Resume executing the thread that reported the debugging event. 
 
   ContinueDebugEvent(DebugEv->dwProcessId, 
                      DebugEv->dwThreadId, 
                      dwContinueStatus);
   }
}

 

But first a process needs to be created and initiated with DEBUG_ONLY_THIS_PROCESS flag

which can be done using the following way:

  DEBUG_EVENT debug_event = {0};
 STARTUPINFO si; 
  
  ZeroMemory( &si, sizeof(si) ); 
  si.cb = sizeof(si); 
  ZeroMemory( &pi, sizeof(pi) );


  CreateProcess ( argv[1], NULL, NULL, NULL, FALSE,

 

Ethical Hacking Training – Resources (InfoSec)

Setting and catching a breakpoint

After the application is initialized, we can set up a break point to instrument an application when it reaches to a certain point during the runtime of an application.

It can be done by replacing the instruction byte at that particular address with 0xcc byte i.e. INT 0xcc (software breakpoint) instruction.

Moreover, when that particular breakpoint area, the original instruction needs to be replaced with a displacement in EIP register.

We can use WriteProcessMemory and ReadProcessMemory to write/read bytes in a remote process.

ReadProcessMemory(pi.hProcess ,pEntryPoint, &OrgByte, 0x01, NULL); WriteProcessMemory(pi.hProcess ,pEntryPoint,”\xcc”, 0x01, NULL);

in this case, at the entry point of an application a breakpoint is set, but first, the byte at EP is read and stored (it will be later on replaced when a breakpoint is hit)

 

               case EXCEPTION_BREAKPOINT: 
            printf("Breakpoint\n");
            if (!fChance) 
            {
			dwContinueStatus = DBG_CONTINUE; // exception continuation 
                  fChance = 1;

                  printf("\n%s", "First Chance Exception");
			break;
             }
	  lcContext.ContextFlags = CONTEXT_ALL;
        GetThreadContext(pi.hThread, &lcContext);
                                   
                                   
        if (DebugEv->u.Exception.ExceptionRecord.ExceptionAddress == pEntryPoint)
	{
                                        
	WriteProcessMemory(pi.hProcess ,DebugEv->u.Exception.ExceptionRecord.ExceptionAddress,&OrgByte, 0x01, NULL); 
                                                
printf(" Entry point address = %p\n", DebugEv->u.Exception.ExceptionRecord.ExceptionAddress);

	lcContext.ContextFlags = CONTEXT_ALL;
	GetThreadContext(pi.hThread, &lcContext);
	lcContext.Eip--; // Move back one byte
	SetThreadContext(pi.hThread, &lcContext);
	 FlushInstructionCache(pi.hProcess,DebugEv- 	>u.Exception.ExceptionRecord.ExceptionAddress,1);

	dwContinueStatus = DBG_CONTINUE   ; // exception continuation 
  	dwContinueStatus = DBG_CONTINUE;
  break;
                                                                          
                                   }

In this case first chance exception is caught and allowed to happen.

When breakpoint at Entrypoint is hit, the instruction at the breakpoint is replaced with the original instruction and eip is displaced back by one byte to execute the original instruction.

Then the new context is set.