技 術 文 章 / 超 人
對于IOS來說,由于系統(tǒng)是封閉的梅猿,APP上架需要通過App Store氓辣,安全性來說相當高。但是對于大廠和知名APP而言袱蚓,別人給的安全保障永遠沒有自己做的來得踏實钞啸。所以對于大廠、少部分企業(yè)級和金融支付類應用來說加固是相當重要的喇潘。
下面是目前幾個專業(yè)加固大廠提供的加固策略
-
網易
網易安全三板斧:
- 第一板斧是防靜態(tài)分析体斩,這里包括字符串加密、符號混淆颖低、代碼邏輯混淆和游戲存檔加密絮吵;
2.第二板斧是防動態(tài)調試、反調試和通信安全(數(shù)據加密)忱屑;
- 第三板斧是外掛檢測蹬敲、加速掛、內存修改掛和自動任務掛等
-
愛加密
-
safengine
-
幾維安全
-
梆梆安全
本文將針對以上幾點進行實現(xiàn)莺戒,對于一些不太容易實現(xiàn)的將會做方向性討論
- 字符串加密
- 代碼混淆(方法命伴嗡,類命,變量名脏毯,符號表)
- 代碼邏輯混淆
- 反調試
字符串加密
對字符串加密的方式目前我所了解到掌握到的最可靠方式就是用腳本將代碼中的所有標記需要加密的字符串進行異或轉換闹究,這樣代碼中就不存在明文字符串了。當然第三方的字符串加密不可能這么簡單食店,具體怎么做的我也不太清楚渣淤。不過為了增加字符串加密的難度復雜性赏寇,我們可以先將字符串用加密工具轉換(例如AES、base64等)后的把加字符串放在工程中价认,并且把解密的鑰匙放在工程中嗅定,用異或轉換,把解密鑰匙和加密后的字符串轉換用踩,這樣就有2層保障渠退,增加了復雜度。
- 首先 我們創(chuàng)建任意一個工程脐彩,在工程中寫入下面的代碼碎乃,并在每句打上斷點,再選擇Xcode工具欄的Debug --> Debug Workflow --> Always Show Disassembly惠奸。這樣你就可以在斷點處進入匯編模式界面梅誓,最后運行程序
/* 加密NSString字符串 */
NSString *str = @"Hello World";
NSLog(@"%@",str);
/* 加密char*字符串 */
char* cStr = "Super Man";
NSLog(@"%s",cStr);
你會發(fā)現(xiàn),你的字符串內容暴露在了匯編模式中佛南,這會導致別人在逆向分析你的工程時能看見你的字符串內容梗掰,我們一般接口、域名嗅回、加解密鑰匙串及穗、AppKey慌核、AppId等比較重要的東西會放在客戶端用作字符串匀奏,這就很容易暴露出來。
- 步驟1 首先需要在工程代碼中進行修改读处,把下面的宏和decryptConfusionCS尘分,decryptConstString函數(shù)放入代碼中猜惋,用宏包含每個需要轉換的字符串。
/* 字符串混淆解密函數(shù)培愁,將char[] 形式字符數(shù)組和 aa異或運算揭秘 */
extern char* decryptConfusionCS(char* string)
{
char* origin_string = string;
while(*string) {
*string ^= 0xAA;
string++;
}
return origin_string;
}
/* 解密函數(shù)著摔,返回的是NSString類型的 */
extern NSString* decryptConstString(char* string)
{
/* 先執(zhí)行decryptConfusionString函數(shù)解密字符串 */
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宏被定義的時候,執(zhí)行加密腳本定续,對字符串進行加密
* 當heyujia_confusion宏被刪除或為定義時谍咆,執(zhí)行解密腳本,對字符串解密
*/
#define heyujia_confusion
#ifdef heyujia_confusion
/* heyujia_confusion 宏被定義私股,那么就進行執(zhí)行解密腳本 */
/* confusion_NSSTRING宏的返回結果是NSString 類型的 */
#define confusion_NSSTRING(string) decryptConstString(string)
/* confusion_CSTRING宏的返回結果是char* 類型的 */
#define confusion_CSTRING(string) decryptConfusionCS(string)
#else
/* heyujia_confusion 宏沒有被定義摹察,那么就執(zhí)行加密腳本 */
/* 加密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 到需要加密的工程目錄下 執(zhí)行
touch confusion.py
和touch decrypt.py
命令,生產加密和解密腳本文件步驟3 把下面代碼加入解密腳本confusion.py中
#!/usr/bin/env python
# encoding=utf8
# -*- coding: utf-8 -*-
# author by heyujia
# 腳本將會用于對指定目錄下的.h .m源碼中的字符串進行轉換
# 替換所有字符串常量為加密的char數(shù)組倡鲸,形式((char[]){1, 2, 3, 0})
import importlib
import os
import re
import sys
# replace替換字符串為((char[]){1, 2, 3, 0})的形式供嚎,同時讓每個字節(jié)與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函數(shù)對字符串進行異或轉換
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 文件
# 對每個文件執(zhí)行obfuscate函數(shù)
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數(shù)組為字符串常量克滴,""
import importlib
import os
import re
import sys
# 替換((char[]){1, 2, 3, 0})的形式為字符串逼争,同時讓每個數(shù)組值與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)
# 修改源代碼,加入字符串加密的函數(shù)
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數(shù)組轉為字符串誓焦,并和0xAA異或。還原代碼")
if len(srcPath) > 0:
openSrcFile(srcPath)
else:
print("請輸入正確的源代碼路徑着帽!")
sys.exit()
步驟5 根據自己的需求修改下腳本里面的代碼 和 文件路徑杂伟。
步驟6 把步驟1中的宏heyujia_confusion注釋了,然后執(zhí)行加密腳本仍翰,在終端中輸入
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,請注意腳本中的每行代碼的換行符和縮進格式必須標準
-
執(zhí)行完步驟6后的結果
此時字符串已被加密蕾羊,運行程序會發(fā)現(xiàn)一切正常
加密后匯編界面看不見我們的字符串內容了,但是我們用來解密的方法還是暴露在了匯編界面帽驯,所以我們后期還需要對方法名龟再,變量名,類命等做混淆尼变。
-
步驟7 把步驟1中的宏heyujia_confusion取消注釋利凑,然后執(zhí)行解密腳本,在終端中輸入
python decrypt.py
解密后文本又變回了原樣嫌术。
這里只是基本的異或轉換加密哀澈,讓代碼中的字符串變成看不懂的char [],實際操作中遠遠不止這么簡單
例如:
- 首先:我們先用加密工具例如:AES.Base64等把需要轉換的字符串先加密變成加密字符串
- 然后:在用異或轉換加密的腳本把加密字符串進行轉換(包括解密用的鑰匙串)
- 在使用的時候:先異或解密字符串度气,然后根據解密鑰匙串把字符串在轉為可用的字符串
ps.還有一種保護字符串的方法割按,就是使用NSLocalizedString字符串本地化。
雖然跟著我的步驟你確實加密成功了磷籍,但是你卻無法實際驗證适荣。所以要驗證最終的混淆結果是否達到效果,你還需要學習如何破殼解密IPA如何動態(tài)靜態(tài)逆向編程分析工程源碼院领,大家可以先看看我這篇文章弛矛。先掌握逆向分析后在來做代碼混淆,就能驗證混淆結果是否有效
變量比然、方法名丈氓,類名混淆
對于混淆這一塊,網上真的是千篇一律,基本都是copy的念大嬸的內容万俗,沒有一點自己的創(chuàng)新和思考湾笛。網上的方法我也用過,但是有缺陷该编,只能混淆方法名或者說自己固定的內容去替換迄本。第一不自動,對于大項目而言每個方法名自己添加课竣,太麻煩嘉赎。第二變量混淆有問題,因為只是單純的字符串替換于樟,用宏代替公条。當遇到使用_ 下劃線訪問變量時,就會出現(xiàn)錯誤迂曲。
對于變量靶橱、方法名,類名的混淆路捧,其實跟字符串混淆差不多关霸,都是加密混淆,然后解密混淆杰扫。不同的是队寇,變量、方法名章姓,類名的混淆目的是為了讓別人反編譯的時候不知道你的變量佳遣、方法,類是具體用來干什么的凡伊,不會想明文那樣一目了然零渐。增加逆向難度∠得Γ混淆的內容不需要想字符串一樣诵盼,最后程序運行時還要轉成中文正常使用。由于本人對shell腳本語言也不是非常熟悉笨觅,想要按照自己的思路寫一套完整的混淆腳本還不行拦耐。所以這部分也是在網上找的,算是目前最實用最完善的混淆
首先 打開終端cd到需要混淆的工程目錄下见剩,輸入
touch obConfusion.sh
(加密混淆腳本文件)
touch obDecrypt.sh
(解密混淆腳本文件)
生成2個腳本文件然后在工程目錄以外創(chuàng)建一個文件夾杀糯,用于保存加密時生成的加密文本內容,該內容會在解密是用到
最后是在
obConfusion.sh
和obDecrypt.sh
文件中加入腳本內容
下面是加密混淆腳本內容
#!/bin/sh
##################################
# (該腳本是在https://github.com/heqingliang/CodeObfus 上找到的)
# 代碼混淆腳本 heyujia 2018.03.15
#
##################################
#識別含有多字節(jié)編碼字符時遇到的解析沖突問題
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)
#第一個參數(shù)為項目路徑
if [[ $1 ]]
then
if [[ $1 != "_" ]]; then
ProjectPath=$1
fi
fi
#第二個參數(shù)指定密鑰文件路徑及文件名
if [[ $2 ]]
then
if [[ $2 != "_" ]]; then
SecretFile=$2
fi
fi
##############################################################################
#查找文本中所有要求混淆的屬性\方法\類,只會替換文本中ob_開頭和_fus結尾的字符串(區(qū)分大小寫歉铝,例如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;}
#隨機數(shù)生成函數(shù)
function random_int(min, max) {
return int( rand()*(max-min+1) ) + min;
}
#隨機字符串生成函數(shù)
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){
#重復字符串臼勉,直接刪除。所以不用擔心混淆內容過多餐弱,可能會出現(xiàn)重復的混淆字符串
x=substr(x, RSTART+RLENGTH);
continue;
}else{
#不是重復字符串宴霸,添加到替換數(shù)組
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
#
######################################
#識別含有多字節(jié)編碼字符時遇到的解析沖突問題
export LC_CTYPE=C
export LANG=C
#配置項:
#已經混淆的項目路徑
ProjectPath="/Users/xieyujia/Desktop/ios/學習項目/daimahunxiao"
#這個是文件路徑而不是目錄,是混淆的時候生成的文本文件路徑膏蚓,每次不一樣瓢谢。所以每次加密后,解密時需要更換路徑
SecretFile="/Users/xieyujia/Desktop/ios/學習項目/tihuan20180315_1456"
#第一個參數(shù)為項目路徑
if [[ $1 ]]
then
if [[ $1 != "_" ]]; then
ProjectPath=$1
fi
fi
#第二個參數(shù)指定密鑰文件路徑及文件名
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結尾的字符串(區(qū)分大小寫,例如oB就不會做混淆)破停,如果注釋內容有該類型的字符串翅楼,也會進行替換。對于使用 下劃線訪問的變量屬性真慢,不會有影響毅臊,一樣會替換成對應的混淆內容。
提供一個shell腳本學習的網站
代碼邏輯混淆
下一小節(jié)代碼邏輯混淆黑界,目前還在代碼邏輯混淆這塊還在攻破學習中管嬉,網上大多數(shù)方法不好或者過時無效。