背景:事件

10月16日晚八点半,阿里安全-猎户座实验室漏洞预警捕获了oss网站一条不起眼的漏洞修复披露信息

描述轻描淡写,指出libssh基础软件的实现代码,其服务端模式功能,在与客户端通信建立连接中、客户端用户身份校验时,期待客户端主动发起校验请求数据包时,错误接收到表明客户端确认校验成功的报文,将直接绕过校验过程、建立连接。

简单说,怎么理解这个问题呢?就好像微信添加好友的操作一样,正常应该是你发送一条好友请求过去,对方先看你声称的身份他是否认识,再看你的请求消息是否反映你身份确实没问题,通过之后,会提示您添加通过;但是你现在直接发这样一条好友请求“你已添加了xxx,现在可以开始聊天了”,这个好友就自动加上了一样。

问题听起来原理简单到愚蠢、危害严重,马上开始调查。

背景:SSH连接建立与用户校验过程

SSH是一种服务器为主的连接协议,广泛(可以说是最常用)用于企业服务器登录通信。为聚焦问题,我们仅需要简单描述与问题相关的部分:SSH连接建立与用户验证。

连接建立过程面向传输层,是初始阶段,其作用是保证客户端和服务端的SSH工具协议兼容、建立可信通道。这里可以类比TLS/SSL握手过程,客户端发起请求,双端进行秘钥交换、通信秘钥协商,自不必多讲。

之后进行的用户验证当然是面向用户,保证发起连接请求的一方具有合法有效的登录身份,并依此身份在服务端建立会话。根据思科的协议介绍,其过程如下:

1、客户端发送标识为SSH_MSG_USERAUTH_REQUEST的消息;

2、服务端检验客户端发送的用户名是否有效,无效则发送

SSH_MSG_USERAUTH_FAILURE,并断开连接建立阶段建立的连接;

3、服务端发送

SSH_MSG_USERAUTH_FAILURE消息,并附带服务端支持的身份校验方式。总共有三种:基于证书公钥,密码,基于客户端主机;由策略,服务端可能支持其中1到3种方式;

4、客户端选择上述列表中自己支持的一种校验方式,并重新发送SSH_MSG_USERAUTH_REQUEST消息,这次带上它选择的校验方式以及该方式所需的必要字段数据;

5、基于上述校验方式,若干数据交互进行校验,不展开;

6、若身份校验成功,但服务端仍需要其它校验方式双重校验,走到步骤3,并设置其中部分校验成功标志为true;若校验失败,走到步骤3,并配置部分校验成功标志为false;

7、全部校验通过后,服务端发送一个SSH_MSG_USERAUTH_SUCCESS消息,结束校验过程。

为严谨起见,另外参考SSH 校验协议的RFC标准,对上述过程进行细化:

1、上述步骤2,服务端也可以走到步骤3,但对随后客户端返回的SSH_MSG_USERAUTH_REQUEST置之不理;

2、上述步骤4,客户端可能发送带有不受服务端列出支持的

SSH_MSG_USERAUTH_REQUEST请求,服务端不应据此拒绝连接,应该仅仅忽视,哪怕客户端反复发送这样的无效请求;

3、上述步骤5,在特定的校验方法进行的交互过程中,客户端可随时重新发送SSH_MSG_USERAUTH_REQUEST请求,此时服务端应当放弃之前的特定方法校验状态,并重新从步骤4-5开始;

4、上述步骤7,SSH_MSG_USERAUTH_SUCCESS信息仅应当被发送一次;服务端在发出该消息后,若该回话客户端重新发送

SSH_MSG_USERAUTH_REQUEST请求,服务端应当静默忽略;

5、上述步骤4,客户端可发送一个

SSH_MSG_USERAUTH_REQUEST消息,其中校验方式为none,如果服务器确实支持无校验登录,则直接返回

SSH_MSG_USERAUTH_SUCCESS响应;但是即便如此,服务器也不应在步骤3的支持校验方式列表中标明这种none方式;

6、大量更多细节可能性……

以上内容_其实与本次问题无关_,只是想说明,SSH和其它协议标准本身其实有很大灵活性,对一些细节的规定,哪怕是重要必需的规定,也可能以这样不起眼的方式标注。这也会导致协议的各种实现对标准存在不同的解读,那么实现出来的机制千差万别也可以理解了。

漏洞表象分析:其实很简单

