做过iOS积分墙(OfferWall)的朋友都知道,这类Apple明令禁止的App虽然上不了台面,但却既能给墙上的各种App增加曝光率,又能给开发者带来可观的收益。
一涉及到金钱,各种小聪明就蠢蠢欲动了——类似于游戏外挂,刷积分墙的工具也能给背后的那一双双手带来不菲的现金收入。这些工具中,由论坛一位不方便透露姓名的朋友制作的iGrimace就是很有代表性的一款越狱插件,它只在地下市场流通,能够轻松将越狱iOS伪装成一台台全新的机器,骗过各种积分墙App的检察,把一个个钱币硬生生地从积分墙上抠下来,装进自己的腰包(本文对积分墙和iGrimace类工具本身不予置评,仅仅讨论hook root App的方法)
相信那些具备iOS逆向工程能力,又对iGrimace有一定研究的朋友都能发现,iGrimace的实际功能只占这个插件体积的1/3,另外的部分是作者精心构造的防护代码,用以防护盗版。因此,对于大多数越狱开发者来说,重写一个iGrimace的工作量比■■它其实要小得多。但是明知山有虎,偏向虎山行,很多朋友偏偏想要■■它,来挑战自己,锻炼技术。iGrimace因为含有reboot等功能,需要以root权限运行,而CydiaSubstrate对root权限的App是无效的(mobile权限的代码怎么能够影响root权限的代码?),所以书上讲的基于CydiaSubstrate的tweak对root权限的App不起作用。那么问题来了,学挖掘机技术到底哪家强?
一、谁在阻止dylib注入root权限的App?
从这个帖子,我们知道,现在iOS上的绝大多数以root权限运行的App,都是通过setuid + bash来实现的;而这个帖子又说,可执行文件如果被setuid或setgid,那么DYLD_环境变量会被dyld无视,因此DYLD_INSERT_LIBRARIES不起作用,tweak自然无效。这两点的综合作用,造成了root App的高保护性,没有dylib的加持,想要修改这类App,貌似就只剩静态patch这一条路了。事实上,很多■■版也是这么做的——他们吭哧吭哧地阅读汇编代码,把一个个校验判断给nop掉。事情真的有这么复杂吗?
二、让dyld对DYLD_INSERT_LIBRARIES旧情复燃?
从这个帖子提到的switch case语句可以得知,当sRestrictedReason为restrictedBySetGUid时,DYLD_INSERT_LIBRARIES因setuid而被dyld忽略。我们顺着这条线索,看看restrictedBySetGUid是怎么来的:
static bool processRestricted(const macho_header* mainExecutableMH)
{
// all processes with setuid or setgid bit set are restricted
if ( issetugid() ) {
sRestrictedReason = restrictedBySetGUid;
return true;
}
原来,当issetugid()返回1时,sRestrictedReason被设置为restrictedBySetGUid。如果要修改这个返回值,就必须patch dyld,这样就要一机一patch,不仅不具备通用性,还非常危险,我们不能冒这个险。因此DYLD_INSERT_LIBRARIES这条路基本可以放弃了,虽然旧爱还是最美,但也是时候放手追求新的生活鸟~
三、为什么必须是DYLD_INSERT_LIBRARIES?
到现在为止,我们讨论问题的大前提,都默认是要通过DYLD_INSERT_LIBRARIES这个环境变量来动态指定需要加载的dylib。虽然在越狱开发界,一众大佬一再强调,相对于传统hack,我们的优势就在于“动态hook”——但这不代表我们就可以完全丢掉传统的“静态patch”。既然DYLD_INSERT_LIBRARIES这个动态方法被限制得这么死,我们何不换个思路,看看从偏静态(用“偏静态”这个词的原因稍后会解释)的角度,我们能做些什么?
四、除DYLD_INSERT_LIBRARIES 外,dylib还有其他的加载方式吗?
还记得书上第19页对dylib的简单介绍吗?当一个App启动时,dyld把App需要的dylib加载进App的内存空间。注意这里的字眼:“App需要的dylib”——App需要的dylib,是怎么指定的呢?如果你对文件格式有所了解,就会知道,这些App运行所需要的信息,一般都存放在其MachO头部中,其中dylib的信息是由load commands指定的,
如图所示:
因为这些信息是以静态的方式存放在二进制文件里(不是由DYLD_INSERT_LIBRARIES动态指定),而又是由dyld动态加载的,所以我们给它起了个“偏静态”的名字。也就是说,在此App得到执行时,dyld会查看其MachO头部中的load commands,并把里面LC_LOAD_DYLIB相关的dylib给加载到进程的内存空间。
五、曲线救国,通过LC_LOAD_DYLIB实现dylib的加载
既然DYLD_INSERT_LIBRARIES被各种手段严格限制,动态的方式难以发挥作用,我们就另辟蹊径,用LC_LOAD_DYLIB来围魏救赵。看似神秘的LC_LOAD_DYLIB也只是MachO头部的一个load command而已,我们要做的,就是修改App可执行文件的头部,给它添加这么一个load command,并指定load我们构造的dylib就好。这个操作有点类似于碟中谍3里的颅内炸弹,显得十分高级
往MachO头部添加一个新的load command是一个细致活儿,像楼主这样对其头部结构不够熟悉的菜鸟,很容易因为一两个字节的偏差毁掉整个MachO文件。好在丹麦高手Asger Hautop Drewsen开发的insert_dylib正是为了解决这个问题而存在的,我们直接使用这个工具来完成操作就好了~
六、实例演示
下面,我就拿这个帖子里编写的一个root权限App来作为目标,将界面的红色背景修改为蓝色。
编写tweak
代码如下,很简单:
@interface RootViewController: UIViewController
@end
%hook RootViewController
- (void)loadView
{
%orig;
self.view.backgroundColor = [UIColor blueColor];
}
%end
用Theos环境编译后,注意保留生成的dylib,将其重命名为iosre.dylib后拷贝到iOS中,放在/var/tmp/下备用。(注意这里的dylib路径,与下面一致)
生成RootApp.app/RootApp
snakeninnysimac:rootapp snakeninny$ make
Making all for application RootApp...
Copying resource directories into the application wrapper...
Compiling main.m...
Compiling RootAppApplication.mm...
Compiling RootViewController.mm...
Linking application RootApp...
Stripping RootApp...
Signing RootApp...
生成的可执行文件是/Users/snakeninny/Code/rootapp/obj/RootApp.app/RootApp。注意,这里不要移动RootApp的位置。
生成insert_dylib
用insert_dylib给RootApp添加一个LC_LOAD_DYLIB,指定加载iosre.dylib
snakeninnysimac:~ snakeninny$ /path/to/insert_dylib /var/tmp/iosre.dylib /Users/snakeninny/Code/rootapp/obj/RootApp.app/RootApp // 注意这里的dylib路径,与上面一致
The provided dylib path doesn't exist. Continue anyway? [y/n] y // OSX在本地找/var/tmp/iosre.dylib,当然找不到,因为它在iOS上
Binary is a fat binary with 3 archs.
LC_CODE_SIGNATURE load command found. Remove it? [y/n] y
LC_CODE_SIGNATURE load command found. Remove it? [y/n] y
LC_CODE_SIGNATURE load command found. Remove it? [y/n] y
Added LC_LOAD_DYLIB to all archs in /Users/snakeninny/Code/rootapp/obj/RootApp.app/RootApp_patched
insert_dylib在RootApp的同目录下生成了一个新文件,RootApp_patched,用它替换掉原来的RootApp:
snakeninnysimac:~ snakeninny$ mv /Users/snakeninny/Code/rootapp/obj/RootApp.app/RootApp_patched /Users/snakeninny/Code/rootapp/obj/RootApp.app/RootApp
RootApp位于iOS上的/Applications/下,无需签名校验,因此我们不需要像这个帖子提到的那样安装AppSync来禁用iOS签名校验。
安装,测试
snakeninnysimac:rootapp snakeninny$ make package install // 这里的make package install会打包并安装我们修改过的RootApp,而不是生成新的RootApp
Making all for application RootApp...
Copying resource directories into the application wrapper...
make[2]: Nothing to be done for `internal-application-compile'.
Making stage for application RootApp...
dpkg-deb: building package `com.naken.rootapp' in `./com.naken.rootapp_0.0.1-14_iphoneos-arm.deb'.
install.exec "cat > /tmp/_theos_install.deb; dpkg -i /tmp/_theos_install.deb && rm /tmp/_theos_install.deb" < "./com.naken.rootapp_0.0.1-14_iphoneos-arm.deb"
(Reading database ... 4934 files and directories currently installed.)
Preparing to replace com.naken.rootapp 0.0.1-13 (using /tmp/_theos_install.deb) ...
Unpacking replacement com.naken.rootapp ...
Setting up com.naken.rootapp (0.0.1-14) ...
snakeninnysimac:rootapp snakeninny$
启动RootApp,背景由红色变为了蓝色:
点击中间的黄色按钮,iOS重启了。很明显,/var/tmp/iosre.dylib被加载到了RootApp里,且其原功能不受影响。实验成功!
总结
同“防止tweak依附,App有高招;破解App保护,tweak留一手”一样,这篇帖子涉及到了一些MachO文件格式的内容,而绝大多数AppStore开发者对于这方面的知识可以说是一无所知。随着国内iOS逆向工程氛围的逐渐成熟,我们也都能感觉到,App攻防的战场逐渐由台前转为幕后,对iOS开发者的能力要求也越来越高。在可以预见的未来,iOS开发门槛只会越来越低,只有精于这个方向的人才方能具备足够的竞争力,在一片红海中脱颖而出。多学一些东西总归是没什么坏处,在对正向开发越来越熟悉的档口,逆向工程或许就是你脱胎换骨的点金石。