关于安全:iOS代码混淆加固方案

42次阅读

共计 11280 个字符,预计需要花费 29 分钟才能阅读完成。

对于 iOS 来说,因为零碎是关闭的,APP 上架须要通过 App Store,安全性来说相当高。然而对于大厂和出名 APP 而言,他人给的平安保障永远没有本人做的来得虚浮。所以对于大厂、少部分企业级和金融领取类利用来说加固是相当重要的。
上面是目前几个业余加固大厂提供的加固策略

  • 网易

  • 网易平安三板斧:
  1. 第一板斧是防动态剖析,这里包含字符串加密、符号混同、代码逻辑混同和游戏存档加密;

2. 第二板斧是防动静调试、反调试和通信安全(数据加密);

  1. 第三板斧是外挂检测、减速挂、内存批改挂和主动工作挂等
  • 爱加密


  • safengine

  • 几维平安

  • 梆梆平安

本文将针对以上几点进行实现,对于一些不太容易实现的将会做方向性探讨

  • 字符串加密
  • 代码混同(办法命,类命,变量名,符号表)
  • 代码逻辑混同
  • 反调试
    • *

字符串加密

对字符串加密的形式目前我所理解到把握到的最牢靠形式就是用脚本将代码中的所有标记须要加密的字符串进行异或转换,这样代码中就不存在明文字符串了。当然第三方的字符串加密不可能这么简略,具体怎么做的我也不太分明。不过为了减少字符串加密的难度复杂性,咱们能够先将字符串用加密工具转换(例如 AES、base64 等)后的把加字符串放在工程中,并且把解密的钥匙放在工程中,用异或转换,把解密钥匙和加密后的字符串转换,这样就有 2 层保障,减少了复杂度。

  • 首先 咱们创立任意一个工程,在工程中写入上面的代码,并在每句打上断点,再抉择 Xcode 工具栏的 Debug –> Debug Workflow –> Always Show Disassembly。这样你就能够在断点处进入汇编模式界面,最初运行程序
/* 加密 NSString 字符串 */
    NSString *str = @"Hello World";
    NSLog(@"%@",str);
    /* 加密 char* 字符串 */
    char* cStr = "Super Man";
    NSLog(@"%s",cStr);


你会发现,你的字符串内容裸露在了汇编模式中,这会导致他人在逆向剖析你的工程时能看见你的字符串内容,咱们个别接口、域名、加解密钥匙串、AppKey、AppId 等比拟重要的货色会放在客户端用作字符串,这就很容易裸露进去。

  • 步骤 1 首先须要在工程代码中进行批改,把上面的宏和 decryptConfusionCS,decryptConstString 函数放入代码中,用宏蕴含每个须要转换的字符串。

/* 字符串混同解密函数,将 char[] 模式字符数组和 aa 异或运算揭秘 */
extern char* decryptConfusionCS(char* string)
{
    char* origin_string = string;
    while(*string) {
        *string ^= 0xAA;
        string++;
    }
    return origin_string;
}

/* 解密函数,返回的是 NSString 类型的 */
extern NSString* decryptConstString(char* string)
{
    /* 先执行 decryptConfusionString 函数解密字符串 */
    char* str = decryptConfusionCS(string);
    /* 获取字符串的长度 */
    unsigned long len = strlen(str);
    NSUInteger length = [[NSString stringWithFormat:@"%lu",len] integerValue];
     NSString *resultString = [[NSString alloc]initWithBytes:str length:length encoding:NSUTF8StringEncoding];
    return resultString;
}


/*
 * 应用 heyujia_confusion 宏管制加密解密
 * 当 heyujia_confusion 宏被定义的时候,执行加密脚本,对字符串进行加密
 * 当 heyujia_confusion 宏被删除或为定义时,执行解密脚本,对字符串解密
 */
#define heyujia_confusion