根据官方描述,这个漏洞就在于,在上述SSH用户身份校验过程中的步骤5,本应该是进行若干校验方法的数据交互过程中,一个恶意客户端直接向服务端发送了SSH_MSG_USERAUTH_SUCCESS消息给服务端,导致服务端直接跳过步骤5-7建立了连接。实施攻击也非常简单,甚至不需要借助现有组件,直接写脚本按照RFC标准,与被攻击的以libssh实现的SSH服务端通信,先完成连接建立过程,之后在校验阶段的上述节点发送不合时宜的SUCCESS消息即可。

初看漏洞CVE介绍时候,很多人可能都和我有同样的感想:怎么会有这么明显而愚蠢的漏洞?!怕不是故意留的后门吧!真实跟到代码里面分析后,才会发现……嗯,确实是一个愚蠢的漏洞,但看起来也是我们都可能犯的编程bug。对漏洞直接问题和验证,之前已经有若干篇外部分析,接下来我们简单分析一下成因。

这个漏洞的根本原因在于,libssh同时实现了客户端和服务端的功能,且因为很多机制上的相通之处,比如会话相关的结构体在服务端和客户端代码完全相同,所以并没有严格进行隔离,甚至很多地方没有通过编译器标志做区分,都生成了出来。这样一来,开发脑袋一糊涂就对漏洞关键代码产生了混淆。

协议实现状态机中的特定类型标志消息的处理,libssh代码中采用了注册回调的方式进行响应,这些在packet.c文件代码中全部实现。其中,全部消息类型标志按照RFC定义的数字作为下标,将处理对应消息的回调函数指针存储到一个数组中。有些消息仅在客户端处理,则在这个数组中使用WITH_SERVER宏设定数组对应项为NULL,该数组如下(除非特殊标注,后续代码均取自官方已修复该漏洞的版本):

例如其中的50号消息,在服务端模式下设定为使用ssh_packet_userauth_request回调函数处理消息体,但非服务端模式下处理函数设为NULL。但是我们注意到,针对52号消息类型SSH2_MSG_USERAUTH_SUCCESS,虽然标准中该消息仅由服务端在校验成功后向客户端发送一次,服务端本身不处理从客户端发来的此类消息,但这里认定客户端和服务端都使用ssh_packet_userauth_success函数响应这种消息。这基于什么考虑呢?我们后回书分析。

那么我们就看一下这个ssh_packet_userauth_success函数的处理前序过程。所有消息统一的处理分发回调为如下函数,这也是官方修复代码的关键位置,其中patch代码为:

显然可见,修复漏洞之前,代码针对另一端发送的任意消息,在当前状态机正常的情况下,直接调用ssh_packet_process(session, session->in_packet.type);,再根据in_packet.type调用注册的回调函数。对于SSH2_MSG_USERAUTH_SUCCESS类型消息,也就调用了ssh_packet_userauth_success。在客户端,我们知道这里后续工作就是知晓服务端已认可校验通过,并开始了连接过程。但是,服务端也同样维持有一样的session结构记录当前会话客户端的状态,而在修复漏洞之前,代码中没有明确此处服务端还是客户端的功能区分,服务端也会调用上述函数,用于设定服务端维护的会话状态为通过,从而实现了客户端身份校验的绕过。

上面的漏洞修复代码,仍然没有进行服务端和客户端的功能区分隔离,而是新增了一个全局包过滤过程。查看这个ssh_packet_incoming_filter函数,其中针对所有消息类型,都检查了当前会话的状态,判断当前会话状态是否接受当前数据包类型的正确时机,若否,则数据包筛选放弃。这样我们就可以看一下,在包过滤逻辑中,SSH2_MSG_USERAUTH_SUCCESS消息应当是在客户端和/或服务端的什么状态机模式下接受:

所以说到底还是在这里过滤,若当前主机在会话中为服务端,则直接过滤舍弃。这么看起来,为了实现漏洞修复的干净代码,其实应当在服务端模式下直接在前文处设定该类消息回调为NULL,当前的实现方式应当是为了漏洞修复中的过渡分析吧。

开发bug,还是软件供应链上游后门?

如前所述,这个问题以其形成原理之愚蠢、暴露威胁之致命,让人无法不怀疑它是一个后门。顺着这个思路,我调查了一下这个libssh到底是怎么回事……

SSH的实现有几个重复的轮子:OpenSSH,libssh,libssh2。OpenSSH原本是OpenBSD的一套实现,最终因为完备性得以在*nix上普及;而从历史沿革看,Linux中是libssh先实现了SSHv1协议功能。而之后出现了SSHv2协议,因为某些专利纠结,猜测普及存在一个过程,这之间出现了实现SSHv2协议的libssh2。因此在外部上看到一些讨论,认为libssh和libssh2的区别就在于协议栈,但事实上,在解决了某些非技术问题后,libssh也支持了安全性更好的SSHv2,并且在某版本后官方给出了其对SSHv1的R.I.P。同时libssh2不支持服务端模式、椭圆曲线等大量关键特性(对比可见libssh2官方比较页面),所以从一个SDK角度看,libssh显然更完备。

