關于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安全的簡單策略--上卷

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。