小工具-数据解包

当前项目中,使用了 Socket 长连接与服务端进行数据交互。不同于往常的 http/https 的请求,有很多开源的三方库,比如 DoraemonKit,可以方便我们可视化请求的数据交互,或者是直接使用抓包工具来展示数据。但是,现在项目中,数据交互由自研的 Socket 连接库和数据解包工具组合来完成。自研库出来的的数据是数据的指针,再由解包工具进行解包得到业务方需要的数据。

背景

在这个数据交互的过程中,测试无法通过抓包获取到请求的参数及返回结果,当发生问题时,无法快速定位问题,只能由客户端开发人员进行调试,才能得到请求时具体的数据往来由此定位问题,导致问题解决的效率比较低。

这个工具旨在能将数据的交互可视化,使问题快速暴露,并得以解决。

期望目标

  • 获取请求时的请求参数
  • 获取请求之后的请求结果
  • 数据可视化。

解决方案

请求示例

  • 同步请求
1
2
3
4
5
6
/// 接口定义
- (int) search:(int64_t)groupId aKeyword:(NSString*)keyword aOption:(int32_t)option aRspList:(NSArray**)rspList;

/// 请求示例
NSArray *result = nil;
[[WebSearchClient get] search:0 aKeyword:@"" aOption:1 aRspList:&result];
  • 异步请求
1
2
3
4
5
6
7
/// 接口定义
- (BOOL) async_search:(int64_t)groupId aKeyword:(NSString*)keyword aOption:(int32_t)option aCallback:(void(^)(int __retcode, NSArray* rspList)) __callback;

/// 请求示例
[[WebSearchClient get] async_search:0 aKeyword:@"" aOption:1 aCallback:^(int __retcode, NSArray *rspList) {
// todo something
}];
  • 通过 Aspect 对同步请求方法进行 Hook,得到请求参数及结果。
  • 结合 Aspect & BlockHook 对异步方法进行 Hook,得到请求参数及结果。

前期准备

前提知识

AOP

在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

有了这个思想作为方向之后,接下来我们就需要解决 切入点 和 切片逻辑 这两个问题了。

  • 切入点

XXXClient 及 XXXPack 都是由内部工具生成的模板代码:

  1. XXXClient:处理所有的请求逻辑。都继承于 AaceCaller 类。
  2. XXXPack:自定义的数据解包工具。

本来准备直接 hook 基类,但是基类中请求参数和请求结果都是已经被工具类 ProtocolPacker 包装过的 NSData 数据,想要解析出来存在困难。

最终方案就定位,针对每一个业务的 XXXClient 中的请求方法进行 hook。在 Client 类的请求方法结束之后,数据也得到了 Pack 的解包,这个时候,我们就可以获取到请求的参数及返回的数据。

  • 切片逻辑

具体的获取数据的逻辑,需要分为 同步请求 和 异步请求两种处理。后面会展开介绍。

准备工作

  1. 利用 CocoaPods 接入 Aspect 及 BlockHook。
  2. 获取一个类的所有子类工具方法。
  3. 获取一个类的所有方法。
  4. Type Encodings 类型编码

实施

步骤

  1. 先找到 AaceCaller 的所有子类,也就是项目中所有的 XxxClient。
  2. 遍历所有 XxxClient 类,并找出其所有的方法。
  3. 删选过滤。找到其 同步请求 和 异步请求 的方法。
  4. 针对不同的方法,执行不同的 Hook 逻辑。

同步请求

