//========================================== // Matt Pietrek // Microsoft Systems Journal, February 2000 // FILE: DelayLoadProfileDLL.CPP //========================================== #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> #include "DelayLoadProfileDLL.H" //=========================================================================== BOOL WINAPI DllMain( HINSTANCE hinstDLL, // handle to DLL module DWORD fdwReason, // reason for calling function LPVOID lpvReserved) // reserved { if ( fdwReason == DLL_PROCESS_ATTACH ) // When initializing.... { // We don't need thread notifications for what we're doing. Thus, get // rid of them, thereby eliminating some of the overhead of this DLL DisableThreadLibraryCalls( hinstDLL ); return PrepareToProfile(); } else if ( fdwReason == DLL_PROCESS_DETACH ) // When shutting down... { ReportProfileResults(); } return TRUE; } //=========================================================================== // Top level routine to find the EXE's imports, and redirect them bool PrepareToProfile( void ) { HMODULE hModEXE = GetModuleHandle( 0 ); PIMAGE_NT_HEADERS pExeNTHdr = PEHeaderFromHModule( hModEXE ); if ( !pExeNTHdr ) return false; DWORD importRVA = pExeNTHdr->OptionalHeader.DataDirectory [IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; if ( !importRVA ) return false; // Convert imports RVA to a usable pointer PIMAGE_IMPORT_DESCRIPTOR pImportDesc = MakePtr( PIMAGE_IMPORT_DESCRIPTOR, hModEXE, importRVA ); // Save off imports address in a global for later use g_pFirstImportDesc = pImportDesc; // Iterate through each import descriptor, and redirect if appropriate while ( pImportDesc->FirstThunk ) { PSTR pszImportModuleName = MakePtr( PSTR, hModEXE, pImportDesc->Name); if ( IsModuleOKToHook(pszImportModuleName) ) { RedirectIAT( pImportDesc, (PVOID)hModEXE ); } pImportDesc++; // Advance to next import descriptor } return true; } //=========================================================================== // Called at shutdown time. Locates and scans through the stubs we build to // see how many times each imported DLL was called. bool ReportProfileResults( void ) { HMODULE hModEXE = GetModuleHandle( 0 ); PIMAGE_IMPORT_DESCRIPTOR pImportDesc = g_pFirstImportDesc; // Iterate through each import descriptor while ( pImportDesc->FirstThunk ) { PSTR pszImportModuleName = MakePtr(PSTR, hModEXE, pImportDesc->Name); // If we haven't hooked it, move on to the next import. Otherwise, // go report the calls made to the DLL if ( !IsModuleOKToHook(pszImportModuleName) ) { pImportDesc++; continue; } // Get address stored in the first IAT entry for this DLL PIMAGE_THUNK_DATA pIAT = MakePtr( PIMAGE_THUNK_DATA, hModEXE, pImportDesc->FirstThunk); // The above address *probably* points at one of our redirected stubs DLPD_IAT_STUB * pDLPDStub = (DLPD_IAT_STUB *)pIAT->u1.Function; // Make sure we're pointing at our redirected stubs. If not, // spit out a warning and move on if ( (pDLPDStub->instr_CALL !=0xE8) || (pDLPDStub->instr_JMP !=0xE9)) { char szOutBuffer[MAX_PATH * 2]; sprintf( szOutBuffer, "DelayLoadProfile: %s was not hooked", pszImportModuleName ); OutputDebugString( szOutBuffer ); pImportDesc++; continue; } DWORD nCalls = 0; // Set initial count to 0 // Iterate through each of our stubs, tallying up the calls while ( pDLPDStub->pszNameOrOrdinal ) { nCalls += pDLPDStub->count; pDLPDStub++; // Advance to next stub } // Format the results and send to the debugger (DelayLoadProfile.EXE) char szOutBuffer[512]; sprintf(szOutBuffer, "DelayLoadProfile: %s was called %u times", pszImportModuleName, nCalls ); OutputDebugString( szOutBuffer ); pImportDesc++; // Advance to next imported DLL } return true; } //=========================================================================== // Builds stubs for and redirects the IAT for one DLL (pImportDesc) bool RedirectIAT( PIMAGE_IMPORT_DESCRIPTOR pImportDesc, PVOID pBaseLoadAddr ) { PIMAGE_THUNK_DATA pIAT; // Ptr to import address table PIMAGE_THUNK_DATA pINT; // Ptr to import names table PIMAGE_THUNK_DATA pIteratingIAT; // Figure out which OS platform we're on OSVERSIONINFO osvi; osvi.dwOSVersionInfoSize = sizeof(osvi); GetVersionEx( &osvi ); // If no import names table, we can't redirect this, so bail if ( pImportDesc->OriginalFirstThunk == 0 ) return false; pIAT = MakePtr(PIMAGE_THUNK_DATA, pBaseLoadAddr, pImportDesc->FirstThunk); pINT = MakePtr( PIMAGE_THUNK_DATA, pBaseLoadAddr, pImportDesc->OriginalFirstThunk ); // Count how many entries there are in this IAT. Array is 0 terminated pIteratingIAT = pIAT; unsigned cFuncs = 0; while ( pIteratingIAT->u1.Function ) { cFuncs++; pIteratingIAT++; } if ( cFuncs == 0 ) // If no imported functions, we're done! return false; // These next few lines ensure that we'll be able to modify the IAT, // which is often in a read-only section in the EXE. DWORD flOldProtect, flNewProtect, flDontCare; MEMORY_BASIC_INFORMATION mbi; // Get the current protection attributes VirtualQuery( pIAT, &mbi, sizeof(mbi) ); // remove ReadOnly and ExecuteRead attributes, add on ReadWrite flag flNewProtect = mbi.Protect; flNewProtect &= ~(PAGE_READONLY | PAGE_EXECUTE_READ); flNewProtect |= (PAGE_READWRITE); if ( !VirtualProtect( pIAT, sizeof(PVOID) * cFuncs, flNewProtect, &flOldProtect) ) { return false; } // Allocate memory for the redirection stubs. Make one extra stub at the // end to be a sentinel DLPD_IAT_STUB * pStubs = new DLPD_IAT_STUB[ cFuncs + 1]; if ( !pStubs ) return false; // Scan through the IAT, completing the stubs and redirecting the IAT // entries to point to the stubs pIteratingIAT = pIAT; while ( pIteratingIAT->u1.Function ) { pStubs->data_call = (DWORD_PTR)DelayLoadProfileDLL_UpdateCount - (DWORD_PTR)&pStubs->instr_JMP; pStubs->data_JMP = *(PDWORD)pIteratingIAT - (DWORD_PTR)&pStubs->count; // Technically, we don't absolutely need the pszNameOrOrdinal field, // but it's nice to have. It also serves as a useful sentinel value. if ( IMAGE_SNAP_BY_ORDINAL(pINT->u1.Ordinal) ) // import by ordinal { pStubs->pszNameOrOrdinal = pINT->u1.Ordinal; } else // It's imported by name { PIMAGE_IMPORT_BY_NAME pImportName = MakePtr(PIMAGE_IMPORT_BY_NAME, pBaseLoadAddr, pINT->u1.AddressOfData ); pStubs->pszNameOrOrdinal = (DWORD)&pImportName->Name; } // Cheez-o hack to see if what we're importing is code or data. // If it's code, we shouldn't be able to write to it if ( IsBadWritePtr( (PVOID)pIteratingIAT->u1.Function, 1 ) ) { pIteratingIAT->u1.Function = (DWORD)pStubs; } else if ( osvi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS ) { // Special hack for Win9x, which builds stubs for imported // functions in system DLLs (Loaded above 2GB). These stubs are // writeable, so we have to explicitly check for this case if ( pIteratingIAT->u1.Function > 0x80000000 ) pIteratingIAT->u1.Function = (DWORD)pStubs; } pStubs++; // Advance to next stub pIteratingIAT++; // Advance to next IAT entry pINT++; // Advance to next INT entry } pStubs->pszNameOrOrdinal = 0; // Final stub is a sentinel // Put the page attributes back the way they were. VirtualProtect( pIAT, sizeof(PVOID) * cFuncs, flOldProtect, &flDontCare); return true; } //=========================================================================== // Called from the DLPD_IAT_STUB stubs. Increments "count" field of the stub void DelayLoadProfileDLL_UpdateCount( PVOID dummy ) { __asm pushad // Save all general-purpose registers // Get return address, then subtract 5 (size of a CALL X instruction) // The result points at a DLPD_IAT_STUB // pointer math! &dummy-1 really subtracts sizeof(PVOID) PDWORD pRetAddr = (PDWORD)(&dummy - 1); DLPD_IAT_STUB * pDLPDStub = (DLPD_IAT_STUB *)(*pRetAddr - 5); pDLPDStub->count++; #if 0 // Remove the above conditional to get a cheezy API trace from // the loader process. It's slow! if ( !IMAGE_SNAP_BY_ORDINAL( pDLPDStub->pszNameOrOrdinal) ) OutputDebugString( (PSTR)pDLPDStub->pszNameOrOrdinal ); #endif __asm popad // Restore all general-purpose registers } • • • //=========================================================================== // Add any DLLs here that you don't want this DLL to redirect bool IsModuleOKToHook( PSTR pszModule ) { if ( 0 == lstrcmpi( pszModule, "KERNEL32.DLL" ) ) return false; if ( 0 == lstrcmpi( pszModule, "MFC42U.DLL" ) ) return false; if ( 0 == lstrcmpi( pszModule, "MFC42.DLL" ) ) return false; return true; }
Figure 2 Redirecting the IAT to Stubs
Figure 3 Excepts from DebugInjector.CPP
//========================================== // Matt Pietrek // Microsoft Systems Journal, February 2000 // FILE: DebugInjector.CPP //========================================== #include "stdafx.h" #include <stdio.h> #include <malloc.h> #include <stddef.h> #include "DebugInjector.h" static PSTR s_arszDebugEventTypes[] = { "", "EXCEPTION", "CREATE_THREAD", "CREATE_PROCESS", "EXIT_THREAD", "EXIT_PROCESS", "LOAD_DLL", "UNLOAD_DLL", "OUTPUT_DEBUG_STRING", "RIP", }; ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// • • • bool CDebugInjector::LoadProcess( PSTR pszCmdLine ) { STARTUPINFO startupInfo; memset(&startupInfo, 0, sizeof(startupInfo)); startupInfo.cb = sizeof(startupInfo); BOOL bCreateProcessRetValue; bCreateProcessRetValue = CreateProcess( 0, // lpszImageName pszCmdLine, // lpszCommandLine 0, // lpsaProcess 0, // lpsaThread FALSE, // fInheritHandles DEBUG_ONLY_THIS_PROCESS, // fdwCreate 0, // lpvEnvironment 0, // lpszCurDir &startupInfo, // lpsiStartupInfo &m_ProcessInformation ); // lppiProcInfo return bCreateProcessRetValue != FALSE; } • • • //=================================================================== bool CDebugInjector::Run( void ) { DEBUG_EVENT dbgEvent; DWORD dwContinueStatus; // The debug loop. Runs until the debuggee terminates while ( 1 ) { WaitForDebugEvent(&dbgEvent, INFINITE); dwContinueStatus = HandleDebugEvent( dbgEvent ); if ( dbgEvent.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT ) break; ContinueDebugEvent( dbgEvent.dwProcessId, dbgEvent.dwThreadId, dwContinueStatus ); } return true; } • • • //=================================================================== DWORD CDebugInjector::HandleException(DEBUG_EVENT &dbgEvent ) { EXCEPTION_RECORD & exceptRec = dbgEvent.u.Exception.ExceptionRecord; // If this is a second chance exception, the debugee is going to // die. Spit out the exception code and address if ( dbgEvent.u.Exception.dwFirstChance == FALSE ) { printf( "Exception code: %X Addr: %08X\r\n", exceptRec.ExceptionCode, exceptRec.ExceptionAddress ); } // If we've gone through the mechanics of injection already, just // pass the exception on to the debugee if ( m_bInjected ) return DBG_EXCEPTION_NOT_HANDLED; // If it isn't a breakpoint, we don't want to know about it. if ( exceptRec.ExceptionCode != EXCEPTION_BREAKPOINT ) return DBG_EXCEPTION_NOT_HANDLED; static bool s_bFirstBP = FALSE; DWORD dwContinueStatus = DBG_CONTINUE; // Is this the DebugBreak breakpoint? if ( s_bFirstBP == false ) { SetEntryPointBP(); s_bFirstBP = true; } // Is this the breakpoint we set at the EXE's entry point? else if ( exceptRec.ExceptionAddress == m_pExeEntryPoint ) { RemoveEntryPointBP(); SaveEntryPointContext( dbgEvent ); PlaceInjectionStub(); } // Is this the BP immediately after our LoadLibrary call? else if ( exceptRec.ExceptionAddress == m_pStubInTargetBP ) { RestoreEntryPointContext(); m_bInjected = true; } return dwContinueStatus; } • • • //=================================================================== bool CDebugInjector::PlaceInjectionStub( void ) { //===================================================== // Locate where the stub will be in the target process m_pStubInTarget = (LOADLIBRARY_STUB*)GetMemoryForLoadLibraryStub(); if ( !m_pStubInTarget ) return false; m_pStubInTargetBP = (PBYTE)m_pStubInTarget + offsetof(LOADLIBRARY_STUB, instr_INT_3); //===================================================== // Complete the stub fields that can't be preinitialized strcpy( m_stub.data_DllName, m_pszDLLToInject ); m_stub.operand_PUSH_value = (DWORD)m_pStubInTarget + offsetof( LOADLIBRARY_STUB, data_DllName); m_stub.operand_MOV_EAX = (DWORD)GetProcAddress(GetModuleHandle("KERNEL32.DLL"),"LoadLibraryA"); //===================================================== // Copy the stub into the target process bool retValue; retValue = WriteTargetMemory(m_pStubInTarget, &m_stub,sizeof(m_stub)); if ( !retValue ) return false; //===================================================== // Change the EIP register in the target thread to point // at the stub we just copied in. CONTEXT stubContext = m_originalThreadContext; stubContext.Eip = (DWORD)m_pStubInTarget; SetThreadContext( m_CreateProcessDebugInfo.hThread, &stubContext ); return true; } • • •