关于iOS HTML安全的简单策略--下卷

关于iOS HTML安全的简单策略--下卷

时隔三年,终于要写下卷了,其实这个方法早就想好的了,但貌似大家不是很喜欢关注本地HTML安全的问题,主要是跨平台的手段太多了HTML不是一个优秀的“解”。

上卷说到加密的HTML放置于本地,等到App运行的时候才去解密。我们预期最终的一个方案就是用到的时候,没有用到时候就保持一个原有的加密状态。这是预期效果,思路就是获取相应的js的一个加载路径,动态去解码js文件。

1.可行性分析

1.1.NSURLProtocol

动态去加载JS的关键就是NSURLProtocol,这是个什么东西呢?简单来说一个网络层的处理代理,里面有几个关键的方法,网上相关的文章也有很多,这里就不做详细的讲解了,关于一些循环的问题,可以参考

/**
 * 关键方法,防止循环调用,
 * 但理论上我们只是使用本地的flie没有发起网络请求,
 * 应该不会存在这个问题。,算是一个拓展了解。
 * 这顺带提一下
 */
[NSURLProtocol propertyForKey:@"xxxxxx" inRequest:request]

的用法。

NSURLProtocol的作用.png
// 有三个类方法,两个对象方,这五个方法需要依次实现。
// 这里是判断如何是否需要处理我们的一个网络请求
//在我们这个需求里面,可以看作是否要拦截我们的请求

+ (BOOL)canInitWithRequest:(NSURLRequest *)request;

// 拦截下来的NSURLRequest收否需要额外的处理,这里是用于做重定向的
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request;

// 这里是判断是否与缓存是同一个请求,如果是就使用缓存,如果不是,就重新请求。
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;

// 开始处理请求
// 成功拦截了请求
// 系统将自动初始化一个你注册出来的NSURLProtocol子类出来处理请求。
- (void)startLoading;

// 请求结束了,这里不管是正常结束还是出错都会走这方法。
- (void)stopLoading;

看完了NSURLProtocol的关键方法,我们来看看关键属性:

/**
 * client是一个代理对象,这里由网络请求的底层去管理,
 * 会自动给NSURLProtocol赋值。这里无需去探究client具体是什么
 * 对象
 */
@property (nullable, readonly, retain) id <NSURLProtocolClient> client;

/**
 * 关于NSURLProtocolClient关键代理,
 * 这里的方法系统已经实现了,
 * 只需要开发者在适当的时机调用就好了
 */
/*!
 * 这里是重定向,很大程度上是返回一个新的Response
 */
- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;

/*!
 * 去查找一个缓存的Response
 */
- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;

/*!
 * 返回一个已经实现的response,并添加缓存
 */
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;

/*!
 * 返回一个请data,给response
 */
- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;

/*!
 * 当前请求已经结束
 */
- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;

/*!
 * 当前请求失败,返回一个Error
 */
- (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error;

/*!
 * https进行双向认证
 */
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;

/*!
 * 取消https进行双向认证
 */
- (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;


1.2.写一个小demo看看

    NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
    
    NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url
                                                cachePolicy:NSURLRequestReloadIgnoringCacheData
                                            timeoutInterval:20];
    
    [[[NSURLSession sharedSession] dataTaskWithRequest:urlRequest
                                     completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"我是从%@中获取数据:%@", url.absoluteString, content);
        
    }] resume];

这是请求结果打印:

直接下载了百度的HTML

从上面我们可以看到直接把"https://www.baidu.com"的HTML文件内容给下载下来了

这里提供了一个思路,实际网络中,我们引入一个js文件,也是发出一个http请求去将这个js下载回来,简单来说我们是不是可以拦截请求,然后将我们实现解密好的data扔回去给webView自己去解析呢。

1.3.我们一块来拦截百度

创建一个NSURLProtocol的子类

// AvalanchingURLProtocol.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface AvalanchingURLProtocol : NSURLProtocol

@end

NS_ASSUME_NONNULL_END

// AvalanchingURLProtocol.m
#import "AvalanchingURLProtocol.h"

@interface AvalanchingURLProtocol ()<NSURLSessionDelegate>


@end

@implementation AvalanchingURLProtocol

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    // 官方的解释是如果协议可以处理给定的请求,则为“是”,否则为“否”。
    // 这里理解成是否是处理
    return YES;
}

+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
    // 返回一个NSURLRequest
    return request;
}

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
    // 对比是否是相同的Request, 如果相同这使用缓存
    return [super requestIsCacheEquivalent:a toRequest:b];
}

- (void)startLoading {
    // 开始处理请求方法
    NSString *string = @"你的百度被我偷了,我是另外一个数据";
    NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
    
    // 返回数据
    [[self client] URLProtocol:self didLoadData:data];
    // 结束本次请求
    [self.client URLProtocolDidFinishLoading:self];
    
}

- (void)stopLoading {
    // 请求结束了回调放法
}

@end
// AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    // 注册协议处理者
    [NSURLProtocol registerClass:[AvalanchingURLProtocol class]];
    
    NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
    
    NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url
                                                cachePolicy:NSURLRequestReloadIgnoringCacheData
                                            timeoutInterval:20];
    
    [[[NSURLSession sharedSession] dataTaskWithRequest:urlRequest
                                     completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"我是从%@中获取数据:%@", url.absoluteString, content);
        
    }] resume];
    
    return YES;
}

