现在公司做的金融类项目,对于安全和反欺诈要求比较高,之前对App安全只有部分涉及,在这个项目中系统化的对App安全防护做了一些工作.

一.网络安全
http网络请求不安全是大家都知道的,所以配合后端做https的升级是必要的.
另一方面就是防中间人攻击,中间人就是介于客户端和服务端中间,截获两端的信息进行篡改,对两端进行欺骗.
我们目前采用的方法是对于传输的参数进行验签和加密.先上代码吧.

```
/*
请求参数:
data : 接口需要的参数(如果加密的话使用AES的CBC加密方式 ,key为时间戳timestamp +秘钥SecretKey)
token :  除部分接口调用时还没用token之外,都要上传
sign:  除sign本身外所有参数加上秘钥SecretKey降序排序以key1=value1&key2=value2的形式拼接然后MD5
version :  版本号
timestamp :  时间戳(到毫秒,例如1559635536593)
appId : 包名
requestId  :  请求id    时间戳(到毫秒,例如1559635536593)拼接一个10000内的随机数,
encryptedStatus :    是否加密 字符串类型 0 不加密 1 加密
*/

// 数据结构 :
{
  "data" : {
 "参数key" : "值value"
"token" : “bc53e2667ee7cf78d4b6c7b5a9c81af51534844249"
  },
“sign”:  “”
  "version" : “1.0.1",
  "timestamp" : “1559635536593", 
  appId” : “com.fsdf.fdfdf”
“requestId” : “1559635536593567”
“encryptedStatus”:  "1"
}

/*
服务器响应数据
message  :  接口提示信息
encryptedStatus :  是否加密 字符串类型 0 不加密 1 加密
data  :  json数据
code  :  状态码
requestId : 请求id
*/
// 数据结构 :
{
"message" : "成功"
“encryptedStatus”: "0"
“data” : {
  },
 "code" : 0
"requestId": ""
}
```
如果对MD5和AES加密不了解的可以去百度,这里不多说了.这里主要解释几个要点.
1.网络请求data里的为接口的私有参数,data外的是共有参数,所有接口都要上传共有参数
2.sign是防篡改的,一般逻辑是对参数做一定的排序,然后通过不同的转换,最后通过
MD5输出统一长度的标识码,后端使用同样的逻辑去校验,这样保证信息传输过程中没有篡改

3.token是客户端登录后服务端返回的,每个用户有且只有一个token,token可以用来做登录时效和保证单端登录,服务端每个接口都需要校验token是否有效,无效的token直接以错误状态码返回.
4.requestId请求ID,主要是用来拦截延时请求和重复请求.
5.appId主要区分请求的来源方

二.日志
生产环境中不要输出日志,系统日志是可以查到的,一般使用宏定义就可以避免,或者使用DDLOG.例如
```
// 输出
#ifdef DEBUG
#define SSLog(format, ...) printf("class: <%p %s:(%d) > method: %s %s",self,
[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],
__LINE__, __PRETTY_FUNCTION__, [[NSString stringWithFormat:(format),
##__VA_ARGS__] UTF8String] )
#else
#define SSLog(format, ...)
#endif
```

三.本地信息存储

本地信息存储就遵循一个原则,重要信息不能明文保存或者简单编码保存,不管是存在沙盒还是keychain中都是不安全的.我的解决方案是尽量将敏感信息存储于服务器端,使用时请求,使用完销毁,需要保存到本地的信息使用AES加密后保存,AES密钥及加密方法封装为静态库保存,尽量提高反编译的难度.

四.反编译防护

现在的反编译技术已经十分强大了,能查到的方法不是失效了就是不能通过苹果的机审.由于现在的破解技术已经能获取系统的root权限并且可以手动开辟线程运行自己的代码,想做到绝对的安全对于小公司或者技术力量不强大的公司基本没可能了,在几天的研究中总结了几个方法:

1.防tweak依附 ,防gdb调试

