本文由 简悦SimpRead 转码,原文地址 github.com
Simple Input Manager Bundle Loader。为msolo/simbl的开发做贡献,请创建一个账户 ......
我已经编写了一些与现有应用程序集成的插件。就像我经常被要求提供源代码一样,我也被问到我是如何想出这一切的。这不是火箭科学--它更像是考古学。基本上,从某个地方开始,慢慢地削去,并作出一系列逐步的更明智的决定。显然,这是对这一过程的粗略简化,但没有什么黑科技--你只需要一些提示。这基本上就是本文件的目的--让你开始。我把它作为一个wiki页面,因为我预见到会有额外的澄清和评论的需要。
基础知识
你应该对Cocoa和Objective-C有所了解,因为我将要谈论的几乎所有东西都与它有某种联系。你不需要是专家,我第一次使用ProjectBuilder/Cocoa/Objective-C是为了把符号补全侵入ProjectBuilder。我几乎是同时学会了Cocoa/Objective-C、黑客技术和反转技术。如果你知道使用XCode/ProjectBuilder来建立一个捆绑包并加载它,你就具备了良好的条件。即使你不知道,也要大胆地继续阅读。
选择你的目标
那么,假设你想黑掉Terminal.app,这样你就可以改变ANSI文本的颜色。好的选择。现在你必须想出办法--但没有源代码。不要害怕,公民,在编译的Objective-C中嵌入了大量的元数据,你可以很容易地重新创建代码的内部结构。但是你需要一些工具来做这件事。
交易的工具
nm
包含在几乎所有类似Unix的平台上。它可以跳出一个C语言函数名、被篡改的C++调用、以及Objective-C信息名的列表。strings
也是相当普遍的。它从给定的二进制文件中跳出字符串--这比你想象的更方便。很多时候,当你看二进制文件中的字符串时,"秘密 "偏好键就会暴露出来。gdb
有很好的装备来帮助你前进。你可以在gdb中运行任何应用程序,并在Objective-C信息上设置断点,就像你对C函数一样。你也可以在运行时做一些尝试来探索数据结构。非常强大,但不是最容易使用的工具。- class-dump是你的朋友。class-dump可以加载一段给定的Objective-C代码并生成一个相当有说服力的头文件,其中包含的类。这将给你一个很好的应用程序的快照,你可以从其中包含的信息中学到很多。
- FScript/FScriptAnywhere是一种类似Smalltalk的脚本语言,与Objective-C和Cocoa很好地结合在一起。FScriptAnywhere是一个SIMBL插件,可以将FScript解释器加载到任何Cocoa应用程序中。一旦解释器被加载,你就可以在运行时探索代码对象--检查值、调用方法等。这是很强大的--但你当然也可以完全破坏应用程序的数据。对任何自动保存数据库或复杂数据结构的东西(如iPhoto或Mail)要特别小心,在你开始摆弄之前一定要做好备份。
- SIMBL是一个框架,用于在应用程序启动时将标准的捆绑程序加载到其中。你基本上编译了一个标准的Cocoa包(使用默认的XCode模板),但在Info.plist中添加了几个特殊的键,以指定哪些应用程序应该加载你的包。文章中还有更多信息和一些代码片段。
中间部分
很明显,每个应用程序都是不同的。有些程序比其他程序更容易进行反向工程。通常情况下,应用程序的工程化程度越高,就越容易解开它并进行修改。弄清楚你可以做哪些改变是一个小小的挑战,没有什么可以说的,可以使它更容易。你得设法找出有效的方法。你需要寻找你可以轻松覆盖的方法,如果可能的话,尽量不要直接访问成员变量。我的策略通常是 "classdump "二进制文件并读取类名,然后开始搜索相关的方法名。有时使用FSG会容易得多。如果你想知道一个表格视图从哪里得到它的数据,你可以使用Script的对象浏览器部分来揭示一个控件后面的视图对象。现在你可以在视图的层次结构中上下移动,找到你最感兴趣的对象。从那里你可以探索相关的数据源。通常这能让你更快地到达 "你想去的地方"。
修补代码
假设你已经找到了一个合适的方法来覆盖(现在看来这可能是一个很大的假设,但最终你会找到你的位置),你需要知道如何用你的代码来替代现有的函数。有几个 "简单 "的选择,但它们都有一些限制。
Posing
类摆放是Objective-C运行时启用的有用技术。简而言之,这允许你将类A的所有未来实例创建为类B,其中类B是A的子类。[[B class] poseAsClass:[A class]];
我不会说得太详细--你可以阅读苹果关于类摆放的文档或+ [NSObject poseAsClass:(Class)_class]
。摆放有几个缺点。
-
只有在摆放后创建的实例才会被创建为子类。这意味着,如果A类的实例是在你执行姿势之前创建的,它们可能会在周围徘徊。通常情况下,这不会成为一个大问题,但在你疯狂地想知道为什么你的代码没有被替换进来之前,最好先知道这一点。
-
你不能在子类中添加实例变量(这主要违背了子类的意义)。我不是100%确定这里的推理,但我敢猜测,这与一些关于对象大小不变的假设有关。幸运的是,你可以用下面的习语来近似实例变量。
static NSMutableDictionary* s_fakeIvars = nil;
- (void) initialize {
s_fakeIvars = [[NSMutableDictionary alloc] init];
}
- (id) init {
self = [super init];
[s_fakeIvars setObject:[NSMutableDictionary dictionary] forKey:[NSNumber numberWithInt:(int)self]];
}
- (void) dealloc {
[super dealloc]; // note: assumes 32-bit pointers
[s_fakeIvars removeObjectForKey:[NSNumber numberWithInt:(int)self]];
}
- 如果你冒充一个已经生成头文件的类(例如,用classdump),如果大小发生变化(例如,目标应用程序的新版本),你的插件将可能导致应用程序崩溃。
- 多个插件可以冒充同一个类--这可能会变得有点棘手。
Method Swizzling
我忘了我在哪里第一次听到 "method swizzling "这个词,但它是一个好词。基本上,swizzling意味着你将某个特定方法的实现与另一个方法进行了交换。另一种说法是 "重命名一个方法"。你在插手Objective-C运行时的内部结构,但这并不像听起来那么邪恶。这里有一个来自DuctTape的片段,可能值1000字。
/**
* Renames the selector for a given method.
* Searches for a method with _oldSelector and reassigned _newSelector to that
* implementation.
* @return NO on an error and the methods were not swizzled
*/
BOOL DTRenameSelector(Class _class, SEL _oldSelector, SEL _newSelector) {
Method method = nil;
// First, look for the methods
method = class_getInstanceMethod(_class, _oldSelector);
if (method == nil)
return NO;
method->method_name = _newSelector;
return YES;
}
这意味着选择器的原始名称被放弃了,不能再调用预期的代码。这本身可能并不那么有用,所以我经常使用以下成语。
// never implemented, just here to silence a compiler warning
@interface WebInternalImage (PHWebInternalImageSwizzle)
- (void) _webkit_scheduleFrame;
@end
@implementation WebInternalImage (PHWebInternalImage)
- (void) initialize {
DTRenameSelector([self class], @selector(scheduleFrame), @selector (_webkit_scheduleFrame));
DTRenameSelector([self class], @selector(_ph_scheduleFrame), @selector(scheduleFrame));
}
- (void) _ph_scheduleFrame {
// do something crazy...
...
// call the "super" method - this method doesn't exist until runtime
[self _webkit_scheduleFrame];
}
@end
这个片段来自WebKit中的图像动画代码,PithHelmet重写了该代码。很明显,你的方法和原始方法应该有相同的签名。我通常会在代码中预留相关的缩写,以保持代码的完整性。由于Objective-C中所有的方法调用都是在运行时进行查询的,所以以前调用- [WebInternalImage scheduleFrame]
的东西现在都被转到了你的方法。非常酷。方法旋转有一些和类摆放一样的限制和变通。你不能添加真正的实例变量,但幸运的是,上面描述的技术对swizzling也很有效。一个好处是,一旦你对方法进行了刷新,任何对象都会调用你的代码--无论它的目标是在刷新之前还是之后创建的。正如我之前所说,这并不经常有用,但当你需要它时,你会很高兴它能发挥作用。
构建你的补丁
现在你需要把你的代码放到你的目标应用程序中。我将带领你完成一个非常基本的SIMBL插件。
为什么使用SIMBL?
严格来说,这并不是必须的。你可以尝试把你的插件打包成一个InputManager,但当你处理这种方法的各种缺点时,你最终会写出你自己的SIMBL版本,并浪费大量时间来解决真正的通用问题。当然,这很无聊,这可能是不这样做的最好理由。SIMBL为你做了几件很酷的事情。
- 以安全的方式加载Cocoa捆绑包,这样你的插件就只能安装在预定的应用程序中
- 正确地搜索库的层次结构,以支持用户特定的和系统范围内的插件安装
- 确保只加载一个版本的插件(倾向于用户指定的版本)
- 允许你针对一个应用程序的特定版本来抑制不必要的崩溃 此外,我估计SIMBL已经安装在超过10万台机器上(在许多不同的国家),所以它已经被很好地测试,并且最不可能在任何应用程序本身产生一个问题。
创建一个SIMBL插件包。
创建一个SIMBL插件项目是非常简单的,但有几件事还没有真正被正确记录下来。
-
在XCode中创建一个新的 "Cocoa Bundle "项目。
-
创建一个基本的插件类--比如,
MySamplePlugin
。你将用这个类作为一个跳板来设置你其余的黑客。 -
编辑
Info.plist
。- 设置
NSPrincipalClass
为MySamplePlugin
- 创建一个新的数组键
SIMBLTargetApplications
- 创建一个
SIMBLTargetApplications
的子字典,键为BundleIdentifier
,MaxBundleVersion
和MinBundleVersion
。所有这些都应该有字符串值。BundleIdentifier
应该与你的目标程序相匹配 - 比如说com.apple.Terminal
MaxBundleVersion
应该是你的目标程序的最大值_CFBundleVersion
的一个字符串版本。为了安全起见,你可以只写当前的版本。MinBundleVersion
应该是你的目标应用程序的最小值CFBundleVersion
的字符串版本。同样,你可以只写当前的版本。- 目前,所有的版本号都必须是可解析的整数,但这在将来可能会改变。当你开始与他人分享你的插件时,这些键变得更加相关。基本上,它们是一种防止你的插件加载到你还没有机会测试的应用程序的版本中的方法。如果SIMBL遇到了一个具有越界版本的应用程序,它会礼貌地告诉用户,它不会加载该插件,并与开发人员联系。
<key>SIMBLTargetApplications</key> <array> <dict> <key>BundleIdentifier</key> <string>com.apple.Safari</string> <key>MaxBundleVersion</key> <string>412</string> <key>MinBundleVersion</key> </dict> </array>
- 设置
-
构建你的插件类。你可能想把大部分的初始化代码放在
load
class方法中。这是一个特殊的方法,SIMBL在加载一个插件的主类时寻找这个方法。load
在一个很好的 "安全 "时间被调用,此时应用程序已经大部分初始化了,并且几乎准备好与用户交互。虽然你可以把你的初始化代码放在其他方法中,但我建议把它放在这里。通常我把这个插件类变成一个单子对象,因为有一个单一的位置来调用 "家 "通常是很好的。
@implementation MySamplePlugin*
/**
* A special method called by SIMBL once the application has started and all classes are initialized.
***/
+ (void) load {
MySamplePlugin*** plugin = [MySamplePlugin sharedInstance];
// ... do whatever
NSLog(@"MySamplePlugin installed");
}
/**
* @return the single static instance of the plugin object
***/
+ (MySamplePlugin***) sharedInstance {
static MySamplePlugin* plugin = nil;
if (plugin == nil)
plugin = [[MySamplePlugin alloc] init];
return plugin;
}
- 编译。希望你不会得到太多的错误,但这有时会有点棘手。你可能需要设置这些方便的标志(LinkerSettings)之一,以便在Cocoa中进行黑客攻击(在其他链接器标志下设置这些标志)
-undefined suppress
(不过,当使用两级命名空间时,XCode不喜欢这样)-undefined define_a_way
。
当你在构建一个捆绑包时,这些抑制了链接警告,你知道它将被加载到一个内存空间,其中某些函数被假定为已经被定义。这对于让很多SIMBL的黑客工作是很有帮助的。
- 确保你的插件被加载。通常我创建一个用户插件目录,并将插件捆绑在一起,这样我就可以在不影响其他用户的情况下快速打开或关闭它们。
$ mkdir -p ~/Library/Application/Support/SIMBL/Plugins
$ cd ~/Library/Application/Support/SIMBL/Plugins
$ ln -s ~/MySamplePlugin/build/MySamplePlugin.bundle .
常见的陷阱
事情不可避免地会出错,你可能会浪费很多时间来追踪问题。这里有一些可能会咬你的事情。
软件更新
如果你做了一些类的摆设,或者最终不得不直接访问成员变量,你可能会这样做,请注意,当底层应用程序被升级时,你的插件可能会损坏。如果你用来编译你的插件的@interface
与实际的应用程序不同步,你会得到随机崩溃。这是我检查任何新代码发布的第一件事。你几乎总是要调整你扩展的对象的大小。
符号冲突
如果你有一段名称冲突的代码,事情会变得很难看。有3种常见的方式会发生这种情况。1. 你给一个类的名字和另一个已经存在于应用程序运行时的类完全一样(这包括在运行时可能已经安装的其他插件)。我通常为一个特定项目的所有类挑选一个2或3个字母的前缀,以避免命名冲突。
- 你有一个类别,定义了一个新的方法,与现有的方法有相同的名字。同样,我通常会使用一个前缀--特别是对Foundation或AppKit对象的类别。
- 你定义了一个与现有C函数同名的C函数。
你在这里感觉到了一个模式吗?是的,前缀。
负责任的修补
使用版本检查来关闭你在应用程序中的插件,这些插件根本无法工作--你不想通过破坏每一个应用程序的升级来给其他开发者/Apple带来不必要的压力。相信我--我学到了艰难的方法。我确信在Safari开发者附近有一份写着我名字的黑名单。
出去玩吧。你只需要修补一下就能掌握它的窍门。如果你需要帮助,请在维基上留言或给我发邮件。