0x0 When your hook doesn't work
Introduction #
I am dedicating the Good Issue
series to a lot of my unfinished/practice codes I write (almost everyday)
which never make their way into a project. But, I end up learning a lot in the process. This series will
provide some insights of my learnings and I hope that you learn something after reading this kind of articles.
All the articles which belong to the series shall contain the same introduction.
The bug #
Can you identify a bug in the code below? What does the code lead to? infinite loop? recursion?
std::ofstream bufferFile1(BUFFER1, std::ios::binary | std::ios::ate);
std::ofstream bufferFile2(BUFFER2, std::ios::binary | std::ios::ate);
HMODULE dsound = GetModuleHandleA("C:\\Windows\\System32\\dsound.dll");
typedef HRESULT (__cdecl *unlock_fn_t)(void*, void*, DWORD, void*, DWORD);
unlock_fn_t unlock_fn = (unlock_fn_t) ((uint64_t)dsound + OFFSET);
volatile HRESULT __cdecl hook_unlock(
IDirectSoundBuffer* self,
LPVOID lpvAudioPtr1,
DWORD dwAudioBytes1,
LPVOID lpvAudioPtr2,
DWORD dwAudioBytes2
) {
if (lpvAudioPtr1 != nullptr) {
bufferFile1.write((char*) lpvAudioPtr1, dwAudioBytes1);
bufferFile1.flush();
}
if (lpvAudioPtr2 != nullptr) {
bufferFile2.write((char*) lpvAudioPtr2, dwAudioBytes2);
bufferFile2.flush();
}
return self->Unlock(lpvAudioPtr1, dwAudioBytes1, lpvAudioPtr2, dwAudioBytes2);
}
void attach() {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach((void**) &unlock_fn, (void*)hook_unlock);
DetourTransactionCommit();
}
Well, because of the bug, I was just shocked to see that the size of buffer files was nearly 30GB.
Context #
I happened to come across this situation when I was attempting to write an audio hook for my/our least favourite music player, whose situation is degrading because of the company.
I was using:
Well, I was targeting the DirectSound API, to be precise.
I was trying to extract some data off of the Unlock
function, which is defined as:
HRESULT Unlock(
LPVOID lpvAudioPtr1,
DWORD dwAudioBytes1,
LPVOID lpvAudioPtr2,
DWORD dwAudioBytes2
); // in dsound.h
So, the body of the hook function looked like this:
std::ofstream bufferFile1(...);
std::ofstream bufferFile2(...);
if (lpvAudioPtr1 != nullptr) {
bufferFile1.write((char*) lpvAudioPtr1, dwAudioBytes1);
bufferFile1.flush();
}
if (lpvAudioPtr2 != nullptr) {
bufferFile2.write((char*) lpvAudioPtr2, dwAudioBytes2);
bufferFile2.flush();
}
Now, if the hook worked perfectly, I would have the following raw audio (in audacity)
You would also argue that the buffer might not be suitable for listening, but, I will fix it some day.
So, let me explain what the bug exactly is about.
If you looked up the documentation of DetourAttach
, you would see:
LONG DetourAttach(
_Inout_ PVOID * ppPointer,
_In_ PVOID pDetour
);
Isn’t it weird that it needs a double pointer void**
instead of a void*
?
Now, DetourAttach
modifies the pointer of the target function we have. Basically DetourAttach
works like this:
If we are attaching to a function which looks like this:
# hook_function()
mov rax, rax
mov rbx, rbx
mov rcx, rcx
mov rdx, rdx
Attaching to the function would make it to:
# hook_function()
jmp detour_body ; overwrites a few instructions (copies them somewhere else)
mov rbx, rbx
mov rcx, rcx
mov rdx, rdx
...
And the detour would look like:
# detour_body()
inc rax
jmp back_to_hook_function # this is not call
And the overwritten parts would look like:
# overwritten_code()
mov rax, rax ; overwritten
jmp hook_function_after_detour
We can choose to call or not call the original function inside the detour body. While doing so, if we called the function
by its original pointer, it would recursively call itself. Therefore, we should use the pointer which we used
in the DetourAttach
function because it would point to the overwritten part of the code. If we used the original
pointer, we would have something like this:
hook_function()
detour_body()
├── hook_function()
└── detour_body()
├── hook_function()
└── detour_body()
├── hook_function()
└── detour_body()
(Repeats infinitely until disk runs out of free space)
Note that the transfer of control from hook_function()
to detour_body()
is actually a jmp
and not a call
which means it would go for infinite times.
Therefore, in the detour body, always use the pointer which points to the overwritten_code(...)
like:
volatile HRESULT __cdecl hook_unlock(IDirectSoundBuffer* self, LPVOID lpvAudioPtr1, DWORD dwAudioBytes1, LPVOID lpvAudioPtr2, DWORD dwAudioBytes2 ) {
if (lpvAudioPtr1 != nullptr) {
bufferFile1.write((char*) lpvAudioPtr1, dwAudioBytes1);
bufferFile1.flush();
}
if (lpvAudioPtr2 != nullptr) {
bufferFile2.write((char*) lpvAudioPtr2, dwAudioBytes2);
bufferFile2.flush();
}
// return self->Unlock(lpvAudioPtr1, dwAudioBytes1, lpvAudioPtr2, dwAudioBytes2); WRONG
return unlock_fn(self, lpvAudioPtr1, dwAudioBytes1, lpvAudioPtr2, dwAudioBytes2);
}
I hope you have learnt something about hooking after reading this article. Have a nice day!