查看: 208|回复: 0

[IOS开发教程] iOS通过shell脚本批量修改属性

发表于 6 天前

背景

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

结果

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

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

分析

原理分析

objc代码中的类名、属性、方法、源文件路径等信息最终会被打包到二进制文件中,保存在二进制文件中的.sym符号表段中,可以使用objdump -t命令查看二进制符号信息,以下的命令把objdump -t的结果写入到文件InjectedContentKit_Example_Symbols中去。

  1. objdump -t InjectedContentKit_Example > InjectedContentKit_Example_Symbols
复制代码

文件的内容会很大,所以选择了几个代表性的内容说明:

  1. 0000000100026350 l d __TEXT,__text __text
  2. # 这里保存的是类源文件的路径符号信息
  3. 0000000000000000 l d *UND* /Users/aron/PuTaoWorkSpace/project/sscatch/DevPods/InjectedContentKit/InjectedContentKit/Classes/Composer/PubSearchDataComposer.h
  4. # 这里保存的是属性对应的var信息
  5. 0000000000000000 l d *UND* _OBJC_IVAR_$_TextCardItem._title
  6. 0000000000000000 l d *UND* _OBJC_IVAR_$_TextCardItem._showReact
  7. 0000000000000000 l d *UND* _OBJC_IVAR_$_TextCardItem._topChart
  8. 0000000000000000 l d *UND* _OBJC_IVAR_$_TextCardItem._reaction
  9. # 这里保存的是属性信息对应的getter方法信息
  10. 00000001000264a0 l F __TEXT,__text -[TextCardItem title]
  11. 00000001000264c0 l F __TEXT,__text -[TextCardItem showReact]
  12. 00000001000264f0 l F __TEXT,__text -[TextCardItem topChart]
  13. 0000000100026510 l F __TEXT,__text -[TextCardItem setTopChart:]
  14. # 这里保存的是属性信息对应的setter方法信息
  15. 00000001000028a0 l F __TEXT,__text -[SSCatchInviteScheduler setOrganizer:]
  16. 00000001000028e0 l F __TEXT,__text -[SSCatchInviteScheduler setInputCardBack:]
  17. 0000000100002920 l F __TEXT,__text -[SSCatchInviteScheduler setInputTextBack:]
  18. # 这里保存的是类文件的文件名信息
  19. 0000000000000000 l d *UND* PubSearchDataComposer.m
  20. 000000005a937587 l d __TEXT,__stub_helper __stub_helper
  21. 00000001000251c0 l d __TEXT,__text __text
复制代码

从上面可以看出,二进制中保留了很多信息和源代码有很大关系,我们做个简单的猜测苹果后台机器审查二进制的时候会通过二进制中的符号进行对比,如果两个二进制(一个主版本、一个壳版本)代码中的符号重合度超过某个阈值,就会判定这是发布壳版本的行为,而这是苹果说不允许的,所以可行的方法是修改源文件中的这些信息来绕过苹果的审查机制。

另外猜测苹果应该是不会根据代码中的流程控制来判断的,因为二进制中的控制流程已经是机器码了,反编译出来也就是汇编代码,只要稍微做点改动二进制(.text段)就会变化很大。所以从这个方面来判断就难度很大了。

步骤分析

主要有以下几个步骤

  1. 寻找到需要替换的源文件中的所有的属性,处理之后保存在配置文件中
  2. 用户自定义一个黑名单配置文件
  3. 某部分需要隔离的代码中的属性生成黑名单配置文件
  4. 把需要替换的源文件中的所有匹配的属性做批量的替换

这里说明下为什么第一步需要保存在配置文件中,因为第三步的操作有部分和第一步是相同的,所有这部分单独出来一个模块共用,都是输入一个文件夹,最终保存在指定的文件中,后面的代码中可以看到这部分。

实现

单步实现

1、寻找到需要替换的源文件中的所有的属性,处理之后保存在配置文件中

这一步的功能是客户端输入一个需要处理的源码文件夹,递归遍历该源码文件夹获取所有源码文件(.h .m 文件)。使用正则匹配找到属性名称,暂时保存到数组中,最后经过黑名单过滤、去重过滤、其他过滤条件过滤,最终把待处理的属性保存到客户端输入的输出文件中。

