检测常见ASP.NET配置的安全漏洞WEB安全(精选6篇)

2023-06-12| 编辑: 佚名| 查看: 133 |原作者: 叶红雨|来自: 衙媒网

关于检测常见ASP.NET配置的安全漏洞WEB安全(精选6篇)这个问题?下面小编为大家介绍下“检测常见ASP.NET配置的安全漏洞WEB安全(精选6篇)”的详细内容:【导读】感谢网友“痕鸠”参与投稿,下面小编给大家带来检测 ...

  关于检测常见ASP.NET配置的安全漏洞WEB安全(精选6篇)这个问题?下面小编为大家介绍下“检测常见ASP.NET配置的安全漏洞WEB安全(精选6篇)”的详细内容:

  【导读】感谢网友“痕鸠”参与投稿, 下面小编给大家带来检测常见ASP.NET配置的安全漏洞WEB安全(共6篇), 希望能帮助到大家!

篇1:检测常见ASP.NET配置的安全漏洞WEB安全

  Troy Hunt文章的分析数据来自他所开发的一个简易网站扫瞄服务--ASafaWeb, Automated Security Analyser for ASP.NET Websites, 使用者只要提供Internet上公开ASP.NET网站的URL, ASafaWeb会发出几个Request, 藉此检查网站是否存在一些常见的安全漏洞。

  由今年1至3月扫描过的网站记录, 排除掉ASafaWeb测试网站及非ASP.NET网站后共有7,184份检测结果, Hunt做出简单的统计。虽然我觉得这份结果由于是使用者主动提供网站进行检测, 甚至无法排除用户会刻意制造问题情境考验ASafaWeb的检查效果, 因此数据高低未必能精确反应实际情况, 但还是很有参考价值, 值得我们关心一下ASP.NET有哪些常见的配置漏洞, 确定自己都了解并检查手边网站有无类似状况, 绝对是件好事。以下是Hunt列出的常见ASP.NET配置安全漏洞:

  未隐藏错误讯息

  开发人员常会将方便排错, 但正式上线时却忘了移除, 导致一旦程序出错, 相关程序代码细节甚至程序片段就赤裸裸地展示出来, 

  可能由其中找到相关的文件位置、数据库信息、组件版本... 等信息, 提供入侵的指引。

  关闭Request Validation

  依Hunt的统计, 近30%的网站豪迈地关闭了全站的Request验证。若真有需要, 针对页面关闭就好, 至少伤害面变小, 但如果心有余力, 避开此限制保持后门紧闭还是上策。

  未更新Windows/IIS

  去年底被揭露的HTTP POST Hash DoS漏洞, 攻击者用简单的Request就能让网站忙到死去活来, 终至服务瘫痪。微软已在2月发布补定, 但是似乎还有50%的网站未完成更新。

  ELMAH存取未设限

  关于ELMAH存取设定的风险之前也有文章 《大叔手记(18):利用Elmah和Google体验一把入侵的快感》提过, 稍有不慎, 程序里的秘密就会大放送, 十分危险, 甚至 还可能藉此伪造ASP.NET Session冒充身份, 挺恐怖的。

  未关闭Trace

  虽然比例不高, 但通过trace.axd 还是能搜集到很多重要情报, 上线到正式环境时记得关闭。

  ?

篇2:检测企业Web平台的安全漏洞

  漏洞扫描就是对计算机系统或者其他网络设备进行安全相关的检测, 以找出安全隐患和可被 利用的漏洞, 作为一种保证Web信息系统和网络安全必不可少的手段, 我们有必要仔细研究利用。值得注意的是, 漏洞扫描软件是把双刃剑,  利用它入侵系统, 而系统管理员掌握它以后又可以有效的防范 入侵。

  四种漏洞扫描技术

  漏洞扫描通常采用两种策略, 第一种是被动式策略, 第二种是主动式策略。所谓被动式策略就是基于主机之上, 对系统中不合适的设置、脆弱的口令以及其他与安全规则抵触的对象进行检查;而主动式策略是基于网络的, 它通过执行一些脚本文件模拟对系统进行攻击的行为并记录系统的反应, 从而发现其中的漏洞。利用被动式策略的扫描称为系统安全扫描, 利用主动式的策略扫描称为网络安全扫描。

  漏洞扫描有以下四种检测技术:

  1.基于应用的检测技术。它采用被动的、非破坏性的办法检查应用软件包的设置, 发现安全漏洞。

  2.基于主机的检测技术。它采用被动的、非破坏性的办法对系统进行检测。通常, 它涉及到系统的内核、文件的属性、操作系统的补丁等。这种技术还包括口令解密、把一些简单的口令剔除。因此, 这种技术可以非常准确地定位系统的问题, 发现系统的漏洞。它的缺点是与平台相关, 升级复杂。

  3.基于目标的漏洞检测技术。它采用被动的、非破坏性的办法检查系统属性和文件属性, 如数据库、注册号等。通过消息文摘算法, 对文件的加密数进行检验。这种技术的实现是运行在一个闭环上, 不断地处理文件、系统目标、系统目标属性, 然后产生检验数, 把这些检验数同原来的检验数相比较。一旦发现改变就通知管理员。

  4.基于网络的检测技术。它采用积极的、非破坏性的办法来检验系统是否有可能被攻击崩溃。它利用了一系列的脚本模拟对系统进行攻击的行为, 然后对结果进行分析。它还针对已知的网络漏洞进行检验。网络检测技术常被用来进行穿透实验和安全审记。这种技术可以发现一系列平台的漏洞, 也容易安装。但是, 它可能会影响网络的性能。

  网络漏洞扫描

  在上述四种方式当中, 网络漏洞扫描最为适合我们的Web信息系统的风险评估工作, 其扫描原理和工作原理为:通过远程检测目标主机TCP/IP不同端口的服务, 记录目标的回答。通过这种方法, 可以搜集到很多目标主机的各种信息(例如:是否能用匿名登录, 是否有可写的FTP目录, 是否能用Telnet, httpd是否是用root在运行)。

  在获得目标主机TCP/IP端口和其对应的网络访问服务的相关信息后, 与网络漏洞扫描系统提供的漏洞库进行匹配, 如果满足匹配条件, 则视为漏洞存在。此外, 通过模拟 的进攻手法, 对目标主机系统进行攻击性的安全漏洞扫描, 如测试弱势口令等, 也是扫描模块的实现方法之一。如果模拟攻击成功, 则视为漏洞存在。

  在匹配原理上, 网络漏洞扫描器采用的是基于规则的匹配技术, 即根据安全专家对网络系统安全漏洞、 攻击案例的分析和系统管理员关于网络系统安全配置的实际经验, 形成一套标准的系统漏洞库, 然后再在此基础之上构成相应的匹配规则, 由程序自动进行系统漏洞扫描的分析工作。

  所谓基于规则是基于一套由专家经验事先定义的规则的匹配系统。例如, 在对TCP80端口的扫描中, 如果发现/cgi-bin/phf/cgi-bin/Count.cgi, 根据专家经验以及CGI程序的共享性和标准化, 可以推知该WWW服务存在两个CGI漏洞。同时应当说明的是, 基于规则的匹配系统有其局限性, 因为作为这类系统的基础的推理规则一般都是根据已知的安全漏洞进行安排和策划的, 而对网络系统的很多危险的威胁是来自未知的安全漏洞, 这一点和PC杀毒很相似。

  这种漏洞扫描器是基于浏览器/服务器(B/S)结构。它的工作原理是:当用户通过控制平台发出了扫描命令之后, 控制平台即向扫描模块发出相应的扫描请求, 扫描模块在接到请求之后立即启动相应的子功能模块, 对被扫描主机进行扫描。通过分析被扫描主机返回的信息进行判断, 扫描模块将扫描结果返回给控制平台, 再由控制平台最终呈现给用户。

  另一种结构的扫描器是采用插件程序结构。可以针对某一具体漏洞, 编写对应的外部测试脚本。通过调用服务检测插件, 检测目标主机TCP/IP不同端口的服务, 并将结果保存在信息库中, 然后调用相应的插件程序, 向远程主机发送构造好的数据, 检测结果同样保存于信息库, 以给其他的脚本运行提供所需的信息, 这样可提高检测效率, 

  如, 在针对某FTP服务的攻击中, 可以首先查看服务检测插件的返回结果, 只有在确认目标主机服务器开启FTP服务时, 对应的针对某FTP服务的攻击脚本才能被执行。采用这种插件结构的扫描器, 可以让任何人构造自己的攻击测试脚本, 而不用去了解太多扫描器的原理。这种扫描器也可以用做模拟 攻击的平台。采用这种结构的扫描器具有很强的生命力, 如著名的Nessus就是采用这种结构。这种网络漏洞扫描器是基于客户端/服务器(C/S)结构, 其中客户端主要设置服务器端的扫描参数及收集扫描信息。具体扫描工作由服务器来完成。漏洞扫描器的发展趋势

  值得我们注意的是漏洞扫描软件从最初的专门为UNIX系统编写的一些只具有简单功能的小程序, 发展到现在, 已经出现了多个运行在各种操作系统平台上的、具有复杂功能的商业程序。今后的发展趋势主要有以下几点, 我们可以根据实际Web信息系统风险评估的需求进行选用:

  1.使用插件或者叫做功能模块技术。每个插件都封装一个或者多个漏洞的测试手段, 主扫描程序通过调用插件的方法来执行扫描。仅仅是添加新的插件就可以使软件增加新功能, 扫描更多漏洞。在插件编写规范公布的情况下, 用户或者第三方公司甚至可以自己编写插件来扩充软件的功能。同时这种技术使软件的升级维护都变得相对简单, 并具有非常强的扩展性。

  2.使用专用脚本语言。这其实就是一种更高级的插件技术, 用户可以使用专用脚本语言来扩充软件功能。这些脚本语言语法通常比较简单易学, 往往用十几行代码就可以定制一个简单的测试, 为软件添加新的测试项。脚本语言的使用, 简化了编写新插件的编程工作, 使扩充软件功能的工作变得更加容易, 也更加有趣。

  3.由漏洞扫描程序到安全评估专家系统。最早的漏洞扫描程序只是简单地把各个扫描测试项的执行结果罗列出来, 直接提供给测试者而不对信息进行任何分析处理。而当前较成熟的扫描系统都能够将对单个主机的扫描结果整理, 形成报表, 能够并对具体漏洞提出一些解决方法。不足之处是对网络的状况缺乏一个整体的评估, 对网络安全没有系统的解决方案。未来的安全扫描系统, 应该不但能够扫描安全漏洞, 还能够智能化地协助网络信息系统管理人员评估本网络的安全状况, 给出安全建议, 成为一个安全评估专家系统。

  Web系统的风险等级评估

  在实现了对Web信息系统的安全扫描后, 便可根据扫描结果, 对Web信息系统的安全性能进行评估, 从而给出Web信息系统的风险状况。这里, 风险评估的依据是根据扫描结果, 根据Web信息系统所具有的漏洞数目及漏洞的危害程度, 将Web信息系统的安全状态进行分级。

  划分的风险评估级别如下:

  1.A级:扫描结果显示没有漏洞, 但这并不表明系统没有漏洞, 因为有许多漏洞是尚未发现的, 我们只能针对已知的漏洞进行测试。

  2.B级:具有一些泄漏服务器版本信息之类的不是很重要内容的漏洞, 或者提供容易造成被攻击的服务, 如允许匿名登录, 这种服务可能会造成许多其它漏洞。

  3.C级:具有危害级别较小的一些漏洞, 如可以验证某账号的存在, 可以造成列出一些页面目录、文件目录等, 不会造成严重后果的漏洞。

  4.D级:具有一般的危害程度的漏洞。如拒绝服务漏洞,造成Web信息系统不能正常工作;可以让 获得重要文件的访问权的漏洞等。

  5.E级:具有严重危害程度的漏洞。如存在缓冲区溢出漏洞, 存在木马后门, 存在可以让 获得根用户权限或根用户的shell漏洞,根目录被设置一般用户可写等一些后果非常严重的漏洞。

  另外, 我们需要强调的是:漏洞的产生主要源于Web信息系统的不正当配置以及其提供的服务自身的弱点。前面我们详细介绍了如何使用漏洞扫描来进行风险评估。其实还有一个非常重要的问题我们不能忽略, 那就是需要检测Web信息系统到底提供了哪些服务, 因为它直接关系到系统的漏洞的产生以及危害。一方面, Web信息系统为用户提供了多种优质的网络服务, 包括Http、Ftp、Smtp、Pop3等;另一方面, 服务的增多意味着更多的风险。每种服务本身都必然存在着某些缺陷, 而这些缺陷很有可能被高明的 利用来对系统进行攻击。所以, 提供特定服务的服务器应该尽可能开放提供服务必不可少的端口, 而将与服务器服务无关的服务关闭, 比如:一台作为和ftp服务器的机器, 应该只开放80和25端口, 而将其他无关的服务如关掉, 以减少系统漏洞。

  因此, 我们需要针对Web系统的实际用途使用相关的工具来对系统开放的网络服务及其端口等进行有效地检测和适时的处理, 以及时关闭那些不必需要的服务和端口, 以免为 和不法用户利用, 从而侵入信息系统。显然, 这是一项非常艰巨和长期的工作, 管理者们需要在技术和管理两个层面上投入相当的物力和财力, 从而保证Web信息系统的安全性。

