iOS使用shell指令碼批量修改屬性

aron1992發表於2019-04-04

背景

公司需要做一系列的殼版本,殼版本如果內容雷同提交到App Store會有被拒絕的風險,除了我在上一篇文章中說道的在殼版本中注入混淆的程式碼,防止被蘋果檢測到內容太過雷同而導致稽核被拒絕。還有另一種可行的方法是批量修改原始檔中的類名、屬性、方法名稱等會在二進位制檔案中留下符號標記的資訊,繞過蘋果的機器稽核。
這篇文章介紹的是如何使用指令碼批量修改屬性名稱,後續還有系列的包括使用指令碼批量修改類名稱、方法名稱等資訊的文章。

結果

本文的Demo程式碼YTTInjectedContentKit

使用方法

  • 開啟測試工程

測試工程位於專案目錄下面的DevPods/InjectedContentKit/Example/目錄下,開啟InjectedContentKit.xcworkspace即可

  • 執行命令

在命令列中進入到專案目錄下面的DevPods/InjectedContentKit/Example/injectContentShell子目錄,在我的電腦對應的目錄為/Users/aron/git-repo/YTTInjectedContentKit/DevPods/InjectedContentKit/Example/injectContentShell,然後執行./RenameProperty.sh批量替換屬性

➜  injectContentShell git:(master) pwd
/Users/aron/git-repo/YTTInjectedContentKit/DevPods/InjectedContentKit/Example/injectContentShell
➜  injectContentShell git:(master) ./RenameProperty.sh 
檢測到配置檔案存在 /Users/aron/git-repo/YTTInjectedContentKit/DevPods/InjectedContentKit/Example/injectContentShell/RenameProperties.cfg
需處理原始碼目錄存在 /Users/aron/git-repo/YTTInjectedContentKit/DevPods/InjectedContentKit/Example/injectContentShell/../InjectedContentKit
檢測到配置檔案存在 /Users/aron/git
// 省略...
正在處理屬性 invitationCode.....
正在處理屬性 organizer.....
正在處理屬性 ruleCardBack.....
done.
複製程式碼

下面是執行指令碼替換了屬性的結果圖,指令碼把所有需要替換的屬性新增了abc字尾,當然依然是可以正常編譯執行的

替換結果圖
替換結果圖

分析

原理分析

objc程式碼中的類名、屬性、方法、原始檔路徑等資訊最終會被打包到二進位制檔案中,儲存在二進位制檔案中的.sym符號表段中,可以使用objdump -t命令檢視二進位制符號資訊,以下的命令把objdump -t的結果寫入到檔案InjectedContentKit_Example_Symbols中去。

objdump -t InjectedContentKit_Example > InjectedContentKit_Example_Symbols
複製程式碼

檔案的內容會很大,所以選擇了幾個代表性的內容說明:

0000000100026350 l    d  __TEXT,__text	__text
# 這裡儲存的是類原始檔的路徑符號資訊
0000000000000000 l    d  *UND*	/Users/aron/PuTaoWorkSpace/project/sscatch/DevPods/InjectedContentKit/InjectedContentKit/Classes/Composer/PubSearchDataComposer.h

# 這裡儲存的是屬性對應的var資訊
0000000000000000 l    d  *UND*	_OBJC_IVAR_$_TextCardItem._title
0000000000000000 l    d  *UND*	_OBJC_IVAR_$_TextCardItem._showReact
0000000000000000 l    d  *UND*	_OBJC_IVAR_$_TextCardItem._topChart
0000000000000000 l    d  *UND*	_OBJC_IVAR_$_TextCardItem._reaction

# 這裡儲存的是屬性資訊對應的getter方法資訊
00000001000264a0 l     F __TEXT,__text	-[TextCardItem title]
00000001000264c0 l     F __TEXT,__text	-[TextCardItem showReact]
00000001000264f0 l     F __TEXT,__text	-[TextCardItem topChart]
0000000100026510 l     F __TEXT,__text	-[TextCardItem setTopChart:]

