你所认为 iOS 中的安全并不安全

经历过最近轰动一时的 Xcode Ghost 事件后,可以看出,即便大如腾讯这般的企业,在面对 APP 的安全性时,态度也是不够严谨的。各大媒体却将矛头指向了苹果手机(标题:苹果手机不安全),这种低俗的竞争手段还真让人汗颜。作为开发人员,我觉得非常有必要修习安全相关的知识,即便不是精通,起码要有些常识。

这篇文章,便是在普及 iOS 安全的基本常识,而你目前关于这方面的认知,可能都是错的!

逆向有多难

谈及一个应用的安全,其实更多的便是关于一个应用的逆向,所谓逆向便是通过一系列的手法,从原始的可执行二进制文件中分析出有漏洞的地方,从而进行窜改,以达到不可告人的目的。逆向有多难呢?对于未涉及该领域的人来说,常常处于两个极端,一种认为非常简单,一种认为难如登天。

曾经有个人对我说,他可以在一分钟内将我手头上的一份可执行文件向上抽象,并画出它们的类交互图。当时,我弱弱的问了一句:什么是向上抽象?然后被他狠狠的鄙视了,并且问旁边的人,公司招我来做啥。这样的言论出自一个技术人员的口中,这让人非常担忧,不假思索的态度真心不适合从事需要严谨对待的程序设计。他所说的向上抽象便是进行逆向分析,而我只是在质疑他的言论,质疑点很多,但大体如下:

  • 我质疑你根本不懂Objective-C,如何进行类交互图绘制?难道你能在一分钟里绘制出一份如蜘蛛网般的objc_msgSend
  • 我质疑你根本就没搞清楚PEELFMach-O之间的区别,甚至含义
  • 我质疑你调试的能力,只接触过 gdb,很难让我相信你能在一台越狱手机上通过 lldb 完成所需的调试操作

那么逆向到底有多难?当然,肯定不会如上述言论中的那般简单,就算我们是拿到全部源码也达不到那般速度。从复杂度而言,它并不复杂,只是可能会非常繁琐,接下来的示例,可能会改变你一直以来的看法。

你所认为的安全手段

在一个应用中,面对一些必须持久化的敏感数据,为了安全起见,大家通常都会对这些数据进行加密存储,防止用户窃取一些类似TokenAppKey,甚至更为重要的数据。那么下面就模拟一个这样的应用,我们要存储一个用户对象,用户对象中有一个需要受保护的凭证字段,该凭证主要用于和服务端进行通讯。

下面是该对象的接口代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@interface User : NSObject

/**
* 加载保存的用户
*
* @return 用户信息
*/

+ (User *)loadLastSaveUser;

/**
* 用户唯一标识
*/

@property (nonatomic, assign) NSUInteger identifier;

/**
* 用户凭证
*/

@property (nonatomic, copy) NSString *credentials;

/**
* 保存用户
*/

- (void)save;

@end

下面是实现部分的相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@interface User () <NSCoding>

@end

@implementation User

- (instancetype)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) {
self.identifier = [decoder decodeIntegerForKey:NSStringFromSelector(@selector(identifier))];
self.credentials = [decoder decodeObjectForKey:NSStringFromSelector(@selector(credentials))];
}
return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeInteger:self.identifier forKey:NSStringFromSelector(@selector(identifier))];
[coder encodeObject:self.credentials forKey:NSStringFromSelector(@selector(credentials))];
}

+ (User *)loadLastSaveUser {
NSData *userData = [[NSUserDefaults standardUserDefaults] dataForKey:kDefaultKeyForUser];
userData = SuperPowerDecrypt(userData); // 解密
if (userData == nil) return nil;

return [NSKeyedUnarchiver unarchiveObjectWithData:userData];
}

- (void)save {
NSData *userData = [NSKeyedArchiver archivedDataWithRootObject:self];
userData = SuperPowerEncrypt(userData); // 加密

[[NSUserDefaults standardUserDefaults] setObject:userData forKey:kDefaultKeyForUser];
[[NSUserDefaults standardUserDefaults] synchronize];
}

@end

可以看到,我们这里进行了非常强大(SuperPower)的加解密持久化操作,相信大家肯定都知道这个是可以被攻破的,但是,我们要攻破这样一个存储内容,需要多大气力呢?

不下于三种的攻破方式