请求输出的结果:
篡改之后的结果.png

走到这一步,相信机智如你,应该都懂了吧。

2.总体的思路

2.1.0 还有些什么东西

1.要拦截WKWebView,那么我们就希望拦截WKWebView;
2.并不是所有的Request都是需要拦截的;

/**
 * 通过NSClassFromString获取WKBrowsingContextController私有
 * 类,这里网上有建议说要将WKBrowsingContextController拆成
 * 符串数组,用的时候再拼接起来,以躲避Apple的机审,目前还没
 * 有被apple拒审的情况
 */
NSString *className = @"WKBrowsingContextController";
Class cls = NSClassFromString(className);
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:")

/// 拆分一个例子
#define STRNG_ENCRYPT_KEY @"kljdjgkajkdlgjlkasjdlkg"

static NSString * AES_KEY() {
    unsigned char key[] = {
        (STRNG_ENCRYPT_KEY ^ 'W'),
        (STRNG_ENCRYPT_KEY ^ 'K'),
        (STRNG_ENCRYPT_KEY ^ 'B'),
        (STRNG_ENCRYPT_KEY ^ 'r'),
        (STRNG_ENCRYPT_KEY ^ 'o'),
        (STRNG_ENCRYPT_KEY ^ 'w'),
        (STRNG_ENCRYPT_KEY ^ 's'),
        (STRNG_ENCRYPT_KEY ^ 'i'),
        (STRNG_ENCRYPT_KEY ^ 'n'),
        (STRNG_ENCRYPT_KEY ^ 'g'),
        (STRNG_ENCRYPT_KEY ^ 'C'),
        (STRNG_ENCRYPT_KEY ^ 'o'),
        (STRNG_ENCRYPT_KEY ^ 'n'),
        (STRNG_ENCRYPT_KEY ^ 't'),
        (STRNG_ENCRYPT_KEY ^ 'e'),
        (STRNG_ENCRYPT_KEY ^ 'x'),
        (STRNG_ENCRYPT_KEY ^ 't'),
        (STRNG_ENCRYPT_KEY ^ 'C'),
        (STRNG_ENCRYPT_KEY ^ 'o'),
        (STRNG_ENCRYPT_KEY ^ 'n'),
        (STRNG_ENCRYPT_KEY ^ 't'),
        (STRNG_ENCRYPT_KEY ^ 'r'),
        (STRNG_ENCRYPT_KEY ^ 'o'),
        (STRNG_ENCRYPT_KEY ^ 'l'),
        (STRNG_ENCRYPT_KEY ^ 'l'),
        (STRNG_ENCRYPT_KEY ^ 'e'),
        (STRNG_ENCRYPT_KEY ^ 'r'),
        (STRNG_ENCRYPT_KEY ^ '\0')
    };
    unsigned char * p = key;
    while (((*p) ^= STRNG_ENCRYPT_KEY != '\0')) {
        p++;
    }
    return [NSString stringWithUTF8String:(const char *)key];
}

然后就是自定义协议了,为了和常规的协议混淆,减少不必要的操作,一般会定一个自定义的协议。所谓协议可以简单理解是"http://xxxx.xxxx.com"中的http这个协议名,这里并不是让开发者去实现类似于http的传输协议,而是用一个特殊的字符是代替"http"来标识这是我自己的协议。

SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");

if (cls && sel) {
    if ([(id)cls respondsToSelector:sel]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Warc-performSelector-leaks"
            // 注册自定义协议
        [(id)cls performSelector:sel withObject:@"app"];
#pragma clang diagnostic pop
    }
}
// @"app"就是我们的自定义协议,那么url就应该是app://xxxxx.xxxxxx

接下来就和上述的情况一样了这里贴上- (void)startLoading的代码作为事例。

- (void)startLoading {
    
    // 这里模拟你在上卷学会解密后获取的字符串,.js解密了内容也是一个字符串
    NSString *string = @"var i = 0;function onclickAction() {var tag1 = document.getElementById(\"myh1\");tag1.innerHTML = '我是新的内容 ' + i++;}";
    // 转成Data 扔回去
    NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
    
    // 返回数据
    [[self client] URLProtocol:self didLoadData:data];
    // 结束本次请求
    [self.client URLProtocolDidFinishLoading:self];
    
}

用于存放于本地加载的index.html代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id='htag1'>
        
        <button id='myh1' onclick="onclickAction()">
           Hello world
        </button>
        
    </div>
</body>
<script src="app://diazhaotianlas.semodejfg"></script>
</html>

viewController加载WKWebView的代码

- (void)viewDidLoad {
    [super viewDidLoad];
    // 创建webView
    WKWebView *webView = [[WKWebView alloc] init];
    webView.frame = self.view.bounds;
    [self.view addSubview:webView];


    // 加载URL
    NSString *path = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];

    NSURL *url = [NSURL fileURLWithPath:path];

    NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:20];

    [webView loadRequest:request];
}

让我们跑起来

让我们跑起来.gif

至此我们的简单的安全策略就结束了!!!!

关于iOS HTML安全的简单策略--上卷

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335