分析第一步当然是抓包。
很轻松抓到连接,开始分析其数据加解密。
- 小说内容解密:
通过调用栈发现其调用了jni方法进行解密
com.km.encryption.api.Security.decrypt(String str, String str2);
通过frida打印传入的值,发现 第一个参数传入null,没有实质意义。第二个参数传入小说的密文。
-
那我可就打开ida了。
发现其连动态注册都懒得搞,真是方便分析。
- 进入 Java_com_km_encryption_api_Security_decrypt ,静态分析一波
发现其jni完全没有防护,真是怎么简单怎么来。
最终发现在jni调用的java方法,顿时语塞
int __fastcall AKeyGenerator::decode(int a1, JNIEnv *a2, int a3, int a4)
{
jclass v8; // r0
void *v9; // r10
jmethodID v10; // r0
jmethodID v11; // r8
const char *v12; // r9
int v13; // r5
jstring v14; // r0
void *v15; // r6
void *v16; // r8
JNIEnv v17; // r0
int v18; // r0
int v19; // r9
JNIEnv v21; // r0
void *v22; // r1
int v23; // [sp+8h] [bp-20h]
v8 = (*a2)->FindClass(a2, "com/km/encryption/aes/AESManager");
if ( v8 )
{
v9 = v8;
v10 = (*a2)->GetMethodID(a2, v8, "", "(Ljava/lang/String;Ljava/lang/String;)V");
if ( !v10 )
goto LABEL_8;
v11 = v10;
if ( (*a2)->GetStringLength(a2, (jstring)a3) != 6 )
goto LABEL_8;
v23 = a4;
v12 = (*a2)->GetStringUTFChars(a2, a3, 0);
v13 = operator new[](0x11u);
*(_DWORD *)v13 = *(_DWORD *)v12;
*(_WORD *)(v13 + 4) = *((_WORD *)v12 + 2);
_memcpy_chk(v13 + 6, a1, *(_DWORD *)(a1 + 164), 11);
*(_BYTE *)(v13 + 16) = 0;
(*a2)->ReleaseStringUTFChars(a2, (jstring)a3, v12);
v14 = (*a2)->NewStringUTF(a2, v13);
if ( v14 )
{
v15 = v14;
(*a2)->NewStringUTF(a2, (const char *)(a1 + 128));
v16 = (void *)_JNIEnv::NewObject(a2, v9, v11, v15);
v17 = *a2;
if ( v16 )
{
v18 = (int)v17->GetMethodID(a2, v9, "decrypt", "(Ljava/lang/String;)[B");
if ( v18 )
{
v19 = _JNIEnv::CallObjectMethod(a2, v16, v18, v23);
(*a2)->ReleaseStringUTFChars(a2, v15, (const char *)v13);
(*a2)->DeleteLocalRef(a2, v9);
(*a2)->DeleteLocalRef(a2, v15);
(*a2)->DeleteLocalRef(a2, v16);
return v19;
}
(*a2)->ReleaseStringUTFChars(a2, v15, (const char *)v13);
(*a2)->DeleteLocalRef(a2, v9);
(*a2)->DeleteLocalRef(a2, v15);
v21 = *a2;
v22 = v16;
}
else
{
v17->ReleaseStringUTFChars(a2, v15, (const char *)v13);
(*a2)->DeleteLocalRef(a2, v9);
v21 = *a2;
v22 = v15;
}
}
else
{
LABEL_8:
v21 = *a2;
v22 = v9;
}
v21->DeleteLocalRef(a2, v22);
}
return 0;
}
返回java层进行查看,原来就是AES加解密。这不就简单的吗
public static byte[] decrypt(String str, String str2) throws Exception {
try {
byte[] decode = Base64.decode(str.getBytes("UTF-8"), 0);
IvParameterSpec ivParameterSpec = new IvParameterSpec(decode, 0, 16);
byte[] copyOfRange = Arrays.copyOfRange(decode, 16, decode.length);
Cipher instance = Cipher.getInstance("AES/CBC/NoPadding");
instance.init(2, new SecretKeySpec(str2.getBytes(), "AES"), ivParameterSpec);
return instance.doFinal(copyOfRange);
} catch (Exception unused) {
return "fail".getBytes();
}
}
一眼看去就明白了。
编写frida脚本对 AES的init进行hook,获取其key值。
AES解密(AES/CBC/NoPadding) key: 242ccb8230d709e1(固定值)
新建java对其加解密。
String data = "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";
String result;
try {
result = new String(decrypt(data,"242ccb8230d709e1"));
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
成功输出内容,耐思。
再看看其sign
- 通过其输出 为固定32位,怀疑是否为MD5。
- 可惜测试下来,比对结果总是不对。
通过代码搜索最终定位到其同样定位了
com.km.encryption.api.Security.sign(byte[] bArr);
非常诧异,在ida里并未发现其静态注册的jni函数。刚开始还以为用了动态注册,用了yang大神的[frida_hook_libart],一番操作下来,也没有发现其动态注册,十分恼火。
- 使用objection,对libcommon-encryption.so 的导出函数进行分析,发现其注册在 Java_com_km_encryption_api_Security_token
int __fastcall Java_com_km_encryption_api_Security_token(_JNIEnv *a1, int a2, void *a3)
{
AndroidUtils *v5; // r5
MD5KeyGenerator *v6; // r6
size_t v7; // r5
void *v8; // r9
size_t v9; // r0
size_t v10; // r8
size_t v11; // r6
char *v12; // r10
char *v13; // r1
int v14; // r4
const void *v16; // [sp+8h] [bp-A8h]
unsigned __int8 v17; // [sp+Ch] [bp-A4h] BYREF
_BYTE v18[11]; // [sp+Dh] [bp-A3h] BYREF
char v19[12]; // [sp+18h] [bp-98h] BYREF
char v20[108]; // [sp+24h] [bp-8Ch] BYREF
v5 = (AndroidUtils *)AndroidUtils::Instance(_stack_chk_guard);
if ( !AndroidUtils::isInitialized(v5) )
AndroidUtils::init(v5, a1);
if ( AndroidUtils::isCheckFailed(v5) )
return (int)a1->functions->NewStringUTF(&a1->functions, "FAIL");
v6 = (MD5KeyGenerator *)MD5KeyGenerator::Instance(0);
if ( !MD5KeyGenerator::isInitialized(v6) )
MD5KeyGenerator::init(v6, a1);
if ( !MD5KeyGenerator::isInitialized(v6) )
return (int)a1->functions->NewStringUTF(&a1->functions, "FAIL");
v7 = a1->functions->GetArrayLength(&a1->functions, a3);
v8 = (void *)as_unsigned_char_array(a1, a3);
v16 = (const void *)MD5KeyGenerator::getKeyData(v6);
v9 = MD5KeyGenerator::getKeyDataSize(v6);
v10 = v9 + v7;
v11 = v9;
v12 = (char *)operator new[](v9 + v7 + 1);
qmemcpy(v12, v8, v7);
qmemcpy(&v12[v7], v16, v11);
v12[v10] = 0;
std::string::basic_string((int)v19, v12);
MessageDigestAlgorithm::MessageDigestAlgorithm((MessageDigestAlgorithm *)v20);
MessageDigestAlgorithm::toStr((MessageDigestAlgorithm *)&v17);
v13 = *(char **)&v18[7];
if ( !(v17 << 31) )
v13 = v18;
v14 = (int)a1->functions->NewStringUTF(&a1->functions, v13);
operator delete[](v8);
operator delete[](v12);
std::string::~string(&v17);
std::string::~string(v19);
return v14;
}
- 发现其在里面做了字符串拼接,难道是加了salt
qmemcpy(v12, v8, v7);
qmemcpy(&v12[v7], v16, v11);
- 直接编写frida脚本对其必经的函数
int __fastcall std::string::basic_string(int a1, char *s)
{
size_t v4; // r0
*(_DWORD *)a1 = 0;
*(_DWORD *)(a1 + 4) = 0;
*(_DWORD *)(a1 + 8) = 0;
v4 = strlen(s);
std::string::__init(a1, s, v4);
return a1;
}
frida 脚本如下:
function hooknative(){
var libencrypt_base = Process.findModuleByName("libcommon-encryption.so");
if(libencrypt_base){
console.log("libencrypt_base",JSON.stringify(libencrypt_base));
var sub_sign = libencrypt_base.base.add(0xD329);
Interceptor.attach(sub_sign,{
onEnter:function(args){
console.log("sub_sign -->",ptr(args[1]).readCString())
},onLeave:function(){
}
})
}
}
输入如下:
sub_sign --> AUTHORIZATION=app-version=60000application-id=com.kmxs.readerchannel=qm-tengxun_lfis-white=1net-env=1platform=androidqm-params=cLGUuq2-HTZ5gI9wgI9wgI9QgekzN3Me4To-th9wgI9QgI9wgI9wgI9wgI9wH5w5pyRlmqN2tq2-HTZ5gT9Lgh9EgT4nghfeghFnpho5pI4e4hHLpqG-gTu24TGzAIfLAIfLAIG-AqHlAI9n4eOLpyNzNhs2gefl4h05taGQ4qg5A5GsFeZeNeZMgeZUgeZEAI1aN5HjHSNYOLUlpCH5A5HrtT97gaHjHSkLuCNMpqFQmqF5A5G0ufZMu_kAulu7feNqByGjiMo5OMOEcSKUcIuCkexlFfpxpSGLkooVBz1hpeRwuCpy3EGnBIu0c3QHBzoshMdlBfo3fI4U3UgUfCgDgCsDBqNY4eRYBUGSH5w5mqU2m3HWH5HjHzUDpyRjHTZ5fy2rpqw5taGEByHQmqU2m3HWH5HjHSuj45UUmqF5A5GsFegENIgUgeOrFT45taGTBy22BSFQmqF5A5HwghHegeNz4hgLgqFEges5H5w54SGxBzF5A5GSBlJSByf5taGD4q2-HTZ5HSM=reg=d3dGiJc651gSQ8w1
- 果不其然,发现其在需加密的字符串后面拼接了16个字节(d3dGiJc651gSQ8w1)
使用在线MD5加密,测试了一下,结果完全对上。
奈斯,真是愉快的分析。