Starcraft 2 Anti-Debugging
1.0 Introduction
I have always admired and to some extent maybe even adored Blizzard as a company. The first Starcraft was released back in 1998, and the latest patch was release in the beginning of last year. That's twelve years after the game was first released. Their maintenance seems to be endless, and their code quality has always been exceedingly high. Another admireable thing about Blizzard is that they have never had any packers, obfuscators or anti-debugging crap in their products. Instead relying exclusively on Warden, which is an ingenious anti-hacking system.
That was until the release of Starcraft II, which introduces some immensely dirty hacks.
2.0 Anti-debugging logic in Starcraft II
Rant rant!
First and foremost they introduced heavy obfuscation to "protect" their binaries (battle-net.dll). Obfuscation is (imho) a waste of money. It decreases performance and stability of the product, and offers nothing in return. Any semi-competent reverse engineer will be more or less unaffected by it. The time frame for achieving the desired results may expand slightly, but that is about it. This is something I would expect a seemingly intelligent company to realize.
Secondly, and this is where it get really dirty, they added circular debugging. The NT kernel only supports one debugger per debuggee. That is, a processes can only be debugged by one debugger at the time. So, during a crazy mescaline trip and with zero research, the Blizzard anti-hacking committee decided that,
- "Hey! Lets utilize this one-debugger-per-debuggee concept"
- "Yes! No one will ever be able to debug us!"
------------------- -----------------
| | --- Debug --> | |
| SC2.exe (parent) | | SC2.exe (child) |
| | <-- Debug --- | |
------------------- -----------------
2.1 Why circular debugging is bad
It so bad it got its own chapter. At first glance, it may seem like a perfectly good solution to prevent ill-minded people from debugging your software. However, it is not.
Badness 1, upon receiving a debug event (an exception, a new thread, a new module, etc, etc) the NT kernel will suspend the entire process. Now what would happen if both processes received a debug event at the same time? Both processes would be suspended, and both processes would depend on each other to continue. The end.
Some might argue that,
- "Well, as long as they make sure never to raise concurrent debug events, they are safe
and the circular debugging defense hence rocks!"
Well, no. Blizzard can never guarantee that such a scenario won't arise and hence,
it is a really flawed design. Imagine for instance if a third (none Starcraft related process)
decides to install a DLL into every running process.
As of writing this I realize that I am not sure if threads running with NtSetInformationThread(ThreadHideFromDebugger); are suspended. No Starcraft II threads set this, but doing so might be a dirty fix to said problem. Using undocumented APIs is really not a good option though and should be avoided at all cost. I guess it doesn't matter. Err... let us get back on track.
Badness 2, it may cause the infamous blue screen of death. I am not sure if this has actually been made public anywhere. I discovered it about a year ago at work, when doing research on the debugging internals of NT kernel. It turns out that circular debugging may cause BSOD under several circumstances. Unfortunately, I can only seem to remember one.
It seems to have been fixed for Win7, but I tested it just now on xpsp3, and it "worked" like a charm. Circular debugging POC.
For obvious reasons, using techniques that may randomly BSOD is really bad. Really bad.
3.0 How to bypass the Anti-Debugging in Starcraft 2
Bypassing the Starcraft II anti-debugging protection proved to be kind of simple. I wanted to remove it as generically as possible, no on-disk patches or version dependent logic.
I decided to inject a DLL which then would "simulate" the child process, and thus removing the dependency for the child debugger (and debuggee). This allowing me to debug Starcraft 2 instead.
3.1 Writing a custom launcher
Since the circular debugging is installed early in the Starcraft 2 initialization sequence, and, the DLL has to be injected before this, I decided to write a custom launcher. When launched, SC2.exe (the Starcraft 2 executable) checks for a memory map created by the launcher (0x10000 bytes). If this memory map is not present, or contains invalid data, SC2 will abort execution. So before creating the SC2 process, the map "StarCraft II IPC Mem" has to be created. Format of the map is listed below.
typedef struct __tag_sc2_ipc
{
/* 0x00 */ uint32_t nCounter[3];
/* 0x0C */ uint32_t nScPid;
/* 0x10 */ uint32_t nUnknown0[4];
/* 0x20 */ uint32_t nLaunchType;
/* 0x24 */ uint32_t nScLaunched;
/* 0x28 */ uint32_t nLauncherPid;
/* 0x2C */ uint32_t nUnknown1[8];
/* 0x4C */ uint32_t nUnknown2;
/* 0x50 */ ...
}
SC2_IPC;
For details on how to initialize the map, see the source code.
3.2 Injecting the DLL
Nothing special here. Simply inject the DLL, just added this section for completeness sake =)
[+] DLL injection
[*] Create suspended SC2 process (AFTER the map is initialized)
[*] Write infinite loop at entry point of SC2 (0xEB, 0xFE)
[*] Resume process
[*] Wait until entry point is reached
[*] Suspend process
[*] Restore original entry point
[*] Get the remote address (in case of ASLR) of LoadLibraryW()
[*] Create a remote thread to load the library
[*] Resume process
4.0 The DLL
Starcraft II will create the child processes using CreateProcess(..., DEBUG_PROCESS | CREATE_SUSPENDED); So, the first thing done in the DLL is to hook (lazy hot-patching :)) CreateProcessW(). From here, I return simulated (or dummy if you will) process and thread data. Next I hook all debugging functions used in SC2; if called with simulated handles, the appropriate action is taken.
4.1 SuspendThread and ResumeThread
To resume the process after creating it with CREATE_SUSPENDED, SC2 will need ResumeThread. It also makes heavy use of SuspendThread. I added a tiny reference counter in case later versions sanitize the value returned. Other than that, they don't do anything.
4.2 WaitForDebugEvent and ContinueDebugEvent
Once the child SC2 process has been created and resumed, SC2 enters a for(;;) { WaitForDebugEvent() ContinueDebugEvent(); } loop. It doesn't seem to care too much about what data is actually returned from WaitForDebugEvent(). I simulate the creation event and the system breakpoint event since those are the once SC2 seem to care for. After that, all that is done is to honor the dwTimeout parameter with a Sleep and returning FALSE.
SC2 always seem to send DBG_CONTINUE to ContinueDebugEvent().
4.3 GetThreadContext and SetThreadContext
Called once for every exception event. All SC2 does though is to go SetThreadContext(GetThreadContext()).
4.4 VirtualQueryEx and VirtualProtectEx
When a condition is reached (possibly the system breakpoint event), SC2 starts to enumerate all memory regions in the debuggee image (starting from the address returned in the create process event). I simply substitute the simulated process handle with GetCurrentProcess() since, well, they are the same image. For each enumerated memory region, SC2 changes the page protection to PAGE_EXECUTE_READWRITE.
What SC2 is actually trying to do is to copy over debugger code into the debuggee.
4.5 ReadProcessMemory and WriteProcessMemory
Used to copy code into the debuggee.
4.6 Process32FirstW and CheckRemoteDebugger
When SC2 is under the impression that he (she?) has successfully copied over all the code into the debuggee, he will suspend execution for a while, then call Process32FirstW(). SC2 then continues to search through all processes for one whos parentpid equals the pid of SC2. The pid of that process is then compared to the pid returned from CreateProcessW. Since SC2 doesn't care for any other processes, I simply hooked Process32FirstW and made it return the desired values.
Next, SC2 calls OpenProcess() on the parentpid. Since this is the actual PID of SC2 and not a simulated one, there is no need to hook OpenProcess(). If OpenProcess() is sucessful (which it is), SC2 make sure that the process is debugged using CheckRemoteDebugger().
5.0 EOF
Check out the source
Written by Pellsson, 2010-03-08