介绍
需要分析支付某打开扫一扫,扫到目标二维码后是如何跳转到转账页面的。
定位
发现扫码从数据包无从下手定位,尝试关键字:QRScan,QRCode,Translate等都找不到对应的函数,所以最终通过View的点击事件。
1 2 3 4 5 6 7 8 9 | var Button = Java.use("android.widget.Button");Button.setOnClickListener.overload("android.view.View$OnClickListener").implementation = function (listener) { console.log("Button clicked"); //listener.onClick(this); // 调用原来的点击事件 printStack(); this.setOnClickListener(listener); console.log("Button clicked end"); return; }; |
打印了堆栈,从堆栈结果里面找到了一个疑似创建扫码相机的View:
com.alipay.mobile.scan.ui2.NScanTopView
因为这个类代码量巨大,5000多行,而且都是混淆过的名称,我直接发给ai进行分析,得出了大概扫描后的回调结果是在a方法:
1 2 3 4 | @Override // com.alipay.mobile.scan.ui.BaseScanTopViewpublic final void a(com.alipay.mobile.bqcscanservice.BQCScanResult r25) {// ... existing code ...} |
但是代码的过程看不到,只能看Smail指令,并且对应参数r25里面的BQCScanResult也是啥都没有,只能通过方法的执行过程分析(还是给AI分析)随后得出hook代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | let NScanTopView = Java.use("com.alipay.mobile.scan.ui2.NScanTopView"); NScanTopView["a"].overload('com.alipay.mobile.bqcscanservice.BQCScanResult').implementation = function (result) { console.log("NScanTopView.a is called"); if(result != null) { // 1. 先确认是否是 MultiMaScanResult if(result.$className === "com.alipay.mobile.mascanengine.MultiMaScanResult") { let multiResult = Java.cast(result, Java.use("com.alipay.mobile.mascanengine.MultiMaScanResult")); // 2. 获取 maScanResults 数组 let scanResults = multiResult.maScanResults.value; if(scanResults && scanResults.length > 0) { // 3. 获取第一个结果 let firstResult = scanResults[0]; // 4. 打印扫描文本 console.log("Scan Text:", firstResult.text.value); // 5. 打印更多信息(如果需要) if(firstResult.rect) { console.log("Scan Rect:", JSON.stringify({ left: firstResult.rect.value.left, top: firstResult.rect.value.top, right: firstResult.rect.value.right, bottom: firstResult.rect.value.bottom })); } if(firstResult.type) { console.log("Scan Type:", firstResult.type.value); } } } // 备用方案:通过反射获取所有可能的属性 try { let fields = result.getClass().getDeclaredFields(); for(let i = 0; i < fields.length; i++) { let field = fields[i]; field.setAccessible(true); console.log(`Field ${field.getName()}: ${field.get(result)}`); } } catch(e) { console.log("Error getting fields:", e); } } // 调用原始方法 return this["a"](result); }; |
输出结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Scan Text: https://qr.alipay.com/fkx17137rejqeh4j3v4cib0?t=1733923946234Scan Rect: {"left":{"_p":["<instance: android.graphics.Rect>",2,{"className":"int","name":"I","type":"int32","size":1,"byteSize":4,"defaultValue":0},"0x7048f4b8","0x7ae97bd488","0x7ae97c07a0"]},"top":{"_p":["<instance: android.graphics.Rect>",2,{"className":"int","name":"I","type":"int32","size":1,"byteSize":4,"defaultValue":0},"0x7048f4d8","0x7ae97bd488","0x7ae97c07a0"]},"right":{"_p":["<instance: android.graphics.Rect>",2,{"className":"int","name":"I","type":"int32","size":1,"byteSize":4,"defaultValue":0},"0x7048f4c8","0x7ae97bd488","0x7ae97c07a0"]},"bottom":{"_p":["<instance: android.graphics.Rect>",2,{"className":"int","name":"I","type":"int32","size":1,"byteSize":4,"defaultValue":0},"0x7048f4a8","0x7ae97bd488","0x7ae97c07a0"]}}Scan Type: QRField candidate: falseField classicFrameCount: 44Field frameCount: 44Field frameType: 0Field maScanResults: [Lcom.alipay.mobile.mascanengine.MaScanResult;@178c311Field readerParams: nullField recognizedPerformance: type=Normal^scanType=3^unrecognizedFrame=42^sumDurationOfUnrecognized=1694^durationOfRecognized=51^durationOfBlur=0^durationOfBlurSVM=0^detectFrameCountBlurSVM=0^detectAvgDurationBlurSVM=0.0^durationOfNoNeedCheckBlurSVM=0^whetherGetTheSameLaplaceValue=false^sameLaplaceValueCount=0^Field rsBinarized: falseField rsBinarizedCount: 0Field rsInitTime: 0Field totalEngineCpuTime: nullField totalEngineTime: 2811662Field totalScanTime: 48566 |
这样就拿到了扫码结果,但是我需要接着跟踪它将扫码结果进行转账通讯的部分,经过分析,最终处理扫描二维码的逻辑是在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | // 1. Hook BaseScanTopView 的所有方法,找到使用 c 的地方 let BaseScanTopView = Java.use("com.alipay.mobile.scan.ui.BaseScanTopView"); let NScanTopView = Java.use("com.alipay.mobile.scan.ui2.NScanTopView"); // Hook 所有方法 let methods = BaseScanTopView.class.getDeclaredMethods(); methods.forEach(method => { let methodName = method.getName(); if(BaseScanTopView[methodName]) { try { BaseScanTopView[methodName].implementation = function() { console.log(`\n[*] BaseScanTopView.${methodName} 被调用`); // 尝试通过反射获取 c 字段的值 try { let field = this.getClass().getSuperclass().getDeclaredField("c"); field.setAccessible(true); let cValue = field.get(this); if(cValue) { console.log("[*] c 字段值类型:", cValue.$className); // 如果是 by 接口的实现,hook 它的方法 if(Java.use("com.alipay.mobile.scan.ui.by").class.isAssignableFrom(cValue.getClass())) { console.log("[*] 找到 by 接口实现类:", cValue.$className); // Hook 这个实现类的方法 let implClass = Java.use(cValue.$className); if(implClass.a) { implClass.a.overload('com.alipay.mobile.mascanengine.MaScanResult', 'com.alipay.mobile.scan.util.DataTransChannel') .implementation = function(maScanResult, dataTransChannel) { console.log("\n[*] by 实现类的 a 方法被调用"); if(maScanResult != null) { console.log("[*] 扫描结果:", maScanResult.text.value); } return this.a(maScanResult, dataTransChannel); }; } } } } catch(e) { //console.log("[!] 获取 c 字段失败:", e); } // 调用原方法 return this[methodName].apply(this, arguments); }; } catch(e) { //console.log(`[!] Hook ${methodName} 失败:`, e); } } }); // 2. Hook NScanTopView 的 b 方法 NScanTopView["b"].overload('com.alipay.mobile.bqcscanservice.BQCScanResult').implementation = function (bQCScanResult) { console.log("\n[*] NScanTopView.b 被调用"); // 在调用前尝试获取 c 字段 try { let field = this.getClass().getSuperclass().getDeclaredField("c"); field.setAccessible(true); let cValue = field.get(this); if(cValue) { console.log("[*] c 字段实现类:", cValue.$className); } } catch(e) { console.log("[!] 获取 c 字段失败:", e); } let result = this["b"](bQCScanResult); return result; }; // 3. 动态查找并 hook by 接口的实现类 Java.choose("com.alipay.mobile.scan.ui.by", { onMatch: function(instance) { console.log("[*] 找到 by 接口实例:", instance.$className); // Hook 这个实例的方法 let implClass = Java.use(instance.$className); if(implClass.a) { implClass.a.overload('com.alipay.mobile.mascanengine.MaScanResult', 'com.alipay.mobile.scan.util.DataTransChannel') .implementation = function(maScanResult, dataTransChannel) { console.log("\n[*] by 实现类的 a 方法被调用"); if(maScanResult != null) { console.log("[*] 扫描结果:", maScanResult.text.value); } return this.a(maScanResult, dataTransChannel); }; } }, onComplete: function() {} }); |
输出结果:
1 2 3 4 5 6 7 8 9 10 11 12 | [*] NScanTopView.b 被调用[*] c 字段实现类: com.alipay.mobile.scan.as.main.MainCaptureActivityMainCaptureActivity.a is called: maScanResult=com.alipay.mobile.mascanengine.MaScanResult@80f5308, dataTransChannel=DataTransChannel{isFromAlbum=false, albumImagePath='null', scanCode='null', payLinkToken='null', controlType='camera', isFromRoute=false, isCache=false, bizType='null', custProgressDialog=com.alipay.mobile.scan.ui2.NBluePointView{a02faa1 I.E...... ......I. 0,0-1080,2028}}MainCaptureActivity.a result=true[*] BaseScanTopView.p 被调用[*] c 字段值类型: com.alipay.mobile.scan.as.main.MainCaptureActivity[*] 找到 by 接口实现类: com.alipay.mobile.scan.as.main.MainCaptureActivity[*] BaseScanTopView.q 被调用[*] c 字段值类型: com.alipay.mobile.scan.as.main.MainCaptureActivity[*] 找到 by 接口实现类: com.alipay.mobile.scan.as.main.MainCaptureActivity |
然后去查看了 com.alipay.mobile.scan.as.main.MainCaptureActivity 这个类的 a 方法实现过程:
1 2 3 4 5 6 7 8 9 10 11 12 | public final boolean a(com.alipay.mobile.mascanengine.MaScanResult r17, com.alipay.mobile.scan.util.DataTransChannel r18) { /* r16 = this; r1 = r16 ………………………… ………………………… ………………………… boolean r0 = r2.a(r3, r4, r5, r6, r7, r8) */ } |
因为无法正常编译成java代码,并且很长,一如既往的让AI帮我分析,AI得出结果这个方法是一个判断扫描结果类型,进行路由分发的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // 判断不同的扫码类型com.alipay.mobile.mascanengine.MaScanType r3 = com.alipay.mobile.mascanengine.MaScanType.PRODUCTcom.alipay.mobile.mascanengine.MaScanType r11 = r2.typeif (r3 == r11) goto Ldb // 商品条码// ... 其他类型判断com.alipay.mobile.mascanengine.MaScanType r3 = com.alipay.mobile.mascanengine.MaScanType.QR // 二维码// 二维码处理if (r3 == r11) goto Lc3 // QR类型 r10.put(r9, r3) // 存储扫描文本 // 处理隐藏数据 if (!TextUtils.isEmpty(r2.hiddenData)) { r10.put("hiddenData", r2.hiddenData) } // 收集扫描性能数据r0.put("totalTime", r8) r0.put("scanTime", r8)r0.put("realTime", r8)com.alipay.phone.scancode.y.a r0 = r1.d// ... 参数准备boolean r0 = r2.a(r3, r4, r5, r6, r7, r8) // 调用实际的业务处理方法 |
然后在这个混淆的方法里面,找到了关键类
com.alipay.phone.scancode.y.a
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | let ScanCodeHandler = Java.use("com.alipay.phone.scancode.y.a"); ScanCodeHandler.a.overload('java.lang.String', 'java.util.Map', 'java.lang.String', 'java.lang.String', 'com.alipay.mobile.scan.util.DataTransChannel', 'java.lang.String').implementation = function(str1, map, str2, str3, channel, str4) { console.log("\n[*] 扫码业务处理被调用"); console.log("[*] 扫描类型:", str1); console.log("[*] 扫描内容:", str3); let result = this.a(str1, map, str2, str3, channel, str4); console.log("[*] 处理结果:", result); return result; };[*] 扫码业务处理被调用[*] 扫描类型: qrCode[*] 扫描内容: https://qr.alipay.com/fkx17******0?t=1733900000000[*] 处理结果: trueMainCaptureActivity.a result=true |
最后跟踪进 a 方法里面的实现过程(发现也是无法正常编译的java代码)
这个函数的过程作用大致如下:
1、初始化参数处理
2、检查是否由路由传过来的参数
3、检查是否支持离线支付
4、处理路由
5、缓存处理
6、记录支付行为
最后,hook了这个方法里面的相关涉及的类,得出hook代码以及输出结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | // 1. Hook 主要的路由处理方法 let CodeRouter = Java.use("com.alipay.phone.scancode.y.a"); CodeRouter.a.overload('java.lang.String', 'java.util.Map', 'java.lang.String', 'java.lang.String', 'int', 'java.lang.String') .implementation = function(type, map, sourceId, content, flag, token) { console.log("\n[*] 扫码支付路由被调用"); console.log("[*] 类型:", type); console.log("[*] 内容:", content); console.log("[*] 来源ID:", sourceId); console.log("[*] Token:", token); // 打印Map参数 // 2. Hook DataTransChannel try { let channel = this.D.value; if(channel != null) { console.log("[*] DataTransChannel 信息:"); console.log(" 是否来自相册:", channel.f115683a.value); console.log(" 图片路径:", channel.b.value); console.log(" 扫描内容:", channel.c.value); console.log(" Channel类型:", channel.e.value); // 打印额外参数 if(channel.m != null) { let extraParams = channel.m.value; console.log("[*] 额外参数:"); let extraIterator = extraParams.entrySet().iterator(); while(extraIterator.hasNext()) { let entry = extraIterator.next(); console.log(" ", entry.getKey() + " = " + entry.getValue()); } } } } catch(e) { console.log("[!] 获取Channel信息失败:", e); } // 3. Hook 离线支付检查 try { if(this.I != null) { let offlineHandler = this.I.value; let offlineResult = offlineHandler.a(content); if(offlineResult != null) { console.log("[*] 离线支付信息:"); console.log(" URI:", offlineResult.f); console.log(" Method:", offlineResult.g); console.log(" Type:", offlineResult.d); } } } catch(e) { console.log("[!] 获取离线支付信息失败:", e); } // 4. Hook RouteInfo 创建 try { let RouteInfo = Java.use("com.alipay.mobile.scan.biz.RouteInfo"); RouteInfo.$init.implementation = function() { console.log("[*] 创建新的RouteInfo"); return this.$init(); }; RouteInfo.setUri.implementation = function(uri) { console.log("[*] 设置RouteInfo URI:", uri); return this.setUri(uri); }; RouteInfo.setMethod.implementation = function(method) { console.log("[*] 设置RouteInfo Method:", method); return this.setMethod(method); }; } catch(e) { console.log("[!] Hook RouteInfo失败:", e); } // 打印调用栈 console.log("[*] 调用栈:"); console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); let result = this.a(type, map, sourceId, content, flag, token); console.log("[*] 路由处理结果:", result); return result; }; [*] 设置RouteInfo Method: native[*] 创建新的RouteInfo[*] 设置RouteInfo Method: native[*] 设置RouteInfo URI: alipays://platformapi/startapp?appId=20001001&bizType=UTP_QR_CODE&pageData=%7B%22tpl%22%******* |
通过输出结果,发现最后会生成出路由URI,用于唤起支付界面的,尝试通过adb直接打开这个URI看看是否能直接跳转到转账界面。
adb shell am start -a android.intent.action.VIEW -d "alipays://platformapi/startapp?appId=20001001&bizType=UTP_QR_CODE&pageData=省略参数"
最后成功直接打开转账界面,由此分析出了整个扫码转账的执行环节。。