iOS 逆向之 Cycript 高级玩法(非越狱) & .cy文件的封装

3,765 阅读8分钟

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 命令:

如上图所示,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):

打开 MonkeyDevDemo,只需将你要调试的 ipa/app (脱壳还是必要的) 丢到新建项目的这个目录下:

运行项目,就可以将应用直接运行到你的真机上了:

上图中,红框标注出的 CYListenServer(6666); 正是我们前面提到的,用 Cycript 实现动态调试应用的前提:一个可远程连接的端口号--6666,在控制台中同样可以找到打印日志:

我相信你一定注意到了日志中的这一行:

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();

赶快来试试手:找到淘宝首页底部 “淘” 按钮并将其隐藏掉:

是不是突然想拿微信发个 ¥0.01 的红包,然后用新学的这招操作一波:

emmmmmm...友情提示,登录破解的微信应用,大概率会被微信封号的,别问我怎么知道的 (>_<)

  言归正传,上面列出的几个命令,基本可以满足你快速摸清一个 app 各个复杂页面的结构,同时也可以精准的定位并修改目标视图的UI。有的同学如果没接触过 Cycript ,建议先看一下 官方文档,熟悉下支持的语法和数据结构,多找几个小case有目的的练习,很快就能上手玩了,这对于想通过学习大厂优秀 app 的设计与实现思路的同学来说,是个不可错过的好途径。

Tips:  
1. 进入了 cy# JavaScript 控制台之后,相当于处在一个进程中,因此定义的变量在进程生命周期中一直可用。
2. #0x10c144d00 :#+对象地址=拿到该对象

高级玩法

  与 加强版 LLDB —— 修改 .lldbinit 文件 & 插件安装 类似,Cycript 支持加载自定义脚本,这极大的提高了它的调试效率,在前面简单使用中列出的可用快捷命令可不是 Cycript 本来就有的,而是 MonkeyDev 的作者加载了自己写的网络脚本才支持的:

我们可以打开该地址查看对应的.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 除了在逆向工程中, 在正向开发和日常学习中,依然非常好用。

愿你有所收获~