iOS-程式碼混淆加固策略
對於IOS來說,由於系統是封閉的,APP上架需要通過App Store,安全性來說相當高。但是對於大廠和知名APP而言,別人給的安全保障永遠沒有自己做的來得踏實。所以對於大廠、少部分企業級和金融支付類應用來說加固是相當重要的。
下面是目前幾個專業加固大廠提供的加固策略
網易
網易安全三板斧:
-
第一板斧是防靜態分析,這裡包括字串加密、符號混淆、程式碼邏輯混淆和遊戲存檔加密;
-
第二板斧是防動態除錯、反除錯和通訊安全(資料加密);
-
第三板斧是外掛檢測、加速掛、記憶體修改掛和自動任務掛等
愛加密
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.py 和 touch 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 -*-
必須有這三行程式碼,才能在指令碼中輸入中文
(3.如果報IndentationError: unexpected indent,請注意指令碼中的每行程式碼的換行符和縮排格式必須標準
執行完步驟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.sh和obDecrypt.sh檔案中加入指令碼內容
下面是加密混淆指令碼內容
#!/bin/sh
##################################
# (該指令碼是在https://github.com/heqingliang/CodeObfus 上找到的)
# 程式碼混淆指令碼 heyujia 2018.03.15
#
##################################
#識別含有多位元組編碼字元時遇到的解析衝突問題
export LC_CTYPE=C
export
#配置項:
#專案路徑,會混淆該路徑下的檔案
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
#配置項:
#已經混淆的專案路徑
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指令碼學習的網站
作者:樹下敲程式碼的超人