# 這裡儲存的是屬性資訊對應的setter方法資訊
00000001000028a0 l     F __TEXT,__text	-[SSCatchInviteScheduler setOrganizer:]
00000001000028e0 l     F __TEXT,__text	-[SSCatchInviteScheduler setInputCardBack:]
0000000100002920 l     F __TEXT,__text	-[SSCatchInviteScheduler setInputTextBack:]

# 這裡儲存的是類檔案的檔名資訊
0000000000000000 l    d  *UND*	PubSearchDataComposer.m
000000005a937587 l    d  __TEXT,__stub_helper	__stub_helper
00000001000251c0 l    d  __TEXT,__text	__text
複製程式碼

從上面可以看出,二進位制中保留了很多資訊和原始碼有很大關係,我們做個簡單的猜測蘋果後臺機器審查二進位制的時候會通過二進位制中的符號進行對比,如果兩個二進位制(一個主版本、一個殼版本)程式碼中的符號重合度超過某個閾值,就會判定這是釋出殼版本的行為,而這是蘋果說不允許的,所以可行的方法是修改原始檔中的這些資訊來繞過蘋果的審查機制。

另外猜測蘋果應該是不會根據程式碼中的流程控制來判斷的,因為二進位制中的控制流程已經是機器碼了,反編譯出來也就是彙編程式碼,只要稍微做點改動二進位制(.text段)就會變化很大。所以從這個方面來判斷就難度很大了。

步驟分析

主要有以下幾個步驟

  1. 尋找到需要替換的原始檔中的所有的屬性,處理之後儲存在配置檔案中
  2. 使用者自定義一個黑名單配置檔案
  3. 某部分需要隔離的程式碼中的屬性生成黑名單配置檔案
  4. 把需要替換的原始檔中的所有匹配的屬性做批量的替換

這裡說明下為什麼第一步需要儲存在配置檔案中,因為第三步的操作有部分和第一步是相同的,所有這部分單獨出來一個模組共用,都是輸入一個資料夾,最終儲存在指定的檔案中,後面的程式碼中可以看到這部分。

實現

單步實現

1、尋找到需要替換的原始檔中的所有的屬性,處理之後儲存在配置檔案中

這一步的功能是客戶端輸入一個需要處理的原始碼資料夾,遞迴遍歷該原始碼資料夾獲取所有原始碼檔案(.h .m 檔案)。使用正則匹配找到屬性名稱,暫時儲存到陣列中,最後經過黑名單過濾、去重過濾、其他過濾條件過濾,最終把待處理的屬性儲存到客戶端輸入的輸出檔案中。

可以分解為一下幾個小步驟

  • 遞迴遍歷資料夾獲取原始碼檔案
  • 正則匹配原始碼檔案的屬性
  • 過濾屬性(可選)
  • 儲存屬性到檔案

這部分功能的原始碼如下:
檔名: GetAndStoreProperties.sh
該指令碼在多個地方都有用到,所以作為一個單獨的模組,定義了一些引數,以適應不同的應用場景。在下面可以看到使用該指令碼的地方。

#!/bin/bash
########################
# 指令碼功能:從指定目錄獲取和儲存屬性到指定的檔案
# 輸入引數 -i 輸入的資料夾
# 輸入引數 -o 儲存的檔案
# 輸入引數 -f 使用黑名單和自定義過濾條件的引數
# 輸入引數 -c 自定義的黑名單檔案
########################

####### 引數定義
param_input_dir=""
param_output_file=""
param_custom_filter_file=""
param_should_use_filter=0

####### 引數解析
while getopts :i:o:c:f opt
do
	case "$opt" in
		i) param_input_dir=$OPTARG
			echo "Found the -i option, with parameter value $OPTARG"
			;;
		o) param_output_file=$OPTARG
			echo "Found the -o option, with parameter value $OPTARG"
			;;
		c) param_custom_filter_file=$OPTARG
			echo "Found the -c option, with parameter value $OPTARG"
			;;
		f) echo "Found the -f option" 
			param_should_use_filter=1
			;;
		*) echo "Unknown option: $opt";;
	esac
