爆破Webp
NSO BLASTPASS iMessage exploit 分析
作者:Ian Beer,Google Project Zero
2023年9月7日,Apple 发布了iOS的带外安全更新:

大约在同一时间,2023年9月7日,Citizen Lab发布了一篇博客文章,将iOS 16.6.1中修复的两个CVE与"在野外捕获的NSO Group零点击、零日漏洞利用"联系起来:
"[目标]是受雇于一家总部位于华盛顿特区、设有国际办事处的民间社会组织的人员...
该漏洞利用链能够在受害者没有任何交互的情况下,入侵运行最新版本iOS(16.6)的iPhone。
漏洞利用涉及包含恶意图像的PassKit附件,这些附件从攻击者的iMessage账户发送给受害者。"
前一天,即2023年9月6日,Apple 向WebP项目报告了一个漏洞,并在报告中表示他们计划在第二天为Apple客户发布一个自定义修复。
第二天,WebP团队在公共git仓库中发布了他们的第一个建议修复,五天后,即9月12日,Google发布了新的Chrome稳定版本,其中包含了WebP修复。Apple和Google都将此问题标记为在野外被利用,提醒其他WebP集成商也应快速集成该修复,同时也引起了安全研究社区的密切关注...
几周后,即2023年9月21日,前Project Zero团队负责人Ben Hawkes(与@mistymntncop合作)在Isosceles博客上发布了关于该漏洞根本原因的第一篇详细分析。几个月后,11月3日,一个名为Dark Navy的团队发布了他们的第一篇博客文章:对WebP漏洞的两部分分析(第一部分 - 第二部分)以及针对Chrome的概念验证漏洞利用(CVE-2023-4863)。
虽然Isosceles和Dark Navy的帖子非常详细地解释了底层的内存损坏漏洞,但他们未能解决这个谜题的另一个迷人部分:究竟如何在一次性的零点击设置中成功利用这个漏洞?正如我们很快将看到的,损坏原语非常有限。在没有样本访问权限的情况下,几乎不可能知道。
11月中旬,在与Amnesty International Security Lab合作下,我能够获得一些BLASTPASS PKPass样本文件以及来自失败漏洞利用尝试的崩溃日志。
这篇博客文章涵盖了我对这些样本的分析,以及弄清楚NSO最近的一个零点击iOS漏洞利用如何真正工作的过程。对我来说,这个过程始于立即休了三个月的陪产假,并于2024年3月恢复,故事从这里开始:
场景设定
关于WebP漏洞的根本原因及其产生的原语的详细分析,我建议先阅读我之前提到的三篇博客文章(Isosceles、Dark Navy 1、Dark Navy 2)。我不会在这里重述他们的分析(既因为你应该阅读他们的原创作品,也因为分析相当复杂!)相反,我将简要讨论WebP以及该漏洞产生的损坏原语。
WebP
WebP是一种相对现代的图像文件格式,首次发布于2010年。实际上,WebP实际上是两种完全不同的图像格式:基于VP8视频编解码器的有损格式和单独的无损格式。这两种格式除了都使用RIFF容器并将字符串WEBP作为第一个块名外,没有任何共同点。从那时起(文件第12字节开始),它们就完全不同了。漏洞存在于无损格式中,RIFF块名为VP8L。
无损WebP广泛使用霍夫曼编码;BLASTPASS样本中至少存在10个霍夫曼树。在文件中,它们存储为规范霍夫曼树,这意味着只保留代码长度。在解压缩时,这些长度直接转换为两级霍夫曼解码表,其中五个最大的表都被压缩到同一个预分配的缓冲区中。这些表的最大大小(事实证明并不完全)是根据它们编码的符号数量预先计算的。如果你看到这部分并且有点困惑,上面引用的其他三篇博客文章详细解释了这一点。
通过控制符号长度,可以定义各种奇怪的树,其中许多是无效的。根本问题是WebP代码只在构建解码表之后检查树的有效性。但解码表的预计算大小仅对有效树是正确的。
正如Isosceles博客文章所指出的,这意味着漏洞的一个基本部分是,触发漏洞会被检测到,尽管是在内存损坏之后,并且图像解析在几行代码后就会停止。这带来了另一个利用谜题:在零点击环境中,如何利用一个每次触发问题都会停止解析任何攻击者控制数据的漏洞?
第二个谜题涉及实际的损坏原语。该漏洞将在霍夫曼表缓冲区末尾的已知偏移处写入一个HuffmanCode结构:
// 霍夫曼查找表条目
typedef struct {
uint8_t bits;
uint16_t value;
} HuffmanCode;
正如DarkNavy指出的,虽然bits和value字段名义上由攻击者控制,但实际上灵活性并不大。第五个霍夫曼表(位于预分配缓冲区末尾的那个,其中一部分可能被越界写入)只有40个符号,将value限制在最大值39(0x27),bits将在1到7之间(对于第二级表条目)。bits和value之间有一个填充字节,这使得可能被越界写入的最大值是0x00270007。而碰巧这正是漏洞利用实际写入的值——而且他们可能没有太多选择。
霍夫曼表分配大小的灵活性也不大。漏洞利用中的表分配是12072(0x2F28)字节,将被四舍五入以适应0x3000字节的libmalloc small区域。选择的代码长度使得溢出像这样发生:

