如何让指定的程序崩溃

为了验证程序崩溃信息的收集和上报机制,需要让程序崩溃。为了达到这个目的,一般的做法是临时在程序中加入引发异常的代码。这种做法只在开发人员自己编译的版本中有效,在正式版本中无法进行验证。另外一种方法是加入一个隐藏开关,但这样会在正式版本中带上调试代码,也不理想。最好的方法是在不修改任何代码的前提下,也能够让程序崩溃。

这看起来似乎不可能,但实际上是可行的,而且做法简单得让人惊讶。Windows API有一个CreateRemoteThread函数,这个函数可以在指定的进程中创建一个线程,并且能够让这个线程执行任意代码——DLL注入就是通过这种方式实现的。显然,只要用这个函数创建一个会产生异常的线程就可以了。先来看一下这个函数的原型:

1
2
3
4
5
6
7
8
9
HANDLE WINAPI CreateRemoteThread(
_In_ HANDLE hProcess,
_In_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_ LPDWORD lpThreadId
);

值得关注的是hProcesslpStartAddress参数,其它参数的含义可以参考MSDN文档,这里不再赘述。先来看lpStartAddress参数,这个参数指定线程的入口点地址,这个地址必须是在目标进程的地址空间中。在本文的特殊需求下,我们并不需要传一个实际可执行的地址,只需要传入0即可。这样的话,该线程会尝试从0地址执行,这毫无疑问会引发访问违规异常,导致程序崩溃——这就达到了我们的目的。

再来看hProcess参数,这个参数表示目标进程的句柄,这个句柄必须具有以下访问权限:

  • PROCESS_CREATE_THREAD
  • PROCESS_QUERY_INFORMATION
  • PROCESS_VM_OPERATION
  • PROCESS_VM_WRITE
  • PROCESS_VM_READ

有了这些基础知识,我们就可以写一个简单的命令行程序来让指定的进程崩溃了。完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <Windows.h>
#include <iostream>

int main(int argc, char** argv) {

if (argc < 2) {
std::wcout << L"A PID is needed." << std::endl;
return 1;
}

int pid = atoi(argv[1]);
if (pid <= 0) {
std::wcout << L"Invalid PID." << std::endl;
return 2;
}

HANDLE process_handle = OpenProcess(
PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
FALSE,
pid);

if (process_handle == nullptr) {
std::wcout << L"Open process failed. Error: " << GetLastError() << '.' << std::endl;
return 3;
}

HANDLE thread_handle = CreateRemoteThread(process_handle, nullptr, 0, 0, nullptr, 0, nullptr);
if (thread_handle == nullptr) {
std::wcout << L"Create remote thread failed. Error: " << GetLastError() << '.' << std::endl;
return 4;
}

std::wcout << L"Crashed. :)" << std::endl;
return 0;
}

假设这个命令行程序的名称是crasher.exe,目标进程的PID是10032,那么只要像下面这样调用就能让这个进程崩溃:

crasher.exe 10032

如果你的目标进程是一个普通程序,那么本文到这里就结束了。但如果你的目标进程是一个服务程序,情况就有所不同了。CreateRemoteThread有一个限制,它不能跨会话来创建线程。例如,服务程序都在0号会话下运行,而制造崩溃的程序是在当前用户登录的会话下运行(必定不是0号会话),所以CreateRemoteThread的调用会失败。为了解决这个问题,需要让制造崩溃的程序转移到0号会话下运行。可以借助Sysinternals出品的PsExec工具来实现这个目的,只要执行下面的命令行即可:

PsExec.exe -accepteula -s crasher.exe 10032

-accepteula参数表示接受Sysinternals的最终用户许可协议。Sysinternals的工具在首次使用的时候都会弹出最终用户许可协议对话框,使用这个参数可以防止对话框弹出。-s表示在系统会话,也就是0号会话中执行后面的命令。最终效果就是在0号会话中执行了crasher.exe 10032这个命令。