
有一类崩溃很特别:文件一个不少、依赖完整、版本也匹配,程序却在运行到某一步时直接崩掉。 它也是用 DLL修复工具 扫不出任何异常的典型情况。
崩溃的位置往往很怪——不在某个模块里,而是指向一片说不上来的地址。
这一类的根因不在依赖,而在内存权限:代码能执行的地方,比你以为的窄得多。
现代系统的内存被划成一个个页,每页有独立的权限:可读、可写、可执行。
关键在于:这三项是分开授予的。
模块被映射进来时,它的不同部分会被赋予不同权限:
所以"这块内存我能写"和"这块内存我能执行"是两件独立的事。
这条规则的反面就是它要防的东西:把一段机器码写进数据里,然后跳过去执行。
按老习惯,这是可行的——写进去,跳过去,代码就跑起来了。而在现在的系统上,跳过去的那一刻会被直接拒绝:那一页的权限里没有"可执行"。
表现就是一次访问冲突,而崩溃地址落在一片栈或堆的范围内——看起来跟任何模块都不沾边。
这解释了一类很有特点的故障:
因为它要的不是文件,是权限。
这条默认规则防住了"把数据当代码"这类手法,但它也切到了两类合理场景:
第一类,需要动态生成代码的运行时。
脚本引擎、虚拟机、以及各种即时编译机制,它们的正常工作方式就是在运行时产生机器码再执行。
它们不能靠"关掉全局保护"来解决——正确做法是显式地把那片内存标记为可执行,而不是让整个进程都放开。
第二类,老程序与保护方案。
早年的程序和保护壳往往默认"写进去就能执行"。系统策略一变,它们就崩——而这类程序通常已经不再更新,只能靠系统侧给它开例外。
这个取舍值得记住:默认拒绝的代价是"一些老程序跑不了",换来的是"一大类攻击手法直接失效"。这是一次有意为之的交换。
三个线索:
线索一,看崩溃地址落在哪。
线索二,看它对系统的敏感程度。
同一份程序,老系统正常、新系统崩——这类"随系统版本变化"的特征是重要提示。
线索三,看这个程序是否需要动态生成代码。
如果它自带脚本、插件编译、表达式求值这类能力,那么它可能存在需要显式声明的部分。
这一层的问题和依赖完全无关(所以 DLL修复工具 在这里没有用武之地):
所以按名称查找、补齐文件的思路在这里不管用——DLL修复工具 这类工具扫描的结果通常是"一切正常",而这个"正常"说的是文件层面,与"能不能执行"无关。
判断这个区别的意义在于:它能把"反复补文件"这条死路提前终止,直接转到正确的方向上。
第一条,优先为个别程序设置例外,而不是全局关闭。
这是最小代价原则。关掉全局保护等于把整台机器的防护一起降下来,而需要配合的通常只有那一个程序。
第二条,如果程序是自己维护的,就改代码。
需要动态生成代码的地方,显式声明那片内存的用途;不需要的地方,就不要指望数据区能执行。
第三条,注意声明范围。
声明可执行的范围要尽量小,用完能撤销就撤销。"整块放开、一直放开"是最省事也最危险的写法。
第四条,别把"可写"当成"可执行"。
这两件事在权限上是分开的,不要依赖任何"写了就能跑"的隐含假设。
关于"文件都在却在某一步崩了",记住四条:
这里的迁移经验是一个设计取向:默认拒绝 %2B 显式声明。它让"不该被执行的代码"根本没有机会执行,代价是兼容性——但兼容性问题的正解是为个别对象显式声明,而不是把整套默认规则关掉。这个思路在很多安全策略上都是通用的。
https://www.ijinshan.com/functions/repairdll.html?channel=4086
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。