但也许也是因为是在维持之前版本代码结构的情况下全面改造用于对新协议栈支持的原因,所以libssh的代码看起来难免有一些吞吞吐吐,存在一些不干净和假定,也许这就是这次如此愚蠢的编程bug的原因。……但是真的是如此吗?

实际上,在看到漏洞详情的那一刻,今年负责阿里安全-功守道·软件供应链安全大赛的我直接就联想到,这不就是我们系统基础软件设施·C赛季的一道标志性题目的样子吗?甚至当时就有题目确实在OpenSSH上面动手埋了类似功能的后门,可见这样的想法其实是很直接、容易想到的,只不过libssh的这个真实例子中,这个bug模样的漏洞也完全可以解读为编程手抖的原因。那么这到底是不是有可能是一种蓄意埋藏的后门呢?我做了如下的一些调查和无责任推断。

libssh开发者相关

前面说到,libssh可以说是一个比较小众的组件,在系统和关键应用中依赖的并不多。那么它的信用度如何?在其官网上,仅列举了三个使用libssh的大型项目:KDE使用libssh实现内建的sftp模块,用于进行主机间安全文件传输;GitHub的服务端产品使用libssh,实现SSH信道;X2Go使用libssh实现安全远程桌面应用。前两者也可以说是比较重型,但也仍然是非核心应用社区。对于开源领域公开使用libssh并潜在可能受到影响的面,可参见其他分析。

根据开发页面介绍,libssh只有少数几个人利用业余时间开发完成,但这几个原始开发团队人员无处可考。此外,该软件规避了任何成型的开发团队对产品的所有权,规避任何公司对libssh的背书,要求所有在官方git上贡献代码的、属于单位或组织的个人进行单独特殊协议签署,而同时所有贡献代码的人员(所谓社区)自动拥有对该软件的所有权。所以这样一来,特定代码的形成和引入的追溯也较为麻烦。与此相反,libssh却拥有设计新颖完善的官网,并且显然有专人进行更新维护,在ICANN上查看域名注册whois信息,libssh.org的注册人、管理员、组织信息完全为空。

接下来分析在安全事件出现后,为libssh发布安全补丁和新版本代码的人员,在git上显示其名为Andreas Schneider,邮箱显示组织为cryptomilk。这里访问某不存在的网站Twitter了解人员信息动态,在近几年、漏洞发生前后,该人在持续为libssh提供功能和修复代码,并跟进该漏洞影响、提供修复和使用建议,是一个正常的核心开发人员,此处似乎没有更多信息。

libssh漏洞引入追溯

关于这个漏洞的表层成因,我们已经分析的比较清楚了。这样的说法,显然是因为还发现了一些深层成因,请往下看。

根据官方公告,该漏洞影响的是0.6及以上的全部版本。那么为什么是这个版本开始,是因为旧版本到0.6之间存在过大的改动,还是从0.6版本随一些新特性,人为引入了代码bug(漏洞)呢?是不是可能,旧版本中有充分的校验,在新版本中被不经意地去除;或者新版本引入一些复杂的机制造成了旁路呢?

这里我们首先进行了粗线条的比对,可见0.5.x到0.6之间,整体软件代码架构没有太大变化;而涉及到漏洞相关的几个关键环节,按照先后顺序,有这么几个发现:

1、全局消息处理回调函数ssh_packet_socket_callback,在修复漏洞之前的全部历史版本中,都不存在对特定消息类型校验是否为服务端可接受类型判断,所以bug的引入点不在这里;

2、注册回调的那个数组default_packet_handlers[],新旧版本存在对某些消息类型中,若只有服务端或客户端处理,在新代码中通过WITH_SERVER宏区分编译的情况;但针对SSH2_MSG_USERAUTH_SUCCESS消息,始终是默认在客户端和服务端都注册为ssh_packet_userauth_success回调函数处理;所以bug的引入点也不在这里;

3、直接定位到ssh_packet_userauth_success函数,这里发现了一处从0.6的tag处新增差异:新代码在新版本中,维护的会话状态结构体session中,新增了一个flags字段,并且在这个回调函数中,设定了这个标志。

经过二分查找,我最终定位到了在会话中额外引入FLAG机制的一个代码commit,这确实是在发布0.6版本之前的一处代码改动,可参见GitHub mirror页面信息,这个改动的时间在2012年12月24日:

