如何让指定 Windows 程序崩溃

11 篇文章 1 订阅

下文中两种方法的源码:

https://gitee.com/langshanglibie/crasher

https://gitee.com/langshanglibie/SetContextCrasher

一、为何要把人家搞崩溃呢

看到这个标题,大家可能觉得奇怪,为什么要让指定程序崩溃呢,难道是想作恶吗?😓

哈哈,绝对不是,真实原因是这样的。如果大家用过 Windows 电脑,可能见过类似下面这样的软件崩溃提示框。

QQ
微信
迅雷
WPS​​​
百度网盘
百度如流

Windows 上软件通常会做一个崩溃捕捉的功能。在软件发生崩溃时,弹出个友好的提示框,先道个歉(的确是自己错了嘛,对吧),然后提示框关闭的时候,还会主动帮用户重启软件,多贴心。同时,也会将一些崩溃信息上传到服务端,服务端对这些崩溃进行分类,然后按照崩溃数目对崩溃类型进行排序,最后开发人员按顺序修复。因此,当我们开发、测试崩溃捕捉功能时,自然就经常需要让自己的程序发生崩溃。

为了达到这个目的,一般的做法是临时在程序中加入引发异常的代码。这种做法只在临时测试版本中有效,在正式版本中无法进行验证。另外一种方法是加入一个隐藏开关的逻辑,但这样会带到正式版本中,有风险。另外,我们也需要参考下别人的这个功能,所以也希望可以随心所欲地让别人的软件崩溃,但是别人的软件我们可没法修改代码。所以,最好的方法是在不修改任何代码的情况下,能够让指定程序崩溃,指哪打哪。下面就介绍两种这样的方法。

二、代码注入法

试想,如果我们可以将一段有问题、会引起崩溃的代码,注入到指定进程并运行,这样,此进程必崩无疑。

注入代码的方法有很多种,比如全局钩子、APC 注入、远程线程等。这里,我们介绍经典的远程线程注入的方法。

顾名思义,远程线程就是指一个进程在另一个进程的虚拟地址空间中创建线程,然后运行指定代码。当然,这里我们为了搞崩溃别的进程,这个指定的代码就可以是一段会引起异常的代码。

创建远程线程可以用 CreateRemoteThread 函数,原型如下。

HANDLE 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
);

我们来看看关键参数 lpStartAddress,这个参数指定远程线程的入口点地址,这个地址必须是在目标进程的地址空间中。在本文的特殊需求下,我们并不需要传一个实际可执行的地址,只需要传入 0 即可。这样的话,该线程会尝试从 0 地址处执行。

Windows 进程的虚拟地址空间中有一个特殊的区间,叫空指针赋值分区,从地址 0x00000000 到 0x0000FFFF。如果线程试图读写位于这一分区内的地址,就会引发访问违规,随即导致程序崩溃——哈哈,这就达到了我们的目的。

主要流程

核心代码

constint pid = <目标进程 PID>;
const DWORD desiredAccess = PROCESS_CREATE_THREAD |
                            PROCESS_QUERY_INFORMATION |
                            PROCESS_VM_OPERATION |
                            PROCESS_VM_WRITE |
                            PROCESS_VM_READ;
// 打开进程,获得句柄
const HANDLE hProcess = OpenProcess(desiredAccess, FALSE, pid);
// 创建远程线程
CreateRemoteThread(hProcess, nullptr, 0, 0, nullptr, 0, nullptr);
// 关闭进程句柄
CloseHandle(hProcess);

三、修改指令指针寄存器法

这个方法更直接更暴力,它劫持 CPU 的指令指针寄存器 RIP 或 EIP,使其直接指向 0 地址处。

RIP 是 x64 架构中的 64 位寄存器,存储着 CPU 要执行的下一条指令的地址,EIP 是对应的 x86 架构中的 32 位寄存器。如果下一条指令从 0 地址处开始执行,同前一个方法一样,必然引起访问违规,进而导致崩溃。

指令指针寄存器存放在线程的 CONTEXT 结构体中,可以用 GetThreadContext 获取指定线程的 CONTEXT,然后修改指令指针寄存器的值后,再通过 SetThreadContext 替换原始的 CONTEXT。

主要流程

核心代码

// 打开目标进程的一个线程,获得线程句柄,threadId 为该线程的 id
const DWORD desiredAccess = THREAD_GET_CONTEXT |
                            THREAD_SET_CONTEXT |                 
                            THREAD_QUERY_INFORMATION;
const HANDLE hThread = OpenThread(desiredAccess, FALSE, threadId);
// 获取线程 CONTEXT 之前,必须先挂起该线程
SuspendThread(hThread);
// 获取线程 CONTEXT
CONTEXT context = {0};
context.ContextFlags = CONTEXT_ALL;
GetThreadContext(hThread, &context);
// 根据目标进程位数的不同,修改 RIP 或 EIP
#ifdef _WIN64
context.Rip = 0;
#else
context.Eip = 0;
#endif
// 替换 CONTEXT
SetThreadContext(hThread, &context);
// 恢复线程
ResumeThread(hThread);
// 关闭线程句柄
CloseHandle(hThread);

四、效果演示

说了这么多,咱们实践一把,以微信为例,使用代码注入法,传入微信进程 ID,可以看到微信瞬间崩溃,弹出了崩溃提示框。😂😂😂

五、总结

通过这两种方法,我们就可以在不修改任何代码的前提下,做到让指定程序崩溃,方便了开发和测试工作。顺带也可以感受到 Windows 上一个程序的能力之大、破坏力之强,这也是黑客比较青睐 Windows 的原因之一。

  • 29
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值