iOS 逆向之 Cycript 高階玩法(非越獄) & .cy檔案的封裝

小可長江發表於2019-04-14

What is Cycript

摘自官方文件
  Cycript is a hybrid of ECMAScript some-6, Objective-C++, and Java. It is implemented as a Cycript-to-JavaScript compiler and uses (unmodified) JavaScriptCore for its virtual machine. It concentrates on providing "fluent FFI" with other languages by adopting aspects of their syntax and semantics as opposed to treating the other language as a second-class citizen.

  The primary users of Cycript are currently people who do reverse engineering work on iOS. Cycript features a highly interactive console that features live syntax highlighting and grammar-assisted tab completion, and can even be injected into a running process (similar to a debugger) using Cydia Substrate. This makes it an ideal tool for "spelunking".

  However, Cycript was specifically designed as a programming environment and maintains very little (if any) "baggage" for this use case. Many modules from node.js can be loaded into Cycript, while it also has direct access to libraries written for Objective-C and Java. It thereby works extremely well as a scripting language.

簡言之
  Cycript 是由 Cydia 創始人 Saurik 推出的一款指令碼語言,它混合了 ECMAScript 6.0(簡稱ES6,是JavaScript 語言的下一代標準)、Objective-C ++ 和 Java 的語法直譯器。這意味著我們能夠在一個命令中使用 OC 或者 JavaScript,甚至兩者並用。Cycript 目前的主要用途是在 iOS 上進行逆向工作,使用 Cydia Substrate 可以注入正在執行的程式(類似於偵錯程式),這使它成為“探險”的理想工具。

How to use

1. 安裝

在這裡下載 SDK到本地,為了方便每次直接可以使用,建議將可執行檔案 cycript 的路徑配置到環境變數中(在 .bash_profile/.zshrc [取決於你用哪個終端] 中 export 一下),開啟終端,執行 cycript 命令:

iOS 逆向之 Cycript 高階玩法(非越獄) & .cy檔案的封裝

如上圖所示,cy# 提示符表示進入了 JavaScript 控制檯。你鍵入的所有內容都將由 JavaScriptCore 執行,這是 Apple 對 Safari 使用的 JavaScript 語言的實現。且在你鍵入時,你的命令將使用 Cycript 的詞法分析器進行語法突出顯,如果出現語法錯誤,則會出現提示。你可以使用 ctrl+C 取消鍵入,或 ctrl+D 退出該環境。

2. 使用

預熱準備

  關於 Cycript 的用法,這一篇只圍繞 iOS 逆向工程來展開講述,這也是 Cycript 目前用的最廣的領域。對比上一篇提到的 LLDB ,Cycript 的亮點在於它可以動態注入,在執行時可以隨時獲取、修改程式中物件的值。而 LLDB 不管是在正向開發還是逆向工程中,它只能進行斷點靜態除錯分析,效率相比 Cycript 有明顯的不足。

  用 Cycript 實現動態除錯應用的前提,是你的應用為其開好了一個可連線的埠,鑑於越獄機並不是人人都有,此篇我主要為大家介紹非越獄環境下如何使用 Cycript 進行除錯,讓大家都有實操的條件。在開始使用 Cycript 之前,我們還需要準備另一個工具。

  在過去兩個多月的系列文章中,我將 iOS 的應用簽名原理、自動重簽名指令碼以及程式碼注入等知識串講了一遍,其實這些工作全都有工具幫我們整合好了,相信你也猜到了,沒錯,這個工具就是 MonkeyDev ---- 原有 iOSOpenDev 的升級,非越獄外掛開發整合神器!關於 MonkeyDev 的安裝 這裡就不展開贅述了,安裝成功後,新建一個 MonkeyApp 專案 (MonkeyDevDemo):

iOS 逆向之 Cycript 高階玩法(非越獄) & .cy檔案的封裝

開啟 MonkeyDevDemo,只需將你要除錯的 ipa/app (脫殼還是必要的) 丟到新建專案的這個目錄下:

iOS 逆向之 Cycript 高階玩法(非越獄) & .cy檔案的封裝

執行專案,就可以將應用直接執行到你的真機上了:

iOS 逆向之 Cycript 高階玩法(非越獄) & .cy檔案的封裝

上圖中,紅框標註出的 CYListenServer(6666); 正是我們前面提到的,用 Cycript 實現動態除錯應用的前提:一個可遠端連線的埠號--6666,在控制檯中同樣可以找到列印日誌:

iOS 逆向之 Cycript 高階玩法(非越獄) & .cy檔案的封裝

我相信你一定注意到了日誌中的這一行:

Download cycript(https://cydia.saurik.com/api/latest/3) then run: ./cycript -r 192.168.199.236:6666

  沒錯,它就是在告訴你,server 埠繫結成功,終端執行 ./cycript -r 192.168.199.236:6666 就能連線到執行中的應用了。192.168.199.236 是我當前手機的 ip 地址。
  其實現原理,簡單來講,就是 hook 了 AppDelegate 裡的 application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *) 方法,在該方法裡開啟 Cycript 並繫結到6666埠。

簡單使用

