2019-03-18 10:45:37分类:IOS应用设计6935
应用程序加载过程
对于诸多逆向爱好者来说,给一个app脱壳是一项必做的事情。基于安全性的考虑,苹果对上架到appstore的应用都会进行加密处理,所以如果直接逆向一个从appstore下载的应用程序时,所能看到的“源代码”将非常的晦涩难懂。为了能看懂应用程序的“源代码”,就必须对应用程序进行解密,也就是所谓的脱壳。脱壳后的目的是可以分析应用程序的一些技术实现原理,或者利用一些漏洞进行攻击和测试。
这篇文章不是一篇介绍如何利用工具去进行脱壳的教程,而只是简单的分析这些常用脱壳工具的实现原理。要想了解脱壳原理,就要先去了解一个被加密的应用程序是如何被运行的。下面一张图片简单的介绍了一个被加壳后的应用程序被加载和运行的过程:
脱壳原理以及常见的工具
要对一个壳应用进行脱壳处理,无非就是采用静态脱壳和动态脱壳两种方法:静态脱壳就是在已经掌握和了解到了壳应用的加密算法和逻辑后在不运行壳应用程序的前提下将壳应用程序进行解密处理。静态脱壳的方法难度大,而且加密方发现应用被破解后就可能会改用更加高级和复杂的加密技术;动态脱壳就是从运行在进程内存空间中的可执行程序映像(image)入手,来将内存中的内容进行转储(dump)处理来实现脱壳处理。这种方法实现起来相对简单,且不必关心使用的是何种加密技术。从上面的壳应用程序运行的过程就可以看出无论壳程序如何被加密处理,最终运行后在进程中的代码映像(image)始终是被解密后的原始程序二进制。所以只要一个进程内存空间中的代码映像(image)能被读取和访问就可以实现动态脱壳。下面要介绍的两个工具就是巧妙的运用了两种不同的访问技巧来实现动态脱壳的。
一、利用动态库注入来实现脱壳的dumpdecrypted/frida-ios-dump
dumpdecrypted和frida-ios-dump都是在github上开源的项目,下载地址分别为:https://github.com/stefanesser/dumpdecrypted和https://github.com/AloneMonkey/frida-ios-dump。关于使用这两个工具来进行脱壳的文档非常之多。我们知道一个应用除了有一个可执行程序外,还会链接非常多的动态库。动态库加载后和可执行程序共享相同的进程内存空间,而且动态库中的代码是可以访问整个进程内存空间中的有权限的区域的,包括可执行程序的image被加载到进程中的内存区域。因此只要想办法让应用程序加载某个特定的第三方动态库,也就是让这个第三方动态库注入到应用程序的进程中去就可以实现将被解密过后的可执行程序在进程内存中的image信息转储到文件中去从而实现脱壳处理。对于一个越狱后的设备来说主要可以通过两种方法来实现第三方动态库的注入:
设置环境变量DYLD_INSERT_LIBRARIES的值指向这个第三方动态库的路径。然后运行要脱壳的应用程序即可。 DYLD_INSERT_LIBRARIES环境变量的设置是一个操作系统提供的特性,所有运行的程序都会加载这个环境变量中所指向的动态库文件。
将第三方动态库文件保存在越狱设备的/Library/MobileSubstrate/DynamicLibraries/目录下并编写对应的库的同名plist文件,所有plist中指定的可执行程序一旦运行就会加载对应的动态库(此目录即Tweak插件所在的目录)。
还有一种直接修改对应mach-o格式的可执行文件内容来实现动态库注入。
动态库加载的问题解决后就需要解决动态库中代码运行的时机问题了。要想让一个被加载的动态库在加载后自动运行某一段代码可以有四种方法:
建立一个C++全局对象,并在对象所属类的构造函数中添加特定代码。
建立一个OC类,并在OC类的+load方法中添加特定的代码。
生成动态库时指定一个初始化init入口函数,并在入口函数中添加特定的代码。
在动态库中定义一个带有_attribute_((constructor))声明的函数,并在函数内添加特定的代码。
如果你想更进一步的了解上述那些方法的加载的原理,请参考我的文章:深入解构iOS系统下的全局对象和初始化函数
dumpdecrypted这个工具就是通过建立一个名为dumpdecrypted.dylib的动态库并在库内部定义了一个
函数来实现脱壳的。这个函数的大体实现会在后面继续介绍。
二、利用父子进程关系来实现脱壳的Clutch
Clutch也是一个在github上开源的项目,下载地址为:
https://github.com/KJCracks/Clutch。关于这个工具的使用教程也非常之多。我们知道在unix系列的操作系统中父进程可以通过fork或者posix_spawnp两个函数来运行或者建立一个子进程的,这两个函数都会返回对应的子进程ID(PID)。iOS系统则可以通过task_for_pid函数来从进程ID获取进程在mach内核子系统中的mach port标识。得到mach port 标识后,就可以借助mach_vm_read_overwrite函数来读取指定进程空间中的任意虚拟内存区域中所存储的内容。因此Clutch内部的实现就是Clutch这个程序对将要进行脱壳的程序文件路径调用posix_spawnp函数来运行从而成为其子进程,然后借助task_for_pid以及mach_vm_read_overwrite函数来读取脱壳程序子进程在内存中已经被解密后的可执行程序的image所映射的内存空间来达到脱壳的目的的。
一个思考:可能在实际中并不一定要求是父子进程关系,是否只要某个具有特权的程序或者运行在root用户上的程序只要拿到了对应进程的PID就可以通过mach子系统提供的API来读取其他进程内存空间中的信息呢?
上述的两种方法中不管是dumpdecrypted还是Clutch最终都是将被解密后的可执行程序的image在内存中的映射写入到一个文件中去来保存脱壳后的内容。参考dumpdecrypted中的dumptofile函数实现以及Clutch中的Dumpers目录下的实现代码就可以看出:一个可执行程序image在内存中映射的内容的结构和mach-o格式的可执行文件结构基本上是保持一致的。都是有一个mach_header结构体头还有诸多的load_command结构体组成。因此所谓的dump处理就是将内存中的这些结构和数据原封不动的写入到文件中去即完成了脱壳中的最核心的部分。如果想仔细的阅读这部分代码的实现,建议先了解一下mach-o文件格式的组成。
后记
当你了解了这些内部实现后,也许你会发觉其实它的原理很简单。而且有可能你也能很快的去实现。可问题的关键是为什么这些方法总是别人能想到,而我们却想不到呢?这是否和国人的思维以及解决问题的方式相关呢?在我们的教育和实践体系中更多的是拿来主义和实用主义,往往很少人会对问题进行深入的探索研究以及进行问题的关联性思考。但愿这种情况在未来能够得到改进,尤其作为一个程序员,更加应该秉持探索求知的强烈意愿而不是简单复制和应用就满足了。