这里注意到:

1)session.h代码,原本代码中,仅使用session->auth_state和session->session_state两个字段来标识当前会话的状态为已通过校验,但是现在额外新增了一个session->flags字段;且在auth.c代码中,将其SSH_SESSION_FLAG_AUTHENTICATED标志位设为真。

2)同一个commit中,修改了客户端SDK功能实现代码client.c,根据session->flags的SSH_SESSION_FLAG_AUTHENTICATED标志位,设定session->session_state。

那么新增这个session->flags字段,还有什么其它标志位可以表示呢?我们看一下:

就只有消息是否阻塞,以及是否已经校验,这两个标志位,而这两个标志位都是之前由session其它字段表示的,这里新增这样一种机制,只把原来的两个布尔字段放到了一个按位标识里,显得非常去裤释气……可接下来,我定位到了2013年7月14日的另一处代码提交,这处代码改在了其服务端的SDK功能代码文件server.c:

其中在服务器端的ssh_server_connection_callback这个回调函数位置,新增了一段代码,根据session->flags字段的SSH_SESSION_FLAG_AUTHENTICATED,将session->session_state字段设定为SSH_SESSION_STATE_AUTHENTICATED。这样以后,之后的网络交互过程,在检测会话状态时,就会得到已校验的判断,校验绕过的效果这样才最终得以闭环。这段代码有没有很眼熟呢?再往上看一个图,这个代码片段,就是在2012年12月24日新增session->flags字段的同时,添加到client.c文件的客户端实现代码中的,这里看似莫名其妙地被直接复制到了服务端代码中。

这里我们只摆事实,以及合理判断。以上的代码改动,完全是一种不必要的旁路工作,是一种看似随意的闲笔。那么一种代码改动,从工程角度来看,完全没有必要、可能引入问题、添加一种机制之后隔半年才对这种机制的真实利用代码进行补充,这种行为,我只能认定为比较可疑了

当然,说巧不巧,上述两处可疑的代码提交都是由同一个开发者完成:Aris Adamantiadis,立马检查其在某不存在网站Twitter的页面:

其中我们可以得到这么几个信息:

此人为安全研究员&黑客。自言:Will hack for food and shelter. Opinions not even my own.

已经有人公开质疑这次的漏洞是该人特意引入的后门了,对此作者当然是否认三联了……

就这么多,是否足以判断本次漏洞实质为精心构造的软件供应链上游后门,交由各位自行判断。

影响面与问题

发现该问题后第一时刻,当然首先应当确认我们日常使用的SSH服务是否受到影响。这个问题也是受到广泛关注的,在Stack Overflow上面也有人问了同样的问题,回答是OpenSSH的服务端程序sshd是独立实现的SSH协议栈,与libssh完全没有关系,不受影响。

在其它的分析文章里,有安全研究人员分析使用libssh实现的SSH服务端的受影响情况,包括影响面、漏洞利用方式和代码等;同时,上述核心开发者自己也声称,通过使用官方代码附带的服务端功能调用示例代码,进行显式校验,则其实也不受此次漏洞影响。至此看起来本次漏洞影响比较微弱,怀疑有人在此投毒是杞人忧天了。……但是真的如此吗?

我们知道现在*nix上都已经普遍使用了OpenSSH做实现,那么在传统服务器上还另外实现一套SSH服务端有什么意义呢?问题就在这里,libssh从项目性质来看,就瞄准的是SDK场景而非工具场景,为有需要实现SSH通信协议和建立于此的可信通信信道提供开发组件的。而作为二方库嵌入到其它产品中,这种情形比较可能在哪里出现呢?显然我们能想到,类似网络管理、任务下发等场景可能存在,那么在多种网络设备做大型集群管理时,很可能libssh被用来做二次开发、剪裁后使用。

果不其然,随后我们就察觉到F5官方发布安全通告,其BIG-IP (AFM)产品线网络设备产品受到libssh的影响,BIG-IQ Centralized Management是否受到影响仍在调查中。由此可能带来的间接漏洞影响面,以及类似的衍生问题,点到为止,大家自行体会。

由此我们需要思考一串问题:

1)底层网络服务端的SSH功能实现,有没有使用libssh实现的?

毕竟在特定场景下,不都能使用OpenSSH的完备重型工具,且会有二次开发需求将一个轻量的ssh功能集成的可能,那这个时候是可能使用libssh的;而很多场景下,其实业务逻辑并不严格需要SSH功能,只是需要一套比较标准可靠的安全协商加密通信机制,从而选择了嵌入libssh的方式,这样的逻辑,很可能出现在自研的底层网络与主机管理,甚至是新型的IoT物联网设备间通信的情况;

