Interface Inspector逆向与破解

前言

Interface Inspector是用于浏览MacOS app页面布局的利器,也是一个逆向的辅助工具,可以attach到正在执行的应用上,查看界面元素在内存中的地址。

软件是收费的,当然我们也可以遵循软件逆向界面分析 -> 动态分析 -> 静态分析 -> 动态库注入的一般思路进行破解。

License机制

首先从官网下载试用版,打开软件首先就弹出激活与试用的界面。最喜欢这么直接的打招呼了。
我们直接从界面上的Register按钮入手,使用Hopper打开Interface Inspector,搜索Register的string,然后按下x键查看引用:

register
viewController

可以看到Register按钮被-[SMEnterLicenseViewController loadView]方法引用。研究这个方法所在的类,很容易找到-[SMEnterLicenseViewController register:]的action方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void -[SMEnterLicenseViewController register:](void * self, void * _cmd, void * arg2) {
rdx = arg2;
r15 = self;
r12 = _objc_msgSend;
[self commitEditing];
r14 = [[r15 licenseName] retain];
if ([r14 length] != 0x0) {
rbx = [(r12)(r15, @selector(licenseCode), rdx) retain];
r12 = (r12)(rbx, @selector(length), rdx);
[rbx release];
[r14 release];
if (r12 != 0x0) {
r14 = [[r15 delegate] retain];
r12 = [[r15 licenseName] retain];
rbx = [[r15 licenseCode] retain];
[r14 enterLicenseViewControllerDidSelectRegister:r15 withLicenseName:r12 code:rbx];
[rbx release];
[r12 release];
rdi = r14;
[rdi release];
}
}
else {
rdi = r14;
[rdi release];
}
return;
}

这里调用了[r14 enterLicenseViewControllerDidSelectRegister:r15 withLicenseName:r12 code:rbx]方法进行注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void -[SMLicenseWindowController enterLicenseViewControllerDidSelectRegister:withLicenseName:code:](void * self, void * _cmd, void * arg2, void * arg3, void * arg4) {
r8 = arg4;
var_30 = self;
r12 = [arg3 retain];
r13 = [r8 retain];
rax = [SMLicenseManager sharedInstance];
rax = [rax retain];
var_38 = rax;
rcx = r13;
rbx = [rax registerLicenseWithName:r12 code:rcx];
[r13 release];
[r12 release];
rsi = @selector(loadView:);
if (rbx != 0x0) {
_objc_msgSend(var_30, rsi, 0x2, rcx, r8);
r14 = [[var_30 delegate] retain];
[r14 licenseWindowControllerDidRegister:var_30, rcx, r8];
[r14 release];
}
else {
_objc_msgSend(var_30, rsi);
}
[var_38 release];
return;
}

可以看到这里的registerLicenseWithName:code:里面是通过[SMLicenseManager verifyLicenseWithName:code:]验证的。可以直接修改该方法使其返回YES。

签名校验

查看[SMAppDelegate applicationWillFinishLaunching:]方法:

1
2
3
4
5
6
7
8
9
10
11
void -[SMAppDelegate applicationWillFinishLaunching:](void * self, void * _cmd, void * arg2) {
rdx = arg2;
rbx = self;
var_30 = *___stack_chk_guard;
rax = [NSBundle mainBundle];
rax = [rax retain];
var_128 = rax;
if ([rax codeSignState] != 0x2) goto loc_100024851;
// ...
}

也就是说,app启动完成之前,会调用[[NSBundle mainBundle] codeSignState]来检查bundle的签名是否合法。如果不合法,会弹出一个提示框Signature of the Interface Inspector is broken,然后app就直接退出了。所以我们首先要绕过这个检查。

显然,codeSignState这个函数并非官方API,应该是作者自己添加的NSBundle category。搜索了一下该函数名,没有找到。于是就去app的Frameworks目录下看看是不是由第三方库引入的。这里有3个framework:DFeedback.frameworkSMFoundation.frameworkSparkle.framework,很明显,第一个肯定是收集用户feedback用的,第三个是著名的升级框架,那就剩下第二个了。用Hopper加载SMFoundation,果然搜索到了codeSignState这个函数。