在上一篇 LLDB 中推薦的外掛 chisel 裡,很多好用的命令在 MonkeyDev 中也都做了支援 :

NSString* pvc(void);

NSString* pviews(void);

NSString* pactions(vm_address_t address);

NSString* pblock(vm_address_t address);

NSString* methods(const char * classname);

NSString* ivars(vm_address_t address);

NSString* choose(const char* classname);

NSString* vmmap();
複製程式碼

iOS 逆向之 Cycript 高階玩法(非越獄) & .cy檔案的封裝

趕快來試試手:找到淘寶首頁底部 “淘” 按鈕並將其隱藏掉:

iOS 逆向之 Cycript 高階玩法(非越獄) & .cy檔案的封裝

iOS 逆向之 Cycript 高階玩法(非越獄) & .cy檔案的封裝

是不是突然想拿微信發個 ¥0.01 的紅包,然後用新學的這招操作一波:

iOS 逆向之 Cycript 高階玩法(非越獄) & .cy檔案的封裝
emmmmmm...友情提示,登入破解的微信應用,大概率會被微信封號的,別問我怎麼知道的 (>_<)

  言歸正傳,上面列出的幾個命令,基本可以滿足你快速摸清一個 app 各個複雜頁面的結構,同時也可以精準的定位並修改目標檢視的UI。有的同學如果沒接觸過 Cycript ,建議先看一下 官方文件,熟悉下支援的語法和資料結構,多找幾個小case有目的的練習,很快就能上手玩了,這對於想通過學習大廠優秀 app 的設計與實現思路的同學來說,是個不可錯過的好途徑。

Tips:  
1. 進入了 cy# JavaScript 控制檯之後,相當於處在一個程式中,因此定義的變數在程式生命週期中一直可用。
2. #0x10c144d00 :#+物件地址=拿到該物件
複製程式碼

高階玩法

  與 加強版 LLDB —— 修改 .lldbinit 檔案 & 外掛安裝 類似,Cycript 支援載入自定義指令碼,這極大的提高了它的除錯效率,在前面簡單使用中列出的可用快捷命令可不是 Cycript 本來就有的,而是 MonkeyDev 的作者載入了自己寫的網路指令碼才支援的:

iOS 逆向之 Cycript 高階玩法(非越獄) & .cy檔案的封裝
我們可以開啟該地址檢視對應的.cy檔案原始碼

然後呢?然後我們也可以自己搞一份自己除錯時常用的指令碼,這裡推薦一個小碼哥寫的 mjcript

載入.cy指令碼的方式也為你準備好了:通過MonkeyDev載入網路或者自己的cy指令碼

來感受一波自定義指令碼的效率:

