sp1d3r

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) Raw audio

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!

comments powered by Disqus