篇3:Web.config 安全相关配置WEB安全

  web.config 位于根目录

  1、authentication节点

  

  

  

  基于窗体(Forms)的身份验证配置站点, 当没有登陆的用户访问需要身份验证的网页, 网页自动跳转到登陆网页。其中元素loginUrl表示登陆网页的名称, name表示Cookie名称

  2、authorization 节点

  

  

  allow???向授权规则映射添加一个规则, 该规则允许对资源进行访问。

  deny???向授权规则映射添加一条拒绝对资源的访问的授权规则。

  users=“*” 是指任何用户???users=“?” 是指经身份验证的用户

  注意: 运行时, 授权模块从最本地的配置文件开始, 循环访问 allow 和 deny 元素, 直到它找到适合特定用户帐户的第一个访问规则。然后, 该授权模块根据找到的第一个访问规则是 allow 还是 deny 规则来允许或拒绝对 URL 资源的访问。默认的授权规则为 。因此, 默认情况下允许访问, 除非另外配置。

  如果在根目录下的web.config配置太繁琐, 可以配置到相应目录下, 例如User目录下的web.config文件

  3、customErrors 节点

  

  

  mode=“On|Off|RemoteOnly”>

  

  defaultRedirect 可选的属性。指定出错时将浏览器定向到的默认 URL。如果未指定该属性, 则显示一般性错误。

  Mode 必选的属性。指定是启用或禁用自定义错误, 还是仅向远程客户端显示自定义错误。

  此属性可以为下列值之一。

  值 说明

  On 指定启用自定义错误。如果未指定 defaultRedirect, 用户将看到一般性错误。

  Off 指定禁用自定义错误。这允许显示标准的详细错误。

  RemoteOnly 指定仅向远程客户端显示自定义错误并且向本地主机显示 ASP.NET 错误。这是默认值。

  默认值为 RemoteOnly。

  error 可选的元素。指定给定 HTTP 状态代码的自定义错误页。错误标记可以出现多次。子标记的每一次出现均定义一个自定义错误条件。

  例如:

  

  

  

  

  这里可以让用户自定义出错页。

  4、pages 节点

  

  

  validateRequest=“true”

  该值确定 ASP.NET 是否针对危险值检查来自浏览器的输入。如果 ASP.NET 针对危险值检查来自浏览器的输入, 则为 true;否则为 false。默认值为 true。

  这个功能是为了防止跨站脚本等危险代码。使全局默认为true。只有小数页面, 如搜索页面

  Search.aspx?设为 : ValidateRequest=“false” 。为了可以搜索类似 等内容, 如果只是文字性的输入, 可修改页 search.aspx 的设置, 以增强系统安全性。

  Security.config 配置说明

  文件位于config目录

  1、后台页面访问配置

  noCheckAdminLogOn

  后台不检查权限的页面

  

  

  

  2、检查外站链接的后台页面配置

  noCheckUrlReferrer

  后台不检查来源页的列表, 即管理员用户可以直接访问的文件列表, 

  后台设置是默认不允许直接访问, 这样可以保护后页页面不被非法方式访问和外站链接访问, 有效防止跨站请求伪造。

  

  如果文件不在列表中, 直接在URL 里访问, 将出现错误提示:

  产生错误的可能原因:

  对不起, 为了系统安全, 不允许直接输入地址访问本系统的后台管理页面。

  如需要, 用户可以加上自定义的内容。

  3、防止跨站请求伪造追加安全码的页面配置

  checkSecurityCode

  页面提交时检查安全码。

  防止不正常操作(恶意操作)造成系统重大损失。也是对一些重要操作的保护, 防止跨站请求伪造。

  如:

  

  

  4、页面操作权限码的配置

  checkPermissions

  页面操作权限码的配置, 检查后台管理员是否有相关操作码的权限。

  

  operateCode 为操作码 根据操作码判断是否存在此操作的权限。

  checkType?权限判断类型,  or and

  or?操作码中的权限进行或运算, 即有其中任何一种权限, 就返回true

  这个默认值是or 而且对于单一权限码的,可以不用配置

  and 操作码中的权限进行与运算,即要求有全部权限才返回true 否则返回false.

  AjaxLabel.config 配置说明

  是对AJAX.aspx 的文件访问权限控制配置文件。

  由于前台AJAX标签过于强大, 会致使 AJAX标签 会出现一些危险性, 对此我们做了一个XML安全文件来配置那些AJAX标签可以直接引用。这个AjaxLabel.config 文件是在 网站根目录的Config 目录下。如果标签没有记录, 就会出现 本标签禁止访问!

  例如:

  

  

  

  是指标签名 为 “内容评论PK标签” 的标签可以被ajas.aspx调用, 而且参数param只能为 “generalid”,类型为Int, 这样能有效防止恶意攻击。

  如果用户需自定义标签, 而且需要ajax.aspx 文件调用, 那就在AjaxLabel.config 中配置

  app_offlineX.htm 文件作用

  如果你要COPY站点, 进行站点维护, 部署, 和进行大量修改, 有可能要停掉你的WEB应用程序了, 而以一个友好的方式提示给用户, 比如什么“本网站正在更新”等等的信息,你可以把文件app_offlineX.htm 改名为app_offline.htm(大小写没关系)的静态HTM页面文件, 其中修改成你要临时显示的内容, 将其放在你的应用的根目录下。这样, 任何外部的请求的话, 都会马上被转移到该页面了。

  网站维护完成后记得将文件名app_offline.htm改回。

  AllowString.xml 文件配置

  文件位于Common目录下

  文件的作用是:会员发表信息时启用防XSS(跨站攻击)设置时, 让用户设置允许会员提交部份特殊js 代码。

  XSS是指攻击者利用网站程序对用户输入过滤不足, 输入可以显示在页面上对其他用户造成影响的HTML代码, 从而盗取用户资料、利用用户身份进行某种动作或者对访问者进行病毒侵害的一种攻击方式。

  例如:

  

  

  功能是: 允许保留 nmousewheel=“return bbimg(this)” 和 nload=“resizepic(this)” 代码。这是对FCK上传图片功能的一个保留。

  如用户想让系统过滤不要太严格, 可在这里加上相应保留代码。

篇4:检测你的Web系统有多少安全漏洞

  Internet的开放性使得Web系统面临入侵攻击的威胁, 而建立一个安全的Web系统一直是人们的目标, 一个实用的方法是, 建立比较容易实现的相对安全的系统, 同时按照一定的安全策略建立相应的安全辅助系统, 漏洞扫描器就是这样一类安全辅助系统。

  漏洞扫描就是对计算机系统或者其他网络设备进行安全相关的检测, 以找出安全隐患和可被 利用的漏洞。作为一种保证Web信息系统和网络安全必不可少的手段, 我们有必要仔细研究利用。值得注意的是, 漏洞扫描软件是把双刃剑,  利用它入侵系统, 而系统管理员掌握它以后又可以有效的防范 入侵。

  四种漏洞扫描技术

  漏洞扫描通常采用两种策略, 第一种是被动式策略, 第二种是主动式策略。所谓被动式策略就是基于主机之上, 对系统中不合适的设置、脆弱的口令以及其他与安全规则抵触的对象进行检查;而主动式策略是基于网络的, 它通过执行一些脚本文件模拟对系统进行攻击的行为并记录系统的反应, 从而发现其中的漏洞。利用被动式策略的扫描称为系统安全扫描, 利用主动式的策略扫描称为网络安全扫描。

  漏洞扫描有以下四种检测技术:

  1.基于应用的检测技术。它采用被动的、非破坏性的办法检查应用软件包的设置, 发现安全漏洞。

  2.基于主机的检测技术。它采用被动的、非破坏性的办法对系统进行检测。通常, 它涉及到系统的内核、文件的属性、操作系统的补丁等。这种技术还包括口令解密、把一些简单的口令剔除。因此, 这种技术可以非常准确地定位系统的问题, 发现系统的漏洞。它的缺点是与平台相关, 升级复杂。

  3.基于目标的漏洞检测技术。它采用被动的、非破坏性的办法检查系统属性和文件属性, 如数据库、注册号等。通过消息文摘算法, 对文件的加密数进行检验。这种技术的实现是运行在一个闭环上, 不断地处理文件、系统目标、系统目标属性, 然后产生检验数, 把这些检验数同原来的检验数相比较。一旦发现改变就通知管理员。

  4.?基于网络的检测技术。它采用积极的、非破坏性的办法来检验系统是否有可能被攻击崩溃。它利用了一系列的脚本模拟对系统进行攻击的行为, 然后对结果进行分析。它还针对已知的网络漏洞进行检验。网络检测技术常被用来进行穿透实验和安全审记。这种技术可以发现一系列平台的漏洞, 也容易安装。但是, 它可能会影响网络的性能。

  网络漏洞扫描

  在上述四种方式当中, 网络漏洞扫描最为适合我们的Web信息系统的风险评估工作, 其扫描原理和工作原理为:通过远程检测目标主机TCP/IP不同端口的服务, 记录目标的回答。通过这种方法, 可以搜集到很多目标主机的各种信息(例如:是否能用匿名登录, 是否有可写的FTP目录, 是否能用Telnet, httpd是否是用root在运行)。

  在获得目标主机TCP/IP端口和其对应的网络访问服务的相关信息后, 与网络漏洞扫描系统提供的漏洞库进行匹配, 如果满足匹配条件, 则视为漏洞存在。此外, 通过模拟 的进攻手法, 对目标主机系统进行攻击性的安全漏洞扫描, 如测试弱势口令等, 也是扫描模块的实现方法之一。如果模拟攻击成功, 则视为漏洞存在。

  在匹配原理上, 网络漏洞扫描器采用的是基于规则的匹配技术, 即根据安全专家对网络系统安全漏洞、 攻击案例的分析和系统管理员关于网络系统安全配置的实际经验, 形成一套标准的系统漏洞库, 然后再在此基础之上构成相应的匹配规则, 由程序自动进行系统漏洞扫描的分析工作。

  所谓基于规则是基于一套由专家经验事先定义的规则的匹配系统。例如, 在对TCP80端口的扫描中, 如果发现/cgi-bin/phf/cgi-bin/Count.cgi, 根据专家经验以及CGI程序的共享性和标准化, 可以推知该WWW服务存在两个CGI漏洞。同时应当说明的是, 基于规则的匹配系统有其局限性, 因为作为这类系统的基础的推理规则一般都是根据已知的安全漏洞进行安排和策划的, 而对网络系统的很多危险的威胁是来自未知的安全漏洞, 这一点和PC杀毒很相似。

  这种漏洞扫描器是基于浏览器/服务器(B/S)结构。它的工作原理是:当用户通过控制平台发出了扫描命令之后, 控制平台即向扫描模块发出相应的扫描请求, 扫描模块在接到请求之后立即启动相应的子功能模块, 对被扫描主机进行扫描。通过分析被扫描主机返回的信息进行判断, 扫描模块将扫描结果返回给控制平台, 再由控制平台最终呈现给用户。

  另一种结构的扫描器是采用插件程序结构。可以针对某一具体漏洞, 编写对应的外部测试脚本, 

  通过调用服务检测插件, 检测目标主机TCP/IP不同端口的服务, 并将结果保存在信息库中, 然后调用相应的插件程序, 向远程主机发送构造好的数据, 检测结果同样保存于信息库, 以给其他的脚本运行提供所需的信息, 这样可提高检测效率。如, 在针对某FTP服务的攻击中, 可以首先查看服务检测插件的返回结果, 只有在确认目标主机服务器开启FTP服务时, 对应的针对某FTP服务的攻击脚本才能被执行。采用这种插件结构的扫描器, 可以让任何人构造自己的攻击测试脚本, 而不用去了解太多扫描器的原理。这种扫描器也可以用做模拟 攻击的平台。采用这种结构的扫描器具有很强的生命力, 如著名的Nessus就是采用这种结构。这种网络漏洞扫描器的结构如图2所示, 它是基于客户端/服务器(C/S)结构, 其中客户端主要设置服务器端的扫描参数及收集扫描信息。具体扫描工作由服务器来完成。漏洞扫描器的发展趋势

  值得我们注意的是漏洞扫描软件从最初的专门为UNIX系统编写的一些只具有简单功能的小程序, 发展到现在, 已经出现了多个运行在各种操作系统平台上的、具有复杂功能的商业程序。今后的发展趋势主要有以下几点, 我们可以根据实际Web信息系统风险评估的需求进行选用:

  1.?使用插件或者叫做功能模块技术。每个插件都封装一个或者多个漏洞的测试手段, 主扫描程序通过调用插件的方法来执行扫描。仅仅是添加新的插件就可以使软件增加新功能, 扫描更多漏洞。在插件编写规范公布的情况下, 用户或者第三方公司甚至可以自己编写插件来扩充软件的功能。同时这种技术使软件的升级维护都变得相对简单, 并具有非常强的扩展性。

  2.?使用专用脚本语言。这其实就是一种更高级的插件技术, 用户可以使用专用脚本语言来扩充软件功能。这些脚本语言语法通常比较简单易学, 往往用十几行代码就可以定制一个简单的测试, 为软件添加新的测试项。脚本语言的使用, 简化了编写新插件的编程工作, 使扩充软件功能的工作变得更加容易, 也更加有趣。

  3.?由漏洞扫描程序到安全评估专家系统。最早的漏洞扫描程序只是简单地把各个扫描测试项的执行结果罗列出来, 直接提供给测试者而不对信息进行任何分析处理。而当前较成熟的扫描系统都能够将对单个主机的扫描结果整理, 形成报表, 能够并对具体漏洞提出一些解决方法。不足之处是对网络的状况缺乏一个整体的评估, 对网络安全没有系统的解决方案。未来的安全扫描系统, 应该不但能够扫描安全漏洞, 还能够智能化地协助网络信息系统管理人员评估本网络的安全状况, 给出安全建议, 成为一个安全评估专家系统。

  Web系统的风险等级评估

  在实现了对Web信息系统的安全扫描后, 便可根据扫描结果, 对Web信息系统的安全性能进行评估, 从而给出Web信息系统的风险状况。这里, 风险评估的依据是根据扫描结果, 根据Web信息系统所具有的漏洞数目及漏洞的危害程度, 将Web信息系统的安全状态进行分级。

  划分的风险评估级别如下:

  l.A级:扫描结果显示没有漏洞, 但这并不表明系统没有漏洞, 因为有许多漏洞是尚未发现的, 我们只能针对已知的漏洞进行测试。

  2.B级:具有一些泄漏服务器版本信息之类的不是很重要内容的漏洞, 或者提供容易造成被攻击的服务, 如允许匿名登录, 这种服务可能会造成许多其它漏洞。

  3.C级:具有危害级别较小的一些漏洞, 如可以验证某账号的存在, 可以造成列出一些页面目录、文件目录等, 不会造成严重后果的漏洞。

  4.D级:具有一般的危害程度的漏洞。如拒绝服务漏洞,造成Web信息系统不能正常工作;可以让 获得重要文件的访问权的漏洞等。

  5.E级:具有严重危害程度的漏洞。如存在缓冲区溢出漏洞, 存在木马后门, 存在可以让 获得根用户权限或根用户的shell漏洞,根目录被设置一般用户可写等一些后果非常严重的漏洞。

  另外, 我们需要强调的是:漏洞的产生主要源于Web信息系统的不正当配置以及其提供的服务自身的弱点。前面我们详细介绍了如何使用漏洞扫描来进行风险评估。其实还有一个非常重要的问题我们不能忽略, 那就是需要检测Web信息系统到底提供了哪些服务, 因为它直接关系到系统的漏洞的产生以及危害。一方面, Web信息系统为用户提供了多种优质的网络服务, 包括Http、Ftp、Smtp、Pop3等;另一方面, 服务的增多意味着更多的风险。每种服务本身都必然存在着某些缺陷, 而这些缺陷很有可能被高明的 利用来对系统进行攻击。所以, 提供特定服务的服务器应该尽可能开放提供服务必不可少的端口, 而将与服务器服务无关的服务关闭, 比如:一台作为和ftp服务器的机器, 应该只开放80和25端口, 而将其他无关的服务如关掉, 以减少系统漏洞。

  因此, 我们需要针对Web系统的实际用途使用相关的工具来对系统开放的网络服务及其端口等进行有效地检测和适时的处理, 以及时关闭那些不必需要的服务和端口, 以免为 和不法用户利用, 从而侵入信息系统。显然, 这是一项非常艰巨和长期的工作, 管理者们需要在技术和管理两个层面上投入相当的物力和财力, 从而保证Web信息系统的安全性。