done


####### 配置

# 屬性黑名單配置檔案
blacklist_cfg_file="$(pwd)/DefaultBlackListPropertiesConfig.cfg"

####### 資料定義

# 定義儲存原始檔的陣列
declare -a implement_source_file_array
implement_source_file_count=0


# 定義儲存屬性的陣列
declare -a tmp_props_array
props_count=0


# mark: p384
# 遞迴函式讀取目錄下的所有.m檔案
function read_source_file_recursively {
	echo "read_implement_file_recursively"
	if [[ -d $1 ]]; then
		for item in $(ls $1); do
			itemPath="$1/${item}"
			if [[ -d $itemPath ]]; then
				# 目錄
				echo "處理目錄 ${itemPath}"
				read_source_file_recursively $itemPath
				echo "處理目錄結束====="
			else 
				# 檔案
				echo "處理檔案 ${itemPath}"
				if [[ $(expr "$item" : '.*\.m') -gt 0 ]] || [[ $(expr "$item" : '.*\.h') -gt 0 ]]; then
					echo ">>>>>>>>>>>>mmmmmmm"
					implement_source_file_array[$implement_source_file_count]=${itemPath}
					implement_source_file_count=$[ implement_source_file_count + 1 ];
				fi
				echo ""
			fi
		done
	else
		echo "err:不是一個目錄"
	fi
}