请求返回的结果,是通过指针赋值的方式,直接传递给业务层,所以,在获取请求参数时,能获取到的是一个内部结构 NSConcreateValue 的对象,其实这就是一个对象的指针。我们需要做的就是根据这个指针将对应的数据对象取出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
+ (void)_forSyncRequest:(Class)cls selName:(NSString *)selName {
SEL sel = NSSelectorFromString(selName);

[cls aspect_hookSelector:sel withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
NSObject *instance = aspectInfo.instance;
NSInvocation *invocation = aspectInfo.originalInvocation;
NSArray *arguments = aspectInfo.arguments;

NSMethodSignature *methodSignature = invocation.methodSignature;

NSMutableArray *requestArguments = [[NSMutableArray alloc] init];
NSMutableArray *responseArray = [[NSMutableArray alloc] init];

for (int i = 0; i < arguments.count; i++) {
NSObject *argument = arguments[i];

// 前两个参数分别是 实例对象 和 Sel,为默认参数,这里取得时候直接跳过。
// 后面的参数则是调用方法时,传入的参数。
const char *type = [methodSignature getArgumentTypeAtIndex:2 + i];
NSString *argumentType = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];;

if ([argumentType isEqualToString:@"^@"]) {
// 指向对象的指针。 针对client,这个一般都是我们传入用来获取返回值的对象指针
NSValue *value = (NSValue *)argument;

void **aaa = value.pointerValue;

NSObject *use = (__bridge NSObject *)(*aaa);

NSArray *array = [self findIvars:use.class];

if (use) {
[responseArray addObject:use];
} else {
[responseArray addObject:@"instance empty"];
}
}
...省略判断逻辑代码
}

// 自定义请求对象
SHMAaceHookModel *result = [[SHMAaceHookModel alloc] init];
result.clientName = NSStringFromClass(cls);
result.selName = selName;
result.arguments = requestArguments;
result.responses = responseArray;

[[SHMAaceHookCache sharedCache] saveHookResult:result];

} error:NULL];
}

异步请求

Aspect hook 方法之后,获取到 block 对象,接着用 BlockHook 对 block 进行拦截,在 block 执行完成之后,我们获取其中的参数,也即为我们需要的请求数据结果。

根据获取到的参数的类型编码确定参数的类型,并根据类型作相应的解析,得到我们需要的结果数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
+ (void)_forAsyncRequest:(Class)cls selName:(NSString *)selName {
SEL sel = NSSelectorFromString(selName);

[cls aspect_hookSelector:sel withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo) {
NSObject *instance = aspectInfo.instance;
NSInvocation *invocation = aspectInfo.originalInvocation;
NSArray *arguments = aspectInfo.arguments;

NSMethodSignature *methodSignature = invocation.methodSignature;

NSMutableArray *requestArguments = [[NSMutableArray alloc] init];
NSMutableArray *responseArray = [[NSMutableArray alloc] init];

[aspectInfo.originalInvocation retainArguments];

for (int i = 0; i < arguments.count; i++) {
NSObject *argument = arguments[i];

// 前两个参数分别是 实例对象 和 Sel,为默认参数,这里取得时候直接跳过。
// 后面的参数则是调用方法时,传入的参数。
const char *type = [methodSignature getArgumentTypeAtIndex:2 + i];
NSString *argumentType = [NSString stringWithUTF8String:type];

Class blockCls = NSClassFromString(@"NSBlock");
if ([argument isKindOfClass:blockCls]) {
__unsafe_unretained id block = argument;

[block block_interceptor:^(BHInvocation * _Nonnull invocation, IntercepterCompletion _Nonnull completion) {
completion();

for (int i = 1; i < invocation.methodSignature.numberOfArguments; i++) {

const char *argType = [invocation.methodSignature getArgumentTypeAtIndex:i];
NSString *argumentType = [NSString stringWithCString:argType encoding:NSUTF8StringEncoding];

NSLog(@"aace async hook ======= argumentType = %@", argumentType);

...省略判断逻辑代码
if ([argumentType hasPrefix:@"@\"NS"]) {
// oc 对象
void *obj;
[invocation getArgument:&obj atIndex:i];

NSObject *object = (__bridge NSObject *)obj;

if (object) {
[responseArray addObject:object];
}
}
}

// 异步请求需要等待 block 结束后存储
SHMAaceHookModel *result = [[SHMAaceHookModel alloc] init];
result.clientName = NSStringFromClass(cls);
result.selName = selName;
result.arguments = requestArguments;
result.responses = responseArray;

[[SHMAaceHookCache sharedCache] saveHookResult:result];
}];

} else {
if (argument) {
[requestArguments addObject:argument];
}
}
}

} error:NULL];
}

数据收集

  • SHMAaceHookModel:请求的模型