tweak和gdb我就不解释了,想了解的可以去google,我只说得到的解决方法,不过记住这样做只能提高反编译的难度,保护总比破坏难,能做总比不做好,下面代码都是在main.m中.
```
#include<dlfcn.h>
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

// 防tweak依附
static __inline__ __attribute__((always_inline)) int anti_tweak()
{
    uint8_t lmb[] = {'S', 'u', 'b', 's', 't', 'r', 'a', 't', 'e', '/', 'D',
'y', 'n', 'a', 'm', 'i', 'c', 0, };
    NSString *dir = [NSString stringWithFormat:@"/%@/%@%s%@", @"Library",
@"Mobile", lmb, @"Libraries"];
    NSArray *dirFiles = [[NSFileManager defaultManager]
contentsOfDirectoryAtPath:dir error:nil];
    NSArray *plistFiles = [dirFiles filteredArrayUsingPredicate:
                           [NSPredicate predicateWithFormat:
                            [NSString stringWithFormat:@"%@ %@%@
'.%@%@'",@"self", @"EN", @"DSWITH", @"pli", @"st"]]];
    int cnt = 0;
    for (NSString *file in plistFiles) {
        NSString *filePath = [dir stringByAppendingPathComponent:file];
        NSString *fileContent = [NSString stringWithContentsOfFile:filePath
encoding:NSUTF8StringEncoding error:nil];
        if (fileContent && [fileContent rangeOfString:[[NSBundle mainBundle]
bundleIdentifier]].location != NSNotFound) {
            cnt ++;
        }
    }
    // 返回有针对本 app 的 tweak 数量,为 0 说明没有
    return cnt;
}

// 防止GDB挂起
typedef int(*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int
_data);
#if!defined(PT_DENY_ATTACH)
#define PT_DENY_ATTACH 31
#endif  // !defined(PT_DENY_ATTACH)
void disable_gdb() {
    void* handle = dlopen(0, RTLD_GLOBAL |RTLD_NOW);
    ptrace_ptr_t ptrace_ptr = dlsym(handle,"ptrace");
    ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
    dlclose(handle);
}


int main(int argc, char * argv[]) {
#if DevelopMent == 0    // 只在生产环境检测 因为它会关闭调试模式 控制台无法输出
    disable_gdb();
#endif
    // 防tweak依附
    if (anti_tweak() != 0  || getenv("DYLD_INSERT_LIBRARIES") != NULL) {
        exit(0);
    }
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil,
NSStringFromClass([AppDelegate class]));
    }
}

```

2.安装包文件完整性检测
原理就是将每次打包后的文件列表的MD5值保存在服务器端,每次APP启动时校验本地文件,如果不一致做出相应处理.代码:
```
#import "FileHash.h" // GitHub上搜索
/**
 比对文件哈希值 校验完整性
 @param info 服务器端的文件哈希值
 */
+ (void)toGetVersionWithInfo:(NSDictionary *)info {
    if (![info isEqualToDictionary:[self getBundleFileHash]]) {
        exit(0);
    }
}

//获得所有资源文件名
- (NSArray *)allFilesAtPath:(NSString *)dir{
    NSMutableArray * arr = [NSMutableArray array];
    NSFileManager * manager = [NSFileManager defaultManager];
    NSArray *temp = [manager contentsOfDirectoryAtPath:dir error:nil];
    
    for (NSString * fileName in temp) {
        BOOL flag = YES;
        NSString * fullpath = [dir stringByAppendingPathComponent:fileName];
        if ([manager fileExistsAtPath:fullpath isDirectory:&flag]) {
            if (!flag ) {
                [arr addObject:fileName];
                //                NSLog(@"%@",fileName);
            }
        }
    }
    return arr;
}

//生成资源文件名及对应的hash的字典, eg:@{@"appicon":@"wegdfser45t643232324234"};
- (NSDictionary *)getBundleFileHash{
    NSMutableDictionary * dicHash = [NSMutableDictionary dictionary];
    NSArray * fileArr = [self allFilesAtPath:[[NSBundle
mainBundle]resourcePath]];
    for (NSString * fileName in fileArr) {
        //对应的文件生成hash
        NSString * HashString = [FileHash md5HashOfFileAtPath:[[[NSBundle
mainBundle] resourcePath] stringByAppendingPathComponent:fileName]];
        if (HashString != nil) {
            [dicHash setObject:HashString forKey:fileName];
        }
    }
    //所有资源文件的hash就保存在这数组里
//    NSLog(@"-----%@",dicHash);
    return dicHash;
}
```

3.方法名混淆
方法名混淆现在不好过机审,建议只用于关键方法,怎么做大家自己搜索吧,关键词: confuse.sh      func.list

4.App切换到后台之后截图防护
做法就是在切换时候添加蒙版
```
- (void)applicationWillResignActive:(UIApplication *)application {
    _bar = [[UIToolbar alloc]initWithFrame:[UIScreen mainScreen].bounds];
    _bar.barStyle = UIBarStyleBlackTranslucent;
    [self.window addSubview:_bar];
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
    [_bar removeFromSuperview];
    _bar = nil;
}
```
目前项目中做的防护就这些了,希望大家一起交流.