记一次使用 Xposed RPC 和 BurpGuard 应对金融APP参数签名及加密的详细过程
字数 1710 2025-08-24 07:48:23

使用Xposed RPC和BurpGuard应对金融APP参数签名及加密的详细教程

前言

本教程详细记录了如何通过Xposed RPC和BurpGuard解决金融APP参数签名及加密问题的完整过程。通过学习本教程,你将掌握以下知识:

  1. APP请求逆向分析思路
  2. 如何查阅相关资料解决技术难题
  3. APP请求签名和加密的原理分析
  4. 白盒WbSM4加密算法的理解
  5. Xposed RPC的实现方法
  6. BurpGuard的使用技巧

测试环境准备

  • 模拟器:Mumu模拟器
  • 系统工具:Magisk、Lsposed
  • 分析工具:算法助手Plus、frida、Jadx
  • 代理工具:BurpSuite

初探签名及加密机制

请求包分析

观察APP的HTTPS请求,发现以下关键字段:

  • MsgId:请求标识
  • AppSign:请求签名
  • cTxt:加密的请求内容

测试发现修改MsgId会返回401错误,说明APP实现了请求签名验证机制。

加密机制分析

脱壳与反编译

  1. 使用算法助手Plus配合frida脚本对APP进行脱壳(APP使用爱加密企业版加固)
  2. 使用Jadx反编译脱壳后的代码
  3. 搜索关键词发现代码被抽空,部分逻辑在native层实现

加密算法定位

  1. 通过关键词搜索找到两处加密相关代码,发现ECB模式
  2. 跟进代码发现xxx.WbSM4Util$Companion类的encryptDataECB方法
  3. 关键加密调用:WbSm4().encode()
  4. 加密流程:
    • 传入明文字节对象和长度
    • 不直接可见密钥相关操作
    • 最终进行Base64编码

白盒SM4算法分析

  1. WbSm4是native实现
  2. 查询资料确认是国密SM4算法
  3. 白盒SM4特点:
    • 密钥与算法高度融合
    • 难以直接提取密钥
    • 需要采用RPC远程调用方式处理

签名机制分析

签名生成流程

  1. 搜索关键词appsign定位签名相关代码
  2. 发现WelfareTaskRequestService.APPSIGN常量
  3. 签名生成调用:
    HeaderUtils.generateAppSign(
        ServiceUrls.getAccessKeyListUrl(...),
        uuid,
        HeaderUtils.requestBodyToString(create)
    )
    
  4. 签名参数:
    • 完整URL
    • UUID(作为MsgId)
    • 完整请求Body

签名验证特点

  1. 请求body与msgid和appsign强相关
  2. 任何参数错误都会导致请求不合法

Xposed RPC实现

技术选型

  1. 使用Xposed框架(无检测,而frida有检测)
  2. 参考过检测方法(可无脑过爱加密企业版检测)

RPC实现方案

采用NanoHTTPD在APP内部启动HTTP服务器与外部通信:

  1. 开放两个接口:
    • /encrypt:用于加密数据和生成签名
    • /decrypt:用于解密数据

核心代码实现

HTTP服务器基础框架

class HTTPServer extends NanoHTTPD {
    public HTTPServer(int port) { super(port); }
    
    @Override
    public Response serve(IHTTPSession session) {
        JsonObject responseJson = new JsonObject();
        String encryptData = "";
        String decryptData = "";
        Map<String, String> map = new HashMap<>();
        
        try {
            session.parseBody(map);
        } catch (Exception e) {
            return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", e.getMessage());
        }
        
        if (session.getMethod() == Method.POST) {
            switch (session.getUri()) {
                case "/encrypt":
                    responseJson.addProperty("encryptData", encryptData);
                    return newFixedLengthResponse(Response.Status.OK, "application/json", responseJson.toString());
                case "/decrypt":
                    responseJson.addProperty("decryptData", decryptData);
                    return newFixedLengthResponse(Response.Status.OK, "application/json", responseJson.toString());
                default:
                    return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Not Found");
            }
        } else {
            return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Not Found");
        }
    }
}