在这个函数里,作者用了SecStaticCodeCheckValidityWithErrors方法来检查签名的合法性的。具体用法可以查阅文档,这里只提一下比较重要的2个参数staticCoderequirement:staticCode是待校验的code object,requirement则表示staticCode需要满足的校验条件。作者使用的校验条件是certificate leaf = H"0E1D40082148472951C6FB2DDCD8800D82629792"

查阅了一下文档才明白这个用法。其实就是校验一下签名证书的叶子节点是不是 H”0E1D40082148472951C6FB2DDCD8800D82629792” 这个值,而这一串字符是签名证书的SHA1 FingerPrints,由40 个HEX 字符组成,可以在自己的开发者证书里查到。

也就是说,如果用别人的证书重新签名该app,而又没有同时修改这个校验条件的话,那么最终的签名就是不合法的,app就会闪退了。

要绕过applicationWillFinishLaunching中的codeSignState的验证,只要将返回值的判断从0x2修改为0x1即可:

asm_codesign

或者直接把跳转逻辑NOP掉:

NOP_codesign

生成一个新的可执行文件,File -> Produce New Executable,选择Remove Signature`替换原可执行文件。

License验证的tricky

再次打开App发现闪退了,看来还是有什么地方没改好。重新回去检查整个 SMLicenseManager类,最终发现了这么个函数[SMLicenseManager load]。作者在这里耍了一个小花招:

1
2
3
4
5
6
void ___24+[SMLicenseManager load]_block_invoke(void * _block) {
if ([SMLicenseManager verifyLicenseWithName:@"Test User" code:@"GAWAE-8C69D-7LZ5H-9D8M3-HVEG7-KHNQC-CQ7RF-SEPQC-CRF82-G47U5-H6DKAB-8SKA7-EWSCM-7Q7SV-MYF4"] != 0x0) {
[*_NSApp terminate:0x0];
}
return;
}

虽然我们把[SMLicenseManager verifyLicenseWithName:code:]改成了总是返回YES,但是代码走到这里撞墙了,Test User验证之后也返回YES了,然后App直接terminate:了。

也就是说,作者在这里放了一个本来就是非法的License,正常验证的话肯定是返回 NO的,就不会导致直接退出了。所以我们还需要把这里的判断条件改成XXX == NO,这样就没有问题了。

比较简单的方法是直接绕过注册函数registerLicenseWithName中的逻辑,将原来的je修改为jne

verify_lisence

更完美的破解

修改完成后通过Hopper生成新的二进制程序,清除代码签名。
打开App,弹出激活窗口,随意输入姓名与激活码,确认即可。但是下次打开时还是会弹出激活窗口,因此这样的破解并不完美。

因此我们直接修改[[SMLicenseManager sharedInstance]isLicensed]方法:

NOP_license

mov_license

NOP_words

再次打开App,现在已经完美破解了,甚至不需要输入License信息。

Attach应用的bug

至此Interface Inspector已经可以直接运行了,但是在macOS 10.12上Attach程序的时候会报错。

原因就是因为这个软件使用了mach_inject_bundle.framework框架,macOS 10.12上__pthread_set_self方法修改为了_pthread_set_self,作者已经修复了这个bug。

mach_inject 1.3: Nov 08 2016
[NEW] Add demo project (Erwan Barrier)
[CHANGE] Switch from __pthread_set_self to _pthread_set_self on 10.12. (wzqcongcong, rentzsch)
[FIX] Adjust stack alignment for 10.9 to avoid crash. issue 5 (Giovanni Donelli)
[FIX] Comment out fprintf for 10.10 and genericize CODE_SIGN_IDENTITY. issue 13 (Rainburst)
[FIX] Add error check. (Nat!)

所以只需要去Github上下载一份源码重新编译一下,将编译出mach_inject_bundle.framework文件放在/Library/Frameworks/mach_inject_bundle.framework目录下,替换掉旧的。

这里有个需要注意的小细节,Interface Inspector在加载mach_inject_bundle_stub这个bundle时,是按照Bundle ID com.rentzsch.mach-inject-bundle-stub来查找的,所以需要把工程文件里的Bundle ID改成跟原来的Bundle ID一样。

好了,再次使用interface-inspector注入app,成功了。

可以参考网上其他公开的方法来破解。本文只作记录逆向学习使用,破解完成后切勿公开,支持正版软件。