一个包任意切换不同测试环境是一直想做的。
以前开发任务少,打包少,就感觉和自己离的远。
这几天重新提起,于是用了三天时间,分析思路与实现。
记录一下这份喜悦。
首先mpaas客户端切换环境需要更改6个位置的配置代码文件:无线保镖图片,info.pllist,配置文件(纯文本),还有3个代码文件。
1.info.pllist 通过重写系统的NSBundle类的infoDictionary属性的set方法实现。这个在之前的博客中已经写过不再复述。链接:更改bundleid后蚂蚁金服MPSafeKeyboard的安全键盘不显示的解决办法
2.配置文件(纯文本)通过
方法一:fishhook系统open方法(实践表明只有第一次安装客户端才会走open)
方法二:hook系统NSBundle的- [pathForResource:ofType:],然后替换为相应文件
3.代码文件通过全局变量或沙盒标记,直接判断
4.无线保镖图片通过
方法一:对系统NSData由file创建方法
方法二:UIimage由file创建方法,
方法三:NSBundle路径获取方法,
方法四:open方法
方法五:hopper搜索图片名字
方法六:find.命令查找图片名字,确定其所在库
方法七:根据后来发现mpaas提供了支持不同环境读取不同图片的方法,得以实现。
5.切换环境后还要对上个环境在沙盒的缓存数据进行清除,实验表明切换后需要删掉沙盒 Preferences文件夹内容,即仅删除[NSUserDefaults standardUserDefaults]下所有内容即可
以下是详细过程
上来先找图片和配置文件的open方法,结果控制台把内容都打印完了也没走到我的open方法,但其他文件会走。那就是fishhook晚了,由main方法里改到了第一个执行的load方法里。知识点:每个类都有load,哪个类的load先走?
答案是:Build Phases的Compile Sources文件上下顺序。
放到这个load里面,果然我的代码比其他任何代码都先走,但还是打印完其他信息才走我的open,而且只有第一次卸载安装客户端才会。但是打印信息肯定用NSLog,直接替换为我的MyNSLog,这下找到了打印第一个配置信息的类。
竟然是写到了定位方法里。说明客户端在load阶段就将所有配置信息都完成了。
然后看看第二个打印
这个打印出了配置文件的内容,然后hopper一下这个方法,果然直接用的pathForResource
void +[MPLiteSettingService initialize](void * self, void * _cmd) {
var_-48 = self;
if ([self checkCoverInstallation] != 0x0) {
rbx = [[NSUserDefaults standardUserDefaults] retain];
[rbx removeObjectForKey:@"mPaaSConfig_meta"];
[rbx release];
rbx = [[NSUserDefaults standardUserDefaults] retain];
[rbx synchronize];
[rbx release];
}
rbx = [[NSBundle mainBundle] retain];
r14 = [[rbx pathForResource:@"meta" ofType:@"config"] retain];
[rbx release];
rdx = r14;
[MPLiteSettingService generateMetaConfigWithPath:rdx type:@"meta"];
rbx = [[MPaaSInterface sharedInstance] retain];
r13 = [rbx enableSettingService];
[rbx release];
if (r13 != 0x0) {
var_-112 = r14;
r14 = [[NSBundle mainBundle] retain];
rbx = [[r14 pathForResource:@"Settings" ofType:@"bundle"] retain];
[r14 release];
var_-56 = rbx;
if (rbx != 0x0) {
rcx = @"bundle";
rbx = [[NSUserDefaults standardUserDefaults] retain];
rdx = @"mPaasSettingServiceRegistered";
r14 = [[rbx objectForKey:rdx, rcx] retain];
[rbx release];
if (r14 == 0x0) {
rbx = [[NSUserDefaults standardUserDefaults] retain];
[rbx setObject:@"Registered" forKey:@"mPaasSettingServiceRegistered"];
[rbx release];
[var_-48 registerDefaultsFromSettingsWithPlist:@"Root.plist" forBundle:var_-56];
rdx = @"Service.plist";
rcx = var_-56;
[var_-48 registerDefaultsFromSettingsWithPlist:rdx forBundle:rcx];
rbx = [[NSUserDefaults standardUserDefaults] retain];
[rbx synchronize];
[rbx release];
}
rbx = [[NSUserDefaults standardUserDefaults] retain];
rdx = @"kMPSelectedEnvironment";
r13 = [[rbx objectForKey:rdx, rcx] retain];
[rbx release];
rsi = @selector(length);
var_-48 = r13;
rbx = _objc_msgSend;
if (_objc_msgSend(r13, rsi, rdx, rcx) != 0x0) {
var_-64 = r14;
if ([var_-48 isEqualToString:@"Customizing"] != 0x0) {
r15 = rbx;
rax = (r15)(@class(NSMutableDictionary), @selector(alloc), @"Customizing");
rax = (r15)(rax, @selector(init), @"Customizing");
rdi = *__settings;
*__settings = rax;
[rdi release];
rbx = [(r15)(@class(NSUserDefaults), @selector(standardUserDefaults), @"Customizing") retain];
r14 = [(r15)(rbx, @selector(objectForKey:), @"rpcGW") retain];
[rbx release];
var_-104 = r14;
r12 = @"";
if (r14 == 0x0) {
r12 = @"";
r14 = r12;
}
(r15)(*__settings, @selector(setObject:forKey:), r14);
rbx = [(r15)(@class(NSUserDefaults), @selector(standardUserDefaults), r14) retain];
r14 = [(r15)(rbx, @selector(objectForKey:), @"logGW") retain];
[rbx release];
var_-96 = r14;
if (r14 == 0x0) {
r12 = @"";
r14 = r12;
}
(r15)(*__settings, @selector(setObject:forKey:), r14, @"logGW");
rbx = [(r15)(@class(NSUserDefaults), @selector(standardUserDefaults), r14, @"logGW") retain];
r14 = [(r15)(rbx, @selector(objectForKey:), @"mpaasapi", @"logGW") retain];
[rbx release];
var_-88 = r14;
if (r14 == 0x0) {
r12 = @"";
r14 = r12;
}
(r15)(*__settings, @selector(setObject:forKey:), r14, @"mpaasapi");
rbx = [(r15)(@class(NSUserDefaults), @selector(standardUserDefaults), r14, @"mpaasapi") retain];
r14 = [(r15)(rbx, @selector(objectForKey:), @"syncserver", @"mpaasapi") retain];
[rbx release];
var_-80 = r14;
if (r14 == 0x0) {
r12 = @"";
r14 = r12;
}
(r15)(*__settings, @selector(setObject:forKey:), r14, @"syncserver");
rbx = [(r15)(@class(NSUserDefaults), @selector(standardUserDefaults), r14, @"syncserver") retain];
r14 = [(r15)(rbx, @selector(objectForKey:), @"syncport", @"syncserver") retain];
[rbx release];
var_-72 = r14;
rdx = r14;
if (r14 == 0x0) {
r12 = @"";
rdx = r12;
}
(r15)(*__settings, @selector(setObject:forKey:), rdx, @"syncport");
rbx = [(r15)(@class(NSUserDefaults), @selector(standardUserDefaults), rdx, @"syncport") retain];
r13 = [(r15)(rbx, @selector(objectForKey:), @"appId", @"syncport") retain];
[rbx release];
rdx = r13;
if (r13 == 0x0) {
r12 = @"";
rdx = r12;
}
(r15)(*__settings, @selector(setObject:forKey:), rdx, @"appId");
rbx = [(r15)(@class(NSUserDefaults), @selector(standardUserDefaults), rdx, @"appId") retain];
r14 = [(r15)(rbx, @selector(objectForKey:), @"workspaceId", @"appId") retain];
[rbx release];
if (r14 != 0x0) {
r12 = r14;
}
rsi = @selector(setObject:forKey:);
rdx = r12;
rcx = @"workspaceId";
(r15)(*__settings, rsi, rdx, rcx);
[r14 release];
[r13 release];
[var_-72 release];
[var_-80 release];
[var_-88 release];
[var_-96 release];
rdi = var_-104;
}
else {
r15 = rbx;
r14 = [(r15)(@class(NSBundle), @selector(bundleWithPath:), var_-56) retain];
rbx = [(r15)(r14, @selector(pathForResource:ofType:), var_-48, @"config") retain];
[r14 release];
rsi = @selector(generateMetaConfigWithPath:type:);
rdx = rbx;
rcx = var_-48;
(r15)(@class(MPLiteSettingService), rsi, rdx, rcx);
rdi = rbx;
}
[rdi release];
r14 = var_-64;
}
[var_-48 release];
[r14 release];
}
[var_-56 release];
r14 = var_-112;
}
NSLog(@"Setting service with values: %@", *__settings);
[r14 release];
return;
}
然后对这个pathForResource进行hook.创建新的分类NSBundle+YYY放到Build Phases的Compile Sources最上面。发现还是没拦截住,原因应该是NSBundle的分类按照主类所在的顺序执行,因此交换方法写在上面那个load里,但是这里。。。
有新发现。
交换后的方法还在分类写,执行交换的方法在上面load写,竟然没有问题。配置文件获取成功。
还有一张图片没找到
试了很多办法
然后记得以前find. 命令搜到过 图片名字就在那个库里,hopper就没有
在对整个工程find 搜matches竟然都没有。直接搜名字
然后搜出来了这个
//
// MPaaSConfigInfo.h
// APMPaaS
//
// Created by yangwei on 17/4/25.
// Copyright © 2017年 Alipay. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface MPaaSConfigInfo : NSObject
/**
* 当前App的无线保镖图片,是否需要区分平台。
* 设置为NO,表示无线保镖SDK不需要区分平台,使用默认的 yw_1222.jpg 安全图片;
* 设置为YES,表示无线保镖SDK需要区分平台,通过分配的 authCode,来指定在当前App中使用的无线保镖安全图片,并将图片设置为yw_1222_authCode.jpg;
*
* 默认返回为NO。钱包中不需要关心;
* mPaaS用户一般也不需要修改,只有在 yw_1222.jpg 图片与其他平台发生冲突时(如同时使用 mPaaS与阿里百川相关服务),需要在 category 中重写此方法,返回YES来区分图片。
*
* @return 默认返回NO
*/
+ (BOOL)enableMPaaSAuthCode;
/**
* 获取设置无线保镖接口的authCode值
*
* 根据 < enableMPaaSAuthCode > 的返回值,若为NO,此方法返回nil;若为 YES,此方法返回 @"1000";
* 钱包中默认为nil,mPaaS 用户根据需要重写 < enableMPaaSAuthCode > 方法
*
* @return 默认返回 nil
*/
+ (NSString *)openSecurityAuthCode;
@end
这个图片就使用蚂蚁提供的来做了。
但还想看看为啥找不到?
对enableMPaaSAuthCode进行跟踪。因为它会把名字拼到一起
bool +[MPaaSConfigInfo enableMPaaSAuthCode](void * self, void * _cmd) {
rax = 0x1 & 0xff;
return rax;
}
继续看看哪里调用了enableMPaaSAuthCode
只有
void * +[MPaaSConfigInfo openSecurityAuthCode](void * self, void * _cmd) {
if ([MPaaSConfigInfo enableMPaaSAuthCode] != 0x0) {
rbx = @"1000";
[rbx retain];
}
else {
rbx = 0x0;
}
rax = [rbx autorelease];
return rax;
}
然后找openSecurityAuthCode
0000000102085728 dq 0x1018c8f10 ; @selector(openSecurityAuthCode),
"openSecurityAuthCode", DATA XREF=
-[DTRpcOperation signRequest:]+1241,
-[DTRpcOperation signRequest:]+1584,
-[DTURLRequestOperation rpcV1Sign:newSign:request:]+568,
-[DTURLRequestOperation rpcV1Sign:newSign:request:]+832,
+[APSyncUtils signString:appKey:]+272,
+[NSString safetySignatureWithInputStr:]+265
看哪个呢这个吧[DTRpcOperation signRequest:]
void -[DTRpcOperation signRequest:](void * self, void * _cmd, void * arg2) {
r13 = self;
r14 = [arg2 retain];
r15 = [[r13 request] retain];
rbx = [[r13 httpBodyParameters] retain];
[rbx release];
if (rbx != 0x0) {
rbx = [[OpenSecurityGuardManager getInstance] retain];
r12 = [[rbx getSecureSignatureComp] retain];
[rbx release];
if (r12 != 0x0) {
var_88 = r12;
var_78 = r15;
var_68 = r14;
var_70 = [[NSMutableString string] retain];
rbx = [[r13 method] retain];
r14 = [[rbx operationType] retain];
[r14 release];
[rbx release];
COND = r14 == 0x0;
r14 = var_70;
r13 = r13;
if (!COND) {
r12 = [[r13 method] retain];
rbx = [[r12 operationType] retain];
rcx = rbx;
[r14 appendFormat:@"Operation-Type=%@"];
[rbx release];
[r12 release];
}
r15 = [[r13 httpBodyParameters] retain];
r12 = var_68;
rbx = [[r15 objectForKey:@"requestData"] retain];
[r15 release];
var_90 = rbx;
if (rbx != 0x0) {
rbx = [var_90 retain];
if ([r13 isProtocolBuffers] == 0x0) {
r14 = [[rbx dataUsingEncoding:0x4] retain];
r15 = [[r14 base64EncodedStringWithOptions:0x0] retain];
[rbx release];
[r14 release];
rbx = r15;
r14 = var_70;
}
COND = [r14 length] == 0x0;
rcx = @"&";
if (COND) {
rcx = @"";
}
[r14 appendFormat:@"%@Request-Data=%@"];
[rbx release];
}
if (r12 != 0x0) {
COND = [r14 length] == 0x0;
rcx = @"&";
if (COND) {
rcx = @"";
}
[r14 appendFormat:@"%@Ts=%@"];
}
NSLog(@"Rpc sign string v2:\n%@", r14);
r14 = [[DTRpcInterface sharedInstance] retain];
rbx = [[r13 request] retain];
rdx = rbx;
var_80 = [[r14 signKeyForRequest:rdx] retain];
[rbx release];
[r14 release];
r12 = [DTRpcUtils useNewSign];
rbx = [[r13 customAppKey] retain];
r15 = [rbx length];
[rbx release];
if (r15 != 0x0) {
r15 = var_78;
rbx = [[r13 customAppKey] retain];
[var_80 release];
var_80 = rbx;
rcx = @"appkey";
rdx = rbx;
[r15 setValue:rdx forHTTPHeaderField:rcx];
var_60 = @"input";
var_58 = var_70;
rbx = [[NSDictionary dictionaryWithObjects:rdx forKeys:rcx count:0x1] retain];
r14 = [[OpenSecurityGuardParamContext createParamContextWithAppKey:var_80 paramDict:rbx requestType:*_OPEN_ENUM_SIGN_COMMON_MD5] retain];
[rbx release];
r12 = [[MPaaSConfigInfo openSecurityAuthCode] retain];
rsi = @selector(signRequest:authCode:);
rdx = r14;
rbx = [_objc_msgSend(var_88, rsi) retain];
[r12 release];
if (rbx != 0x0) {
rsi = @selector(setValue:forHTTPHeaderField:);
rdx = rbx;
_objc_msgSend(r15, rsi);
}
[rbx release];
rdi = r14;
}
else {
r15 = var_78;
if (r12 != 0x0) {
var_50 = @"input";
var_40 = var_70;
*(&var_50 + 0x8) = @"atlas";
*(&var_40 + 0x8) = @"a";
rax = [NSDictionary dictionaryWithObjects:rdx forKeys:rcx count:0x2];
rax = [rax retain];
var_A0 = rax;
r12 = [[OpenSecurityGuardParamContext createParamContextWithAppKey:var_80 paramDict:rax requestType:*_OPEN_ENUM_SIGN_ATLAS] retain];
var_98 = r12;
rbx = [[MPaaSConfigInfo openSecurityAuthCode] retain];
r14 = [[var_88 signRequest:r12 authCode:rbx] retain];
[rbx release];
if (r14 != 0x0) {
[r15 setValue:r14 forHTTPHeaderField:@"Sign"];
[r15 setValue:@"1" forHTTPHeaderField:@"SignType"];
NSLog(@"%@", cfstring_a);
}
rsi = r14;
rdx = var_70;
NSLog(@"sign v2:%@,content:%@", rsi, rdx);
[r14 release];
[var_98 release];
rdi = var_A0;
}
else {
var_60 = @"input";
var_58 = var_70;
rbx = [[NSDictionary dictionaryWithObjects:rdx forKeys:rcx count:0x1] retain];
r14 = [[OpenSecurityGuardParamContext createParamContextWithAppKey:var_80 paramDict:rbx requestType:*_OPEN_ENUM_SIGN_COMMON_MD5] retain];
[rbx release];
r12 = [[MPaaSConfigInfo openSecurityAuthCode] retain];
rsi = @selector(signRequest:authCode:);
rdx = r14;
rbx = [_objc_msgSend(var_88, rsi) retain];
[r12 release];
if (rbx != 0x0) {
rsi = @selector(setValue:forHTTPHeaderField:);
rdx = rbx;
_objc_msgSend(r15, rsi);
}
[rbx release];
rdi = r14;
}
}
[rdi release];
r14 = var_68;
r12 = var_88;
if ([r13 isProtocolBuffers] == 0x0) {
rbx = [[var_90 dataUsingEncoding:0x4] retain];
[r15 setHTTPBody:rbx];
[rbx release];
}
[var_80 release];
[var_90 release];
[var_70 release];
}
[r12 release];
}
[r15 release];
[r14 release];
if (*___stack_chk_guard != *___stack_chk_guard) {
__stack_chk_fail();
}
return;
}
r12 = [[MPaaSConfigInfo openSecurityAuthCode] retain];
rsi = @selector(signRequest:authCode:);
rdx = r14;
rbx = [_objc_msgSend(var_88, rsi) retain];
[r12 release];
没看到调用就release了。
那肯定在signRequest:authCode这个方法里传值了
void * -[SecurityGuardOpenSecureSignature signRequest:authCode:](void * self, void * _cmd, void * arg2, void * arg3) {
rax = loc_100b214f9(self, _cmd, arg2, arg3);
return rax;
}
这个再点已经跳不过去了
加密的东西肯定保护得好,至此图片名字获取已失败告终
代码写完,配置一个新环境,代码写死后成功开启了。
现在需要一个全局变量控制加载哪个环境的参数。
还要加一个图形界面选择环境切换,重写蚂蚁封装的didfinishLaunch,结果黑屏 此方案kill
另一个方案,先进默认环境,点击某个按钮提示切换环境,选择对应环境做三个事
1.清上面说的NSUserDefaults内容和沙盒内所有文件
2.将环境名称写入本地,沙盒的其他地方新创建文件来做。
3.退出重新进
然后新打开app就是切换后的环境,和以前卸载重装一样
如果直接退出,下次进来则是不会清除上次的数据的,还可以按照上次设置的环境来用。
后记:
蚂蚁也提供了动态切换环境的方法,iOS 环境切换 - 移动开发平台 mPaaS - 阿里云
这个方法和上面的方式同时使用会使上面的失效,解决办法:hook方法pathForResource,在读取Settings.bundle的时候进行拦截
- (nullable NSString *)pathForResource:(nullable NSString *)name ofTypeS:(nullable NSString *)ext;
{
if ([name containsString:@"Settings"]) {
name = @"";
}
return [self pathForResource:name ofTypeS:ext];
}
再后记:
实现代码见下面这篇博客 拿来即用删掉即走:iOS客户端无侵入、一包任意环境切换实践篇__小呵呵的博客-CSDN博客
。
。
。