
声明本文章中所有内容仅供学习交流使用不用于其他任何目的不提供完整代码抓包内容、敏感网址、数据接口等均已做脱敏处理严禁用于商业用途和非法用途否则由此产生的一切后果均与作者无关本文章未经许可禁止转载禁止任何修改后二次传播擅自使用本文讲解的技术而导致的任何意外作者均不负责若有侵权请联系作者立即删除目标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:] 方法的同样是接受的第一个参数继续hookvar 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是一个blockIDA 里显示成 __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; // [xsp0h] __int64 v11; // [xsp8h] id (*v12)(...); // [xsp10h] void *v13; // [xsp18h] MCDHTTPEngine *v14; // [xsp20h] id v15; // [xsp28h] 然后代码 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 在 [xsp0]它就是这个 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; // [xsp0h] [xbp-50h] __int64 v11; // [xsp8h] [xbp-48h] id (*v12)(...); // [xsp10h] [xbp-40h] void *v13; // [xsp18h] [xbp-38h] MCDHTTPEngine *v14; // [xsp20h] [xbp-30h] id v15; // [xsp28h] [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 在 [xsp28h]就等价于 *(id *)(block 0x28) v3; 其中 xsp 当前栈上这个临时 block 的起始位置 简单说xsp 就是栈指针[xsp28h] 表示这个局部变量在当前栈帧中相对栈指针偏移 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值 和 tokenhook 写入请求头的位置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 (headerKeyLowerd) { 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 (keyLowerd) { 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值是数美sdktoken就是设备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再回去跟authorizationhook一下入参剩下的慢慢分析总结就是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逆向也是半小时左右直接杀穿了并且生成了一份详细的分析文档内容较多这边就不放了可以看到请求是没问题的