#IOS Spam 上架被拒原因分析及处理办法
问题描述
上传审核了一个马甲应用,根据之前的经验,在上架之前详细分析和对比的生成的ipa文件然后根据别人给的意见添加了一定比例的垃圾代码,删除了多余的资源文件以减少相同比例,甚至将每个图片资源的名称都改了名称,但还是提示了如下问题:
4. 3 Design: Spam
Guideline 4.3 - Design
We noticed that your app provides the same feature set as other apps submitted to the App Store; it simply varies in content or language, which is considered a form of spam.
The next submission of this app may require a longer review time.
初探
Project 和 Target 名称
Target 名称是可以单独修改的,Project名称需要与左边 Progject Nagivator 中名称一致,并且修改Project Navigator会提示修改 Target名称;
Scheme 名称
分析ipa文件可知,Scheme名称会是主可执行文件的名称,也就是生成的ipa文件名。
点击如上Scheme会弹出Manage Scheme点击进入编辑Scheme界面
混淆代码
分析过程
首先观察并对比如下 obj-c 代码反编译后生成的二进制文件
外部声明宏定义字符串
#define xxx_ooo @"xxx_ooo"
#define xxx_ooo_1 @"xxx_ooo_111"
#define xxx_ooo_2 @"xxx_ooo_222"
temp.h文件
#import <Foundation/Foundation.h>@interfacetemp:NSObject@end
temp.m 文件
#import "temp.h"@implementationtemp-(void)heheda{NSString*str=xxx_ooo_1;}-(void)aiyouwei{NSString*str=xxx_ooo_2;NSLog(@"%@",str);}-(void)zhijieting{if(NO){[selfheheda];}}-(void)jianjieting{// [BBJQUtilkit shouldexe] 直接返回falseif([BBJQUtilkitshouldexe]){[selfheheda];}}@end
二进制文件分析
字符串分析
虽然我们在代码中使用宏定义声明了三个字符串,但是二进制文件中只存在xxx_ooo_222一个字符串
代码判断
-(void) heheda{
NSString *str = xxx_ooo_1;
}
bx lr: 将lr 寄存器中内容返回到pc寄存器,相当于return;
-(void) zhijieting{
if(NO){
[self heheda];
}
}
同上,也是直接返回;
-(void) jianjieting{
if([BBJQUtilkit shouldexe]){
[self heheda];
}
}
有具体代码实现
分析可以看出,虽然zhijieting 和jianjieting 两个方法的逻辑是相同的,但是由于jianjieting 多调用了一层,导致编译器认为这是个正常的判断,从而正确的将我们的代码编译了。而zhijieting 则不然,因为是对常量值NO做判断,在编译阶段直接优化了所有代码。
junk 构建方案
根据以上推理和测试,我们基本可以确认那些代码会在编译阶段保留,这样我们先构建junk代码;
假设需要构建10个junk 类,每个类中有公共属性,公共方法和私有方法,每个类模板如下
// 文件junk1.h@interfacejunk1@property(nonatmic,strong)NSString*junk1_string1...@property(nonatmic,strong)NSString*junk1_stringN-(void)junk1_public_func1;...-(void)junk1_public_funcN;@end// 文件junk1.m#import "junk1.h"@implementejunk1()-(void)junk1_public_func1{if([Utilcallexec()]){[[[junk2alloc]init]junk2_public_funcX]...[[[junkXalloc]init]junkX_public_funcY]}}...-(void)junk1_public_funcN{// 同junk1_public_func1}-(void)junk1_private_func1{// 同junk1_public_func1}...-(void)junk1_private_funcN{// 同junk1_public_func1}@end
如上给出了junk1类的示例代码,其他代码相同。
为了方便脚本实现,每个类头文件(.h)文件名与实现文件(.m)文件名相同且都为类名。
python脚本实现
随机字符串
随机字符串用于类名、变量名和方法名,我们的目的就是将这些名词合理的组合起来,因此构建出合理的随机字符串时脚本实现junk代码的基础。
为了使随机字符串在一定程度上有意义,我的方案是准备一篇英文文献作为输入源,根据根据需要的长度来获取一个字符串。这样构建随机切出来的字符串会有一定的意义,以防审核时机器扫码代码会识别太过无意义的类名和方法名(这个场景纯属猜测,具体机器扫码什么策略谁也不知道)。
由于是找的英文文章,其中会包含部分不合适的字符,需要过滤,因此我们随机字符串生成分两步:
过滤文章
defcode_filter():input_path="./junkcode/coderes.txt"out_path='./junkcode/coderes_out.txt'withopen(input_path,"r")asinfile:withopen(out_path,"w")asoutfile:lines="".join(infile.readlines())nstring=MutableString()forchinlines:# 判断每个字符是否为字母或者下划线ifchin"abcdefghigklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ":nstring+=choutfile.writelines(str(nstring))
coderes.txt中包含的是原始文章,coderes_out.txt为过滤后的文章。
截取指定长度
coderes=""# 保存所有文本信息cur_index=0# 当前位置计数器# 首先将所有过滤后的文本读取出来withopen('./junkcode/coderes_out.txt')asresfile:coderes=coderes.join(resfile.readlines())defget_code_str():globalcur_indexrslen=len(coderes)ifcur_index+20>rslen:cur_index=0cur_index+=20# 返回根据当前位置确定的长度为20的字符串returncoderes[cur_index-20:cur_index]
构建junk 类
构建junk 类也分为两步,第一步创建出junk模板,第二步根据已有的junk类名为所有方法添加调用,如下代码为构建junk类的基本流程,详细代码见附件。
defcreate_junk_frame():ifnotos.path.exists(junk_path):os.mkdir(junk_path)withopen(os.path.join(junk_path,"all_juk.h"),"w")asajkfile:foriinrange(0,150):#创建150个junk类,类名以“CLASS_”开头class_name="CLASS_"+get_code_str()# 根据class_name初步创建create_class(junk_path,class_name)# 将所有类import到all_juk.h文件,方便之后拖入工程ajkfile.writelines(includ_header.format(class_name))## 提取所有可用方法extract_func_call(junk_path)## 为每个m文件中{}添加方法add_func_ctn_in_m(junk_path)# 初步创建类框架的代码defcreate_class(filepath,classname):h_file_path=os.path.join(filepath,classname+".h")m_file_path=os.path.join(filepath,classname+".m")withopen(h_file_path,"w")ashfile:withopen(m_file_path,"w")asmfile:# 首先写入h文件和m文件的头部信息hfile.writelines(h_header_f.format(classname))mfile.writelines(m_header_f.format(classname))#为h文件写入 get_random_10() 个属性foriinrange(0,get_random_10()):hfile.writelines(prop_formate.format(get_code_str()))#为m文件写入get_random_10()个属性foriinrange(0,get_random_10()):mfile.writelines(m_func_realize.format(get_code_str(),"{}"))#写入20个方法,根据should_static()方法随机判断是否需要为static方法(+,-)foriinrange(0,get_random_20()):func_name=get_code_str()ifshould_static():hfile.writelines(h_func_static.format(func_name))mfile.writelines(m_func_static.format(func_name,"{}"))else:hfile.writelines(h_func_dec.format(func_name))mfile.writelines(m_func_realize.format(func_name,"{}"))# 写入h问价和m问价尾部信息hfile.writelines("@end")mfile.writelines("@end")# 为每个m文件中{}添加方法 defadd_func_ctn_in_m(root_path):forroot,dirs,filesinos.walk(root_path):forfile_nameinfiles:#找到m文件iffile_name.endswith(".m"):class_name=file_name[:-2]mfile_path=os.path.join(root,file_name)withopen(mfile_path)asrfile:withopen(mfile_path+"_","w")aswfile:line=rfile.readline()whileline:# 查找其中包含{}的行,在create_class()时,我们将所有的方法实现为空方法,因此这里直接查找if"{}"inline:self_static=line.startswith("+")func_name=line[line.index(")")+1:line.index("{")].strip(" ")# 添加10行代码ten_line_code=get_10_line_code(class_name,func_name,self_static)line=line.replace("{}","{"+ten_line_code+"}")wfile.writelines(line)line=rfile.readline()os.remove(mfile_path)os.rename(mfile_path+"_",mfile_path)
添加junk类调用
考虑这样一个场景:当前代码添加了200个junk类,在二进制文件上与之前相比有很大不同,但是当前代码是全包含之前代码的。这样机器也可能会认为你在修改别人的代码,从而报spam。
初始代码:
-(void) origion_fun_gaiming{
NSString *a = @"";
NSLog(@"%@",a);
// JunkClass 为插入的代码调用
[JunkClass junk_func];
NSData *data =[NSData dataWithContentsOfFile:a];
NSLog(@"%lu", (unsigned long)data.length);
}
于是我们做如下对比,源代码反编译图
改名后反编译截图:
添加junk调用反编译图
从图中可以看出,添加一行调用后,反编译后会有明显不同,假如每隔的我们每隔3-5行就添加一行随机junk代码的调用,可以保证将原来的代码被充分混淆。
根据我们之前的分析,我们指定如下混淆方案:
在.h 文件中添加类为junk 类中的属性;
在.m 文件的代码部分每个3-5行添加junk调用
# 混淆代码definsert_junk_call():# 抽取所有junk 类名及其方法extract_func_call(junk_path)fordirinneed_hunxiao_dir:full_dir=os.path.join(base_path_dir,dir)# 如上代码进入需要混淆的类所在目录# 遍历所有文件forroot,dirs,filesinos.walk(full_dir):forfile_nameinfiles:# 对.h文件进行混淆iffile_name.endswith(".h"):h_hunxiao(os.path.join(root,file_name))# 对.m文件进行混淆iffile_name.endswith(".m"):m_hunxiao(os.path.join(root,file_name))# 混淆头文件 defh_hunxiao(h_file_path):hunxiao_codes=""import_headers=""foriinrange(0,get_random_5()):# gethunxiao_pro() 获取import 代码和 属性声明代码,其格式如下# h_hunxiao_f='''@property(nonatomic, strong) {0} *{1};'''# import_header_f = '''#import "{0}.h"'''hunxiao_one,header_one=gethunxiao_pro()hunxiao_codes+=hunxiao_one+"\n"import_headers+=header_one+"\n"# 将以上获取的import 代码和 属性声明代码写入文件withopen(h_file_path)asrfile:withopen(h_file_path+"_","w")aswfile:line=rfile.readline()after_interface=Falsewhileline:if"@interface"inline:line=import_headers+lineafter_interface=Trueif"@end"inlineandafter_interface:line=hunxiao_codes+linewfile.writelines(line)line=rfile.readline()os.remove(h_file_path)os.rename(h_file_path+"_",h_file_path)#混淆m文件defm_hunxiao(m_file_path):withopen(m_file_path)asrfile:withopen(m_file_path+"_","w")aswfile:cur_depth=0line_counter=0#一个计数器,每隔3行写入一个junk调用line=rfile.readline()depth_counter=0last_depth=0whileline:cur_depth+=line.count("{")cur_depth-=line.count("}")iflast_depth>0andcur_depth<=0:depth_counter+=1ifcur_depth>0and";"inlineand"return"notinline:line_counter+=1ifline_counter%3==0anddepth_counter>1:line+=dict_all_func_call[get_random_int(len(dict_all_func_call))]last_depth=cur_depthwfile.writelines(line)line=rfile.readline()os.remove(m_file_path)os.rename(m_file_path+"_",m_file_path)
字符串提取
根据前期的调研我们知道程序中的字符串也会出现在二进制文件中,假如机器扫描时将字符串作为一个判断标准,那么在过spam时就需要考虑将字符串提取出来并做处理了。
我的方案:自己定义一个方法,方法接受给定key值,通过NSLocalizedString 获取对应字符串,判断字符串是否为加密,如果加密则解密后返回,否则直接返回。
// the fu*k code
+(NSString*) decodekeycode:(NSString*) keycode{
NSString *str = NSLocalizedString(keycode, nil);
if([str hasPrefix:app_id]){
// app_id 为解密字符串所需key
str = [str substringFromIndex:app_id.length];
NSData *nsdata = [[NSData alloc] initWithBase64EncodedData:str options:0];
NSData *nsCode = [app_id dataUsingEncoding:NSUTF8StringEncoding];
const char *bytes = [nsdata bytes];
const char *codebytes = [nsCode bytes];
char* buffer = malloc([nsdata length]);
for(int i=0; i<[nsdata length]; i++){
int j = i % [nsCode length];
buffer[i] = bytes[i]^codebytes[j];
}
NSData * nsBuffer = [[NSData alloc]initWithBytes:buffer length:[nsdata length]];
return [[NSString alloc] initWithData:nsBuffer encoding:NSUTF8StringEncoding];
}
return str;
}
要实现如上的功能,需要python的强力支持,至少需要做到两步
一、生成唯一key值 唯一key的作用有两个,一是唯一标识文本,二是在新马甲中能够统一修改,这样在新的马甲与本体不会因为字符串相同被查。
# key = 要修改的key值
# code = 当前应用id
def newKey(key, code, ):
print code, key
new_key = key+ code
m5 = hashlib.md5()
m5.update(new_key)
return m5.hexdigest()
二、加密字符串
因为字符串都提取到strings文件中,如果将文本全部明文暴露的话还是有可能被认为spam,在此我们变种base64加密,变种通过一个异或操作,将每个马甲的文本变得不同,通过base64将亦或后的数据变为能够通过文本保存的格式。
def encodeValue(code, value):
orxstr = ""
for i in range(0, len(value)):
j = i % len(code)
rst = ord(list(value)[i]) ^ ord(list(code)[j])
orxstr = orxstr + chr(rst)
res = base64.b64encode(orxstr)
print res
return code +res
def decodeValue(code, value):
res = base64.b64decode(value[len(code):])
orxstr = ""
for i in range(0, len(res)):
j = i% len(code)
rst = ord(list(res)[i]) ^ ord(list(code)[j])
orxstr = orxstr + chr(rst)
res = orxstr
return res
图片及其他资源
图片资源肯定要修改,ipa文件中car文件即包含了工程中的图片资源,可以使用cartool 来提取。
从cartool 我们能够看出,图片资源的名称被保留了,因此我们需要修改工程中图片名称,同时也需要将切图换一下,以防机器进行图片相似度匹配。
在一些情况下,我们也许会将一些资源例如固定的json数据,提示html页面放到工程中,这写资源在打包后会原封不动的放到ipa文件中,所以我们需要修改这些文件内容,最好修改成服务端下载的。
源码下载
当前代码比较混乱,暂时不放到github上了,单独将py文件拿出来。 代码下载
网友评论