要攻破这样的防护,其实非常简单,以至于可以有很多不同的方式来攻破,以下我们例举一些非常有代表性的手法。一般在进行破解之前,我们会通过class-dump导出所有的Objective-C头文件,当然,从 AppStore 上下载的应用,苹果都是经过加密的,可以用Clutch或其他工具进行解密,这可是非常简单的步骤,然后便可以导出所有的Objective-C头文件了,以下是我们导出的User类文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface User : NSObject <NSCoding>
{
unsigned long long _identifier;
NSString *_credentials;
}

+ (id)loadLastSaveUser;
- (void).cxx_destruct;
@property(copy, nonatomic) NSString *credentials; // @synthesize credentials=_credentials;
- (void)encodeWithCoder:(id)arg1;
@property(nonatomic) unsigned long long identifier; // @synthesize identifier=_identifier;
- (id)initWithCoder:(id)arg1;
- (void)save;

@end

一切都暴露无疑,那么,有了这样一个头文件,我们就可采用不同的手段来进行攻破了。

Cycript 神器

cy一直都是 saurik 大神的御用前缀,Cycript也是他非常具有代表性的作品之一,详细内容我就不再介绍了,相信有兴趣的通过搜索引擎可以很容易的了解到。我们这里,就通过该工具就可以非常容易的解密到User中所存储的内容,具体方式如下:

这个过程可以说,非常简单,只要将Cycript附加的进程,然后直接调用相关类的方法,我们就可以获取到需要得到的信息。

lldb 调试

除了上面所的方式,我们还可以使用lldb进行调试,获取到我们相应的信息,具体步骤如下:

依然是十分的方便,豪不费力气的我们就获得到了想要的信息,可以想象这个所谓的加密解密是多么的不堪一击。除了po之外,我们还可以通过增加符号断点,然后读取寄存器中的值,也是可以达到相同的效果。通过窜改寄存器中的值,我们可以扰乱原有设定的程序逻辑,比如窜改identifier,如果服务端安全性没有做好,这时候我们可以冒充其它用户进行相应的网络操作了。

无处不在的 Hook

除了上面对User对象操作的方式之外,我们甚至完全不用考虑User的存在,因为这个凭证最终是需要通过网络请求进行发送的,即便是使用 https 也无妨,因为我们不需要通过抓包就可以提前获取到请求的内容。这便是进行 Hook 操作,通过MobileSubstrate配合theos,我们可以非常方便的编写自己的 tweak,从而 Hook 一些我们感兴趣的方法,比如这里我们就可以 Hook 掉 NSURLRequest 设置请求头的方法,将内容 Dump 出来,这里偷懒一下,就不进行具体的演示操作了。

矛盾所在

看了上面操作,是不是对原先这样的设计产生质疑了?其实,如果是这种程度的防护,根本就是多余的操作,因为一旦攻击者对你存储的数据感兴趣,那么你这样费尽苦心的加解密对攻击者而言根本没有任何意义,而普通用户更不会关心你数据是否是加密存储。那么,问题究竟出在那?

问题通常存在于客户端将锁和钥匙都放在了一起,很多时候这都是无可奈何的做法,那么所能做的便是将这钥匙藏得更难发现点,但,终归会有开锁的时候,这时攻击者会偷瞄到你钥匙存放的地方,整个防线便崩溃了。就算你有特别的技巧,在开锁的时候让自己隐身,但锁一打开,攻击者可能就直奔进去,拿走了自己想要的东西。

就比如我们要防止 MP3 音频数据被盗,使用了非常复杂的加密算法,社么矩阵啊,什么向量啊,但最终客户端还是需要调用系统 API 进行播放的,无论是CoreAudio还是AudioTookbox中的方法,都可以被攻击者 Hook 掉,从而 Dump 出原始的 PCM 数据,你的加密只是降低播放性能,增加自己的工作量而已。

没有绝对的安全,就我所知的所有 iOS 安全防护,也都有相应破解手段,只不过是更加繁琐了一点而已。只要你的应用防不过操作系统,那么肯定就有破解的手段,当然,你的应用最终是需要进行正确执行的,所以操作系统肯定会完全知情,这便是所有的矛盾所在。

何去何从

那么,难道我们就这样放任不管了么?我们应该尽可能的将安全数据存放置于服务器中,并且所有核心的校验也都应该是服务器进行的,这样客户端便没有了后顾之忧。就算你窜改数据,服务端也是不会认账的,自然,客户端那些毫无意义的加密解密操作便可以去掉了。

安全是一个比较庞大的话题,本篇通过非常简单的一个实例普及了下常识性的内容,如果进行深入研究,那么你会发现一些更鲜为人知的黑魔法,加油吧,少年!