首页 > 系统 > iOS > 正文

iOS通过shell脚本批量修改属性

2020-07-26 02:29:35
字体:
来源:转载
供稿:网友

背景

公司需要做一系列的壳版本,壳版本如果内容雷同提交到App Store会有被拒绝的风险,除了我在上一篇文章中说道的在壳版本中注入混淆的代码,防止被苹果检测到内容太过雷同而导致审核被拒绝。还有另一种可行的方法是批量修改源文件中的类名、属性、方法名称等会在二进制文件中留下符号标记的信息,绕过苹果的机器审核。
这篇文章介绍的是如何使用脚本批量修改属性名称,后续还有系列的包括使用脚本批量修改类名称、方法名称等信息的文章。

结果

下面是执行脚本替换了属性的结果图,脚本把所有需要替换的属性添加了abc后缀,当然依然是可以正常编译运行的

源码:https://gitee.com/dhar/YTTInjectedContentKit

分析

原理分析

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._title0000000000000000 l d *UND*	_OBJC_IVAR_$_TextCardItem._showReact0000000000000000 l d *UND*	_OBJC_IVAR_$_TextCardItem._topChart0000000000000000 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.m000000005a937587 l d __TEXT,__stub_helper	__stub_helper00000001000251c0 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 optdo	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";;	esacdone####### 配置# 属性黑名单配置文件blacklist_cfg_file="$(pwd)/DefaultBlackListPropertiesConfig.cfg"####### 数据定义# 定义保存源文件的数组declare -a implement_source_file_arrayimplement_source_file_count=0# 定义保存属性的数组declare -a tmp_props_arrayprops_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 FilteredUserRestrictionLabelaboutusButtonactivitySamplersaddAddressPressaddressSamplersaddressTextBoxappealPressappliedGroupedSamplersappliedSamplersapplyPressasyncArrayasyncListSampleraudioPlayer

2. 用户自定义一个黑名单配置文件

在实践的过程中,替换属性的符号有时候会把系统类的属性替换了,比如

  • 把 AppDelegate 中的 window 属性替换了,导致了编译链接没错,但是界面出不来了,因为初始的window对象找不到了
  • 把 UIButton 中的 titleLabel 属性替换了,直接导致了编译出错

对于这类问题,需要在黑名单中配置一些默认的过滤属性,对于黑名单中的这些属性不处理即可,在我的业务场景下,黑名单文件的配置如下:

文件名:DefaultBlackListPropertiesConfig.cfg

# BlackListPropertiesConfig.cfg# 属性黑名单配置,在此配置文件中的属性不需要替换名称windownametitletitleLabellayoutappealSamplers

在 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}"	fielse	tmp_props_array[$props_count]=$prop_name	props_count=$[ props_count + 1 ]fi	

3. 某部分需要隔离的代码中的属性生成黑名单配置文件

这部分的功能其实就是调用 GetAndStoreProperties.sh 这个脚本,最终把文件输出的文件以追加的方式写入到用户自定义的黑名单属性文件中。

#...# 黑名单类目录declare -a custom_blacklist_search_dirscustom_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 ConfigsDBFilePathValidityStringaccessQueueageattributedNameStringavatarURLStringavatarUrlStringbackColorStringbodySchedulerbodyViewcatchDateStringcellHeightchannelKeycityNameconditionString# ....

4. 把需要替换的源文件中的所有匹配的属性做批量的替换

这一步在前面三部的基础上,查找并替换源码目录中在 PropertiesConfigs.cfg 配置文件中出现的属性和属性的引用,查找使用grep命令、替换使用了sed命令。脚本代码如下

#!/bin/bash# 属性重命名脚本####### 配置# classes类目录classes_dir="$(pwd)/../InjectedContentKitx"# 黑名单类目录declare -a custom_blacklist_search_dirscustom_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"	checkInputDestDirfi####### 数据定义# 定义属性保存数组declare -a rename_properties_config_content_arraycfg_line_count=0# 读取属性配置文件function read_rename_properties_configs {	IFS_OLD=$IFS	IFS=$'/n'	# 删除文件行首的空白字符 https://www.VeVB.COm/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_propertiesecho "done."

总结

以上就是基于shell脚本,以壳版本为场景,把属性的批量替换做了一个半自动化的实现步骤,如果不妥之处,还请不吝赐教。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表