Keeping in line with our previous post “How to prevent users from killing your service or process” and continuing down the road of securing our service this post looks at the prevention of debugging. There are a lot of anti-debugging posts out there, most of them written in C++ and assembly and are therefor of little use to the managed world. One of the most complete I’ve seen is from Symantec “Windows Anti-Debug Reference | Symantec Connect Community” and an interesting one on stackoverflow “What is your favourite anti-debugging trick?“. These both fall short on providing any solution that is both easy to implement and built with managed code.

Let’s face it nothing is going to be bullet proof in this arena, and certainly not the solution I’m going to suggest. Yet it is easy and will prevent someone from attaching a debugger after the program is running. I’m really not concerned with trying to prevent a debug-session from startup. Why? Because you can’t. The debugger can jump past any code you have that tries to verify that a debugger is not currently active. Besides this it provides me very little value, I want to protect an actively running process (a service). If the user has the rights to stop it in the first place (i.e. they are an admin) then there isn’t anything I can do to stop them from debugging.

What I want is to prevent someone from attaching a debugger to this service to protect potentially sensitive information. How? Well as it turns out the easiest way to prevent a debug session from starting is with a debug session. So what we need is to debug ourselves! Oh wait you can’t :( but what you can do is easily spawn another process to debug this process while we debug that new process. This reciprocal or circular debug session will prevent either process from being debugged. Further, any attempt to kill either process will immediately terminate the other process thus thwarting an effort to kill one debugger so that you can attach one.

To accomplish this in managed code we have two choices, either using the managed debugger API or the native win32 debugger API. It turns out that the managed debugger is excessively complicated and requires an extraordinary amount of COM code to pull it off (see the mdbg sample) So I chose to go with a raw win32 debug session and see if we could pull that off easily with a few PInvoke calls. Sure enough, this is really easy.

So let’s jump in and take a look at the debugging API calls we are going to need…

const int DBG_CONTINUE = 0x00010002;
const int DBG_EXCEPTION_NOT_HANDLED = unchecked((int)0x80010001);

enum DebugEventType : int
{
    CREATE_PROCESS_DEBUG_EVENT = 3, //Reports a create-process debugging event. The value of u.CreateProcessInfo specifies a CREATE_PROCESS_DEBUG_INFO structure.
    CREATE_THREAD_DEBUG_EVENT = 2, //Reports a create-thread debugging event. The value of u.CreateThread specifies a CREATE_THREAD_DEBUG_INFO structure.
    EXCEPTION_DEBUG_EVENT = 1, //Reports an exception debugging event. The value of u.Exception specifies an EXCEPTION_DEBUG_INFO structure.
    EXIT_PROCESS_DEBUG_EVENT = 5, //Reports an exit-process debugging event. The value of u.ExitProcess specifies an EXIT_PROCESS_DEBUG_INFO structure.
    EXIT_THREAD_DEBUG_EVENT = 4, //Reports an exit-thread debugging event. The value of u.ExitThread specifies an EXIT_THREAD_DEBUG_INFO structure.
    LOAD_DLL_DEBUG_EVENT = 6, //Reports a load-dynamic-link-library (DLL) debugging event. The value of u.LoadDll specifies a LOAD_DLL_DEBUG_INFO structure.
    OUTPUT_DEBUG_STRING_EVENT = 8, //Reports an output-debugging-string debugging event. The value of u.DebugString specifies an OUTPUT_DEBUG_STRING_INFO structure.
    RIP_EVENT = 9, //Reports a RIP-debugging event (system debugging error). The value of u.RipInfo specifies a RIP_INFO structure.
    UNLOAD_DLL_DEBUG_EVENT = 7, //Reports an unload-DLL debugging event. The value of u.UnloadDll specifies an UNLOAD_DLL_DEBUG_INFO structure.
}

[StructLayout(LayoutKind.Sequential)]
struct DEBUG_EVENT
{
    [MarshalAs(UnmanagedType.I4)]
    public DebugEventType dwDebugEventCode;
    public int dwProcessId;
    public int dwThreadId;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1024)]
    public byte[] bytes;
}

[DllImport("Kernel32.dll", SetLastError = true)]
static extern bool DebugActiveProcess(int dwProcessId);
[DllImport("Kernel32.dll", SetLastError = true)]
static extern bool WaitForDebugEvent([Out] out DEBUG_EVENT lpDebugEvent, int dwMilliseconds);
[DllImport("Kernel32.dll", SetLastError = true)]
static extern bool ContinueDebugEvent(int dwProcessId, int dwThreadId, int dwContinueStatus);
[DllImport("Kernel32.dll", SetLastError = true)]
public static extern bool IsDebuggerPresent();