Xposed模块完整实现

package com.example.xposed;

import android.util.Log;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
import fi.iki.elonen.NanoHTTPD;

public class Hooker implements IXposedHookLoadPackage {
    private HTTPServer httpServer = new HTTPServer(50000);

    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
        try {
            Class<?> clazz = Class.forName("xxx.WbSM4Util$Companion", true, loadPackageParam.classLoader);
            Method methods[] = clazz.getDeclaredMethods();
            Constructor<?> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            
            Crypto crypto = new Crypto();
            crypto.setClassLoader(loadPackageParam.classLoader);
            crypto.setObject(constructor.newInstance());
            
            for (int i = 0; i < methods.length; i++) {
                if (methods[i].getName().equals("decryptDataECB")) {
                    methods[i].setAccessible(true);
                    crypto.setDecrypt(methods[i]);
                }
                if (methods[i].getName().equals("encryptDataECB")) {
                    methods[i].setAccessible(true);
                    crypto.setEncrypt(methods[i]);
                }
            }
            
            if (crypto.getEncrypt() != null && crypto.getDecrypt() != null) {
                this.httpServer.setCrypto(crypto);
                startHttpServer();
            }
        } catch (Exception e) {
            Log(e.toString());
        }
    }

    private void startHttpServer() {
        if (httpServer != null && httpServer.getCrypto() != null) {
            new Thread(() -> {
                try {
                    httpServer.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
                    Log("HTTP Server started on port " + httpServer.getListeningPort());
                } catch (IOException e) {
                    Log("Error starting HTTP Server: " + e.toString());
                }
            }).start();
        }
    }

    public static class HTTPServer extends NanoHTTPD {
        private Crypto crypto;
        
        public HTTPServer(int port) { super(port); }
        public Crypto getCrypto() { return crypto; }
        public void setCrypto(Crypto crypto) { this.crypto = crypto; }
        
        @Override
        public Response serve(IHTTPSession session) {
            JsonObject responseJson = new JsonObject();
            JsonObject body = new JsonObject();
            Map<String, String> map = new HashMap<>();
            
            try {
                session.parseBody(map);
                String data;
                
                if (session.getMethod() == Method.POST) {
                    body = JsonParser.parseString(map.get("postData")).getAsJsonObject();
                    
                    switch (session.getUri()) {
                        case "/encrypt":
                            // 从BurpGuard获取需要加密的数据和签名需要的url
                            data = body.get("data").getAsString();
                            String url = body.get("url").getAsString();
                            
                            // 反射获取签名方法
                            Class<?> headerUtil = Class.forName("xxx.HeaderUtils", false, crypto.getClassLoader());
                            java.lang.reflect.Method generateAppSign = headerUtil.getDeclaredMethod(
                                "generateAppSign", String.class, String.class, String.class);
                            generateAppSign.setAccessible(true);
                            
                            // 加密数据
                            String encryptData = this.crypto.getEncrypt().invoke(
                                crypto.getObject(), data, false).toString();
                            JsonObject jsondata = new JsonObject();
                            jsondata.addProperty("cTxt", encryptData);
                            
                            String uuid = UUID.randomUUID().toString();
                            // 生成签名
                            String appsign = generateAppSign.invoke(
                                null, url, uuid, jsondata.toString()).toString();
                            
                            // 构建响应
                            responseJson.addProperty("msgid", uuid);
                            responseJson.addProperty("appsign", appsign);
                            responseJson.addProperty("encryptData", encryptData);
                            
                            return newFixedLengthResponse(
                                Response.Status.OK, "application/json", responseJson.toString());
                            
                        case "/decrypt":
                            // 解密数据
                            data = body.get("data").getAsString();
                            String decryptData = this.crypto.getDecrypt().invoke(
                                crypto.getObject(), data, false).toString();
                            responseJson.addProperty("decryptData", decryptData);
                            
                            return newFixedLengthResponse(
                                Response.Status.OK, "application/json", responseJson.toString());
                            
                        default:
                            return newFixedLengthResponse(
                                Response.Status.NOT_FOUND, "text/plain", "Not Found");
                    }
                } else {
                    return newFixedLengthResponse(
                        Response.Status.NOT_FOUND, "text/plain", "Not Found");
                }
            } catch (InvocationTargetException e) {
                Log(e.getCause().fillInStackTrace().toString());
                return newFixedLengthResponse(
                    Response.Status.BAD_REQUEST, "text/plain", "Invalid JSON data");
            } catch (Exception e) {
                Log(e.getCause().fillInStackTrace().toString());
            }
            return newFixedLengthResponse(
                Response.Status.INTERNAL_ERROR, "text/plain", "Invalid Request");
        }
    }
}

