iOS攻防(六):使用Cycript一窥运行程序的神秘面纱(入门篇)


从那个夜晚我懂得了隐藏温暖——在凛冽的寒风中,身体中那点温暖节俭地用于此后多年的爱情和生活。我的亲人说我是个很冷的人,不是的,我把仅有的温暖全给了你们。

——刘亮程《寒风吹彻》

简介

Cycript allows developers to explore and modify running applications on either iOS or Mac OS X using a hybrid of Objective-C++ and JavaScript syntax through an interactive console that features syntax highlighting and tab completion.
(It also runs standalone on Android and Linux and provides access to Java, but without injection.)

Cycript安装

  • 安装Cycript有两种方式(手机需越狱):
  • 方式一:直接在Cydia搜索Cycript安装,简单方便。
  • 方式二:通过Linux包管理器安装:
    • 首先在手机上Cydia搜索安装 APT 0.6 Transitional包管理工具。
    • 使用Terminal(如果手机有安装)或者电脑SSH连接手机,通过包管理器安装apt-get install cycript
  • 终端输入cycript,出现 cy#提示符,说明已经成功启动了Cycript(需要SSH连接手机)。
  • 如果想要退出Cycript和断开远程登录:
    • Control + D快捷键可以退出Cycript
    • 终端输入logout可以断开手机远程登录

Cycript基本用法

测试Cycript注入头条新闻

  • 开始使用Cycript,注入一个正在运行(需要再前台)的程序。这里我们以今日头条为例。
  • 首先使用电脑终端通过SSH连接到手机,不知道如何使用SSH的请参考之前的文章,这里不再赘述。
  • 获取今日头条app的进程ID,建议清空后台应用,只保留需要hook的App在前台即可。
    • News即为今日头条进程名称,如果想知道某个app在运行时候的进程名称,可以下载对应的应用安装包,解压缩找到Payload目录下的xxx.app,其中xxx包名即是进程名称,如微信是WeChat,网易云音乐是NeteaseMusic
    • 在运行的进程中查找过滤我们需要的进程名称,命名如下:
1
ps -e | grep News
  • 成功之后,就可以看到今日头条应用进程的一些信息,如进程ID9347,进程的可执行文件路径等待。
  • 拿到进程ID后,我们就可以注入住这个进程,从而对应用为所欲为了😈,如何注入进程,命令如下:
1
2
cycript -p 9347 // 需要hook的程序打开到前台
// cycript -p News
  • 此时,我们可以简单测下下是否hook成功:
1
2
[[UIApplication sharedApplication] setStatusBarHidden:YES] // 隐藏状态栏
[[UIApplication sharedApplication] setStatusBarHidden:NO] // 显示状态栏
cycript_hock_test

常用数据类型及类型判断

  • 常用的数据类型(字符串、数组、字典、Bool, NSNumber)和我们在OC中使用的完全一样。
  • 类型判断使用instanceof,如:
1
2
3
4
5
6
cy# @"Hello World" instanceof String
true
cy# @[1, 2, 3] instanceof Array
true
cy# @"Hello World" instanceof Array
false

字符串及数组截取

  • 字符串及数组截取操作,分别使用substrslice
  • 下标从x开始截取y个长度。
1
2
3
4
cy# @"Hello World".substr(0, 5)
"Hello"
cy# @[1, 2, 3].slice(0, 2)
[@1, @2]

创建及获取对象

  • 注入程序后,我们可以根据需要创建需要用到的对象,或者根据内存地址获取到内存中的对象。
    • 创建对象和OC语法差别不大。
    • 根据内存地址获取对象,我们可以使用# 或者使用Instance
1
2
3
4
5
6
// 创建对象
var view = [[UIView alloc] init]
[new UIView init]
// 获取对象
#0x1719f9a0
var same = new Instance(0x1719f9a0)
cycript_hock_newObj

获取对象信息

  • 方法1: 简单基本获取方法。
    • 直接在对象前面加个*
1
*controller()
  • 方法2:方法一无法获取,就使用方法2