#ifdef heyujia_confusion
/* heyujia_confusion 宏被定义,那么就进行执行解密脚本 */
/* confusion_NSSTRING 宏的返回后果是 NSString 类型的 */
#define confusion_NSSTRING(string) decryptConstString(string)
/* confusion_CSTRING 宏的返回后果是 char* 类型的 */
#define confusion_CSTRING(string) decryptConfusionCS(string)
#else
/* heyujia_confusion 宏没有被定义,那么就执行加密脚本 */
/* 加密 NSString 类型的 */
#define confusion_NSSTRING(string) @string
/* 加密 char * 类型的 */
#define confusion_CSTRING(string) string
#endif

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {[super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    /* 应用 confusion_NSSTRING 宏蕴含须要加密的 NSString 字符串 */
    NSString *str = confusion_NSSTRING("Hello World");
    NSLog(@"%@",str);
    /* 应用 confusion_NSSTRING 宏蕴含须要加密的 char* 字符串 */
    char* cStr = confusion_CSTRING("Super Man");
    NSLog(@"%s",cStr);    
}
  • 步骤 2 应用终端 cd 到须要加密的工程目录下 执行 touch confusion.pytouch decrypt.py命令,生产加密和解密脚本文件
  • 步骤 3 把上面代码退出解密脚本 confusion.py 中
#!/usr/bin/env python
# encoding=utf8
# -*- coding: utf-8 -*-
# author by heyujia
# 脚本将会用于对指定目录下的.h .m 源码中的字符串进行转换
# 替换所有字符串常量为加密的 char 数组,模式((char[]){1, 2, 3, 0})

import importlib
import os
import re
import sys


# replace 替换字符串为 ((char[]){1, 2, 3, 0}) 的模式,同时让每个字节与 0xAA 异或进行加密
# 当然能够不应用 0xAA 应用其余的十六进制也行 例如 0XBB、0X22、0X11
def replace(match):
    string = match.group(2) + '\x00'
    replaced_string = '((char []) {' + ','.join(["%i" % ((ord(c) ^ 0xAA) if c != '\0' else 0) for c in list(string)]) + '})'
    return match.group(1) + replaced_string + match.group(3)


# obfuscate 办法是批改传入文件源代码中用 confusion_NSSTRING 标记的所有字符串
# 应用 replace 函数对字符串进行异或转换
def obfuscate(file):
    with open(file, 'r') as f:
        code = f.read()
        f.close()
        code = re.sub(r'(confusion_NSSTRING\(|confusion_CSTRING\()"(.*?)"(\))', replace, code)
        code = re.sub(r'//#define ggh_confusion', '#define ggh_confusion', code)
        with open(file, 'w') as f:
            f.write(code)
            f.close()


# openSrcFile 办法是读取源码门路下的所有.h 和.m 文件
# 对每个文件执行 obfuscate 函数
def openSrcFile(path):
    print("混同的门路为"+ path)
    # this folder is custom
    for parent,dirnames,filenames in os.walk(path):
        #case 1:
        #        for dirname in dirnames:
        #            print(("parent folder is:" + parent).encode('utf-8'))
        #            print(("dirname is:" + dirname).encode('utf-8'))
        #case 2
        for filename in filenames:
            extendedName = os.path.splitext(os.path.join(parent,filename))
            if (extendedName[1] == '.h' or extendedName[1] == '.m'):
                print("解决源代码文件:"+ os.path.join(parent,filename))
                obfuscate(os.path.join(parent,filename))


#这里须要批改源码的门路为本人工程的文件夹名称
srcPath = '../daimahunxiao'

if __name__ == '__main__':
    print("本脚本用于对源代码中被标记的字符串进行加密")
    
    if len(srcPath) > 0:
        openSrcFile(srcPath)
    else:
        print("请输出正确的源代码门路")
        sys.exit()
  • 步骤 4 把上面的解密代码放入 decrypt.py 解密脚本中
#!/usr/bin/env python
# encoding=utf8
# -*- coding: utf-8 -*-
# author by heyujia
# 解密脚本
# 替换所有标记过的加密的 char 数组为字符串常量,""

import importlib
import os
import re
import sys


# 替换 ((char[]){1, 2, 3, 0}) 的模式为字符串,同时让每个数组值与 0xAA 异或进行解密
def replace(match):
    string = match.group(2)
    decodeConfusion_string = ""for numberStr in list(string.split(',')):
        if int(numberStr) != 0:
            decodeConfusion_string = decodeConfusion_string + "%c" % (int(numberStr) ^ 0xAA)
    replaced_string = '\"' + decodeConfusion_string + '\"'

    print("replaced_string =" + replaced_string)
    
    return match.group(1) + replaced_string + match.group(3)


# 批改源代码,退出字符串加密的函数
def obfuscate(file):
    with open(file, 'r') as f:
        code = f.read()
        f.close()
        code = re.sub(r'(confusion_NSSTRING\(|confusion_CSTRING\()\(\(char \[\]\) \{(.*?)\}\)(\))', replace, code)
        code = re.sub(r'[/]*#define ggh_confusion', '//#define ggh_confusion', code)
        with open(file, 'w') as f:
            f.write(code)
            f.close()


#读取源码门路下的所有.h 和.m 文件
def openSrcFile(path):
    print("解密门路:"+ path)
    # this folder is custom
    for parent,dirnames,filenames in os.walk(path):
        #case 1:
        #        for dirname in dirnames:
        #            print(("parent folder is:" + parent).encode('utf-8'))
        #            print(("dirname is:" + dirname).encode('utf-8'))
        #case 2
        for filename in filenames:
            extendedName = os.path.splitext(os.path.join(parent,filename))
            #读取所有.h 和.m 的源文件
            if (extendedName[1] == '.h' or extendedName[1] == '.m'):
                print("已解密文件:"+ os.path.join(parent,filename))
                obfuscate(os.path.join(parent,filename))


#源码门路
srcPath = '../daimahunxiao'
if __name__ == '__main__':
    print("字符串解混同脚本,将被标记过的 char 数组转为字符串,并和 0xAA 异或。还原代码")
    if len(srcPath) > 0:
        openSrcFile(srcPath)
    else:
        print("请输出正确的源代码门路!")
        sys.exit()
  • 步骤 5 依据本人的需要批改下脚本外面的代码 和 文件门路。
  • 步骤 6 把步骤 1 中的宏 heyujia_confusion 正文了,而后执行加密脚本,在终端中输出 python confusion.py,
    (1. 如果报错,请查看下本人 Mac 电脑中的 python 版本,如果是 python3 就输出python3 confusion.py.
    (2. 如果报Non-ASCII character '\xe8' in file confusion.py on line 2 相干的错,请确定脚本的后面 3 行是
#!/usr/bin/env python
# encoding=utf8
# -*- coding: utf-8 -*-

执行完步骤 6 后的后果

此时字符串已被加密,运行程序会发现一切正常


加密后汇编界面看不见咱们的字符串内容了,然而咱们用来解密的办法还是裸露在了汇编界面,所以咱们前期还须要对办法名,变量名,类命等做混同。

  • 步骤 7 把步骤 1 中的宏 heyujia_confusion 勾销正文,而后执行解密脚本,在终端中输出python decrypt.py

  • 解密后文本又变回了原样。

这里只是根本的异或转换加密,让代码中的字符串变成看不懂的 char [],实际操作中远远不止这么简略
例如:

  • 首先:咱们先用加密工具例如:AES.Base64 等把须要转换的字符串先加密变成加密字符串
  • 而后:在用异或转换加密的脚本把加密字符串进行转换(包含解密用的钥匙串)
  • 在应用的时候:先异或解密字符串,而后依据解密钥匙串把字符串在转为可用的字符串

ps. 还有一种爱护字符串的办法,就是应用 NSLocalizedString 字符串本地化。


尽管跟着我的步骤你的确加密胜利了,然而你却无奈理论验证。所以要验证最终的混同后果是否达到成果,你还须要学习如何破壳解密 IPA 如何动静动态逆向编程剖析工程源码,大家能够先看看我这篇文章。先把握逆向剖析后在来做代码混同,就能验证混同后果是否无效

变量、办法名,类名混同

对于混同这一块,网上真的是千篇一律,根本都是 copy 的念大婶的内容,没有一点本人的翻新和思考。网上的办法我也用过,然而有缺点,只能混同办法名或者说本人固定的内容去替换。第一不主动,对于大我的项目而言每个办法名本人增加,太麻烦。第二变量混同有问题,因为只是单纯的字符串替换,用宏代替。当遇到应用_ 下划线拜访变量时,就会呈现谬误。

对于变量、办法名,类名的混同,其实跟字符串混同差不多,都是加密混同,而后解密混同。不同的是,变量、办法名,类名的混同目标是为了让他人反编译的时候不晓得你的变量、办法,类是具体用来干什么的,不会想明文那样高深莫测。减少逆向难度。混同的内容不须要想字符串一样,最初程序运行时还要转成中文失常应用。因为自己对 shell 脚本语言也不是十分相熟,想要依照本人的思路写一套残缺的混同脚本还不行。所以这部分也是在网上找的,算是目前最实用最欠缺的混同

  • 首先 关上终端 cd 到须要混同的工程目录下,输出
    touch obConfusion.sh(加密混同脚本文件)
    touch obDecrypt.sh(解密混同脚本文件)
    生成 2 个脚本文件
  • 而后在工程目录以外创立一个文件夹,用于保留加密时生成的加密文本内容,该内容会在解密是用到
  • 最初是在 obConfusion.shobDecrypt.sh文件中退出脚本内容

上面是加密混同脚本内容

#!/bin/sh
##################################
#(该脚本是在 https://github.com/heqingliang/CodeObfus 上找到的)#  代码混同脚本  heyujia 2018.03.15
#
##################################

#辨认含有多字节编码字符时遇到的解析抵触问题
export LC_CTYPE=C
export LANG=C

#配置项:#我的项目门路, 会混同该门路下的文件
ProjectPath="/Users/xieyujia/Desktop/ios/ 学习我的项目 /daimahunxiao"
#这个门路是混同胜利后,原文本和替换文本解密对应的文件寄存门路(该门路不能在我的项目目录或其子目录),混同胜利后会在该门路下生成一个解密时须要的文件,依据该文件的文本内容把混同后的内容更换为原文本内容,该文件名的组成由 $(date +%Y%m%d)"_"$(date +%H%M)及日期_小时组成,每分钟会不一样。所以解密的时候须要每次更换文件门路
SecretFile="/Users/xieyujia/Desktop/ios/ 学习我的项目 /tihuan"$(date +%Y%m%d)"_"$(date +%H%M)

#第一个参数为我的项目门路
if [[$1]]
then
if [[$1 != "_"]]; then
ProjectPath=$1
fi
fi
#第二个参数指定密钥文件门路及文件名
if [[$2]]
then
if [[$2 != "_"]]; then
SecretFile=$2
fi
fi
##############################################################################

#查找文本中所有要求混同的属性 \ 办法 \ 类,只会替换文本中 ob_结尾和_fus 结尾的字符串(辨别大小写,例如 oB_就不会做混同),如果正文内容有该类型的字符串,也会进行替换。对于应用 _下划线拜访的变量属性,不会有影响,一样会替换成对应_的混同内容。resultfiles=`grep 'ob_[A-Za-z0-9_]*_fus' -rl $ProjectPath`
#查找后果为空则退出
if [[-z $resultfiles]]
then
echo "我的项目没有须要混同的代码"
exit
else
echo "开始混同代码..."
echo  > $SecretFile
fi

x=$(awk  '
BEGIN{srand();k=0;}
#随机数生成函数
function random_int(min, max) {return int( rand()*(max-min+1) ) + min;
}
#随机字符串生成函数
function random_string(len) {
result="UCS"k;
alpbetnum=split("a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z", alpbet, ",");
for (i=0; i<len; i++) {result = result""alpbet[ random_int(1, alpbetnum) ];
}
return result;
}
/ob_[A-Za-z0-9_]*_fus/{
x = $0;
#匹配须要混同的属性变量办法
while (match(x, "ob_[A-Za-z0-9_]*_fus") > 0) {tempstr=substr(x, RSTART, RLENGTH);
#判断是否有之前曾经找过的反复字符串
for (i = 0; i < k; i++){if (strarr[i] == tempstr){break;}
}
if(i<k){
#反复字符串,间接删除。所以不必放心混同内容过多,可能会呈现反复的混同字符串
x=substr(x, RSTART+RLENGTH);
continue;
}else{
#不是反复字符串,增加到替换数组
strarr[k++]=tempstr;
}
randomstr=random_string(20);
printf("%s:%s|", tempstr,randomstr);
#替换随机字符串
gsub(tempstr,randomstr, x);
x = substr(x, RSTART+RLENGTH);
}
}' $resultfiles )

#加密对写入密钥文件
echo $x > $SecretFile

recordnum=1
while [[1 == 1]]; do
record=`echo $x|cut -d "|" -f$recordnum`
if [[-z $record]]
then
break
fi
record1=`echo $record|cut -d ":" -f1`
echo "原项:"$record1
record2=`echo $record|cut -d ":" -f2`
echo "加密项:"$record2
#替换文件夹中所有文件的内容(反对正则)#单引号不能扩大
sed -i ''"s/${record1}/${record2}/g" `grep $record1 -rl $ProjectPath`
echo "第"$recordnum"项混同代码处理完毕"
let "recordnum = $recordnum + 1"
done

#查找须要混同的文件名并替换
filerecordnum=1
while [[1 == 1]]; do
filerecord=`echo $x|cut -d "|" -f$filerecordnum`
if [[-z $filerecord]]
then
break
fi
filerecord1=`echo $filerecord|cut -d ":" -f1`
#echo "原项:"$filerecord1
filerecord2=`echo $filerecord|cut -d ":" -f2`
#echo "加密项:"$filerecord2
#改文件名

find $ProjectPath -name $filerecord1"*"| awk 'BEGIN{frecord1="'"$filerecord1"'";frecord2="'"$filerecord2"'";finish=1}
{
filestr=$0;
gsub(frecord1,frecord2,filestr);
print "mv" $0 "" filestr";echo 第 "finish" 个混同文件处理完毕 ";
finish++;
}'|bash
let "filerecordnum = $filerecordnum + 1"
done

上面是解密混同脚本的内容

#!/bin/sh
######################################
#
#  代码还原脚本  RyoHo 2018.03.15
#
######################################

#辨认含有多字节编码字符时遇到的解析抵触问题
export LC_CTYPE=C
export LANG=C

#配置项:#曾经混同的我的项目门路
ProjectPath="/Users/xieyujia/Desktop/ios/ 学习我的项目 /daimahunxiao"
#这个是文件门路而不是目录,是混同的时候生成的文本文件门路,每次不一样。所以每次加密后,解密时须要更换门路
SecretFile="/Users/xieyujia/Desktop/ios/ 学习我的项目 /tihuan20180315_1456"
#第一个参数为我的项目门路
if [[$1]]
then
if [[$1 != "_"]]; then
ProjectPath=$1
fi
fi
#第二个参数指定密钥文件门路及文件名
if [[$2]]
then
if [[$2 != "_"]]; then
SecretFile=$2
fi
fi
##############################################################################
#内容还原
x=`cat $SecretFile`
recordnum=1
while [[1 == 1]]; do
record=`echo $x|cut -d "|" -f$recordnum`
if [[-z $record]]
then
break
fi
record1=`echo $record|cut -d ":" -f1`
echo "原项:"$record1
record2=`echo $record|cut -d ":" -f2`
echo "加密项:"$record2
#若我的项目中加密项与密钥文件的加密项不合乎则退出程序
searchresult=`grep $record2 -rl $ProjectPath`
if [[-z $searchresult]]; then
echo "指定的密钥文件不能还原"
exit
fi
#替换文件夹中所有文件的内容(反对正则)#单引号不能扩大
sed -i ''"s/${record2}/${record1}/g" $searchresult
echo "第"$recordnum"项混同代码还原结束"
let "recordnum = $recordnum + 1"
done
#文件还原
filerecordnum=1
while [[1 == 1]]; do
filerecord=`echo $x|cut -d "|" -f$filerecordnum`
if [[-z $filerecord]]
then
break
fi
filerecord1=`echo $filerecord|cut -d ":" -f1`
#echo "原项:"$filerecord1
filerecord2=`echo $filerecord|cut -d ":" -f2`
#echo "加密项:"$filerecord2
#改文件名

find $ProjectPath -name $filerecord2"*"| awk '
BEGIN{
frecord1="'"$filerecord1"'";
frecord2="'"$filerecord2"'";
finish=1;
}
{
filestr=$0;
gsub(frecord2,frecord1,filestr);
print "mv" $0 ""filestr";echo 第 "finish" 个混同文件还原结束 "
finish++;
}'|bash
let "filerecordnum = $filerecordnum + 1"
done

应大家须要把脚本源码地址放进去

倡议大家看看脚本内容,有利于学习了解。该脚本是有针对性的混同内容,能够本人批改脚本中的正则表达式来确定混同的内容。脚本中只会替换文本中 ob_结尾和_fus 结尾的字符串(辨别大小写,例如 oB_就不会做混同),如果正文内容有该类型的字符串,也会进行替换。对于应用 _下划线拜访的变量属性,不会有影响,一样会替换成对应_的混同内容。
提供一个 shell 脚本学习的网站

正文完
 0