端口转发与测试

  1. 使用ADB转发端口:
    adb forward tcp:50000 tcp:50000
    
  2. 测试HTTP服务器是否启动成功
  3. 测试接口:
    • 解密接口:发送加密数据,获取明文
    • 加密接口:发送明文和URL,获取加密数据和签名

BurpGuard实现

项目介绍

BurpGuard项目地址:https://github.com/yinsel/BurpGuard

实现原理

  1. 使用mitmproxy作为中间人代理
  2. 分为两个处理程序:
    • ClientProxyHandler:处理客户端请求(解密)
    • BurpProxyHandler:处理Burp请求(加密)

核心代码实现

ClientProxyHandler.py

from mitmproxy import http
import httpx
import json
import traceback

class ClientProxyHandler:
    def __init__(self) -> None:
        self.client = httpx.Client(timeout=None, verify=False)
    
    def request(self, flow: http.HTTPFlow):
        try:
            req = flow.request
            
            if req.method == "POST" and "json" in req.headers["Content-Type"] and "\"cTxt\"" in req.text:
                json_data = req.json()
                result = self.client.post(
                    "http://127.0.0.1:50000/decrypt",
                    json={"data": json_data["cTxt"]}
                ).json()
                
                req.text = result["decryptData"]
                req.headers["burp"] = "1"
                
        except Exception as e:
            traceback.print_exception(e)
        finally:
            return flow

addons = [ClientProxyHandler()]

BurpProxyHandler.py

from mitmproxy import http
import httpx
import json
import traceback

class BurpProxyHandler:
    def __init__(self) -> None:
        self.client = httpx.Client(timeout=None, verify=False)
    
    def request(self, flow: http.HTTPFlow):
        try:
            req = flow.request
            
            if req.headers.get("burp"):
                json_data = req.json()
                result = self.client.post(
                    "http://127.0.0.1:50000/encrypt",
                    json={"data": req.text, "url": req.url}
                ).json()
                
                req.text = json.dumps({"cTxt": result["encryptData"]}, separators=(',', ':'))
                req.headers["msgid"] = result["msgid"]
                req.headers["appsign"] = result["appsign"]
                
        except Exception as e:
            traceback.print_exception(e)
        finally:
            return flow

addons = [BurpProxyHandler()]

代理配置

  1. 配置模拟器代理为8081(ClientProxyHandler)
  2. 配置BurpSuite上游代理为8082(BurpProxyHandler)
  3. 运行BurpGuard:
    python BurpGuard.py
    

最终效果验证

  1. 在模拟器安装并激活Xposed模块
  2. 操作APP,使用Burp拦截请求
  3. 观察请求body已自动解密
  4. 在重发器中修改请求内容,测试签名和加密自动处理功能
  5. 确认可以正常进行渗透测试操作

总结

通过本教程,我们完整实现了:

  1. 对加固APP的脱壳和逆向分析
  2. 白盒SM4加密算法的RPC调用
  3. 复杂签名机制的自动化处理
  4. 与BurpSuite的无缝集成
  5. 自动化加解密和签名验证流程

