声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!
目标
body中的tel,部分请求头,不一一列举了
这边由于测试次数过多,无法发送验证码了,目的就是通过协议拿到这个响应
古法逆向
1. tel
处于body中,先hook json序列化方法看看
var json_ = ObjC.classes.NSJSONSerialization["+ dataWithJSONObject:options:error:"].implementation; var telKey = ObjC.classes.NSString.stringWithString_("tel"); function showAddr(addr) { var m = Process.findModuleByAddress(addr); var mod = m ? (m.name + " + 0x" + addr.sub(m.base).toString(16)) : "unknown"; return addr + " " + mod + " " + DebugSymbol.fromAddress(addr); } Interceptor.attach(json_, { onEnter: function (args) { this.hasTel = false; var arg2 = ObjC.Object(args[2]); var tel = arg2.objectForKey_(telKey); if (!tel.isNull()) { this.hasTel = true; console.log(111) console.log('args[2]:' + arg2); console.log('args[2] type :' + arg2.$className); console.log('tel:' + ObjC.Object(tel)); console.log('[caller ret] ' + showAddr(this.returnAddress)); } }, onLeave: function (retval) { } })ida进入-[MCDHTTPEngine URLRequestWithModernRequest:]方法,发现这个__NSDictionary对象是方法接受的第一个参数,继续hook
// MCDHTTPEngine URLRequestWithModernRequest: var json_ = ObjC.classes.MCDHTTPEngine["- URLRequestWithModernRequest:"].implementation; function showAddr(addr) { var m = Process.findModuleByAddress(addr); var mod = m ? (m.name + " + 0x" + addr.sub(m.base).toString(16)) : "unknown"; return addr + " " + mod + " " + DebugSymbol.fromAddress(addr); } Interceptor.attach(json_, { onEnter: function (args) { var arg2 = ObjC.Object(args[2]); console.log('args[2]:' + arg2); console.log('args[2] type :' + arg2.$className); console.log('[caller ret] ' + showAddr(this.returnAddress)); } , onLeave: function (retval) { } })进入-[MCDHTTPEngine fetchUrlRequest:] 发现传入-[MCDHTTPEngine URLRequestWithModernRequest:] 方法的同样是接受的第一个参数,继续hook
var func_ = ObjC.classes.MCDHTTPEngine['- fetchUrlRequest:']; Interceptor.attach(func_.implementation, { onEnter: function (args){ console.log('args[2]:' + ObjC.Object(args[2])); console.log('args[2] type :' + ObjC.Object(args[2]).$className); // console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n') var m = Process.findModuleByAddress(this.returnAddress); var offset = this.returnAddress.sub(m.base); console.log('offset:' + offset.toString(16)); console.log('func: ', DebugSymbol.fromAddress(this.returnAddress)); } })继续追上层调用,找到该偏移值所处代码位置
会发现传入的__NSDictionary对象就是 *(_QWORD *)(a1 + 40),再去看a1来源以及本函数入参
会发现一个问题,传进来int64类型的a1,给它加上40转为指针再解引用,就变成了一个__NSDictionary对象,我第一眼看的时候也很抽象,实际上,这是ida的原因,a1是一个block
IDA 里显示成: __int64 a1,只是因为反编译器不知道它真实类型,所以按 64 位整数展示了。 在 64 位程序里: 指针 = 8 字节 = 64 bit = __int64 / uint64_t 所以 IDA 常把未知对象指针写成: __int64 a1 void *a1 _QWORD a1 它们本质上都可能是地址。这里为什么能确定它像 block?因为访问结构很典型: *(id *)(a1 + 32) *(_QWORD *)(a1 + 40) block 对象在 arm64 下大概是: struct Block_literal { void *isa; // a1 + 0x00 int flags; // a1 + 0x08 int reserved; // a1 + 0x0c void *invoke; // a1 + 0x10 void *descriptor; // a1 + 0x18 // 后面开始是捕获变量 id capture0; // a1 + 0x20 / 32 id capture1; // a1 + 0x28 / 40 }; 所以: *(id *)(a1 + 32) 就是 block 捕获的第一个对象。 *(_QWORD *)(a1 + 40) 就是 block 捕获的第二个对象。 换成更准确的类型可以这样理解: id __fastcall sub_10024DE14(struct Block_literal *block) { id engine = block->capture0; id request = block->capture1; id urlReq = [engine fetchUrlRequest:request]; } 或者伪结构: struct MyBlock { void *isa; int flags; int reserved; void *invoke; void *descriptor; id capturedEngine; // +0x20 id capturedRequest; // +0x28 }; 对应的代码: objc_msgSend(*(id *)(a1 + 32), "fetchUrlRequest:", *(_QWORD *)(a1 + 40)); 就是: [block->capturedEngine fetchUrlRequest:block->capturedRequest];所以: __int64 a1 不代表它真的是整数业务参数,只是 IDA 没识别出来。 你可以在 IDA 里按 Y 改函数签名,比如改成: id __fastcall sub_10024DE14(void *block) 或者更语义化: id __fastcall sub_10024DE14(struct MyBlock *block) 这样看起来会更清楚。hook当前函数或者按x查看交叉引用,定位传入block的位置,进而跟踪block的创建位置
// 24DE14 var base = Process.findModuleByName("McdApp").base; var addr = base.add(0x24DE14); Interceptor.attach(addr, { onEnter: function (args) { // sub_10024DE14(__int64 a1) // a1 是 block 地址,所以是 args[0],不是 args[2] var block = args[0]; if (block.isNull()) { console.log("block is NULL"); return; } // block + 0x20 == *(id *)(a1 + 32) // block + 0x28 == *(_QWORD *)(a1 + 40) var enginePtr = block.add(0x20).readPointer(); var paramPtr = block.add(0x28).readPointer(); console.log("block:", block); console.log("enginePtr:", enginePtr); console.log("paramPtr:", paramPtr); if (!enginePtr.isNull()) { var engine = ObjC.Object(enginePtr); console.log("engine type:", engine.$className); console.log("engine:", engine); } if (!paramPtr.isNull()) { var param = ObjC.Object(paramPtr); console.log("param type:", param.$className); console.log("param:", param); } var m = Process.findModuleByAddress(this.returnAddress); var offset = this.returnAddress.sub(m.base); console.log('offset:' + offset.toString(16)); console.log('func: ', DebugSymbol.fromAddress(this.returnAddress)); } })跟进 -[MCDHTTPEngine createRequestWithOriginRequest:] 方法可以看到
这几行局部变量的栈偏移: void **v10; // [xsp+0h] __int64 v11; // [xsp+8h] id (*v12)(...); // [xsp+10h] void *v13; // [xsp+18h] MCDHTTPEngine *v14; // [xsp+20h] id v15; // [xsp+28h] 然后代码: v10 = _NSConcreteStackBlock; v11 = 3254779904LL; v12 = sub_10024DE14; v13 = &unk_102866F08; v14 = self; v3 = objc_retain(objc_retain(a3)); v15 = v3; v4 = objc_retainBlock(&v10); 因为 v10 在 [xsp+0],它就是这个 stack block 的起始地址。 所以: v10 = block + 0x00 v11 = block + 0x08 v12 = block + 0x10 v13 = block + 0x18 v14 = block + 0x20 v15 = block + 0x28 因此这句: v15 = v3; 实际就是: *(id *)((char *)&v10 + 0x28) = v3; 而 v3 来自: v3 = objc_retain(objc_retain(a3)); 所以等价于: *(id *)(block + 0x28) = a3; 之后: v4 = objc_retainBlock(&v10); 会把这个 stack block copy/retain 成一个真正可执行的 block。后面调用: ((__int64 (__fastcall *)(void ***))v4[2])(v4) 也就是: block->invoke(block) 进入 sub_10024DE14(a1) 后,a1 就是 block 地址,所以: *(_QWORD *)(a1 + 40) 就是创建时的: v15 也就是原来的: a3 xsp 是 IDA/反编译器里表示的 栈指针 SP。 在 ARM64 里: SP = Stack Pointer 也就是当前函数栈帧的起始/当前栈位置。 IDA 反编译里看到: void **v10; // [xsp+0h] [xbp-50h] __int64 v11; // [xsp+8h] [xbp-48h] id (*v12)(...); // [xsp+10h] [xbp-40h] void *v13; // [xsp+18h] [xbp-38h] MCDHTTPEngine *v14; // [xsp+20h] [xbp-30h] id v15; // [xsp+28h] [xbp-28h] 意思是这些变量都在当前函数的栈上,而且是连续排列的。 可以理解成: 栈上某个地址 = xsp xsp + 0x00 -> v10 xsp + 0x08 -> v11 xsp + 0x10 -> v12 xsp + 0x18 -> v13 xsp + 0x20 -> v14 xsp + 0x28 -> v15 所以这几个变量其实组成了一个结构体,也就是 block: struct Block { void *isa; // xsp + 0x00 = v10 long flags; // xsp + 0x08 = v11 void *invoke; // xsp + 0x10 = v12 void *descriptor; // xsp + 0x18 = v13 id capture0; // xsp + 0x20 = v14 id capture1; // xsp + 0x28 = v15 }; 代码里: v4 = objc_retainBlock(&v10); &v10 就是:xsp + 0x00 也就是把 v10 所在的栈地址当成 block 起始地址。 所以: v15 = v3; 由于 v15 在 [xsp+28h],就等价于: *(id *)(block + 0x28) = v3; 其中: xsp = 当前栈上这个临时 block 的起始位置 简单说:xsp 就是栈指针,[xsp+28h] 表示这个局部变量在当前栈帧中相对栈指针偏移 0x28 的位置。因此,要看params怎么来的,要继续hook -[MCDHTTPEngine createRequestWithOriginRequest:]的入参或者直接查看交叉引用找上层函数
var func_imp = ObjC.classes.MCDHTTPEngine['- createRequestWithOriginRequest:'].implementation; Interceptor.attach(func_imp, { onEnter: function (args){ console.log('args[2]:' + ObjC.Object(args[2])); console.log('args[2] type :' + ObjC.Object(args[2]).$className); // console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n') var m = Process.findModuleByAddress(this.returnAddress); var offset = this.returnAddress.sub(m.base); console.log('offset:' + offset.toString(16)); console.log('func: ', DebugSymbol.fromAddress(this.returnAddress)); } })跟进该方法
来自a3,继续hook入参
// MCDHTTPEngine sendHTTPRequest:callback: var func_ = ObjC.classes.MCDHTTPEngine['- sendHTTPRequest:callback:']; Interceptor.attach(func_.implementation, { onEnter: function (args){ console.log('args[2]:' + ObjC.Object(args[2])); console.log('args[2] type :' + ObjC.Object(args[2]).$className); var m = Process.findModuleByAddress(this.returnAddress); var offset = this.returnAddress.sub(m.base); console.log('offset:' + offset.toString(16)); console.log('func: ', DebugSymbol.fromAddress(this.returnAddress)); } })在这个函数内部就找到了tel的加密入口
以上只是一种定位追踪的方法,同样的一开始可以hook看看tel是什么时候被放进字典的,多尝试不同的方法,最终通过hook +[NSDictionary dictionaryWithObjects:forKeys:count:]也是可以定位到的
v10就是明文手机号,进入+[MCDEncryption encryWithString:] 分析tel的加密算法
流程也很明显,手机号转utf8,使用AES128 key固定 iv为空 ECB模式,最后返回NSData再转hex
结果没有问题,tel至此结束
2. 各请求头
a. d值 和 token
hook 写入请求头的位置
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=='d') { 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') var m = Process.findModuleByAddress(this.returnAddress); var offset = this.returnAddress.sub(m.base) console.log('offset: ' + offset) console.log('func: ' + DebugSymbol.fromAddress(this.returnAddress)) } }, 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(); if (keyLower=='d') { 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); }根据输出定位到block
查看交叉引用,跳转到 -[AFJSONRequestSerializer requestBySerializingRequest:withParameters:error:] 方法,先hook一下看看入参
// -[AFJSONRequestSerializer requestBySerializingRequest:withParameters:error:] var func_ = ObjC.classes.AFJSONRequestSerializer['- requestBySerializingRequest:withParameters:error:']; Interceptor.attach(func_.implementation, { onEnter: function (args){ console.log('args[2]:' + ObjC.Object(args[2])); console.log('headers: ' + ObjC.Object(args[2]).allHTTPHeaderFields().toString()) console.log('args[2] type :' + ObjC.Object(args[2]).$className); } })可以看到啥也没有,那么所有请求头就都是在方法内部赋值的了
v11应该就是headers,看看v34这个block做了什么事
AFHTTPRequestSerializer 中已经存在的header跳过,不存在的直接set,结合刚刚hook到的情况,入参的headers为空,那么只能是在AFHTTPRequestSerializer 别的方法中设置的,先看看v11返回什么
var base = Process.getModuleByName('McdApp').base var addr = base.add(0xFEC7E4) Interceptor.attach(addr, { onEnter: function(args){ console.log(111) console.log('x22 : ' + ObjC.Object(this.context.x22)) } })确实有一部分是AFHTTPRequestSerializer 内设置的
// -[AFHTTPRequestSerializer setValue:forHTTPHeaderField:] var func_ = ObjC.classes.AFHTTPRequestSerializer['- setValue:forHTTPHeaderField:']; Interceptor.attach(func_.implementation, { onEnter: function (args){ if(ObjC.Object(args[3]).toString()=='d'){ console.log('key args[3]:' + ObjC.Object(args[3])); console.log('value args[2]:' + ObjC.Object(args[2])) var m = Process.findModuleByAddress(this.returnAddress) var offset = this.returnAddress.sub(m.base) console.log('offset: ' + offset) console.log('func: '+ DebugSymbol.fromAddress(this.returnAddress)) } }, onLeaver: function (retval){ } })追到 -[MCDHTTPConnectionKit sessionManagerWithRequest:]
token的赋值点就在下方,简要看了一下,d值是数美sdk,token就是设备uuid,后续再单独分析,先固定就行
b. x-mcd-gw-v , x-hmac-digest 和 authorization
直接搜字符串定位到
x-mcd-gw-v是定值1,先去看v27是啥
再去hook一下入参,看看这个HTTPBody是啥
很明显了,body->utf8,然后传到+[MCDHTTPSignV4 convertDIGEST:error:] 加密,继续跟
先看密钥的值,多hook几次,发现是定值
核心逻辑: for (i = 0; i != 41; ++i) bytes[i] = byte_1022277EE[i] ^ (i - 29); v6 = CFStringCreateWithBytes( kCFAllocatorDefault, bytes, 40, 0x8000100u, 0 ); 也就是: 从 byte_1022277EE 取 41 个字节 每个字节 XOR (i - 29) 然后取前 40 字节当 UTF-8 字符串继续看加密位置,也很明显的HMAC-SHA256
再回去跟authorization
hook一下入参
剩下的慢慢分析,总结就是
AK = v4AKPro(); SK = v4SKPro(); method = request.HTTPMethod; // POST path = request.URL.path; // /bff/passport/verifyCode/send query = request.URL.query ?: ""; // 没有 query 就是空字符串 gmtTime = 当前 GMT 时间字符串; // Sun, 12 Jul 2026 14:42:34 GMT 参与签名的 header 只取这些: signHeaders = { "ct", "language", "p", "sid", "sv", "token", "v", "x-mcd-gw-v" }; 过滤掉空值后排序: headers = 当前 request.headers; validHeaders = []; for key in signHeaders { value = headers[key]; if value != nil && value != "" { validHeaders.append(key); } } validHeaders.sort(); 生成 signedHeaders: signedHeaders = join(validHeaders, ";"); 生成 canonicalHeaders: canonicalHeaders = ""; for key in validHeaders { canonicalHeaders += key + ":" + headers[key] + "\n"; } 然后拼接待签名字符串: canonicalString = method + "\n" + path + "\n" + query + "\n" + AK + "\n" + gmtTime + "\n" + canonicalHeaders; 注意:canonicalHeaders 最后一行后面也有 \n。 然后计算签名: signature = Base64( HMAC_SHA256( key = SK ASCII bytes, data = canonicalString UTF8 bytes ) ); 最后拼接生成: Authorizationc. 其余header
剩下的header都比较简单,hook设置头部的方法即可
AI逆向
也是半小时左右,直接杀穿了,并且生成了一份详细的分析文档,内容较多这边就不放了
可以看到请求是没问题的