爱码者说

利用 AI 分析某平台的移动网关服务协议(一)

2025/12/08
11
0

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

这个协议还是挺复杂的,不止数据做了签名验证,请求体也是经过加密的,不过请求体的加密不算复杂。本着柿子挑软的捏的精神,本篇文章就先来分析下这个请求体是如何加密的。

抓包

直接抓包,是抓不到这个请求的,需要特殊的手段,直接看抓包的数据吧

从请求头中的x-clienttraceid字段中能看关键词:mpaas,再看下请求体,直接看是乱码

切换成 hex 看下

猜测请求体应该是进行了某种加密。

关键代码跟踪

从抓包的结果已经能看出是使用了 mpass,那就可以根据关键词查找相关资料了,可以直接到官方查看开发文档,经查找,这个协议是使用了 mpass 的移动网关服务。

继续查看开发者文档,可以发现存放网络相关全局配置的文件名称,然后搜索这个文件名,搜索结果如下

就一个文件,那就好办了,直接查看这个类在哪里被调用,最后就可以跟到 JNI 方法

可以看到这里会有不同的加密,具体使用哪种加密,是在刚才的配置文件中指定的,这个 app 使用的是RSA,就是使用的 decode 和 encode 方法。

已经找到是那个 so 和使用的哪个 JNI 方法了,接着就是来分析加解密方法,为了方便分析算法,可以先使用 unidbg 来模拟执行。

使用 unidbg 模拟执行

在模拟执行的时候,发现程序总是自动断点,不能顺利运行

查了下资料,说是模拟执行引擎不支持这个汇编指令,换个模拟执行引擎就可以了,把模拟执行引擎换成Unicorn2Factory�就不会自动断点了。

剩下的就是补环境了,补一下handler�就可以运行起来了。

看一下主要的代码

记得,要先初始化,再调用 encode,不然会出错。调用代码如下

运行一下程序,可以正常的输出结果

接着就来分析这个加密的算法了。

还原加密算法

根据 unidbg 模拟执行的日志,可以看这个加密函数的入口地址

到 IDA 中看下

直接 F5 反汇编看下c 的代码

可以发现这个代码是静态注册的,也没经过任何的混淆,代码往下拉,可以看到调用了fake_island::client::encode

一直跟下去,就能找到最终加密的代码,如下

__int64 __fastcall fake_island::client::encode(
__int64 *a1,
void *src,
size_t n,
unsigned __int8 *a4,
size_t a5,
unsigned int a6,
fake_island::buffer *this,
fake_island::buffer *a8,
fake_island::buffer *a9)
{
    // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

    StatusReg = _ReadStatusReg(TPIDR_EL0);
    v32[31] = *(_QWORD *)(StatusReg + 40);
    v13 = a5;
    v14 = n;
    v30 = a4;
    switch ( a6 )
        {
            case 0u:
                if ( !*a1 )
                    goto LABEL_28;
                v29 = a5;
                if ( (fake_island::buffer::add(this, src, n) & 1) != 0 )
                {
                    v23 = *a1;
                    v28[2] = StatusReg;
                    v24 = EVP_PKEY_get0_RSA(v23);
                    v28[1] = v28;
                    v25 = (char *)v28 - (((unsigned int)RSA_size() + 15LL) & 0x1FFFFFFF0LL);
                    v26 = RSA_public_encrypt(v14, src, v25, v24, 1);
                    if ( v26 < 0 )
                        v17 = 6;
                    else
                        v17 = (fake_island::buffer::add(a9, v25, v26) & 1) == 0;
                    v13 = v29;
                    if ( !v17 )
                        goto LABEL_3;
                }
                else
                {
                    return 1;
                }
                return v17;
            case 1u:
                v21 = a1[1];
                v22 = 10;
                goto LABEL_31;
            case 2u:
                v21 = a1[2];
                v22 = 14;
                LABEL_31:
                v17 = fake_island::client::xchg_ecc(v21, src, n, this, a9, v22);
                if ( v17 )
                    return v17;
                goto LABEL_3;
            case 3u:
                v17 = fake_island::client::xchg_ssm(a1[3], src, n, this, a9, 23);
                if ( v17 )
                    return v17;
                goto LABEL_3;
            case 4u:
            case 5u:
            case 6u:
                v17 = fake_island::client::xchg_antssm(a1[4], a1[5], n, a4, this, a9);
                if ( !v17 )
                {
                    LABEL_3:
                    if ( a6 > 1 )
                    {
                        if ( a6 - 4 > 2 )
                        {
                            fake_island::sm4_crypto::sm4_crypto((fake_island::sm4_crypto *)v32);
                            fake_island::cbc_128::cbc_128((fake_island::cbc_128 *)v31);
                            v31[0] = off_A9A0;
                            fake_island::cbc_128::initialize(
                                (fake_island::cbc_128 *)v31,
                                *(const unsigned __int8 **)this,
                                *((_QWORD *)this + 1));
                            if ( (fake_island::cbc_128_en::update((fake_island::cbc_128_en *)v31, v30, v13, a8) & 1) != 0 )
                            {
                                if ( (fake_island::cbc_128_en::final((fake_island::cbc_128_en *)v31, a8) & 1) != 0 )
                                    v17 = 0;
                                else
                                    v17 = 16;
                            }
                            else
                            {
                                v17 = 15;
                            }
                            fake_island::cbc_128::~cbc_128((fake_island::cbc_128 *)v31);
                            fake_island::sm4_crypto::~sm4_crypto((fake_island::sm4_crypto *)v32);
                        }
                        else
                        {
                            fake_island::ssm_sm4_crypto::ssm_sm4_crypto((fake_island::ssm_sm4_crypto *)v31);
                            fake_island::ssm_cbc_128::ssm_cbc_128((fake_island::ssm_cbc_128 *)v31);
                            v31[0] = off_AA30;
                            v18 = (void *)a1[4];
                            v19 = a1[6];
                            v20 = a1[7];
                            fake_island::ssm_cbc_128::initialize(
              (fake_island::ssm_cbc_128 *)v31,
              v18,
              *(const unsigned __int8 **)this,
              *((_QWORD *)this + 1));
            if ( v19 )
              (*(void (__fastcall **)(_QWORD *, __int64, __int64))(v31[0] + 16LL))(v31, v19, v20);
            if ( ((*(__int64 (__fastcall **)(_QWORD *, unsigned __int8 *, size_t, fake_island::buffer *))(v31[0] + 32LL))(
                    v31,
                    v30,
                    v13,
                    a8)
                & 1) != 0 )
              v17 = 0;
            else
              v17 = 16;
            fake_island::ssm_cbc_128::~ssm_cbc_128((fake_island::ssm_cbc_128 *)v31);
            fake_island::ssm_sm4_crypto::~ssm_sm4_crypto((fake_island::ssm_sm4_crypto *)v31);
          }
        }
        else
        {
          fake_island::aes_128::aes_128((fake_island::aes_128 *)v32);
          fake_island::cbc_128::cbc_128((fake_island::cbc_128 *)v31);
          v31[0] = off_A940;
          fake_island::cbc_128::initialize(
            (fake_island::cbc_128 *)v31,
            *(const unsigned __int8 **)this,
            *((_QWORD *)this + 1));
          if ( (fake_island::cbc_128_en::update((fake_island::cbc_128_en *)v31, v30, v13, a8) & 1) != 0 )
          {
            if ( (fake_island::cbc_128_en::final((fake_island::cbc_128_en *)v31, a8) & 1) != 0 )
              v17 = 0;
            else
              v17 = 16;
          }
          else
          {
            v17 = 15;
          }
          fake_island::cbc_128::~cbc_128((fake_island::cbc_128 *)v31);
          fake_island::aes_128::~aes_128((fake_island::aes_128 *)v32);
        }
      }
      break;
    default:
LABEL_28:
      v17 = 2;
      break;
  }
  return v17;
}