The structure DEBUG_EVENT above is actually only 96 bytes in total and the bytes 12 to 96 are actually a union. I did not need any real details about the specifics so I chose to use slam a blob here and not worry about it. The byte array could be reduced in theory to 84 bytes but I really didn’t care since we are only going to create a single one of these. The rest is pretty strait-forward PInvoke junk, nothing fancy.

Using the methods above to debug a process looks something like the following code:

// Start a thread to perform the debug loop
new Thread(DebuggerThread) { IsBackground = true, Name = "DebuggerThread" }
    .Start(processId);
// Debugging thread main loop
static void DebuggerThread(object arg)
{
    DEBUG_EVENT evt = new DEBUG_EVENT();
    evt.bytes = new byte[1024];
    // Attach to the process we provided the thread as an argument
    if (!DebugActiveProcess((int)arg))
        throw new Win32Exception();

    while (true)
    {
        // wait for a debug event
        if (!WaitForDebugEvent(out evt, -1))
            throw new Win32Exception();
        // return DBG_CONTINUE for all events but the exception type
        int continueFlag = DBG_CONTINUE;
        if (evt.dwDebugEventCode == DebugEventType.EXCEPTION_DEBUG_EVENT)
            continueFlag = DBG_EXCEPTION_NOT_HANDLED;
        // continue running the debugee
        ContinueDebugEvent(evt.dwProcessId, evt.dwThreadId, continueFlag);
    }
}

Frankly I had no idea how trivial this was to do. Once I had this working I was able to quickly build a method that takes my program arguments and detects if it is a parent or child process and act appropriately. For the child we need only debug our parent process, for the parent we need to both spawn the child and debug it. Ideally this would be done as a single step since it is possible to start a process as a debugee; however, I didn’t bother to figure out how to do that. If you know and don’t mind sharing please drop a comment. Since I do know how to start a process and I know how to debug an active process I chose that approach. It doesn’t seem like it would be any more or less secure, but I could be wrong on that point. Anyway the following is what my process start-up looks like…

static void Main(string[] args)
{
    NativeDebug.DebugSelf(args);
    ... stuff ...
}
public static void DebugSelf(string[] args)
{
    Process self = Process.GetCurrentProcess();
    // Child process?
    if (args.Length == 2 && args[0] == "--debug-attach")
    {
        int owner = int.Parse(args[1]);
        Process pdbg = Process.GetProcessById(owner);
        new Thread(KillOnExit) { IsBackground = true, Name = "KillOnExit" }.Start(pdbg);
        //Wait for our parent to debug us
        WaitForDebugger();
        //Start debugging our parent process
        DebuggerThread(owner);
        //Now is a good time to die.
        Environment.Exit(1);
    }
    else // else we are the Parent process...
    {
        ProcessStartInfo psi =
            new ProcessStartInfo(Environment.GetCommandLineArgs()[0], "--debug-attach " + self.Id)
                {
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    ErrorDialog = false,
                    WindowStyle = ProcessWindowStyle.Hidden
                };
        // Start the child process
        Process pdbg = Process.Start(psi);
        if (pdbg == null)
            throw new ApplicationException("Unable to debug");
        // Monitor the child process
        new Thread(KillOnExit) {IsBackground = true, Name = "KillOnExit"}.Start(pdbg);
        // Debug the child process
        new Thread(DebuggerThread) {IsBackground = true, Name = "DebuggerThread"}.Start(pdbg.Id);
        // Wait for the child to debug us
        WaitForDebugger();
    }
}
static void WaitForDebugger()
{
    DateTime start = DateTime.Now;
    while (!IsDebuggerPresent())
    {
        if ((DateTime.Now - start).TotalMinutes > 1)
            throw new TimeoutException("Debug operation timeout.");
        Thread.Sleep(1);
    }
}
static void KillOnExit(object process)
{
    ((Process)process).WaitForExit();
    Environment.Exit(1);
}

So we’ve written about 100 lines of code or so and spent almost no time at all getting it up and running, a good ROI for this kind of stuff. It worked quite well even running under the NETWORK SERVICE account as a service. It certainly stops me from debugging it although I’m certainly not what I’d call an accomplished hacker. Given the time and effort to put this together I’d have to call it a win for active debugger prevention. The down side of course is that there are now two processes running and trying to tell them apart is difficult at first launch.

If you’re going to do something like this, I would suggest adding an ‘if (!IsDebuggerPresent())’ to the else clause of our DebugSelf method above. This would allow you to launch with a debugger but not to attach one at a later time. Have fun with it and as with any code on this site, “Don’t blame me”. I didn’t make you use it ;)

My own intentions for this are to make a reasonable effort to secure a service running in a controlled but insecure environment. I would never ship something like this to a customer and hope you would not either. Preventing a consumer from accessing software they purchased is not at all what I’m after. IMHO if information is on their machine it is theirs to debug and view all they want.

One more thing, in case you are wondering this was not my idea. I read about doing this in a security article some years ago but I can’t seem to locate it.

Comments