iOS uses shell script to batch modify properties

background

The company needs to make a series of shell versions. If the shell versions are submitted to the App Store with the same content, there will be a risk of being rejected, except that I said in the previous article that the obfuscated code is injected into the shell version to prevent the content from being detected by Apple. Too similar and the review will be rejected. Another feasible method is to batch modify the class names, properties, method names, etc. in the source file, which will leave symbolic information in the binary file, bypassing Apple's machine audit.
This article describes how to use scripts to modify property names in batches, and there will be a series of articles that include using scripts to batch modify class names, method names and other information.

A series of articles on shell combat
iOS uses shell scripts to inject obfuscated content iOS uses shell
scripts to batch modify class names

result

Instructions

  • Open test project

The test project is located in the directory under the project directory , just DevPods/InjectedContentKit/Example/open itInjectedContentKit.xcworkspace

  • Excuting an order

Go to the subdirectory under the project directory on the command line, DevPods/InjectedContentKit/Example/injectContentShellthe corresponding directory on my computer is /Users/aron/git-repo/YTTInjectedContentKit/DevPods/InjectedContentKit/Example/injectContentShell, and then execute the ./RenameProperty.shbatch replacement attribute

➜  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.

The following is the result of executing the script to replace the attributes. The script adds abc suffix to all the attributes that need to be replaced. Of course, it can still be compiled and run normally.

The demo code of this article YTTInjectedContentKit

Replace the resulting graph

analyze

Principle analysis

The class name, attribute, method, source file path and other information in the objc code will eventually be packaged into the binary file and saved in the .sym symbol table segment in the binary file. You can use the objdump -tcommand to view the binary symbol information. The following command puts objdump -tThe result is written to the file InjectedContentKit_Example_Symbols.

objdump -t InjectedContentKit_Example > InjectedContentKit_Example_Symbols

The content of the file will be large, so a few representative content descriptions have been selected:

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

As can be seen from the above, a lot of information retained in the binary has a lot to do with the source code. Let's make a simple guess that Apple's background machine will compare the symbols in the binary when reviewing the binary. If two binary (one main version) , a shell version) the symbol coincidence in the code exceeds a certain threshold, it will be determined that this is the behavior of publishing the shell version, which is not allowed by Apple, so the feasible method is to modify the information in the source file to circumvent the through Apple's censorship mechanism.

In addition, it is guessed that Apple should not judge based on the flow control in the code, because the control flow in the binary is already machine code, and the decompiled code is the assembly code, as long as you make a little change to the binary (.text section), it will be dramatic change. So it is very difficult to judge from this aspect.

step analysis

There are mainly the following steps

  1. Find all the attributes in the source file that need to be replaced, and save them in the configuration file after processing
  2. User-defined blacklist configuration file
  3. Attributes in a certain part of the code that need to be isolated generate a blacklist configuration file
  4. Batch replace all matching attributes in the source file that needs to be replaced

Here is why the first step needs to be saved in the configuration file, because some of the operations in the third step are the same as the first step. All these parts are shared by a separate module, which is input into a folder, and finally saved in the specified file, you can see this part in the code behind.

accomplish

single-step implementation

1. Find all the attributes in the source file that need to be replaced, and save them in the configuration file after processing

The function of this step is that the client inputs a source code folder to be processed, and recursively traverses the source code folder to obtain all source code files (.h .m files). Use regular matching to find the attribute name, temporarily save it in the array, and finally filter the blacklist, deduplication, and other filtering conditions, and finally save the to-be-processed attribute to the output file input by the client.

It can be broken down into a few small steps

  • Recursively traverse folders to get source files
  • Regularly match attributes of source files
  • filter attribute (optional)
  • save properties to file

The source code of this part of the function is as follows:
File name: GetAndStoreProperties.sh
This script is used in many places, so as a separate module, some parameters are defined to adapt to different application scenarios. Below you can see where the script is used.

#!/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}

The PropertiesConfigs.cfgsection as follows:

# Properties Configs Filtered
UserRestrictionLabel
aboutusButton
activitySamplers
addAddressPress
addressSamplers
addressTextBox
appealPress
appliedGroupedSamplers
appliedSamplers
applyPress
asyncArray
asyncListSampler
audioPlayer

2. User defines a blacklist configuration file

In practice, the symbols that replace the attributes sometimes replace the attributes of the system class, such as

  • AppDelegateReplacing the windowattribute in , resulting in the compilation link is correct, but the interface cannot come out, because the initial window object cannot be found
  • UIButtonReplaced the titleLabelattribute in , which directly caused a compilation error

For such problems, it is necessary to configure some default filtering attributes in the blacklist, and these attributes in the blacklist can be ignored. In my business scenario, the configuration of the blacklist file is as follows:

File name: DefaultBlackListPropertiesConfig.cfg

# BlackListPropertiesConfig.cfg
# 属性黑名单配置,在此配置文件中的属性不需要替换名称
window
name
title
titleLabel
layout
appealSamplers

The code snippets used in the GetAndStoreProperties.shscript are as follows. In fact, the grepcommand is used to search. If it is found when judging, it will not be processed. For details, please refer to the complete GetAndStoreProperties.shscript

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. Properties in a certain part of the code that need to be isolated generate a blacklist configuration file

The function of this part is actually to call GetAndStoreProperties.shthis script, and finally write the output file of the file to the user-defined blacklist attribute file by appending.

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

The final generated user-defined blacklist file is as follows
: CustomBlackListPropertiesConfig.cfg

# Properties Configs
DBFilePath
ValidityString
accessQueue
age
attributedNameString
avatarURLString
avatarUrlString
backColorString
bodyScheduler
bodyView
catchDateString
cellHeight
channelKey
cityName
conditionString
# ....

4. Batch replace all matching attributes in the source file to be replaced

In this step, on the basis of the previous three parts, find and replace the attributes and attribute references that appear in the PropertiesConfigs.cfgconfiguration find and use grepcommands, and replace the used sedcommands. The script code is as follows

#!/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."

Summarize

The above is based on the shell script, taking the shell version as the scenario, and making a semi-automatic implementation step for batch replacement of attributes.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324921498&siteId=291194637