逆向学习之二:破解Cornerstone客户端

废话若干

感到高兴的是,总算有第二篇的逆向学习系列的博文了。这次选了Cornerstone(当前版本是2.7.17)作为研究对象,也是有点不好意思,因为某程度是感觉这货略贵(逃……。其他的原因就是,正好最近刚要要用到,但试用时间快到了,还有就是之前用这个东西的盗版也中过一次毒(羞耻感ˊ_>ˋ)。也是正式小试牛刀的一次机会,去认真逆向一个商业软件的注册保护策略。

对了,不要求放出破解好的二进制文件了,我不会放出来的。原则上我还是支持正版的,我在iOS和Mac上都会花钱买正版。逆向应用更多是为了学习和研究,如果只是想为了省钱,其实是不现实的,因为逆向的时间和精力是需要很多的。逆向的过程可以知道别人的验证策略和漏洞,远比用盗版更有成功感。

初步尝试

现在都懒得class-dump了,直接拖进去Hopper看伪码。哦,对了,其实我不会开发Mac的应用的,不过估计生命周期那些玩意和iOS不会差太多吧,都是AppDelegate之类的几个方法。一开始,直接上老方法。

1. 搜索”License””Licensing”之类的关键字

直接搜索这类和注册相关的关键字,真的是屡试不爽(iOS上我经常这么做,毕竟良好的命名是程序员应有的品质)。可看到:

jt1

从这里的东西可以猜到,处理许可证的一些逻辑也是在V4ApplicationDelegate里面完成的。我暂时猜想,试用到期后的一些阻止进入正常界面的业务逻辑也是在V4ApplicationDelegate里面完成的。

接下来就是苦逼地看伪码,在V4ApplicationDelegateinit方法,就可以发现这个二进制执行文件并没有的类,例如ZStandardLicensingPolicy。这时,我就想起dylib这种玩意了,毕竟Mac的应用都可以用真framework,直接看看framework目录有什么。老规矩,”License””Licensing”之类的东西要特别优待,可以发现一个叫LicensingKit的framework,里面有两个玩意,分别是LicensingKitLicenseToolLicenseTool基本都是一些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的汇编指令填充。把:

jt2

改成:

jt3

然后导出,可执行文件,提示签名会被破坏了,无视之。然后替换原app的LicensingKit

这个时候我满心期待观察会出现什么事情,结果是Cornerstone启动后就直接闪退了。。。心想我的正版Hopper不是这么弱逼吧,改个文件都改坏了。

多次失败

1. 测试汇编修改功能

我只好顺便找了一个从App Store上下载的应用让Hopper修改了一下,发现怎么改都可以正常运行。难道就Cornerstone特别奇葩一点?

2. 测试修改资源文件

Info.plist看到了ZTrialPeriodDuration的键值是14,抱着试试看的心态又改了一下(如果有怎么简单,就满街都是盗版了),果然也出现了类似刚刚闪退的现象。查看了一下系统的日志,也没看出什么异常。

我感觉这是很可能是Cornerstone自己的保护机制,发现一旦修改了文件,就闪退。

猜测可能

五一这几天看了这么久伪码,也是一知半解,遇到了这种闪退,开始有点会徒劳无功的感觉。我重复思考了很久,到底它是在什么时候去做这个完整性检验。

1. AppDelegate的相关生命周期方法

看了initapplicationWillFinishLaunching:,看起来没什么问题,除了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,可以正常使用了。最后修改样子是这样:

jt4

总结

这次真的有点乱来,如果一开始就直接用动态调试的话,就不用走这么多弯路了。不过目前还不是很圆满,因为没做到实现注册成功的样子。不过看到注册会发送注册码到服务器验证的,似乎本地破解算法不太实际。不过得益还是有的:

  1. 对于关键的验证逻辑,命名真的需要注意
  2. C函数虽然可以逃过swizzling,但是对静态破解还是无力的,真的不要抱太大希望
  3. 文件校验保护对于菜鸟来说(例如我),还是有一定作用的,但还是太弱小了

PS

Cornerstone的保护机制还是比较简单的,但是结尾还是要提醒大家,支持正版才能让世界变得更美好。