(function(exports) {
	var invalidParamStr = 'Invalid parameter';
	var missingParamStr = 'Missing parameter';

	// app id
	CJAppId = [NSBundle mainBundle].bundleIdentifier;

	// mainBundlePath
	CJAppPath = [NSBundle mainBundle].bundlePath;

	// document path
	CJDocPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];

	// caches path
	CJCachesPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];

	// 載入系統動態庫
	CJLoadFramework = function(name) {
		var head = "/System/Library/";
		var foot = "Frameworks/" + name + ".framework";
		var bundle = [NSBundle bundleWithPath:head + foot] || [NSBundle bundleWithPath:head + "Private" + foot];
  		[bundle load];
  		return bundle;
	};

	// keyWindow
	CJKeyWin = function() {
		return UIApp.keyWindow;
	};

	// 根控制器
	CJRootVc =  function() {
		return UIApp.keyWindow.rootViewController;
	};

	// 找到顯示在最前面的控制器
	var _CJFrontVc = function(vc) {
		if (vc.presentedViewController) {
        	return _CJFrontVc(vc.presentedViewController);
	    }else if ([vc isKindOfClass:[UITabBarController class]]) {
	        return _CJFrontVc(vc.selectedViewController);
	    } else if ([vc isKindOfClass:[UINavigationController class]]) {
	        return _CJFrontVc(vc.visibleViewController);
	    } else {
	    	var count = vc.childViewControllers.count;
    		for (var i = count - 1; i >= 0; i--) {
    			var childVc = vc.childViewControllers[i];
    			if (childVc && childVc.view.window) {
    				vc = _CJFrontVc(childVc);
    				break;
    			}
    		}
	        return vc;
    	}
	};

	CJFrontVc = function() {
		return _CJFrontVc(UIApp.keyWindow.rootViewController);
	};

	// 遞迴列印UIViewController view的層級結構
	CJVcSubviews = function(vc) {
		if (![vc isKindOfClass:[UIViewController class]]) throw new Error(invalidParamStr);
		return vc.view.recursiveDescription().toString(); 
	};

	// 遞迴列印最上層UIViewController view的層級結構
	CJFrontVcSubViews = function() {
		return CJVcSubviews(_CJFrontVc(UIApp.keyWindow.rootViewController));
	};

	// 獲取按鈕繫結的所有TouchUpInside事件的方法名
	CJBtnTouchUpEvent = function(btn) {
		var events = [];
		var allTargets = btn.allTargets().allObjects()
		var count = allTargets.count;
    	for (var i = count - 1; i >= 0; i--) { 
    		if (btn != allTargets[i]) {
    			var e = [btn actionsForTarget:allTargets[i] forControlEvent:UIControlEventTouchUpInside];
    			events.push(e);
    		}
    	}
	   return events;
	};

	// CG函式
	CJPointMake = function(x, y) {
		return {0 : x, 1 : y}; 
	};

	CJSizeMake = function(w, h) {
		return {0 : w, 1 : h}; 
	};

	CJRectMake = function(x, y, w, h) {
		return {0 : CJPointMake(x, y), 1 : CJSizeMake(w, h)};
	};

	// 遞迴列印controller的層級結構
	CJChildVcs = function(vc) {
		if (![vc isKindOfClass:[UIViewController class]]) throw new Error(invalidParamStr);
		return [vc _printHierarchy].toString();
	};

	// 遞迴列印view的層級結構
	CJSubviews = function(view) {
		if (![view isKindOfClass:[UIView class]]) throw new Error(invalidParamStr);
		return view.recursiveDescription().toString(); 
	};

	// 判斷是否為字串 "str" @"str"
	CJIsString = function(str) {
		return typeof str == 'string' || str instanceof String;
	};

	// 判斷是否為陣列 []、@[]
	CJIsArray = function(arr) {
		return arr instanceof Array;
	};

	// 判斷num是否為數字
	CJIsNumber = function(num) {
		return typeof num == 'number' || num instanceof Number;
	};

	var _CJClass = function(className) {
		if (!className) throw new Error(missingParamStr);
		if (CJIsString(className)) {
			return NSClassFromString(className);
		} 
		if (!className) throw new Error(invalidParamStr);
		// 物件或者類
		return className.class();
	};

	// 列印所有的子類
	CJSubclasses = function(className, reg) {
		className = _CJClass(className);

		return [c for each (c in ObjectiveC.classes) 
		if (c != className 
			&& class_getSuperclass(c) 
			&& [c isSubclassOfClass:className] 
			&& (!reg || reg.test(c)))
			];
	};

	// 列印所有的方法
	var _CJGetMethods = function(className, reg, clazz) {
		className = _CJClass(className);

		var count = new new Type('I');
		var classObj = clazz ? className.constructor : className;
		var methodList = class_copyMethodList(classObj, count);
		var methodsArray = [];
		var methodNamesArray = [];
		for(var i = 0; i < *count; i++) {
			var method = methodList[i];
			var selector = method_getName(method);
			var name = sel_getName(selector);
			if (reg && !reg.test(name)) continue;
			methodsArray.push({
				selector : selector, 
				type : method_getTypeEncoding(method)
			});
			methodNamesArray.push(name);
		}
		free(methodList);
		return [methodsArray, methodNamesArray];
	};

	var _CJMethods = function(className, reg, clazz) {
		return _CJGetMethods(className, reg, clazz)[0];
	};

	// 列印所有的方法名字
	var _CJMethodNames = function(className, reg, clazz) {
		return _CJGetMethods(className, reg, clazz)[1];
	};

	// 列印所有的物件方法
	CJInstanceMethods = function(className, reg) {
		return _CJMethods(className, reg);
	};

	// 列印所有的物件方法名字
	CJInstanceMethodNames = function(className, reg) {
		return _CJMethodNames(className, reg);
	};

	// 列印所有的類方法
	CJClassMethods = function(className, reg) {
		return _CJMethods(className, reg, true);
	};

	// 列印所有的類方法名字
	CJClassMethodNames = function(className, reg) {
		return _CJMethodNames(className, reg, true);
	};

	// 列印所有的成員變數
	CJIvars = function(obj, reg){
		if (!obj) throw new Error(missingParamStr);
		var x = {}; 
		for(var i in *obj) { 
			try { 
				var value = (*obj)[i];
				if (reg && !reg.test(i) && !reg.test(value)) continue;
				x[i] = value; 
			} catch(e){} 
		} 
		return x; 
	};

	// 列印所有的成員變數名字
	CJIvarNames = function(obj, reg) {
		if (!obj) throw new Error(missingParamStr);
		var array = [];
		for(var name in *obj) { 
			if (reg && !reg.test(name)) continue;
			array.push(name);
		}
		return array;
	};
})(exports);
複製程式碼

只要你想,只要你能,更多姿勢,等你解鎖。

summary

  無使用場景的學習多半都是在浪費時間,不經常使用的知識也無法產生價值。Cycript 也不例外,如果僅僅是出於好奇,花了兩個小時玩了一下下,然後從此別過,其實意義真的不大。有些同學覺得廣泛涉獵,在面試的時候可以誇誇其談,能增加一點“大佬”感,我個人是不認同的,稍微深入一點的問題你就說不上來或者乾脆不懂裝懂反而會適得其反。所以個人還是建議,既然學了一個東西,就盡力學的深入一點,並在工作中不斷思考,如何利用已學知識去提高效率。Cycript 除了在逆向工程中, 在正向開發和日常學習中,依然非常好用。

願你有所收穫~

相關文章