声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!
目标
手机验证码登录和账密登录
phone,pwd,kbsv等等需要还原
古法逆向
1. phone
看着像base64, hook一下 - [NSData base64EncodedStringWithOptions:],会发现根本不走这个方法,那就去hook序列化方法,看看转成json时是密文还是明文:
ObjC.classes.NSJSONSerialization["+ dataWithJSONObject:options:error:"]
1. 作用就是把 Foundation 对象转成 JSON 格式的 NSData
2. 常见输入:NSDictionary, NSMutableDictionary, NSArray, NSMutableArray
3. 输出:NSData *, 最终可以作为 HTTP body 的 JSON 字节流
frida注入,点击发送验证码,输出:
========== KFC phone ==========
class = __NSDictionaryM
phone = xxxxxxxxxxxxxxxxxx
encodeList = (
phone
)
mainBrand = KFC
secretKey = kfc
sendType = 3
caller = 0xxxxxxxxxxxxxxxxx
发现转成json时已经是密文了,并且传入的是__NSDictionaryM类型,所以继续去hook phone被放进字典的地方
console.log("[*] hook.js loaded");
if (!ObjC.available) {
console.log("[-] ObjC not available");
} else {
console.log("[*] ObjC available");
console.log("[*] will install hook after 3 seconds");
setTimeout(function () {
var hook_func = ObjC.classes.__NSDictionaryM['- setObject:forKey:'];
Interceptor.attach(hook_func.implementation, {
onEnter: function (args) {
var key = new ObjC.Object(args[3]).toString();
if (key == "phone") {
var value = new ObjC.Object(args[2]).toString();
console.log("[*] setObject:forKey: phone = " + value);
}
},
onLeave: function (retval) {
}
})
}, 3000);
}
重新注入点击发送,hook不到,说明phone 不是通过 setObject:forKey: 写进去的
补充:
虽然之前看到 class = __NSDictionaryM,但这只说明最终传给 JSON 的对象是可变字典,不代表它一定是这样生成的:[dict setObject:value forKey:@"phone"];
常见的情况:
1. 用下标写入
oc可能是:dict[@"phone"] = value
它走的是 -[__NSDictionaryM setObject:forKeyedSubscript:]
2. 字典是一次性构造的
比如:
@{
@"phone": encryptedPhone,
@"mainBrand": @"KFC"
}
走的是 -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]
3. 先创建不可变字典,再 mutableCopy
比如:
NSDictionary *d = @{
@"phone": encryptedPhone
};
NSMutableDictionary *m = [d mutableCopy];
最终你看到的是: __NSDictionaryM
4. Swift / 框架内部构造
如果这部分是 Swift 或网络框架内部生成的,可能走 CoreFoundation / Swift bridge, 不一定走你 hook 的 ObjC 方法
现在应该继续hook __NSDictionaryM["- setObject:forKeyedSubscript:"]和__NSPlaceholderDictionary["- initWithObjects:forKeys:count:"] 判断到底是写入还是一次性构造的
核心逻辑:
setObject:forKey: 普通写入
setObject:forKeyedSubscript: dict[@"phone"] = xxx
initWithObjects:forKeys:count: @{@"phone": xxx}
console.log("[*] hook.js loaded");
if (!ObjC.available) {
console.log("[-] ObjC not available");
} else {
console.log("[*] ObjC available");
console.log("[*] will install hook after 3 seconds");
setTimeout(function () {
var hook_func2 = ObjC.classes.__NSDictionaryM['- setObject:forKeyedSubscript:'];
Interceptor.attach(hook_func2.implementation, {
onEnter: function (args) {
var key = new ObjC.Object(args[3]).toString();
if (key == "phone") {
var value = new ObjC.Object(args[2]).toString();
console.log("[*] setObject:forKeyedSubscript: phone = " + value);
console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n')
}
},
onLeave: function (retval) {
}
})
}, 3000);
}
输出(已脱敏):
[Apple iPhone::PID::4927 ]-> [*] setObject:forKeyedSubscript: phone = xxxxxx
0x1042d3c SUPER_APP!-[YUMCUnitedUserAuthSmsCodeHttpService params]
0x105d708 SUPER_APP!-[YUMCApiManager buildRequestWithService:]
0x105d4bc SUPER_APP!-[YUMCApiManager loadDataWithService:callback:]
0x104672c SUPER_APP!-[YUMCUnitedUserAuthUserSignInService loadCodeSendingRequestWithService:completion:]
0x104e628 SUPER_APP!-[YUMCUnitedUserAuthUserSignInService codeLoginStepSendCodeWithPhone:codeType:completion:]
0x104e79c SUPER_APP!-[YUMCUnitedMsgCodeSendBiz initiateCodeSendWithType:phone:]
0x104ec74 SUPER_APP!-[YUMCUnitedPhoneLoginVC handleCodeSendingWithPhone:]
0x104ea50 SUPER_APP!-[YUMCUnitedMsgCodeInteractionBiz retrieveCodeAfterValidataion]
0x1824748 UIKitCore!-[UIApplication sendAction:to:from:forEvent:]
0x1831070 UIKitCore!-[UIControl sendAction:to:forEvent:]
0x104c888 SUPER_APP!-[UIButton(VerticalCenter) YUMCKFCElderClickExposure_sendAction:to:forEvent:]
0x104c830 SUPER_APP!-[UIButton(VerticalCenter) sendAction:to:forEvent:]
0x182e9ec UIKitCore!-[UIControl _sendActionsForEvents:withEvent:]
0x182f98c UIKitCore!-[UIButton _sendActionsForEvents:withEvent:]
0x18314c0 UIKitCore!-[UIControl touchesEnded:withEvent:]
0x104ce69 SUPER_APP!-[UIButton(VerticalCenter) touchesEnded:withEvent:]
会发现写入的时候已经是密文了,这里先做个补充:
1. __NSDictionaryM 是 NSMutableDictionary 在运行时的真实类名
平时代码里写的是:NSMutableDictionary *dict = [NSMutableDictionary dictionary];
但运行时真正的对象类名通常不是 NSMutableDictionary,而是 Foundation 的私有类: __NSDictionaryM , M = Mutable
2. NSDictionary / NSMutableDictionary / __NSDictionaryM 的关系:
NSDictionary
抽象公开类
NSMutableDictionary
抽象公开类,继承 NSDictionary
__NSDictionaryM
真正干活的私有实现类
3. 所以为什么不去hook NSMutableDictionary?
可以hook,但是很多时候 hook 不到。原因是 Objective-C 方法派发时,真正执行的是子类实现
例如对象真实类型是: __NSDictionaryM
调用:
[dict setObject:value forKey:key]
最终跑的可能是:
-[__NSDictionaryM setObject:forKey:]
而不是:
-[NSMutableDictionary setObject:forKey:]
所以实际逆向时常用:
-[__NSDictionaryM setObject:forKey:]
frida输出堆栈,方法的地址和ida里实际的地址会不一致,不能直接根据frida输出的某个方法的调用地址直接去ida里跳转。
因为 App 运行时有 ASLR,模块会被随机加载到一个 base。
正确换算是: IDA地址 = IDA镜像基址 + (运行时地址 - Frida运行时模块base)
function ida(addr) {
var m = Process.findModuleByAddress(addr);
var offset = addr.sub(m.base);
var idaAddr = ptr("0x100000000").add(offset);
console.log("runtime = " + addr);
console.log("base = " + m.base);
console.log("offset = " + offset);
console.log("IDA = " + idaAddr);
}
var imp = ObjC.classes.YUMCUnitedUserAuthSmsCodeHttpService["- params"].implementation;
ida(imp);
接着跟进顶层的param方法
进入-[YUMCUnitedUserAuthSmsCodeHttpService phone方法发现只是单纯 return self->_phone,所以这边也已经加密完成了。线索丢失了,去看看这个类是否有可疑的方法:
很明显有个setPhone,所以可以猜测:
phone在oc里的实现可能是: @property NSString *phone;
这样编译器通常会自动生成:
- phone
- setPhone:
继续hook
console.log("[*] hook.js loaded");
if (!ObjC.available) {
console.log("[-] ObjC not available");
} else {
console.log("[*] ObjC available");
console.log("[*] will install hook after 0.3 seconds");
setTimeout(function () {
var hook_func = ObjC.classes.YUMCUnitedUserAuthSmsCodeHttpService['- setPhone:'];
Interceptor.attach(hook_func.implementation, {
onEnter: function (args) {
console.log('setphone = ' + new ObjC.Object(args[2]).toString());
console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n')
},
onLeave: function (retval) {
// console.log('phone = ' + new ObjC.Object(retval).toString());
// console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n')
}
})
}, 300);
}
输出:
[Apple iPhone::PID::6658 ]-> setphone = xxxxxxxxx
0x1054f0 SUPER_APP!-[YUMCUnitedUserAuthUserSignInService codeLoginStepSendCodeWithPhone:codeType:completion:]
0x10579c SUPER_APP!-[YUMCUnitedMsgCodeSendBiz initiateCodeSendWithType:phone:]
0x105a74 SUPER_APP!-[YUMCUnitedPhoneLoginVC handleCodeSendingWithPhone:]
0x105e50 SUPER_APP!-[YUMCUnitedMsgCodeInteractionBiz retrieveCodeAfterValidataion]
0x182748 UIKitCore!-[UIApplication sendAction:to:from:forEvent:]
0x183170 UIKitCore!-[UIControl sendAction:to:forEvent:]
0x105888 SUPER_APP!-[UIButton(VerticalCenter) YUMCKFCElderClickExposure_sendAction:to:forEvent:]
0x105830 SUPER_APP!-[UIButton(VerticalCenter) sendAction:to:forEvent:]
0x182eec UIKitCore!-[UIControl _sendActionsForEvents:withEvent:]
0x18298c UIKitCore!-[UIButton _sendActionsForEvents:withEvent:]
0x1834c0 UIKitCore!-[UIControl touchesEnded:withEvent:]
0x105910 SUPER_APP!-[UIButton(VerticalCenter) touchesEnded:withEvent:]
0x182190 UIKitCore!_UIGestureEnvironmentUpdate
0x18212c UIKitCore!-[UIGestureEnvironment _updateForEvent:window:]
0x182c30 UIKitCore!-[UIWindow sendEvent:]
0x182a64 UIKitCore!-[UIApplication sendEvent:]
也已经是密文状态,ida跟进-[YUMCUnitedUserAuthUserSignInService codeLoginStepSendCodeWithPhone:codeType:completion:]方法,会看到
v13就是frida输出的密文,来源于encryptedPassword方法返回值,传入v10也就是明文手机号,跟进这个方法
很明显最终调用的是- [YUMCUnitedBasicUserBusiness unitedLoginDidEnctyptString:],跟进去后发现它属于OBJC_UNDEF段,看不到它的函数实现,去Functions框直接搜索YUMCUnitedBasicUserBusiness unitedLoginDidEnctyptString: 也是搜不到的
在frida注入的时候console.log(ObjC.classes.YUMCUnitedBasicUserBusiness);也是找不到的,可能是在别的文件实现的,这边只做导入调用
这边可以尝试:
//法一:
var soAddr = Process.getModuleByName('SUPER_APP').base
var inline_B80FE4 = soAddr.add(0xB80FE0 ) // 这边就是要 hook 的地址,需要加上基址
Interceptor.attach(inline_B80FE4, {
onEnter: function(args) {
console.log('this.context.x0 = ' + ObjC.Object(this.context.x0).toString());
console.log('this.context.x0 = ' + ObjC.Object(this.context.x0).$className);
},
onLeave: function(retval) {
}
})
//法二:
console.log("[*] hook.js loaded");
if (!ObjC.available) {
console.log("[-] ObjC not available");
} else {
console.log("[*] ObjC available");
console.log("[*] will install hook after 0.3 seconds");
setTimeout(function () {
for (var name in ObjC.classes) {
try {
var cls = ObjC.classes[name];
var methods = cls.$ownMethods;
methods.forEach(function (m) {
if (m.indexOf("unitedLoginDidEnctyptString") !== -1) {
console.log(name + " " + m);
}
});
} catch (e) {}
}
}, 300);
}
发现它是属于UserService类的,这是什么情况?
因为 IDA 的静态分析看到的是“声明/静态类型”,运行时真正派发到的是“实际对象类型”。
你看到:
IDA: -[YUMCUnitedBasicUserBusiness unitedLoginDidEnctyptString:]
Frida: UserService - unitedLoginDidEnctyptString:
说明:
v6 在 IDA 里被推断成 YUMCUnitedBasicUserBusiness 类型
但运行时 v6 实际对象是 UserService
Objective-C 方法调用是动态派发的。
举个简单的例子:
YUMCUnitedBasicUserBusiness *v6 = [UserService sharedInstance];
[v6 unitedLoginDidEnctyptString:phone];
静态看,变量类型是:YUMCUnitedBasicUserBusiness *
所以ida显示: -[YUMCUnitedBasicUserBusiness unitedLoginDidEnctyptString:]
但运行时对象真实类是: UserService,所以真正执行的是 -[UserService unitedLoginDidEnctyptString:]
还有一种情况:继承
@interface UserService : YUMCUnitedBasicUserBusiness @end
父类声明了方法:- unitedLoginDidEnctyptString:
子类 UserService 实现了它。
那 IDA 静态调用点会显示父类:YUMCUnitedBasicUserBusiness
但 runtime 查到实现类是:UserService
这也完全正常。
所以应该进入UserService unitedLoginDidEnctyptString:方法
传入了明文和key,继续跟进
再跟
这边就是最终加密的方法了,传入明文,0加密/1解密,key,只需要分析0的分支即可
分析过程省略,伪代码如下
先对 手机号v7 进行utf8编码 得到v10 nsdata
v11 = v10 length
v12 = v10 bytes
v20 = 0
v14 = key
CCCrypt(a4, 1u, 1u, v14, 8uLL, v14, v12, v11, dataOut, (v11 & 0xFFFFFFFFFFFFFFF8LL) + 8, &v20);
des cbc加密 key==iv = D963r1wp(phone)
v16 = dataout,v20 传入dataWithBytes:length: 转nsdata
v17 = v16 toBase64
return v17
使用加解密工具验证,结果一致
2. 请求头
注:有些是定值,学习为主都去看看
a. rcsav
headers和前面phone的hook方法就不一样了,headers一般不走NSJSONSerialization,它主要处理的是body里的json,而headers通常是设置到 NSMutableURLRequest里:
例如:
[request setValue:value forHTTPHeaderField:@"rcsdcid"];
[request setAllHTTPHeaderFields:headersDict];
hook:
console.log("[*] hook.js loaded");
if (!ObjC.available) {
console.log("[-] ObjC not available");
} else {
console.log("[*] ObjC available");
setTimeout(function () {
var set_header = ObjC.classes.NSMutableURLRequest["- setValue:forHTTPHeaderField:"];
var set_headers = ObjC.classes.NSMutableURLRequest["- setAllHTTPHeaderFields:"];
Interceptor.attach(set_header.implementation, {
onEnter: function (args) {
var headerField = new ObjC.Object(args[3]);
var headerKey = headerField.toString();
var headerKeyLower = headerKey.toLowerCase();
if (headerKeyLower.indexOf("rcsav") == 0) {
var headerValue = new ObjC.Object(args[2]);
console.log('header_key: ' + headerKey + ' header_value: ' + headerValue.toString());
console.log(Thread.backtrace(this.context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join('\n') + '\n')
}
},
onLeave: function (retval) {}
})
Interceptor.attach(set_headers.implementation, {
onEnter: function (args) {
var headers = new ObjC.Object(args[2]);
try {
var keys = headers.allKeys();
var count = keys.count();
for (var i = 0; i < count; i++) {
var k = keys.objectAtIndex_(i);
var key = k.toString();
var keyLower = key.toLowerCase();
// 模糊匹配:rcsav 开头的 header
if (keyLower.indexOf("rcsav") == 0) {
var v = headers.objectForKey_(k);
console.log('header_key: ' + key + ' header_value: ' + v.toString());
console.log(Thread.backtrace(this.context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join('\n') + '\n')
}
}
} catch (e) {
console.log('setAllHTTPHeaderFields parse error: ' + e);
}
},
onLeave: function (retval) {}
})
}, 30);
}
根据堆栈定位到顶层函数[YUMCUnitedUserAuthSmsCodeHttpService interceptRequest:]
可以hook看看这个函数的入参和返回值是啥
console.log("[*] hook.js loaded");
if (!ObjC.available) {
console.log("[-] ObjC not available");
} else {
console.log("[*] ObjC available");
setTimeout(function () {
var inter_req = ObjC.classes.YUMCUnitedUserAuthSmsCodeHttpService["- interceptRequest:"];
Interceptor.attach(inter_req.implementation, {
onEnter: function (args) {
var req = new ObjC.Object(args[2]);
console.log('\n========== interceptRequest ==========');
console.log('request: ' + req.toString());
try {
console.log('URL: ' + req.URL().absoluteString().toString());
} catch (e) {
console.log('URL: <error> ' + e);
}
try {
var method = req.HTTPMethod();
console.log('method: ' + (method && !method.handle.isNull() ? method.toString() : 'nil'));
} catch (e) {
console.log('method: <error> ' + e);
}
try {
var headers = req.allHTTPHeaderFields();
console.log('\n[headers]');
if (headers && !headers.handle.isNull()) {
console.log(headers.toString());
} else {
console.log('nil');
}
} catch (e) {
console.log('[headers error] ' + e);
}
try {
var body = req.HTTPBody();
console.log('\n[body]');
if (body && !body.handle.isNull()) {
var len = parseInt(body.length().toString());
console.log('length: ' + len);
var bodyStr = ObjC.classes.NSString.alloc().initWithData_encoding_(body, 4);
if (bodyStr && !bodyStr.handle.isNull()) {
console.log('utf8: ' + bodyStr.toString());
} else {
console.log('utf8: nil');
}
console.log('hex:');
console.log(hexdump(body.bytes(), {
offset: 0,
length: len,
header: true,
ansi: false
}));
} else {
console.log('nil');
}
} catch (e) {
console.log('[body error] ' + e);
}
console.log('======================================\n');
},
onLeave: function (retval) {
var res = new ObjC.Object(retval)
try {
var headers = res.allHTTPHeaderFields();
console.log('\n[headers]');
if (headers && !headers.handle.isNull()) {
console.log(headers.toString());
} else {
console.log('nil');
}
} catch (e) {
console.log('[headers error] ' + e);
}
}
})
}, 30);
}
传入一个NSMutableURLRequest对象,要看里面的内容得调用对应的oc方法:
var req = ObjC.Object(args[2]);
1. toString() 得到的就是它的description,一般是地址和url信息
2. .URL().absoluteString().toString() 得到url字符串
3. .HTTPMethod().toString() 得到请求方式 get post...
4. .allHTTPHeaderFields().toString() 得到请求头
5. body(NSData类型的)
一种是 hexdump(req.HTTPBody().bytes())
另一种是 var bodyStr = ObjC.classes.NSString.alloc().initWithData_encoding_(body, 4);
console.log("body string = " + bodyStr.toString());
6. .allHTTPHeaderFields().toString() 得到headers
附:hook的时候要看实参内容:
1. Objective-C 对象指针
优先使用ObjC.Object(ptr).toString() 或者调用它的方法
2. NSData 的 bytes
使用hexdump,因为它是原始字节
3. char * c字符串
ptr.readUTF8String(), 还有一些别的readxxxx方法
4. 原始 buffer
hexdump
结果比较多我就不放了,入参时请求头只有User-Agent,Content-Type,Accept-Language,其余的除了x-yumc开头的都是这个函数内部添加的
先去追rcasv,函数内部很明显,和之前的phone属于同一个类,发现还有setRcsav方法
hook它,看看是哪里赋值的
// YUMCUnitedUserAuthSmsCodeHttpService setRcsav:
console.log("[*] hook.js loaded");
if (!ObjC.available) {
console.log("[-] ObjC not available");
} else {
console.log("[*] ObjC available");
console.log("[*] will install hook after 0.3 seconds");
setTimeout(function () {
var hook_func = ObjC.classes.YUMCUnitedUserAuthSmsCodeHttpService['- setRcsav:'];
Interceptor.attach(hook_func.implementation, {
onEnter: function (args) {
console.log('rcsav = ' + ObjC.Object(args[2]).toString());
console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n')
},
onLeave: function (retval) {
}
})
}, 300);
}
发现好几个参数都是这边调用set方法设置的,rscav就是app的版本号
+[NSBundle mainBundle]
↓
-[NSBundle infoDictionary]
↓
objectForKeyedSubscript:@"CFBundleShortVersionString"
↓
setRcsav:
也就是 Info.plist 里的 CFBundleShortVersionString,位置在ipa解包后的Payload/xxx.app/Info.plist:
这是 iOS App 包的固定结构,一个 iOS 应用打包成 .ipa 后,本质上是一个 zip 包。解压后结构通常是:
Payload/
xxx.app/
Info.plist
xxx # 主可执行文件
Frameworks/
PlugIns/
...
Info.plist是是 iOS App 必须有的元信息文件,苹果规定 App 的版本号、Bundle ID、显示名称等元信息都放在 bundle 的 Info.plist 里。常见字段:
CFBundleIdentifier Bundle ID
CFBundleShortVersionString App 显示版本号
CFBundleVersion Build 版本号
CFBundleDisplayName App 显示名称
至此rcsav结束
b. rcsdcid
和rcsav同一个方法
比较简单,就是标准的UUID
v3 = objc_retainAutoreleasedReturnValue(+[NSUUID UUID](&OBJC_CLASS___NSUUID, "UUID"));
v4 = objc_retainAutoreleasedReturnValue(-[NSUUID UUIDString](v3, "UUIDString"));
v5 = objc_retainAutoreleasedReturnValue(-[NSString lowercaseString](v4, "lowercaseString"));
objc_msgSend(v2, "setCrUUID:", v5);
c. kbsv,kbck,kbcts
inlinehook一下后一行,查看v4的请求头就知道这三个头是在里头复制的,接着先hook YUMCSecret_secretRequestWithPath:clientKey:clientSec:requestType:params: 看看入参
多测试几次发现key和sec是定值,string表可以直接查到,params就是请求发送的所有参数字典,传进去的requestType = 1,所以只需要看这一块代码即可
其中有个YUMC_MD5就是标准的md5算法,最后转32位小写
id __cdecl -[NSString YUMC_MD5](NSString *self, SEL a2)
{
NSData *v2; // x19
void *v3; // x20
__int64 i; // x23
unsigned __int8 md[16]; // [xsp+8h] [xbp-48h] BYREF
v2 = objc_retainAutorelease(objc_retainAutoreleasedReturnValue(-[NSString dataUsingEncoding:](self, "dataUsingEncoding:", 4LL)));
CC_MD5(-[NSData bytes](v2, "bytes"), (CC_LONG)-[NSData length](v2, "length"), md);
v3 = objc_retainAutoreleasedReturnValue(+[NSMutableString string](&OBJC_CLASS___NSMutableString, "string"));
for ( i = 0LL; i != 16; ++i )
objc_msgSend(v3, "appendFormat:", CFSTR("%02x"), md[i]);
objc_release(v2);
return objc_autoreleaseReturnValue(v3);
}
整体的流程大概就是
d. x-yumc-client-deviceid和x-yumc-client-citycode
同样的hook设置请求头的方法定位该请求头位置
header_key: x-yumc-client-deviceid header_value: c0280xxxxxxxxxxxxx30de5xxxxxxxxd
0x18084e8c8 CoreFoundation!__NSDICTIONARY_IS_CALLING_OUT_TO_A_BLOCK__
0x1808a9120 CoreFoundation!-[__NSDictionaryI enumerateKeysAndObjectsWithOptions:usingBlock:]
0x103423180 SUPER_APP!-[YUMCNetworkingTrackDemux addtionRequestHeader:]
0x103423200 SUPER_APP!0xa9f200 (0x100a9f200)
0x103422dac SUPER_APP!-[YUMCApiProxy YUMCNetworking_X23_callApiWithRequest:callBack:]
0x103424098 SUPER_APP!-[YUMCApiProxy callApiWithRequest:callBack:]
0x103d3d598 SUPER_APP!-[YUMCApiManager loadDataWithService:callback:]
0x10350672c SUPER_APP!-[YUMCUnitedUserAuthUserSignInService loadCodeSendingRequestWithService:completion:]
0x103506628 SUPER_APP!-[YUMCUnitedUserAuthUserSignInService codeLoginStepSendCodeWithPhone:codeType:completion:]
0x1034c979c SUPER_APP!-[YUMCUnitedMsgCodeSendBiz initiateCodeSendWithType:phone:]
0x1034c97f4 SUPER_APP!0xb457f4 (0x100b457f4)
0x1034e5a74 SUPER_APP!-[YUMCUnitedPhoneLoginVC handleCodeSendingWithPhone:]
0x1034c7e50 SUPER_APP!-[YUMCUnitedMsgCodeInteractionBiz retrieveCodeAfterValidataion]
0x182fe4748 UIKitCore!-[UIApplication sendAction:to:from:forEvent:]
0x183103870 UIKitCore!-[UIControl sendAction:to:forEvent:]
0x191aaec6c libsystem_malloc.dylib!free
继续hook -[YUMCNetworkingTrackDemux addtionRequestHeader:] 看入参
console.log("[*] hook.js loaded");
if (!ObjC.available) {
console.log("[-] ObjC not available");
} else {
console.log("[*] ObjC available");
console.log("[*] will install hook after 0.3 seconds");
setTimeout(function () {
var hook_func = ObjC.classes.YUMCNetworkingTrackDemux['- addtionRequestHeader:'];
Interceptor.attach(hook_func.implementation, {
onEnter: function (args) {
var arg = new ObjC.Object(args[2]);
console.log('headers = ' + arg.allHTTPHeaderFields().toString());
console.log('body = ' + hexdump(arg.HTTPBody().bytes()))
// console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n')
},
onLeave: function (retval) {
}
})
}, 10000);
}
发现传入的是,打印它的请求头和body发现就是之前设置好的所有头和body,现在就只差x-yumc开头的这几个头,进入addtionRequestHeader方法,很明显剩下几个header是block内设置的
先hook一下v7看看返回是什么
this.context.x0 = <**NSGlobalBlock**: 0x102751cd8>
this.context.x0 type = NSGlobalBlock
v7为一个block对象,v8就是是取 block 的 invoke 函数地址调用后的结果,返回的肯定是dictionary对象,最后调用enumerateKeysAndObjectsUsingBlock:v9 设置请求头
Block 的代码位置就是它结构里的 invoke 指针,也就是你看到的:v7[2]
两种hook方法:
var soAddr = Process.getModuleByName('SUPER_APP').base;
var inline_B7ED00 = soAddr.add(0xA9F118);
function printBlockInvoke(blockPtr) {
try {
// 64 位 Block 布局:
// 0x00 isa
// 0x08 flags + reserved
// 0x10 invoke 函数指针
var invoke = blockPtr.add(0x10).readPointer();
console.log('block ptr = ' + blockPtr);
console.log('block invoke = ' + invoke);
console.log('block symbol = ' + DebugSymbol.fromAddress(invoke));
var m = Process.findModuleByAddress(invoke);
if (m) {
var offset = invoke.sub(m.base);
console.log('block module = ' + m.name);
console.log('block base = ' + m.base);
console.log('block offset = ' + offset);
console.log('block IDA = ' + ptr('0x100000000').add(offset));
}
} catch (e) {
console.log('printBlockInvoke error = ' + e);
}
}
console.log('[*] SUPER_APP base = ' + soAddr);
console.log('[*] hook target = ' + inline_B7ED00);
console.log('[*] target symbol = ' + DebugSymbol.fromAddress(inline_B7ED00));
Interceptor.attach(inline_B7ED00, {
onEnter: function(args) {
try {
var x0obj = ObjC.Object(this.context.x0);
console.log('this.context.x0 = ' + x0obj);
console.log('this.context.x0 type = ' + x0obj.$className);
if (x0obj.$className.indexOf('Block') !== -1) {
printBlockInvoke(this.context.x0);
}
} catch (e) {
console.log('x0 parse error = ' + e);
console.log('x0 raw = ' + this.context.x0);
}
},
onLeave: function(retval) {
try {
console.log('this.context.x20 = ' + ObjC.Object(this.context.x20));
console.log('this.context.x20 type = ' + ObjC.Object(this.context.x20).$className);
} catch (e) {
console.log('x20 parse error = ' + e);
console.log('x20 raw = ' + this.context.x20);
}
}
});
var track = ObjC.classes.YUMCNetworkingTrack.shareInstance();
var block = track.additionHeaderBlock();
console.log("block = " + block);
var invoke = block.handle.add(0x10).readPointer();
console.log("invoke = " + invoke);
console.log("symbol = " + DebugSymbol.fromAddress(invoke));
var m = Process.findModuleByAddress(invoke);
console.log("module = " + m.name);
console.log("base = " + m.base);
console.log("offset = " + invoke.sub(m.base));
console.log("IDA = " + ptr("0x100000000").add(invoke.sub(m.base)));
为什么要add(0x10)?
64 位 Block 结构大概是:
struct Block {
void *isa; // offset 0x00
int flags; // offset 0x08
int reserved; // offset 0x0c
void *invoke; // offset 0x10
void *descriptor; // offset 0x18
};
最后根据终端输出的ida地址 0x1000ca530,定位到该block,需要重点看的就是deviceid
整体流程大概就是:先看缓存,存在则直接返回,不存在继续计算,从 Keychain 取 UUID,如果 Keychain 没有,就新生成 UUID,对 UUID 做 MD5,缓存并返回
Keychain 是 iOS 系统提供的安全存储区,可以理解成:比 NSUserDefaults 更安全、更持久的本地数据库
App 常用它保存:
token
账号标识
设备 UUID
加密 key
登录凭证
证书
它有几个特点:
1. 比普通文件安全
普通数据可能存在:NSUserDefaults 沙盒文件 plist SQLite 这些比较容易被看到。Keychain 由系统安全服务管理,权限更严格。
2. 可以持久保存
很多 App 把设备 UUID 存到 Keychain,因为卸载 App 后,Keychain 数据有时仍然保留,重新安装还能读回来,所以适合做“稳定设备标识”。
3. 按 App 权限隔离
一般情况下,一个 App 只能访问自己的 Keychain item。如果配置了 Keychain Access Group,同一开发者的多个 App 可以共享。
3. pwd和smsCode
使用账密登录就是多了一个pwd以及获取验证码后的登录多了一个smsCode,它们的流程和phone一样,des-->base64;
4. 成果
短信
账密(填错误的pwd就是返回这些)
AI 逆向
新开一个会话,让ai从0开始分析,全程不干扰,半小时底裤都扒光了
总结
记录一下IOS逆向实战学习过程,一起学习·交流可加:yyxxkkyyxxkk
说明 每个请求参数的意义、来源和生成算法。
一、通用加密算法
----------------
用于加密:
- phone
- pwd
算法:
Base64(DES-CBC-PKCS7(明文))
参数:
- DES key: xxxxxxxx
- DES iv : xxxxxxxx
- 模式 : CBC
- Padding: PKCS7
- 输出 : Base64 字符串
IDA 对应逻辑:
- +[NSString encryptWithText:andKey:]
- +[NSString encrypt:encryptOrDecrypt:key:]
- CCCrypt(op=0, alg=DES, options=PKCS7, key=key, iv=key)
- GTMBase64 stringByEncodingData
Python 对应:
def des_b64(text: str, key: str = "xxxxxxxx") -> str:
key_bytes = key.encode("utf-8")
data = pad(text.encode("utf-8"), DES.block_size)
cipher = DES.new(key_bytes, DES.MODE_CBC, iv=key_bytes)
return base64.b64encode(cipher.encrypt(data)).decode("utf-8")
二、签名参数
------------
请求头中关键签名字段:
- kbck
- kbcts
- kbsv
1. kbck
意义:
客户端 key。
值:
xxxxxxxx
来源:
IDA 中 +[YUMCKFCNetworkingInitializer initSDK]
2. clientSec
意义:
客户端签名密钥,只参与 kbsv 计算,不直接放进 header。
值:
xxxxxxxx
来源:
IDA 中 +[YUMCKFCNetworkingInitializer initSDK]
3. kbcts
意义:
客户端毫秒时间戳。
生成算法:
kbcts = str(int(time.time() * 1000))
示例:
1752141234567
4. kbsv
意义:
请求签名。
生成算法:
sign_src = (
kbck + "\t" +
clientSec + "\t" +
kbcts + "\t" +
"/" + path + "\t\t" +
body_json
)
kbsv = md5(sign_src.encode("utf-8")).hexdigest()
注意:
- 输出是小写 MD5 hex。
- path 不带 /api。
- 实际请求 URL 带 /api。
短信接口:
- 请求 URL : /api/user/sendSmsCode
- 签名 path: user/sendSmsCode
- 签名中使用: /user/sendSmsCode
密码登录接口:
- 请求 URL : /api/user/loginByPassword
- 签名 path: user/loginByPassword
- 签名中使用: /user/loginByPassword
三、JSON 序列化
---------------
IDA 使用:
NSJSONSerialization dataWithJSONObject:options:0
Python 对应:
json.dumps(body, separators=(",", ":"), ensure_ascii=False)
特点:
- 无多余空格
- UTF-8
- body_json 既用于签名,也作为真实请求体发送
- 签名和请求体必须使用同一份 body_json
四、phone.py 参数说明
--------------------
接口:
https://xxxxx
签名 path:
user/sendSmsCode
Body:
{
"phone": des_b64(phone),
"mainBrand": "KFC",
"sendType": "3",
"encodeList": ["phone"],
"isFromCustomerClient": True,
"secretKey": "kfc",
}
1. phone
意义:
手机号,加密后传输。
生成算法:
phone = DES-CBC-PKCS7-Base64(手机号)
Python:
"phone": des_b64(phone)
2. mainBrand
意义:
主品牌。
固定值:
KFC
3. sendType
意义:
验证码发送类型。
当前值:
3
IDA 中来自:
setSendType:
4. encodeList
意义:
告诉服务端哪些字段被加密。
短信接口只加密手机号:
["phone"]
5. isFromCustomerClient
意义:
标记请求来自客户客户端。
固定值:
True
6. secretKey
意义:
加密品牌标识。
固定值:
kfc
来源:
encryptionBrand
五、pwd.py 参数说明
-------------------
接口:
https://xxxxx
签名 path:
user/loginByPassword
Body:
{
"phone": des_b64(phone),
"gbCityCode": CITY_CODE,
"isFromCustomerClient": True,
"deviceId": device_id,
"secretKey": "kfc",
"encodeList": ["phone", "pwd"],
"mainBrand": "KFC",
"subBrands": ["PHHS", "LA", "TACO", "NLS", "HJH"],
"pwd": des_b64(password),
}
1. phone
意义:
手机号,加密后传输。
生成算法:
phone = DES-CBC-PKCS7-Base64(手机号)
2. pwd
意义:
密码,加密后传输。
生成算法:
pwd = DES-CBC-PKCS7-Base64(密码)
说明:
pwd 和 phone 使用同一套 DES 加密算法。
3. gbCityCode
意义:
城市编码。
当前值:
350200
脚本变量:
CITY_CODE = "350200"
同时 header 中也会带:
x-yumc-client-citycode: 350200
4. isFromCustomerClient
意义:
标记请求来自客户客户端。
固定值:
True
5. deviceId
意义:
设备 ID。
当前脚本生成算法:
device_id = str(uuid.uuid4()).upper()
示例:
A1B2C3D4-1111-2222-3333-123456789ABC
说明:
pwd.py 中 body 的 deviceId 和 header 的 x-yumc-client-deviceid 使用同一个值。
6. secretKey
意义:
加密品牌标识。
固定值:
kfc
7. encodeList
意义:
告诉服务端哪些字段被加密。
密码登录接口加密两个字段:
["phone", "pwd"]
8. mainBrand
意义:
主品牌。
固定值:
KFC
9. subBrands
意义:
联合登录的子品牌列表。
当前值:
["PHHS", "LA", "TACO", "NLS", "HJH"]
IDA 中来自:
setSubBrands:
六、请求头参数说明
------------------
4. rcsav
意义:
App 版本号。
当前值:
6.34.1
脚本变量:
APP_VERSION = "6.34.1"
5. rcsdcid
意义:
请求/设备追踪 ID。
当前脚本生成算法:
str(uuid.uuid4()).lower()
示例:
a1b2c3d4-1111-2222-3333-123456789abc
7. x-yumc-client-deviceid
意义:
设备 ID。
当前脚本生成算法:
device_id = str(uuid.uuid4()).upper()
说明:
密码登录时,它和 body 中的 deviceId 保持一致。
8. x-yumc-client-channel
意义:
客户端渠道。
固定值:
app
9. user-agent
意义:
模拟 App UA。
当前格式:
SUPER_APP/6.34.1 (iPhone; iOS 15.8.3; Scale/3.00) iOS KFCSuperAPP v6.34.1
生成方式:
由 APP_VERSION 拼接。
10. x-yumc-client-citycode
意义:
城市编码。
当前值:
350200
11. kbck
意义:
客户端 key。
固定值:
xxxxxxxx
12. kbcts
意义:
毫秒时间戳。
生成算法:
str(int(time.time() * 1000))
13. kbsv
意义:
MD5 请求签名。
生成算法:
md5(
kbck + "\t" +
clientSec + "\t" +
kbcts + "\t" +
"/" + sign_path + "\t\t" +
body_json
)
输出:
小写 32 位 hex 字符串。
七、固定常量汇总
----------------
ENC_KEY:
xxxxxxxx
CLIENT_KEY / kbck:
xxxxxxxx
CLIENT_SEC:
xxxxxxxx
APP_VERSION:
6.34.1
CITY_CODE:
350200
phone.py URL:
https://xxxxx
phone.py SIGN_PATH:
user/sendSmsCode
pwd.py URL:
https://xxxxx
pwd.py SIGN_PATH:
user/loginByPassword