总结:32位值0x270007将被写入0x3000字节霍夫曼表分配末尾0x58字节处。然后WebP解析将失败,解码器将退出。
似曾相识?
Project Zero博客的长期读者此时可能会感到似曾相识……我不是已经写过一篇关于NSO零点击iPhone零日漏洞利用的博客文章了吗?该漏洞利用了从iMessage附件解析的图像中使用的稍微晦涩的无损压缩格式中的漏洞。
确实如此。
BLASTPASS与FORCEDENTRY有许多相似之处,我最初的直觉(结果完全错误)是这个漏洞利用可能采用类似的方法,使用一些更高级的WebP功能来构建一个奇怪的机器。为此,我首先编写了一个WebP解析器,看看实际使用了哪些功能。
变换
与JBIG2非常相似,WebP也支持对输入像素数据的可逆变换:


我最初的理论是,漏洞利用可能以类似于FORCEDENTRY的方式运行,并在图像缓冲区边界之外应用这些变换序列来构建一个奇怪的机器。但在用Python实现了足够的WebP格式来解析VP8L块的每一位之后,很明显它只触发了霍夫曼表溢出,没有其他操作。VP8L块只有1052字节,几乎全部都是触发溢出所需的10个霍夫曼表。
Pass中有什么?
虽然BLASTPASS通常被称为"WebP漏洞"的漏洞利用,但攻击者实际上并不只是发送一个WebP文件(尽管iMessage支持WebP)。他们发送一个PassKit PKPass文件,其中包含一个WebP。这一定有原因。所以让我们退一步,实际看看我收到的一个样本文件:
171K sample.pkpass
$ file sample.pkpass
sample.pkpass: Zip archive data, at least v2.0 to extract, compression method=deflate
PKPass zip存档中有五个文件:
60K background.png
5.5M logo.png
175B manifest.json
18B pass.json
3.3K signature
5.5MB的logo.png就是WebP图像,只是扩展名是.png而不是.webp:
$ file logo.png:
logo.png: RIFF (little-endian) data, Web/P image
PKPass格式最接近的规范似乎是Wallet开发者指南,虽然它没有明确说明.png文件实际上应该是便携式网络图形图像,但这大概是本意。这是与FORCEDENTRY的另一个相似之处,当时使用了类似的技巧在尝试解析GIF时访问PDF解析器。
PKPass文件需要一个有效的签名,该签名包含在manifest.json和signature中。签名有一个可能是假的名字和更多的时间戳,表明PKPass很可能是在每次漏洞利用尝试时动态生成和签名的。
pass.json只是这样:
{"pass": "PKpass"}
最后是background.png:
$ file background.png
background.png: TIFF image data, big-endian, direntries=15, height=16, bps=0, compression=deflate, PhotometricIntepretation=RGB, orientation=upper-left, width=48
有趣。另一个具有误导性扩展名的文件;这次是一个带有.png扩展名的TIFF文件。
我们将在分析后期回到这个TIFF,因为它在漏洞利用流程中起着关键作用,但现在我们将专注于WebP,并做一个简短的插曲:
Blastdoor
到目前为止,我只提到了WebP漏洞,但我在本文开头链接的Apple公告提到了两个独立的CVE:
第一个,ImageIO中的CVE-2023-41064,是WebP漏洞(尽管为了保持混淆,与上游WebP修复的CVE-2023-4863不同——但它们是同一个漏洞)。
第二个,"Wallet"中的CVE-2023-41061,在Apple公告中描述为:"恶意制作的附件可能导致任意代码执行"。
"Citizen Lab称此攻击为'BLASTPASS',因为攻击者找到了一种巧妙的方法来绕过'BlastDoor' iMessage沙箱。我们没有完整的技术细节,但看起来通过将图像漏洞利用捆绑在PassKit附件中,恶意图像将在不同的、未沙箱化的进程中处理。这对应于Apple发布的第一个CVE,CVE-2023-41061。"
这个理论有道理——FORCEDENTRY有一个类似的技巧,JBIG2漏洞实际上是在IMTranscoderAgent内部被利用的,而不是在限制更严格的BlastDoor沙箱中。但在我所有的实验以及我见过的所有野外崩溃日志中,这个假设似乎并不成立。
PKPass文件及其包含的图像确实在BlastDoor沙箱内部被解析,崩溃或有效载荷执行就发生在那里——稍后我们还将看到证据,表明最终被评估的NSExpression有效载荷期望在BlastDoor内部运行。
我猜CVE-2023-41061更可能指的是PKPass的宽松解析,它没有拒绝不是png的图像。
2024年底,我收到了另一组野外崩溃日志,其中包括两个确实强烈表明也存在一条路径可以在MobileSMS进程中触发WebP漏洞,在BlastDoor沙箱之外!有趣的是,时间戳表明这些设备是在2023年11月被攻击的,即漏洞被修补两个月后。
在这些情况下,WebP代码是通过由CKAttachmentMessagePartChatItem创建的ChatKit CKPassPreviewMediaObject在MobileSMS进程中访问到的。
WebP中有什么?
我提到WebP文件中的VP8L块只有大约1KB。然而在上面的文件列表中,WebP文件是5.5MB!那么其余部分是什么呢?扩展我的WebP解析器,我们看到还有一个RIFF块:
EXIF : 0x586bb8
exif is Intel byte alignment
EXIF has n_entries=1
tag=8769 fmt=4 n_components=1 data=1a
subIFD has n_entries=1
tag=927c fmt=7 n_components=586b8c data=2c
这是一个(非常非常大的)EXIF——相机用于存储图像元数据的标准格式——比如相机型号、曝光时间、光圈等。
它是一种基于标签的格式,几乎所有的5.5MB都在一个id为0x927c的标签内。那么这是什么?
浏览一个在线EXIF标签列表,在镜头焦距标签下方和用户评论标签上方,我们发现了0x927c:

