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