免责声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!有侵权,请联系我,立即删除!
这个协议还是挺复杂的,不止数据做了签名验证,请求体也是经过加密的,不过请求体的加密不算复杂。本着柿子挑软的捏的精神,本篇文章就先来分析下这个请求体是如何加密的。
直接抓包,是抓不到这个请求的,需要特殊的手段,直接看抓包的数据吧

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

猜测请求体应该是进行了某种加密。
从抓包的结果已经能看出是使用了 mpass,那就可以根据关键词查找相关资料了,可以直接到官方查看开发文档,经查找,这个协议是使用了 mpass 的移动网关服务。
继续查看开发者文档,可以发现存放网络相关全局配置的文件名称,然后搜索这个文件名,搜索结果如下

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

可以看到这里会有不同的加密,具体使用哪种加密,是在刚才的配置文件中指定的,这个 app 使用的是RSA,就是使用的 decode 和 encode 方法。
已经找到是那个 so 和使用的哪个 JNI 方法了,接着就是来分析加解密方法,为了方便分析算法,可以先使用 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 模拟的源码放在了知识星球,需要的自取。
