本文内容仅做为学习交流,但愿你们多多支持正版软件。swift
Emmmmm... 其实最初是准备写一篇关于 iOS 应用的逆向笔记的,不过一直没找到合适的目标 App 以及难度适宜的功能点来做为写做素材...bash
破解了 Bartender 以后我以为对于 Bartender 的破解过程难度适中,很是适合当作素材来写,且不管是 Mac App 仍是 iOS App,逆向的思路都是相通的,因此就写了这篇文章~微信
国庆以前,果果放出了最新操做系统 macOS Mojave 的正式版本,相信不少小伙伴都跟我同样在正式版发布后紧跟着就升级了系统(此前因为工做设备参与项目产出须要确保系统稳定性因此没敢尝鲜的同窗应该不仅我一我的哈)。app
升级到正式版 macOS Mojave 以后,我兴致勃勃的在新系统中各处探索了一番,而后将系统切换到 Dark Mode 后打开 Xcode 心满意足地敲(搬)起了代码(砖)...async
嘛~ 又是一个惬意的午后,有时候人就是这么容易知足(笑)~函数
等等!这是什么鬼!?个人 Bartender 怎么不能正常工做了(其实如今回想起来应该是试用期到期了)...工具
本文将以 Bartender 为目标 App,讲解如何经过静态分析工具 Hopper 逐步分析 Bartender 的内部实现逻辑并结合动态分析等手段逐步破解 Bartender 的过程与思路~学习
Bartender 是一款能够帮助咱们整理屏幕顶部菜单栏图标的工具。ui
随着咱们安装的 App 不断增多,屏幕顶部菜单栏上面的图标也会对应不断增长。这些 App 的图标并不是出自一家之手,风格各异,随着数目增多逐渐显得杂乱不堪。spa
咱们能够经过 Bartender 来隐藏或从新排列这些恼人的小图标,能够将没什么用可是运行起来却要显示的 App 图标始终隐藏,将偶尔会用的 App 图标隐藏到 Bartender 功能按钮后面(用到的时候能够经过点击 Bartender 功能按钮切换显隐),只显示经常使用的或者咱们认为好看的应用图标。
除此以外 Bartender 还具有一些其余更加深刻的功能(好比支持所有菜单栏条目范围的搜索等等),毫无疑问它是一款很是棒的菜单栏图标管理工具。
Note: 重申,Bartender 仅售 15 刀,仍是推荐各位使用正版,本文仅做为学习交流。
Hopper 是一款不错的 mac OS 与 Linux 反汇编工具,同时还提供必定的反编译能力,能够利用它来调试咱们的程序。此外,Hopper 还支持控制流视图模式,Python 脚本,LLDB & GDB,而且提供了 Hopper SDK 可供扩展,在 Hopper SDK 的基础上你甚至能够扩展本身的文件格式和 CPU 支持。
值得一提的是 Hopper 的做者是一名独立开发者,他的平常工做环境也是在 mac OS 上,因此在 mac OS 上的 Hopper 是彻底使用 Cocoa Framework 实现的,而 Linux 版本的 Hopper 则选择使用 Qt 5 来实现。
我的认为 Hopper 在 mac OS 上面的运行表现很是好,不少细节(好比类型颜色区分等)都作的不错,功能简洁的同时快捷键也很好记(Hopper 提供的功能已经覆盖了绝大多数使用场景)。
最关键的一点是收费良心,我的证书只要 99 刀,当之无愧的人人都买得起的逆向工具!固然若是你以为贵,Hopper 还提供试用,试用形式相似于 Charles,每次开启后能够试用 30 分钟,通常状况下这已经够用了。
Note: Hopper v4.4.0 支持 Mojave Dark Mode。
这一章节的内容会详细的讲述我我的在破解 Bartender 过程当中的想法以及中间遇到问题时解决问题的思路,以前没有涉足逆向或者逆向经验尚浅的同窗可能会以为比较晦涩,这种状况最好结合本身的实际操做反复阅读没有理解的地方直到真正弄明白为止。
相信本身,每一份努力终会有所回报!当有朝一日本身也能够经过本身的逆向技术破解 & 定制化本身感兴趣的 App 时,你会发现一切的努力都是值得的。
从 Bartender 官网下载最新的 Bartender,截止本文提笔以前 Bartender 的最新版本为 3.0.47。
将下载好的压缩包解压以后获得 Bartender 3.app,将 Bartender 3.app 文件复制到本身的 Application 文件夹下。右键点击 Bartender 3.app 选择“显示包内容”,在 Contents 目录下找到 MacOS 目录,里面有咱们要的目标二进制文件 Bartender 3。
打开 Hopper,将目标二进制文件拖入 Hopper,在弹出的弹窗中选择 OK 后等待 Hopper 分析完毕。
在左侧的分栏中选择 Proc.
,这可让咱们查看 Hopper 分析出来的方法。分栏下面有搜索框,内部能够经过输入关键词来过滤出咱们想要的结果。由于通常的 App 都是经过某些方法判断是否受权的,这里咱们先输入 is
(注意 is 前面加空格),而后观察过滤出来的结果。
果不其然,发现里面有三个 [xxx isLicensed]
方法,点击方法 Hopper 会跳转至方法处。
Note: 三处
[xxx isLicensed]
的方法内部逻辑几乎同样,这里拿[Bartender_3.AppDelegate isLicensed]
讲解,其余两处不作赘述。
Emmmmm... 这里的汇编代码仍是比较简单的,虽然我不是很了解 x86 的汇编指令,不过 Hopper 已经帮助咱们作了一些辅助性工做。其中开始处的 push rbp
以及结束处 pop rbp
能够简单理解为入栈出栈,call sub_100067830
能够理解为调用地址 0x100067830
处的方法,pop
以前的 movsx eax, al
和 ARM64 中的 mov
指令相似,能够理解为将 al
内存储的东西移动到 eax
寄存器中,eax
寄存器用于存储 x86 的方法返回值。
咱们能够看出这里调用了地址 0x100067830
处的函数,拿到结果以后又调用了 imp___stubs__$S10ObjectiveC22_convertBoolToObjCBoolyAA0eF0VSbF
方法将结果作了转化,最后将结果赋值给 eax 寄存器用于结果返回。其中 imp___stubs__$S10ObjectiveC22_convertBoolToObjCBoolyAA0eF0VSbF
咱们能够根据名称推测出该方法的做用应该是将 Bool
转化为 Objective-C 的 BOOL 而已。
那么关键信息应该在 sub_100067830
处,双击 sub_100067830
Hopper 会跳转到 0x100067830
处,这样咱们就能够分析其中的具体实现了。不过 0x100067830
内部的实现比较复杂,跳转过去以后发现汇编代码很是多,还有不少跳转... 这时候咱们能够经过 Hopper 顶部中间靠右一点的分栏,点击显示为 if(b) f(x);
的按钮查看伪代码。
Hopper 解析出来的伪代码风格相似 Objective-C 代码,能够看到 0x100067830
内部经过 NSUserDefaults
以及其余的逻辑实现,其中还包括其余的形式为 sub_xxxxxx
的方法调用,这种状况下若是咱们继续跳转到这些方法的地址查看其内部实现颇有可能陷入递归中...
那么这种状况该如何处理呢?
分析问题,咱们找到 [xxx isLicensed]
而且以为这有可能就是 Bartender 中判断受权与否的函数,那么咱们只须要将三处 [xxx isLicensed]
的返回值改成 true
便可。因此这里咱们没有必要一步步的看其内部实现,先返回 [Bartender_3.AppDelegate isLicensed]
处。前面讲过在 x86 汇编中 eax
寄存器用于存储方法的返回值,咱们在 [Bartender_3.AppDelegate isLicensed]
按快捷键 option + A
插入汇编代码 mov eax, 0x1
将 eax
永远赋值为 1
即 true
以后跟 ret
即 return 指令直接让函数返回 true
就能够达到咱们的目的了。
用快捷键 shift + command + E
导出二进制文件,覆盖到原 Bartender 目录中,尝试运行。你会发现一开始是成功的,屏幕顶部的菜单栏图标也被正常管理了,可是过了大约 10s 以后一切又变回了原样,而且还会弹出一个试用期到期的弹窗...
那么咱们刚才修改的三处 [xxx isLicensed]
为何没有产生做用呢?其实它已经产生做用了,虽然咱们不能够正常使用 Bartender,可是打开 Bartender 的 License 界面咱们能够发现这里的界面已经显示咱们付过款了,尽管这并无什么卵用就是了...
到这里咱们彷佛没有什么头绪了,由于延时方法有不少,光是凭借这一条线索很难定位到阻止咱们破解的目标代码位置。
逆向过程当中的思路很重要,若是遇到思路断了的状况不要着急也不要气馁,咱们能够从新运行程序,尝试不一样的操做并观察操做对应的表现 & 结果。
通过反复运行程序,我发现每次从新启动 Bartender 均可以有大约 10s 的可用时间,若是启动以后直接主动点击 Bartender 的功能按钮则会直接弹出试用期到期弹窗且顶部菜单栏图标也会直接回到以前杂乱的样子。
这时候个人思路从延时方法转移到了这个 Trial ended 弹窗以及 Bartender 的功能按钮点击以后的对应方法上。这就是动态分析,它能够帮助咱们从新找回思路。
有了思路,对应的方法并不难找。咱们能够利用 Hopper 的 Tag Scope 先把可能出现的区域找出来,再到对应的区域下的方法列表中寻找咱们的目标方法位置。
这里我很快就找到了目标函数 -[_TtC11Bartender_311AppDelegate bartenderStatusItemClickWithSender:]
, 其内部调用了 sub_100029ac0(arg2);
其中 arg2
就是 sender
,也就是这个 Bartender 的功能按钮了。
int sub_100029ac0(int arg0) {
sub_100022840(arg0);
rbx = **_NSApp;
if (rbx == 0x0) goto loc_100029f44;
loc_100029ae7:
[rbx retain];
r14 = [[rbx currentEvent] retain];
rdi = rbx;
if (r14 == 0x0) goto loc_100029bef;
loc_100029b18:
[rdi release];
if (([r14 modifierFlags] & 0x80000) != 0x0) goto loc_100029b6e;
loc_100029b33:
[r14 retain];
if ((([r14 modifierFlags] & 0x40000) != 0x0) || ([r14 type] == 0x4)) goto loc_100029b66;
loc_100029bcc:
rbx = [r14 type];
[r14 release];
if (rbx == 0x3) goto loc_100029b6e;
loc_100029bec:
rdi = r14;
goto loc_100029bef;
loc_100029bef:
[rdi release];
r14 = [[swift_getInitializedObjCClass(@class(NSUserDefaults)) standardUserDefaults] retain];
if (*qword_1000e7e70 != 0xffffffffffffffff) {
swift_once(qword_1000e7e70, sub_100069790);
}
rbx = *qword_1000ee1f8;
r15 = *qword_1000ee200;
swift_bridgeObjectRetain(rbx);
r15 = (extension in Foundation):Swift.String._bridgeToObjectiveC() -> __ObjC.NSString(rbx, r15);
swift_bridgeObjectRelease(rbx);
rbx = [[r14 objectForKey:r15] retain];
[r15 release];
[r14 release];
if (rbx != 0x0) {
swift_getObjectType(rbx);
var_50 = rbx;
}
else {
intrinsic_movaps(var_40, 0x0);
var_50 = intrinsic_movaps(var_50, 0x0);
}
rax = sub_10001c9a0(&var_50, &var_78);
if (var_58 != 0x1) goto loc_100029cd8;
loc_100029ccd:
sub_10001c2f0(&var_78);
goto loc_100029d44;
loc_100029d44:
if (*(int8_t *)(r13 + *objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded) == 0x1) {
rax = sub_1000230e0(0x1);
}
else {
*(int8_t *)(r13 + *objc_ivar_offset__TtC11Bartender_311AppDelegate_performDelayedClicks) = 0x1;
rax = sub_1000215f0();
if ((rax & 0x1) == 0x0) {
rbx = *objc_ivar_offset__TtC11Bartender_311AppDelegate_performDelayedClicks;
rax = *(int8_t *)(r13 + rbx);
rax = !rax & 0x1;
*(int8_t *)(r13 + rbx) = rax;
}
}
return rax;
loc_100029cd8:
rcx = *qword_1000e8a98;
if (rcx == 0x0) {
rcx = swift_getObjCClassMetadata(swift_getInitializedObjCClass(@class(NSDictionary)));
*qword_1000e8a98 = rcx;
}
rax = swift_dynamicCast(&var_28, &var_78, *type metadata for Any + 0x8);
if (rax == 0x0) goto loc_100029d44;
loc_100029d24:
r14 = var_28;
if ([r14 count] == 0x0) goto loc_100029d8f;
loc_100029d3c:
[r14 release];
goto loc_100029d44;
loc_100029d8f:
r15 = [objc_allocWithZone(@class(NSAlert)) init];
rbx = sub_1000a7f20("No menu items have been setup", 0x1d, 0x1, rcx, 0x6);
r12 = (extension in Foundation):Swift.String._bridgeToObjectiveC() -> __ObjC.NSString(rbx, 0x1);
swift_bridgeObjectRelease(rbx);
[r15 setMessageText:r12];
[r12 release];
rbx = sub_1000a7f20("No menu items have been setup in Bartender Preferences, so Bartender is not doing anything yet. Would you like to open preferences now.", 0x87, 0x1, rcx, 0x6);
r12 = (extension in Foundation):Swift.String._bridgeToObjectiveC() -> __ObjC.NSString(rbx, 0x1);
swift_bridgeObjectRelease(rbx);
[r15 setInformativeText:r12];
[r12 release];
[r15 setAlertStyle:0x1];
rbx = sub_1000a7f20("Open Preferences", 0x10, 0x1, rcx, 0x6);
r12 = (extension in Foundation):Swift.String._bridgeToObjectiveC() -> __ObjC.NSString(rbx, 0x1);
swift_bridgeObjectRelease(rbx);
rbx = [[r15 addButtonWithTitle:r12] retain];
[r12 release];
[rbx release];
rbx = sub_1000a7f20("Dismiss", 0x7, 0x1, rcx, 0x6);
r12 = (extension in Foundation):Swift.String._bridgeToObjectiveC() -> __ObjC.NSString(rbx, 0x1);
swift_bridgeObjectRelease(rbx);
rbx = [[r15 addButtonWithTitle:r12] retain];
[r12 release];
[rbx release];
if ([r15 runModal] == 0x3e8) {
sub_100029a10();
}
[r15 release];
rax = [r14 release];
return rax;
loc_100029b6e:
*(int8_t *)(r13 + *objc_ivar_offset__TtC11Bartender_311AppDelegate_performDelayedClicks) = 0x0;
rdi = r14;
if (([rdi modifierFlags] & 0x40000) == 0x0) {
sub_100020de0();
}
else {
if (*(int8_t *)(r13 + *objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded) == 0x1) {
sub_1000230e0(0x1);
}
else {
sub_100020fe0(rdi);
}
}
rax = [r14 release];
return rax;
loc_100029b66:
[r14 release];
goto loc_100029b6e;
loc_100029f44:
asm { ud2 };
rax = sub_100029f46();
return rax;
}
复制代码
PS: 为了便于读者结合后面分析部分的内容快速定位(Command + F),上面的伪代码没有使用截图形式展现。
其中很醒目的是 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded
咱们按照以前的方法,将伪代码先切回汇编模式,找到对应的汇编代码处。
这是一段明显的 if
语句汇编代码,看下面的 mov edi, 0x1
这一小节就是指 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded
为 true
以后执行的代码,表示要是试用期到期就执行 0x1000230e0
处的方法。咱们记下这个地址以后把这两处的汇编代码经过上文插入汇编代码的方式修改一下,将这个 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded
直接替换为 0x0
即 false
。
在逆向工程中,切忌不能够冒进,时值今日几乎全部应用都会采起措施来增长其逆向难度。这时候千万不要想着一步到位,应该在适量修改以后尝试导出二进制,用动态分析的方法验证一下结果。由于咱们这时候不是正向开发者,在没有见到上下文的状况下修改代码极可能会把程序改为一个不可用的状态(好比正常功能损坏或者频繁 Crash),因此最好步步为营。
这里咱们导出修改以后的二进制文件,按照 Bartender 的原路径覆盖以前的二进制文件验证一下结果。我在这个阶段运行时发现若是正常开启 Bartender 仍是会有一个 10s 左右的可用时长,以后依然会弹出试用期到期弹窗,而且程序变为不可用状态;而若是重启 Bartender 在试用期弹窗弹出以前点击功能按钮则能够正常切换,可是再次点击按钮却切换不回来了,而且程序运行 10s 左右仍会弹出试用期到期弹窗,可是菜单栏上面的图标不会变失效,只是切不回去而已。
到目前为止若是不在意功能仅仅想要隐藏菜单栏的图标已是能够凑合用了,可是这显然不是咱们想要的最终结果。
经过上面运行程序后观察到的状况我推测在 -[_TtC11Bartender_311AppDelegate bartenderStatusItemClickWithSender:]
内部切换回来的逻辑中仍然有地方对是否到期作了判断,咱们上面只是成功修改了切换过去的逻辑,那么切换回来的逻辑在哪呢?
按逻辑推测,正向切换的时候是使用 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded
作判断,反向切换应该同理才对,咱们去追踪 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded
的使用,最终发现 sub_10001f870
中使用了 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded
且 sub_10001f870
被 sub_100029a10
调用,sub_100029a10
又被 sub_100029ac0
调用,sub_100029ac0
就是上文在 -[_TtC11Bartender_311AppDelegate bartenderStatusItemClickWithSender:]
中被调用的函数,这不只知足了被 Bartender 功能按钮所引用的条件,同时还对 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded
有所引用,因此我用插入汇编的方式将 sub_10001f870
中关于 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded
的使用改成了 0x0
,即 false
。
嘛~ 导出二进制覆盖,发现此次的 Bartender 已经能够正常使用功能了,不过试用期到期的弹窗问题依然存在,尽管它并不影响使用,但我仍是没法接受这样一个半成品的状态。
还记得上文中得出的 0x1000230e0
吗,若是试用期到期则会执行 0x1000230e0
地址处的方法,咱们经过快捷键 G
跳转到 0x1000230e0
地址,看一下里面的实现逻辑。
void sub_1000230e0(int arg0) {
r14 = arg0;
r15 = r13 + *objc_ivar_offset__TtC11Bartender_311AppDelegate_trialOverWindow;
rbx = swift_unknownWeakLoadStrong(r15);
if (rbx != 0x0) {
[rbx center];
[rbx release];
rbx = **_NSApp;
if (rbx != 0x0) {
[rbx retain];
[rbx activateIgnoringOtherApps:sign_extend_64($S10ObjectiveC22_convertBoolToObjCBoolyAA0eF0VSbF(r14 & 0xff))];
[rbx release];
rbx = swift_unknownWeakLoadStrong(r15);
if (rbx != 0x0) {
[rbx makeKeyAndOrderFront:0x0];
[rbx release];
}
else {
asm { ud2 };
sub_100023199();
}
}
else {
asm { ud2 };
loc_100023195();
}
}
else {
asm { ud2 };
loc_100023191();
}
return;
}
复制代码
经过上面的伪代码,咱们能够初步判断这个 0x1000230e0
内部就是弹出试用期到期弹窗的方法。接着咱们经过快捷键 X
查看关于 0x1000230e0
的引用,能够发现有三处调用,一个一个看下去发现第一个 sub_100022840
中的调用最像是延时调用,由于其中有 Hopper 反编译出来的 Dispatch 相关的伪代码。
$Ss10SetAlgebraPyxqd__cs8SequenceRd__7ElementQyd__ADRtzlufCTj(&var_A0, r13);
swift_release(*__swiftEmptyArrayStorage);
(extension in Dispatch):__ObjC.OS_dispatch_queueasyncAfterdeadlineqosflags.execute(Dispatch.DispatchTime, Dispatch.DispatchQoS, Dispatch.DispatchWorkItemFlags, @convention(block) () -> ()) -> ()(var_40, var_68, var_B0, var_30);
(*(var_D0 + 0x8))(var_B0, var_C8);
(*(var_C0 + 0x8))(var_68, var_B8);
_Block_release(var_30);
swift_release(var_D8);
(var_38)(var_40, var_70, rdx);
[var_A8 release];
sub_1000230e0(0x0);
rbx = var_48;
goto loc_100022df5;
复制代码
切到汇编模式,找到对应的汇编代码。
因为 sub_1000230e0(0x0);
是在 Dispatch 中调用的,考虑到修改后程序的稳定性,这里经过 Hopper 的 Modify 菜单中提供的 NOP Region 填平 call sub_1000230e0
汇编代码。
老规矩,导出二进制文件覆盖 Bartender 中的二进制后重启 Bartender 验收成果。
清爽~ 此次运行 Bartender 发现不但能够正常使用功能,以前烦人的试用期到期弹窗也被咱们成功干掉了。
每一次逆向的过程都是未知的,有的时候可能会很顺利(直接 mov eax, 0x1
+ ret
就搞定),有的时候可能会很曲折,有的时候可能还会以失败收尾。我写这篇文章主要是想与你们交流在逆向过程当中的常规方法以及遇到困难时的一些解决思路,其实不管是 Bartender 仍是其余应用,不管是 Mac 应用仍是 iOS 应用,逆向的思路都是相通的,愿各位同窗往后能够触类旁通。
若是有任何问题欢迎在文章下方留言或在个人微博 @Lision 联系我,真心但愿个人文章能够为你带来价值~
补充~ 我建了一个技术交流微信群,想在里面认识更多的朋友!若是各位同窗对文章有什么疑问或者工做之中遇到一些小问题均可以在群里找到我或者其余群友交流讨论,期待你的加入哟~
Emmmmm..因为微信群人数过百致使不能够扫码入群,因此请扫描上面的二维码关注公众号进群。