声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!
目标
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逆向
也是半小时左右,直接杀穿了,并且生成了一份详细的分析文档,内容较多这边就不放了
可以看到请求是没问题的