在利用theos開發一些插件時,我們經常會用到以下幾個指令:
%hook 指定需要hook的類名,以%end結尾
//hook的是SpringBoard這個類里面的方法 %hook SpringBoard -(void)_menuButtonDown:(id)down { NSLog(@"You've pressed home button"); %orig; //call the original _menuButtonDown } %end
%orig 執行被hook函數的原始代碼,類似於super.method功能
%hook ClassName - (void) _menuButtonDown: (id)down { NSlog(@"ss"); //如果去掉%orig,那么原始函數不會得到執行。 %orig; } @end
%new 該指令用來給現有的class添加一個新的函數。與Runtime中的class_addMethod相同。
%hook SpringBoard //hook內部的代碼 默認都是替換被hook類中函數的實現,所以如果不加%new,theos默認是去類中找namespaceNewMethod這個方法,替換它的方法實現。所以如果我們是新增的函數而不是更改原函數的內部實現則需要加%new這個指令 %new -(void) namespaceNewMethod { NSlog(@"你好"); } @end
%log 用來打印log的,將信息輸入到syslog中,可以以%log([(<type>)<expr>,...])
的格式追加其打印信息,如下:
%hook SpringBoard -(void) _menuButtonDown :(id)down { %log((NSString * )@"IOSER",(NSString *)@"Debug"); %orig; } @end
%group 該指令用於將%hook
分組,便於代碼管理及按條件初始化分組,必須以%end
結尾:一個%group
可以包含多個%hook
,所有不屬於某個自定義group的%hook
會被隱式歸類到%group _ungrounped
中,%gruop
的用法如下:

1 //這段代碼的含義為在%group iOS7hook中勾住了iOS7Class的iOS7Method,同理在iOS8Class的iOS8Method。然后在%group _ungrouped中勾住SpringBoard類的powerDown函數。 2 3 %group iOS7Hook 4 %hook iOS7Class 5 -(id) iOS7Method 6 { 7 8 id result = %orig; 9 NSlog(@"This class & method only exist in ios 8."); 10 return result; 11 } 12 @end 13 @end // iOS7Hook 14 15 16 %group iOS8Hook 17 %hook iOS8Class 18 -(id) iOS8Method 19 { 20 21 id result = %orig; 22 NSlog(@"This class & method only exist in ios 8."); 23 return result; 24 } 25 @end 26 @end // iOS8Hook 27 28 29 //所有不屬於某個自定義group的%hook會被隱式歸類到%group _ungrounped 所以其實下面powerDown函數其實是屬於group_ungrounped 組的 30 %hook SpringBoard 31 -(void) powerDown 32 { 33 %orig; 34 } 35 @end 36 37 //%ctor: tweak 的constructor ,完成初始化工作;如果不是顯示定義,theos會自動生成一個一個%ctor並在其中調用%init(_ungrouped)。默認只會自動初始化_ungrouped,不會初始化自定義的group 38 %ctor { 39 //只有調用了%init,對應的%group才能起作用、 40 %init(iOS7Hook); 41 %init(iOS8Hook); 42 43 //默認組 44 %init(_ungrouped); 45 }
%init 該指令用於初始化某個%group
,必須在%hook
或%ctor
內調用,如果帶參數,則初始化指定的group,如果不帶參數,則初始化_ungrouped
只有調用了%init
,對應的%group
才能起作用
%ctor { //帶參數的話是初始化自定義的group %init(iOS8); //不帶參數的話是初始化默認的_ungrouped %init; }
%ctor tweak的構造器,用來初始化。如果開發者沒有重寫這個方法,theos會自動生成%ctor
並在其中調用%init(_ungrouped)。
%ctor
一般可以用來初始化%group
,以及進行MSHookFunction
等操作。
注意:%ctor
不需要以%end
結尾。
1 %hook SpringBoard 2 3 -(void) reboot 4 { 5 NSlog(@"你好"); 6 %orig; 7 } 8 %end 9 10 //如果開發者不去重寫這個方法,theos默認是實現了這個方法,並在這個方法里初始化了_ungrouped 所以默認hook都會生效,但是如果用戶實現了這個方法但卻沒做初始化操作那就回導致hook失效 11 %ctor 12 { 13 // need to call %init explicitly! 14 } 15 //這里 %hook無法生效,因為這里顯示定義了%ctor,卻沒有顯示的調用%init,因此%group(_ungrouped)不起作用。
%c 該指令用來獲取一個類的名稱,類似於objc_getClass。%c([+|-]Class)
%dtor 在程序退出是調用。
tweak工程文件
我們創建完tweak項目后,會在文件夾內看到以下幾個文件:
control文件:該文件記錄了工程的基本信息,會被打包進deb包中,字段內容如下:
Package: com.leegof.reversedemo Name: ReverseDemo Depends: mobilesubstrate Version: 0.0.1 Architecture: iphoneos-arm Description: An awesome MobileSubstrate tweak! Maintainer: LeeGof Author: LeeGof Section: Tweaks
- Package字段:用於描述這個deb包的名字,采用的命名方式和bundle identifier類似,可以按需更改;
- Name字段:用於描述這個工程的名字,可以按需更改;
- Depends字段:用於描述這個deb包的“依賴”。“依賴”指的是這個程序運行的基本條件,可以填寫固件版本或其他程序,如果當前iOS不滿足“依賴”中所定義的條件,則此tweak無法正常工作,可以按需更改。例如:
//表示當前iOS版本必須在6.0以上,且必須安裝MobileSubstrate,才能正常運行這個tweak。 Depends: mobilesubstrate, firmware (>=6.0)
- Version字段:用於描述這個deb包的版本號,可以按需更改;
- Architecture字段:用於描述deb包安裝的目標設備架構,不要更改;
- Description字段:deb包的簡單介紹,可以按需更改;
- Maintainer字段:用於描述deb包的維護人,即deb包的制作者而非tweak的作者,可以按需更改;
- Author字段:用於描述tweak的作者,可以按需更改;
- Section字段:用於描述deb包所屬的程序類別,不要更改。
control文件中可以自定義的字段還有很多,一般上面的信息就已經足夠了。更全面的可以查看官方網站。
值得注意的是:Theos在打包deb時會對control文件做進一步處理。比如更改Version字段為:0.0.1-2,標識Theos的打包次數,方便管理;增加Installed-Size字段,用於描述deb包安裝后的估算大小,與實際大小可能有偏差,不要更改。
Makefile:該文件用來指定工程編譯和鏈接要用到的文件、框架、庫等信息,將整個過程自動化,自動生成的字段內容如下:
include $(THEOS)/makefiles/common.mk TWEAK_NAME = ReverseDemo ReverseDemo_FILES = Tweak.xm include $(THEOS_MAKE_PATH)/tweak.mk after-install:: install.exec "killall -9 SpringBoard"
- 第一行的include字段指定了工程的common.mk文件,固定寫法,不要修改;
- TWEAK_NAME字段填入的是建立工程時命令行輸入的Project Name,與control文件中的“Name”字段對應,不要更改;
- ReverseDemo_FILES:指定工程中參與編譯的源文件,如果工程中需要用到多個源文件則用空格將各個文件名分開,也可以用通配符,但是需要制定具體的路徑,比如scr文件夾內有二十個xm文件,如果一個個輸入太繁瑣,所以可以寫成ReverseDemo_FILES = Tweak.xm scr/*.m 可以按需修改。
- include字段指定工程的mk文件,這里新建的是tweak工程,所以填入的是tweak.mk文件,還可以根據需求填入application.mk以及tool.mk文件;
- 最后一行after-install字段指定安裝程序后需要執行的操作,這里需要注入SpringBoard進程並執行自己的代碼,因此需要重啟SpringBoard進程,好讓MobileSubstrate加載對應的dylib。
Makefile文件除了自動生成的這些字段外,還可以根據功能手動添加其他字段:
- ARCHS字段可以用來指定處理器架構,一般情況下填寫“ARCHS = armv7 arm64”即可;
- TARGET字段用來指定SDK版本,例如:TARGET = iphone:7.0
- THEOS_DEVICE_IP =192.168.1.100 指定安裝的手機ip(ssh) THEOS_DEVICE_PORT = 10010 指定端口
- framework字段可以指定要導入的框架,比如這里的測試demo中填寫的是“ReverseDemo_FRAMEWORKS = UIKit”,UIKit為后續測試代碼需要用到的框架,另一方面,還可以通過ReverseDemo_PRIVATE_FRAMEWORKS字段指定要導入的私有庫,格式不變。例如:
ReverseDemo_FRAMEWORKS = UIKit CoreTelephony CoreAudio ReverseDemo_PRIVATE_FRAMEWORKS = AppSupport ChatKit
ReverseDemo.plist:記錄工程的配置信息,描述了tweak的作用范圍,內容如下:
Filter下是一系列Array,可以分為三類:
Bundle:指定若干bundle為tweak的作用對象。如:com.apple.springboard
Classes:指定若干class為tweak的作用對象。如:NSString
Executables:指定若干可執行文件為tweak的作用對象。如:callservicesd
這三類Array可以混合使用,但當Filter下有不同類的Array時,需要添加一個“Mode: Any”鍵值對。當Filter下的Array只有一類時,不需要添加。
Tweak.xm:該文件是實現具體功能的關鍵所在,是實現具體功能的源文件,這個文件支持Logos和C、C++語法。文件內容如下:
%hook ClassName // 要替換方法的實現 - (void)messageName:(int)argument { %log; // Write a message about this call, including its class, name and arguments, to the system log. %orig; // Call through to the original function with its original arguments. %orig(nil); // Call through to the original function with a custom argument. // If you use %orig(), you MUST supply all arguments (except for self and _cmd, the automatically generated ones.) } %end
Tweak插件的圖片資源
iOS中常用的兩種加載圖片資源的方式:
+ (nullable UIImage *)imageNamed:(NSString *)name; // load from main bundle - (nullable instancetype)initWithContentsOfFile:(NSString *)path; //load image by path
+imageNamed:方式從程序的main bundle加載圖片,由於我們自己單獨開發的插架,資源是需要單獨管理的,無法從宿主app的MainBundle中讀取圖片。所以我們需要使用第二種方式,給定圖片的路徑去加載圖片。那么圖片的存放的路徑應該存放在手機的那個目錄下呢?
在tweak項目中,可以創建名稱為layout文件夾,將所有圖片等資源文件存放在這里。在打包安裝插架到手機中時,layout中的資源會打包到手機的根目錄下。也就是說layout就對應手機的根目錄/
圖片資源放到創建的layout文件夾中相當放到了設備的根路徑下,這時候我們可以在插件中引用該圖片資源。但是放到根路徑下顯然是不合適的,因為如果開發的插件圖片資源較多的話,根路徑會很亂。所以我們需要將插件中用到的圖片放到特定的文件夾內,也就是這個文件夾內是設備上所有插件的圖片資源庫,即下面這個preference文件夾。當然這里只是小的建議,並不是強制要求,直接放根路徑或任何路徑下都可以引用。
我們還可以為開發的插件在preference文件夾下新建一個新的文件夾,比如新建一個TweakWechatImage的文件夾,里面存放的都是我們開發的wechat插件的圖片。所以我們需要在layout中建立一個Library/preferenceLoader/Preferences/TweakWechatImage的文件夾路徑,這樣插件打包安裝時會自動將圖片資源放到設備上的這個路徑中。
這個時候,我們在調用圖片時,只需要填寫全路徑即可:
[UIImage imageWithContentsOfFile:@"/Library/PreferenceLoader/Preferences/TweakWechatImage/test.png"];
當然,如果多處地方用到圖片資源的話,我們可以寫一個宏定義,不用每次都寫這么一大串:
//這是宏定義的語法,一個是@“a”空格@"b”含義就是: a/b 第二個是# :字符串操作符,用於將參數序列化成一個字符串;即#abc 含義為@"abc" #define IMAGE_PATH(IMG_NAME) @"/Library/PreferenceLoader/Preferences/TweakWechatImage" #IMG_NAME
我們下次再調用圖片時就很簡潔了:
[UIImage imageWithContentsOfFile:IMAGE_PATH(test.png)];
Tweak插件的實現原理
我們通過Theos開發的插件其實只是改變app中某些方法在內存中的調用實現,並沒有修改app的執行文件。在點擊app圖標運行時,crype中的Substrate插件(Substrate負責管理Device/Library/MobileSubstrate文件夾中的內容,這個插件是越獄后自動裝好的)會去這個路徑查看各個plist(plist中規定了對應插件的應用范圍)文件的內容。查看是否存在該app的插件,有的話則在調用插件中的方法時修改其內部實現。
實現過程為:
所以,theos的Tweak插件不會對原來的可執行文件進行修改,僅僅是修改了內存中的代碼邏輯。
未脫殼的App是否支持tweak? 支持,因為tweak是在內存中實現的,並沒有修改.app包中的可執行文件 tweak效果是否永久性? 視情況而定,如果更新了App,新的版本沒有了這個類,或者不使用這個方法了,那么我們將無法hook到這個方法,tweak將會失效 未越獄的手機是否支持tweak? 不可以,未越獄的手機就沒有Cydia,也就沒有Substrate。所以就不能去查找插件了。 能不能對Swift/C函數進行tweak ? 原理是可以,但是方法跟oc不一樣 能不能對游戲項目進行tweak? 可以,但游戲很多是用C#、c++代碼編寫的,而且一般都有混淆的,所以很難。
tweak插件的卸載
- 直接從
/Library/MobileSubstrate/DynamicLibraries
文件夾刪除插件對應的Plist文件和dylib文件 - 這種方式卸載不是很干凈
方法二
- 通過Cydia卸載,在cydia的已安裝模塊去查找對應的插件 然后在插件詳情頁將其卸載。
- 推薦這種方式, 卸載比較徹底
注意點:
1、我們通過make package打包,默認是打debug包。如果想打release的包,需要將make package指令換成make package debug=0 debug一般用在開發測試,release一般用在正式環境,一般況下release包會比debug包小一點。
2、編寫插件並不一定只能在Tweak.x文件中,可以創建多文件來開發,這樣目錄清晰,而且文件的格式並不一定限於.x文件,也可以是.h .m .xm等等。只是注意要在makefile中加入參與編譯的文件,文件中引入其他文件時要寫全路徑。