1
2
3
4
5
6
7
8
@interface SHMAaceHookModel : NSObject

@property (nonatomic, copy) NSString *clientName; // 对应业务的服务名
@property (nonatomic, copy) NSString *selName; // 请求方法名
@property (nonatomic, copy) NSArray *arguments; // 请求参数
@property (nonatomic, copy) NSArray *responses; // 请求结果

@end

然后再重写模型的 description 方法,将请求的信息组装用于展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
- (NSString *)description {
NSMutableString *description = [[NSMutableString alloc] initWithString:@""];

/// client
NSString *client = [NSString stringWithFormat:@"Clicnt Name: \n\t%@\n\n", self.clientName ?: @"Empty Client Name"];
[description appendString:client];

/// method
NSString *method = [NSString stringWithFormat:@"Method Name: \n\t%@\n\n", self.selName ?: @"Empty Method Name"];
[description appendString:method];

/// arguments
[description appendString:@"Request Arguments: \n"];
for (NSObject *obj in self.arguments) {
// NSNumber Bool NSArray
if ([obj isKindOfClass:NSClassFromString(@"__NSCFBoolean")]) {
// Bool 类型
BOOL use = (BOOL)obj;
NSString *desc = use ? @"YES" : @"NO";
NSString *boolDesc = [NSString stringWithFormat:@"\tBool : %@\n", desc ?: @"Empty value"];
[description appendString:boolDesc];

} else if ([obj isKindOfClass:NSNumber.class]) {
// 数字类型
NSNumber *use = (NSNumber *)obj;
NSString *numberDesc = [NSString stringWithFormat:@"\tNSNumber : %@\n", use ?: @"Empty value"];
[description appendString:numberDesc];
} else if ([obj isKindOfClass:NSArray.class]) {
NSArray *use = (NSArray *)obj;

if (use.count > 0) {
[description appendString:@"\t NSArray Items: \n"];
}

for (NSObject *subItem in use) {
if ([self isSystemClass:subItem.class]) {
// 系统类
NSString *systemClsDesc = [NSString stringWithFormat:@"\t\t%@ : %@ ", NSStringFromClass(subItem.class), subItem];
[description appendString:systemClsDesc];
} else {
// 自定义类
NSString *customClsDesc = [NSString stringWithFormat:@"\t\t%@ \n", NSStringFromClass(subItem.class)];
[description appendString:customClsDesc];
[description appendString:[self descForCustomClass:subItem prefixSpace:@"\t\t\t"]];
}
}
} else {
if ([self isSystemClass:obj.class]) {
// 系统类
NSString *systemClsDesc = [NSString stringWithFormat:@"\t%@ : %@ ", NSStringFromClass(obj.class), obj];
[description appendString:systemClsDesc];
} else {
// 自定义类
NSString *customClsDesc = [NSString stringWithFormat:@"\t%@ \n", NSStringFromClass(obj.class)];
[description appendString:customClsDesc];
[description appendString:[self descForCustomClass:obj prefixSpace:@"\t\t\t"]];
}
}
}
[description appendString:@"\n"];

/// responses
[description appendString:@"Response objects: \n"];
for (NSObject *obj in self.responses) {
// NSNumber Bool NSArray
if ([obj isKindOfClass:NSClassFromString(@"__NSCFBoolean")]) {
// Bool 类型
BOOL use = (BOOL)obj;
NSString *desc = use ? @"YES" : @"NO";
NSString *boolDesc = [NSString stringWithFormat:@"\tBool : %@\n", desc ?: @"Empty value"];
[description appendString:boolDesc];

} else if ([obj isKindOfClass:NSNumber.class]) {
NSNumber *use = (NSNumber *)obj;
NSString *numberDesc = [NSString stringWithFormat:@"\tNSNumber : %@\n", use ?: @"Empty value"];
[description appendString:numberDesc];
} else if ([obj isKindOfClass:NSArray.class]) {
NSArray *use = (NSArray *)obj;

if (use.count > 0) {
[description appendString:@"\t NSArray Items: \n"];
}

for (NSObject *subItem in use) {
if ([self isSystemClass:subItem.class]) {
// 系统类
NSString *systemClsDesc = [NSString stringWithFormat:@"\t\t%@ : %@ ", NSStringFromClass(subItem.class), subItem];
[description appendString:systemClsDesc];
} else {
// 自定义类
NSString *customClsDesc = [NSString stringWithFormat:@"\t\t%@ \n", NSStringFromClass(subItem.class)];
[description appendString:customClsDesc];
[description appendString:[self descForCustomClass:subItem prefixSpace:@"\t\t\t"]];
}
}
} else {
if ([self isSystemClass:obj.class]) {
// 系统类
NSString *systemClsDesc = [NSString stringWithFormat:@"\t%@ : %@ ", NSStringFromClass(obj.class), obj];
[description appendString:systemClsDesc];
} else {
// 自定义类
NSString *customClsDesc = [NSString stringWithFormat:@"\t%@ \n", NSStringFromClass(obj.class)];
[description appendString:customClsDesc];
[description appendString:[self descForCustomClass:obj prefixSpace:@"\t\t\t"]];
}
}
}

[description appendString:@"\n\n"];


return description;
}