可以分解为一下几个小步骤

  • 递归遍历文件夹获取源码文件
  • 正则匹配源码文件的属性
  • 过滤属性(可选)
  • 保存属性到文件

这部分功能的源码如下:

文件名: GetAndStoreProperties.sh

该脚本在多个地方都有用到,所以作为一个单独的模块,定义了一些参数,以适应不同的应用场景。在下面可以看到使用该脚本的地方。

  1. #!/bin/bash
  2. ########################
  3. # 脚本功能:从指定目录获取和保存属性到指定的文件
  4. # 输入参数 -i 输入的文件夹
  5. # 输入参数 -o 保存的文件
  6. # 输入参数 -f 使用黑名单和自定义过滤条件的参数
  7. # 输入参数 -c 自定义的黑名单文件
  8. ########################
  9. ####### 参数定义
  10. param_input_dir=""
  11. param_output_file=""
  12. param_custom_filter_file=""
  13. param_should_use_filter=0
  14. ####### 参数解析
  15. while getopts :i:o:c:f opt
  16. do
  17. case "$opt" in
  18. i) param_input_dir=$OPTARG
  19. echo "Found the -i option, with parameter value $OPTARG"
  20. ;;
  21. o) param_output_file=$OPTARG
  22. echo "Found the -o option, with parameter value $OPTARG"
  23. ;;
  24. c) param_custom_filter_file=$OPTARG
  25. echo "Found the -c option, with parameter value $OPTARG"
  26. ;;
  27. f) echo "Found the -f option"
  28. param_should_use_filter=1
  29. ;;
  30. *) echo "Unknown option: $opt";;
  31. esac
  32. done
  33. ####### 配置
  34. # 属性黑名单配置文件
  35. blacklist_cfg_file="$(pwd)/DefaultBlackListPropertiesConfig.cfg"
  36. ####### 数据定义
  37. # 定义保存源文件的数组
  38. declare -a implement_source_file_array
  39. implement_source_file_count=0
  40. # 定义保存属性的数组
  41. declare -a tmp_props_array
  42. props_count=0
  43. # mark: p384
  44. # 递归函数读取目录下的所有.m文件
  45. function read_source_file_recursively {
  46. echo "read_implement_file_recursively"
  47. if [[ -d $1 ]]; then
  48. for item in $(ls $1); do
  49. itemPath="$1/${item}"
  50. if [[ -d $itemPath ]]; then
  51. # 目录
  52. echo "处理目录 ${itemPath}"
  53. read_source_file_recursively $itemPath
  54. echo "处理目录结束====="
  55. else
  56. # 文件
  57. echo "处理文件 ${itemPath}"
  58. if [[ $(expr "$item" : '.*\.m') -gt 0 ]] || [[ $(expr "$item" : '.*\.h') -gt 0 ]]; then
  59. echo ">>>>>>>>>>>>mmmmmmm"
  60. implement_source_file_array[$implement_source_file_count]=${itemPath}
  61. implement_source_file_count=$[ implement_source_file_count + 1 ];
  62. fi
  63. echo ""
  64. fi
  65. done
  66. else
  67. echo "err:不是一个目录"
  68. fi
  69. }
  70. # 读取源码中的属性,保存到数组中
  71. # 参数一: 源码文件路径
  72. function get_properties_from_source_file {
  73. local class_file=$1;
  74. echo "class_file=${class_file}"
  75. properties=$(grep "@property.*" ${class_file})
  76. IFS_OLD=$IFS
  77. IFS=$'\n'
  78. for prop_line in $properties; do
  79. echo ">>>>>${prop_line}"
  80. asterisk_seperator_pattern="\*"
  81. if [[ ${prop_line} =~ ${asterisk_seperator_pattern} ]]; then
  82. # 从左向右截取最后一个string后的字符串
  83. prop_name=${prop_line##*${asterisk_seperator_pattern}}
  84. # 从左向右截取第一个string后的字符串
  85. seal_pattern=";*"
  86. seal_pattern_replacement=""
  87. prop_name=${prop_name//${seal_pattern}/${seal_pattern_replacement}}
  88. subsring_pattern="[ |;]"
  89. replacement=""
  90. prop_name=${prop_name//${subsring_pattern}/${replacement}}
  91. if [[ ${param_should_use_filter} -gt 0 ]]; then
  92. grep_result=$(grep ${prop_name} ${blacklist_cfg_file})
  93. echo "grep_result = >>${grep_result}<<"
  94. custom_grep_result=""
  95. if [[ -n ${param_custom_filter_file} ]]; then
  96. custom_grep_result=$(grep ${prop_name} ${param_custom_filter_file})
  97. fi
  98. if [[ -n ${grep_result} ]] || [[ -n ${custom_grep_result} ]]; then
  99. echo "--${prop_name}--存在配置文件中"
  100. else
  101. echo "--${prop_name}--XXX不存在配置文件中"
  102. tmp_props_array[$props_count]=$prop_name
  103. props_count=$[ props_count + 1 ]
  104. echo ">>>>>>>result_prop_name=${prop_name}"
  105. fi
  106. else
  107. tmp_props_array[$props_count]=$prop_name
  108. props_count=$[ props_count + 1 ]
  109. fi
  110. fi
  111. done
  112. IFS=$IFS_OLD
  113. }
  114. # 获取目录下的所有源文件,读取其中的属性
  115. function get_properties_from_source_dir {
  116. local l_classed_folder=$1
  117. echo "获取需要处理的源文件... ${l_classed_folder}"
  118. # 读取需要处理目标文件
  119. read_source_file_recursively ${l_classed_folder}
  120. echo "读取源文件中的属性..."
  121. for(( i=0;i<${#implement_source_file_array[@]};i++))
  122. do
  123. class_file=${implement_source_file_array[i]};
  124. echo "处理源文件:${class_file}"
  125. get_properties_from_source_file ${class_file}
  126. done;
  127. }
  128. # 把获取到的属性过滤之后写入文件中
  129. # 过滤步骤包含去重、去掉简单词汇、去掉长度少于多少的词汇
  130. # 如果在执行的过程中遇到特殊情况,添加到黑名单配置(DefaultBlackListPropertiesConfig.cfg文件中添加配置)
  131. function post_get_properties_handle {
  132. local prop_config_file=$1
  133. # 写入文件中
  134. echo "# Properties Configs" > ${prop_config_file}
  135. for key in $(echo ${!tmp_props_array[*]})
  136. do
  137. # echo "$key : ${tmp_props_array[$key]}"
  138. echo ${tmp_props_array[$key]} >> ${prop_config_file}
  139. done
  140. # 去重
  141. cfg_back_file="${prop_config_file}.bak"
  142. mv ${prop_config_file} ${cfg_back_file}
  143. sort ${cfg_back_file} | uniq > ${prop_config_file}
  144. # 过滤
  145. if [[ ${param_should_use_filter} -gt 0 ]]; then
  146. mv ${prop_config_file} ${cfg_back_file}
  147. echo "# Properties Configs Filtered" > ${prop_config_file}
  148. IFS_OLD=$IFS
  149. IFS=$'\n'
  150. # 上一行的内容
  151. lastLine="";
  152. for line in $(cat ${cfg_back_file} | sed 's/^[ \t]*//g')
  153. do
  154. if [[ ${#line} -le 6 ]] || [[ $(expr "$line" : '^#.*') -gt 0 ]]; then
  155. # 长度小于等于6或者注释内容的行不处理
  156. echo "less then 6 char line or comment line"
  157. else
  158. if [[ -n ${lastLine} ]]; then
  159. # 上一行是非空白行
  160. # 比较上一行内容是否是当前行的一部分,不是添加上一行
  161. if [[ ${line} =~ ${lastLine} ]]; then
  162. echo "${line} 和 ${lastLine} 有交集"
  163. else
  164. echo ${lastLine} >> ${prop_config_file}
  165. fi
  166. fi
  167. # 更新上一行
  168. lastLine=${line}
  169. fi
  170. done
  171. IFS=${IFS_OLD}
  172. fi
  173. # 删除临时文件
  174. rm -f ${cfg_back_file}
  175. }
  176. get_properties_from_source_dir ${param_input_dir}
  177. post_get_properties_handle ${param_output_file}
复制代码

使用以上脚本生成的配置文件 PropertiesConfigs.cfg 部分如下:

  1. # Properties Configs Filtered
  2. UserRestrictionLabel
  3. aboutusButton
  4. activitySamplers
  5. addAddressPress
  6. addressSamplers
  7. addressTextBox
  8. appealPress
  9. appliedGroupedSamplers
  10. appliedSamplers
  11. applyPress
  12. asyncArray
  13. asyncListSampler
  14. audioPlayer
复制代码

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

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

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

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

文件名:DefaultBlackListPropertiesConfig.cfg

  1. # BlackListPropertiesConfig.cfg
  2. # 属性黑名单配置,在此配置文件中的属性不需要替换名称
  3. window
  4. name
  5. title
  6. titleLabel
  7. layout
  8. appealSamplers
复制代码

在 GetAndStoreProperties.sh 脚本使用到的代码片段如下,其实就是使用了 grep 命来查找,判断时候有找到,如果有就不处理,具体的可以看上面提供的完整的 GetAndStoreProperties.sh 脚本代码

  1. if [[ ${param_should_use_filter} -gt 0 ]]; then
  2. grep_result=$(grep ${prop_name} ${blacklist_cfg_file})
  3. echo "grep_result = >>${grep_result}<<"
  4. custom_grep_result=""
  5. if [[ -n ${param_custom_filter_file} ]]; then
  6. custom_grep_result=$(grep ${prop_name} ${param_custom_filter_file})
  7. fi
  8. if [[ -n ${grep_result} ]] || [[ -n ${custom_grep_result} ]]; then
  9. echo "--${prop_name}--存在配置文件中"
  10. else
  11. echo "--${prop_name}--XXX不存在配置文件中"
  12. tmp_props_array[$props_count]=$prop_name
  13. props_count=$[ props_count + 1 ]
  14. echo ">>>>>>>result_prop_name=${prop_name}"
  15. fi
  16. else
  17. tmp_props_array[$props_count]=$prop_name
  18. props_count=$[ props_count + 1 ]
  19. fi
复制代码

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

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

  1. #...
  2. # 黑名单类目录
  3. declare -a custom_blacklist_search_dirs
  4. custom_blacklist_search_dirs=("/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/SSCatchAPI"
  5. "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Categories"
  6. "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Components"
  7. "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/External"
  8. "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/HandyTools"
  9. "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Macros" )
  10. # ...
  11. # 属性黑名单配置文件
  12. custom_blacklist_cfg_file="$(pwd)/CustomBlackListPropertiesConfig.cfg"
  13. # ...
  14. # 获取自定义的黑名单属性并保存到文件中
  15. echo "" > ${custom_blacklist_cfg_file}
  16. for (( i = 0; i < ${#custom_blacklist_search_dirs[@]}; i++ )); do
  17. custom_blacklist_search_dir=${custom_blacklist_search_dirs[${i}]}
  18. ./GetAndStoreProperties.sh \
  19. -i ${custom_blacklist_search_dir}\
  20. -o ${custom_blacklist_cfg_tmp_file}
  21. cat ${custom_blacklist_cfg_tmp_file} >> ${custom_blacklist_cfg_file}
  22. done
  23. #...
复制代码

最终生成的用户自定义的黑名单文件部分如下

文件:CustomBlackListPropertiesConfig.cfg

  1. # Properties Configs
  2. DBFilePath
  3. ValidityString
  4. accessQueue
  5. age
  6. attributedNameString
  7. avatarURLString
  8. avatarUrlString
  9. backColorString
  10. bodyScheduler
  11. bodyView
  12. catchDateString
  13. cellHeight
  14. channelKey
  15. cityName
  16. conditionString
  17. # ....
复制代码

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

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

  1. #!/bin/bash
  2. # 属性重命名脚本
  3. ####### 配置
  4. # classes类目录
  5. classes_dir="$(pwd)/../InjectedContentKitx"
  6. # 黑名单类目录
  7. declare -a custom_blacklist_search_dirs
  8. custom_blacklist_search_dirs=("/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/SSCatchAPI"
  9. "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Categories"
  10. "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Components"
  11. "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/External"
  12. "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/HandyTools"
  13. "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Macros" )
  14. # 配置文件
  15. cfg_file="$(pwd)/PropertiesConfigs.cfg"
  16. # 属性黑名单配置文件
  17. blacklist_cfg_file="$(pwd)/DefaultBlackListPropertiesConfig.cfg"
  18. # 属性黑名单配置文件
  19. custom_blacklist_cfg_file="$(pwd)/CustomBlackListPropertiesConfig.cfg"
  20. custom_blacklist_cfg_tmp_file="$(pwd)/TmpCustomBlackListPropertiesConfig.cfg"
  21. # 属性前缀,属性前缀需要特殊处理
  22. class_prefix=""
  23. # 属性后缀
  24. class_suffix="abc"
  25. # 检测文件是否存在,不存在则创建
  26. checkOrCreateFile() {
  27. file=$1
  28. if [[ -f $file ]]; then
  29. echo "检测到配置文件存在 $file"
  30. else
  31. echo "创建配置文件 $file"
  32. touch $file
  33. fi
  34. }
  35. # 配置文件检查
  36. checkOrCreateFile $cfg_file
  37. # 循环检测输入的文件夹
  38. function checkInputDestDir {
  39. echo -n "请输入需处理源码目录: "
  40. read path
  41. if [[ -d $path ]]; then
  42. classes_dir=$path
  43. else
  44. echo -n "输入的目录无效,"
  45. checkInputDestDir
  46. fi
  47. }
  48. # 需处理源码目录检查
  49. if [[ -d $classes_dir ]]; then
  50. echo "需处理源码目录存在 $classes_dir"
  51. else
  52. echo "请确认需处理源码目录是否存在 $classes_dir"
  53. checkInputDestDir
  54. fi
  55. ####### 数据定义
  56. # 定义属性保存数组
  57. declare -a rename_properties_config_content_array
  58. cfg_line_count=0
  59. # 读取属性配置文件
  60. function read_rename_properties_configs {
  61. IFS_OLD=$IFS
  62. IFS=$'\n'
  63. # 删除文件行首的空白字符 http://www.jb51.net/article/57972.htm
  64. for line in $(cat $cfg_file | sed 's/^[ \t]*//g')
  65. do
  66. is_comment=$(expr "$line" : '^#.*')
  67. echo "line=${line} is_common=${is_comment}"
  68. if [[ ${#line} -eq 0 ]] || [[ $(expr "$line" : '^#.*') -gt 0 ]]; then
  69. echo "blank line or comment line"
  70. else
  71. rename_properties_config_content_array[$cfg_line_count]=$line
  72. cfg_line_count=$[ $cfg_line_count + 1 ]
  73. # echo "line>>>>${line}"
  74. fi
  75. done
  76. IFS=${IFS_OLD}
  77. }
  78. function print_array {
  79. # 获取数组
  80. local newarray
  81. newarray=($(echo "$@"))
  82. for (( i = 0; i < ${#newarray[@]}; i++ )); do
  83. item=${newarray[$i]}
  84. echo "array item >>> ${item}"
  85. done
  86. }
  87. # 重命名所有的属性
  88. function rename_properties {
  89. # 读取属性配置文件
  90. read_rename_properties_configs
  91. # print_array ${rename_properties_config_content_array[*]}
  92. # 执行替换操作
  93. for (( i = 0; i < ${#rename_properties_config_content_array[@]}; i++ )); do
  94. original_prop_name=${rename_properties_config_content_array[i]};
  95. result_prop_name="${class_prefix}${original_prop_name}${class_suffix}"
  96. sed -i '{
  97. s/'"${original_prop_name}"'/'"${result_prop_name}"'/g
  98. }' `grep ${original_prop_name} -rl ${classes_dir}`
  99. echo "正在处理属性 ${original_prop_name}....."
  100. done
  101. }
  102. checkOrCreateFile ${custom_blacklist_cfg_tmp_file}
  103. # 获取自定义的黑名单属性并保存到文件中
  104. echo "" > ${custom_blacklist_cfg_file}
  105. for (( i = 0; i < ${#custom_blacklist_search_dirs[@]}; i++ )); do
  106. custom_blacklist_search_dir=${custom_blacklist_search_dirs[${i}]}
  107. ./GetAndStoreProperties.sh \
  108. -i ${custom_blacklist_search_dir}\
  109. -o ${custom_blacklist_cfg_tmp_file}
  110. cat ${custom_blacklist_cfg_tmp_file} >> ${custom_blacklist_cfg_file}
  111. done
  112. # 获取和保存属性到熟悉配置文件
  113. ./GetAndStoreProperties.sh \
  114. -i ${classes_dir}\
  115. -o ${cfg_file}\
  116. -f \
  117. -c ${custom_blacklist_cfg_file}
  118. # 执行属性重命名
  119. rename_properties
  120. echo "done."
复制代码

总结

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



回复

使用道具 举报