篇5:IIS7 配置(ASP.NET 2.0, WCF, ASP.NET MVC,php)WEB服务器

  ASP.NET 2.0 部署

  1.首先打开win7 的特性, 路径我已标注

  下面选中的是ASP.NET2.0, 如果要支持ASP.NET1.1,你的选中IIS6兼容

  2. 设置安全选项

  3. 添加.Net经典应用程序池

  4.将站点转换为Application

  5.为站点添加 yourmachinenameIIS_IUSRS权限

  6.右键站点—Manage Application-Advanced Setting 设置当前站点为Classic .Net AppPool

  2. 部署WCF

  打开“开始|所有程序|附件|命令提示符”, 输入“cd C:WindowsMicrosoft.NETFrameworkv3.0Windows Communication Foundation”, 回车, 

  IIS7 配置大全(ASP.NET 2.0, WCF, ASP.NET MVC,php)WEB服务器

  , 

  再输入“ServiceModelReg.exe -i”, 回车。

  3. 部署ASP.NET MVC

  在IIS7下, 部署ASP.NET MVC是最方便的, 只需做如下的配置, 添加一个集成模式的.NET Application Pool,将它指定给你的ASP.NET MVC Application

  vista和Win7 beta用的都是IIS7,所以配置是一样的

  提示: 本人为了使用ASP.NET MVC, 需要升级XP的IIS5.1到IIS6.0,通过多种方法都是失败的, 最后升级系统为Windows Server , 谁如果能成功, 请提供个方法。

  本文的系统是windows 7 beta.

  ?