# 讀取原始碼中的屬性,儲存到陣列中
# 引數一: 原始碼檔案路徑
function get_properties_from_source_file {
	local class_file=$1;
	echo "class_file=${class_file}"

	properties=$(grep "@property.*" ${class_file})
	IFS_OLD=$IFS
	IFS=$'\n'
	for prop_line in $properties; do
		echo ">>>>>${prop_line}"

		asterisk_seperator_pattern="\*"
		if [[ ${prop_line} =~ ${asterisk_seperator_pattern} ]]; then
			# 從左向右擷取最後一個string後的字串
			prop_name=${prop_line##*${asterisk_seperator_pattern}}
			# 從左向右擷取第一個string後的字串
			seal_pattern=";*"
			seal_pattern_replacement=""
			prop_name=${prop_name//${seal_pattern}/${seal_pattern_replacement}}
			subsring_pattern="[ |;]"
			replacement=""
			prop_name=${prop_name//${subsring_pattern}/${replacement}}

			if [[ ${param_should_use_filter} -gt 0 ]]; then
				grep_result=$(grep ${prop_name} ${blacklist_cfg_file})
				echo "grep_result = >>${grep_result}<<"
				custom_grep_result=""
				if [[ -n ${param_custom_filter_file} ]]; then
					custom_grep_result=$(grep ${prop_name} ${param_custom_filter_file})
				fi
				if [[ -n ${grep_result} ]] || [[ -n ${custom_grep_result} ]]; then
					echo "--${prop_name}--存在配置檔案中"
				else
					echo "--${prop_name}--XXX不存在配置檔案中"

					tmp_props_array[$props_count]=$prop_name
					props_count=$[ props_count + 1 ]
					echo ">>>>>>>result_prop_name=${prop_name}"
				fi
			else
				tmp_props_array[$props_count]=$prop_name
				props_count=$[ props_count + 1 ]
			fi			
		fi
	done
	IFS=$IFS_OLD
}

# 獲取目錄下的所有原始檔,讀取其中的屬性
function get_properties_from_source_dir {

	local l_classed_folder=$1

	echo "獲取需要處理的原始檔... ${l_classed_folder}"
	# 讀取需要處理目標檔案
	read_source_file_recursively ${l_classed_folder}

	echo "讀取原始檔中的屬性..."
	for(( i=0;i<${#implement_source_file_array[@]};i++)) 
	do 
		class_file=${implement_source_file_array[i]}; 
		echo "處理原始檔:${class_file}"
		get_properties_from_source_file ${class_file}
	done;
}

# 把獲取到的屬性過濾之後寫入檔案中
# 過濾步驟包含去重、去掉簡單詞彙、去掉長度少於多少的詞彙
# 如果在執行的過程中遇到特殊情況,新增到黑名單配置(DefaultBlackListPropertiesConfig.cfg檔案中新增配置)
function post_get_properties_handle {

	local prop_config_file=$1

	# 寫入檔案中
	echo "# Properties Configs" > ${prop_config_file}
	for key in $(echo ${!tmp_props_array[*]})
	do
	    # echo "$key : ${tmp_props_array[$key]}"
	    echo ${tmp_props_array[$key]} >> ${prop_config_file}
	done

	# 去重
	cfg_back_file="${prop_config_file}.bak"
	mv ${prop_config_file} ${cfg_back_file}
	sort ${cfg_back_file} | uniq > ${prop_config_file}
	
	# 過濾
	if [[ ${param_should_use_filter} -gt 0 ]]; then
		mv ${prop_config_file} ${cfg_back_file}
		echo "# Properties Configs Filtered" > ${prop_config_file}
		IFS_OLD=$IFS
		IFS=$'\n'
		# 上一行的內容
		lastLine="";
		for line in $(cat ${cfg_back_file} | sed 's/^[ \t]*//g')
		do
			if [[ ${#line} -le 6 ]] || [[ $(expr "$line" : '^#.*') -gt 0 ]]; then
				# 長度小於等於6或者註釋內容的行不處理
				echo "less then 6 char line or comment line"
			else
				if [[ -n ${lastLine} ]]; then
					# 上一行是非空白行
					# 比較上一行內容是否是當前行的一部分,不是新增上一行
					if [[ ${line} =~ ${lastLine} ]]; then
						echo "${line}${lastLine} 有交集"
					else
						echo ${lastLine} >> ${prop_config_file}
					fi
				fi
				# 更新上一行
				lastLine=${line}
			fi	
		done
		IFS=${IFS_OLD}
	fi

	# 刪除臨時檔案
	rm -f ${cfg_back_file}
}


get_properties_from_source_dir ${param_input_dir}
post_get_properties_handle ${param_output_file}

複製程式碼

使用以上指令碼生成的配置檔案 PropertiesConfigs.cfg 部分如下:

# Properties Configs Filtered
UserRestrictionLabel
aboutusButton
activitySamplers
addAddressPress
addressSamplers
addressTextBox
appealPress
appliedGroupedSamplers
appliedSamplers
applyPress
asyncArray
asyncListSampler
audioPlayer
複製程式碼

2. 使用者自定義一個黑名單配置檔案

在實踐的過程中,替換屬性的符號有時候會把系統類的屬性替換了,比如

  • AppDelegate 中的 window 屬性替換了,導致了編譯連結沒錯,但是介面出不來了,因為初始的window物件找不到了
  • UIButton 中的 titleLabel 屬性替換了,直接導致了編譯出錯

對於這類問題,需要在黑名單中配置一些預設的過濾屬性,對於黑名單中的這些屬性不處理即可,在我的業務場景下,黑名單檔案的配置如下:

檔名:DefaultBlackListPropertiesConfig.cfg

# BlackListPropertiesConfig.cfg
# 屬性黑名單配置,在此配置檔案中的屬性不需要替換名稱
window
name
title
titleLabel
layout
appealSamplers
複製程式碼

GetAndStoreProperties.sh 指令碼使用到的程式碼片段如下,其實就是使用了 grep 命來查詢,判斷時候有找到,如果有就不處理,具體的可以看上面提供的完整的 GetAndStoreProperties.sh 指令碼程式碼

if [[ ${param_should_use_filter} -gt 0 ]]; then
	grep_result=$(grep ${prop_name} ${blacklist_cfg_file})
	echo "grep_result = >>${grep_result}<<"
	custom_grep_result=""
	if [[ -n ${param_custom_filter_file} ]]; then
		custom_grep_result=$(grep ${prop_name} ${param_custom_filter_file})
	fi
	if [[ -n ${grep_result} ]] || [[ -n ${custom_grep_result} ]]; then
		echo "--${prop_name}--存在配置檔案中"
	else
		echo "--${prop_name}--XXX不存在配置檔案中"

		tmp_props_array[$props_count]=$prop_name
		props_count=$[ props_count + 1 ]
		echo ">>>>>>>result_prop_name=${prop_name}"
	fi
else
	tmp_props_array[$props_count]=$prop_name
	props_count=$[ props_count + 1 ]
fi	
複製程式碼

3. 某部分需要隔離的程式碼中的屬性生成黑名單配置檔案

這部分的功能其實就是呼叫 GetAndStoreProperties.sh 這個指令碼,最終把檔案輸出的檔案以追加的方式寫入到使用者自定義的黑名單屬性檔案中。

#...
# 黑名單類目錄
declare -a custom_blacklist_search_dirs
custom_blacklist_search_dirs=("/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/SSCatchAPI" 
	"/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Categories" 
	"/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Components" 
	"/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/External" 
	"/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/HandyTools" 
	"/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Macros" )
# ...

# 屬性黑名單配置檔案
custom_blacklist_cfg_file="$(pwd)/CustomBlackListPropertiesConfig.cfg"

# ...
# 獲取自定義的黑名單屬性並儲存到檔案中
echo "" > ${custom_blacklist_cfg_file}
for (( i = 0; i < ${#custom_blacklist_search_dirs[@]}; i++ )); do
	custom_blacklist_search_dir=${custom_blacklist_search_dirs[${i}]}
	./GetAndStoreProperties.sh \
		-i ${custom_blacklist_search_dir}\
		-o ${custom_blacklist_cfg_tmp_file}
	cat ${custom_blacklist_cfg_tmp_file} >> ${custom_blacklist_cfg_file}
done
#...
複製程式碼

最終生成的使用者自定義的黑名單檔案部分如下
檔案:CustomBlackListPropertiesConfig.cfg

# Properties Configs
DBFilePath
ValidityString
accessQueue
age
attributedNameString
avatarURLString
avatarUrlString
backColorString
bodyScheduler
bodyView
catchDateString
cellHeight
channelKey
cityName
conditionString
# ....
複製程式碼

4. 把需要替換的原始檔中的所有匹配的屬性做批量的替換

這一步在前面三部的基礎上,查詢並替換原始碼目錄中在 PropertiesConfigs.cfg 配置檔案中出現的屬性和屬性的引用,查詢使用grep命令、替換使用了sed命令。指令碼程式碼如下

#!/bin/bash
# 屬性重新命名指令碼

####### 配置
# classes類目錄
classes_dir="$(pwd)/../InjectedContentKitx"
# 黑名單類目錄
declare -a custom_blacklist_search_dirs
custom_blacklist_search_dirs=("/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/SSCatchAPI" 
	"/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Categories" 
	"/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Components" 
	"/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/External" 
	"/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/HandyTools" 
	"/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Macros" )
# 配置檔案
cfg_file="$(pwd)/PropertiesConfigs.cfg"
# 屬性黑名單配置檔案
blacklist_cfg_file="$(pwd)/DefaultBlackListPropertiesConfig.cfg"
# 屬性黑名單配置檔案
custom_blacklist_cfg_file="$(pwd)/CustomBlackListPropertiesConfig.cfg"
custom_blacklist_cfg_tmp_file="$(pwd)/TmpCustomBlackListPropertiesConfig.cfg"
# 屬性字首,屬性字首需要特殊處理
class_prefix=""
# 屬性字尾
class_suffix="abc"


# 檢測檔案是否存在,不存在則建立
checkOrCreateFile() {
	file=$1
	if [[ -f $file ]]; then
		echo "檢測到配置檔案存在 $file"
	else
		echo "建立配置檔案 $file"
		touch $file
	fi
}

# 配置檔案檢查
checkOrCreateFile $cfg_file

# 迴圈檢測輸入的資料夾
function checkInputDestDir {
	echo -n "請輸入需處理原始碼目錄: "
	read path
	if [[ -d $path ]]; then
		classes_dir=$path
	else
		echo -n "輸入的目錄無效,"
		checkInputDestDir
	fi
}

# 需處理原始碼目錄檢查
if [[ -d $classes_dir ]]; then
	echo "需處理原始碼目錄存在 $classes_dir"
else
	echo "請確認需處理原始碼目錄是否存在 $classes_dir"
	checkInputDestDir
fi


####### 資料定義

# 定義屬性儲存陣列
declare -a rename_properties_config_content_array
cfg_line_count=0


# 讀取屬性配置檔案
function read_rename_properties_configs {
	IFS_OLD=$IFS
	IFS=$'\n'
	# 刪除檔案行首的空白字元 http://www.jb51.net/article/57972.htm
	for line in $(cat $cfg_file | sed 's/^[ \t]*//g')
	do
		is_comment=$(expr "$line" : '^#.*')
		echo "line=${line} is_common=${is_comment}"
		if [[ ${#line} -eq 0 ]] || [[ $(expr "$line" : '^#.*') -gt 0 ]]; then
			echo "blank line or comment line"
		else
			rename_properties_config_content_array[$cfg_line_count]=$line
			cfg_line_count=$[ $cfg_line_count + 1 ]
			# echo "line>>>>${line}"
		fi	
	done
	IFS=${IFS_OLD}
}

function print_array {
	# 獲取陣列
	local newarray
	newarray=($(echo "$@"))
	for (( i = 0; i < ${#newarray[@]}; i++ )); do
		item=${newarray[$i]}
		echo "array item >>> ${item}"
	done
}

# 重新命名所有的屬性
function rename_properties {

	# 讀取屬性配置檔案
	read_rename_properties_configs
	# print_array ${rename_properties_config_content_array[*]}

	# 執行替換操作
	for (( i = 0; i < ${#rename_properties_config_content_array[@]}; i++ )); do
		original_prop_name=${rename_properties_config_content_array[i]};
		result_prop_name="${class_prefix}${original_prop_name}${class_suffix}"
		sed -i '{
			s/'"${original_prop_name}"'/'"${result_prop_name}"'/g
		}' `grep ${original_prop_name} -rl ${classes_dir}`
		echo "正在處理屬性 ${original_prop_name}....."
	done
}

checkOrCreateFile ${custom_blacklist_cfg_tmp_file}

# 獲取自定義的黑名單屬性並儲存到檔案中
echo "" > ${custom_blacklist_cfg_file}
for (( i = 0; i < ${#custom_blacklist_search_dirs[@]}; i++ )); do
	custom_blacklist_search_dir=${custom_blacklist_search_dirs[${i}]}
	./GetAndStoreProperties.sh \
		-i ${custom_blacklist_search_dir}\
		-o ${custom_blacklist_cfg_tmp_file}
	cat ${custom_blacklist_cfg_tmp_file} >> ${custom_blacklist_cfg_file}
done


# 獲取和儲存屬性到熟悉配置檔案
./GetAndStoreProperties.sh \
	-i ${classes_dir}\
	-o ${cfg_file}\
	-f \
	-c ${custom_blacklist_cfg_file}


# 執行屬性重新命名
rename_properties

echo "done."

複製程式碼

總結

以上就是基於shell指令碼,以殼版本為場景,把屬性的批量替換做了一個半自動化的實現步驟,如果不妥之處,還請不吝賜教。

相關文章