前言
我们怎么动态分析与调试别人的应用,然后注入脚本以达到我们自己的目的?都听说过Hook,那么它的原理是什么,有哪几种Hook技术?所谓磨刀不误砍柴工,下面就这几个问题罗列总结一下Hook原理与动态调试,作为一个笔记仅供参考和学习。
1.0 Hook原理
HOOK,中文译为“挂钩”或“钩子”。在iOS逆向中是指改变程序运行流程的一种技术。通过hook可以让别人的程序执行自己所写的代码,我们重点要了解其原理,这样能够对恶意代码进行有效的防护。下面就几种hook技术分析一下。
1.1 MethodSwizzle
这个是我们比较熟悉的Hook方式,调用OC系统API,利用OC的Runtime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法。在OC中,SEL 和 IMP 之间的关系,就好像一本书的“目录”。SEL是方法编号,就“标题”一样。IMP是方法实现的真实地址,就像“页码”一样。他们是一一对应的关系,如下图所示我们通过改变这种对应关系达到hook的目的。
使用案例
下面以hook微信登录事件为例,当点击登录按钮时,在不影响原有登录逻辑下,获取输入的密码。
- 参考 逆向与砸壳文章,新建一个demo工程,将砸完壳的微信ipa包放进APP文件夹中,
先运行工程把demo安装进手机 - 用
appSign.sh脚本重签名微信并安装进手机,注意appSign.sh脚本中除了要删除macho中不能签名的Watch、plugin文件夹,最新版本的微信8.0.16还需要删除com.apple.WatchPlaceholder文件夹,如下
Xcode运行重签名的微信,lldb附加调试微信登录界面
- 点击登录按钮,可以看到登录的消息发送
id为WCAccountMainLoginViewController,SEL为onNext,找到了登录方法,我们还需要知道密码控件是哪一个UITextField,Class-dump微信头文件,静态分析WCAccountMainLoginViewController.h头文件如下没有找到和密码相关的UI组件,但是有一个属性名为
_mainPageView的文件,打开WCAccountLoginMainPageView.h文件寻找密码相关的UI
貌似找到了一个很密码相关的属性
_passwordTextItem,打开文件WCRedesignTextItem.h
文件
WCRedesignTextItem.h中存在一个属性_textFiled,打开WCUITextField.h文件
WCUITextField继承自UITextField,至此我们已经定位到了密码组件,keyvalue关系为:WCAccountMainLoginViewController->_mainPageView->_passwordTextItem->_textField。
onNext方法找到了,密码组件也找到了,就可以撸hook代码了,demo中新建一个wgy.framework,编写hook代码如下密码是获取到了,但是
[self newnext]调用原来的逻辑却奔溃了,断点在23行处,打印此时的id和sel,id为WCAccountMainLoginViewController,sel为newNext,但是WCAccountMainLoginViewController里并没有newNext方法,所以需要把这个方法添加进类里,修改为如下
+(void)load{
Method oldmethod=class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));
const char * typeencode= method_getTypeEncoding(oldmethod);
class_addMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(newNext),method_getImplementation(class_getInstanceMethod(self, @selector(newNext))), typeencode);
method_exchangeImplementations(oldmethod, class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(newNext)));
}
-(void)newNext{
UITextField* password=[[[self valueForKey:@"_mainPageView"] valueForKey:@"_passwordTextItem"] valueForKey:@"_textField"];
NSLog(@"%@",password.text);
[self newNext];
}
method_exchangeImplementations虽然可以交换方法,但是在调用原方法时稍微不注意就可能奔溃,一般可以使用class_replaceMethod代替
+(void)load{
Method oldmethod=class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));
const char * typeencode= method_getTypeEncoding(oldmethod);
oldimp= class_replaceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext), newNext, typeencode);
}
IMP (*oldimp)(id self,SEL _cmd);
void newNext(id self,SEL _cmd){
UITextField* password=[[[self valueForKey:@"_mainPageView"] valueForKey:@"_passwordTextItem"] valueForKey:@"_textField"];
NSLog(@"%@",password.text);
oldimp(self,_cmd);
}
分析:IMP=函数名=函数实现地址,这里用oldimp保存onNext函数地址以便调用原来的逻辑,oldimp是一个函数地址所以要用*指针指向,等价关系MP test=(*oldimp)(id self,SEL _cmd)。class_replaceMethod确实简洁了一点点,但是很多第三方hook框架使用setImp和getImp这两个api来实现hook,比如Monkey框架,我也感觉setImp和getImp使用更符合逻辑,修改如下
+(void)load{
oldimp=method_getImplementation(class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext)));
method_setImplementation(class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext)), newNext);
}
IMP (*oldimp)(id self,SEL _cmd);
void newNext(id self,SEL _cmd){
UITextField* password=[[[self valueForKey:@"_mainPageView"] valueForKey:@"_passwordTextItem"] valueForKey:@"_textField"];
NSLog(@"%@",password.text);
oldimp(self,_cmd);
}
- 逻辑代码写好后,最后需要把这个
FrameWork注入进Macho LoadCommands段中,这里面涉及dyld加载原理,使用yololib工具修改MachO,yololib下载下来拷贝进工程目录中,在appSign.sh最下面添加如下脚本,重新运行APP就注入成功了
yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/wgyFramework.framework/wgyFramework"
MethodSwizzle主要用来Hook OC方法,常使用如下api
method_exchangeImplementations:交换两个api,使用时要注意,避免调用原来方法奔溃的问题class_replaceMethod:替换原来方法的IMP,注意保存原函数指针地址。method_getImplementation、method_setImplementation:getImp保存原函数指针地址,setImp设置新函数指针地址,使用上更符合逻辑,推荐使用。
1.2 FishHook
FaceBook提供的一个工具,利用MachO文件加载原理,通过动态修改懒加载和非懒加载两个表的指针地址达到Hook C函数的目的。特别注意这里的C函数指的是系统自带的C函数,比如MethodSwizzle函数、NSLog函数等等系统共享缓存中的C函数,并不能Hook自己写的C函数。先看看怎么使用,然后再分析一下原理
使用案例
这里以Hook系统api”method_exchangeImplementations“C函数为例,同时也可以感受一下逆向的防护,我们把method_exchangeImplementations系统函数替换成我们自定义的一个函数,这样别人使用系统函数method_exchangeImplementations攻击你APP时就会失效。步骤如下
- 新建
fishhookdemo工程,主界面定义一个UIButton按钮,点击按钮触发事件onNext,弹出提示"欢迎您",目的是演示防护。 - demo工程中新建
protected.framework用于防护,注意防护代码一般使用Framework,因为FrameWork比MachO主工程先加载,而自己写的FrameWork比别人注入的FrameWork先加载,所以我们要尽可能的先执行我们的防护代码 - 下载fishHook源文件,拷贝
fishhook.c和fishhook.h文件进protected.framework,新建防护文件saveProtect.h和saveProtect.m - saveProtect防护代码编写如下所示,注意
注释中函数的使用
@interface saveProtect : NSObject
//原函数地址 暴露给自己使用
CF_EXPORT void (*old_exchange)(Method m1,Method m2);
@end
@implementation saveProtect
+ (void)load{
struct rebinding rebind;
rebind.name="method_exchangeImplementations";
rebind.replacement=new_exchangeImp;
rebind.replaced=(void*)&old_exchange;
struct rebinding rebs[]={rebind};
rebind_symbols(rebs, 1);
}
//原始函数指针,可以放在头文件暴露给自己工程使用
void (*old_exchange)(Method m1,Method m2);
//新函数
void new_exchangeImp(Method m1,Method m2){
NSLog(@"检测到Hook");
}
@end
- 打包
fishhookdemo.ipa - 再新建一个demo工程破解fishhookdemo.ipa,编写
MethodSwizzle代码HookonNext方法
+(void)load{
class_addMethod(objc_getClass("ViewController"), @selector(newNext), newNext, "v@:");
method_exchangeImplementations(class_getInstanceMethod(objc_getClass("ViewController"), @selector(onNext)), class_getInstanceMethod(objc_getClass("ViewController"), @selector(newNext)));
}
void newNext(id self,SEL _cmd){
NSLog(@"hook到了");
}
如果method_exchangeImplementations Hook成功,点击Button按钮应该打印“hook到了”,而事实是依然弹出“欢迎您”提示,说明method_exchangeImplementations函数已经失效,成功防护。
FishHook原理
ios有个特殊的位置,存放系统动态库,即动态共享缓存,FishHook利用PIC技术动态修改重绑定时指针地址的值。
- 由于
外部的函数调用,在编译时没法确定内存位置。 - 苹果采用
PIC技术(位置无关代码)。在macho文件Data段,建立两张表,懒加载和非懒加载表,存放执行外部函数的指针 首次调用懒加载函数,会去找桩代码执行,首次执行会执行binder函数进行绑定FishHook利用stringtable->symbols>indirect sybols->懒加载符号表之间的对应关系,通过重绑定修改指针的值
1.3 InlineHook
所谓inlineHook就是直接修改目标函数的头部代码,让它跳转到我们自定义的函数里执行我们的代码,从而达到Hook的目的,这种Hook技术一般用在静态语言的Hook上,比如自定义的C函数,或者像swift语言的C函数,这很好解决了FishHook不能Hook C函数的问题。这里推荐一个牛逼的框架Dobby,它可以像使用FishHook一样简单,话不多说直接上步骤。
编译Dobby
git clone https://github.com/jmpews/Dobby.git --depth=1,把Dobby源下载到本地,depth用于指定克隆深度,为1表示只克隆最近一次commit- 由于
Dobby是跨平台的,所以需要编译成Xcode工程。运行以下命令,创建一个build_for_ios_arm64文件夹放编译后的Xcode工程
cd Dobby && mkdir build_for_ios_arm64 && cd build_for_ios_arm64
cmake .. -G Xcode \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=cmake/ios.toolchain.cmake \
-DPLATFORM=OS64 \
-DARCHS=arm64 \
-DENABLE_BITCODE=0 \
-DENABLE_ARC=0 \
-DENABLE_VISIBILITY=1 \
-DDEPLOYMENT_TARGET=9.3 \
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
-DDynamicBinaryInstrument=ON -DNearBranchTrampoline=ON \
-DPlugin.FindSymbol=ON -DPlugin.HideLibrary=ON -DPlugin.ObjectiveC=ON
- 编译完成后
build_for_ios_arm64文件夹下Xcode工程如下,再编译xcode工程,生成DobbyX.framework
使用案例
- 新建
demo工程 - 拷贝
DobbyX.framework进demo工程,运行demo工程可能出现两种错误bitcode错误:要么在编译DobbyX.framework时设置支持bitcode,要么demo中bitcode=NOdyld: Library not loaded: @rpath/DobbyX.framework/DobbyX Reason: image not found错误:Framework首次拖入工程,Xcode不会帮我们拷贝进Macho,所以需要手动拷贝一下,如下所示
- 主界面添加测试函数
sum(),Dobby勾住sum()函数替换成new_sum(),点击屏幕调用sum()函数,代码如下
@implementation ViewController
int sum(int a,int b){
return a+b;
}
- (void)viewDidLoad {
[super viewDidLoad];
//参数1:需要hook的函数地址
//参数2:新函数地址
//参数3:保留原始函数的指针的地址
DobbyHook(sum, new_sum, (void*)&originsum);
}
//保存原始函数指针地址,以便后续调用
int (*originsum)(int a,int b);
int new_sum(int a,int b){
NSLog(@"原来的结果为:%i",originsum(a,b));
return a*b;
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"结果:%i",sum(10, 20));
}
点击屏幕控制台输出如下:
很显然已经成功Hook住了sum()函数,并且执行了我们自己的函数。C函数看汇编,看下Dobby怎么实现的,断住sum()函数,hook前和hook后汇编如下图
hook后只有前三行不同,而且没有拉伸栈空间,跳进br x17,看一下x17
x17就是新函数new_sum(),Dobby直接在原始函数sum()头部插入新函数,所以说inlineHook就是直接修改目标函数的头部代码。跟进new_sum()看汇编,由于在new_sum()里调用了原始函数sum(),所以进入了x8
跟进blr x8看下汇编
如果调用了原始函数就拉伸栈空间,br x17回原始函数执行。inlinehook可以hook自定义的C函数,那么能否Hook系统函数呢?继续Hook NSLog()试试,如下图所示
Dobby果然很强大,
系统外部函数也可以Hook,这样的话似乎就可以不用FishHook了,直接整Dobby就行了。
原理总结
inlineHook就是直接修改目标函数的头部代码。- 如果新函数中调用了原始函数,才会拉伸栈空间并且调用原始函数否则原始函数不执行。
inlinehook是在头部动态插入新函数,__text段是只读的。
地址替换函数
逆向中很难知道自定义函数名称的,毕竟release包是脱符号的,我们往往通过分析Macho拿到函数的偏移地址,然后再拿到Macho的首地址ASLR,偏移地址+ASLR即为函数真实地址。以sum()函数为例,这里简化下步骤,直接断住sum()函数lldb看一下sum()地址,然后对照Macho验证一下。
如图所示sum()函数偏移值为:
0x104119e18-0x1041114000=0x5e18,macho中看一下0x5e18偏移量
对比
lldb sum汇编和Macho中汇编,这应该是sum()函数,知道了sum()函数偏移量为0x5e18,那么做戏做全套,打包demo工程,像上面那样新建一个示例工程重签名demo工程,如下图所示
分析:
-
结果显示很成功的Hook住了sum()函数,特别注意通过代码获取的
ASLR是没有加上0x100000000这样一个偏移基地址pagezero的,所以这里要在sum()偏移地址0x5e18前需要加上0x100000000。 -
把
DobbyX.framework拖进主工程后,记得在wgyFramework中要链接DobbyX.framework,否则编译找不到这个动态库 -
同时也需要在
wgyFramework中设置Framework search path引用路径,否则导入头文件会报找不到头文件的错误。(Library Search Paths是这是.a的路径)
1.4 Monkey
这是一个为越狱和非越狱开发人员准备的工具,集成了四个模块Logos Tweak、CaptainHook Tweak、Command-line Tool、Monke4App,它是集重签名、hook、动态调试等等于一身的强大工具,也是逆向必玩的一个工具,工具虽然很强大,但也不意味着前面说的hook原理就没有用,工具毕竟是工具,知道其原理才能不掉队,比如Monkey hook的原理就是封装的getImp和setImp,下面看下如何使用Monkey。
- 安装thoes
//1.先安装最新theos recursive表示循环递归安装依赖库
sudo git clone --recursive https://github.com/theos/theos.git /opt/theos
//2.安装ldid签名工具,越狱插件签名工具,codesign是官方的签名工具,
brew install ldid
- Xcode安装插件
sudo /bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/AloneMonkey/MonkeyDev/master/bin/md-install)
- 安装成功后,
/opt目录下会有theos、MonkeyDev、frida-ios-dump文件夹,同时xcode新建工程会出现MonkeyApp选项 提醒:如果安装过程中出现错误,可以参考下面两篇文章解决
- 注意:编译工程如果出现# libstdc++ not found的错误,参考错误解决方法
- 修改
/opt/MonkeyDev/Tools/pack.sh文件,添加删除com.apple.WatchPlaceholder,否则最新的微信不能重签名
使用案例
还是以hook微信登录事件为例,当点击登录按钮时,在不影响原有登录逻辑下,获取输入的密码。Xcode新建demo工程选择MonkeyApp,把砸完壳的微信.ipa包拷贝进TargetApp文件夹
在Logos文件夹下编写Hook代码如下,运行程序就能很简单地Hook onNext方法。
Logos语法
上面的简单案例是通过Logos语法编写的Hook代码,所以有必要了解一下Logos语法,根据官网Logos语法分为如下三块,记忆是没用的,需要自己一般看文档一边使用,也就这么几个指令。
总结:
%hook,%end:勾住某个类%group,%end:分组,每一组都需要%ctor()函数构造,通过%init(组名称)进行初始化%log输出方法的详细信息(调用者、方法名、方法参数)%orig调用原始方法,可以传递参数、接收返回值%c类似getClass函数,获取一个类对象%new添加某个方法- 实际应用中经常使用
MSHookIvar获取类的属性,比如MSHookIvar<UITableView*>(self,"_tableView"),可以获取self类中_tableView属性 - 实际开发中经常使用
响应链条找到特定的类,比如[tableview.nextResponder.nextResponder isKindofClass:%c(WeiXinViewController)],通过tableview响应链条判断tableview是否属于类WeiXinViewController
2.0 动态调试
逆向是没有源码的,所以不能在源码中设置断点,其次调试别人上架的应用是脱符号的,不能直接给某个符号下断点,只能动态调试找到内存地址,然后给具体的内存地址下断点。所以就需要熟练掌握动态调试的方法,然后再配合静态分析就可以很轻松的玩转逆向。下面说下动态调试工具lldb和cycript以及Reveal。
2.1 LLDB
默认内置于Xcode中的动态调试工具。标准的 LLDB 提供了一组广泛的命令,旨在与老版本的 GDB 命令兼容。 除了使用标准配置外,还可以很容易地自定义 LLDB 以满足实际需要。
常用指令如下:
普通断点
设置断点:breakpoint set -n xxx,给xxx方法下断点,如果是某个类的某个方法,需要加双引号,比如“-[viewController touchbegin:]”。breakpoint set -r xxx,断住所有包含“xxx”的地方。breakpoint -a 地址,给地址下断点。(b -n xxx,是简写)- 查看所有断点:
breakpoint list - 删除断点:
breakpoint delete - 禁用/启用断点:
breakpoint disable/breakpoint enable - 继续执行:
c(continue简写) - 单步执行,将子函数单做整体一步执行:
n(next简写) - 单步运行,遇到子函数会进去:
s
函数调用栈类型
- 查看函数调用栈:
bt - 选择进入具体堆栈:
frame select 12,进去编号为12的堆栈,只是进入堆栈,数据不不会变 - 走完当前方法,返回上层
调用栈frame:finish 查看当前函数栈的id和方法参数:frame variable,这个还是很方便使用的。- 函数调用栈回滚:
up/down - 回滚函数栈:
thread return,直接返回不执行后面的代码,区别于up/down,它是会影响执行结果的
内存断点
- 监听内存值的变化,当发生变化时会进入断点:
watchpoint set variable p1->name,或者watchpoint set expression 内存地址 - 内存断点移除:
whatch delete,还有disable等等,与breakpoint相似 - 全局断点监听:
target stop-hook add -o “frame variable”,stop-hook断点的意思,只要断点来了就显示指令frame variable
其他
- 执行代码:
ppo表示调用对象的discraption方法,这个指令不多说,用的最多了。 - 查看指令:
help breakpoint - 查看镜像列表:
image list - 寄存器读写:
register read/write x0 - 读取内存值:
Memory read 或者 x
只记是没有用的,必须要参照着练习,更多指令请参考lldb官网。使用这些指令太繁琐,很多时候需要些好多指令才能定位到自己的视图,所以我们需要借助封装了lldb api好用的工具。
chisel
Facebook封装的lldb api,使用python调用的工具。首先下载安装chisel,具体安装步骤参照这个网址,配置好.lldbinit之后就可以使用了。
常用便捷指令
pviews:打印视图层级,pview -u,打印上一级视图层级pvc:打印当前控制器pactions 指针地址:打印按钮所在页面和它的点击事件方法presponder 指针地址:获取按钮的响应链条pclass 指针地址:打印控制器继承关系pmethods 指针地址:打印所有方法pinternals 指针地址:打印所有成员属性fvc -v 指针地址:定位属于哪个控制器fv UI名称:打印当前控制器有几个这样的view,比如 fv WCUITextFiledflicker 指针地址:定位到的控件会闪烁,这个很好用哦vs 指针地址:进入具体控件并且可以调试 ,q退出调试。这个对于寻找组件很方便。(w)move to superview (移动到父视图)(s)move to first subview(移动到第一个子视图)(a)move to previous sibling(同级往下移动)(d)move to next sibling(同级往上移动)(p)print the hierarchy(打印层次结构)
这里以定位微信登录界面中的登录按钮为例玩一下。首先pviews打印所有视图,随便找一个组件地址,flicker定位一下这个组件,定位到了会闪烁,然后vs组件地址进入组件中调试,通过w、s、a、d、p指令定位到登录按钮,如下图所示。
DerekSelander
这个lldb工具结合chisel使用的话更方便一点,所以一般同时安装这两个工具一起使用。首先把DerekSelander下载到本地,然后解压缩放到指定的目录下,比如我这边放在/opt目录下,编辑.lldbinit指定dslldy.py路径,如下图所示就搞定了。
常用便捷指令
search UIView:全局搜索视图methods 指针地址:打印所有方法(而且还有方法地址,可以给地址下断点)sbt:打印调用堆栈信息, 注意这个堆栈信息是恢复部分方法符号的,bt命令打印的话是没有符号的。
更多指令参考DerekSelander,这里结合chisel指令看一下主界面有哪些方法。
2.2 cycript
Cycript是由Cydia创始人Saurik推出的一款脚本语言,Cycript混合了OC、JavaScript语法的解释器,这意味着我们能够在一个命令中使用OC或者JavaScript,甚至两者并用。它能够挂钩正在运行的进程,能够在运行时修改很多东西。区别于lldb,cycript不会阻塞进程,所以动态调试应用很方便,这也玩逆向经常使用的工具,而lldb附加进程是阻塞的状态。安装步骤如下
- 官网: www.cycript.org/
- 下载后使用Cycript这个可执行文件
- 为了方便,我们可以放在
/opt/cycript_0.9.594(opt目录有可选的意思),同时在~/.bash_profile中配置环境变量(执行文件路径)
特别提醒:如果上面安装了Monkey,可以不用下载安装配置cycript了,因为MonkeyDev/bin中已经有这个可执行文件了,工具的使用当然是以方便为前提,所以强烈建议安装并且配置环境变量如下,这样控制台可以直接使用MonkeyDev/bin里面的工具,如class-dump、dump.py、cycript。
非越狱调试
cycript可以附加进程动态调试的前提是手机里必须得有cycript静态库,Monkey工程会自动把cycript静态库打包进APP里,所以只能动态调试这个APP而不能调试别的APP,Monkey默认注入进手机里的cycript的端口号是6666,Mac电脑上的cycript工具通过端口号连接APP里的cycript。
Mac连接手机端APP里cycript命令如下:
cycript -r 192.168.1.98:6666
- 此处
192.168.2.2是手机的ip地址,6666是手机上APP里cycript的端口号,可以把cycript命令封装成shell脚本,但是这里不推荐,毕竟一旦无线网变了ip就变了,所以还是封装成USB连接方便一点。 - 在越狱与砸壳文章中我们写了一个
usbConnect.sh脚本,封装了SSH通过USB链接手机。这里同样封装两个shell脚本,一个cyusbConnect.sh做USB端口映射,一个cyLogin.sh做连接APP端的cycript。这里比较苦恼不知道怎么把cyusbConnect.sh和usbConnect.sh这两个端口映射脚本合在一起,有知道的小伙伴一定要给我留言。
常用命令
- 强烈建议把
UIApp、UIApp.keyWindow.rootViewController等等常用命令封装成自定义的cy文件,然后导入到手机里使用,无论在越狱环境下还是非越狱环境下自定义cy文件还是很方便的,比如下面的自定义GY.cy。
//IIFE 匿名函数自执行表达式
(function(exports){
APPID = [NSBundle mainBundle].bundleIdentifier,
APPPATH = [NSBundle mainBundle].bundlePath,
//如果有变化,就用function去定义!!
GYRootVc = function(){
return UIApp.keyWindow.rootViewController;
};
GYKeyWindow = function(){
return UIApp.keyWindow;
};
GYGetFrontVcFromRootVc = function(rootVC){
var currentVC;
if([rootVC presentedViewController]){
rootVC = [rootVC presentedViewController];
}
if([rootVC isKindOfClass:[UITabBarController class]]){
currentVC = GYGetFrontVcFromRootVc(rootVC.selectedViewController);
}else if([rootVC isKindOfClass:[UINavigationController class]]){
currentVC = GYGetFrontVcFromRootVc(rootVC.visibleViewController);
}else{
currentVC = rootVC;
}
return currentVC;
};
//当前正在显示的控制器
GYFrontVc = function(){
return GYGetFrontVcFromRootVc(GYRootVc());
};
GYVcViews=function(vc){
if (![vc isKindOfClass:[UIViewController class]]) throw new Error(invalidParamStr);
return vc.view.recursiveDescription().toString();
}
//当前正在显示的UI层级
GYFrontViews = function(){
var currentVC=GYGetFrontVcFromRootVc(GYRootVc());
return GYVcViews(currentVC);
};
// 获取按钮绑定的所有TouchUpInside事件的方法名
GYTouchUpEvent = 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;
};
// 递归打印view的层级结构
GYSubviews = function(view) {
if (![view isKindOfClass:[UIView class]]) throw new Error(invalidParamStr);
return view.recursiveDescription().toString();
};
var _GYClass = function(className) {
if (!className) throw new Error(missingParamStr);
if (MJIsString(className)) {
return NSClassFromString(className);
}
if (!className) throw new Error(invalidParamStr);
// 对象或者类
return className.class();
};
// 打印所有的子类
GYSubclasses = function(className, reg) {
className = GYClass(className);
return [c for each (c in ObjectiveC.classes)
if (c != className
&& class_getSuperclass(c)
&& [c isSubclassOfClass:className]
&& (!reg || reg.test(c)))
];
};
var _GYGetMethods = function(className, reg, clazz) {
className = GYClass(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 _GYMethods = function(className, reg, clazz) {
return GYGetMethods(className, reg, clazz)[0];
};
var _GYMethodNames = function(className, reg, clazz) {
return GYGetMethods(className, reg, clazz)[1];
};
//打印所有的对象方法
GYInstanceMethods = function(className, reg) {
return _GYMethods(className, reg);
};
// 打印所有的对象方法名字
GYInstanceMethodNames = function(className, reg) {
return _GYMethodNames(className, reg);
};
// 打印所有的类方法
GYClassMethods = function(className, reg) {
return _GYMethods(className, reg, true);
};
// 打印所有的类方法名字
GYClassMethodNames = function(className, reg) {
return _GYMethodNames(className, reg, true);
};
// 打印所有的成员变量
GYIvars = 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;
};
// 打印所有的成员变量名字
GYIvarNames = 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);
Xcode打开Monkey项目,导入GY.cy文件如下,Xcode重新运行项目,GY.cy就被打包进APP中了,使用自定义cy文件时,需要@import
Monkey工程会自动给我们下载md.cy和MS.cy文件并打包进APP,md.cy提供一些便捷指令如pviews()、pvcs()、rp()、pactions(),MS.cy提供动态插入代码功能,注意这两个cy文件不需要@import导入。但常常因为网络原因这两个文件下载不下来,所以需要更换git源地址,把这两个url地址替换为raw.fastgit.org/AloneMonkey… 和 raw.fastgit.org/AloneMonkey…
演示如下:
越狱调试
cycript在手机越狱的情况下调试有个好处,首先不需要用Monkey运行项目,然后可以附加手机里的所有APP,前提是在越狱商店Cydia里下载cycript插件,插件下载好之后,SSH连接手机就可以cycript附加进程了。
cycript插件文件夹如上图所示,虽然可以附加手机里的应用,但是一些快捷指令比如APPID、pvcs()并不能用,所以需要导入我们自定义的GY.cy,和git上下载下来的md.cy,scp拷贝cy文件进手机/usr/lib/cycript0.9/com/目录下,这里在com目录下新建一个gy文件夹专门放自己的cy文件,如下所示
使用的时候
@import导入一下就可以愉快地使用快捷指令了,附加正版的微信演示如下
分析:
cycript -p 进程id/appid,附加进程。导入自定义的GY.cy是可以使用的,但是导入md.cy却不能使用,这个还没有搞清楚,有知道的小伙伴给我留言。
2.3 Reveal
Reveal是一款可以动态调试APP UI的工具,相比于xcode自带的View Debugger,它不会阻塞APP进程而且可以动态调试UI显示效果。这里就越狱环境下如何使用Reveal简单介绍下。
- 越狱手机在
cydia商店里安装Reveal Loader SSH连接手机,cd Library目录下新建mkdir RHRevealLoader文件夹。Mac电脑安装Reveal工具,这里推荐一个破解工具下载平台- Ma电脑
拷贝RevealServer可执行程序进手机端RHRevealLoader文件夹。打开Reveal程序,Help-->Show Reveal Library in Finder-->iOS Library-->RevealServer.framework-->RevealServer。scp拷贝RevealServer进手机,并且重命名为libReveal.dylib
重启手机- 手机进入
设置-->Reveal-->打开想要调试的APP - Mac重新打开Reveal工具,此时就可以动态调试UI了,当然你也必须先SSH连接上手机。