歡迎光臨
每天分享高質量文章

iOS-代碼混淆加固策略

作者:樹下敲代碼的超人

鏈接:https://www.jianshu.com/p/628a0c232c2a


對於IOS來說,由於系統是封閉的,APP上架需要通過App Store,安全性來說相當高。但是對於大廠和知名APP而言,別人給的安全保障永遠沒有自己做的來得踏實。所以對於大廠、少部分企業級和金融支付類應用來說加固是相當重要的。


下麵是目前幾個專業加固大廠提供的加固策略


  • 網易

網易

網易安全三板斧:


1、第一板斧是防靜態分析,這裡包括字串加密、符號混淆、代碼邏輯混淆和游戲存檔加密;

2、第二板斧是防動態除錯、反除錯和通信安全(資料加密);

3、第三板斧是外掛檢測、加速掛、記憶體修改掛和自動任務掛等


  • 愛加密

愛加密

  • safengine

afengine

  • 幾維安全

幾維安全

  • 梆梆安全

梆梆安全


本文將針對以上幾點進行實現,對於一些不太容易實現的將會做方向性討論


  • 字串加密

  • 代碼混淆(方法命,類命,變數名,符號表)

  • 代碼邏輯混淆

  • 反除錯


字串加密


對字串加密的方式目前我所瞭解到掌握到的最可靠方式就是用腳本將代碼中的所有標記需要加密的字串進行異或轉換,這樣代碼中就不存在明文字串了。當然第三方的字串加密不可能這麼簡單,具體怎麼做的我也不太清楚。不過為了增加字串加密的難度複雜性,我們可以先將字串用加密工具轉換(例如AES、base64等)後的把加字串放在工程中,並且把解密的鑰匙放在工程中,用異或轉換,把解密鑰匙和加密後的字串轉換,這樣就有2層保障,增加了複雜度。


  • 首先 我們創建任意一個工程,在工程中寫入下麵的代碼,併在每句打上斷點,再選擇Xcode工具欄的Debug –> Debug Workflow –> Always Show Disassembly。這樣你就可以在斷點處進入彙編樣式界面,最後運行程式

斷點處進入彙編樣式界面

你會發現,你的字串內容暴露在了彙編樣式中,這會導致別人在逆向分析你的工程時能看見你的字串內容,我們一般接口、域名、加解密鑰匙串、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) + ''
    replaced_string = '((char []) {' + ', '.join(["%i" % ((ord(c) ^ 0xAAif c != '' else 0for 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 ‘è’ in file confusion.py on line 2相關的錯,請確定腳本的前面3行是


#!/usr/bin/env python
# encoding=utf8
# -*- coding: utf-8 -*-

必須有這三行代碼,才能在腳本中輸入中文
(3.如果報IndentationError: unexpected indent,請註意腳本中的每行代碼的換行符和縮進格式必須標準


  • 執行完步驟6後的結果


執行完不步驟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 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; iresult = 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 if (strarr[i] == tempstr){break;}
}
if(i#重覆字串,直接刪除。所以不用擔心混淆內容過多,可能會出現重覆的混淆字串
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腳本學習的網站

代碼邏輯混淆


下一小節代碼邏輯混淆,目前還在代碼邏輯混淆這塊還在攻破學習中,網上大多數方法不好或者過時無效。



編號285,輸入編號直達本文

●輸入m獲取文章目錄

推薦↓↓↓

Web開發

更多推薦18個技術類微信公眾號

涵蓋:程式人生、演算法與資料結構、黑客技術與網絡安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

赞(0)

分享創造快樂