这是听起来非常模糊但迷人的:"MakerNote - 制造商特定信息。"
查阅维基百科以获取一些澄清,我们了解到:
"'MakerNote'标签包含通常为专有二进制格式的信息。"
修改webp解析器以转储MakerNote标签,我们看到:
$ file sample.makernote
sample.makernote: Apple binary property list
Apple为"专有二进制格式"选择的格式是二进制plist!
确实:在IDA中查看ImageIO库,在WebP解析器、EXIF解析器、MakerNote解析器和二进制plist解析器之间有一条清晰的路径。
unbplisting
我在之前的博客文章中介绍了二进制plist格式。那是我第二次不得不分析大型bplist。第一次(对于FORCEDENTRY沙箱逃逸)主要是手工完成的,只使用了plutil的人类可读输出。去年,对于Safari沙箱逃逸分析,bplist是437KB,我不得不编写一个自定义的bplist解析器来弄清楚发生了什么。保持指数曲线增长,今年的bplist又大了10倍。
在这种情况下,很明显bplist必须是堆布局的一部分——而且大小为5.5MB,可能相当复杂。那么它在做什么呢?
切换视图
我直觉认为bplist会使用重复的字典键作为堆布局的基本构建块,但运行我的解析器时没有输出任何重复键……直到我意识到我的工具在转储之前直接将解析后的字典存储为Python字典。修复工具以保留键和值的列表后,很明显存在重复键。很多重复键:

