iOS 反逆向防御¶
概述¶
本章涵盖了推荐用于处理或提供对敏感数据或功能访问权限的应用的深度防御措施。研究表明,许多 App Store 应用通常包含这些措施。
这些措施应根据对未经授权篡改应用和/或代码逆向工程所造成的风险评估,按需应用。
- 应用绝不能使用这些措施来替代安全控制,因此预计应用将满足其他基线安全措施,例如 MASVS 安全控制的其余部分。
- 应用应巧妙地组合使用这些措施,而不是单独使用它们。目的是阻止逆向工程师执行进一步的分析。
- 将某些控制集成到你的应用中可能会增加应用的复杂性,甚至会对应用的性能产生影响。
你可以在这些 OWASP 文档中了解更多关于逆向工程和代码修改的原则和技术风险
通用免责声明¶
缺少任何这些措施都不会导致漏洞 - 相反,它们旨在提高应用抵御逆向工程和特定客户端攻击的能力。
这些措施都不能保证 100% 有效,因为逆向工程师将始终拥有对设备的完全访问权限,因此(在有足够的时间和资源的情况下)将始终获胜!
例如,防止调试实际上是不可能的。如果应用是公开可用的,它可以在不受信任的设备上运行,该设备完全在攻击者的控制之下。一个非常坚定的攻击者最终会通过修补应用二进制文件或使用诸如 Frida 之类的工具在运行时动态修改应用的行为来设法绕过应用的所有反调试控制。
下面讨论的技术将允许你检测攻击者可能针对你的应用的各种方式。由于这些技术是公开记录的,因此通常很容易绕过。使用开源检测技术是提高应用韧性的一个好的第一步,但标准的反检测工具可以轻松绕过它们。商业产品通常提供更高的韧性,因为它们将结合多种技术,例如
- 使用未记录的检测技术
- 以各种方式实施相同的技术
- 在不同的场景中触发检测逻辑
- 为每个构建提供独特的检测组合
- 与后端组件一起工作以进行额外的验证和 HTTP 有效负载加密
- 将检测状态传达给后端
- 高级静态混淆
越狱检测¶
添加越狱检测机制是为了提高逆向工程防御的难度,使在越狱设备上运行应用更加困难。这阻止了逆向工程师喜欢使用的一些工具和技术。像大多数其他类型的防御一样,越狱检测本身并不是很有效,但在应用的源代码中分散检查可以提高整体防篡改方案的有效性。
你可以在 Dana Geist 和 Marat Nigmatullin 的研究报告 “Jailbreak/Root Detection Evasion Study on iOS and Android” 中了解更多关于越狱/Root 检测的信息。
常见的越狱检测检查¶
在这里,我们介绍三种典型的越狱检测技术
基于文件的检查
应用可能会检查通常与越狱相关的文件和目录,例如
/Applications/Cydia.app
/Applications/FakeCarrier.app
/Applications/Icy.app
/Applications/IntelliScreen.app
/Applications/MxTube.app
/Applications/RockApp.app
/Applications/SBSettings.app
/Applications/WinterBoard.app
/Applications/blackra1n.app
/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist
/Library/MobileSubstrate/DynamicLibraries/Veency.plist
/Library/MobileSubstrate/MobileSubstrate.dylib
/System/Library/LaunchDaemons/com.ikey.bbot.plist
/System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist
/bin/bash
/bin/sh
/etc/apt
/etc/ssh/sshd_config
/private/var/lib/apt
/private/var/lib/cydia
/private/var/mobile/Library/SBSettings/Themes
/private/var/stash
/private/var/tmp/cydia.log
/var/tmp/cydia.log
/usr/bin/sshd
/usr/libexec/sftp-server
/usr/libexec/ssh-keysign
/usr/sbin/sshd
/var/cache/apt
/var/lib/apt
/var/lib/cydia
/usr/sbin/frida-server
/usr/bin/cycript
/usr/local/bin/cycript
/usr/lib/libcycript.dylib
/var/log/syslog
检查文件权限
应用可能会尝试写入应用沙箱之外的位置。例如,它可能会尝试在例如 /private
目录中创建一个文件。如果文件成功创建,应用可以假设设备已越狱。
do {
let pathToFileInRestrictedDirectory = "/private/jailbreak.txt"
try "This is a test.".write(toFile: pathToFileInRestrictedDirectory, atomically: true, encoding: String.Encoding.utf8)
try FileManager.default.removeItem(atPath: pathToFileInRestrictedDirectory)
// Device is jailbroken
} catch {
// Device is not jailbroken
}
检查协议处理程序
应用可能会尝试调用众所周知的协议处理程序,例如 cydia://
(默认情况下在安装 Cydia 后可用)。
if let url = URL(string: "cydia://package/com.example.package"), UIApplication.shared.canOpenURL(url) {
// Device is jailbroken
}
自动越狱检测绕过¶
绕过常见越狱检测机制的最快方法是 objection。你可以在 jailbreak.ts 脚本中找到越狱绕过的实现。
手动越狱检测绕过¶
如果自动绕过无效,你需要亲自动手并对应用二进制文件进行逆向工程,直到找到负责检测的代码片段,并静态地修补它们或应用运行时钩子来禁用它们。
步骤 1:逆向工程
当需要逆向工程二进制文件以查找越狱检测时,最明显的方法是搜索已知的字符串,例如“jail”或“jailbreak”。请注意,这并不总是有效的,尤其是在采取了弹性措施的情况下,或者仅仅是当开发者避免了如此明显的术语时。
示例:下载 DVIA-v2,解压缩它,将主二进制文件加载到 radare2 for iOS 并等待分析完成。
r2 -A ./DVIA-v2-swift/Payload/DVIA-v2.app/DVIA-v2
现在你可以使用 is
命令列出二进制文件的符号,并对字符串“jail”应用不区分大小写的 grep (~+
)。
[0x1001a9790]> is~+jail
...
2230 0x001949a8 0x1001949a8 GLOBAL FUNC 0 DVIA_v2.JailbreakDetectionViewController.isJailbroken.allocator__Bool
7792 0x0016d2d8 0x10016d2d8 LOCAL FUNC 0 +[JailbreakDetection isJailbroken]
...
如你所见,有一个具有签名 -[JailbreakDetectionVC isJailbroken]
的实例方法。
步骤 2:动态钩子
现在你可以使用 Frida 通过执行所谓的早期 Instrumentation 来绕过越狱检测,也就是说,在启动时立即替换函数实现。
在你主机上使用 frida-trace
frida-trace -U -f /Applications/DamnVulnerableIOSApp.app/DamnVulnerableIOSApp -m "-[JailbreakDetectionVC isJailbroken]"
这将启动应用,跟踪对 -[JailbreakDetectionVC isJailbroken]
的调用,并为每个匹配的元素创建一个 JavaScript 钩子。用你最喜欢的编辑器打开 ./__handlers__/__JailbreakDetectionVC_isJailbroken_.js
并编辑 onLeave
回调函数。你可以简单地使用 retval.replace()
替换返回值以始终返回 0
onLeave: function (log, retval, state) {
console.log("Function [JailbreakDetectionVC isJailbroken] originally returned:"+ retval);
retval.replace(0);
console.log("Changing the return value to:"+retval);
}
这将提供以下输出
$ frida-trace -U -f /Applications/DamnVulnerableIOSApp.app/DamnVulnerableIOSApp -m "-[JailbreakDetectionVC isJailbroken]:"
Instrumenting functions... `...
-[JailbreakDetectionVC isJailbroken]: Loaded handler at "./__handlers__/__JailbreakDetectionVC_isJailbroken_.js"
Started tracing 1 function. Press Ctrl+C to stop.
Function [JailbreakDetectionVC isJailbroken] originally returned:0x1
Changing the return value to:0x0
反调试检测¶
在使用调试器探索应用时,这是一个非常强大的逆向工程技术。你不仅可以跟踪包含敏感数据的变量并修改应用的控制流,还可以读取和修改内存和寄存器。
有几种适用于 iOS 的反调试技术,可以分为预防性或反应性。当这些技术适当地分布在整个应用中时,它们可以作为一种支持措施来提高整体韧性。
- 预防性技术作为第一道防线,阻止调试器完全附加到应用。
- 反应性技术允许应用检测到调试器的存在,并有机会偏离正常行为。
使用 ptrace¶
如 调试 中所见,iOS XNU 内核实现了一个 ptrace
系统调用,该系统调用缺少正确调试进程所需的大部分功能(例如,它允许附加/单步执行,但不允许读取/写入内存和寄存器)。
尽管如此,ptrace
系统调用的 iOS 实现包含一个非标准且非常有用的功能:防止调试进程。此功能实现为 PT_DENY_ATTACH
请求,如 官方 BSD 系统调用手册中所述。简而言之,它确保没有其他调试器可以附加到调用进程;如果调试器尝试附加,进程将终止。使用 PT_DENY_ATTACH
是一种相当有名的反调试技术,因此你可能在 iOS 渗透测试期间经常遇到它。
在深入了解细节之前,重要的是要知道
ptrace
不是公共 iOS API 的一部分。禁止使用非公共 API,App Store 可能会拒绝包含它们的应用。因此,ptrace
不会在代码中直接调用;当通过dlsym
获取ptrace
函数指针时会调用它。
以下是上述逻辑的示例实现
#import <dlfcn.h>
#import <sys/types.h>
#import <stdio.h>
typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);
void anti_debug() {
ptrace_ptr_t ptrace_ptr = (ptrace_ptr_t)dlsym(RTLD_SELF, "ptrace");
ptrace_ptr(31, 0, 0, 0); // PTRACE_DENY_ATTACH = 31
}
绕过: 为了演示如何绕过这种技术,我们将使用一个实现了这种方法的反汇编二进制文件的示例
让我们分解二进制文件中发生的事情。调用 dlsym
时,将 ptrace
作为第二个参数(寄存器 R1)。寄存器 R0 中的返回值在偏移量 0x1908A 处移动到寄存器 R6。在偏移量 0x19098 处,使用 BLX R6 指令调用寄存器 R6 中的指针值。要禁用 ptrace
调用,我们需要用 NOP
(0x00 0xBF
Little Endian) 指令替换指令 BLX R6
(0xB0 0x47
Little Endian)。修补后,代码将类似于以下内容
Armconverter.com 是一个用于在字节码和指令助记符之间转换的便捷工具。
其他基于 ptrace 的反调试技术的绕过可以在 Alexander O'Mara 的“Defeating Anti-Debug Techniques: macOS ptrace variants” 中找到。
使用 sysctl¶
检测附加到调用进程的调试器的另一种方法涉及 sysctl
。根据 Apple 文档,它允许进程设置系统信息(如果具有适当的权限)或仅检索系统信息(例如进程是否正在被调试)。但是,请注意,仅应用使用 sysctl
的事实可能表明存在反调试控制,尽管这 并非总是如此。
Apple 文档存档包含一个示例,该示例检查调用带有适当参数的 sysctl
返回的 info.kp_proc.p_flag
标志。根据 Apple 的说法,除非 它是用于程序的调试版本,否则你不应使用此代码。
绕过: 绕过此检查的一种方法是修补二进制文件。当编译上面的代码时,代码的后半部分的拆卸版本类似于以下内容
在偏移量 0xC13C 处的指令之后,MOVNE R0, #1
被修补并更改为 MOVNE R0, #0
(字节码中的 0x00 0x20),修补后的代码类似于以下内容
你还可以通过使用调试器本身并在调用 sysctl
时设置断点来绕过 sysctl
检查。在 iOS Anti-Debugging Protections #2 中演示了这种方法。
使用 getppid¶
iOS 上的应用程序可以通过检查其父 PID 来检测它们是否由调试器启动。通常,应用程序由 launchd 进程启动,该进程是在用户模式下运行的第一个进程,并且具有 PID=1。但是,如果调试器启动一个应用程序,我们可以观察到 getppid
返回一个与 1
不同的 PID。此检测技术可以用原生代码(通过系统调用)实现,使用 Objective-C 或 Swift 如下所示
func AmIBeingDebugged() -> Bool {
return getppid() != 1
}
绕过: 与其他技术类似,这也有一个简单的绕过(例如,通过修补二进制文件或使用 Frida 钩子)。
文件完整性检查¶
检查文件完整性有两种常见方法:使用应用程序源代码完整性检查和使用文件存储完整性检查。
应用程序源代码完整性检查¶
在“调试”( 调试)中,我们讨论了 iOS IPA 应用程序签名检查。我们还了解到,坚定的逆向工程师可以通过使用开发者或企业证书重新打包和重新签名应用程序来绕过此检查。使这更加困难的一种方法是添加自定义检查,以确定签名在运行时是否仍然匹配。
Apple 通过 DRM 负责完整性检查。但是,额外的控件(如下面的示例)是可能的。解析 mach_header
以计算指令数据的开始位置,该位置用于生成签名。接下来,将签名与给定的签名进行比较。确保生成的签名存储或编码在其他地方。
int xyz(char *dst) {
const struct mach_header * header;
Dl_info dlinfo;
if (dladdr(xyz, &dlinfo) == 0 || dlinfo.dli_fbase == NULL) {
NSLog(@" Error: Could not resolve symbol xyz");
[NSThread exit];
}
while(1) {
header = dlinfo.dli_fbase; // Pointer on the Mach-O header
struct load_command * cmd = (struct load_command *)(header + 1); // First load command
// Now iterate through load command
//to find __text section of __TEXT segment
for (uint32_t i = 0; cmd != NULL && i < header->ncmds; i++) {
if (cmd->cmd == LC_SEGMENT) {
// __TEXT load command is a LC_SEGMENT load command
struct segment_command * segment = (struct segment_command *)cmd;
if (!strcmp(segment->segname, "__TEXT")) {
// Stop on __TEXT segment load command and go through sections
// to find __text section
struct section * section = (struct section *)(segment + 1);
for (uint32_t j = 0; section != NULL && j < segment->nsects; j++) {
if (!strcmp(section->sectname, "__text"))
break; //Stop on __text section load command
section = (struct section *)(section + 1);
}
// Get here the __text section address, the __text section size
// and the virtual memory address so we can calculate
// a pointer on the __text section
uint32_t * textSectionAddr = (uint32_t *)section->addr;
uint32_t textSectionSize = section->size;
uint32_t * vmaddr = segment->vmaddr;
char * textSectionPtr = (char *)((int)header + (int)textSectionAddr - (int)vmaddr);
// Calculate the signature of the data,
// store the result in a string
// and compare to the original one
unsigned char digest[CC_MD5_DIGEST_LENGTH];
CC_MD5(textSectionPtr, textSectionSize, digest); // calculate the signature
for (int i = 0; i < sizeof(digest); i++) // fill signature
sprintf(dst + (2 * i), "%02x", digest[i]);
// return strcmp(originalSignature, signature) == 0; // verify signatures match
return 0;
}
}
cmd = (struct load_command *)((uint8_t *)cmd + cmd->cmdsize);
}
}
}
绕过
- 修补反调试功能,并通过使用 NOP 指令覆盖相关代码来禁用不需要的行为。
- 修补用于评估代码完整性的任何存储的哈希值。
- 使用 Frida 钩取文件系统 API,并返回原始文件的句柄,而不是修改后的文件。
文件存储完整性检查¶
应用程序可以选择通过在给定键值对或存储在设备上的文件上创建 HMAC 或签名来确保应用程序存储本身的完整性,例如在密钥链、UserDefaults
/NSUserDefaults
或任何数据库中。
例如,应用程序可能包含以下代码以使用 CommonCrypto
生成 HMAC
// Allocate a buffer to hold the digest and perform the digest.
NSMutableData* actualData = [getData];
//get the key from the keychain
NSData* key = [getKey];
NSMutableData* digestBuffer = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, [actualData bytes], (CC_LONG)[key length], [actualData bytes], (CC_LONG)[actualData length], [digestBuffer mutableBytes]);
[actualData appendData: digestBuffer];
此脚本执行以下步骤
- 将数据作为
NSMutableData
获取。 - 获取数据密钥(通常来自密钥链)。
- 计算哈希值。
- 将哈希值附加到实际数据。
- 存储步骤 4 的结果。
之后,它可能会通过执行以下操作来验证 HMAC
NSData* hmac = [data subdataWithRange:NSMakeRange(data.length - CC_SHA256_DIGEST_LENGTH, CC_SHA256_DIGEST_LENGTH)];
NSData* actualData = [data subdataWithRange:NSMakeRange(0, (data.length - hmac.length))];
NSMutableData* digestBuffer = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, [actualData bytes], (CC_LONG)[key length], [actualData bytes], (CC_LONG)[actualData length], [digestBuffer mutableBytes]);
return [hmac isEqual: digestBuffer];
- 将消息和 hmacbytes 提取为单独的
NSData
。 - 重复用于在
NSData
上生成 HMAC 的过程的步骤 1-3。 - 将提取的 HMAC 字节与步骤 1 的结果进行比较。
注意:如果应用程序还加密文件,请确保按照 Authenticated Encryption 中描述的那样,先加密然后再计算 HMAC。
绕过
- 从设备检索数据,如 “设备绑定” 部分中所述。
- 更改检索到的数据并将其返回到存储。
逆向工程工具检测¶
逆向工程师常用的工具、框架和应用程序的存在可能表明有人试图对应用程序进行逆向工程。其中一些工具只能在越狱设备上运行,而另一些工具则强制应用程序进入调试模式或依赖于在手机上启动后台服务。因此,应用程序可能有不同的方法来实现检测逆向工程攻击并对其做出反应,例如通过终止自身。
你可以通过查找相关的应用程序包、文件、进程或其他特定于工具的修改和人工制品来检测已安装的常用逆向工程工具,这些工具是以未修改的形式安装的。在以下示例中,我们将讨论检测 Frida Instrumentation 框架的不同方法,该框架在本指南中以及现实世界中被广泛使用。其他工具(例如 ElleKit)也可以类似地检测到。请注意,注入、钩取和 DBI(动态二进制 Instrumentation)工具通常可以通过运行时完整性检查隐式检测到,这将在下面讨论。
绕过
绕过逆向工程工具检测时,应遵循以下步骤
- 修补反逆向工程功能。通过使用 radare2/iaito 或 Ghidra 修补二进制文件来禁用不需要的行为。
- 使用 Frida 或 ElleKit 钩取 Objective-C/Swift 或原生层上的文件系统 API。返回原始文件的句柄,而不是修改后的文件。
Frida 检测¶
Frida 以 frida-server 的名称以其默认配置(注入模式)在越狱设备上运行。当你显式附加到目标应用程序时(例如,通过 frida-trace 或 Frida CLI),Frida 会将 frida-agent 注入到应用程序的内存中。因此,你可能会期望在附加到应用程序后(而不是之前)在那里找到它。在 Android 上,验证这一点非常简单,因为你可以简单地在 proc
目录(/proc/<pid>/maps
)中的进程 ID 的内存映射中 grep 字符串“frida”。但是,在 iOS 上,proc
目录不可用,但你可以使用函数 _dyld_image_count
列出应用程序中加载的动态库。
Frida 也可以在所谓的嵌入模式下运行,该模式也适用于非越狱设备。它包括将 frida-gadget 嵌入到 IPA 中并强制应用程序将其加载为其中一个本机库。
应用程序的静态内容(包括其 ARM 编译的二进制文件及其外部库)存储在 <Application>.app
目录中。如果你检查 /var/containers/Bundle/Application/<UUID>/<Application>.app
目录的内容,你将找到嵌入的 frida-gadget 作为 FridaGadget.dylib。
iPhone:/var/containers/Bundle/Application/AC5DC1FD-3420-42F3-8CB5-E9D77C4B287A/SwiftSecurity.app/Frameworks root# ls -alh
total 87M
drwxr-xr-x 10 _installd _installd 320 Nov 19 06:08 ./
drwxr-xr-x 11 _installd _installd 352 Nov 19 06:08 ../
-rw-r--r-- 1 _installd _installd 70M Nov 16 06:37 FridaGadget.dylib
-rw-r--r-- 1 _installd _installd 3.8M Nov 16 06:37 libswiftCore.dylib
-rw-r--r-- 1 _installd _installd 71K Nov 16 06:37 libswiftCoreFoundation.dylib
-rw-r--r-- 1 _installd _installd 136K Nov 16 06:38 libswiftCoreGraphics.dylib
-rw-r--r-- 1 _installd _installd 99K Nov 16 06:37 libswiftDarwin.dylib
-rw-r--r-- 1 _installd _installd 189K Nov 16 06:37 libswiftDispatch.dylib
-rw-r--r-- 1 _installd _installd 1.9M Nov 16 06:38 libswiftFoundation.dylib
-rw-r--r-- 1 _installd _installd 76K Nov 16 06:37 libswiftObjectiveC.dylib
查看 Frida 留下的这些痕迹,你可能已经想象到检测 Frida 将是一项微不足道的任务。虽然检测这些库很简单,但绕过这种检测同样简单。工具的检测是一场猫捉老鼠的游戏,事情可能会变得复杂得多。下表简要介绍了一组典型的 Frida 检测方法,并简要讨论了它们的有效性。
以下一些检测方法在 IOSSecuritySuite 中实现
方法 | 描述 | 讨论 |
---|---|---|
检查环境中的相关人工制品 | 人工制品可以是打包文件、二进制文件、库、进程和临时文件。对于 Frida,这可能是目标(越狱)系统中运行的 frida-server(负责通过 TCP 公开 Frida 的守护程序)或应用程序加载的 Frida 库。 | 对于非越狱设备上的 iOS 应用程序,无法检查正在运行的服务。Swift 方法 CommandLine 在 iOS 上不可用,无法查询有关正在运行的进程的信息,但有一些非官方方法,例如使用 NSTask。尽管如此,当使用此方法时,应用程序将在 App Store 审核过程中被拒绝。没有其他公共 API 可用于查询 iOS 应用程序中正在运行的进程或执行系统命令。即使它是可能的,绕过它就像重命名相应的 Frida 人工制品(frida-server/frida-gadget/frida-agent)一样容易。检测 Frida 的另一种方法是遍历加载的库列表并检查可疑的库(例如,名称中包含“frida”的库),这可以通过使用 _dyld_get_image_name 来完成。 |
检查打开的 TCP 端口 | 默认情况下,frida-server 进程绑定到 TCP 端口 27042。测试此端口是否打开是检测守护程序的另一种方法。 | 此方法以默认模式检测 frida-server,但侦听端口可以通过命令行参数更改,因此绕过它非常简单。 |
检查响应 D-Bus 身份验证的端口 | frida-server 使用 D-Bus 协议进行通信,因此你可以期望它响应 D-Bus AUTH。向每个打开的端口发送一条 D-Bus AUTH 消息并检查答案,希望 frida-server 会自我暴露。 |
这是一种相当强大的检测 frida-server 的方法,但 Frida 提供了不需要 frida-server 的替代操作模式。 |
请记住,此表远非详尽无遗。例如,另外两种可能的检测机制是
- 命名管道(frida-server 用于外部通信),或
- 检测 跳板(有关 iOS 应用程序中检测跳板的进一步说明和示例代码,请参阅 “Prevent bypassing of SSL certificate pinning in iOS applications”)
两者都将帮助检测 Substrate 或 Frida 的 Interceptor,但例如,对 Frida 的 Stalker 无效。请记住,这些检测方法中的每一种的成功都取决于你是否正在使用越狱设备、越狱和方法的特定版本和/或工具本身的版本。最后,这是保护在不受控制的环境(最终用户的设备)上处理的数据的猫捉老鼠游戏的一部分。
模拟器检测¶
模拟器检测的目标是增加在模拟设备上运行应用程序的难度。这迫使逆向工程师击败模拟器检查或利用物理设备,从而阻止了大规模设备分析所需的访问权限。
如基本安全测试章节中的 在 iOS 模拟器上测试 一节中所述,唯一可用的模拟器是 Xcode 附带的模拟器。模拟器二进制文件被编译为 x86 代码而不是 ARM 代码,并且为真实设备(ARM 体系结构)编译的应用程序无法在模拟器中运行,因此与具有各种 模拟 选择的 Android 相比,模拟 保护并不是 iOS 应用程序的那么令人担忧。
但是,自发布以来,Corellium(商业工具)已启用真正的模拟,使其与 iOS 模拟器区分开来。除此之外,作为一种 SaaS 解决方案,Corellium 支持大规模设备分析,限制因素仅是可用的资金。
随着 Apple Silicon (ARM) 硬件的广泛普及,传统的 x86 / x64 架构检测方法可能已经不够用了。一种潜在的检测策略是识别常用模拟解决方案的功能和局限性。例如,Corellium 不支持 iCloud、蜂窝服务、摄像头、NFC、蓝牙、App Store 访问或 GPU 硬件模拟 (Metal)。因此,巧妙地结合检查这些功能中的任何一项,都可以作为存在模拟环境的指标。
代码混淆¶
"移动应用篡改和逆向工程"章节介绍了通常可在移动应用中使用的一些知名混淆技术。
名称混淆¶
标准编译器基于源代码中的类名和函数名生成二进制符号。因此,如果没有应用混淆,符号名称仍然有意义,并且可以直接从应用程序二进制文件中轻松读取。例如,可以通过搜索相关关键字(例如“jailbreak”)来定位检测越狱的函数。下面的列表显示了来自_ DVIA-v2的被反汇编的JailbreakDetectionViewController.jailbreakTest4Tapped
函数。
__T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest4TappedyypF:
stp x22, x21, [sp, #-0x30]!
mov rbp, rsp
在混淆之后,我们可以观察到符号名称不再有意义,如下面的列表所示。
__T07DVIA_v232zNNtWKQptikYUBNBgfFVMjSkvRdhhnbyyFySbyypF:
stp x22, x21, [sp, #-0x30]!
mov rbp, rsp
然而,这仅适用于函数、类和字段的名称。实际代码保持不变,因此攻击者仍然可以读取函数的反汇编版本并尝试理解其用途(例如,检索安全算法的逻辑)。
指令替换¶
此技术用更复杂的表示形式替换标准二进制运算符,例如加法或减法。 例如,加法x = a + b
可以表示为x = -(-a) - (-b)
。 但是,使用相同的替换表示形式很容易被逆向,因此建议为单个情况添加多种替换技术,并引入一个随机因子。 这种技术很容易被反混淆,但是根据替换的复杂性和深度,应用它仍然很耗时。
控制流平坦化¶
控制流平坦化用更复杂的表示形式替换原始代码。 转换将函数的主体分解为基本块,并将它们全部放在一个带有switch语句的无限循环中,该语句控制程序流。 这使得程序流程难以遵循,因为它消除了通常使代码更易于阅读的自然条件构造。
该图像显示了控制流平坦化如何更改代码。 有关更多信息,请参见 "通过控制流平坦化混淆C++程序"。
死代码注入¶
此技术通过将死代码注入到程序中来使程序的控制流更加复杂。 死代码是一段不影响原始程序行为的代码存根,但会增加逆向工程的开销。
字符串加密¶
应用程序通常使用硬编码的密钥、许可证、令牌和端点URL进行编译。 默认情况下,所有这些都以纯文本形式存储在应用程序二进制文件的数据部分中。 此技术对这些值进行加密,并将代码存根注入到程序中,这些代码将在程序使用该数据之前对其进行解密。
推荐工具¶
- 可以使用 SwiftShield执行名称混淆。 它读取Xcode项目的源代码,并在使用编译器之前将所有类、方法和字段的名称替换为随机值。
- obfuscator-llvm 在中间表示(IR)上运行,而不是在源代码上运行。 它可以用于符号混淆、字符串加密和控制流平坦化。 由于它基于IR,因此与SwiftShield相比,它可以隐藏有关应用程序的更多信息。
在论文 “使用混淆保护百万用户 iOS 应用程序:动机、陷阱和经验”中了解更多关于 iOS 混淆技术的信息。
设备绑定¶
设备绑定的目的是阻止攻击者尝试将应用程序及其状态从设备A复制到设备B,并在设备B上继续执行该应用程序。在设备A被确定为可信后,它可能比设备B具有更多权限。当应用程序从设备A复制到设备B时,这种情况不应改变。
自 iOS 7.0以来,硬件标识符(例如MAC地址)已被禁止,但还有其他在iOS中实现设备绑定的方法
identifierForVendor
:可以使用[[UIDevice currentDevice] identifierForVendor]
(在Objective-C中)、UIDevice.current.identifierForVendor?.uuidString
(在Swift3中)或UIDevice.currentDevice().identifierForVendor?.UUIDString
(在Swift2中)。 如果在安装来自同一供应商的其他应用程序后重新安装该应用程序,则identifierForVendor
的值可能不相同,并且在更新应用程序包的名称时可能会更改。 因此,最好将其与钥匙串中的某些内容结合使用。- 使用钥匙串:可以将某些内容存储在钥匙串中以标识应用程序的实例。 为了确保不备份此数据,请使用
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
(如果要保护数据并正确执行密码或Touch ID要求)、kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
或kSecAttrAccessibleWhenUnlockedThisDeviceOnly
。 - 使用 Google 实例 ID:请参阅 此处的 iOS 实现。
如果启用了密码和/或Touch ID,则基于这些方法的任何方案都将更加安全,存储在钥匙串或文件系统中的材料受到保护类(例如kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
和kSecAttrAccessibleWhenUnlockedThisDeviceOnly
)的保护,并且SecAccessControlCreateFlags
设置为kSecAccessControlDevicePasscode
(对于密码)、kSecAccessControlUserPresence
(密码、Face ID或Touch ID)、kSecAccessControlBiometryAny
(Face ID或Touch ID)或kSecAccessControlBiometryCurrentSet
(Face ID/Touch ID:但仅限当前注册的生物识别)之一。