BlackHat议题解析:Windows程序的数字签名校验“漏洞”
* 本文原创作者:维一零,本文属FreeBuf原创奖励计划,未经许可禁止转载
在今年的黑帽大会上,国外的一个安全研究员展示了如何通过Windows的数字签名bypass对恶意程序代码的检测。
下载大会的该演讲的ppt大概看了一下,报告分为两部分,第一部分展示数字签名的的校验“漏洞”,第二部分展示该作者自己研究实现的一个pe程序加载器,用来配合第一部分的“漏洞“bypass杀毒软件对恶意程序的检测。
本文重点在于第一部分的这个数字签名校验”漏洞“,通过回顾分析数字签名的校验来阐述这个”漏洞“的原理。
数字签名与数字证书
讲这个”漏洞“之前先讲一下数字签名的原理,理解一般的数字签名验证过程。下面是数字签名的相关概念和验证过程:
数字签名:对一段数据摘要使用私钥进行加密,公钥进行解密校验 数字证书:对数字签名的解密公钥和身份信息使用CA的私钥进行加密,系统信任的CA公钥进行解密
如上图所示,数字签名用来保护所有者的数据,所以从这些数据出发,所有者签发数据的数字签名过程是要先计算这些数据的摘要(也就是对这些数据做个hash运算,目前由于SHA-1开始不安全,正在逐渐换成SHA-2算法)。
然后使用所有者的私钥进行加密(目前主流还是RSA,也有一些ECC或其他算法),加密后的数据带上所有者的信息按照一定的标准格式组织好其实就是所谓的数字签名。
而接收方(使用者)要验证这个数字签名,一般通过数字证书,因为其中包含两个重要的信息,一个就是用于解密数字签名的公钥(通过这个公钥才能正确解密出数据所有者事先加密好的数据摘要值,用于验证比对数据的一致性),另一个就是指明了这个公钥的所有者的信息(当然要和数字签名所有者的信息一致)。
而数字证书其实是由第三方的可信机构颁发给数字签名所有者的一份数据文件,生成算法和数字签名类似,只不过它是把所有者的公钥和身份这些固定信息数据进行了加密返回给了所有者,此时所有者的身份正常是得到了可信机构的认证了。
最后一个环节,数字签名、数字证书通常都是附加到所有者的数据后面一起传送给了使用者,但是如何解密数字证书来进行验证呢?
我的理解是,这个要依托于系统,比如windows,他们会维护一批可信机构的列表,在验证证书的时候,就会先从这个列表里校验和获取证书签发机构的身份和公钥,并返回到用户系统进行存储和使用。
正是通过这样一整套的相对完善的机制,最终比较靠谱的保证了接收数据的有效性。而本文所指的Windows程序的数字签名,其实就是指所有者要保护的数据是一些Windows下的可执行程序这样一种情况。
可被利用的“漏洞”
从上面的签名校验流程看,由于整个过程涉及环节比较多,相应的每个环节都存在有被攻破的可能性,比如hash碰撞、根证书伪造等,但是本文不涉及此类问题。
实际上,数字签名仍然是保护数据有效的一种可靠的途径,只要操作得当,算法强度足够,被攻破的可能性还是比较低。而本文所说的“漏洞”,也不是上面整个验证过程的漏洞,下面就来看一看这个“漏洞”是什么意思吧。
找一个带有数字签名的程序来实验,就以微软官方的库文件msvcr100.dll为例。
右键点击文件属性,可以看到有一个数字签名的标签,依次点击可以查看到下面的签名有效状态,表示这个程序的数字签名验证成功,“程序数据没有被篡改”(这里加引号说明理解需要谨慎,详看下文)。
然后修改这个文件,比如在尾部随便添加几个字节的数据,再次查看数字签名的状态后如下图所示,说明系统检测到文件被篡改,数字签名验证为无效。
这就给我们造成一种感觉,数字签名真的可以保证数据不被篡改,只要数据受到篡改,就会被系统的验证机制检测到并提示签名无效。
然而,这种感觉其实并不靠谱,因为我们实际上只是对签名文件添加了一些数据,并没有修改到程序的原始数据区域的部分,理论上只要系统能够找到程序的数字签名并通过上述的验证流程,那么数字签名就仍是有效的,受保护数据没有遭到篡改。
那么,有没有办法让这个添加了数据的文件去正确识别数字签名并显示签名有效呢,答案是肯定的,而这也正是本文所指的“漏洞”。
这里直接给出这个“漏洞”的信息:对于一个Windows的可执行程序,签发数字签名的时候需要计算的数据摘要并不会是程序文件的全部数据,而是要排除一些特定区域的数据。
而这些区域当然和PE文件结构有关,具体地,不管是签发时还是校验时计算的hash都会排除一个checksum字段、一个Security数据目录字段以及数字签名证书部分的数据。至于原因,当然是为了合理地组织pe程序的数字签名,符合pe文件格式的标准了。
下面是三个字段的相关信息:
struct IMAGE_NT_HEADERS
由于计算hash时排除以上3个相关的数据,故这三个字段本身就是Windows系统留给用户可以操作修改的区域。
而实际上,要让上述在尾部添加数据的文件显示数字签名有效,即Windows系统对数字签名可正确识别,只需要更新Security.Size这个字段一致,且满足添加的数据长度为8的整数倍即可:
由此可见,数字签名状态正常,并不意味着带签名的程序就是完整未被篡改过的,通过修改程序的字段数据可以实现在带签名的程序后面添加任意的数据且签名状态验证正常。
上面的三个字段如果同时修改,即checksum、Security.Size和dwLength都正常,除非很严格的去对数字签名的数据部分进行检查,否则比较难以检测出带签名的程序文件是否被恶意篡改添加了非法数据。
而这一点,就构成所谓的“漏洞”,能够成为很多恶意程序的良好的藏身之处,借此bypass很多防火墙、反病毒等软件的检测。
比如黑客可以将恶意代码嵌入带有正常签名的程序后面来绕过一些防护系统的检测,从而为后续的一些攻击行动起到一个很好的掩护作用。
另一种Windows程序的签名“漏洞”
经过以上的分析,所谓的签名“漏洞”,其实是数字签名文件组织上的概念误区,导致将签名的正常状态和签名文件的完整性混淆在一起。这里介绍一种更为方便的Windows程序,能够以更简单的方式实现以上同样的效果。
打开Windows系统文件夹“C:/Windows/Installer”,在这个目录下可以找到一些具有微软数字签名的msi/msp格式的文件。
我们知道msi是微软格式的安装包,点击运行后系统会自动调用Msiexec.exe来启动程序,而这种程序相比于PE程序这个文件格式来说,对数字签名的识别好像更为简单,也没有PE程序结构的特殊限制。
于是,随便往带有签名的msi文件尾部添加一些数据,而无需再修改文件的其他数据,msi的数字签名状态仍然校验正常。比如我系统找到的下面一个文件:
可以看到,对于msi签名文件,随意添加数据签名状态不变,说明即使被添加了非法数据,系统也能顺利找到msi程序的数字签名进行校验,而由于msi程序的原始保护部分没有受到篡改,所以数字签名状态验证正常。
总结
通过本文的分析,实际上这个程序签名的“漏洞”并算不上通常意义上的漏洞,但是却因为本身比较容易被概念混淆,使用可能没有充分得当,导致可能存在一些比较严重的可利用环节。
所以本文的结论就是:程序的数字签名只能用于验证程序的代码和数据是否被篡改,而无法用于验证程序文件本身是否被篡改。
* 本文原创作者:维一零,本文属FreeBuf原创奖励计划,未经许可禁止转载
- .Net 转战 Android 4.4 日常笔记(4)--按钮事件和国际化
- .Net 转战 Android 4.4 日常笔记(3)--目录结构分析
- .Net 转战 Android 4.4 日常笔记(2)--HelloWorld入门程序
- 读书笔记(二)对象激活和上下文
- 程序员的噩梦有哪些?除了改需求,还有这些…
- UC Berkeley提出新型分布式执行框架Ray:有望取代Spark
- 卡奇话爬虫使用方法以及下载地址
- flash读取XML 背景自动适应大小
- 记录一个发邮件的cs文件
- ASP.NET MVC5+EF6+EasyUI 后台管理系统(56)-插件---单文件上传与easyui使用fancybox
- xml-rpc(2)-first demo_v2
- xml-rpc(1)-first demo
- ASP.NET MVC5+EF6+EasyUI 后台管理系统(55)-工作流设计-表单布局
- 网站源文件被注入了iframe代码—ARP欺骗的木马病毒攻击
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 基于laravel Request的所有方法详解
- 浅谈thinkphp的nginx配置,以及重写隐藏index.php入口文件方法
- php图片裁剪函数
- Laravel自定义 封装便捷返回Json数据格式的引用方法
- Laravel模糊查询区分大小写的实例
- laravel实现一个上传图片的接口,并建立软链接,访问图片的方法
- Laravel中validation验证 返回中文提示 全局设置的方法
- laravel5表单唯一验证的实例代码
- 实现laravel 插入操作日志到数据库的方法
- laravel validate 设置为中文的例子(验证提示为中文)
- Laravel 使用查询构造器配合原生sql语句查询的例子
- php面试实现反射注入的详细方法
- laravel框架 api自定义全局异常处理方法
- laravel实现于语言包的完美切换方法
- PHP校验15位和18位身份证号的类封装