[Zz] Tracking of API functions using debug Functions
Release date:Source: CCIDBy Peng Chunhua
If we can write a function similar to the debugger ourselves, this debugger must meet our requirements for the tracing and monitoring tool, that is, to automatically record input and output parameters and automatically keep the target process running. Next we will introduce the scheme that can simply output monitoring results without knowing the function prototype-using the debug function to monitor API functions.
Monitor API functions using debug Functions
As you know, VC can be used for debugging.ProgramIn addition to debugging the debug program, you can also debug the release program.
(Assembly when debugging the release program)Code). If you know the function entry address, you only need to set the breakpoint on the function entry. When the program calls the function that sets the breakpoint, VC will suspend the Target Program
Run the program and you will get all the things you want in the target program memory. Generally, as long as you have enough patience and perseverance, as well as some assembly knowledge
Yes.
However, because the VC debugger will pause the running of the Target Program at each breakpoint, too much suspension of the target program is intolerable for monitoring tasks. So there won't be too many people who will actually use the VC debugger as a good API function monitor.
If the VC debugger can automatically output the stack value (that is, the function
The stack value (that is, the output parameter of the function) and the value of the CPU register (that is, the return value of the function) are automatically output at the end of the function operation, and the target program is not paused. Everything is from
No intervention is required. Will you use it as a monitor? I will.
I don't know how to make Vc do this (maybe VC does, but I don't know. If anyone knows, please pass
Let me know, Thank you), but I know that VC ALSO CALLS windows
The API functions complete the tasks of the debugger, and these functions can obviously meet my requirements. What I need to do is to use these API functions to write a simple debugger that occurs at the breakpoint of the target program.
Automatically output monitoring results and automatically resume the running of the target program.
Obviously, if you use the VC debugger as the monitor, you can get simple input and output without knowing the prototype of the target function.
Output Parameters and function running results. Because the monitoring code is not injected into the target program, there will be no conflict between the Monitoring Target Function and the monitoring code. The VC debugger can clearly trace recursive functions or trace
The DLL module calls the functions of the DLL itself and the EXE calls its own functions internally. As long as you know the entry address of the target function, you can trace it (the function that monitors the EXE itself can generate the EXE
When selecting the output map file, you can refer to the map file to get the address of the EXE internal function ). I have never heard that VC cannot debug multiple threads. At most, it is troublesome to debug multiple threads.
The thread can be debugged. Obviously, VC can also debug the code in dllmain. These have proved that debugging functions can achieve our goal.
How can we compile a program to achieve our goals? Which debugging functions are required?
First, let the target program enter the debugging status:
For a started process, you can use the debugactiveprocess function to capture the target process and bring it into the debug state.
Bool debugactiveprocess (DWORD dwprocessid ); |
The dwprocessid parameter is the ID of the target process. How to Use the toolhelp Series
Number or psapi library function to obtain the process ID of a running program in manyArticleAs described in, we will not repeat it here. For server programs, the target process cannot be captured because they have no permissions.
The monitoring program has the debugging permission to capture the target process (the user must have the debugging permission ).
To start a new program, you can use the CreateProcess function to set necessary parameters to switch the target program to the debug state.
Bool CreateProcess (lpctstr lpapplicationname, lptstr lpcommandline, Lpsecurity_attributes lpprocessattributes, lpsecurity_attributes Lpthreadattributes, bool binherithandles, DWORD dwcreationflags, lpvoid Lpenvironment, maid directory, lpstartupinfo, Lpprocess_information lpprocessinformation ); |
For details about this function, refer to msdn. Here I will only introduce the parameters we are interested in. Here is a general usage
The method is different. dwcreationflags must be set to debug_process or debug_only_this_process. In this way
The target program will enter the debugging status. Debug_process and debug_only_this_process are described here.
Debug_only_this_process only debugs the target process. The debug_process Parameter not only debugs the target process, but also debugs
There are sub-processes. For example, if you use debug_only_this_processto start B .exein a.m., only a.exe will not debug the process.
B .exe. if debug_process, a.exeand B .exe are merged. For the sake of simplicity, this article only discusses the startup Parameter
Debug_only_this_process.
Usage:
Startupinfo ST = {0 }; Process_information pro = {0 }; St. cb = sizeof (ST ); CreateProcess (null, pszcmd, null, null, false, Debug_only_this_process, Null, szpath, & St, & Pro )); // Close the handle --- these handles are no longer used in the debugging program, so they can be closed Closehandle (PRO. hthread ); Closehandle (PRO. hprocess ); |
Second, monitor programs in the debugged status:
The target process is in the debugged state and the debugging program (here the debugging program is our monitoring program, which will not be said later
Is responsible for the scheduling of debugging operations on the program to be debugged. The debug program uses the waitfordebugevent function to obtain debugging messages from the debugged program.
After the test message is processed, the debugging process will pause until the debugging program uses the continuedebugevent function to notify the program to continue running.
Bool waitfordebugevent ( Lpdebug_event lpdebugevent, // debug event information DWORD dwmilliseconds // time-out value ); |
You can obtain the debugging message in the lpdebugevent parameter. Note that this function must be consistent with
The thread that the program enters the debugging status is the same thread. That is to say, the thread called through debugactiveprocess or CreateProcess is a thread. In addition, I
We also like to set dwmilliseconds to-1 (infinite wait ). So I usually put the CreateProcess and waitfordebugevent functions in
Used in a new thread.
Typedef struct _ debug_event { DWORD dwdebugeventcode; DWORD dwprocessid; DWORD dwthreadid; Union { Exception_debug_info exception; Create_thread_debug_info createthread; Create_process_debug_info createprocessinfo; Exit_thread_debug_info exitthread; Exit_process_debug_info exitprocess; Load_dll_debug_info loaddll; Unload_dll_debug_info unloaddll; Output_debug_string_info debugstring; Rip_info ripinfo; } U; } Debug_event, * lpdebug_event; |
In this debugging message structure, dwdebugeventcode records the message code that causes debugging interruption. For details about the message code, refer to msdn. The message code we are interested in is:
Prediction_debug_event: Debugging exceptions Crate_thread_debug_event: generated by a new thread Create_process_debug_event: generated by a new process. Note: Only one debug_only_this_process operation is performed, When debug_process is enabled, the sub-process may be started multiple times. Exit_thread_debug_event: A thread stops running. Exit_process_debug_event: A process is aborted. Note: Only one debug_only_this_process operation is performed, In debug_process, there may be multiple times. Load_dll_debug_event: a DLL module is loaded. Unload_dll_debug_event: a DLL module is uninstalled. |
After obtaining the debugging message of the Target Program, the debugging program processes the message code differently and notifies the program to continue running.
Bool continuedebugevent ( DWORD dwprocessid, // process to continue DWORD dwthreadid, // thread to continue DWORD dwcontinuestatus // continuation status ); |
This function notifies the debugged program to continue running.
Example:
Debug_event DBE; Bool RC; CreateProcess (null, pszcmd, null, null, false, Debug_only_this_process, Null, szpath, & St, & Pro )); While (waitfordebugevent (& DBE, infinite )) { // If the message is exited, the debugging monitoring is complete. If (DBE. dwdebugeventcode = exit_process_debug_event) Break; // Go to debugging, monitoring, and processing Rc = ondebugevent (& DBE ); If (RC) Continuedebugevent (DBE. dwprocessid, DBE. dwthreadid, dbg_continue ); Else Continuedebugevent (DBE. dwprocessid, DBE. dwthreadid, Dbg _ dbg_exception_not_handled ); } // Debug the Message Processing Program Bool winapi ondebugevent (debug_event * pevent) { // We have not performed any operations on the target process. Therefore, true is returned first. Return true; } |
The above programs are the simplest debugging programs. However, it is basically useless. You have not set a breakpoint in the target process, so you cannot complete the task of monitoring API functions.
Set a breakpoint for the target process:
Our goal is to monitor the input and output of API functions. First, we should know which API functions are provided in the DLL module.
And the endpoint of these Apis. As mentioned earlier, APIs in the broad sense also include internal functions that have not been exported. If you have the debug version of the DLL module and the debug connection file (PDB file), you can also
Test the information to obtain information about the internal function.
· Obtain the function name and function entry address.
There are many methods to get the function entry address through the program. For the DLL compiled by VC, if it is a debug version, you can use the imagehlp library function to obtain debugging information and analyze the function entry address. If the debug version is not available, you can obtain the function entry address by analyzing and exporting the function table.
1. Use the imagehlp library function to get the function name and function entry address of the debug version.
You can use the imagehlp library function to analyze the debug information. The associated function is
Syminitialize, symenumeratesymbols, and undecoratesymbolname. For more information about these functions, see msdn.
Description and usage. However, imagehlp can only be used to analyze programs compiled with VC. This method cannot be used to analyze programs compiled with C ++ builder.
2. Export the DLL table to get the function export name and function entry address.
In most cases, we still want to monitor the input and output parameters of the release version. After all, the debug version
This is not the final product we provide to users. Different debugging and release compilation conditions lead to different results, which have been discussed in many BBS. So I think tracking and monitoring
The release version is more practical.
The exported function name is available on msdn after the DLL export table is analyzed.Source code. For details about how to export tables, refer to the article about pe structure.
3. Use the OLE function to obtain the COM interface
You can also use OLE functions to analyze the interface functions provided by DLL. The interface function is not used to export a table through DLL.
Output. You can use the loadtypelib function to analyze the COM interface and obtain the entry address of the com record interface. In this way, you can monitor the call of the COM interface. This is the API
Hook cannot be implemented. Here I am not going to analyze the COM interface. Search loadtypelib on msdn
You can find the relevant source code to modify the sample keyword to achieve your goal.
Here, we use the computer to automatically analyze the target module to obtain the DLL export function.
This is just to get a series of function names and function addresses. The function name is just a name that makes it easy for us to recognize functions. The function entry address is what we really care about. In other words
If you can ensure that an address must be the entry address of a function (including internal functions), you can define your own name for this function, add it to your function management table.
The input and output parameters of this function are monitored. This is also the reason for implementing the monitoring function of EXE internal functions. If you have a map file generated during EXE compilation (you can select to generate a map file during compilation)
By analyzing the map file, you can get the entry address of the internal function and add the internal function to your function management table. (Whether the name of a function is FUNA or
Funb is meaningless, but the name "FUNA" or "funb" is meaningful for the monitoring result. You can output the monitoring result using the MessageBox function.
It is output by FUNA name, so you can define your own name when monitoring some internal functions without name ).
· Set a breakpoint at the function entry address
It is very easy to set the breakpoint, as long as the 0xcc (int
3) write the specified address. In this way, when the program runs to the specified address, debugging interruption information will be generated to notify the debugging program. You can modify the memory data of a specified process through
Writeprocessmemory function. Generally, code segments are protected, so a function is also used.
Virtualprotectex. In actual situations, when a breakpoint occurs, the debugging program should also write the original code back to the program to be debugged.
unsigned char setbreakpoint (DWORD padd, unsigned char code) {< br> unsigned char B; bool RC; DWORD dwread, dwoldflg; // address above 0x80000000 is a common area of the system, If (padd> = 0x80000000 | padd = 0) return code; // obtain the original code rc = readprocessmemory (_ ghdebug, padd, & B, sizeof (byte), & dwread ); // the original code is the same as the code to be modified, and there is no need to modify it again If (rc = 0 | B = Code) return code; // modify page number protection attributes virtualprotectex (_ ghdebug, padd, sizeof (unsigned char), page_readwrite, & dwoldflg ); // modify the target code writeprocessmemory (_ ghdebug, padd, & code, sizeof (unsigned char), & dwread ); // restore page number protection attributes virtualprotectex (_ ghdebug, padd, sizeof (unsigned char), dwoldflg, & dwoldflg); return B; } |
When you set a breakpoint, you must save the original code so that the code can be restored when the breakpoint is restored. I
General Usage: Set the breakpoint m_code = setbreakpoint (pfunadd, 0xcc); restore the breakpoint: setbreakpoint (
Pfunadd, m_code); remember, the code of each function entry address may be different. You should save the original code for each breakpoint address, so that no error will occur during restoration.
Now, the breakpoint has been set in the target program. When the target program calls a function with the breakpoint set, a debugging interruption message will be generated to notify the debugging program. We need to compile our debugging interrupt program in the debugging program.
Compile and debug the interrupt handling program
When the debugging program is interrupted, A prediction_debug_event message is generated to notify the debugging program for processing. The prediction_debug_info structure is also filled in.
Typedef struct _ exception_debug_info { Prediction_record predictionrecord; DWORD dwfirstchance; } Prediction_debug_info, * lpexception_debug_info; Typedef struct _ exception_record { DWORD exceptioncode; DWORD exceptionflags; Struct _ prediction_record * predictionrecord; Pvoid exceptionaddress; DWORD numberparameters; Ulong_ptr predictioninformation [prediction_maximum_parameters]; } Prediction_record, * pexception_record; |
In this structure, we are interested in generating the interrupted address exceptionaddress and the Information Code exceptioncode. In the Information Code, the Information Code related to our task is:
Prediction_breakpoint: indicates the breakpoint interruption information code. Prediction_single_step: the code of the single-step interrupt Information |
Breakpoint interruption occurs when we set the breakpoint 0xcc code to run. Due to the interruption
The original code must be written back to The debugged program to continue running. However, once the code is written back to the target program, when the target program calls this function again, there will be no interruption, and we can only monitor it once.
. Therefore, after writing the original code back to the program to be debugged, we must run the program in one step to generate another debugging information for one step interruption. In the process of one-step interruption, we will
0xcc code writes the function entry address to ensure that the call is interrupted again.
First, we must prepare for the Interrupt Processing and manage the thread ID and handle. To manage single-step interrupt processing, we must also maintain a thread-based single-step address management, so that the program to be debugged can have multi-thread functions. -- We cannot guarantee that a single step is not interrupted by other threads of the process.
// We use a map to manage the relationship between thread IDs and thread handles // Use a map to manage the relationship between the function address and the breakpoint. Typedef Map <DWORD, handle, less <DWORD> thread_map; Typedef Map <DWORD, void *, less <DWORD> thread_singlestep_map; Thread_map _ gthreads; Fun_break_map _ gfunbreaks; // Assume that the following scheme is used to manage the original code when the breakpoint is set Byte Code = setbreakpoint (pfunadd, 0xcc ); If (code! = 0xcc) _ Gfunbreaks [pfunadd] = code; ... // Debug the processing program Bool winapi ondebugevent (debug_event * pevent) { Bool rc = true; Switch (pevent-> dwdebugeventcode) { Case create_process_debug_event: // Record the relationship between thread ID and thread handle _ Gthreads [pevent-> dwthreadid] = pevent-> U. createprocessinfo. hthread; ... Break; Case create_thread_debug_event: // Record the relationship between thread ID and thread handle _ Gthreads [pevent-> dwthreadid] = pevent-> U. createthread. hthread; ... Break; Case exit_thread_debug_event: // Clears the thread ID when the thread exits _ Gthreads. Erase (pevent-> dwthreadid ); ... Break; Case exception_debug_event: // Interrupt handling program Rc = ondebugexception (pevent ); Break; ... } Return RC; } |
The following describes the interrupt processing program. Similarly, we only consider the interrupt information code that we care about. In the event of an interruption, I
Get the context information of the interrupted thread through getthreadcontext (& context. In this case, context. ESP is the return address of the function,
The value at the position of context. ESP + 4 is the first parameter of the function, and context. ESP + 8 is the second parameter, and so on, you can obtain any parameter you want. Note that
Because the parameters are in the content of the debugging process, you must use the readprocessmemory function to get the following information:
DWORD Buf [4]; // obtain four parameters Readprocessmemory (_ ghdebug, (void *) (context. ESP + 4), & Buf, sizeof (BUF ), & Dwread ); |
Then Buf [0] is the first parameter, and Buf [1] is the second parameter... Note that
When calling the FUNA (int A, char * P, openfilename * POF) function, Buf [0] = A, Buf [1] =
P here Buf [1] is the pointer of P rather than the content of P. If you want to access the content of P, you must use the readprocessmemory function to obtain the content of P again. For Structure
The body pointer must also be like this:
// Get P content: Char pbuf [256]; Readprocessmemory (_ ghdebug, (void *) (BUF [1]), & pbuf, sizeof (pbuf), & dwread ); // Obtain the POF content: Openfilename Readprocessmemory (_ ghdebug, (void *) (BUF [2]), & of, sizeof (of), & dwread ); |
If a pointer exists in the struct, to obtain the pointer content, you must read the memory of the program to be debugged in the same way as to get P content. In general, you must realize that all the content of the monitored target program is read to the memory of the target process. These pointers are the memory address of the target process, not the address of the debugging process.
Obviously, when the debugging process
When the debugging information is interrupted, the debugging program can only get the input parameters of the function, but cannot get the expected output parameters and return values! To achieve our goal, we must re-produce the function at the end of the function call.
Generate an interrupt and obtain the output parameters and return values of the function. When the function entry is interrupted, you must set the breakpoint of the return address of the function. In this way, when the function returns, the output parameters and return values of the function can be obtained.
. For the implementation instructions here, refer to the source code in the appendix.
You can refer to the source code in the Appendix to write your own simple debugging and monitoring program. Of course, there are several reasons
I have not explained the complexity here. One is the processing of the breakpoint returned by the function, such as the try and catch processing. The return_fun_stack knot must be re-designed.
Structure, consider some troubleshooting can still solve this problem. Another problem is that the function's entry breakpoint has nothing to do with the returned breakpoint. This problem can be solved better. You only need to design it again.
Structures such as return_fun and fun_break_map can be associated. As long as I want to analyze how to implement the process of Interrupt debugging
The readers will follow up on the transformation.
About Win9x System
Careful readers can find a problem, that is, there is a limitation in the setbreakpoint function,
That is, the function entry address cannot be greater than 0x80000000. Indeed, we know that the space above 0x80000000 is a common space of the system, and we generally cannot modify the process of these spaces.
Otherwise, the system will be affected. In the NT environment, all the DLL files are loaded under 0x80000000. The code for modifying the space below 0x80000000 does not affect other processes.
. Therefore, you can use the above scheme to monitor all DLL Functions in NT. However, in Win9x, kernel32.dll, user32.dll, and gdi32.dll
System DLL is loaded to a space above 0x80000000. Modifying the code of these space will corrupt the system. So, can't the functions of these DLL modules be monitored in 9x?
Indeed, on the Win9x Platform, you cannot use the breakpoint setting method at the function entrance for monitoring. We must
Use another method to implement this function. As mentioned above
You can use the hook modification module to import tables to change the API entry to the entry of your own Monitoring Program, or to implement the monitoring function. If you use APIs
There are limits on the hook method, that is, you must know the function prototype and write the corresponding monitoring code for each function. The flexibility is limited. Our goal is no matter how many DLL, no matter how many DLL
We can monitor fewer export functions without modifying our programs. Therefore
Hook cannot accomplish our goal, but we can use the solution of modifying the import table to achieve our goal. First, modify the import table and direct the function call address to our monitoring code. In the monitoring code, we
You do not need to program functions, but simply call JMP.
XXXX. Then, when you set a breakpoint, It is not set at the function entry point, but on our monitoring code. In this way, when our module calls the system API function, it can achieve monitoring
Yes. Modification Principle
Assume that our monitoring code is in the 0x20000000 space of the target process.
When the table is exported, the address of the exported table function is calculated and set to JMP in the monitoring code.
XXXX code. In this way, the address written to the import table of the EXE module is the address of the monitoring code. When the target program calls the MessageBox function, the program will jump to the monitoring code first.
Run the JMP command in the MessageBox entry address of user32.dll. After such processing, we want to monitor the call of the MessageBox function.
When a breakpoint is set at 0x20000000 of the Code, the purpose of monitoring is achieved. Due to space limitations, we will not discuss it here.
Extended applications
You can easily expand your monitoring and tracking functions on this basis. You only need to modify the program that records the input and output function results to obtain a new function:
1. added the function of getting the current time point to record the input and output parameters, thus implementing the function of monitoring the function call performance.
(Equivalent to the truetime function of numbench) due to the debug technology, the obtained time will include the switching time of the process caused by the debugging function. The wait time is only a reference price
Value, but it is generally sufficient for analysis performance.
2. Add the function call counter where the input and output parameters are recorded to implement the truecoverage function of numbench.
3. Monitor the input and output values of the malloc, free, and realloc functions and perform statistics to implement a simple memory leak check function. The key is that you can use the map file to get the addresses of functions such as the release version malloc to track the release version.
4. Add the stackwalk function to record input parameter processing to implement call.
Stack function to analyze which function calls itself. This function can also be implemented in the JMP solution, but you must ensure that the function associated with stackwalk does not call the function you are monitoring.
There is no need to guarantee the hook api (IAT) solution, but the resulting call list may contain your monitoring code.
One thing to note is that our goal is to monitor the running path of the program, not to change the parameters and the modification results. Therefore, in JMP and hook api (IAT) the modification of parameters and running path can not be implemented here.
Others:
The Code testdebug.zip In This appendix provides a simple debugging monitor that automatically outputs the addresses of the four input parameters of the monitoring function and the return values of function calls. This Code only indicates that the API can be tracked through the monitoring function, so the system DLL is not monitored in 9X.
Debugapi.zipis an application written using this tutorial. It implements the input and output parameters function of the most basic tracking and monitoring function in this solution and monitors system DLL in 9X. This program supports the use of Win9x/NT/w2k/XP.
Source code download:Testdebug.zip,debugapi.zip
References:
1. Windows core programming, Jeffrey Richter, Mechanical Industry Press
2. Microsoft's msdn
3. detours can get the source code on the http://research.microsoft.com/sn/detours. The detours function is valid for WINNT and w2k, but not for 9X.