这段代码和刚才 unidbg 模拟的入参,直接丢给克劳德,就能还原出算法了,还原出的java 语言版的算法如下

// Java 实现
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public class MpaasCryptoSimulator {

    public static byte[][] encode(byte[] aesKey, byte[] data, String rsaPublicKeyPem) throws Exception {
        // 输出数组
        byte[][] result = new byte[3][];

        // 输出1: 原始 AES Key (明文)
        result[0] = aesKey;

        // 输出2: AES-128-CBC 加密数据
        // 使用 PKCS5Padding
        Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");

        // 使用全零IV (或者从aesKey派生)
        byte[] iv = new byte[16]; // 需要确认 IV 来源
        IvParameterSpec ivSpec = new IvParameterSpec(iv);

        aesCipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
        result[1] = aesCipher.doFinal(data);

        // 输出3: RSA 加密的 AES Key
        PublicKey publicKey = loadPublicKey(rsaPublicKeyPem);
        Cipher rsaCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey);
        result[2] = rsaCipher.doFinal(aesKey);

        return result;
    }

    private static PublicKey loadPublicKey(String pem) throws Exception {
        String publicKeyPEM = pem
        .replace("-----BEGIN PUBLIC KEY-----", "")
        .replace("-----END PUBLIC KEY-----", "")
        .replaceAll("\\s", "");

        byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);
        return keyFactory.generatePublic(keySpec);
    }

    public static void main(String[] args) throws Exception {
        byte[] aesKey = Base64.getDecoder().decode("cGliUXBVMkl2TkRCaUtIdw==");
        byte[] data = Base64.getDecoder().decode("H4sIAAAAAAAA...");

        String rsaPublicKey = "-----BEGIN PUBLIC KEY-----\n" +
        "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqMuqQR4M94DM4vBhWPAV\n" +
        "...\n" +
        "-----END PUBLIC KEY-----";

        byte[][] result = encode(aesKey, data, rsaPublicKey);

        System.out.println("输出1 (AES Key): " + Base64.getEncoder().encodeToString(result[0]));
        System.out.println("输出2 (加密数据): " + Base64.getEncoder().encodeToString(result[1]));
        System.out.println("输出3 (RSA加密Key): " + Base64.getEncoder().encodeToString(result[2]));
    }
}

经测试,和 unidbg传入相同的入参,返回值确实和unidbg 模拟的一样。

到这里请求里的加密部分算是还原出来了,但是这还不是最终的请求体,最后的请求体是要进过一些长度计算和排列的,这部分可以直接看源码。

最后还原出的请求体如下

可以看下和抓包出来的数据差不多,经验证,确实可以请求通接口

搞定,收工。

结语

为什么这个 so 会这么简单呢?没混淆,没加密,算法没魔改,甚至还是静态注册的 jni 方法,不是大厂的风格呀!那是因为请求头中还有一个Sign,这个就比较复杂了,核心的算法部分,经过了 ollvm 混淆,想还原的话,难度不小,这个就后面再说吧!

这部分 unidbg 模拟的源码放在了知识星球,需要的自取。

博客底部海报.png