原文链接:https://www.inversecos.com/2022/06/guide-to-reversing-and-exploiting-ios.html
欢迎回来我的国王和王后…这是如何逆向工程和利用iOS二进制文件的第2部分。
在此处查看如何对 iOS 应用程序进行逆向工程和修补的第 1 部分。
在本博客文章结束时,您将能够对arm64 iOS二进制文件进行逆向工程,并以两种方式利用它 – 首先通过缓冲区溢出,然后通过ROP链。再一次,我将逐步引导您完成以下内容:
- 构建和编译您自己的 iOS 二进制文件
- 对二进制文件进行逆向工程
- 在不禁用 ASLR 的情况下计算运行时函数地址
- 缓冲区溢出攻击
- 机械钻研链开发
我们只会使用免费工具,因为我不喜欢在书的事情上花钱。因此,对于这篇博客文章/教程,我编译并构建了一个可以使用和滥用的iOS二进制文件。我还在GitHub上为我所有邪恶的骗子提供了源代码!!!不要以为我不知道你存在:P。
从 Github 下载练习二进制文件“dontpopme” 这里.
这篇博客文章建立在第1部分关于如何在iOS应用程序上执行基本反转的知识之上。不同之处在于,这篇博客文章将重点介绍如何利用iOS arm64二进制文件,我们将利用从反转二进制文件中学到的知识来执行两次攻击。因此,我将向您介绍缓冲区溢出和ROP链攻击。
这篇博客文章分为五个部分,如果你熟悉我的博客文章,那么这应该不足为奇。无论如何,这些是部分:
- 我们将采取的步骤的高级演练
- 工具
- 环境设置
- 我们正在利用的二进制文件简介
- Arm64 缓冲区溢出
- Arm64 ROP 链
- 我乌乌的自拍照
高级步骤
以下是本博客文章将涵盖的内容的高级逐步介绍。我们将详细介绍下面的每个小步骤。
- 通过SFTP将iOS二进制文件上传到越狱的iOS上。如果您不知道如何越狱手机,请参阅unC0ver指南。
- 运行二进制文件以测试其工作原理
- 查找缓冲区溢出漏洞
- 检查iOS崩溃日志以获取链接寄存器(LR)的位置,以便我们可以制作有效负载
- 调试二进制文件以查找我们将在有效负载中跳转到的有趣函数
- 在运行时计算函数地址(在 iOS 上启用了 ASLR)
- 执行缓冲区溢出攻击
- 找到我们要用于ROP链的下一个函数
- 执行 ROP 链
工具
以下是我们将用于执行分析的工具:
- LLDB – https://lldb.llvm.org/
- Radare 2 – https://rada.re/n/
- Hopper Disassembler (演示版) – https://www.hopperapp.com/
- 你越狱的iPhone(我使用的是iOS 14.1的旧手机)
- 你的能力和巨大的大脑
环境设置
为了让您逐步按照本演练进行操作,请确保您已准备好越狱电话,并在同一LAN上设置SSH / SFTP并连接。
第 1 步: 从我的 GitHub 页面
下载易受攻击的二进制文件 请仅下载 dontpopme 二进制文件,而不是源代码。我们将把整个练习作为黑匣子攻击来执行。
第 2 步: SFTP 二进制文件到越狱的 iOS 设备上,发送到 /var/mobile 目录
第 3 步: 修改二进制文件
的权限 chmod 777 dontpopme 🙂
第 4 步: 运行二进制文件并检查它是否正常工作:)
第 5 步: 是时候向二进制显示谁是 boss ^_^了
二进制简介
这个二进制文件被称为“dontpopme”,它是一个Mach-O 64位可执行文件,它将为缓冲区溢出和ROP利用目的服务。
二进制文件中的文件读取函数“fread()”C 函数中存在缓冲区溢出漏洞。这个函数基本上从文件中读取,内存不安全!二进制文件需要您将名为“resume.txt”的文件放在与二进制文件相同的目录中。然后,它将尝试读取内容。放入这些内容的内容可能会导致缓冲区溢出;)
隐藏在二进制文件中的还有另外两个秘密函数,它们永远不会在main中调用。缓冲区溢出的目标是使程序跳转到这些函数之一。ROP 链的目标是缓冲溢出>跳转到函数 1 >跃转到函数 2。
我编译此C代码以使其在iOS上运行的方式是使用Theos和clang的组合。以下是我如何编译和签署C代码:
ARM64 缓冲器溢出指南:
让我们对程序进行反向工程并利用缓冲区溢出。
步骤 1:了解缓冲区溢出攻击
这是我能想到的最简单的方法来解释这种攻击,而不会不必要地使其过于复杂。在这一步中,我假设您已经了解了堆栈是什么以及堆栈如何增长。如果您对其工作原理感到困惑,YouTube上有几个关于它的博客和视频:)
简而言之,对于发生缓冲区溢出,程序中使用了一段易受攻击的代码/函数。举个简单的例子,代码可以看起来像这样:
1 2 |
char userInput[5]; //Create a char array of size 5 gets(userInput); //Read user input into the char array |
上面的示例代码定义了一个大小为 5 个字节的字符数组。但是,C 中读取用户输入的 gets() 函数不会检查用户是否只输入 5 个字节。因此,用户可以输入超过 5 个字节并使缓冲区 5 溢出,从而导致堆栈上的区域覆盖。
结果类似于我在下面制作的图表中描述的内容:
用户将一系列输入传递到缓冲区(在本例中为一堆“A”),后跟内存中某个区域的十六进制地址。如果缓冲区溢出足够多,则可以溢出到覆盖堆栈重要区域(包括返回地址)的点。通过模糊化缓冲区输入来精确计算返回地址在堆栈上的位置,可以在缓冲区的该点放置一个新的内存地址,从而触发程序跳转到此位置。邪恶时代带来邪恶的氛围。这是缓冲区溢出工作的高级前提!
步骤2:模糊程序以查找缓冲区溢出
现在你已经了解了发生了什么。我们将在程序上触发缓冲区溢出。如二进制介绍部分所述,二进制文件期望读取名为“resume.txt”的同一目录中的文件。
让我们在iPhone上创建一个名为“resume.txt”的文件,并管道化一系列值来模糊并计算出缓冲区大小。由于这是一个 64 位系统,因此我们将为每个字母传递 8 个字节。
步骤 3:触发缓冲区溢出
让我们运行程序并允许它读取“resume.txt”。正如预期的那样,我们已经溢出缓冲区,并且我们遇到了“总线错误:10”的错误。这基本上意味着程序正在尝试返回或访问不存在的内存区域。这是因为我们已经设法用不指向真实地址的随机字母覆盖了返回指针:)
步骤 4:检查崩溃日志以确定缓冲区大小
每次在 iOS 中使程序崩溃时,都会创建相应的故障转储。您可以在iPhone上的/private/var/mobile/Library/Logs/CrashReporter/<appname-date>*目录中访问这些内容。
正如你在这里看到的,我已经为我的“dontpopme”二进制文件生成了一个崩溃转储。
如果在此故障转储中向下滚动,您将看到所有各种寄存器以及其中包含哪些信息。您需要注意的一个寄存器是“LR”或0x30寄存器。这称为链接寄存器,它将返回地址保存在;)。
链路寄存器内的值为 0x4646464646454545对于 FFFFFEEE 为十六进制。它之所以倒退,是因为系统是小端序。这意味着在FFFFEE的确切位置是我们需要放置我们想要跳转到的函数的地址的地方。
你怎么知道系统是小端序还是大端序?您如何知道还启用了哪些其他控件?让我向您展示一个可以在iOS设备上运行的命令:)如下面的屏幕截图所示,您可以看到“字节序”是“小”。
步骤5:找到一个有趣的函数跳转到
要找到这些函数,让我们使用 radare2。此工具是免费的,您可以将其安装到越狱的iOS设备上。要运行它,您只需键入“r2<二进制名称>”。然后我们要运行:
- aaa – 分析二进制
- afl – 列出二进制文件中存在的所有函数
从上面的屏幕截图中可以看出,有两个可能是用户定义的函数“change”和“runCode”,其余的都是C-native函数。通过阅读这些函数,您可以看到“scanf”和“fread”,它们都容易受到缓冲区溢出攻击。您还可以看到一个名为“execl”的函数。如果您阅读手册页,您将看到它的功能类似于运行系统命令的函数“system()”。
让我们更深入地了解一下 runCode() 函数。您可以通过键入以下命令来执行此操作:
- s sym._runCode
通过查看下面的 asm,您可以看到此函数触发了 execl() 函数。
查看 change() 的函数,可以看到这个函数更改了一些变量并调用 printf,但是没有 execl() 函数。
步骤 6:反汇编函数以更好地了解发生的情况
为此,我们将使用漏斗拆解器的演示版本。加载二进制文件,然后单击函数“runCode”。您将看到它跳转到函数的程序集输出。
从上面的屏幕截图中,您应该能够推断出此函数正在运行execl()函数,并将“/bin/ls”和“-la”的参数传递给它。这意味着如果我们跳转到此函数,我们应该期望它运行此命令。
让我们仔细看看使用Hopper的change()函数。在这个特定的函数中,我们可以推断出它将变量更改为“/bin/uname”和“-a”。
步骤7:记下要跳转到的地址
我们已经检查了程序中的两个函数。总而言之,我们制定了以下信息:
- runCode() – 执行命令 “/bin/ls -la”
- change() – 不执行代码,但将全局变量更改为“/bin/uname -a”
为了防止缓冲区溢出,让我们跳转到 runCode() 函数。让我们记下静态反汇编中 runCode() 的条目地址:0x100007c88。
步骤 8:获取函数地址以计算 slide
如果比较 radare2 和 Hopper 的主地址,您会注意到这些地址与程序中泄露的主地址不同(如下所示)。这是因为ASLR在iOS上启用了,因此实际的功能地址将位于不同的位置。我们已经知道主地址,因为它被程序泄露,我们只需要在运行时计算函数的真实地址位置。这很容易做到!让我来告诉你。
首先从程序中获取main的泄漏地址。不要退出程序,因为地址每次都会更改。您的地址可能与我的地址不同:)
要计算slide(从静态地址到运行时地址的内存差异),我们还需要获取main()的静态地址。我们可以通过使用LLDB并反汇编main函数来做到这一点:
如您在上面看到的,main的静态地址是:0x100007cc8。写下这两个地址!
步骤 9:计算 slide
数学运算简单如下:
要计算 slide,请执行以下计算:
泄漏的主地址 – 调试器主地址 = 差值
要计算 runCode() 函数在运行时的实际地址位置,请执行以下操作:
调试器 runCode() 地址 + 差异 = 运行时 RunCode() 的实际地址。
现在我们有以下组件:
泄露主地址:0x1047c7cc8
调试器主:0x100007cc8
差值:0x1047c7cc8 – 0x100007cc8 = 0x47C0000
让我们计算 runCode() 的实际地址:
调试器 runCode() 地址: 0x100007c88
实际 runCode() 地址: 0x47C0000 + 0x100007c88 = 0x1047C7C88瞧!此正在运行的程序中 runCode() 的实际地址是 0x1047C7C88。
步骤 10:准备缓冲区负载
现在我们已准备好执行攻击。我们知道LR寄存器在“FFFFFEEE”处从我们原始有效载荷中被覆盖。因此,我们的有效负载需要如下所示:
AAAAAAAABBBBBBBBCCCCCDDDDDDDEEEEEee<MALICIOUS ADDRESS>
为了实现这一点,让我们将文本转换为十六进制等效项:
1 2 3 4 5 6 |
A = x41 B = x42 C = x43 D = x44 E = x45 F = x46 |
由于处理器处于小端序中,因此我们还转换runCode()(0x1047C7C88)的地址以匹配它。这使其成为“\x88\x7c\x7c\x04\01”。由于这是 64 位,因此所有地址都有 8 个字节,因此让我们将此地址填充为“\x88\x7c\x7c\x04\01\x00\x00\x00\x00”,这相当于0x0000001047C7C88。
因此,我们的有效负载需要如下所示:
echo -ne “\x41\x41\x41\x41\x41\x41\x41\x41\x42\x42\x42\x42\x42\x42\x42\x42\x43\x43\x43\x43\x43\x43\x43\x43\x44\x44\x44\x44\x44\x44\x44\x45\x45\x45\x45\x45\x88\x7c\x7c\x04\x01\x00\x00\x00\x00” > resume.txt
步骤 11:检查攻击是否成功
现在,让我们运行我们的 dontpopme 二进制文件,并检查我们是否成功执行了 runCode(),从而导致运行 “ls -la” 命令。如您所见,我们成功了!!!1337警报!
但是如果你阅读程序输出,它说“如果你弄清楚我的名字,你就被录用了”。看起来我们无法仅通过简单的缓冲区溢出来找出名称:(这意味着我们需要做一个ROP链!
ARM64 ROP 链指导
你准备好享受真正的乐趣了吗?我们将学习如何做ROP链!!!!!耶!
步骤1:了解我们将如何进行ROP链
如果您不熟悉ROP链的概念,请查看CTF101的这篇文章。我也会用最简单的方式解释它。
让我们参考下面的基本图,我们基本上要扩展初始缓冲区溢出攻击,通过跳转到链中一个接一个的两个函数。您可以进行多次跳转,但对于此示例,我们只执行两次跳转。通常,在更复杂的ROP链中,您将使用称为“小工具”的东西,它是程序中某处存在的一系列指令。这些小工具包含允许您操作寄存器或执行使您更接近利用目标的操作的指令,并且通常以 RET 指令结尾。我们之所以学习ROP链,是因为由于DEP,它可以防止您将自己的恶意可执行代码注入堆栈。这意味着您被迫使用程序中存在的现有代码和函数来执行您的漏洞利用。通过扫描程序中有用的小工具和功能,您可以按顺序重新调整代码的用途,从而实现最终目标。
在“dontpopme”程序的上下文中,我们将链接我们之前确定的以下两个函数:
- change() – 将两个全局变量更改为“/bin/uname”和“-a”
- runCode() – 执行执行执行两个全局变量中的执行 execl() 传递
这将使我们能够完全绕过程序并实现获取“name”标志的最终目标 – 该标志将由“/bin/uname -a”命令打印。
让我们进入:)
步骤 2:计算slide
为了生成有效负载,我们需要再次收集以下值:
- Leaked Main Address
- Change() entry address
- runCode() entry address
- Debugger Main Address
- Debugger Change() address
- Debugger runCode() address
在缓冲区溢出部分中执行的相同格式中,我们需要计算slide,这是泄漏地址和调试器主地址之间的差异:
泄漏的主地址 – 调试器主地址 = 差异
main 的实际运行时地址如下所示:0x102527cc8。请注意,这已更改,因为我重新运行了该程序:)
change() 的静态地址如下所示,如下所示0x100007c50。我们不想收集条目的地址,而是收集第二条指令(突出显示)。我使用 radare2 按照我在缓冲区溢出部分中提供的说明执行此操作:
入口点处 runCode 的静态地址,如下所示:0x100007c88
以下是我们收集的信息摘要:
1 2 3 4 |
Leaked main: 0x102527cc8 Debugger change(): 0x100007c50 Debugger runCode(): 0x100007c88 Debugger main(): 0x100007cc8 |
首先,让我们再次计算slide:
Leaked Main – Debugger Main: 4333927624 – 4294999240 = 38928384
现在,让我们使用slide计算实际的运行时函数地址:
Real runCode() Address: 38928384 + 4294999176 = 4,333,927,560 = 0x102527C88
Real change() address: 38928384 + 4294999120 = 4,333,927,504 = 0x102527C50
这给我们留下了ROP链的两个关键信息:
- change() entry point: 0x102527C50
- runCode() entry point: 0x102527C88
步骤 3:准备payload
正如我们在缓冲区溢出中指出的那样,LR 覆盖发生在“FFFFFEEE”处。但是,负载看起来与缓冲区溢出略有不同。
如果你注意到,change() 函数中“ret”之前的最后一条指令是“LDP x29, x30”。这基本上是从堆栈中弹出x29和x30寄存器,这意味着x30内的任何内容都将是它跳入的返回地址。
对于进一步的上下文,x30寄存器是“LR”,又名链路寄存器,x29是“FP”,也就是帧指针。
这意味着我们的有效负载需要像这样构建:
- AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEE
- Address of change() – \x50\x7c\x52\x02\x01\x00\x00\x00
- 8 bytes of whatever – \xff\xff\xff\xff\xff\xff\xff\xff
- Address of runCode() – \x88\x7c\x52\x02\x01\x00\x00\x00
我们的漏洞利用有效负载将如下所示:
echo -ne “\x41\x41\x41\x41\x41\x41\x41\x41\x42\x42\x42\x42\x42\x42\x42\x42\x43\x43\x43\x43\x43\x43\x43\x43\x44\x44\x44\x44\x44\x44\x44\x44\x45\x45\x45\x45\x45\x45\x50\x7c\x52\x02\x01\x00\x00\x00\x00\xff\xff\xff\xff\xff\x88\x7c\x52\x02\x01\x00\x00\x00” > resume.txt
步骤 4:执行 ROP 链!
首先,让我们使用有效负载创建“resume.txt”:
接下来,让我们执行程序!瞧,我们做到了:)我们得到了达尔文!
结语
如果您想要更多这样的教程,请告诉我。我想分享这张周末的可爱照片,它激发了我写本教程的灵感。乌乌。这是inspo图片<3