ios-Hook原理与动态调试

前言

我们怎么动态分析与调试别人的应用,然后注入脚本以达到我们自己的目的?都听说过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的目的。 image.png

使用案例

下面以hook微信登录事件为例,当点击登录按钮时,在不影响原有登录逻辑下,获取输入的密码

  • 参考 逆向与砸壳文章,新建一个demo工程,将砸完壳的微信ipa包放进APP文件夹中,先运行工程把demo安装进手机
  • appSign.sh脚本重签名微信并安装进手机,注意appSign.sh脚本中除了要删除macho中不能签名的Watch、plugin文件夹,最新版本的微信8.0.16还需要删除com.apple.WatchPlaceholder文件夹,如下

image.png

  • Xcode运行重签名的微信,lldb附加调试微信登录界面

image.png

  • 点击登录按钮,可以看到登录的消息发送idWCAccountMainLoginViewControllerSELonNext,找到了登录方法,我们还需要知道密码控件是哪一个UITextField,Class-dump微信头文件,静态分析WCAccountMainLoginViewController.h头文件如下

image.png 没有找到和密码相关的UI组件,但是有一个属性名为_mainPageView的文件,打开WCAccountLoginMainPageView.h文件寻找密码相关的UI

image.png 貌似找到了一个很密码相关的属性_passwordTextItem,打开文件WCRedesignTextItem.h

image.png 文件WCRedesignTextItem.h中存在一个属性_textFiled,打开WCUITextField.h文件 image.png

WCUITextField继承自UITextField,至此我们已经定位到了密码组件,keyvalue关系为:WCAccountMainLoginViewController->_mainPageView->_passwordTextItem->_textField

  • onNext方法找到了,密码组件也找到了,就可以撸hook代码了,demo中新建一个wgy.framework,编写hook代码如下

image.png 密码是获取到了,但是[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框架使用setImpgetImp这两个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工具修改MachOyololib下载下来拷贝进工程目录中,在appSign.sh最下面添加如下脚本,重新运行APP就注入成功了
yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/wgyFramework.framework/wgyFramework"
复制代码

MethodSwizzle主要用来Hook OC方法,常使用如下api

  • method_exchangeImplementations:交换两个api,使用时要注意,避免调用原来方法奔溃的问题
  • class_replaceMethod:替换原来方法的IMP,注意保存原函数指针地址
  • method_getImplementationmethod_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时就会失效。步骤如下

  1. 新建fishhookdemo工程,主界面定义一个UIButton按钮,点击按钮触发事件onNext,弹出提示"欢迎您",目的是演示防护。
  2. demo工程中新建protected.framework用于防护,注意防护代码一般使用Framework,因为FrameWork比MachO主工程先加载,而自己写的FrameWork比别人注入的FrameWork先加载,所以我们要尽可能的先执行我们的防护代码
  3. 下载fishHook源文件,拷贝fishhook.c和fishhook.h文件进protected.framework,新建防护文件saveProtect.h和saveProtect.m
  4. 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
复制代码
  1. 打包fishhookdemo.ipa
  2. 再新建一个demo工程破解fishhookdemo.ipa,编写MethodSwizzle代码Hook onNext方法
+(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

  1. git clone https://github.com/jmpews/Dobby.git --depth=1,把Dobby源下载到本地,depth用于指定克隆深度,为1表示只克隆最近一次commit
  2. 由于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
复制代码
  1. 编译完成后build_for_ios_arm64文件夹下Xcode工程如下,再编译xcode工程,生成DobbyX.framework

image.png

image.png

使用案例

  1. 新建demo工程
  2. 拷贝DobbyX.framework进demo工程,运行demo工程可能出现两种错误
    • bitcode错误:要么在编译DobbyX.framework时设置支持bitcode,要么demo中bitcode=NO
    • dyld: Library not loaded: @rpath/DobbyX.framework/DobbyX Reason: image not found错误:Framework首次拖入工程,Xcode不会帮我们拷贝进Macho,所以需要手动拷贝一下,如下所示

image.png 3. 主界面添加测试函数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));
}
复制代码

点击屏幕控制台输出如下:

image.png

很显然已经成功Hook住了sum()函数,并且执行了我们自己的函数。C函数看汇编,看下Dobby怎么实现的,断住sum()函数,hook前和hook后汇编如下图

image.png

hook后只有前三行不同,而且没有拉伸栈空间,跳进br x17,看一下x17

image.png

x17就是新函数new_sum()Dobby直接在原始函数sum()头部插入新函数,所以说inlineHook就是直接修改目标函数的头部代码。跟进new_sum()看汇编,由于在new_sum()里调用了原始函数sum(),所以进入了x8

image.png

跟进blr x8看下汇编 image.png

如果调用了原始函数就拉伸栈空间,br x17回原始函数执行。inlinehook可以hook自定义的C函数,那么能否Hook系统函数呢?继续Hook NSLog()试试,如下图所示

image.png Dobby果然很强大,系统外部函数也可以Hook,这样的话似乎就可以不用FishHook了,直接整Dobby就行了。

原理总结

  • inlineHook就是直接修改目标函数的头部代码
  • 如果新函数中调用了原始函数,才会拉伸栈空间并且调用原始函数否则原始函数不执行。
  • inlinehook是在头部动态插入新函数,__text段是只读的。

地址替换函数

逆向中很难知道自定义函数名称的,毕竟release包是脱符号的,我们往往通过分析Macho拿到函数的偏移地址,然后再拿到Macho的首地址ASLR偏移地址+ASLR即为函数真实地址。以sum()函数为例,这里简化下步骤,直接断住sum()函数lldb看一下sum()地址,然后对照Macho验证一下。