- (BOOL)isSystemClass:(Class)cls {
NSBundle *bundle = [NSBundle bundleForClass:cls];
return bundle != [NSBundle mainBundle];
}

- (NSString *)descForCustomClass:(NSObject *)customObj prefixSpace:(NSString *)prefix {
NSMutableString *desc = [[NSMutableString alloc] initWithString:@""];

NSArray *allIvarNames = [self findIvars:customObj.class];

for (NSString *pName in allIvarNames) {

// 处理对象嵌套
NSObject *value = [customObj valueForKey:pName];
if ([self isSystemClass:value.class]) {
// 系统类,检查数组中的类型
if ([value isKindOfClass:NSArray.class]) {
NSArray *temp = (NSArray *)value;

if (temp.count > 0) {
[desc appendString:[NSString stringWithFormat:@"%@ %@ NSArray Items: \n", prefix, pName]];
}

for (NSObject *subValue in temp) {
prefix = [NSString stringWithFormat:@"\t%@", prefix];
NSString *tempStr = [NSString stringWithFormat:@"%@ %@: \n", prefix, NSStringFromClass(subValue.class)];
[desc appendString:tempStr];

NSString *nextDesc = [self descForCustomClass:subValue prefixSpace:[NSString stringWithFormat:@"\t%@", prefix]];
[desc appendString:nextDesc];
}
} else {
NSString *temp = [NSString stringWithFormat:@"%@ %@ : %@\n", prefix, pName, value];
[desc appendString:temp];
}
} else {
//
NSString *temp = [NSString stringWithFormat:@"%@ %@ : %@\n", prefix, pName, value];
[desc appendString:temp];

NSString *nextDesc = [self descForCustomClass:value prefixSpace:[NSString stringWithFormat:@"\t%@", prefix]];
[desc appendString:nextDesc];
}
}

return desc;
}

- (NSArray *)findIvars:(Class)cls {
NSMutableArray *result = [[NSMutableArray alloc] init];

unsigned int count;// 记录属性个数
objc_property_t *properties = class_copyPropertyList(cls, &count);
for (int i = 0; i < count; i++) {
objc_property_t property = properties[i];
// 获取属性的名称 C语言字符串
const char *cName = property_getName(property);
// 转换为Objective C 字符串
NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];

[result addObject:name];
}
return result.copy;
}

@end
  • SHMAaceHookCache:收集请求模型的管理者

  • UI 显示的部分就不写上来了,就是一个 TableView 展示数据了。

触发显示的话,这里用的是晃动手机。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@implementation UIWindow (ShakeEvent)

- (BOOL)canBecomeFirstResponder {//默认是NO,所以得重写此方法,设成YES
return YES;
}

- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event {
}

- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
if (motion == UIEventSubtypeMotionShake) {
// 展示 UI
}
}

- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event {
}

@end

总结

其实也没啥特殊的技巧,主要就是参照 Type Encodings 类型编码 来确定类型,进行对应的解析。