1
[i for (i in *UIApp)]
  • 方法3:建议方法三,方法三能获取到更多
1
function tryPrintIvars(a){ var x={}; for(i in *a){ try{ x[i] = (*a)[i]; } catch(e){} } return x; }

类与增加分类

  • 类定义及使用
1
2
3
4
5
@implementation MyClass : NSObject {
int num;
}
-(int)returnNum {return this -> num;}
@end
cycript_hock_newClass
  • 增加分类
1
2
3
4
5
6
7
8
9
// 定义
@implementation NSObject (TestCategory)
- description {return "test"}
- (double)calculate:(float)num {return num * 10;}
@end
// 创建对象
test = [new NSObject init]
// 调用分类方法
[test calculate:10]

Block

1
2
3
4
5
// 定义一个block
cy# testBlock = ^ int (int value) {return value * 10}

// 调用
cy# testBlock(7)

查找内存中的类信息

  • 使用choose()函数可以查找到内存中对应传递进去的类,如查找今日头条首页中的图片控件choose(UIImageView)
  • 输入信息如下(数据太多删除一部分),此时我们就可以通过UIImageView控件的内存地址获取到实例对象,修改对象的各种信息等等。
1
[#"<UIImageView: 0x17112950; frame = (7.5 10; 11 6); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x17393410>>",#"<UIImageView: 0x17114090; frame = (0 0; 0 0); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x1736a500>>",#"<UIImageView: 0x171160a0; frame = (-5 0; 5 0); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x1736a4d0>>",#"<UIImageView: 0x17138d60; frame = (314.5 -39; 2.5 36); alpha = 0; opaque = NO; autoresize = LM; userInteractionEnabled = NO; layer = <CALayer: 0x17145ad0>>",#"<SSThemedImageView: 0x1713b090; baseClass = UIImageView; frame = (0 0; 0 0); hidden = YES; opaque = NO; gestureRecognizers = <NSArray: 0x1713b380>; layer = <CALayer: 0x17136fb0>>",......",#"<SSThemedImageView: 0x171687b0; baseClass = UIImageV

打印视图层次结构

  • 如果我们想知道某个视图的层次结构信息,也可以不使用Reveal,使用下面指令查看整个层次的结构信息:
1
UIApp.keyWindow.recursiveDescription().toString()

获取某个类的所有内容

  • 我们可以打印出某个类的所有内容信息:
    • 定义执行下面的函数
    • 定义后就可以获取指定类的信息,调用printMethods(className, isa)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function printMethods(className, isa) { 
var count = new new Type("I");
var classObj = (isa != undefined) ? objc_getClass(className)->isa :
objc_getClass(className);
var methods = class_copyMethodList(classObj, count);
var methodsArray = [];
for(var i = 0; i < *count; i++) {
var method = methods[i];
methodsArray.push({selector:method_getName(method),
implementation:method_getImplementation(method)});
}
free(methods);
return methodsArray;
}
printMethods("UIImageView", true)
printMethods("TTArticleTabBarController", true)

获取当前控制器

  • 可以直接打印可见控制器,如果获取不到可用下面的方法
1
UIApp.keyWindow.rootViewController.visibleViewController
  • 也打印出当前控制器名称及内存地址
    • 定义执行下面的函数
    • 调用函数var vc = currentVC(),如输出信息#"<TTArticleTabBarController: 0x17a5bc00>"
1
2
3
4
5
6
7
8
9
10
function currentVC() {
var app = [UIApplication sharedApplication]
var keyWindow = app.keyWindow
var rootController = keyWindow.rootViewController
var visibleController = rootController.visibleViewController
if (!visibleController) {
return rootController
}
return visibleController.childViewControllers[0]
}

加载系统动态库

  • 定义执行下面获取系统动态库的函数
  • 定义好后就可以调用函数如loadFramework("UIKit"),传递想要加载的框架名称加载对应的框架。
1
2
3
4
5
function loadFramework(fw) { 
var h="/System/Library/",t="Frameworks/"+fw+".framework";
[[NSBundle bundleWithPath:h+t]||
[NSBundle bundleWithPath:h+"Private"+t] load];
}

NSLog打印函数

1
2
3
4
5
6
7
8
9
NSLog_ = dlsym(RTLD_DEFAULT, "NSLog")
NSLog = function() {
var types = 'v', args = [], count = arguments.length;
for (var i = 0; i != count; ++i) {
types += '@';
args.push(arguments[i]);
}
new Functor(NSLog_, types).apply(null, args);
}

CG…Make函数

1
2
3
cy# function CGPointMake(x, y) { return {x:x, y:y}; }
cy# function CGSizeMake(w, h) { return {width:w, height:h}; }
cy# function CGRectMake(x, y, w, h) { return {origin:CGPointMake(x,y), size:CGSizeake(w, h)}; }

使用Cycript一窥微信

  • 这里综合上面的知识练练手:
    • 修改微信登录界面按钮文字及背景颜色
    • 在登录界面弹出一个Alert弹框
    • 修改微信登录界面背景图片

注入进程

  • 获取微信的进程ID,使用Cycript注入微信进程。
1
2
3
ps -e | grep WeChat
cycript -p WeChat
// cycript -p 进程ID // 替换为你的微信进程ID

层次结构

  • 查看微信登录界面的层次结构
1
UIApp.keyWindow.recursiveDescription().toString()
  • 输出的层次结构信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
`<iConsoleWindow: 0x14f6e420; baseClass = UIWindow; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x14f6dea0>; layer = <UIWindowLayer: 0x14f6e7a0>>
| <UILayoutContainerView: 0x14e10510; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x14e0cd60>; layer = <CALayer: 0x14e103d0>>
| | <UINavigationTransitionView: 0x14e0d370; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x14e0ddd0>>
| | | <UIViewControllerWrapperView: 0x14f312e0; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x14f31220>>
| | | | <UIView: 0x14e15e80; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x14ef6410>>
| | | | | <UIView: 0x160ce380; frame = (0 20; 320 633); autoresize = W; layer = <CALayer: 0x160c5110>>
| | | | | | <UIImageView: 0x14f2bef0; frame = (0 -20; 320 568); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x14f2bff0>>
| | | | | <UIView: 0x160ce5b0; frame = (0 483; 320 65); autoresize = W+TM; layer = <CALayer: 0x160ce690>>
| | | | | | <FixTitleColorButton: 0x160ce750; baseClass = UIButton; frame = (20 18; 130 47); clipsToBounds = YES; opaque = NO; autoresize = RM; layer = <CALayer: 0x14f2c140>>
| | | | | | | <UIButtonLabel: 0x160ce980; frame = (46.5 13; 37 21.5); text = '\u767b\u5f55'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x160ceae0>>
| | | | | | | | <_UILabelContentLayer: 0x160cfc90> (layer)
| | | | | | <FixTitleColorButton: 0x160ced10; baseClass = UIButton; frame = (170 18; 130 47); clipsToBounds = YES; opaque = NO; autoresize = LM; layer = <CALayer: 0x160ceeb0>>
| | | | | | | <UIButtonLabel: 0x14f2c5f0; frame = (46.5 13; 37 21.5); text = '\u6ce8\u518c'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x14f2c750>>
| | | | | | | | <_UILabelContentLayer: 0x160d0cd0> (layer)
| |
  • Command + F 搜索UIButton,可以找到有两个UIButton对象,并且text属性是显示的Unicode编码(中文在里面显示的就是Unicode字符编码)。我们可以在在线的Unicode和中文互转网站转换一下,如这个网站,可以看到对应的刚好就是界面上显示的登录注册
  • 我们就可以拿到每一个的对应的按钮对象,改变按钮显示文字:
    • 如我拿到的登录按钮内存地址:0x19c87130
    • 注册按钮内存地址:0x19cd4470
    • 注意设置中文时需要将中文转为Unicode编码。
1
2
#0x19c87130.titleLabel.text = @"\u52ab\u8d22"  // \u52ab\u8d22 == "劫财"
#0x19cd4470.titleLabel.text = @"\u52ab\u8272" // \u52ab\u8272 == "劫色"

修改按钮文字背景颜色:

  • 我们搜索上一步获取到的层次结构信息,可以找到叫UIButtonLabel的对象,修改按钮文字背景就是修改UIButtonLabel的背景颜色
  • 只需要根据UIButtonLabel地址修改即可
1
2
#0x15f4f590.backgroundColor = [UIColor redColor]
#0x15f4ffb0.backgroundColor = [UIColor greenColor]

弹出一个Alert弹框

  • 如果对方法签名不熟悉,可以在Xcode中打出来后拷贝进终端
  • 尽量不要有中文和表情
  • 也可以在系统桌面弹出,需要注入到系统的桌面进程SBHomeScreenWindow
1
2
var alert = [[UIAlertView alloc] initWithTitle:@"Hack You" message:@"I've hacked you" delegate:nil cancelButtonTitle:@"No pay" otherButtonTitles:@"Pay", nil];
[alert show];

修改微信登录界面背景图片

  • 如果我们想替换微信的某个图片,这里以登录界面背景为例子,只需要将我们自己的图片放在微信的某个目录下,如Document下,然后修改背景图片控件的图片即可。
  • 将图片放入对应的文档目录下:

    • 可以直接使用工具iMazing,也可以使用scp命令远程传输(越狱第一篇文章SS部分),看个人爱好。
  • 获取微信的文档目录路径地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cy# [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0]

// 输出文档目录路径地址
#"file:///var/mobile/Containers/Data/Application/8B12B9B9-06BE-49C2-ADC2-96BB1C4F5E79/Documents/"

// 图片加载路径地址
/var/mobile/Containers/Data/Application/8B12B9B9-06BE-49C2-ADC2-96BB1C4F5E79/Documents/
```

- 在层次结构信息中查找`UIImageView`即为背景图片控件,根据内存地址获取到图片对象,然后重新设置图片:
- 如我获取到的图片控件内存地址是`0x14706110`
- 我放在文档目录的替换图片为`wechat_testBg.jpg`

```objc
#0x14706110.image = [UIImage imageWithContentsOfFile:@"/var/mobile/Containers/Data/Application/8B12B9B9-06BE-49C2-ADC2-96BB1C4F5E79/Docments/wechat_testBg.jpg"]
  • 综合效果图(没修改文字背景颜色)
cycript_hook_wechat_effect
  • 其他好玩的大家可以自行测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 截屏
cy# var shot = [SBScreenShotter sharedInstance]
cy# [shot saveScreenshot:YES]

// 隐藏桌面
cy# UIApp.keyWindow // 获取SBHomeScreenWindow对象地址,SBHomeScreenWindow就是桌面Window
cy# screen = #0xxxxxxxxxx(上一步获取到的内存地址)

cy# screen.hidden = YES // 隐藏
cy# screen.hidden = NO // 显示

// 改变手机的背景色
cy# screen.backgroundColor = [UIColor redColor]

// 设置badge
cy# [[UIApplication sharedApplication] setApplicationIconBadgeNumber:100]

福利

  • 如果感觉上面一些函数比较复杂,不想每次都去敲一次,那么福利来了,MJRefresh作者将一些Cycript实现的一些实用函数封装成一个脚本放在GitHub上面,基本上囊括了我们上面的那些,还有一些其他的比较常用的函数。
  • 需要的小伙伴们可以直接去在这里下载,至于如何使用大家看文档就可以了,比较简单,都是中文就没必要我在转述一遍了。

  • 通过一系列文章的介绍及实战,可以看到我们的App面临的安全问题是多么的可怕,也意识到保护app数据的重要性。
  • 但是既然知道了这么多可能导致不安全的点,就要做好相应的防,保护重要数据不被攻击者使用Cycript或者Runtime修改,这里有一篇文章对如何防范介绍的很详细,大家可以参考下

参考

要不要鼓励一下😘