This post will discuss API monitoring in a remote process through RPCs (via sockets) and Google’s Protocol Buffers encoding/message interchange format. The purpose is to use the example as a building block for a generic API monitoring client-server application, with the server being resident inside of a DLL that is injected into a remote process. Clients can connect and send messages to install/remove hooks and receive updates from the server when these desired APIs are called by the target application. In summary, the system will interact as follows:
- A process that is the target of monitoring will be running on the system.
- A DLL is injected into this process and a server socket is created and begins listening.
- A separate client application will connect on the server port and begin issuing commands to the server to add/remove hooks.
- The server will receive these commands and inform the client when the desired API is hit. The parameters will be passed back to the client and the server will wait for a response from the client to continue execution in order for the client to properly process the returned parameters.
Below is the example protocol that these components will interact through
package ApiMonitor.ProtoBuf; message Call { required uint32 uiHookId = 1; repeated uint64 uiParameter = 2; } message AddHook { required uint32 uiHookId = 1; required string strDllName = 2; required string strFunctionName = 3; required uint32 uiNumParameters = 4; } message RemoveHook { required uint32 uiHookId = 1; } message MonitorMessage { optional AddHook mAddHook = 1; optional RemoveHook mRemoveHook = 2; optional Call mCall = 3; optional bool bIsContinue = 4; }
Client/Server components will receive a MonitorMessage, which can contain an add hook message, remove hook message, call information message, or a boolean indicating that it is a continue message from the client. The server will operate on AddHook/RemoveHook by performing the appropriate actions, and will generate Call mesages containing the values of the parameters as they are retrieved from the stack as part of the added API hook. The client will generate AddHook/RemoveHook messages, or send a continue message to the server by sending a message with bIsContinue as true. The client will additionally operate on received Call messages from the server and, for this example, display the parameters of the hooked function. Special identifiers (uiHookId) will identify individual hooks for easy removal or dispatching of received call messages. The example code I provide only shows one hooked function, but the idea allows for it to be extended to any arbitrary number.
Adding a hook becomes pretty straightforward. From the client code:
ApiMonitor::ProtoBuf::MonitorMessage mOutgoingMessage; mOutgoingMessage.mutable_maddhook()->set_uihookid(0x123); mOutgoingMessage.mutable_maddhook()->set_strdllname("user32.dll", 10); mOutgoingMessage.mutable_maddhook()->set_strfunctionname("MessageBoxA", 11); mOutgoingMessage.mutable_maddhook()->set_uinumparameters(4); (void)SendOutgoingMessage(sckConnect, &mOutgoingMessage); |
with SendOutgoingMessage being responsible for serialization of the Protocol Buffer message. The message are sent in two parts, with the first containing the size of the incoming message buffer and the latter containing the bytes of the message itself. This functionality is used both in client and server.
const bool Send(SOCKET sckConnect, const char *pBuffer, int uiBufferLength) { int iResult = send(sckConnect, (const char *)pBuffer, uiBufferLength, 0); if (iResult == SOCKET_ERROR) { printf("send failed. Error = %X\n", WSAGetLastError()); closesocket(sckConnect); WSACleanup(); return false; } return true; } const bool SendOutgoingMessage(SOCKET sckConnect, ApiMonitor::ProtoBuf::MonitorMessage *pMessage) { const int iBuffSize = pMessage->ByteSize(); char *pBuffer = (char *)malloc(iBuffSize * sizeof(char)); pMessage->SerializePartialToArray(pBuffer, iBuffSize); bool bRet = Send(sckConnect, (const char *)&iBuffSize, sizeof(int)); bRet &= Send(sckConnect, pBuffer, iBuffSize); free(pBuffer); return bRet; } |
On the server receiving end, the messages are read from the socket and the MonitorMessage is reconstructed. The fields are checked and the appropriate dispatch happens.
int iResult = 0; do { int iBuffSize = 0; iResult = recv(sckClient, (char *)&iBuffSize, sizeof(int), 0); char *pBuffer = (char *)malloc(iBuffSize * sizeof(char)); iResult = recv(sckClient, pBuffer, iBuffSize, 0); ApiMonitor::ProtoBuf::MonitorMessage mReceivedMessage; mReceivedMessage.ParseFromArray(pBuffer, iBuffSize); if (mReceivedMessage.has_biscontinue()) { SetEvent(hWaitEvent); } else if (mReceivedMessage.has_maddhook()) { (void)AddHook(mReceivedMessage.maddhook().uihookid(), mReceivedMessage.maddhook().strdllname().c_str(), mReceivedMessage.maddhook().strfunctionname().c_str(), mReceivedMessage.maddhook().uinumparameters(), &dwAddress); } else if (mReceivedMessage.has_mremovehook()) { (void)RemoveHook(mReceivedMessage.mremovehook().uihookid(), dwAddress); } free(pBuffer); } while (iResult > 0); |
If the message is a continue message then an event is signaled to allow the thread that invoked the target API to continue (this will be discussed further in a bit). Otherwise if the message is an add or remove hook message, the appropriate actions to add/remove it will be taken. The code for this won’t be shown here because the technique has been discussed several times before (see memory breakpoints or the previous usage of them). Additionally, the full source code for all of this is provided. Once the hook is installed and the target API is hit, it will trampoline to a hook function which will retrieve the parameters from the current execution context. The implementation is shown below
static void WINAPI HookFunction(CONTEXT *pContext) { EnterCriticalSection(&critSec); ApiMonitor::ProtoBuf::MonitorMessage mCallMessage; #ifdef _M_IX86 for(DWORD_PTR i = 0; i < dwHookNumParameters; ++i) { DWORD_PTR dwParameter = *(DWORD_PTR *)(pContext->Esp + sizeof(DWORD_PTR) + (i * sizeof(DWORD_PTR))); mCallMessage.mutable_mcall()->add_uiparameter(dwParameter); } #elif defined _M_AMD64 mCallMessage.mutable_mcall()->add_uiparameter(pContext->Rcx); mCallMessage.mutable_mcall()->add_uiparameter(pContext->Rdx); mCallMessage.mutable_mcall()->add_uiparameter(pContext->R8); mCallMessage.mutable_mcall()->add_uiparameter(pContext->R9); #else #error "Unsupported platform" #endif mCallMessage.mutable_mcall()->set_uihookid(dwHookId); SendOutgoingMessage(sckOutgoing, &mCallMessage); WaitForSingleObject(hWaitEvent, INFINITE); LeaveCriticalSection(&critSec); } |
For x86, the parameters are retrieved directly from the stack. For x64, the four parameters are retrieved from registers as per the x64 ABI on Windows. If more parameters were to be retrieved for x64, there would have to be an additional field to specify the stack offset at which they start. The example keeps it simple and uses an API (MessageBoxA) with only four parameters. These values are added to a Call message and sent out back to the client. The thread then halts execution waiting for an event to be signaled. This is the event that is signaled via SetEvent(hWaitEvent); on the listening thread.
Going back to the client, the code for handling this Call message is shown below:
do { ApiMonitor::ProtoBuf::MonitorMessage mIncomingMessage = ReceiveIncomingMessage(sckConnect); assert(mIncomingMessage.mcall().uihookid() == 0x123); HWND hWnd = (HWND)mIncomingMessage.mcall().uiparameter(0); DWORD_PTR dwTextAddress = (DWORD_PTR)mIncomingMessage.mcall().uiparameter(1); DWORD_PTR dwCaptionAddress = (DWORD_PTR)mIncomingMessage.mcall().uiparameter(2); UINT uiType = (UINT)mIncomingMessage.mcall().uiparameter(3); LPSTR lpTextBuffer[64] = { 0 }; LPSTR lpTitleBuffer[64] = { 0 }; DWORD dwProcessId = atoi(argv[1]); HANDLE hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, dwProcessId); SIZE_T dwBytesRead = 0; (void)ReadProcessMemory(hProcess, (LPCVOID)dwTextAddress, lpTextBuffer, sizeof(lpTextBuffer), &dwBytesRead); (void)ReadProcessMemory(hProcess, (LPCVOID)dwCaptionAddress, lpTitleBuffer, sizeof(lpTitleBuffer), &dwBytesRead); printf("Parameters\n" "HWND: %X\n" "Text: %s\n" "Title: %s\n" "Type: %X\n", hWnd, lpTextBuffer, lpTitleBuffer, uiType); mOutgoingMessage.Clear(); mOutgoingMessage.set_biscontinue(true); (void)SendOutgoingMessage(sckConnect, &mOutgoingMessage); } while (!GetAsyncKeyState(VK_F12)); |
The parameters are retrieved from the message. Two of these parameters are addresses, specifically the MessageBox text and caption. These need to be read from the process memory and are done via a ReadProcessMemory call. After these are retrieved and output, the client creates a Continue message and sends it back to the server to continue execution there. After monitoring is finished (via an F12 key press), the client sends a remove hook message with the following:
mOutgoingMessage.Clear(); mOutgoingMessage.mutable_mremovehook()->set_uihookid(0x123); (void)SendOutgoingMessage(sckConnect, &mOutgoingMessage); |
which removes the hook from the target process.
Taking a look at it in action, an example application which repeatedly calls MessageBoxA via
MessageBoxA(NULL, "Hello, World!", "Test", MB_ICONINFORMATION); |
is available. Below is a screenshot of the client after the server DLL was injected into this process.The full source code relating to this can be found here. The static libraries were compiled with VS 2013 and will need to be recompiled if other compilers are used.