在Safari漏洞利用分析中,我描述了我如何使用不同的可视化技术来尝试探索对象的结构,寻找可以用来简化情况的模式。在这种情况下,修改解析器以发出格式良好的花括号和缩进,然后依赖VS Code的自动代码折叠功能,足以浏览和了解布局对象的结构。
有时正确的可视化技术足以弄清楚漏洞利用试图做什么。在这种情况下,原语是基于堆的缓冲区溢出,布局不可避免地会尝试将两个东西放在内存中相邻的位置,我想知道"哪两个东西?"
但无论我盯着和滚动多久,我都无法弄清楚任何事情。是时候尝试不同的方法了。
检测
我编写了一个小助手,使用与MakerNote解析器相同的API加载bplist,并使用Mac Instruments应用程序运行它:

解析单个5.5MB的bplist导致近50万次分配,消耗了近1GB的内存。仅查看这个分配摘要,很明显有很多CFString和CFData对象,可能用于堆整形。进一步查看列表,还有其他有趣的数字:

最后一行中的20'000这个数字太整齐了,不可能是巧合。这个数字与分配的__NSDictionaryM对象数量相符:

最后,在列表的最底部,还有两个突出的分配模式:

有两组非常大的分配:八十个1MB分配和44个4MB分配。
我再次修改了我的bplist工具,以转储每个唯一的字符串或数据缓冲区,以及它被看到的次数和其哈希值。查看文件列表,有一个清晰的模式:
| 对象大小 | 数量 |
|---|---|
| 0x3FFFFF | 44 |
| 0xFFFFF | 80 |
| 0x3FFF | 20 |
| 0x26A9 | 24978 |
| 0x2554 | 44 |
| 0x23FF | 5822 |
| 0x22A9 | 4 |
| 0x1FFF | 2 |
| 0x1EA9 | 26 |
| 0x1D54 | 40 |
| 0x17FF | 66 |
| 0x13FF | 66 |
| 0x3FF | 322 |
| 0x3D7 | 404 |
| 0xF | 112882 |
| 0x8 | 3 |
有大量分配的大小刚好低于十六进制的"整数":0x3ff、0x13ff、0x17ff、0x1fff、0x23ff、0x3fff……这强烈暗示它们的大小被设计为恰好落在某些分配器大小桶内。
几乎所有的分配都只填充了零或'A'。但1MB的那个非常不同:
$ hexdump -C 170ae757_80.bin | head -n 20
00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000010 00 00 00 00 00 00 00 00 80 26 00 00 01 00 00 00 |.........&......|
00000020 1f 00 00 00 00 00 00 00 10 00 8b 56 02 00 00 00 |...........V....|
00000030 b0 c3 31 16 02 00 00 00 60 e3 01 00 00 00 00 00 |..1.....`.......|
*
00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
000004b0 00 00 00 00 00 00 00 00 10 c4 31 16 02 00 00 00 |..........1.....|
000004c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
000004e0 02 1c 00 00 01 00 00 00 00 00 00 00 00 00 00 00 |................|
000004f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000500 00 00 00 00 00 00 00 00 70 80 33 16 02 00 00 00 |........p.3.....|
00000510 b8 b5 e5 57 02 00 00 00 ff ff ff ff ff ff ff ff |...W............|
00000520 58 c4 31 16 02 00 00 00 00 00 00 00 00 00 00 00 |X.1.............|
00000530 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00000550 50 75 2c 18 02 00 00 00 01 00 00 00 00 00 00 00 |Pu,.............|
在1MB对象的hexdump的后面部分,显然有一个NSExpression有效载荷——这个有效载荷在WebP文件上运行strings命令时也可见。iVerify的Matthias Frielingsdorf在BlackHat Asia上做了一个关于这个NSExpression有效载荷初步分析的演讲,我们将在本文末尾回到这一点。
同样引人注目(并且在上面的hexdump中可见):显然有指针在那里。在分析的早期阶段,尚不清楚这是一个以某种方式重新定位的有效载荷,还是存在单独的ASLR泄露步骤。
在稍高的层次上,这个hexdump看起来有点像Objective-C或C++对象,但有些地方很奇怪。为什么前24字节都是零?为什么没有isa指针或虚函数表?看起来好像有一些整数字段在指针之前,但它们是什么?在分析的这一阶段,我一无所知。
动态思考
我做了很多尝试来在真实设备上重现漏洞利用原语;我构建了工具来动态生成和签名合法的PKPass文件,可以通过iMessage发送到测试设备,我可以造成很多崩溃,但我似乎从未深入漏洞利用——堆布局工作的iOS版本范围似乎相当小,而且我没有完全匹配的设备和iOS版本来测试。
无论我尝试什么:通过iMessage发送原始漏洞利用、发送带有触发器和布局的自定义PKPass、在测试应用程序中直接渲染WebP,或者尝试使用PassKit API来渲染PKPass文件,我动态管理的最好结果是触发堆元数据完整性检查失败,我认为这表明漏洞利用失败了。
(有趣的是,使用合法API在应用程序内部渲染PKPass失败,错误是PKPass文件格式错误。确实,漏洞利用样本PKPass格式错误:它缺少多个必需文件。但"安全"的PKPass BlastDoor解析器入口点(PKPassSecurePreviewContextCreateMessagesPreview)在这方面至少不那么严格,会尝试渲染不完整和无效的PKPass)。
尽管让整个PKPass被解析被证明很棘手,但通过一些逆向工程,可以调用正确的底层CoreGraphics API来渲染WebP,并让EXIF/MakerNote被解析。然后通过在霍夫曼表分配时设置断点,我希望能够明显看出溢出目标是什么。但实际上完全不清楚后面的对象是什么:(这里X3指向霍夫曼表的开头,大小为0x3000字节)
(lldb) x/6xg $x3+0x3000
0x112000000: 0x0000000111800000 0x0000000000000000
0x112000010: 0x00000000001a1600 0x0000000000000004
0x112000020: 0x0000000000000001 0x0000000000000019
第一个四字(0x111800000)是一个有效的指针,但这显然不是Objective-C对象,也不像任何其他可识别的对象,与bplist或WebP关系不大。但运行几次测试后,有一个奇怪的模式:
(lldb) x/6xg $x3+0x3000
0x148000000: 0x0000000147800000 0x0000000000000000
0x148000010: 0x000000000019c800 0x0000000000000004
0x148000020: 0x0000000000000001 0x0000000000000019
霍夫曼表是0x2F28字节,分配器将其四舍五入为0x3000。在这两次测试运行中,将分配大小添加到霍夫曼表指针产生了一个可疑的整数。这不可能是巧合。运行更多测试,table+0x3000指针总是8MB对齐的。我记得从我读过的一些关于iOS用户空间分配器的演示中,8MB是一个有意义的数字。这是Synaktiv的一个演示:


8MB是iOS用户空间默认分配器的小型机架区域的大小。看起来他们可能试图布局分配器,而不是针对应用程序特定的数据,而是针对分配器元数据。是时候深入研究一些libmalloc内部机制了!
libmalloc
我建议阅读上面链接的两个演示,以很好地了解iOS默认用户空间malloc实现。Libmalloc在四个抽象级别上管理内存。从大到小依次是:机架、杂志、区域和块。微小、小型和大型机架之间的尺寸分割取决于平台。这个漏洞利用的几乎所有相关分配都来自小型机架,所以我将专注于它。
阅读libmalloc源代码时,我注意到区域尾部虽然仍称为尾部,但现在已移动到区域对象的开头。小型区域以8MB的块管理内存。这8MB被分成(对我们来说)三个相关部分:头部、元数据字数组,然后是形成分配的512字节块:

前0x28字节是头部,其中前两个字段形成小型区域的双向链表:
typedef struct region_trailer {
struct region_trailer *prev;
struct region_trailer *next;
unsigned bytes_used;
unsigned objects_in_use;
mag_index_t mag_index;
volatile int32_t pinned_to_depot;
bool recirc_suitable;
rack_dispose_flags_t dispose_flags;
} region_trailer_t;
小型区域以512字节为单位管理内存,称为块。在iOS上,来自小型区域的分配由最多31个块的连续运行组成。每个块都有一个相关的16位元数据字,称为小型元字,它本身被细分为最高有效位的"空闲"标志和15位计数。
要将连续的块运行标记为正在使用(属于一个分配),第一个元字清除其空闲标志,并将计数设置为运行中的块数。释放时,分配首先被放置在快速重用而不释放的lookaside列表上。但一旦分配真正被释放,分配器将尝试贪婪地合并相邻的块。虽然正在使用的运行不能超过31个块,但空闲运行可以增长到包含整个区域。
布局
下面你可以看到紧跟在包含霍夫曼表作为其最后一个分配的小型区域之后的小型区域的元字数组状态:
(lldb) x/200wh 0x148000028
0x148000028: 0x0019 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x148000038: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x148000048: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x148000058: 0x0000 0x0003 0x0000 0x0000 0x0018 0x0000 0x0000 0x0000
0x148000068: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x148000078: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x148000088: 0x0000 0x0000 0x0000 0x0000 0x0003 0x0000 0x0000 0x001c
0x148000098: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x1480000a8: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x1480000b8: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x1480000c8: 0x0000 0x0000 0x0000 0x001d 0x0000 0x0000 0x0000 0x0000
通过一些简单的数学计算,我们可以将元字数组中的索引转换为其对应的堆指针。这样做可以转储上面显示的分配相关的内存。较大的0x19、0x18和0x1c分配似乎都是通用的布局分配,但两个0x3块分配看起来更有趣。第一个(第一个元字在0x14800005a,用黄色显示)是code_lengths数组,在霍夫曼表构建失败后直接释放。蓝色的0x3块运行(第一个元字在0x148000090)是来自MakerNote的CFSet对象的后备缓冲区,包含对象指针。
回想一下,损坏原语将在0x3000分配(该分配恰好位于这个小型区域的正前方)末尾0x58字节处写入双字0x270007。该损坏具有以下效果(以粗体显示):
(lldb) x/200wh 0x148000028
0x148000028: 0x0019 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x148000038: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x148000048: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x148000058: 0x0007 0x0027 0x0000 0x0000 0x0018 0x0000 0x0000 0x0000
0x148000068: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x148000078: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x148000088: 0x0000 0x0000 0x0000 0x0000 0x0003 0x0000 0x0000 0x001c
0x148000098: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x1480000a8: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x1480000b8: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x1480000c8: 0x0000 0x0000 0x0000 0x001d 0x0000 0x0000 0x0000 0x0000
它将正在使用的分配的大小从3块更改为39块(或从1536字节更改为19968字节)。我之前提到过,正在使用的分配的最大大小应该是31块,但这似乎不是在每个单独的释放路径中都检查。如果事情不太顺利,你会遇到运行时检查。但如果事情顺利,你会遇到这样的情况:
(lldb) x/200wh 0x148000028
0x148000028: 0x0019 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x148000038: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x148000048: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x148000058: 0x0007 0x8027 0x0000 0x0000 0x0018 0x0000 0x0000 0x0000
0x148000068: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x148000078: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x148000088: 0x0000 0x0000 0x0000 0x0000 0x0003 0x