分析某软件的请求加密和响应解密流程_思路分享
只做思路分享,请不要向我询问目标软件,如果有需要可以分享我用来分析的 JEB 文件等
加密算法
既然我们要分析请求与响应那么我们必须要找到一个按钮或操作事件来触发请求, 那我们现在的目的很简单了
- 打开目标软件
- 打开任意一个网络监控软件(这里我使用的 Reqable ),注意过滤无效请求
- 触发操作事件观察网络请求
如下图我找到抽奖领会员按钮被点击后必然会发送一个请求
我们已经找到请求被触发的时机,接下来我们去分析它的反编译后的代码查看它到底是怎么进行加密的
1.在jadx中搜索关键词 -> 找到资源ID -> 查找引用位置 -> 定位到了按钮的定义 -> 查看按钮被点击的回调 -> 发现在回调中会创建新的窗体 -> 查看该窗体的 onCreate 函数。如下:
@Override // com.lvcha.main.BaseActivity
public void onCreate(Bundle bundle0) {
super.onCreate(bundle0);
this.setContentView(0x7F0A0011);
this.o = (TextView)this.findViewById(0x7F080135);
this.p = (ImageView)this.findViewById(0x7F080141);
this.q = (TextView)this.findViewById(0x7F080140);
this.r = (TextView)this.findViewById(0x7F08013C);
this.s = (TextView)this.findViewById(0x7F080142);
this.t = (ImageView)this.findViewById(0x7F08013E);
this.u = (Button)this.findViewById(0x7F08013D);
this.v = (Button)this.findViewById(0x7F080134);
if(vs1.F().Z()) {
this.u.setEnabled(false);
this.u.setText(0x7F0D0020);
}
else {
this.u.setEnabled(true);
this.u.setText(0x7F0D002F);
}
this.o.setText(String.valueOf(vs1.F().J()));
this.u.setOnClickListener((View view0) -> {
this.getClass();
this.startActivityForResult(new Intent(this, LvchaLoginActivity.class).putExtra(hl1.a("vRBNy4s=\n", "0H8prueTtEU=\n"), 2), 1);
});
this.v.setOnClickListener((View view0) -> {
this.getClass();
this.startActivityForResult(new Intent(this, LvchaChargeActivity.class), 1);
});
this.p.setOnClickListener((View view0) -> {
if(this.z != null && (this.z.isRunning())) {
return;
}
if(vs1.F().S() < 1) {
this.y(0x7F0D015F, this.getString(0x7F0D0160));
return;
}
ObjectAnimator objectAnimator0 = ObjectAnimator.ofFloat(this.t, hl1.a("h7nSo7Yz3Zs=\n", "9damwsJasvU=\n"), new float[]{0.0f, 360.0f});
this.z = objectAnimator0;
objectAnimator0.setRepeatCount(-1);
this.z.setDuration(1000L);
this.z.setInterpolator(new LinearInterpolator());
this.z.start();
this.w();
});
this.x();
this.v();
}
我们可以发现代码前面都是对新窗体的页面控件进行设置,而 this.x(); 里面是进行设置监听器,做页面动画使用的,通过观察 this.v(); 发现就是它在发送请求。
ya00.N 方法是响应的回调, ya00.u 是进行发送请求。遇到这样的混淆可以通过AI让它协助分析。
public final void v() {
if((vs1.F().Z()) && (this.u.isEnabled())) {
this.u.setEnabled(false);
this.u.setText(0x7F0D0020);
}
ya0 ya00 = new ya0();
ya00.N((w w0, int v, Object object0) -> {
this.getClass();
if(v != 0) {
if(v != 5) {
return;
}
try {
it1.J(((String)object0), new com.lvcha.main.activity.LvchaLotteryActivity.b(this));
}
catch(Throwable throwable0) {
throwable0.printStackTrace();
}
return;
}
q.O(() -> Toast.makeText(lvchaLotteryActivity0, lvchaLotteryActivity0.getString(0x7F0D00D9), 0).show());
});
ya00.u(vq1.getLotteryCountURL(), vq1.getRequestParams(new ArrayMap()));
public class com.lvcha.main.activity.LvchaLotteryActivity.b implements o01 {
@Override // o01
public void a(int v, String s) {
q.O(() -> Toast.makeText(LvchaLotteryActivity.this, s, 0).show());
}
@Override // o01
public void b(JSONObject jSONObject0) {
vs1.F().E0(jSONObject0.optInt(hl1.a("MZY8VGSTkFA+lj1OdQ==\n", "XflIIAHh6Q8=\n"), 0)); // lottery_count
LvchaLotteryActivity.this.w = jSONObject0.optInt(hl1.a("0Z3FL5aIJm7Th9w5log=\n", "vfKxW/P6XzE=\n"), 0); // lottery_number
LvchaLotteryActivity.this.x = jSONObject0.optInt(hl1.a("SnWfhwcd5npSc4aW\n", "Jhrr82JvnyU=\n"), 0); // lottery_time
q.O(() -> {
LvchaLotteryActivity.this.o.setText(String.valueOf(vs1.F().S()));
LvchaLotteryActivity.this.r.setText(String.valueOf(LvchaLotteryActivity.this.w));
LvchaLotteryActivity.this.s.setText(String.valueOf(LvchaLotteryActivity.this.x));
});
}
}
}
我们找到了请求代码接下来就可以分析它是怎么进行的发送请求
ya00.u 第一个参数我们点进去观察,发现它是一个字符串,这个字符串刚好和我们的请求路径一样,可以推测这是在进行设置请求路径
public static String getLotteryCountURL() {
return vq1.k() + hl1.a("7levzFim01iuVeiGTbrke7RP6A==\n", "wSGc4z/DpxQ=\n"); // a方法返回值: /v3/getLotteryCount
}
ya00.u 第二个参数我们点进去观察,发现它是请求参数的封装Map,特别是 content 请求的加密数据
// 获取请求参数 进行签名
public static Map getRequestParams(Map map0) {
if(map0 == null) {
return null;
}
vs1.F().getGlobalParams(map0);
uv.getSignatureParams(q.getApplication(), map0);
HashMap hashMap0 = new HashMap();
hashMap0.put(hl1.a("9Zx5G79o6w==\n", "lvMXb9oGn/g=\n"), Signature.a(map0)); // a方法返回值: content
if(!it1.X(vs1.F().T())) {
hashMap0.put(hl1.a("tEekhZg=\n", "wCjP4PaUErg=\n"), vs1.F().T());
}
hashMap0.put(hl1.a("nQ==\n", "6/Fbr5mpncM=\n"), hl1.a("OUI=\n", "C3BtcjmYpSw=\n"));
return hashMap0;
}
进入 Signature.a 方法进行观察,发现它对传入的Map进行处理后调用了一个native方法(public static native String cs)
import hl1;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map.Entry;
import java.util.Map;
public abstract class Signature {
static {
System.loadLibrary(hl1.a("v1OS586aota6U5Q=\n", "0yXxj6/0w6I=\n"));
}
public static String a(Map map0) {
ArrayList arrayList0 = new ArrayList();
for(Object object0: map0.entrySet()) {
Map.Entry map$Entry0 = (Map.Entry)object0;
if(map$Entry0.getValue() == null || ("".equals(map$Entry0.getValue().toString()))) {
continue;
}
arrayList0.add(((String)map$Entry0.getKey()) + hl1.a("Tg==\n", "c3Rmtq35VCY=\n") + map$Entry0.getValue() + hl1.a("eQ==\n", "X/1KX2lL9eA=\n")); // 处理参数 k=v&
}
String[] arr_s = (String[])arrayList0.toArray(new String[0]);
return Signature.cs(((String[])Arrays.copyOf(arr_s, arr_s.length + 1)));
}
public static void b() {
}
public static native String cs(String[] arg0) {
}
public static native String d(byte[] arg0) {
}
public static native String ga() {
}
// w.w 真正执行了这个代码
public static native byte[] gc(String arg0) {
}
}
我们对两个方法进行 hook 观察它们到底在做什么,通过观察确定请求所需要的加密数据就是在 cs 内部完成的。
类名:com.lvcha.main.util.Signature
日志名称:a(Map)
参数1
参数类型:android.util.ArrayMap
参数值:{device=a3a8d7c25cdf8bd7, p1=101001, p2=6, p3=2, p4=MI+CC9+Pro, p5=3, p6=30, p7=com.abjlvcha.main, p8=22, p9=0adddcd0-0f6f-4df6-a2e1-b5c8efd4b40f, p10=47, p11=zh, p12=e8704e350d52e68456aeb63f85b17fa6, p13=5775940, uid=dcc04ace3b68479a96d73a29bbc3c726, token=7z+9RIFgc8IH//W2ojpyBbrU9Ah2EO/kGxgvNmh2fp0k2akDC9ATTqPsFSkFCfTOZG0VoqE3wixngkmHVjOnnQYWy+aX066t6HfOMGOGV/IPWWG8FJSVGjaViTeITLHLG0EzSX+AuwSUjH1xG7E0NyIgLXjUKdfxmY++8pXvlnXKMToJy2PqrvMNFbB7Ov24q8YxlRDNlJDiIYthVwUHmrqNnCYxBMfXjGgOlYaNBf1QIuDEkLhyAMEGhk09SJK/6G0J7rlt16xJ8rYv5P/t17Z2+xLi90O3O3tx6BSpvuKRbQg/fJ86r5FMuwLoLYha3bW7LMLLy3Wt99yLvNmP2IWy4XsBhenrD3w61XZl79aNi0R0dv09kYdvjrJXup1sfKPwm7H0is0Zrg6yj3rdaR17D9vRjTECGOEnktU7vEdxvnnL2fHM5q1rDv8NHldYsDsnUOZOOFf7QesSmMh/ylsfFzEXdv0RrSa2z71vfbcRYTP0iweQa4yJzg9h69lvIYJ8dD+UBbiAW+CvEkY8uz29RoxMnr5dKreJ7UaoaLrJFyusaOAtgPSqIWx6/kivUKGdnA==, platform=1}
返回结果类型:java.lang.String
返回结果值:AoxCzVP3zpaAiTLw0ih0/HfJkYMP3R6pcjoAvsKDvp8+gO2nIgNO/S3bTunI/5KW/Qoax+H0Cx0fvfhK0zzG4Rj0CqtMMa9qsTUIrqUs914IHL1ndhgua7+FOQWNWUUE0nIQ1T+7BFdX0+5jHdgDO0MUqREIVnnG8GXoOt8zHA5zWSjpobKzrqAp21UITdEUpXcMHefCBeIk+iJFW312taKTIxHUHLnENhQARiGorY/dpBxa/9v2CEWApufspR1eppsdG3cNIyJjcgqYF0omo+wcodBW9jt92HaR+gBd3qxwbjvRocJkboKMJ/Hb/aH+s7BlXELd+XBoMAS/7EHFwXT+Qx/2jTswN3+0j+MRtFmgPzcAhwc17seuHZXKrxsG7mz+S9MT2QJGoTHTkjcBmNmESD08PeS4bWZ7xC0DrgyhoCp5D4AJzW77lyjRUY9VoA2foiWCfaIZ+ugvsqBtGsPbP37V5j9hL5i6jqZQUjszm4xHqQhVEgB04Xe1klEwhAakRXrHVNiRV7fanNG4zNgxYBEwUYWogCbv1kGNM+Z2WXM8DA7OL2eu8WADgBEjZZHJC1nm9KWIaCrZbNuKbtFnM1dXv9KMVMwXMECPW2hAqrrT5gcCHFlMYpWYJEeIqimQYYpDXnOdL8UH9BEFTATG3+Ut7d5pSK43fLd5K8Vcuxb2/7swDU/Fd8edB4LIXMaiGyU/9MCaEzfaDlnc1k1oAGIcoMi/Bl4PsV0++XEwsekUC47olxwusp9KKP4aSxME8i66Vd8eLWjwmOkwu+z/JDTBrElzFZ7ttLomU5ZCuNPtv8C4vOEjHBIfk605t7wP0GXS6jK5RfXi34nD5e8M6tajKP2peMfz8qW38czHe+25cCfNtUJBLs1fJCIvga7ZX44lJzE4RuUUtOS5KsDMhz9nRyecBLekmPumC0ehSmyPDLRTtNHd6syYHMrdE3sryUuZXX7fMA0jpw3ovckxETG9yqjR6CqEsS4I7U6IbIwOMipFI8UIX+Z78qZUC6GuZZkBgiZqm2OXW3cV2mEu8rEHQapw5pstWTS5KOHRnfKHxIxOySgWh0Vq9GobODxR/5Hn7DrdFQTFh4GYiGVMbKUSc5WFBnqi/ISq3WcXJdmnm/JR1VFmSbEkJrjXk+6t6xajtI+VaK2u8TNV
------------------------------------------------------------------------------------------------
类名:com.lvcha.main.util.Signature
日志名称:cs(String;)
参数1
参数类型:[Ljava.lang.String;
参数值:{device=a3a8d7c25cdf8bd7&,p1=101001&,p2=6&,p3=2&,p4=MI+CC9+Pro&,p5=3&,p6=30&,p7=com.abjlvcha.main&,p8=22&,p9=0adddcd0-0f6f-4df6-a2e1-b5c8efd4b40f&,p10=47&,p11=zh&,p12=e8704e350d52e68456aeb63f85b17fa6&,p13=5775940&,uid=dcc04ace3b68479a96d73a29bbc3c726&,token=7z+9RIFgc8IH//W2ojpyBbrU9Ah2EO/kGxgvNmh2fp0k2akDC9ATTqPsFSkFCfTOZG0VoqE3wixngkmHVjOnnQYWy+aX066t6HfOMGOGV/IPWWG8FJSVGjaViTeITLHLG0EzSX+AuwSUjH1xG7E0NyIgLXjUKdfxmY++8pXvlnXKMToJy2PqrvMNFbB7Ov24q8YxlRDNlJDiIYthVwUHmrqNnCYxBMfXjGgOlYaNBf1QIuDEkLhyAMEGhk09SJK/6G0J7rlt16xJ8rYv5P/t17Z2+xLi90O3O3tx6BSpvuKRbQg/fJ86r5FMuwLoLYha3bW7LMLLy3Wt99yLvNmP2IWy4XsBhenrD3w61XZl79aNi0R0dv09kYdvjrJXup1sfKPwm7H0is0Zrg6yj3rdaR17D9vRjTECGOEnktU7vEdxvnnL2fHM5q1rDv8NHldYsDsnUOZOOFf7QesSmMh/ylsfFzEXdv0RrSa2z71vfbcRYTP0iweQa4yJzg9h69lvIYJ8dD+UBbiAW+CvEkY8uz29RoxMnr5dKreJ7UaoaLrJFyusaOAtgPSqIWx6/kivUKGdnA==&,platform=1&,null}
返回结果类型:java.lang.String
返回结果值:AoxCzVP3zpaAiTLw0ih0/HfJkYMP3R6pcjoAvsKDvp8+gO2nIgNO/S3bTunI/5KW/Qoax+H0Cx0fvfhK0zzG4Rj0CqtMMa9qsTUIrqUs914IHL1ndhgua7+FOQWNWUUE0nIQ1T+7BFdX0+5jHdgDO0MUqREIVnnG8GXoOt8zHA5zWSjpobKzrqAp21UITdEUpXcMHefCBeIk+iJFW312taKTIxHUHLnENhQARiGorY/dpBxa/9v2CEWApufspR1eppsdG3cNIyJjcgqYF0omo+wcodBW9jt92HaR+gBd3qxwbjvRocJkboKMJ/Hb/aH+s7BlXELd+XBoMAS/7EHFwXT+Qx/2jTswN3+0j+MRtFmgPzcAhwc17seuHZXKrxsG7mz+S9MT2QJGoTHTkjcBmNmESD08PeS4bWZ7xC0DrgyhoCp5D4AJzW77lyjRUY9VoA2foiWCfaIZ+ugvsqBtGsPbP37V5j9hL5i6jqZQUjszm4xHqQhVEgB04Xe1klEwhAakRXrHVNiRV7fanNG4zNgxYBEwUYWogCbv1kGNM+Z2WXM8DA7OL2eu8WADgBEjZZHJC1nm9KWIaCrZbNuKbtFnM1dXv9KMVMwXMECPW2hAqrrT5gcCHFlMYpWYJEeIqimQYYpDXnOdL8UH9BEFTATG3+Ut7d5pSK43fLd5K8Vcuxb2/7swDU/Fd8edB4LIXMaiGyU/9MCaEzfaDlnc1k1oAGIcoMi/Bl4PsV0++XEwsekUC47olxwusp9KKP4aSxME8i66Vd8eLWjwmOkwu+z/JDTBrElzFZ7ttLomU5ZCuNPtv8C4vOEjHBIfk605t7wP0GXS6jK5RfXi34nD5e8M6tajKP2peMfz8qW38czHe+25cCfNtUJBLs1fJCIvga7ZX44lJzE4RuUUtOS5KsDMhz9nRyecBLekmPumC0ehSmyPDLRTtNHd6syYHMrdE3sryUuZXX7fMA0jpw3ovckxETG9yqjR6CqEsS4I7U6IbIwOMipFI8UIX+Z78qZUC6GuZZkBgiZqm2OXW3cV2mEu8rEHQapw5pstWTS5KOHRnfKHxIxOySgWh0Vq9GobODxR/5Hn7DrdFQTFh4GYiGVMbKUSc5WFBnqi/ISq3WcXJdmnm/JR1VFmSbEkJrjXk+6t6xajtI+VaK2u8TNV
我们需要在IDA中查看 cs 代码
__int64 __fastcall Java_com_lvcha_main_util_Signature_cs(JNIEnv *env, jclass classz, jobjectArray inputArray)
{
unsigned __int64 StatusReg; // x25
int v6; // w22
jstring v7; // x26
int v8; // w5
int v9; // w6
int v10; // w7
unsigned int v11; // w23
jobject v12; // x24
__int64 (__fastcall *s)(int, int, int, int, int, int, int, int, int, int, int, int, __int64, int, void *); // x0
unsigned __int64 *v14; // x25
__int64 (__fastcall *src)(int, int, int, int, int, int, int, int, int, int, int, int, __int64, int, void *); // x26
size_t n0x17; // x0
size_t n; // x27
void *dest; // x28
__int64 v19; // x0
unsigned __int64 n26; // x20
JNIEnv v21; // x8
char *v22; // x0
char *v23; // x1
unsigned __int64 v25; // x8
int v26; // w3
unsigned __int8 *v27; // x20
unsigned __int8 *v28; // x21
unsigned __int64 v29; // x8
void *src_1; // x1
size_t n_1; // x2
unsigned int _Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char_1; // w9
unsigned int v33; // w8
__int64 v34; // x1
jbyteArray v35; // x21
const jbyte *v36; // x4
unsigned __int64 v37; // x3
void *v38; // x23
void *v39; // x24
__int64 v40; // x22
unsigned __int64 *v41; // x19
unsigned __int64 *v42; // x8
void *v43; // x0
unsigned __int64 *v44; // x20
char v45; // t1
__int64 v46; // [xsp+0h] [xbp-90h]
jstring v47; // [xsp+8h] [xbp-88h]
void *StatusReg_1; // [xsp+10h] [xbp-80h]
__int64 (__fastcall *p__Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char)(int, int, int, int, int, int, int, int, int, int, int, int, __int64, int, void *); // [xsp+18h] [xbp-78h] BYREF
__int64 v50; // [xsp+20h] [xbp-70h]
void *v51; // [xsp+28h] [xbp-68h]
void *v52; // [xsp+30h] [xbp-60h] BYREF
unsigned __int64 *v53; // [xsp+38h] [xbp-58h]
unsigned __int64 v54; // [xsp+40h] [xbp-50h]
_BYTE v55[64]; // [xsp+48h] [xbp-48h] BYREF
__int64 v56; // [xsp+88h] [xbp-8h]
StatusReg = _ReadStatusReg(TPIDR_EL0);
v56 = *(_QWORD *)(StatusReg + 40);
if ( byte_EAA58 != 1 )
return 0;
v6 = (*env)->GetArrayLength(env, inputArray);
sub_69208((__int64)v55, 64, 64, "p14=%ld&", qword_EA7F0);// 这里 qword_EA7F0 的值应该是apk大小
v7 = (*env)->NewStringUTF(env, v55);
(*env)->SetObjectArrayElement(env, inputArray, v6 - 1, v7);
v52 = 0;
v53 = 0;
v54 = 0;
if ( v6 >= 1 )
{
v11 = 0;
v47 = v7;
StatusReg_1 = (void *)StatusReg;
while ( 1 )
{
v12 = (*env)->GetObjectArrayElement(env, inputArray, v11);
s = (__int64 (__fastcall *)(int, int, int, int, int, int, int, int, int, int, int, int, __int64, int, void *))(*env)->GetStringUTFChars(env, v12, 0);
v14 = v53;
src = s;
p__Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char = s;
if ( (unsigned __int64)v53 < v54 )
break;
v19 = std::vector<std::string>::__emplace_back_slow_path<char const*&>(
&v52,
&p__Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char);
LABEL_16:
v21 = *env;
v53 = (unsigned __int64 *)v19;
v21->ReleaseStringUTFChars(
env,
v12,
(const char *)p__Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char);
(*env)->DeleteLocalRef(env, v12);
if ( v6 == ++v11 )
{
v22 = (char *)v52;
v23 = (char *)v53;
v7 = v47;
goto LABEL_20;
}
}
n0x17 = strlen((const char *)s);
if ( n0x17 >= 0xFFFFFFFFFFFFFFF8LL )
sub_6ADA0(v14);
n = n0x17;
if ( n0x17 >= 0x17 )
{
if ( (n0x17 | 7) == 0x17 )
n26 = 26;
else
n26 = (n0x17 | 7) + 1;
dest = (void *)operator new(n26);
v14[1] = n;
v14[2] = (unsigned __int64)dest;
*v14 = n26 | 1;
}
else
{
*(_BYTE *)v14 = 2 * n0x17;
dest = (char *)v14 + 1;
if ( !n0x17 )
goto LABEL_15;
}
memmove(dest, src, n);
LABEL_15:
*((_BYTE *)dest + n) = 0;
v19 = (__int64)(v14 + 3);
goto LABEL_16;
}
v23 = 0;
v22 = 0;
LABEL_20:
v25 = 126 - 2 * __clz(0xAAAAAAAAAAAAAAABLL * ((v23 - v22) >> 3));
if ( v23 == v22 )
v26 = 0;
else
v26 = v25;
p__Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char = caseInsensitiveCompare;
std::__introsort<std::_ClassicAlgPolicy,bool (*&)(std::string const&,std::string const&),std::string*,false>(
(int)v22,
(int)v23,
(int)&p__Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char,
v26,
1,
v8,
v9,
v10,
v46,
(int)v47,
StatusReg_1);
v28 = (unsigned __int8 *)v52;
v27 = (unsigned __int8 *)v53;
p__Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char = 0;
v50 = 0;
v51 = 0;
if ( v52 == v53 )
{
v33 = 0;
_Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char_1 = 0;
}
else
{
do
{
v29 = *v28;
if ( (v29 & 1) != 0 )
src_1 = (void *)*((_QWORD *)v28 + 2);
else
src_1 = v28 + 1;
if ( (v29 & 1) != 0 )
n_1 = *((_QWORD *)v28 + 1);
else
n_1 = v29 >> 1;
std::string::append((int)&p__Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char, src_1, n_1);
v28 += 24;
}
while ( v28 != v27 );
_Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char_1 = (unsigned __int8)p__Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char;
v33 = v50;
}
if ( (_Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char_1 & 1) != 0 )
v34 = v33;
else
v34 = _Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char_1 >> 1;
v35 = (*env)->NewByteArray(env, v34);
if ( ((unsigned __int8)p__Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char & 1) != 0 )
v36 = (const jbyte *)v51;
else
v36 = (char *)&p__Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char + 1;
if ( ((unsigned __int8)p__Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char & 1) != 0 )
v37 = (unsigned int)v50;
else
v37 = (unsigned __int64)(unsigned __int8)p__Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char >> 1;
(*env)->SetByteArrayRegion(env, v35, 0, v37, v36);
v38 = (void *)_JNIEnv::CallStaticObjectMethod(env, qword_EAA60, qword_EAA68);
v39 = (void *)_JNIEnv::CallStaticObjectMethod(env, qword_EAA60, qword_EAA78);
v40 = _JNIEnv::CallStaticObjectMethod(env, qword_EAA60, qword_EAA80);
(*env)->DeleteLocalRef(env, v39);
if ( v38 )
(*env)->DeleteLocalRef(env, v38);
(*env)->DeleteLocalRef(env, v7);
(*env)->DeleteLocalRef(env, v35);
if ( ((unsigned __int8)p__Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char & 1) != 0 )
operator delete(
v51,
(unsigned __int64)p__Z22caseInsensitiveCompareRKNSt6__ndk112basic_stringIcNS_11char & 0xFFFFFFFFFFFFFFFELL);
v41 = (unsigned __int64 *)v52;
if ( v52 )
{
v42 = v53;
v43 = v52;
if ( v52 != v53 )
{
v44 = v53;
do
{
v45 = *((_BYTE *)v44 - 24);
v44 -= 3;
if ( (v45 & 1) != 0 )
operator delete((void *)*(v42 - 1), *(v42 - 3) & 0xFFFFFFFFFFFFFFFELL);
v42 = v44;
}
while ( v44 != v41 );
v43 = v52;
}
v53 = v41;
operator delete(v43, v54 - (_QWORD)v43);
}
return v40;
}
我让AI还原的代码
#include <jni.h>
#include <vector>
#include <string>
#include <algorithm>
#include <cstdio>
// 全局变量
extern bool byte_EAA58 = 1;
extern int64_t qword_EA7F0 = 5,775,940; // apk大小
extern jclass qword_EAA60; // Ru3 类
extern jmethodID qword_EAA68; // 方法: a()[B
extern jmethodID qword_EAA78; // 方法: b([B[B)[B
extern jmethodID qword_EAA80; // 方法: a([B)Ljava/lang/String;
// 不区分大小写比较函数
bool caseInsensitiveCompare(const std::string &a, const std::string &b);
jstring Java_com_lvcha_main_util_Signature_cs(JNIEnv *env, jclass clazz, jobjectArray inputArray)
{
if (!byte_EAA58)
{
// JNI_Load 中进行的赋值
return nullptr;
}
jsize len = env->GetArrayLength(inputArray);
if (len <= 0)
{
return nullptr; // 输入数组不应为空
}
// 构造参数字符串 "p14=0&" 并添加到数组末尾
char buffer[64];
snprintf(buffer, sizeof(buffer), "p14=%ld&", qword_EA7F0);
jstring newElement = env->NewStringUTF(buffer);
env->SetObjectArrayElement(inputArray, len - 1, newElement);
// 将 Java 字符串数组转换为 std::vector<std::string>
std::vector<std::string> vec;
vec.reserve(len);
for (jsize i = 0; i < len; ++i)
{
jstring strObj = (jstring)env->GetObjectArrayElement(inputArray, i);
const char *utfStr = env->GetStringUTFChars(strObj, nullptr);
vec.push_back(utfStr);
env->ReleaseStringUTFChars(strObj, utfStr);
env->DeleteLocalRef(strObj);
}
// 不区分大小写排序
std::sort(vec.begin(), vec.end(), caseInsensitiveCompare);
// 拼接所有字符串
std::string concatenated;
for (const auto &s : vec)
{
concatenated.append(s);
}
// 将拼接结果转换为 Java byte[]
jbyteArray byteArray = env->NewByteArray(concatenated.size());
env->SetByteArrayRegion(byteArray, 0, concatenated.size(), reinterpret_cast<const jbyte *>(concatenated.data()));
// 获取 Ru3 类
jclass clazzRu3 = env->FindClass("u3/Ru3");
// 获取方法 ID
jmethodID methodId_a = env->GetStaticMethodID(clazz, "a", "()[B;");
jmethodID methodId_b = env->GetStaticMethodID(clazz, "b", "([B;[B;)[B;");
jmethodID methodId_a2 = env->GetStaticMethodID(clazz, "a", "([B;)Ljava/lang/String;");
// 调用 Java 方法进行加密处理
jbyteArray key1 = (jbyteArray)env->CallStaticObjectMethod(clazzRu3, methodId_a);
jbyteArray encrypted = (jbyteArray)env->CallStaticObjectMethod(clazzRu3, methodId_b, byteArray, key1);
jstring result = (jstring)env->CallStaticObjectMethod(clazzRu3, methodId_a2, encrypted);
// 清理局部引用
env->DeleteLocalRef(newElement);
env->DeleteLocalRef(byteArray);
env->DeleteLocalRef(key1);
env->DeleteLocalRef(encrypted);
return result;
}
其中 qword_EAA68 qword_EAA78 qword_EAA80 三个全局变量可以发现都是在 JNI_OnLoad 中被赋值的,我们可以通过 unidbg 加载 JNI_OnLoad 并查看它打印的日志
public static void main(String[] args) {
// 创建 Signature 对象
Signature signature = new Signature();
}
/**
* 构造方法,初始化AndroidEmulator模拟器和VM虚拟机
*/
protected Signature() {
emulator = AndroidEmulatorBuilder
.for64Bit() // 32位模拟器
.setProcessName("com.lvcha.main") // 设置进程名
// .addBackendFactory(new DynarmicFactory(true)) // 添加Dynarmic后端工厂
.build();
// 获取内存对象
Memory memory = emulator.getMemory();
// 设置库解析器
LibraryResolver resolver = new AndroidResolver(23);
memory.setLibraryResolver(resolver);
// 开启GDB调试(可以设置断点)
// Debugger debugger = emulator.attach(DebuggerType.CONSOLE);
// debugger.addBreakPoint(null, 0x6A2E8);
// 创建Dalvik虚拟机
vm = emulator.createDalvikVM();
vm.setJni(this);
// 设置Dalvik虚拟机为verbose模式
vm.setVerbose(true);
// 加载本地库,参数1为库文件路径,参数2是否执行JNI_OnLoad方法
DalvikModule dm = vm.loadLibrary(
new File("unidbg-android/src/test/resources/com/abjlvcha/main/liblvchanative.so"),
false
);
// 先获取so库模块
Module module = emulator.getMemory().findModule("liblvchanative.so");
// 开启 CONSOLE 调试(可以设置断点)
Debugger debugger = emulator.attach(DebuggerType.CONSOLE);
// JNI_OnLoad qword_EA7F0 = v21;
// debugger.addBreakPoint(module, 0x69BB8);
// 静态变量 qword_EAA60 的赋值
// debugger.addBreakPoint(module, 0x69808);
// 主动调用JNI_OnLoad方法
dm.callJNI_OnLoad(emulator);
}
打印日志的顺序可以知道 qword_EAA68 qword_EAA78 qword_EAA80 三个全局变量的值
JNIEnv->FindClass(u3/Ru3) was called from RX@0x120697b4[liblvchanative.so]0x697b4
JNIEnv->NewGlobalRef(class u3/Ru3) was called from RX@0x120697c8[liblvchanative.so]0x697c8
JNIEnv->GetStaticMethodID(u3/Ru3.a([B)Ljava/lang/String;) => 0xd4edb7d5 was called from RX@0x120697f8[liblvchanative.so]0x697f8
JNIEnv->GetStaticMethodID(u3/Ru3.a(Ljava/lang/String;)[B) => 0xb0d016f was called from RX@0x12069820[liblvchanative.so]0x69820
JNIEnv->GetStaticMethodID(u3/Ru3.b([B[B)[B) => 0xc740e7ec was called from RX@0x12069850[liblvchanative.so]0x69850
JNIEnv->GetStaticMethodID(u3/Ru3.c([B[B)Ljava/lang/String;) => 0x15f12ab0 was called from RX@0x1206987c[liblvchanative.so]0x6987c
JNIEnv->GetStaticMethodID(u3/Ru3.a([B[B)[B) => 0x32fc78eb was called from RX@0x120698a0[liblvchanative.so]0x698a0
JNIEnv->GetStaticMethodID(u3/Ru3.a()[B) => 0x509a40f9 was called from RX@0x120698c8[liblvchanative.so]0x698c8
后面我们只需要分析 cs native 中调用到的Java三个函数 就可以知道它的加密逻辑了
加密Java代码如下:
package com.abjlvcha.main.util;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.*;
public class LvchaSecurityProvider {
// 自定义Base64编码表
private static final char[] BASE64_TABLE = new char[64];
// 可手动设置的IV(初始化向量),用于调试或固定场景
public static String use_iv = null;
// 静态初始化块:初始化Base64编码表
static {
initBase64Table();
}
// 初始化自定义Base64编码表
private static void initBase64Table() {
int v = 0;
int v5;
// 添加A-Z (26个大写字母)
for (v5 = 0; v5 <= 25; ++v5) {
BASE64_TABLE[v5] = (char) (v5 + 65);
}
int v6 = 26;
int v7;
// 添加a-z (26个小写字母)
for (v7 = 0; v6 <= 51; ++v7) {
BASE64_TABLE[v6] = (char) (v7 + 97);
++v6;
}
int v8 = 52;
// 添加0-9 (10个数字)
while (v8 <= 61) {
BASE64_TABLE[v8] = (char) (v + 0x30);
++v8;
++v;
}
// 添加Base64最后两个字符
BASE64_TABLE[62] = '+';
BASE64_TABLE[0x3F] = '/';
}
/**
* 生成签名
* @Param inputArray 输入参数数组
* @param uid 用户ID,用作加密密钥
* @return Base64编码的签名字符串
*/
public static String generateSignature(String[] inputArray, String uid) {
try {
// 将数组转换为List,便于操作
List<String> params = new ArrayList<>();
Collections.addAll(params, inputArray);
// 修改最后一个参数
params.set(params.size() - 1, "p14=5775940&");
// 对参数进行不区分大小写的字典序排序
Collections.sort(params, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
});
// 拼接所有参数
StringBuilder concatenated = new StringBuilder();
for (String s : params) {
concatenated.append(s);
}
// 将字符串转换为字节数组
byte[] data = concatenated.toString().getBytes(StandardCharsets.UTF_8);
// 使用UID作为密钥
byte[] key = uid.getBytes(StandardCharsets.UTF_8);
// 使用AES-GCM加密
byte[] encrypted = encryptAesGcm(data, key);
// 返回自定义Base64编码结果
return customBase64Encode(encrypted);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 使用AES-GCM加密数据
* @param data 待加密数据
* @param key 密钥
* @return 加密后的字节数组
*/
private static byte[] encryptAesGcm(byte[] data, byte[] key) throws Exception {
// 生成12字节的随机IV
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);
// 如果手动设置了IV,则使用自定义IV
if (use_iv != null && !use_iv.trim().isEmpty())
iv = Base64.getDecoder().decode(use_iv);
// 获取AES/GCM/NoPadding加密实例
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
// 创建密钥规范
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
// 创建GCM参数规范(128位认证标签长度)
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
// 初始化加密器
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec);
// 执行加密
byte[] encrypted = cipher.doFinal(data);
// 构建结果:版本字节(2) + IV + 加密数据
byte[] result = new byte[1 + iv.length + encrypted.length];
result[0] = 2; // 版本标识
System.arraycopy(iv, 0, result, 1, iv.length);
System.arraycopy(encrypted, 0, result, 1 + iv.length, encrypted.length);
return result;
}
/**
* 自定义Base64编码实现
* @param data 待编码数据
* @return Base64编码字符串
*/
private static String customBase64Encode(byte[] data) {
if (data == null) {
return null;
}
int totalBits = data.length * 8;
if (totalBits == 0) {
return "";
}
int remainderBits = totalBits % 24;
int fullGroups = totalBits / 24;
char[] output = new char[(remainderBits == 0 ? fullGroups : fullGroups + 1) * 4];
int outIndex = 0;
int inIndex = 0;
int groupIndex = 0;
// 处理完整的3字节组
while (groupIndex < fullGroups) {
byte b0 = data[inIndex];
int v6 = inIndex + 2;
byte b1 = data[inIndex + 1];
inIndex += 3;
byte b2 = data[v6];
// 第一个字符:取b0的高6位
int idx0;
if ((b0 & 0xFFFFFF80) == 0) {
idx0 = b0 >> 2;
} else {
idx0 = (b0 >> 2) ^ 0xC0;
}
output[outIndex] = BASE64_TABLE[(byte) idx0];
// 第二个字符:取b0的低2位和b1的高4位
int idx1_part1;
if ((b1 & 0xFFFFFF80) == 0) {
idx1_part1 = b1 >> 4;
} else {
idx1_part1 = (b1 >> 4) ^ 0xF0;
}
int idx1 = idx1_part1 | ((b0 & 3) << 4);
output[outIndex + 1] = BASE64_TABLE[(byte) idx1];
int v7 = outIndex + 3;
// 第三个字符:取b1的低4位和b2的高2位
int idx2_part1 = (b1 & 15) << 2;
int idx2_part2;
if ((b2 & 0xFFFFFF80) == 0) {
idx2_part2 = b2 >> 6;
} else {
idx2_part2 = (b2 >> 6) ^ 0xFC;
}
int idx2 = idx2_part1 | idx2_part2;
output[outIndex + 2] = BASE64_TABLE[(byte) idx2];
outIndex += 4;
// 第四个字符:取b2的低6位
output[v7] = BASE64_TABLE[b2 & 0x3F];
groupIndex++;
}
// 处理剩余字节
switch (remainderBits) {
case 8: {
break;
}
case 16: {
byte b3 = data[inIndex];
byte b4 = data[inIndex + 1];
// 第一个字符
int idx0;
if ((b3 & 0xFFFFFF80) == 0) {
idx0 = b3 >> 2;
} else {
idx0 = (b3 >> 2) ^ 0xC0;
}
output[outIndex] = BASE64_TABLE[(byte) idx0];
// 第二个字符
int idx1_part1;
if ((b4 & 0xFFFFFF80) == 0) {
idx1_part1 = b4 >> 4;
} else {
idx1_part1 = (b4 >> 4) ^ 0xF0;
}
int idx1 = idx1_part1 | ((b3 & 3) << 4);
output[outIndex + 1] = BASE64_TABLE[(byte) idx1];
// 第三个字符
int idx2 = (b4 & 15) << 2;
output[outIndex + 2] = BASE64_TABLE[(byte) idx2];
// 第四个字符补'='
output[outIndex + 3] = '=';
return new String(output);
}
default: {
return new String(output);
}
}
// 处理8位剩余的情况(1字节)
byte b5 = (byte) (data[inIndex] & 3);
int v8 = data[inIndex] & 0xFFFFFF80;
int v9 = data[inIndex] >> 2;
if (v8 != 0) {
v9 ^= 0xC0;
}
output[outIndex] = BASE64_TABLE[(byte) v9];
output[outIndex + 1] = BASE64_TABLE[b5 << 4];
// 后两个字符补'='
output[outIndex + 2] = '=';
output[outIndex + 3] = '=';
return new String(output);
}
}
解密算法
寻找响应解密逻辑
原本我是打算直接分析 ya00.N 方法的,但是被混淆太严重了,索性我们使用算法助手直接把Signature类下的所有方法进行hook (其实我分析找到了这个程序的日志记录方法kb0.m方法),hook Signature 类下的所有方法发现每次请求后 gc 方法都会被调用,合理且大胆的猜测它就是,去 so 里看一眼也没事不是吗,查看之后发现它就是进行解密的方法。
__int64 __fastcall Java_com_lvcha_main_util_Signature_gc(JNIEnv *env)
{
void *v2; // x20
void *v3; // x23
__int64 v4; // x21
if ( byte_EAA58 != 1 )
return 0;
v2 = (void *)_JNIEnv::CallStaticObjectMethod(env, qword_EAA60, qword_EAA68);
v3 = (void *)_JNIEnv::CallStaticObjectMethod(env, qword_EAA60, qword_EAA88);
v4 = _JNIEnv::CallStaticObjectMethod(env, qword_EAA60, qword_EAA90);
(*env)->DeleteLocalRef(env, v3);
if ( v2 )
(*env)->DeleteLocalRef(env, v2);
return v4;
}
通过分析 Java_com_lvcha_main_util_Signature_gc 调用的Java层方法确实是在解密,只不过它返回的是字节数组,hook时我们看不到它加密后的字符形式
在 gc 的调用者处(只要w.w方法是真的有个是假的)发现 Signature.gc 的返回值直接被转成字符串了 然后又通过 kb0.m 打印转化后的结果,我们再次去算法助手查看会发现确实是响应的结果,至此我们完成了请求的加解密逻辑的分析
String s3 = new String(Signature.gc(s2));
kb0.m(this.y.a, false, hl1.a("f1unhbY=\n", "HTTD/IydSGA=\n") + s3); // a方法返回值: body:
s2 = s3;
下面是我让AI复原的解密算法逻辑(我更熟悉Java就用Java复原了)
package com.abjlvcha.main.util;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/*
SecurityGCProvider 类
实现了基于 AES-GCM 加密的签名功能
包含自定义 Base64 解码和 AES-GCM 加密/解密方法
*/
@SuppressWarnings("all")
public class SecurityGCProvider {
/**
* Base64 字符映射表
* 用于自定义 Base64 解码
*/
private static final byte[] BASE64_TABLE;
/**
* 线程本地存储的 IV (初始化向量) 数组
* 长度为 12 字节,用于 AES-GCM 加密
*/
private static final ThreadLocal<byte[]> IV_HOLDER;
/**
* 线程本地存储的 SecureRandom 实例
* 用于生成随机 IV
*/
private static final ThreadLocal<SecureRandom> RANDOM_HOLDER;
/**
* 线程本地存储的 Cipher 实例
* 用于 AES-GCM 加密/解密操作
*/
private static final ThreadLocal<Cipher> CIPHER_HOLDER;
static {
// 初始化 Base64 字符映射表
BASE64_TABLE = new byte[0x80];
Arrays.fill(BASE64_TABLE, (byte) -1);
// 大写字母 A-Z 映射到 0-25
for (int i = 90; i >= 65; --i) {
BASE64_TABLE[i] = (byte) (i - 65);
}
// 小写字母 a-z 映射到 26-51
for (int i = 0x7A; i >= 97; --i) {
BASE64_TABLE[i] = (byte) (i - 71);
}
// 数字 0-9 映射到 52-61
for (int i = 57; i >= 0x30; --i) {
BASE64_TABLE[i] = (byte) (i + 4);
}
// '+' 映射到 62
BASE64_TABLE[43] = 62;
// '/' 映射到 63
BASE64_TABLE[0x2F] = 0x3F;
// 初始化线程本地存储
IV_HOLDER = new ThreadLocal<byte[]>() {
@Override
protected byte[] initialValue() {
return new byte[12]; // 12 字节 IV
}
};
RANDOM_HOLDER = new ThreadLocal<SecureRandom>() {
@Override
protected SecureRandom initialValue() {
return new SecureRandom();
}
};
CIPHER_HOLDER = new ThreadLocal<Cipher>() {
@Override
protected Cipher initialValue() {
try {
return Cipher.getInstance("AES/GCM/NoPadding");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
};
}
/**
* 自定义 Base64 解码方法
*
* @param s Base64 编码的字符串
* @return 解码后的字节数组
*/
public static byte[] base64Decode(String s) {
if (s == null) {
return null;
}
char[] arr_c = s.toCharArray();
int v = filterWhitespace(arr_c); // 去除空白字符
if (v % 4 != 0) {
return null; // Base64 长度必须是 4 的倍数
}
int v1 = v / 4;
if (v1 == 0) {
return new byte[0]; // 空字符串
}
byte[] arr_b1 = new byte[v1 * 3]; // 每 4 个 Base64 字符对应 3 个字节
int v2 = 0;
int v3 = 0;
int v4 = 0;
// 处理完整的 4 字符组
while (v2 < v1 - 1) {
int v5 = v3 + 1;
char c = arr_c[v3];
if (isValidBase64Char(c)) {
int v6 = v3 + 2;
char c1 = arr_c[v5];
if (!isValidBase64Char(c1)) {
return null;
}
int v7 = v3 + 3;
char c2 = arr_c[v6];
if (!isValidBase64Char(c2)) {
return null;
}
v3 += 4;
char c3 = arr_c[v7];
if (!isValidBase64Char(c3)) {
return null;
}
// 解码 4 个 Base64 字符为 3 个字节
byte b = BASE64_TABLE[c];
byte b1 = BASE64_TABLE[c1];
byte b2 = BASE64_TABLE[c2];
byte b3 = BASE64_TABLE[c3];
arr_b1[v4] = (byte) (b << 2 | b1 >> 4);
int v8 = v4 + 2;
arr_b1[v4 + 1] = (byte) ((b1 & 15) << 4 | b2 >> 2 & 15);
v4 += 3;
arr_b1[v8] = (byte) (b2 << 6 | b3);
++v2;
continue;
}
return null;
}
// 处理最后一组 4 字符(可能包含填充)
int v9 = v3 + 1;
char c4 = arr_c[v3];
byte[] arr_b = null;
if (isValidBase64Char(c4)) {
int v10 = v3 + 2;
char c5 = arr_c[v9];
if (isValidBase64Char(c5)) {
byte[] arr_b2 = BASE64_TABLE;
byte b4 = arr_b2[c4];
byte b5 = arr_b2[c5];
char c6 = arr_c[v10];
char c7 = arr_c[v3 + 3];
// 无填充情况
if ((isValidBase64Char(c6)) && (isValidBase64Char(c7))) {
byte b6 = arr_b2[c6];
byte b7 = arr_b2[c7];
arr_b1[v4] = (byte) (b4 << 2 | b5 >> 4);
arr_b1[v4 + 1] = (byte) ((b5 & 15) << 4 | b6 >> 2 & 15);
arr_b1[v4 + 2] = (byte) (b7 | b6 << 6);
return arr_b1;
}
// 两个填充字符情况(解码为 1 个字节)
if ((isPadding(c6)) && (isPadding(c7))) {
if ((b5 & 15) != 0) {
return null;
}
int v11 = v2 * 3;
byte[] arr_b3 = new byte[v11 + 1];
System.arraycopy(arr_b1, 0, arr_b3, 0, v11);
arr_b3[v4] = (byte) (b4 << 2 | b5 >> 4);
return arr_b3;
}
// 一个填充字符情况(解码为 2 个字节)
if (!isPadding(c6) && (isPadding(c7))) {
byte b8 = arr_b2[c6];
if ((b8 & 3) != 0) {
return null;
}
int v12 = v2 * 3;
arr_b = new byte[v12 + 2];
System.arraycopy(arr_b1, 0, arr_b, 0, v12);
arr_b[v4] = (byte) (b4 << 2 | b5 >> 4);
arr_b[v4 + 1] = (byte) (b8 >> 2 & 15 | (b5 & 15) << 4);
}
}
}
return arr_b;
}
/**
* 检查字符是否为有效的 Base64 字符
*
* @param c 待检查的字符
* @return 是否为有效的 Base64 字符
*/
private static boolean isValidBase64Char(char c) {
return c < 0x80 && BASE64_TABLE[c] != -1;
}
/**
* 检查字符是否为 Base64 填充字符 '='
*
* @param c 待检查的字符
* @return 是否为填充字符
*/
private static boolean isPadding(char c) {
return c == 61; // '=' 的 ASCII 码
}
/**
* 检查字符是否为空白字符
*
* @param c 待检查的字符
* @return 是否为空白字符
*/
private static boolean isWhitespace(char c) {
return c == 0x20 || c == 13 || c == 10 || c == 9; // 空格、回车、换行、制表符
}
/**
* 过滤字符串中的空白字符
*
* @param arr_c 字符数组
* @return 过滤后的字符长度
*/
private static int filterWhitespace(char[] arr_c) {
int v = 0;
if (arr_c == null) {
return 0;
}
int v1 = 0;
while (v < arr_c.length) {
if (!isWhitespace(arr_c[v])) {
arr_c[v1] = arr_c[v];
++v1;
}
++v;
}
return v1;
}
/**
* 生成随机 IV (初始化向量)
*
* @return 12 字节的随机 IV
*/
private static byte[] generateRandomIV() {
byte[] iv = IV_HOLDER.get();
SecureRandom random = RANDOM_HOLDER.get();
random.nextBytes(iv);
random.nextInt(); // 增加随机性
return iv;
}
/**
* AES-GCM 解密方法
*
* @param encryptedData 加密数据(包含 IV)
* @param key 解密密钥
* @return 解密后的数据
*/
public static byte[] aesGcmDecrypt(byte[] encryptedData, byte[] key) {
try {
byte[] iv = IV_HOLDER.get();
// 从加密数据中提取前 12 字节作为 IV
System.arraycopy(encryptedData, 0, iv, 0, iv.length);
Cipher cipher = CIPHER_HOLDER.get();
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
// GCM 模式,认证标签长度为 128 位
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(0x80, iv);
synchronized (cipher) { // 线程安全处理
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec);
// 解密 IV 之后的数据
return cipher.doFinal(encryptedData, iv.length, encryptedData.length - iv.length);
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* AES-GCM 加密方法
*
* @param data 待加密数据
* @param key 加密密钥
* @return 加密后的数据(包含 IV)
*/
public static byte[] aesGcmEncrypt(byte[] data, byte[] key) {
try {
byte[] iv = generateRandomIV();
byte[] ivCopy = Arrays.copyOf(iv, iv.length);
Cipher cipher = CIPHER_HOLDER.get();
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(0x80, iv);
synchronized (cipher) { // 线程安全处理
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec);
byte[] encrypted = cipher.doFinal(data);
// 拼接 IV 和密文
byte[] result = new byte[ivCopy.length + encrypted.length];
System.arraycopy(ivCopy, 0, result, 0, ivCopy.length);
System.arraycopy(encrypted, 0, result, ivCopy.length, encrypted.length);
return result;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* gc 签名方法
*
* @param input Base64 编码的加密数据
* @param uid 密钥(字符串形式)
* @return 解密后的字符串
*/
public static String gc(String input, String uid) {
try {
// 1. Base64 解码输入数据
byte[] inputBytes = base64Decode(input);
if (inputBytes == null) {
return null;
}
// 2. 将 uid 转换为 UTF-8 字节数组作为密钥
byte[] uidBytes = uid.getBytes(StandardCharsets.UTF_8);
// 3. 使用 AES-GCM 解密数据
byte[] decrypted = aesGcmDecrypt(inputBytes, uidBytes);
if (decrypted == null) {
return null;
}
// 4. 将解密后的数据转换为字符串并返回
return new String(decrypted);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
解密一个响应看一下:
HTTP/1.1 200
Server: nginx/1.24.0 (Ubuntu)
Date: Sun, 05 Apr 2026 09:36:11 GMT
Content-Type: text/plain;charset=UTF-8
Content-Length: 148
Connection: keep-alive
dNdN/BzOb8KwdjEeit7ZuorlcazH95UG/8IvT1+hH89hnpI49Sa7GLNkKus9BiXuHR8fyWmzHTBc2EGMbXTkMkFpXjwkbLxHsbN9pWpmBayZjCvX/wtOFswaohzhe6SoRK2ZaJMT2t9SCux3Pw==
解密结果
{
"code": 1,
"body": {
"lottery_count": 0,
"lottery_time": 47069,
"lottery_number": 11283
}
}
谢谢您的观看,文章有很多不详细的地方,望海涵。