当前位置: 首页 > news >正文

【IOS逆向】麦当劳保姆级分析过程

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!

本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!


目标

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 ) ); 最后拼接生成: Authorization


c. 其余header

剩下的header都比较简单,hook设置头部的方法即可


AI逆向

也是半小时左右,直接杀穿了,并且生成了一份详细的分析文档,内容较多这边就不放了

可以看到请求是没问题的

http://www.jsqmd.com/news/1189996/

相关文章:

  • 计算机系统安全实验:栈帧结构与缓冲区溢出漏洞利用
  • 避坑式网安路线!90%新人都会学错的10个顺序,看完少走一年弯路
  • 从Q250机架到妙算/NX:拆解FAST-Lab 250mm自主无人机硬件选型逻辑
  • Dify Agent模式配置必须在24小时内完成的3项合规加固(GDPR/等保2.0双标适配实录)
  • 【冷冻电镜】IMOD实战:从零构建双轴断层图(Etomo全流程解析)
  • 车型识别 车型检测数据集 suv识别 车辆计数 面包车检测
  • 北京产检、孕期就诊不方便?正规头部陪诊守嘉陪诊,专属孕期温和陪护服务 - 深鉴新闻
  • AI 检索品牌可见度提升方案:五大GEO服务商实力全景 - 资讯纵览
  • 2026界面清爽八字排盘工具怎么选:看字段层级、模块显隐和广告干扰
  • 模板驱动文档自动化:结构化内容复用与智能渲染实战
  • PixWorld:像素空间3D场景生成与重建统一框架实践指南
  • Gitee 企业版测试管理流程优化:四大模块闭环能力再提升
  • Ollama API尚未公开的调试模式启用方法(隐藏flag --verbose=3实测有效),仅限前1000名开发者知晓
  • TLA2518与STM32F745VG的ADC系统设计与优化
  • 微信小程序web-view组件开发实战与优化指南
  • 量化对冲基金公司-Agent或者AI相关职位
  • Python实战:从零构建中文词频分析器——jieba分词、停用词过滤与结果可视化
  • YOLOv11数字识别检测系统:工业质检实战解析
  • 021-如何精准选择学习概念
  • 跨平台落地:C#上位机AI视觉系统在Windows/Linux工控机的统一实现
  • Linux EEVDF调度算法核心公式解析与调优实践
  • 卡美德生物科普NGF(神经生长因子):神经系统发育与修复的关键调节因子
  • Docker构建时外网访问配置与优化指南
  • Simulink——数据字典与信号对象的实战配置
  • YOLO+SAM 落地实录:别被Demo骗了,这才是检测分割一体化的工程真相
  • 【一线大厂Java面试题合集】第73篇-Feed流系统设计
  • RK3588 推理优化:多线程、零拷贝与内存管理
  • SKILL.md标准化文档:提升智能体开发效率的关键
  • 2026年7月最新海口积家官方售后客户服务热线与维修网点地址汇总 - 积家官方售后服务中心
  • 负氧离子检测仪靠什么精准读数?电容吸入法工作全过程讲解