废话若干
感到高兴的是,总算有第二篇的逆向学习系列的博文了。这次选了Cornerstone(当前版本是2.7.17)作为研究对象,也是有点不好意思,因为某程度是感觉这货略贵(逃……。其他的原因就是,正好最近刚要要用到,但试用时间快到了,还有就是之前用这个东西的盗版也中过一次毒(羞耻感ˊ_>ˋ)。也是正式小试牛刀的一次机会,去认真逆向一个商业软件的注册保护策略。
对了,不要求放出破解好的二进制文件了,我不会放出来的。原则上我还是支持正版的,我在iOS和Mac上都会花钱买正版。逆向应用更多是为了学习和研究,如果只是想为了省钱,其实是不现实的,因为逆向的时间和精力是需要很多的。逆向的过程可以知道别人的验证策略和漏洞,远比用盗版更有成功感。
初步尝试
现在都懒得class-dump了,直接拖进去Hopper看伪码。哦,对了,其实我不会开发Mac的应用的,不过估计生命周期那些玩意和iOS不会差太多吧,都是AppDelegate之类的几个方法。一开始,直接上老方法。
1. 搜索”License””Licensing”之类的关键字
直接搜索这类和注册相关的关键字,真的是屡试不爽(iOS上我经常这么做,毕竟良好的命名是程序员应有的品质)。可看到:
从这里的东西可以猜到,处理许可证的一些逻辑也是在V4ApplicationDelegate
里面完成的。我暂时猜想,试用到期后的一些阻止进入正常界面的业务逻辑也是在V4ApplicationDelegate
里面完成的。
接下来就是苦逼地看伪码,在V4ApplicationDelegate
的init
方法,就可以发现这个二进制执行文件并没有的类,例如ZStandardLicensingPolicy
。这时,我就想起dylib这种玩意了,毕竟Mac的应用都可以用真framework,直接看看framework目录有什么。老规矩,”License””Licensing”之类的东西要特别优待,可以发现一个叫LicensingKit
的framework,里面有两个玩意,分别是LicensingKit
和LicenseTool
。LicenseTool
基本都是一些C函数,暂时忽略(没名字的方法,不好推测),LicensingKit
则是很多和”License”相关的方法。
其中,ZStandardLicensingPolicy
这个东西挺有趣的,init
方法伪码:
void * -[ZStandardLicensingPolicy init](void * self, void * _cmd) {
var_38 = self;
var_30 = *0x453d0;
rbx = [[var_38 super] init];
if (rbx != 0x0) {
r14 = *objc_msgSend;
rbx->eulaPolicy = [[ZEULALicensingPolicy alloc] init];
rbx->preReleasePolicy = [[ZPreReleaseLicensingPolicy alloc] init];
rax = [ZLicensePackageRegistrationPolicy alloc];
rax = [rax init];
r15 = *_OBJC_IVAR_$_ZStandardLicensingPolicy.packageRegistrationPolicy;
*(rbx + r15) = rax;
rax = [ZOwnershipLicensingPolicy alloc];
rax = [rax init];
r13 = *_OBJC_IVAR_$_ZStandardLicensingPolicy.ownershipPolicy;
*(rbx + r13) = rax;
rbx->trialPeriodPolicy = [[ZTrialPeriodLicensingPolicy alloc] init];
r12 = *_OBJC_IVAR_$_ZStandardLicensingPolicy.eulaPolicy;
[rbx setNextPolicy:*(rbx + r12)];
rdi = *(rbx + r12);
r12 = *_OBJC_IVAR_$_ZStandardLicensingPolicy.preReleasePolicy;
[rdi setNextPolicy:*(rbx + r12)];
[*(rbx + r12) setNextPolicy:*(rbx + r15)];
[*(rbx + r15) setNextPolicy:*(rbx + r13)];
[*(rbx + r13) setNextPolicy:rbx->trialPeriodPolicy];
}
return rax;
}
这里面各种policy要初始化,其中ZTrialPeriodLicensingPolicy
值得留意。好的就拿这个类先改改看。
2. 修改二进制执行文件
Hopper是可以直接用汇编改文件的,我也是先随意改改看看效果。伪码:
char -[ZTrialPeriodLicensingPolicy apply](void * self, void * _cmd) {
rbx = self;
rdi = rbx->trialPeriod;
if (rdi != 0x0) {
if ([rdi timestamp] == 0x0) {
[rbx->trialPeriod setTimestamp:[NSDate date]];
}
r15 = *_OBJC_IVAR_$_ZTrialPeriodLicensingPolicy.trialPeriod;
r12 = *objc_msgSend;
r14 = [NSDate date];
[r14 timeIntervalSinceDate:[*(rbx + r15) timestamp]];
if (intrinsic_ucomisd(xmm0, *0x2d6d0) > 0x0) {
r15 = *_OBJC_IVAR_$_ZTrialPeriodLicensingPolicy.trialPeriod;
r12 = *objc_msgSend;
rdi = *(rbx + r15);
rdx = r14;
[rdi setTimestamp:rdx];
r14 = *(rbx + r15);
rax = [r14 elapsed];
[r14 setElapsed:rax + 0x1];
}
r15 = *_OBJC_IVAR_$_ZTrialPeriodLicensingPolicy.trialPeriod;
r12 = *objc_msgSend;
rdx = *(rbx + r15);
[ZTrialPeriod setCurrentTrialPeriod:rdx];
rax = [*(rbx + r15) hasExpired];
rax = rax == 0x0 ? 0x1 : 0x0;
}
else {
rax = 0x0;
}
return rax;
}
那我让rdi != 0x0
的跳转失效好了,用nop
的汇编指令填充。把:
改成:
然后导出,可执行文件,提示签名会被破坏了,无视之。然后替换原app的LicensingKit
。
这个时候我满心期待观察会出现什么事情,结果是Cornerstone启动后就直接闪退了。。。心想我的正版Hopper不是这么弱逼吧,改个文件都改坏了。
多次失败
1. 测试汇编修改功能
我只好顺便找了一个从App Store上下载的应用让Hopper修改了一下,发现怎么改都可以正常运行。难道就Cornerstone特别奇葩一点?
2. 测试修改资源文件
在 Info.plist
看到了ZTrialPeriodDuration
的键值是14,抱着试试看的心态又改了一下(如果有怎么简单,就满街都是盗版了),果然也出现了类似刚刚闪退的现象。查看了一下系统的日志,也没看出什么异常。
我感觉这是很可能是Cornerstone自己的保护机制,发现一旦修改了文件,就闪退。
猜测可能
五一这几天看了这么久伪码,也是一知半解,遇到了这种闪退,开始有点会徒劳无功的感觉。我重复思考了很久,到底它是在什么时候去做这个完整性检验。
1. AppDelegate的相关生命周期方法
看了init
和applicationWillFinishLaunching:
,看起来没什么问题,除了finishLaunch
这个方法有点奇怪以外。但是涉及的类很多,感觉要看很久很久。
2. load和initialize
很多人喜欢在这些地方加代码,因为不用显式去调用。这些我也粗略看了一下,也感觉没什么问题(从名字上看)。
再次陷入僵局,因为岔路太多了,看伪码要看到猴年马月才能找到正路,感觉想放弃了。
动态调试
以前一直认为,只要足够牛逼看伪码就可以了,但是到了这时,感觉我还是太弱了。狗神也说过,IDA和LLDB就是屠龙刀和倚天剑,可见静态反汇编和动态调试缺一不可。好的,我也直接用Hopper的动态调试试试。
1. 去除完整性校验保护
Hopper貌似是用GDB调试的,不过我也只用简单的断点什么的。想了一下,还是从applicationWillFinishLaunching:
开始做断点吧。试了几次,发现跑完了整个方法也没闪退,而是出了这个地方才闪退。感觉怪异了,问题不在这里。不过,退出原因是exit(0)
,也是怪异,因为0代表正常退出。再看看伪码这句:
[self performSelector:@selector(finishLaunch) withObject:0x0 afterDelay:0x4755524c];
很奇怪,afterDelay:
的参数没看出来是什么,估计是常量之类的。再看看finishLaunch
的伪码:
void -[V4ApplicationDelegate finishLaunch](void * self, void * _cmd) {
r15 = self;
xmm0 = intrinsic_movsd(xmm0, **NSAppKitVersionNumber);
rax = floor();
if (intrinsic_ucomisd(xmm0, *0x1001d5890) >= 0x0) {
rbx = sub_1001a84bf();
rdi = rbx;
rax = sub_1001a855d(rdi);
if (rax != 0x0) {
rdi = @"LicensingKit.framework";
rax = sub_1001a87ab(rdi, rbx);
if (rax != 0x0) {
rbx = *objc_msgSend;
[r15 setSuppressUI:0x1];
rdi = r15->licensingPolicy;
rax = [rdi applyWithUserInterface:0x1];
if (rax != 0x0) {
rbx = *objc_msgSend;
[r15 setupPanels];
[r15 setupMainMenu];
[r15 setupDockMenu];
[r15 setupMenuNotifications];
[r15 setupFilterMenu];
[r15 setupConfigChangeSource];
rax = [r15 mainWindowController];
[rax launch];
rdx = [V4TaskCompletionNotifier sharedInstance];
[GrowlApplicationBridge setGrowlDelegate:rdx];
[r15 setupUpdateChecks];
rbx = 0x0;
[r15 setSuppressUI:0x0];
if (0x0 != 0x0) {
objc_exception_rethrow();
}
}
else {
exit(0x0);
}
}
else {
exit(0x0);
}
}
else {
exit(0x0);
}
}
else {
rbx = *objc_msgSend;
[r15 setSuppressUI:0x1];
rdi = r15->licensingPolicy;
rax = [rdi applyWithUserInterface:0x1];
if (rax != 0x0) {
rbx = *objc_msgSend;
[r15 setupPanels];
[r15 setupMainMenu];
[r15 setupDockMenu];
[r15 setupMenuNotifications];
[r15 setupFilterMenu];
[r15 setupConfigChangeSource];
rax = [r15 mainWindowController];
[rax launch];
rdx = [V4TaskCompletionNotifier sharedInstance];
[GrowlApplicationBridge setGrowlDelegate:rdx];
[r15 setupUpdateChecks];
rbx = 0x0;
[r15 setSuppressUI:0x0];
if (0x0 != 0x0) {
objc_exception_rethrow();
}
}
else {
exit(0x0);
}
}
return;
}
略复杂,几个嵌套,但考虑到调试的时候看到的是exit(0)
的闪退,猜测很可能是这里。于是又加了几个断点,发现是这里的问题:
rbx = sub_1001a84bf();
rdi = rbx;
rax = sub_1001a855d(rdi);
if (rax != 0x0) {
有点狡猾,用C写的两个方法,就没办法用swizzling的方式替换是吧。深究下去就可以看到这个玩意:
int sub_1001a8396() {
r14 = *objc_msgSend;
rbx = [NSMutableData dataWithLength:0x28];
rax = [rbx mutableBytes];
*(int32_t *)rax = 0x30727470;
*(int8_t *)(rax + 0x1) = *(int8_t *)(rax + 0x1) ^ 0x32;
*(int8_t *)rax = *(int8_t *)rax ^ 0x35;
*(int8_t *)(rax + 0x2) = *(int8_t *)(rax + 0x2) ^ 0x45;
*(int8_t *)(rax + 0x3) = *(int8_t *)(rax + 0x3) ^ 0x4;
*(int8_t *)(rax + 0x5) = 0x34;
*(int8_t *)(rax + 0x4) = 0x37;
*(int8_t *)(rax + 0x6) = 0x39;
*(int8_t *)(rax + 0x8) = 0x30;
*(int8_t *)(rax + 0xa) = 0x31;
*(int8_t *)(rax + 0x9) = 0x34;
*(int8_t *)(rax + 0x7) = 0x46;
*(rax + 0xb) = 0x77;
*(int8_t *)(rax + 0xb) = *(int8_t *)(rax + 0xb) ^ 0x44;
*(int8_t *)(rax + 0x11) = *(int8_t *)(rax + 0x11) ^ 0x3f;
*(int8_t *)(rax + 0xe) = *(int8_t *)(rax + 0xe) ^ 0xdf;
*(int8_t *)(rax + 0x12) = *(int8_t *)(rax + 0x12) ^ 0xc;
*(int8_t *)(rax + 0xd) = *(int8_t *)(rax + 0xd) ^ 0x6f;
*(int8_t *)(rax + 0x10) = *(int8_t *)(rax + 0x10) ^ 0x80;
*(int8_t *)(rax + 0xc) = *(int8_t *)(rax + 0xc) ^ 0x44;
*(int8_t *)(rax + 0xf) = *(int8_t *)(rax + 0xf) ^ 0xaa;
*(int8_t *)(rax + 0x15) = 0xe8;
*(int16_t *)(rax + 0x13) = 0x34;
*(int8_t *)(rax + 0x14) = *(int8_t *)(rax + 0x14) ^ 0x43;
*(int8_t *)(rax + 0x15) = *(int8_t *)(rax + 0x15) ^ 0xdf;
*(int8_t *)(rax + 0x13) = *(int8_t *)(rax + 0x13) ^ 0x76;
*(int8_t *)(rax + 0x17) = 0x36;
*(int8_t *)(rax + 0x1a) = 0x44;
*(int8_t *)(rax + 0x18) = 0x35;
*(int8_t *)(rax + 0x19) = 0x46;
*(int8_t *)(rax + 0x16) = 0x42;
*(int8_t *)(rax + 0x1c) = 0x45;
*(int8_t *)(rax + 0x1d) = 0x43;
*(int8_t *)(rax + 0x1b) = 0x37;
*(int8_t *)(rax + 0x1e) = 0x32;
*(int8_t *)(rax + 0x21) = 0x94;
*(int16_t *)(rax + 0x1f) = 0x8fea;
*(int8_t *)(rax + 0x21) = *(int8_t *)(rax + 0x21) ^ 0xa2;
*(int8_t *)(rax + 0x1f) = *(int8_t *)(rax + 0x1f) ^ 0xdf;
*(int8_t *)(rax + 0x20) = *(int8_t *)(rax + 0x20) ^ 0xbb;
*(int16_t *)(rax + 0x26) = 0xecb4;
*(int32_t *)(rax + 0x22) = 0x17dfa9f6;
*(int8_t *)(rax + 0x26) = *(int8_t *)(rax + 0x26) ^ 0xf2;
*(int8_t *)(rax + 0x22) = *(int8_t *)(rax + 0x22) ^ 0xb4;
*(int8_t *)(rax + 0x25) = *(int8_t *)(rax + 0x25) ^ 0x52;
*(int8_t *)(rax + 0x24) = *(int8_t *)(rax + 0x24) ^ 0x9d;
*(int8_t *)(rax + 0x27) = *(int8_t *)(rax + 0x27) ^ 0xd4;
*(int8_t *)(rax + 0x23) = *(int8_t *)(rax + 0x23) ^ 0x90;
rax = [rbx crc32Checksum];
rcx = rax;
if (rcx == 0xb2058b24) {
}
return rax;
}
当时还没用调试之前也看到了这步,但是着实没看懂,所以忽略了。现在目测就是用crc32来检验拿一堆文件吧,一旦发现修改了,就立即闪退。
果断用nop
填充,跳过这些判断,运行之,终于成功了。此外发现,
rdi = @"LicensingKit.framework";
rax = sub_1001a87ab(rdi, rbx);
if (rax != 0x0) {
对LicensingKit.framework
也有再一次单独检验,随手去除之。
2. 去掉试用弹出框
继续加断点,发现试用框是在applyWithUserInterface:
里弹出来的,去掉之。然后测试成功了。最后模拟超过试用期的情况,不断离线改时间,成功改出了试用期过了的情况。然后再打开,已经修改好的Cornerstone,可以正常使用了。最后修改样子是这样:
总结
这次真的有点乱来,如果一开始就直接用动态调试的话,就不用走这么多弯路了。不过目前还不是很圆满,因为没做到实现注册成功的样子。不过看到注册会发送注册码到服务器验证的,似乎本地破解算法不太实际。不过得益还是有的:
- 对于关键的验证逻辑,命名真的需要注意
- C函数虽然可以逃过swizzling,但是对静态破解还是无力的,真的不要抱太大希望
- 文件校验保护对于菜鸟来说(例如我),还是有一定作用的,但还是太弱小了
PS
Cornerstone的保护机制还是比较简单的,但是结尾还是要提醒大家,支持正版才能让世界变得更美好。
探索精神不错,值得学习!!!
我记得我刚学习iOS没多久的时候就关注了你的博客,深感自己还是很弱小,还要向大神你多多学习啊
conrestone 3 出来了,机制加强了好像