这种方法不仅适用于金融APP,也可应用于其他使用类似安全机制的移动应用测试场景。

使用Xposed RPC和BurpGuard应对金融APP参数签名及加密的详细教程 前言 本教程详细记录了如何通过Xposed RPC和BurpGuard解决金融APP参数签名及加密问题的完整过程。通过学习本教程,你将掌握以下知识: APP请求逆向分析思路 如何查阅相关资料解决技术难题 APP请求签名和加密的原理分析 白盒WbSM4加密算法的理解 Xposed RPC的实现方法 BurpGuard的使用技巧 测试环境准备 模拟器:Mumu模拟器 系统工具:Magisk、Lsposed 分析工具:算法助手Plus、frida、Jadx 代理工具:BurpSuite 初探签名及加密机制 请求包分析 观察APP的HTTPS请求,发现以下关键字段: MsgId :请求标识 AppSign :请求签名 cTxt :加密的请求内容 测试发现修改 MsgId 会返回401错误,说明APP实现了请求签名验证机制。 加密机制分析 脱壳与反编译 使用算法助手Plus配合frida脚本对APP进行脱壳(APP使用爱加密企业版加固) 使用Jadx反编译脱壳后的代码 搜索关键词发现代码被抽空,部分逻辑在native层实现 加密算法定位 通过关键词搜索找到两处加密相关代码,发现ECB模式 跟进代码发现 xxx.WbSM4Util$Companion 类的 encryptDataECB 方法 关键加密调用: WbSm4().encode() 加密流程: 传入明文字节对象和长度 不直接可见密钥相关操作 最终进行Base64编码 白盒SM4算法分析 WbSm4 是native实现 查询资料确认是国密SM4算法 白盒SM4特点: 密钥与算法高度融合 难以直接提取密钥 需要采用RPC远程调用方式处理 签名机制分析 签名生成流程 搜索关键词 appsign 定位签名相关代码 发现 WelfareTaskRequestService.APPSIGN 常量 签名生成调用: 签名参数: 完整URL UUID(作为MsgId) 完整请求Body 签名验证特点 请求body与msgid和appsign强相关 任何参数错误都会导致请求不合法 Xposed RPC实现 技术选型 使用Xposed框架(无检测,而frida有检测) 参考过检测方法(可无脑过爱加密企业版检测) RPC实现方案 采用NanoHTTPD在APP内部启动HTTP服务器与外部通信: 开放两个接口: /encrypt :用于加密数据和生成签名 /decrypt :用于解密数据 核心代码实现 HTTP服务器基础框架 Xposed模块完整实现 端口转发与测试 使用ADB转发端口: 测试HTTP服务器是否启动成功 测试接口: 解密接口:发送加密数据,获取明文 加密接口:发送明文和URL,获取加密数据和签名 BurpGuard实现 项目介绍 BurpGuard项目地址:https://github.com/yinsel/BurpGuard 实现原理 使用mitmproxy作为中间人代理 分为两个处理程序: ClientProxyHandler:处理客户端请求(解密) BurpProxyHandler:处理Burp请求(加密) 核心代码实现 ClientProxyHandler.py BurpProxyHandler.py 代理配置 配置模拟器代理为8081(ClientProxyHandler) 配置BurpSuite上游代理为8082(BurpProxyHandler) 运行BurpGuard: 最终效果验证 在模拟器安装并激活Xposed模块 操作APP,使用Burp拦截请求 观察请求body已自动解密 在重发器中修改请求内容,测试签名和加密自动处理功能 确认可以正常进行渗透测试操作 总结 通过本教程,我们完整实现了: 对加固APP的脱壳和逆向分析 白盒SM4加密算法的RPC调用 复杂签名机制的自动化处理 与BurpSuite的无缝集成 自动化加解密和签名验证流程 这种方法不仅适用于金融APP,也可应用于其他使用类似安全机制的移动应用测试场景。