x64dbg 官博翻译
x64dbg的官博质量很高,这篇文章和句柄相关,翻译mark
原文链接 https://x64dbg.com/blog/
昨天我在调试一些程序,在重新启动调试器后,我看见status标签一直卡在初始化。之后便崩溃
重现步骤:
- 加载一些调试器
- 保持单步一段时间
- 按下重新开始
- 重复直到bug出现
现象:
- 标签卡在初始化
- 标签卡在暂停
发现端倪
在获得稳定的重现后,我开始寻找这为什么发生。TaskThread看起来是正确的,但由于WakeUp函数可能失败,我在ReleaseSemaphore上放置了一个断言,这应该能出发TaskThreadl:
template <typename F, typename... Args>
void TaskThread_<F, Args...>::WakeUp(Args... _args)
{
++this->wakeups;
EnterCriticalSection(&this->access);
this->args = CompressArguments(std::forward<Args>(_args)...);
LeaveCriticalSection(&this->access);
// This will fail silently if it's redundant, which is what we want.
if(!ReleaseSemaphore(this->wakeupSemaphore, 1, nullptr))
__debugbreak();
}
我尝试重现bug,不出意外断言命中了。这时,我猜测是内存崩溃,所以我在TaskThread插入了一些调试手段,来在一块安全的内存区域中储存之前的句柄
struct DebugStruct
{
HANDLE wakeupSemaphore = nullptr;
};
template <int N, typename F, typename... Args>
TaskThread_<N, F, Args...>::TaskThread_(F fn,
size_t minSleepTimeMs, DebugStruct* debug) : fn(fn), minSleepTimeMs(minSleepTimeMs)
{
//make the semaphore named to find it more easily in a handles viewer
wchar_t name[256];
swprintf_s(name, L"_TaskThread%d_%p", N, debug);
this->wakeupSemaphore = CreateSemaphoreW(nullptr, 0, 1, name);
if(debug)
{
if(!this->wakeupSemaphore)
__debugbreak();
debug->wakeupSemaphore = this->wakeupSemaphore;
}
InitializeCriticalSection(&this->access);
this->thread = std::thread([this, debug]
{
this->Loop(debug);
});
}
TaskThread 实例现在被初始化然后像这样调用
void GuiSetDebugStateAsync(DBGSTATE state)
{
GuiSetDebugStateFast(state);
static TaskThread_<
6,
decltype(&GuiSetDebugState),
DBGSTATE>
GuiSetDebugStateTask(
&GuiSetDebugState,
300,
new (VirtualAlloc(0,
sizeof(DebugStruct),
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE)
) DebugStruct()
);
GuiSetDebugStateTask.WakeUp(state, true);
}
现在我启动x64dbg并且使用Process Hacker找到_TaskThread6_XXXXXXXX 信号量,记下句柄。然后我重现并且惊讶的发现wakeupSemaphore是0x640,和启动的一样。
但是,当我再次检查检查句柄视图,0x640不再是信号量的句柄,而是一个映射文件
碰一碰运气
感觉显示WinAPI的错误使用,Application Verifier可以检查这类错误,但是我不会用,所以只能自己撸了。
方法很简单
1. 挂钩CloseHandle API
1. 吧当前的信号量句柄保存到一个全局变量中
1. 如果过句柄已经被释放就崩溃
static DebugStruct* g_Debug = nullptr;
typedef BOOL(WINAPI* CLOSEHANDLE)(HANDLE hObject);
static CLOSEHANDLE fpCloseHandle = nullptr;
static BOOL WINAPI CloseHandleHook(HANDLE hObject)
{
if(g_Debug && g_Debug->wakeupSemaphore == hObject)
__debugbreak();
return fpCloseHandle(hObject);
}
static void DoHook()
{
if(MH_Initialize() != MH_OK)
__debugbreak();
if(MH_CreateHook(GetProcAddress(GetModuleHandleW(L"kernelbase.dll"), "CloseHandle"), &CloseHandleHook, (LPVOID*)&fpCloseHandle) != MH_OK)
__debugbreak();
if(MH_EnableHook(MH_ALL_HOOKS) != MH_OK)
__debugbreak();
}
成功吃鸡
bug发生在TitanEngine模块,ForceClose方法被期望于关闭所有Dll句柄
但是为什么信号量句柄回合之前的一个文件句柄相同呢,答案就在以下事件
- LOAD_DLL_DEBUG_EVENT 从链表中获取文件句柄
- LOAD_DLL_DEBUG_EVENT 立即关闭之前的文件句柄在调试会话
- 当调试器第一次中断并且信号量被用之前的文件句柄(现在被关闭了)创建,TaskThread的静态初始化器被调用
- 这一切都运转正常,知道ForceClose方法被调用并且来自于LOAD_DLL_DEBUG_EVENT文件句柄再次被关闭
- TaskThread中断