image.png 如图所示sum()函数偏移值为:0x104119e18-0x1041114000=0x5e18macho中看一下0x5e18偏移量 image.png 对比lldb sum汇编和Macho中汇编,这应该是sum()函数,知道了sum()函数偏移量为0x5e18,那么做戏做全套,打包demo工程,像上面那样新建一个示例工程重签名demo工程,如下图所示

image.png 分析:

  • 结果显示很成功的Hook住了sum()函数,特别注意通过代码获取的ASLR是没有加上0x100000000这样一个偏移基地址pagezero的,所以这里要在sum()偏移地址0x5e18前需要加上0x100000000

  • DobbyX.framework拖进主工程后,记得在wgyFramework中要链接DobbyX.framework,否则编译找不到这个动态库

image.png

  • 同时也需要在wgyFramework中设置Framework search path引用路径,否则导入头文件会报找不到头文件的错误。(Library Search Paths是这是.a的路径)

image.png

1.4 Monkey

这是一个为越狱和非越狱开发人员准备的工具,集成了四个模块Logos Tweak、CaptainHook Tweak、Command-line Tool、Monke4App,它是集重签名、hook、动态调试等等于一身的强大工具,也是逆向必玩的一个工具,工具虽然很强大,但也不意味着前面说的hook原理就没有用,工具毕竟是工具,知道其原理才能不掉队,比如Monkey hook的原理就是封装的getImpsetImp,下面看下如何使用Monkey

//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选项

image.png

- 注意:编译工程如果出现# libstdc++ not found的错误,参考错误解决方法

  • 修改/opt/MonkeyDev/Tools/pack.sh文件,添加删除com.apple.WatchPlaceholder,否则最新的微信不能重签名

image.png

使用案例

还是以hook微信登录事件为例,当点击登录按钮时,在不影响原有登录逻辑下,获取输入的密码。Xcode新建demo工程选择MonkeyApp,把砸完壳的微信.ipa包拷贝进TargetApp文件夹 image.png

Logos文件夹下编写Hook代码如下,运行程序就能很简单地Hook onNext方法。 image.png

Logos语法

上面的简单案例是通过Logos语法编写的Hook代码,所以有必要了解一下Logos语法,根据官网Logos语法分为如下三块,记忆是没用的,需要自己一般看文档一边使用,也就这么几个指令。

image.png 总结:

  • %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 动态调试

逆向是没有源码的,所以不能在源码中设置断点,其次调试别人上架的应用是脱符号的,不能直接给某个符号下断点,只能动态调试找到内存地址,然后给具体的内存地址下断点。所以就需要熟练掌握动态调试的方法,然后再配合静态分析就可以很轻松的玩转逆向。下面说下动态调试工具lldbcycript以及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的堆栈,只是进入堆栈,数据不不会变
  • 走完当前方法,返回上层调用栈framefinish
  • 查看当前函数栈的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

其他

  • 执行代码:p po表示调用对象的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 WCUITextFiled
  • flicker 指针地址:定位到的控件会闪烁,这个很好用哦
  • 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指令定位到登录按钮,如下图所示。

image.png

DerekSelander

这个lldb工具结合chisel使用的话更方便一点,所以一般同时安装这两个工具一起使用。首先把DerekSelander下载到本地,然后解压缩放到指定的目录下,比如我这边放在/opt目录下,编辑.lldbinit指定dslldy.py路径,如下图所示就搞定了。

image.png

常用便捷指令

  • search UIView:全局搜索视图
  • methods 指针地址:打印所有方法(而且还有方法地址,可以给地址下断点)
  • sbt:打印调用堆栈信息, 注意这个堆栈信息是恢复部分方法符号的,bt命令打印的话是没有符号的。

更多指令参考DerekSelander,这里结合chisel指令看一下主界面有哪些方法。

image.png

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

image.png

非越狱调试

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这两个端口映射脚本合在一起,有知道的小伙伴一定要给我留言。

image.png

常用命令

  • 强烈建议把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

image.png

  • Monkey工程会自动给我们下载md.cyMS.cy文件并打包进APP,md.cy提供一些便捷指令如pviews()、pvcs()、rp()、pactions(),MS.cy提供动态插入代码功能,注意这两个cy文件不需要@import导入。

image.png 但常常因为网络原因这两个文件下载不下来,所以需要更换git源地址,把这两个url地址替换为raw.fastgit.org/AloneMonkey…raw.fastgit.org/AloneMonkey…

演示如下: image.png

越狱调试

cycript在手机越狱的情况下调试有个好处,首先不需要用Monkey运行项目,然后可以附加手机里的所有APP,前提是在越狱商店Cydia里下载cycript插件,插件下载好之后,SSH连接手机就可以cycript附加进程了。

image.png

cycript插件文件夹如上图所示,虽然可以附加手机里的应用,但是一些快捷指令比如APPID、pvcs()并不能用,所以需要导入我们自定义的GY.cy,和git上下载下来的md.cy,scp拷贝cy文件进手机/usr/lib/cycript0.9/com/目录下,这里在com目录下新建一个gy文件夹专门放自己的cy文件,如下所示

image.png 使用的时候@import导入一下就可以愉快地使用快捷指令了,附加正版的微信演示如下

image.png 分析: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

image.png

  • 重启手机
  • 手机进入设置-->Reveal-->打开想要调试的APP
  • Mac重新打开Reveal工具,此时就可以动态调试UI了,当然你也必须先SSH连接上手机。

image.png

猜你喜欢

转载自juejin.im/post/7041128375322148900