2)其它类型产品还有没有使用libssh的服务端的情况,需要如何确认?

C底层程序的依赖情况,远比Java复杂,只有上述一种特殊情况才能追溯到。在非开源产品开发中使用了libssh的如下三种情况下,这种依赖关系就完全无法从上层追查,只有开发和天知道了:

--直接从rpmfind.net之类的搜索引擎上搜索第三方打包好的RPM包下载,在开发环境引用或在生产环境部署。

--拖取官方代码,在开发环境上预先编译打包为静态库或RPM包,之后为产品所使用。

--拖取官方代码,(可能做部分修改、剪裁、添加)并将代码直接纳入到产品代码中编译。

作为企业中的安全管理人员,需要如何针对以上情况,做事后筛查应急止血?又应当如何准备一套说得过去的事前应对方案呢?

反思:我们能够做什么?

以上问题看起来可能让安全研究人员陷入短暂的沉思……在各位低头找漏洞、抬头看情报的时候,有这样一种疑似隐蔽(事实上从结果推断,基本可认定)的软件供应链上游开发者引入的问题,可以轻易实现炸天的功能,果然安全总是要提防人行道、隔离带、草坪超车的车手啊……

实际上,这样一类问题其实在外比较成熟,归结为deniable bugdoor,大概就是可抵赖的、以貌似编程错误而真实实现后门的一类问题或攻击方式。只不过,因为这样问题本身只可能被少数人用来实现隐蔽而长期的攻击效果,所以到目前为止,相关攻击思路并不多见。而这样的问题连认定都存在技术非技术的问题,是不是更没有人做过发现解决方法的尝试呢?实际上是有的,美国国防部高级研究计划局(DARPA)早在2012年4月(注意这个时间节点!)就提出并立项了Vetting Commodity IT Software and Firmware(VET) 项目,对COTS软件进行审核,而其明确的分析唯一目标,就是可能被解释为无意错误的恶意行为,不关注可毫无疑问定性为恶意(后门,rootkit等)的代码,此次出现的问题,不管是否可以明确认定为人为引入,至少都是在该VET项目的分析目标之中的。而这个VET项目,从立项到结项,其中产出了哪些令人瞩目的成果呢?外界公开可考的,为0。这……大概才是让人心惊的事实吧。

也许,现在不是讨论“软件供应链安全威胁全面爆发距离我们还有多远”的时候,也许我们已经在其中了。对此,我们已经束手无措了吗?

今年四月起,阿里安全猎户座实验室基于对安全业界的责任感、对安全情态的预判,预言了软件供应链上安全威胁的迫切性与真实性,并联合中国信息产业商会信息安全分会等单位,牵头组织了“功守道·软件供应链安全大赛”,采用攻守对抗的形式,对我们认定的国家与企业环境基础设施面临的风险要地全覆盖,进行了如下三个赛季的精彩对抗:

1、系统环境与底层软件基础设施-C赛季,5月12日-6月30日,参见:《上篇》,《下篇》;

2、办公环境与开发运维终端软件-PE赛季,7月7日-8月18日,《上篇》,《下篇》

3、线上环境与服务应用软件生态-Java赛季,8月25日-9月30日,《单章》。

在长达整整半年的分站赛阶段,阿里安全作为策划组织方,创造性地对攻防双方做了大量的假设规划、威胁建模、现有分析方案与能力边界可行性分析,由蓝方队伍进行威胁的模型到代码的转化、基于经验与脑洞的攻击思路拟定植入,由红方队伍进行从商用方案到大量工具自研的准备、赛中程序自动化与人工抠代码并行的辛苦,在最终实现了三方最初都没能预期到的收获,在上述罗列的分析文章中仅能写出一二。

在后面,阿里安全将结合当前已发现的新型威胁和背后反映出的潜在趋势,开启本次大赛的决赛阶段。决赛阶段将在以上分站赛阶段脱颖而出的几支队伍之外,通过赛制设计,尽可能面向所有对软件供应链安全感兴趣或致力于缓解危机的业界、研究领域、安全圈开放,欢迎所有人持续关注:https://softsec.security.alibaba.com/

作者:弗为阿里安全猎户座实验室安全专家,研究范围包含且不限于实用性程序动态分析技术、系统新型威胁模型分析,阿里安全2018“功守道”软件供应链安全大赛组织者。

声明:本文来自阿里安全响应中心,版权归作者所有。文章内容仅代表作者独立观点,不代表安全内参立场,转载目的在于传递更多信息。如有侵权,请联系 anquanneican@163.com。