篇6:检测隐藏进程WEB安全

  Detection of the hidden processes

  俄语原文:wasm.ru/article.php?article=hiddndt

  俄文翻译:kao

  community.reverse-engineering.net/viewtopic.php?t=4685

  中文翻译: prince

  后期校验:firstrose

  =============================

  侦测隐藏进程

  [C] Ms-Rem

  2002-2005 wasm.ru - all rights reserved and reversed

  许多用户都有过用Windows自带的任务管理器查看所有进程的经验, 并且很多人都认为在任务管理器中隐藏进程是不可能的, 而实际上, 进程隐藏是再简单不过的事情了。有许多可用的方法和参考源码可以达到进程隐藏的目的。令我惊奇的是只有很少一部分的木马使用了这种技术。估计1000个木马中仅有1个是进程隐藏的。我认为木马的作者太懒了, 因为隐藏进程需要进行的额外工作仅仅是对源代码的拷贝-粘贴。所以我们应该期待即将到来的会隐藏进程的木马。

  自然地, 也就有必要研究进程隐藏的对抗技术。杀毒软件和防火墙制造商就像他们的产品不能发现隐藏进程一样落后了。在少之又少的免费工具中, 能够胜任的也只有Klister(仅运行于Windows 2000平台)了。所有其他公司关注的只有金钱(俄文译者kao注:不完全正确, FSecure的BlackLight Beta也是免费的)。除此之外, 所有的这些工具都可以很容易的anti掉。

  用程序实现隐藏进程探测技术, 我们有两种选择:

  * 基于某种探测原理找到一种隐藏的方法;

  * 基于某个程序找到一种隐藏的方法, 这个要简单一些。

  购买商业软件产品的用户不能修改程序, 这样可以保证其中绑定的程序的安全运行。因此第2种方法提到的程序就是商业程序的后门(rootkits)(例如hxdef Golden edition)。唯一的解决方案是创建一个免费的隐藏进程检测的开源项目, 这个程序使用几种不同的检测方法, 这样可以发现使用某一种方法进行隐藏的进程。任何一个用户都可以抵挡某程序的捆绑程序, 当然那要得到程序的源代码并且按照自己的意愿进行修改。

  在这篇文章中我将讨论探测隐藏进程的基本方法, 列出该方法的示例代码, 并创建一个能够检测上面我们提到的隐藏进程的程序。

  在用户态(ring 3)检测

  我们从简单的用户态(ring 3)检测开始, 不使用驱动。事实上, 每一个进程都会留下某种活动的痕迹, 根据这些痕迹, 我们就可以检测到隐藏的进程。这些痕迹包括进程打开的句柄、窗口和创建的系统对象。要避开这种检测技术是非常简单的, 但是这样做需要留意进程留下所有痕迹, 这种模式没有被用在任何一个公开发行的后门(rootkits)上。(不幸的是内部版本没有对我开放)。用户态方法容易实现, 使用安全, 并且能够得到很好的效果, 因此这种方法不应该被忽略。

  首先我们定义一下用到的数据, 如下:

  Code:

  type

  PProcList = ^TProcList;

  TProcList = packed record

  NextItem: pointer;

  ProcName: array [0..MAX_PATH] of Char;

  ProcId: dword;

  ParrentId: dword;

  end;

  使用ToolHelp API获得所有进程列表

  定义一下获得进程列表的函数。我们要比较这个结果和通过其他途径得到的结果:

  Code:

  {

  Acquiring list of processes by using ToolHelp API.

  }

  procedure GetToolHelpProcessList(var List: PListStruct);

  var

  Snap: dword;

  Process: TPROCESSENTRY32;

  NewItem: PProcessRecord;

  begin

  Snap := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

  if Snap INVALID_HANDLE_VALUE then

  begin

  Process.dwSize := SizeOf(TPROCESSENTRY32);

  if Process32First(Snap, Process) then

  repeat

  GetMem(NewItem, SizeOf(TProcessRecord));

  ZeroMemory(NewItem, SizeOf(TProcessRecord));

  NewItem^.ProcessId := Process.th32ProcessID;

  NewItem^.ParrentPID := Process.th32ParentProcessID;

  lstrcpy(@NewItem^.ProcessName, Process.szExeFile);

  AddItem(List, NewItem);

  until not Process32Next(Snap, Process);

  CloseHandle(Snap);

  end;

  end;

  很明显, 这不会发现任何隐藏进程, 所以这个函数只可以用来做探测隐藏进程的参考。

  通过使用Native API获得进程列表

  再深一个层次的扫描我们要通过Native API ZwQuerySystemInformation获得进程列表。虽然在这个级别(ring 0)什么也发现不了, 但是我们

  仍然应该检查一下。(prince注:有点令人费解, 原文如下:The next scanning level will be acquisition a list of processes through

  ZwQuerySystemInformation (Native API). It is improbable that something will be found out at this level but we should check it

  anyway.)

  Code:

  {

  Acquiring list of processes by using ZwQuerySystemInformation.

  }

  procedure GetNativeProcessList(var List: PListStruct);

  var

  Inf PSYSTEM_PROCESSES;

  NewItem: PProcessRecord;

  Mem: pointer;

  begin

  Info := GetInfoTable(SystemProcessesAndThreadsInformation);

  Mem := Info;

  if Info = nil then Exit;

  repeat

  GetMem(NewItem, SizeOf(TProcessRecord));

  ZeroMemory(NewItem, SizeOf(TProcessRecord));

  lstrcpy(@NewItem^.ProcessName,

  PChar(WideCharToString(Info^.ProcessName.Buffer)));

  NewItem^.ProcessId := Info^.ProcessId;

  NewItem^.ParrentPID := Info^.InheritedFromProcessId;

  AddItem(List, NewItem);

  Info := pointer(dword(info) + info^.NextEntryDelta);

  until Info^.NextEntryDelta = 0;

  VirtualFree(Mem, 0, MEM_RELEASE);

  end;

  通过进程打开的句柄获得进程列表。

  许多隐藏进程无法隐藏他们打开的句柄, 因此我们可以通过使用ZwQuerySystemInformation函数枚举打开的句柄来构建进程列表。

  Code:

  {

  Acquiring the list of processes by using list of opened handles.

  Returns only ProcessId.

  }

  procedure GetHandlesProcessList(var List: PListStruct);

  var

  Inf PSYSTEM_HANDLE_INFORMATION_EX;

  NewItem: PProcessRecord;

  r: dword;

  OldPid: dword;

  begin

  OldPid := 0;

  Info := GetInfoTable(SystemHandleInformation);

  if Info = nil then Exit;

  for r := 0 to Info^.NumberOfHandles do

  if Info^.Information[r].ProcessId OldPid then

  begin

  OldPid := Info^.Information[r].ProcessId;

  GetMem(NewItem, SizeOf(TProcessRecord));

  ZeroMemory(NewItem, SizeOf(TProcessRecord));

  NewItem^.ProcessId := OldPid;

  AddItem(List, NewItem);

  end;

  VirtualFree(Info, 0, MEM_RELEASE);

  end;

  到现在我们已经可能发现一些东西了, 但是我们不应该依赖于像隐藏进程一样简单的隐藏句柄的检查结果, 尽管有些人甚至忘记隐藏他们。

  通过列举创建的窗口来得到进程列表。

  可以将那在系统中注册窗口的进程用GetWindowThreadProcessId构建进程列表。

  Code:

  {

  Acquiring the list of processes by using list of windows.

  Returns only ProcessId.

  }

  procedure GetWindowsProcessList(var List: PListStruct);

  function EnumWindowsProc(hwnd: dword; PList: PPListStruct): bool; stdcall;

  var

  ProcId: dword;

  NewItem: PProcessRecord;

  begin

  GetWindowThreadProcessId(hwnd, ProcId);

  if not IsPidAdded(PList^, ProcId) then

  begin

  GetMem(NewItem, SizeOf(TProcessRecord));

  ZeroMemory(NewItem, SizeOf(TProcessRecord));

  NewItem^.ProcessId := ProcId;

  AddItem(PList^, NewItem);

  end;

  Result := true;

  end;

  begin

  EnumWindows(@EnumWindowsProc, dword(@List));

  end;

  几乎没有人会隐藏窗口, 因此这种检查可以检测某些进程, 但是我们不应该相信这种检测。

  直接通过系统调用得到进程列表。

  在用户态隐藏进程, 一个普遍的做法是使用代码注入(code-injection)技术和在所有进程中拦截ntdll.dll中的ZwQuerySystemInformation函数

  。

  ntdll中的函数实际上对应着系统内核中的函数和系统调用(Windows 2000 中的2Eh中断或者Windows XP中的sysenter指令), 因此大多数简单

  又有效的关于那些用户级的隐藏进程的检测方法就是直接使用系统调用而不是使用API函数。

  Windows XP中ZwQuerySystemInformation函数的替代函数看起来是这个样子:

  Code:

  {

  ZwQuerySystemInformation for Windows XP.

  }

  Function XpZwQuerySystemInfoCall(ASystemInformationClass: dword;

  ASystemInformation: Pointer;

  ASystemInformationLength: dword;

  AReturnLength: pdword): dword; stdcall;

  asm

  pop ebp

  mov eax, $AD

  call @SystemCall

  ret $10

  @SystemCall:

  mov edx, esp

  sysenter

  end;

  由于不同的系统调用机制, Windows 2000的这部分代码看起来有些不同。

  Code:

  {

  Системный вызов ZwQuerySystemInformation для Windows 2000.

  }

  Function Win2kZwQuerySystemInfoCall(ASystemInformationClass: dword;

  ASystemInformation: Pointer;

  ASystemInformationLength: dword;

  AReturnLength: pdword): dword; stdcall;

  asm

  pop ebp

  mov eax, $97

  lea edx, [esp + $04]

  int $2E

  ret $10

  end;

  现在有必要使用上面提到的函数而不是ntdll来枚举系统进程了。实现的代码如下:

  Code:

  {

  Acquiring the list of processes by use of a direct system call

  ZwQuerySystemInformation.

  }

  procedure GetSyscallProcessList(var List: PListStruct);

  var

  Inf PSYSTEM_PROCESSES;

  NewItem: PProcessRecord;

  mPtr: pointer;

  mSize: dword;

  St: NTStatus;

  begin

  mSize := $4000;

  repeat

  GetMem(mPtr, mSize);

  St := ZwQuerySystemInfoCall(SystemProcessesAndThreadsInformation,

  mPtr, mSize, nil);

  if St = STATUS_INFO_LENGTH_MISMATCH then

  begin

  FreeMem(mPtr);

  mSize := mSize * 2;

  end;

  until St STATUS_INFO_LENGTH_MISMATCH;

  if St = STATUS_SUCCESS then

  begin

  Info := mPtr;

  repeat

  GetMem(NewItem, SizeOf(TProcessRecord));

  ZeroMemory(NewItem, SizeOf(TProcessRecord));

  lstrcpy(@NewItem^.ProcessName,

  PChar(WideCharToString(Info^.ProcessName.Buffer)));

  NewItem^.ProcessId := Info^.ProcessId;

  NewItem^.ParrentPID := Info^.InheritedFromProcessId;

  Info := pointer(dword(info) + info^.NextEntryDelta);

  AddItem(List, NewItem);

  until Info^.NextEntryDelta = 0;

  end;

  FreeMem(mPtr);

  end;

  这种方法能检测几乎100%的用户态的后门(rootkits), 例如hxdef的所有版本(包括黄金版)。

  通过分析相关的句柄得到进程列表。

  基于枚举句柄的方法。这个方法的实质并不是查找进程打开的句柄, 而是查找同该进程相关的其他进程的句柄。这些句柄可以是进程句柄也可

  以是线程句柄。当找到进程句柄, 我们就可以用ZwQueryInformationProcess函数得到进程的PID。对于线程句柄, 我们可以通过

  ZwQueryInformationThread得到进程ID。存在于系统中的所有进程都是由某些进程产生的, 因此父进程拥有他们的句柄(除了那些已经被关闭

  的句柄), 对于Win32子系统服务器(csrss.exe)来说所有存在的进程的句柄都是可以访问的。另外, Windows NT大量使用Job objects(prince:

  任务对象?姑且这么翻译吧, 有不妥的地方请指教), 任务对象可以关联进程(比如属于某用户或服务的所有进程), 因此当找到任务对象的句

  柄, 我们就可以利用它得到与之关联的所有进程的ID。使用QueryInformationJobObject和信息类的函数JobObjectBasicProcessIdList就可以

  实现上述功能。利用分析进程相关的句柄得到进程列表的实现代码如下:

  Code:

  {

  Acquiring the list of processes by analyzing handles in other processes.

  }

  procedure GetProcessesFromHandles(var List: PListStruct; Processes, Jobs, Threads: boolean);

  var

  HandlesInf PSYSTEM_HANDLE_INFORMATION_EX;

  ProcessInf PROCESS_BASIC_INFORMATION;

  hProcess : dword;

  tHandle: dword;

  r, l : integer;

  NewItem: PProcessRecord;

  Inf PJOBOBJECT_BASIC_PROCESS_ID_LIST;

  Size: dword;

  THRInf THREAD_BASIC_INFORMATION;

  begin

  HandlesInfo := GetInfoTable(SystemHandleInformation);

  if HandlesInfo nil then

  for r := 0 to HandlesInfo^.NumberOfHandles do

  if HandlesInfo^.Information[r].ObjectTypeNumber in [OB_TYPE_PROCESS, OB_TYPE_JOB, OB_TYPE_THREAD] then

  begin

  hProcess := OpenProcess(PROCESS_DUP_HANDLE, false,

  HandlesInfo^.Information[r].ProcessId);

  if DuplicateHandle(hProcess, HandlesInfo^.Information[r].Handle,

  INVALID_HANDLE_VALUE, @tHandle, 0, false,

  DUPLICATE_SAME_ACCESS) then

  begin

  case HandlesInfo^.Information[r].ObjectTypeNumber of

  OB_TYPE_PROCESS : begin

  if Processes and (HandlesInfo^.Information[r].ProcessId = CsrPid) then

  if ZwQueryInformationProcess(tHandle, ProcessBasicInformation,

  @ProcessInfo,

  SizeOf(PROCESS_BASIC_INFORMATION),

  nil) = STATUS_SUCCESS then

  if not IsPidAdded(List, ProcessInfo.UniqueProcessId) then

  begin

  GetMem(NewItem, SizeOf(TProcessRecord));

  ZeroMemory(NewItem, SizeOf(TProcessRecord));

  NewItem^.ProcessId := ProcessInfo.UniqueProcessId;

  NewItem^.ParrentPID := ProcessInfo.InheritedFromUniqueProcessId;

  AddItem(List, NewItem);

  end;

  end;

  OB_TYPE_JOB : begin

  if Jobs then

  begin

  Size := SizeOf(JOBOBJECT_BASIC_PROCESS_ID_LIST) + 4 * 1000;

  GetMem(Info, Size);

  Info^.NumberOfAssignedProcesses := 1000;

  if QueryInformationJobObject(tHandle, JobObjectBasicProcessIdList,

  Info, Size, nil) then

  for l := 0 to Info^.NumberOfProcessIdsInList - 1 do

  if not IsPidAdded(List, Info^.ProcessIdList[l]) then

  begin

  GetMem(NewItem, SizeOf(TProcessRecord));

  ZeroMemory(NewItem, SizeOf(TProcessRecord));

  NewItem^.ProcessId := Info^.ProcessIdList[l];

  AddItem(List, NewItem);

  end;

  FreeMem(Info);

  end;

  end;

  OB_TYPE_THREAD : begin

  if Threads then

  if ZwQueryInformationThread(tHandle, THREAD_BASIC_INFO,

  @THRInfo,

  SizeOf(THREAD_BASIC_INFORMATION),

  nil) = STATUS_SUCCESS then

  if not IsPidAdded(List, THRInfo.ClientId.UniqueProcess) then

  begin

  GetMem(NewItem, SizeOf(TProcessRecord));

  ZeroMemory(NewItem, SizeOf(TProcessRecord));

  NewItem^.ProcessId := THRInfo.ClientId.UniqueProcess;

  AddItem(List, NewItem);

  end;

  end;

  end;

  CloseHandle(tHandle);

  end;

  CloseHandle(hProcess);

  end;

  VirtualFree(HandlesInfo, 0, MEM_RELEASE);

  end;

  不幸的是, 上面提到的这些方法有些只能得到进程ID, 而不能得到进程名字。因此, 我们还需要通过进程ID得到进程的名称。当然, 当这些进

  程是隐藏进程的时候我们就不能使用ToolHelp API来实现。所以我们应该访问进程内存通过读取该进程的PEB得到进程名称。PEB地址可以用

  ZwQueryInformationProcess函数获得。以上所说的功能实现代码如下:

  Code:

  function GetNameByPid(Pid: dword): string;

  var

  hProcess, Bytes: dword;

  Inf PROCESS_BASIC_INFORMATION;

  ProcessParametres: pointer;

  ImagePath: TUnicodeString;

  ImgPath: array[0..MAX_PATH] of WideChar;

  begin

  Result := '';

  ZeroMemory(@ImgPath, MAX_PATH * SizeOf(WideChar));

  hProcess := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, false, Pid);

  if ZwQueryInformationProcess(hProcess, ProcessBasicInformation, @Info,

  SizeOf(PROCESS_BASIC_INFORMATION), nil) = STATUS_SUCCESS then

  begin

  if ReadProcessMemory(hProcess, pointer(dword(Info.PebBaseAddress) + $10),

  @ProcessParametres, SizeOf(pointer), Bytes) and

  ReadProcessMemory(hProcess, pointer(dword(ProcessParametres) + $38),

  @ImagePath, SizeOf(TUnicodeString), Bytes) and

  ReadProcessMemory(hProcess, ImagePath.Buffer, @ImgPath,

  ImagePath.Length, Bytes) then

  begin

  Result := ExtractFileName(WideCharToString(ImgPath));

  end;

  end;

  CloseHandle(hProcess);

  end;

  当然, 用户态隐藏进程的检测方法不止这些, 还可以想一些稍微复杂一点的新方法(比如, 用SetWindowsHookEx函数对可访问进程的注入和当

  我们的DLL并成功加载后对进程列表的分析), 但是现在我们将用上面提到的方法来解决问题。这些方法的优点是他们可以简单地编程实现, 并

  且除了可以检测到用户态的隐藏进程, 还可以检测到少数的在内核态实现的隐藏进程... 要实现真正可靠的进程隐藏工具我们应该使用Windows

  未公开的内核数据结构编写内核驱动程序。

  内核态(Ring 0)的检测

  恭喜你, 我们终于开始进行内核态隐藏进程的分析。内核态的检测方法同用户态的检测方法的主要区别是所有的进程列表都没有使用API调用而

  是直接来自系统内部数据结构。在这些检测方法下隐藏进程要困难得多, 因为它们都是基于同Windows内核相同的原理实现的, 并且从这些内核

  数据结构中删除进程将导致该进程完全失效。

  内核中的进程是什么?每一个进程都有自己的地址空间, 描述符, 线程等, 内核的数据结构就涉及这些东西。每一个进程都是由EPROCESS结构

  描述, 而所有进程的结构都被一个双向循环链表维护。进程隐藏的一个方法就是改变进程结构链表的指针, 使得链表枚举跳过自身达到进程隐

  藏的目的。避开进程枚举并不影响进程的任何功能。无论怎样, EPROCESS结构总是存在的, 对一个进程的正常功能来说它是必要的。在内核态

  检测隐藏进程的主要方法就是对这个结构的检查。

  我们应该定义一下将要储存的进程信息的变量格式。这个变量格式应该很方便地存储来自驱动的数据(附录)。结构定义如下:

  Code:

  typedef struct _ProcessRecord

  {

  ULONG Visibles;

  ULONG SignalState;

  BOOLEAN Present;

  ULONG ProcessId;

  ULONG ParrentPID;

  PEPROCESS pEPROCESS;

  CHAR ProcessName[256];

  } TProcessRecord, *PProcessRecord;

  应该为这些结构分配连续的大块的内存, 并且不设置最后一个结构的Present标志。

  在内核中使用ZwQuerySystemInformation函数得到进程列表。

  我们先从最简单的方式开始, 通过ZwQuerySystemInformation函数得到进程列表:

  Code:

  PVOID GetNativeProcessList(ULONG *MemSize)

  {

  ULONG PsCount = 0;

  PVOID Info = GetInfoTable(SystemProcessesAndThreadsInformation);

  PSYSTEM_PROCESSES Proc;

  PVOID Mem = NULL;

  PProcessRecord Data;

  if (!Info) return NULL; else Proc = Info;

  do

  {

  Proc = (PSYSTEM_PROCESSES)((ULONG)Proc + Proc->NextEntryDelta);

  PsCount++;

  } while (Proc->NextEntryDelta);

  *MemSize = (PsCount + 1) * sizeof(TProcessRecord);

  Mem = ExAllocatePool(PagedPool, *MemSize);

  if (!Mem) return NULL; else Data = Mem;

  Proc = Info;

  do

  {

  Proc = (PSYSTEM_PROCESSES)((ULONG)Proc + Proc->NextEntryDelta);

  wcstombs(Data->ProcessName, Proc->ProcessName.Buffer, 255);

  Data->Present = TRUE;

  Data->ProcessId = Proc->ProcessId;

  Data->ParrentPID = Proc->InheritedFromProcessId;

  PsLookupProcessByProcessId((HANDLE)Proc->ProcessId, &Data->pEPROCESS);

  ObDereferenceObject(Data->pEPROCESS);

  Data++;

  } while (Proc->NextEntryDelta);

  Data->Present = FALSE;

  ExFreePool(Info);

  return Mem;

  }

  以这个函数做参考, 任何内核态的隐藏进程都不会被检测出来, 但是所有的用户态隐藏进程如hxdef是绝对逃不掉的。

  在下面的代码中我们可以简单地用GetInfoTable函数来得到信息。为了防止有人问那是什么东西, 下面列出完整的函数代码。

  Code:

  /*

  Receiving buffer with results from ZwQuerySystemInformation.

  */

  PVOID GetInfoTable(ULONG ATableType)

  {

  ULONG mSize = 0x4000;

  PVOID mPtr = NULL;

  NTSTATUS St;

  do

  {

  mPtr = ExAllocatePool(PagedPool, mSize);

  memset(mPtr, 0, mSize);

  if (mPtr)

  {

  St = ZwQuerySystemInformation(ATableType, mPtr, mSize, NULL);

  } else return NULL;

  if (St == STATUS_INFO_LENGTH_MISMATCH)

  {

  ExFreePool(mPtr);

  mSize = mSize * 2;

  }

  } while (St == STATUS_INFO_LENGTH_MISMATCH);

  if (St == STATUS_SUCCESS) return mPtr;

  ExFreePool(mPtr);

  return NULL;

  }

  我认为这段代码是很容易理解的...

  利用EPROCESS结构的双向链表得到进程列表。

  我们又进了一步。接下来我们将通过遍历EPROCESS结构的双向链表来得到进程列表。链表的表头是PsActiveProcessHead, 因此要想正确地枚举

  进程我们需要找到这个并没有被导出的符号。在这之前我们应该知道System进程是所有进程列表中的第一个进程。在DriverEntry例程开始时我

  们需要用PsGetCurrentProcess函数得到当前进程的指针(使用SC管理器的API或者ZwLoadDriver函数加载的驱动始终都是加载到System进程的

  上下文中的), BLink在ActiveProcessLinks中的偏移将指向PsActiveProcessHead。像这样:

  Code:

  PsActiveProcessHead = *(PVOID *)((PUCHAR)PsGetCurrentProcess + ActiveProcessLinksOffset + 4);

  现在就可以遍历这个双向链表来创建进程列表了:

  Code:

  PVOID GetEprocessProcessList(ULONG *MemSize)

  {

  PLIST_ENTRY Process;

  ULONG PsCount = 0;

  PVOID Mem = NULL;

  PProcessRecord Data;

  if (!PsActiveProcessHead) return NULL;

  Process = PsActiveProcessHead->Flink;

  while (Process != PsActiveProcessHead)

  {

  PsCount++;

  Process = Process->Flink;

  }

  PsCount++;

  *MemSize = PsCount * sizeof(TProcessRecord);

  Mem = ExAllocatePool(PagedPool, *MemSize);

  memset(Mem, 0, *MemSize);

  if (!Mem) return NULL; else Data = Mem;

  Process = PsActiveProcessHead->Flink;

  while (Process != PsActiveProcessHead)

  {

  Data->Present = TRUE;

  Data->ProcessId = *(PULONG)((ULONG)Process - ActPsLink + pIdOffset);

  Data->ParrentPID = *(PULONG)((ULONG)Process - ActPsLink + ppIdOffset);

  Data->SignalState = *(PULONG)((ULONG)Process - ActPsLink + 4);

  Data->pEPROCESS = (PEPROCESS)((ULONG)Process - ActPsLink);

  strncpy(Data->ProcessName, (PVOID)((ULONG)Process - ActPsLink + NameOffset), 16);

  Data++;

  Process = Process->Flink;

  }

  return Mem;

  }

  为了得到进程名称、ID和父进程ID, 我们利用它们在EPROCESS结构中的偏移地址(pIdOffset, ppIdOffset, NameOffset, ActPsLink)。这些

  偏移随着Windows系统版本的不同而不同, 因此我们要在进程检测程序的代码中进行区分后得到他们正确的值(附录)。

  任何一个通过API截取方式隐藏的进程都将被上面这个方法检测出来。但是如果进程是通过DKOM(直接处理内核对象 - Direct Kernel Object

  Manipulation)方式隐藏, 那这个方法就失效了, 因为这种进程都被从进程链表中删掉了。

  通过列举调度程序(scheduler)中的线程得到进程列表。

  对付这种隐藏进程(俄文翻译kao注:这个地方原文写的比较模糊, 作者大概的意思应该是“使用DKOM的方式检测隐藏进程”)的其中一种检测

  方式是通过调度程序(scheduler)中的线程列表来得到进程列表。Windows 2000有三个维护线程的双向链表(KiWaitInListHead,

  KiWaitOutListHead, KiDispatcherReadyListHead)。前面两个链表包含等待某种事件的线程, 最后面的链表包含的是等待执行的线程。我们

  处理这些链表, 根据线程链表结构ETHREAD中的偏移就可以得到一个线程的ETHREAD指针(俄文翻译kao注:原文中这句话实在是太难懂了, 希望

  我翻译的正确)。这个结构包括了很多进程相关指针, 也就是结构_KPROCESS *Process(0x44, 0x150)和结构_EPROCESS *ThreadsProcess

  (0x22C, 这仅是Windows 2000中的偏移量)。前面两个指针对于一个线程的功能性没有任何影响, 因此可以很容易修改它们来隐藏进程。相反, 

  第三个指针是当切换地址空间时调度程序(schedler)使用的指针, 所以这个指针是不能修改的。我们就用它来找到拥有某个线程的进程。

  Klister就是使用了这种检测方法, 它的最大的缺点就是只能在Windows 2000平台上工作(但是在这个平台上某个补丁包也会让它失效)。导致

  这个情况发生的原因就是这种程序使用了硬编码的线程链表地址, 而在每个补丁包中这些地址可能都是不同的。

  在程序中使用硬编码地址是很糟糕的解决方案, 操作系统的升级就会使你的程序无法正常工作, 要尽量避免使用这种检测方法。所以应该通过

  分析那些使用了这些链表的内核函数来动态地得到它们的地址。

  首先我们试试看在Windows 2000平台上找出KiWaitInListHead和KiWaitOutListHead.使用链表地址的函数KeWaitForSingleObject代码如下:

  Code:

  .text:0042DE56 mov ecx, offset KiWaitInListHead

  .text:0042DE5B test al, al

  .text:0042DE5D jz short loc_42DE6E

  .text:0042DE5F cmp byte ptr [esi+135h], 0

  .text:0042DE66 jz short loc_42DE6E

  .text:0042DE68 cmp byte ptr [esi+33h], 19h

  .text:0042DE6C jl short loc_42DE73

  .text:0042DE6E mov ecx, offset KiWaitOutListHead

  我们使用反汇编器(用我写的LDasm)反汇编KeWaitForSingleObject函数来获得这些地址。当索引(pOpcode)指向指令“mov ecx,

  KiWaitInListHead”, (pOpcode + 5)指向的就是指令“test al, al”, (pOpcode + 24)指向的就是“mov ecx, KiWaitOutListHead”。

  这样我们就可以通过索引(pOpcode + 1)和(pOpcode + 25)正确地得到KiWaitInListHead和KiWaitOutListHead的地址了。搜索地址的代码

  如下:

  Code:

  void Win2KGetKiWaitInOutListHeads()

  {

  PUCHAR cPtr, pOpcode;

  ULONG Length;

  for (cPtr = (PUCHAR)KeWaitForSingleObject;

  cPtr < (PUCHAR)KeWaitForSingleObject + PAGE_SIZE;

  cPtr += Length)

  {

  Length = SizeOfCode(cPtr, &pOpcode);

  if (!Length) break;

  if (*pOpcode == 0xB9 && *(pOpcode + 5) == 0x84 && *(pOpcode + 24) == 0xB9)

  {

  KiWaitInListHead = *(PLIST_ENTRY *)(pOpcode + 1);

  KiWaitOutListHead = *(PLIST_ENTRY *)(pOpcode + 25);

  break;

  }

  }

  return;

  }

  在Windows 2000平台下我们可以用同样的方法得到KiDispatcherReadyListHead, 搜索KeSetAffinityThread函数:

  Code:

  .text:0042FAAA lea eax, KiDispatcherReadyListHead[ecx*8]

  .text:0042FAB1 cmp [eax], eax

  搜索KiDispatcherReadyListHead函数的代码:

  Code:

  void Win2KGetKiDispatcherReadyListHead()

  {

  PUCHAR cPtr, pOpcode;

  ULONG Length;

  for (cPtr = (PUCHAR)KeSetAffinityThread;

  cPtr < (PUCHAR)KeSetAffinityThread + PAGE_SIZE;

  cPtr += Length)

  {

  Length = SizeOfCode(cPtr, &pOpcode);

  if (!Length) break;

  if (*(PUSHORT)pOpcode == 0x048D && *(pOpcode + 2) == 0xCD && *(pOpcode + 7) == 0x39)

  {

  KiDispatcherReadyListHead = *(PVOID *)(pOpcode + 3);

  break;

  }

  }

  return;

  }

  不幸的是, Windows XP内核完全不同于Windows 2000内核。XP下的调度程序(scheduler)只有两个线程链表:KiWaitListHead和

  KiDispatcherReadyListHead。我们可以通过搜索KeDelayExecutionThread函数来查找KeWaitListHead:

  Code:

  .text:004055B5 mov dword ptr [ebx], offset KiWaitListHead

  .text:004055BB mov [ebx+4], eax

  搜索代码如下:

  Code:

  void XPGetKiWaitListHead()

  {

  PUCHAR cPtr, pOpcode;

  ULONG Length;

  for (cPtr = (PUCHAR)KeDelayExecutionThread;

  cPtr < (PUCHAR)KeDelayExecutionThread + PAGE_SIZE;

  cPtr += Length)

  {

  Length = SizeOfCode(cPtr, &pOpcode);

  if (!Length) break;

  if (*(PUSHORT)cPtr == 0x03C7 && *(PUSHORT)(pOpcode + 6) == 0x4389)

  {

  KiWaitInListHead = *(PLIST_ENTRY *)(pOpcode + 2);

  break;

  }

  }

  return;

  }

  最困难的是查找KiDispatcherReadyListHead。主要的问题是KiDispatcherReadyListHead的地址并没有被任何一个导出的函数使用。因此就要

  用更加复杂的搜索算法搞定它。就从KiDispatchInterrupt函数开始, 我们感兴趣的地方只有这里:

  Code:

  .text:00404E72 mov byte ptr [edi+50h], 1

  .text:00404E76 call sub_404C5A

  .text:00404E7B mov cl, 1

  .text:00404E7D call sub_404EB9

  这段代码中的第一个函数调用指向的就是包含KiDispatcherReadyListHead引用的函数。尽管如此, 搜索KiDispatcherReadyListHead的地址却

  变的更加复杂, 因为这个函数的相关代码在Windows XP SP1和SP2中是不同的。在SP2中它是这个样子:

  Code:

  .text:00404CCD add eax, 60h

  .text:00404CD0 test bl, bl

  .text:00404CD2 lea edx, KiDispatcherReadyListHead[ecx*8]

  .text:00404CD9 jnz loc_401F12

  .text:00404CDF mov esi, [edx+4]

  And in SP1:

  SP1中是这样的:

  Code:

  .text:004180FE add eax, 60h

  .text:00418101 cmp [ebp+var_1], bl

  .text:00418104 lea edx, KiDispatcherReadyListHead[ecx*8]

  .text:0041810B jz loc_418760

  .text:00418111 mov esi, [edx]

  仅仅查找一个“lea”指令是不可靠的, 因此我们也应该检查“lea”后面的指令(LDasm中的IsRelativeCmd函数)。搜索

  KiDispatcherReadyListHead的全部代码如下:

  Code:

  void XPGetKiDispatcherReadyListHead()

  {

  PUCHAR cPtr, pOpcode;

  PUCHAR CallAddr = NULL;

  ULONG Length;

  for (cPtr = (PUCHAR)KiDispatchInterrupt;

  cPtr < (PUCHAR)KiDispatchInterrupt + PAGE_SIZE;

  cPtr += Length)

  {

  Length = SizeOfCode(cPtr, &pOpcode);

  if (!Length) return;

  if (*pOpcode == 0xE8 && *(PUSHORT)(pOpcode + 5) == 0x01B1)

  {

  CallAddr = (PUCHAR)(*(PULONG)(pOpcode + 1) + (ULONG)cPtr + Length);

  break;

  }

  }

  if (!CallAddr || !MmIsAddressValid(CallAddr)) return;

  for (cPtr = CallAddr; cPtr < CallAddr + PAGE_SIZE; cPtr += Length)

  {

  Length = SizeOfCode(cPtr, &pOpcode);

  if (!Length) return;

  if (*(PUSHORT)pOpcode == 0x148D && *(pOpcode + 2) == 0xCD && IsRelativeCmd(pOpcode + 7))

  {

  KiDispatcherReadyListHead = *(PLIST_ENTRY *)(pOpcode + 3);

  break;

  }

  }

  return;

  }

  找到线程链表地址之后我们就可以非常简单地枚举出那些进程了, 代码如下:

  Code:

  void ProcessListHead(PLIST_ENTRY ListHead)

  {

  PLIST_ENTRY Item;

  if (ListHead)

  {

  Item = ListHead->Flink;

  while (Item != ListHead)

  {

  CollectProcess(*(PEPROCESS *)((ULONG)Item + WaitProcOffset));

  Item = Item->Flink;

  }

  }

  return;

  }

  CollectProcess是一个非常有用的函数, 它可以增加一个进程到进程列表中去。

  通过拦截系统调用得到进程列表。

  任何一个进程都要通过API来和系统进行交互, 而大多数交互都通过系统调用传递给了内核。当然, 进程也可以不使用任何API而存在, 但是这

  样一来它也就不能做任何有用(或有害)的事情。一般而言, 我们的思路是使用系统调用管理器拦截系统调用, 然后得到管理器中当前进程的

  EPROCESS指针。应该在某段时间收集指针列表, 这个表不会包含信息收集时没有使用任何系统调用的进程(比如, 进程的线程都处于等待状态

  )。

  Windows 2000平台使用2Eh中断进行系统调用, 因此我们需要修改IDT中的相应的中断描述符来拦截系统调用, 这就要用sidt指令得到IDT在内存

  中的位置。该指令返回这样一个结构:

  Code:

  typedef struct _Idt

  {

  USHORT Size;

  ULONG Base;

  } TIdt;

  修改2Eh中断向量的代码如下:

  Code:

  void Set2kSyscallHook()

  {

  TIdt Idt;

  __asm

  {

  pushad

  cli

  sidt [Idt]

  mov esi, NewSyscall

  mov ebx, Idt.Base

  xchg [ebx + 0x170], si

  rol esi, 0x10

  xchg [ebx + 0x176], si

  ror esi, 0x10

  mov OldSyscall, esi

  sti

  popad

  }

  }

  当然在卸载驱动之前还要保存原始状态的信息:

  Code:

  void Win2kSyscallUnhook()

  {

  TIdt Idt;

  __asm

  {

  pushad

  cli

  sidt [Idt]

  mov esi, OldSyscall

  mov ebx, Idt.Base

  mov [ebx + 0x170], si

  rol esi, 0x10

  mov [ebx + 0x176], si

  sti

  xor eax, eax

  mov OldSyscall, eax

  popad

  }

  }

  Windows XP使用sysenter/sysexit指令(出现在Pentium 2处理器中)实现系统调用。这些指令的功能由model-specific registers(MSR)控制

  。系统调用管理器的地址保存在MSR寄存器, SYSENTER_EIP_MSR(0x176)中。用rdmsr指令读取MSR寄存器, 同时设置ECX = 要读取的寄存器的号

  码, 结果保存在两个积存器EDX:EAX中。在我们这里, SYSENTER_EIP_MSR积存器是32位积存器, 所以EDX为0, EAX内是系统调用管理器的地址。

  同样地, 我们也可以用wrmsr指令写MSR积存器。有一个地方需要注意:当写32位MSR积存器的时候, EDX应该被清空, 否则将引起异常并且导致

  系统立即崩溃。

  考虑到所有的事情之后, 替代系统调用管理器的代码如下:

  Code:

  void SetXpSyscallHook()

  {

  __asm

  {

  pushad

  mov ecx, 0x176

  rdmsr

  mov OldSyscall, eax

  mov eax, NewSyscall

  xor edx, edx

  wrmsr

  popad

  }

  }

  恢复原始的系统调用管理器代码:

  Code:

  void XpSyscallUnhook()

  {

  __asm

  {

  pushad

  mov ecx, 0x176

  mov eax, OldSyscall

  xor edx, edx

  wrmsr

  xor eax, eax

  mov OldSyscall, eax

  popad

  }

  }

  Windows XP的另外一个特性是它既可以使用sysenter也可以使用int 2Eh来进行系统调用, 所以我们要替换这两种情况下的系统调用管理器。

  我们的新的系统调用管理器应该得到当前进程的EPROCESS指针, 并且如果是一个新的进程, 我们要把这个新的进程加到我们的进程列表中。

  新的系统调用管理器代码如下:

  Code:

  void __declspec(naked) NewSyscall()

  {

  __asm

  {

  pushad

  pushfd

  push fs

  mov di, 0x30

  mov fs, di

  mov eax, fs:[0x124]

  mov eax, [eax + 0x44]

  push eax

  call CollectProcess

  pop fs

  popfd

  popad

  jmp OldSyscall

  }

  }

  得到进程列表的这段代码应该在某个时间段内工作, 所以我们有这样的问题:如果在列表中的进程被删除掉, 在随后的时间内我们将保留一些

  无效指针, 结果就是检测隐藏进程失败或者导致系统BSOD, 

  解决这个问题的办法是, 用PsSetCreateProcessNotifyRoutine函数注册我们的回调

  函数, 这个回调函数将会在系统创建或者销毁一个进程的时候被调用。当进程被销毁时, 我们也应该把它从我们的表中删除掉。

  回调函数的原型如下:

  Code:

  VOID

  (*PCREATE_PROCESS_NOTIFY_ROUTINE) (

  IN HANDLE ParentId,

  IN HANDLE ProcessId,

  IN BOOLEAN Create

  );

  安装回调函数的代码如下:

  Code:

  PsSetCreateProcessNotifyRoutine (NotifyRoutine, FALSE);

  取消回调函数的代码:

  Code:

  PsSetCreateProcessNotifyRoutine (NotifyRoutine, TRUE);

  这里有一个问题, 回调函数总是在系统被销毁的时候创建, 因此我们不可能直接在这个回调函数中删除进程列表中的相应进程。这样我们就要

  用系统的work items, 首先调用IoAllocateWorkItem函数为work item分配内存, 然后调用IoQueueWorkItem函数(俄文翻译者kao注:这一句我

  不太确定...)将任务放置到工作线程队列中。在处理过程中我们不仅仅从进程列表中删除掉已经终止的进程, 而且还要加入新创建的线程。处

  理代码如下:

  Code:

  void WorkItemProc(PDEVICE_OBJECT DeviceObject, PWorkItemStruct Data)

  {

  KeWaitForSingleObject(Data->pEPROCESS, Executive, KernelMode, FALSE, NULL);

  DelItem(&wLastItem, Data->pEPROCESS);

  ObDereferenceObject(Data->pEPROCESS);

  IoFreeWorkItem(Data->IoWorkItem);

  ExFreePool(Data);

  return;

  }

  void NotifyRoutine(IN HANDLE ParentId,

  IN HANDLE ProcessId,

  IN BOOLEAN Create)

  {

  PEPROCESS process;

  PWorkItemStruct Data;

  if (Create)

  {

  PsLookupProcessByProcessId(ProcessId, &process);

  if (!IsAdded(wLastItem, process)) AddItem(&wLastItem, process);

  ObDereferenceObject(process);

  } else

  {

  process = PsGetCurrentProcess();

  ObReferenceObject(process);

  Data = ExAllocatePool(NonPagedPool, sizeof(TWorkItemStruct));

  Data->IoWorkItem = IoAllocateWorkItem(deviceObject);

  Data->pEPROCESS = process;

  IoQueueWorkItem(Data->IoWorkItem, WorkItemProc, DelayedWorkQueue, Data);

  }

  return;

  }

  这是一个相对可靠的隐藏进程的检测方式, 然而虽然没有进程能够不倚赖系统调用, 但还是有一些进程可以在很长一段时间处于等待状态不进

  行系统调用, 我们无法检测出这样的进程。

  只要想做, 躲避开这种检测方式还是很容易的。想要做到这一点, 那就需要改变隐藏进程的系统调用方式(重定向到另外一个中断或者GDT中的

  调用门)。在Windows XP下做这个工作是相当简单的, 因为可以给ntdll.dll中的KiFastSystemCall函数打补丁和创建一个相应的系统调用门。

  在Windows 2000平台下就稍微有点难度了, 因为int 2Eh调用分散遍及整个ntdll, 但是找到并patch所有的地方也并不是很复杂。综上所述, 依

  赖于这种检测方式可不是聪明之举。

  通过遍历句柄表得到进程列表。

  如果你曾经尝试过利用删除PsActiveProcesses链表中的进程节点来隐藏进程, 可能你会注意到当你调用ZwQuerySystemInformation函数枚举句

  柄的时候, 隐藏进程的句柄也会被枚举出来, 并且还能被检测出它的ProcessId。这是因为为了方便枚举句柄, 所有的句柄表都是由一个双向链

  表HandleTableList维护的。Windows 2000下HANDLE_TABLE结构在链表中的偏移等于0x054, Windows XP下为0x01C, 链表由

  HandleTableListHead开始。HANDLE_TABLE结构包括它的宿主进程的指针(QuotaProcess), Windows 2000下这个偏移等于0x00C, Windows XP

  下这个偏移为0x004。通过遍历这个句柄链表我们就可以构建进程列表了。

  首先我们得找到HandleTableListHead。反汇编内核显示它的引用定位在函数的深处, 所以前面我们用过的反汇编代码的方法已经不能在这里使

  用了。要找到HeadleTableListHead, 我们要注意到HandleTableListHead是一个全局的内核变量, 因此它一定是在内核文件的某一个段

  (Section)里面, 并且HandleTableList的其他成员是在动态分配的内存中, 所以总是受到内核地址空间的限制。根据这些, 我们需要得到任

  何一个进程的HandleTable的指针, 然后遍历链表直到找到定位在这个内核地址空间的成员, 那么这个成员就是HandleTableListHead了。

  我们使用ZwQuerySystemInformation和SystemModuleInformation类计算系统内核的基址和大小。它将返回一个所有已经加载了的模块的描述符

  表, 并且这个表的第一个成员始终是“system”。综上所述, 查找HandleTableListHead的代码如下:

  Code:

  void GetHandleTableListHead()

  {

  PSYSTEM_MODULE_INFORMATION_EX Info = GetInfoTable(SystemModuleInformation);

  ULONG NtoskrnlBase = (ULONG)Info->Modules[0].Base;

  ULONG NtoskrnlSize = Info->Modules[0].Size;

  PHANDLE_TABLE HandleTable = *(PHANDLE_TABLE *)((ULONG)PsGetCurrentProcess() + HandleTableOffset);

  PLIST_ENTRY HandleTableList = (PLIST_ENTRY)((ULONG)HandleTable + HandleTableListOffset);

  PLIST_ENTRY CurrTable;

  ExFreePool(Info);

  for (CurrTable = HandleTableList->Flink;

  CurrTable != HandleTableList;

  CurrTable = CurrTable->Flink)

  {

  if ((ULONG)CurrTable > NtoskrnlBase && (ULONG)CurrTable < NtoskrnlBase + NtoskrnlSize)

  {

  HandleTableListHead = CurrTable;

  break;

  }

  }

  }

  这段代码是非常通用的, 它可以运行于任何Windows NT版本的系统上, 并且不仅可以用来查找HandleTableListHead, 也可以用于其他类似的结

  构。

  得到HandleTableListHead地址后我们就可以遍历句柄表并基于这些信息来构建进程列表了。

  Code:

  void ScanHandleTablesList()

  {

  PLIST_ENTRY CurrTable;

  PEPROCESS QuotaProcess;

  for (CurrTable = HandleTableListHead->Flink;

  CurrTable != HandleTableListHead;

  CurrTable = CurrTable->Flink)

  {

  QuotaProcess = *(PEPROCESS *)((PUCHAR)CurrTable - HandleTableListOffset + QuotaProcessOffset);

  if (QuotaProcess) CollectProcess(QuotaProcess);

  }

  }

  F-Secure Black Light和KProcCheck的最后一个版本用的就是这种检测方法。我想你将会很轻松地找到对付这种检测的方法。

  通过扫描PspCidTable得到进程列表。

  有一件有趣的事情需要注意:如果仅仅把进程节点从PsActiveProcesses链表中删除, 它不能够防止使用API函数OpenProcess打开进程。这样就

  有一种检测进程的方法就是尝试穷举Pid然后调用OpenProcess。我不推荐这个方法, 因为它没有任何优点, 我甚至想说这是一种“ ”方案

  。不过它的存在意味着在系统中除了通过PsActiveProcesses得到进程列表之外还可以通过调用OpenProcess。当穷举ProcessId的时候我们会注

  意到一个进程可以被几个不同的Pid打开, 这暗示可能存在着有点像HANDLE_TABLE的另一个进程列表。为了证明这个的猜想, 我们来看看

  ZwOpenProcess函数:

  Code:

  PAGE:0049D59E ; NTSTATUS __stdcall NtOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess,

  POBJECT_ATTRIBUTES ObjectAttributes,PCLIENT_ID ClientId)

  PAGE:0049D59E public NtOpenProcess

  PAGE:0049D59E NtOpenProcess proc near

  PAGE:0049D59E

  PAGE:0049D59E ProcessHandle = dword ptr 4

  PAGE:0049D59E DesiredAccess = dword ptr 8

  PAGE:0049D59E bjectAttributes= dword ptr 0Ch

  PAGE:0049D59E ClientId = dword ptr 10h

  PAGE:0049D59E

  PAGE:0049D59E push 0C4h

  PAGE:0049D5A3 push offset dword_413560 ; int

  PAGE:0049D5A8 call sub_40BA92

  PAGE:0049D5AD xor esi, esi

  PAGE:0049D5AF mov [ebp-2Ch], esi

  PAGE:0049D5B2 xor eax, eax

  PAGE:0049D5B4 lea edi, [ebp-28h]

  PAGE:0049D5B7 stosd

  PAGE:0049D5B8 mov eax, large fs:124h

  PAGE:0049D5BE mov al, [eax+140h]

  PAGE:0049D5C4 mov [ebp-34h], al

  PAGE:0049D5C7 test al, al

  PAGE:0049D5C9 jz loc_4BE034

  PAGE:0049D5CF mov [ebp-4], esi

  PAGE:0049D5D2 mov eax, MmUserProbeAddress

  PAGE:0049D5D7 mov ecx, [ebp+8]

  PAGE:0049D5DA cmp ecx, eax

  PAGE:0049D5DC jnb loc_520CDE

  PAGE:0049D5E2 loc_49D5E2:

  PAGE:0049D5E2 mov eax, [ecx]

  PAGE:0049D5E4 mov [ecx], eax

  PAGE:0049D5E6 mov ebx, [ebp+10h]

  PAGE:0049D5E9 test bl, 3

  PAGE:0049D5EC jnz loc_520CE5

  PAGE:0049D5F2 loc_49D5F2:

  PAGE:0049D5F2 mov eax, MmUserProbeAddress

  PAGE:0049D5F7 cmp ebx, eax

  PAGE:0049D5F9 jnb loc_520CEF

  PAGE:0049D5FF loc_49D5FF:

  PAGE:0049D5FF cmp [ebx+8], esi

  PAGE:0049D602 setnz byte ptr [ebp-1Ah]

  PAGE:0049D606 mov ecx, [ebx+0Ch]

  PAGE:0049D609 mov [ebp-38h], ecx

  PAGE:0049D60C mov ecx, [ebp+14h]

  PAGE:0049D60F cmp ecx, esi

  PAGE:0049D611 jz loc_4CCB88

  PAGE:0049D617 test cl, 3

  PAGE:0049D61A jnz loc_520CFB

  PAGE:0049D620 loc_49D620:

  PAGE:0049D620 cmp ecx, eax

  PAGE:0049D622 jnb loc_520D0D

  PAGE:0049D628 loc_49D628:

  PAGE:0049D628 mov eax, [ecx]

  PAGE:0049D62A mov [ebp-2Ch], eax

  PAGE:0049D62D mov eax, [ecx+4]

  PAGE:0049D630 mov [ebp-28h], eax

  PAGE:0049D633 mov byte ptr [ebp-19h], 1

  PAGE:0049D637 loc_49D637:

  PAGE:0049D637 or dword ptr [ebp-4], 0FFFFFFFFh

  PAGE:0049D63B loc_49D63B:

  PAGE:0049D63B

  PAGE:0049D63B cmp byte ptr [ebp-1Ah], 0

  PAGE:0049D63F jnz loc_520D34

  PAGE:0049D645 loc_49D645:

  PAGE:0049D645 mov eax, PsProcessType

  PAGE:0049D64A add eax, 68h

  PAGE:0049D64D push eax

  PAGE:0049D64E push dword ptr [ebp+0Ch]

  PAGE:0049D651 lea eax, [ebp-0D4h]

  PAGE:0049D657 push eax

  PAGE:0049D658 lea eax, [ebp-0B8h]

  PAGE:0049D65E push eax

  PAGE:0049D65F call SeCreateAccessState

  PAGE:0049D664 cmp eax, esi

  PAGE:0049D666 jl loc_49D718

  PAGE:0049D66C push dword ptr [ebp-34h] ; PreviousMode

  PAGE:0049D66F push ds:stru_5B6978.HighPart

  PAGE:0049D675 push ds:stru_5B6978.LowPart ; PrivilegeValue

  PAGE:0049D67B call SeSinglePrivilegeCheck

  PAGE:0049D680 test al, al

  PAGE:0049D682 jnz loc_4AA7DB

  PAGE:0049D688 loc_49D688:

  PAGE:0049D688 cmp byte ptr [ebp-1Ah], 0

  PAGE:0049D68C jnz loc_520D52

  PAGE:0049D692 cmp byte ptr [ebp-19h], 0

  PAGE:0049D696 jz loc_4CCB9A

  PAGE:0049D69C mov [ebp-30h], esi

  PAGE:0049D69F cmp [ebp-28h], esi

  PAGE:0049D6A2 jnz loc_4C1301

  PAGE:0049D6A8 lea eax, [ebp-24h]

  PAGE:0049D6AB push eax

  PAGE:0049D6AC push dword ptr [ebp-2Ch]

  PAGE:0049D6AF call PsLookupProcessByProcessId

  PAGE:0049D6B4 loc_49D6B4:

  正如你看到的, 这段代码拷贝给定的指针, 检查是否指向用户地址空间, 核对访问权限和是否有“SetDebugPrivilege”的权限, 然后从

  CLIENT_ID结构中找到ProcessId并传递给PsLookupProcessByProcessId函数, PsLookupProcessByProcessId的功能是得到ProcessId的EPROCESS

  。函数的其余部分对我们来说没什么用, 现在我们来看看PsLookupProcessByProcessId:

  Code:

  PAGE:0049D725 public PsLookupProcessByProcessId

  PAGE:0049D725 PsLookupProcessByProcessId proc near

  PAGE:0049D725

  PAGE:0049D725

  PAGE:0049D725 ProcessId = dword ptr 8

  PAGE:0049D725 Process = dword ptr 0Ch

  PAGE:0049D725

  PAGE:0049D725 mov edi, edi

  PAGE:0049D727 push ebp

  PAGE:0049D728 mov ebp, esp

  PAGE:0049D72A push ebx

  PAGE:0049D72B push esi

  PAGE:0049D72C mov eax, large fs:124h

  PAGE:0049D732 push [ebp+ProcessId]

  PAGE:0049D735 mov esi, eax

  PAGE:0049D737 dec dword ptr [esi+0D4h]

  PAGE:0049D73D push PspCidTable

  PAGE:0049D743 call ExMapHandleToPointer

  PAGE:0049D748 mov ebx, eax

  PAGE:0049D74A test ebx, ebx

  PAGE:0049D74C mov [ebp+ProcessId], STATUS_INVALID_PARAMETER

  PAGE:0049D753 jz short loc_49D787

  PAGE:0049D755 push edi

  PAGE:0049D756 mov edi, [ebx]

  PAGE:0049D758 cmp byte ptr [edi], 3

  PAGE:0049D75B jnz short loc_49D77A

  PAGE:0049D75D cmp dword ptr [edi+1A4h], 0

  PAGE:0049D764 jz short loc_49D77A

  PAGE:0049D766 mov ecx, edi

  PAGE:0049D768 call sub_4134A9

  PAGE:0049D76D test al, al

  PAGE:0049D76F jz short loc_49D77A

  PAGE:0049D771 mov eax, [ebp+Process]

  PAGE:0049D774 and [ebp+ProcessId], 0

  PAGE:0049D778 mov [eax], edi

  PAGE:0049D77A loc_49D77A:

  PAGE:0049D77A push ebx

  PAGE:0049D77B push PspCidTable

  PAGE:0049D781 call ExUnlockHandleTableEntry

  PAGE:0049D786 pop edi

  PAGE:0049D787 loc_49D787:

  PAGE:0049D787 inc dword ptr [esi+0D4h]

  PAGE:0049D78D jnz short loc_49D79A

  PAGE:0049D78F lea eax, [esi+34h]

  PAGE:0049D792 cmp [eax], eax

  PAGE:0049D794 jnz loc_52388A

  PAGE:0049D79A loc_49D79A:

  PAGE:0049D79A mov eax, [ebp+ProcessId]

  PAGE:0049D79D pop esi

  PAGE:0049D79E pop ebx

  PAGE:0049D79F pop ebp

  PAGE:0049D7A0 retn 8

  以上我们所看到的, 证实了存在像HANDLE_TABLE一样组织结构的第2个进程列表。这个表叫做PspCidTable, 它包括进程和线程的列表, 

  PsLookupProcessThreadByCid函数和PsLookupThreadByThreadId函数都用到了这个表。我们看到, 句柄和句柄表的指针被传递给了

  ExMapHandleToPointer函数, 该函数(在句柄有效的情况下)返回一个指向描述给定句柄的表的一个元素 - HANDLE_TABLE_ENTRY。当我们用

  PDBdump分析完ntoskrnl.pdb并且得到分析日志后, 会得到如下结果:

  Code:

  struct _HANDLE_TABLE_ENTRY {

  // static data ------------------------------------

  // non-static data --------------------------------

  /**/ /*|0x4|*/ void* Object;

  /**/ /*|0x4|*/ unsigned long ObAttributes;

  /**/ /*|0x4|*/ struct _HANDLE_TABLE_ENTRY_INFO* InfoTable;

  /**/ /*|0x4|*/ unsigned long Value;

  /**/ /*|0x4|*/ unsigned long GrantedAccess;

  /**/ /*|0x2|*/ unsigned short GrantedAccessIndex;

  /**/ /*|0x2|*/ unsigned short CreatorBackTraceIndex;

  /**/ /*|0x4|*/ long NextFreeTableEntry;

  };//

  We can recover HANDLE_TABLE_ENTRY structure from this:

  我们还原一下HANDLE_TABLE_ENTRY结构的C代码:

  Code:

  typedef struct _HANDLE_TABLE_ENTRY

  {

  union

  {

  PVOID Object;

  ULONG ObAttributes;

  PHANDLE_TABLE_ENTRY_INFO InfoTable;

  ULONG Value;

  };

  union

  {

  union

  {

  ACCESS_MASK GrantedAccess;

  struct

  {

  USHORT GrantedAccessIndex;

  USHORT CreatorBackTraceIndex;

  };

  };

  LONG NextFreeTableEntry;

  };

  } HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;

  怎么使用它呢?首先, 我们比较感兴趣的是Object域的内容, 它是被句柄描述的目标指针和这个表的给定元素的用法标志(我将稍后解释这句

  话)。GrantedAccess域指定了通过这个句柄对目标的访问权限许可, 这个很有趣。比如, 以只读方式打开一个文件, 修改这个域之后就可以写

  这个文件了。这个方法可以用在对一些正在被读/写的文件的访问上(比如, 正在被其他进程锁定的文件)。应该回到我们的问题上来了 - 通

  过对PspCidTable的分析得到进程句柄列表。

  要分析它我们得了解句柄表的格式, 这样才能遍历这个列表。在这个地方Windows 2000和Windows XP有着巨大的不同。由于句柄表格式不尽相

  同, 所以我们应该把操作系统分类进行分析。

  因为Windows 2000的句柄表相对简单一些, 所以我们先分析它。先来看看ExMapHandleToPointer函数:

  Code:

  PAGE:00493285 ExMapHandleToPointer proc near

  PAGE:00493285

  PAGE:00493285

  PAGE:00493285 HandleTable = dword ptr 8

  PAGE:00493285 Handle = dword ptr 0Ch

  PAGE:00493285

  PAGE:00493285 push esi

  PAGE:00493286 push [esp+Handle]

  PAGE:0049328A push [esp+4+HandleTable]

  PAGE:0049328E call ExpLookupHandleTableEntry

  PAGE:00493293 mov esi, eax

  PAGE:00493295 test esi, esi

  PAGE:00493297 jz short loc_4932A9

  PAGE:00493299 push esi

  PAGE:0049329A push [esp+4+HandleTable]

  PAGE:0049329E call ExLockHandleTableEntry

  PAGE:004932A3 neg al

  PAGE:004932A5 sbb eax, eax

  PAGE:004932A7 and eax, esi

  PAGE:004932A9 loc_4932A9:

  PAGE:004932A9 pop esi

  PAGE:004932AA retn 8

  PAGE:004932AA ExMapHandleToPointer endp

  这里我们调用搜索HANDLE_TABLE的函数ExMapHandleToPointer以及设置Lock Bit的ExLockHandleTableEntry函数。要了解句柄表的内部结构我

  们必须反汇编这些函数。先从ExpLookupHandleTableEntry函数开始:

  Code:

  PAGE:00493545 ExpLookupHandleTableEntry proc near

  PAGE:00493545

  PAGE:00493545

  PAGE:00493545 HandleTable = dword ptr 0Ch

  PAGE:00493545 Handle = dword ptr 10h

  PAGE:00493545

  PAGE:00493545 push esi

  PAGE:00493546 push edi

  PAGE:00493547 mov edi, [esp+Handle]

  PAGE:0049354B mov eax, 0FFh

  PAGE:00493550 mov ecx, edi

  PAGE:00493552 mov edx, edi

  PAGE:00493554 mov esi, edi

  PAGE:00493556 shr ecx, 12h

  PAGE:00493559 shr edx, 0Ah

  PAGE:0049355C shr esi, 2

  PAGE:0049355F and ecx, eax

  PAGE:00493561 and edx, eax

  PAGE:00493563 and esi, eax

  PAGE:00493565 test edi, 0FC000000h

  PAGE:0049356B jnz short loc_49358A

  PAGE:0049356D mov eax, [esp+HandleTable]

  PAGE:00493571 mov eax, [eax+8]

  PAGE:00493574 mov ecx, [eax+ecx*4]

  PAGE:00493577 test ecx, ecx

  PAGE:00493579 jz short loc_49358A

  PAGE:0049357B mov ecx, [ecx+edx*4]

  PAGE:0049357E test ecx, ecx

  PAGE:00493580 jz short loc_49358A

  PAGE:00493582 lea eax, [ecx+esi*8]

  PAGE:00493585 loc_493585:

  PAGE:00493585 pop edi

  PAGE:00493586 pop esi

  PAGE:00493587 retn 8

  PAGE:0049358A loc_49358A:

  PAGE:0049358A xor eax, eax

  PAGE:0049358C jmp short loc_493585

  PAGE:0049358C ExpLookupHandleTableEntry endp

  除此之外, 我们来看看从ntoskrnl.pdb中得到的HANDLE_TABLE结构:

  Code:

  struct _HANDLE_TABLE {

  // static data ------------------------------------

  // non-static data --------------------------------

  /**/ /*|0x4|*/ unsigned long Flags;

  /**/ /*|0x4|*/ long HandleCount;

  /**/ /*|0x4|*/ struct _HANDLE_TABLE_ENTRY*** Table;

  /**/ /*|0x4|*/ struct _EPROCESS* QuotaProcess;

  /**/ /*|0x4|*/ void* UniqueProcessId;

  /**/ /*|0x4|*/ long FirstFreeTableEntry;

  /**/ /*|0x4|*/ long NextIndexNeedingPool;

  /**/ /*|0x38|*/ struct _ERESOURCE HandleTableLock;

  /**/ /*|0x8|*/ struct _LIST_ENTRY HandleTableList;

  /**/ /*|0x10|*/ struct _KEVENT HandleContentionEvent;

  }; //

  根据这些数据我们用C语言还原这个结构:

  Code:

  typedef struct _WIN2K_HANDLE_TABLE

  {

  ULONG Flags;

  LONG HandleCount;

  PHANDLE_TABLE_ENTRY **Table;

  PEPROCESS QuotaProcess;

  HANDLE UniqueProcessId;

  LONG FirstFreeTableEntry;

  LONG NextIndexNeedingPool;

  ERESOURCE HandleTableLock;

  LIST_ENTRY HandleTableList;

  KEVENT HandleContentionEvent;

  } WIN2K_HANDLE_TABLE , *PWIN2K_HANDLE_TABLE ;

  显而易见, 句柄表由对象表的三个层次的索引组成。现在我们再来看看ExLookhandleTableEntry函数:

  Code:

  PAGE:00492E2B ExLockHandleTableEntry proc near

  PAGE:00492E2B

  PAGE:00492E2B

  PAGE:00492E2B var_8 = dword ptr -8

  PAGE:00492E2B var_4 = dword ptr -4

  PAGE:00492E2B HandleTable = dword ptr 8

  PAGE:00492E2B Entry = dword ptr 0Ch

  PAGE:00492E2B

  PAGE:00492E2B push ebp

  PAGE:00492E2C mov ebp, esp

  PAGE:00492E2E push ecx

  PAGE:00492E2F push ecx

  PAGE:00492E30 push ebx

  PAGE:00492E31 push esi

  PAGE:00492E32 xor ebx, ebx

  PAGE:00492E34 loc_492E34:

  PAGE:00492E34 mov eax, [ebp+Entry]

  PAGE:00492E37 mov esi, [eax]

  PAGE:00492E39 test esi, esi

  PAGE:00492E3B mov [ebp+var_8], esi

  PAGE:00492E3E jz short loc_492E89

  PAGE:00492E40 jle short loc_492E64

  PAGE:00492E42 mov eax, esi

  PAGE:00492E44 or eax, 80000000h // set WIN2K_TABLE_ENTRY_LOCK_BIT

  PAGE:00492E49 mov [ebp+var_4], eax

  PAGE:00492E4C mov eax, [ebp+var_8]

  PAGE:00492E4F mov ecx, [ebp+Entry]

  PAGE:00492E52 mov edx, [ebp+var_4]

  PAGE:00492E55 cmpxchg [ecx], edx

  PAGE:00492E58 cmp eax, esi

  PAGE:00492E5A jnz short loc_492E64

  PAGE:00492E5C mov al, 1

  PAGE:00492E5E loc_492E5E:

  PAGE:00492E5E pop esi

  PAGE:00492E5F pop ebx

  PAGE:00492E60 leave

  PAGE:00492E61 retn 8

  PAGE:00492E64 loc_492E64:

  PAGE:00492E64 mov eax, ebx

  PAGE:00492E66 inc ebx

  PAGE:00492E67 cmp eax, 1

  PAGE:00492E6A jb loc_4BC234

  PAGE:00492E70 mov eax, [ebp+HandleTable]

  PAGE:00492E73 push offset unk_46D240 ; Timeout

  PAGE:00492E78 push 0 ; Alertable

  PAGE:00492E7A push 0 ; WaitMode

  PAGE:00492E7C add eax, 5Ch

  PAGE:00492E7F push 0 ; WaitReason

  PAGE:00492E81 push eax ; Object

  PAGE:00492E82 call KeWaitForSingleObject

  PAGE:00492E87 jmp short loc_492E34

  PAGE:00492E89 loc_492E89:

  PAGE:00492E89 xor al, al

  PAGE:00492E8B jmp short loc_492E5E

  PAGE:00492E8B ExLockHandleTableEntry endp

  这段代码检查了HANDLE_TABLE_ENTRY结构的Object成员的第31位, 设置该位, 如果该位被设置, 意味着等待HANDLE_TABLE的

  HandleContentionEvent。对我们来说设置TABLE_ENTRY_LOCK_BIT才是最重要的, 因为它是目标地址的一部分, 如果标志位没有设置, 我们就会

  得到无效句柄。现在我们明白了句柄表的格式, 可以写代码来遍历这个表了:

  Code:

  void ScanWin2KHandleTable(PWIN2K_HANDLE_TABLE HandleTable)

  {

  int i, j, k;

  PHANDLE_TABLE_ENTRY Entry;

  for (i = 0; i < 0x100; i++)

  {

  if (HandleTable->Table)

  {

  for (j = 0; j < 0x100; j++)

  {

  if (HandleTable->Table[j])

  {

  for (k = 0; k < 0x100; k++)

  {

  Entry = &HandleTable->Table[j][k];

  if (Entry->Object)

  ProcessObject((PVOID)((ULONG)Entry->Object | WIN2K_TABLE_ENTRY_LOCK_BIT));

  }

  }

  }

  }

  }

  }

  这段代码处理了所有表中的成员, 并且为每一个成员调用了ProcessObject函数。ProcessObject函数检测成员类型并且恰当地处理了它们。这

  个函数代码如下:

  Code:

  void ProcessObject(PVOID Object)

  {

  POBJECT_HEADER bjectHeader = OBJECT_TO_OBJECT_HEADER(Object);

  if (ObjectHeader->Type == *PsProcessType) CollectProcess(Object);

  if (ObjectHeader->Type == *PsThreadType) ThreadCollect(Object);

  }

  我们已经了解了Windows 2000下的句柄表结构, 现在开始分析Windows XP的表结构。从反汇编ExpLookupHandleTableEntry函数开始:

  Code:

  PAGE:0048D3C1 ExpLookupHandleTableEntry proc near

  PAGE:0048D3C1

  PAGE:0048D3C1

  PAGE:0048D3C1 HandleTable = dword ptr 8

  PAGE:0048D3C1 Handle = dword ptr 0Ch

  PAGE:0048D3C1

  PAGE:0048D3C1 mov edi, edi

  PAGE:0048D3C3 push ebp

  PAGE:0048D3C4 mov ebp, esp

  PAGE:0048D3C6 and [ebp+Handle], 0FFFFFFFCh

  PAGE:0048D3CA mov eax, [ebp+Handle]

  PAGE:0048D3CD mov ecx, [ebp+HandleTable]

  PAGE:0048D3D0 mov edx, [ebp+Handle]

  PAGE:0048D3D3 shr eax, 2

  PAGE:0048D3D6 cmp edx, [ecx+38h]

  PAGE:0048D3D9 jnb loc_4958D6

  PAGE:0048D3DF push esi

  PAGE:0048D3E0 mov esi, [ecx]

  PAGE:0048D3E2 mov ecx, esi

  PAGE:0048D3E4 and ecx, 3 // ecx - table level

  PAGE:0048D3E7 and esi, not 3 // esi - pointer to first table

  PAGE:0048D3EA sub ecx, 0

  PAGE:0048D3ED jnz loc_48DEA4

  PAGE:0048D3F3 lea eax, [esi+eax*8]

  PAGE:0048D3F6 loc_48D3F6:

  PAGE:0048D3F6 pop esi

  PAGE:0048D3F7 loc_48D3F7:

  PAGE:0048D3F7 pop ebp

  PAGE:0048D3F8 retn 8

  PAGE:0048DEA4 loc_48DEA4:

  PAGE:0048DEA4 dec ecx

  PAGE:0048DEA5 mov ecx, eax

  PAGE:0048DEA7 jnz loc_52F57A

  PAGE:0048DEAD shr ecx, 9

  PAGE:0048DEB0 mov ecx, [esi+ecx*4]

  PAGE:0048DEB3 loc_48DEB3:

  PAGE:0048DEB3 and eax, 1FFh

  PAGE:0048DEB8 lea eax, [ecx+eax*8]

  PAGE:0048DEBB jmp loc_48D3F6

  PAGE:0052F57A loc_52F57A:

  PAGE:0052F57A shr ecx, 13h

  PAGE:0052F57D mov edx, ecx

  PAGE:0052F57F mov ecx, [esi+ecx*4]

  PAGE:0052F582 shl edx, 13h

  PAGE:0052F585 sub eax, edx

  PAGE:0052F587 mov edx, eax

  PAGE:0052F589 shr edx, 9

  PAGE:0052F58C mov ecx, [ecx+edx*4]

  PAGE:0052F58F jmp loc_48DEB3

  再来看看ntoskrnl.pdb中的HANDLE_TABLE:

  Code:

  struct _HANDLE_TABLE {

  // static data ------------------------------------

  // non-static data --------------------------------

  /**/ /*|0x4|*/ unsigned long TableCode;

  /**/ /*|0x4|*/ struct _EPROCESS* QuotaProcess;

  /**/ /*|0x4|*/ void* UniqueProcessId;

  /**/ /*|0x10|*/ struct _EX_PUSH_LOCK HandleTableLock[4];

  /**/ /*|0x8|*/ struct _LIST_ENTRY HandleTableList;

  /**/ /*|0x4|*/ struct _EX_PUSH_LOCK HandleContentionEvent;

  /**/ /*|0x4|*/ struct _HANDLE_TRACE_DEBUG_INFO* DebugInfo;

  /**/ /*|0x4|*/ long ExtraInfoPages;

  /**/ /*|0x4|*/ unsigned long FirstFree;

  /**/ /*|0x4|*/ unsigned long LastFree;

  /**/ /*|0x4|*/ unsigned long NextHandleNeedingPool;

  /**/ /*|0x4|*/ long HandleCount;

  /**/ /*|0x4|*/ unsigned long Flags;

  /**/ /*|0x1|*/ unsigned char StrictFIF0:1;

  }; //

  利用以上信息还原该结构:

  Code:

  typedef struct _XP_HANDLE_TABLE

  {

  ULONG TableCode;

  PEPROCESS QuotaProcess;

  PVOID UniqueProcessId;

  EX_PUSH_LOCK HandleTableLock[4];

  LIST_ENTRY HandleTableList;

  EX_PUSH_LOCK HandleContentionEvent;

  PHANDLE_TRACE_DEBUG_INFO DebugInfo;

  LONG ExtraInfoPages;

  ULONG FirstFree;

  ULONG LastFree;

  ULONG NextHandleNeedingPool;

  LONG HandleCount;

  LONG Flags;

  UCHAR StrictFIFO;

  } XP_HANDLE_TABLE, *PXP_HANDLE_TABLE;

  从上面的表中可以很明显看出ExpLookupHandleTableEntry函数从HANDLE_TABLE结构中得到TableCode的值, 并基于其低2位的内容计算出表的层

  次数。其余的位指向第1层表。因此Windows XP下的HANDLE_TABLE可以拥有1到3个层次, 每一个层次的表的大小为1FFh。当表中记录的数量增加

  时, 系统会自动增加层数。很明显, 当表中记录的数量超过0x200的时候表就会拥有两层, 当大于0x40000时增加到第3层。不知道当销毁对象时

  系统会不会减少表的层数, 我没有注意到这件事。

  Windows XP下没有ExLockHandleTableEntry函数, 因此表中相应的模块被定位在ExMapHandleToPointer函数中。反汇编这个函数看看它做了什

  么?

  Code:

  PAGE:0048F61E ExMapHandleToPointer proc near

  PAGE:0048F61E

  PAGE:0048F61E

  PAGE:0048F61E var_8 = dword ptr -8

  PAGE:0048F61E var_4 = dword ptr -4

  PAGE:0048F61E HandleTable = dword ptr 8

  PAGE:0048F61E Handle = dword ptr 0Ch

  PAGE:0048F61E

  PAGE:0048F61E mov edi, edi

  PAGE:0048F620 push ebp

  PAGE:0048F621 mov ebp, esp

  PAGE:0048F623 push ecx

  PAGE:0048F624 push ecx

  PAGE:0048F625 push edi

  PAGE:0048F626 mov edi, [ebp+Handle]

  PAGE:0048F629 test di, 7FCh

  PAGE:0048F62E jz loc_4A2A36

  PAGE:0048F634 push ebx

  PAGE:0048F635 push esi

  PAGE:0048F636 push edi

  PAGE:0048F637 push [ebp+HandleTable]

  PAGE:0048F63A call ExpLookupHandleTableEntry

  PAGE:0048F63F mov esi, eax

  PAGE:0048F641 test esi, esi

  PAGE:0048F643 jz loc_4A2711

  PAGE:0048F649 mov [ebp+var_4], esi

  PAGE:0048F64C loc_48F64C:

  PAGE:0048F64C mov ebx, [esi]

  PAGE:0048F64E test bl, 1

  PAGE:0048F651 mov [ebp+var_8], ebx

  PAGE:0048F654 jz loc_508844

  PAGE:0048F65A lea eax, [ebx-1]

  PAGE:0048F65D mov [ebp+Handle], eax

  PAGE:0048F660 mov eax, [ebp+var_8]

  PAGE:0048F663 mov ecx, [ebp+var_4]

  PAGE:0048F666 mov edx, [ebp+Handle]

  PAGE:0048F669 cmpxchg [ecx], edx

  PAGE:0048F66C cmp eax, ebx

  PAGE:0048F66E jnz loc_50884C

  PAGE:0048F674 mov eax, esi

  PAGE:0048F676 loc_48F676:

  PAGE:0048F676 pop esi

  PAGE:0048F677 pop ebx

  PAGE:0048F678 loc_48F678:

  PAGE:0048F678 pop edi

  PAGE:0048F679 leave

  PAGE:0048F67A retn 8

  PAGE:0048F67A ExMapHandleToPointer endp

  ExpLookuphandleTableEntry函数返回指向HANDLE_TABLE_ENTRY的指针后, 我们要检查Object域的低字节, 如果是被设置了的, 说明是被清除了

  的(俄文翻译者kao注:希望我翻译的对...), 如果该位没有被设置, 我们要等到它被设置了为止。因此当得到对象地址的时候我们不应该设

  置高位(Windows 2000平台), 而是要清除低位。综上所述, 扫描表的代码如下:

  Code:

  void ScanXpHandleTable(PXP_HANDLE_TABLE HandleTable)

  {

  int i, j, k;

  PHANDLE_TABLE_ENTRY Entry;

  ULONG TableCode = HandleTable->TableCode & ~TABLE_LEVEL_MASK;

  switch (HandleTable->TableCode & TABLE_LEVEL_MASK)

  {

  case 0 :

  for (i = 0; i < 0x200; i++)

  {

  Entry = &((PHANDLE_TABLE_ENTRY)TableCode);

  if (Entry->Object) ProcessObject((PVOID)((ULONG)Entry->Object & ~XP_TABLE_ENTRY_LOCK_BIT));

  }

  break;

  case 1 :

  for (i = 0; i < 0x200; i++)

  {

  if (((PVOID *)TableCode))

  {

  for (j = 0; j < 0x200; j++)

  {

  Entry = &((PHANDLE_TABLE_ENTRY *)TableCode)[j];

  if (Entry->Object) ProcessObject((PVOID)((ULONG)Entry->Object & ~XP_TABLE_ENTRY_LOCK_BIT));

  }

  }

  }

  break;

  case 2 :

  for (i = 0; i < 0x200; i++)

  {

  if (((PVOID *)TableCode))

  {

  for (j = 0; j < 0x200; j++)

  {

  if (((PVOID **)TableCode)[j])

  {

  for (k = 0; k < 0x200; k++)

  {

  Entry = &((PHANDLE_TABLE_ENTRY **)TableCode)[j][k];

  if (Entry->Object)

  ProcessObject((PVOID)((ULONG)Entry->Object & ~XP_TABLE_ENTRY_LOCK_BIT));

  }

  }

  }

  }

  }

  break;

  }

  }

  我们已经明白对象表格式了。现在想要枚举进程我们还需要得到PspCidTable的地址。也许你已经猜到了, 我们要在

  PsLookupProcessByProcessId函数中搜索, 这个函数中的第1个函数调用包含着PspCidTable的地址。代码如下:

  Code:

  void GetPspCidTable()

  {

  PUCHAR cPtr, pOpcode;

  ULONG Length;

  for (cPtr = (PUCHAR)PsLookupProcessByProcessId;

  cPtr < (PUCHAR)PsLookupProcessByProcessId + PAGE_SIZE;

  cPtr += Length)

  {

  Length = SizeOfCode(cPtr, &pOpcode);

  if (!Length) break;

  if (*(PUSHORT)cPtr == 0x35FF && *(pOpcode + 6) == 0xE8)

  {

  PspCidTable = **(PVOID **)(pOpcode + 2);

  break;

  }

  }

  }

  现在我们知道怎样处理PspCidTable了, 可以非常容易地查看到所有进程的表中的所有元素, 分析那些属于隐藏进程的对象, 就像我们在用户态

  做的一样, 如果你已经理解了前面所讲的东西, 你一定可以做得到。

  中文参考:《JIURL玩玩Win2k进程线程篇 HANDLE_TABLE》(jiurl.nease.net/document/JiurlPlayWin2k/PsHandleTable.htm)

  感谢:firstrose, JIURL, linhanshi.

  ☆ 如何保障Web服务器安全

  ☆ 安全检测报告

  ☆ 儿童常见的安全常识

  ☆ Windows 上配置和应用安全服务器教程

  ☆ 关于无线路由器的安全配置方法导读

  ☆ 通过建立安全模型保障Web数据库安全运行

  ☆ 网管必读:系统设置搞定脚本安全WEB安全

  ☆ Nginx漏洞利用与安全加固方法WEB安全

  ☆ 质量安全监督检测活动总结

  ☆ 浅谈我国机动车安全技术检测发展

  以上就是博学多识的网友关于“检测常见ASP.NET配置的安全漏洞WEB安全(精选6篇)”的解说。

免责声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请联系我们,一经查实,本站将立刻删除。

相关推荐