0x0 背景
2019年5月跟着群友入坑了明日方舟(于2019年11月左右弃坑),顺手拆了CG和立绘(Perfare佬yyds)。后来突发奇想想做个多游戏抽卡模拟器(剧透:咕了)。素材已经到位,接着就是卡池数据了,得想个办法拆出游戏数据,总不能期期手动跟进卡池更新吧?于是说干就干,打开资源文件夹,发现在\AB\Android\gamedata\excel
目录下有gacha_table.ab
文件,这就是卡池数据了。丢进AssetStudio,点开gacha_table
就发现全是看不懂的数据,导出后为gacha_table.bytes
。没办法,只能分析下是怎么解密数据的了。
注:本文为补写,按照回忆重新开始分析,与文中不同的时当时是直接搜索加密相关关键词直接找到几个解密类Hook的。
0x1 寻找解密方法
游戏基于UnityEngine
引擎且使用了il2cpp
。按部就班掏出Il2CppDumper加载libil2cpp.so
及assets\bin\Data\Managed\Metadata\global-metadata.dat
,然后丢IDA分析并加载dump出的IDA python脚本。(值得一提(?)的是,ARM和ARMv7架构对应lib中libil2cpp.so
是完全相同的。仅有少量第三方库存在差异。)
与此同时使用dnSpy
加载dump出的dummy dll寻找到可疑目标类Torappu.GachaDB
,其继承自Torappu.DB.ConstTable -> SingletonAbstractTable -> AbstractTable
。跳转到Torappu.DB
,好家伙,疑似3个加解密方法:
第一个应是Json密文的序列化及反序列化、第二个是很明显的AES加解密,第一次接触RijndaelManaged
类还是在崩坏学园2里、而第三个看着像是基于随机种子的加解密。
查看DBLoader.LoadTable()
方法,伪代码:
int __fastcall DBLoader__LoadTable(int a1, _DWORD *abstractTable)
{
int v3; // r1
int result; // r0
int isProdMode; // r0
int *tableConfig; // r1
int converter; // r5
int v8; // r0
int v9; // r0
if ( !byte_3023ECE )
{
sub_26B1D18(15387);
byte_3023ECE = 1;
}
if ( !abstractTable )
goto LABEL_13;
v3 = (*(int (__fastcall **)(_DWORD *, _DWORD))(*abstractTable + 212))(
abstractTable,
*(_DWORD *)(*abstractTable + 216));
result = 1;
if ( v3 )
return result;
isProdMode = AbstractTable__get_isProdMode(1);
tableConfig = abstractTable + 3;
if ( isProdMode )
tableConfig = abstractTable + 4;
if ( !*tableConfig )
{
LABEL_13:
v9 = ((int (*)(void))loc_26E73D0)();
return CryptUtils__CalculateMD5FromFileOrEmpty(v9);
}
converter = ConverterFactory__Create(*tableConfig, *(_DWORD *)(*tableConfig + 20));
v8 = Class_Torappu_DB_DBLoader;
if ( (*(_BYTE *)(Class_Torappu_DB_DBLoader + 178) & 1) != 0 && !*(_DWORD *)(Class_Torappu_DB_DBLoader + 96) )
v8 = il2cpp_runtime_class_init_0();
return DBLoader___DoLoadTable(v8, abstractTable, converter, 0);
}
发现其调用了ConverterFactory.Create()
创建了实例并传递给了DBLoader._DoLoadTable()
。结合dummy dll来看:
using System;
using Il2CppDummyDll;
namespace Torappu.DB
{
public static class ConverterFactory
{
[Address(RVA = "0x219E9B8", Offset = "0x219E9B8", VA = "0x219E9B8")]
public static IConverter Create(ConverterFactory.ConverterType converterType)
{
return null;
}
public enum ConverterType
{
JSON_DOT_NET,
BSON_DOT_NET,
CRYPTIC_A,
CRYPTIC_B
}
}
}
可见ConverterFactory.Create()
的参数即为使用的加解密方法。而此属性在AbstractTable
的_prodConfig
中。
接着看下ConverterFactory.Create()
的伪代码:
int *__fastcall ConverterFactory__Create(int a1, unsigned int converterType)
{
int *v3; // r4
int *result; // r0
int v5; // r4
int v6; // r4
int *v7; // r5
int v8; // r0
unsigned int v9[5]; // [sp+4h] [bp-14h] BYREF
if ( !byte_3023EBF )
{
sub_26B1D18(14632);
byte_3023EBF = 1;
}
if ( converterType > 3 )
{
LABEL_10:
v9[0] = converterType;
v5 = il2cpp_value_box_0(Class_ConverterFactory_ConverterType, v9);
if ( (*(_BYTE *)(Class_string + 178) & 1) != 0 && !*(_DWORD *)(Class_string + 96) )
il2cpp_runtime_class_init_0(Class_string, 0);
v6 = String__Concat_25024156(0, StringLiteral_11169, v5, StringLiteral_11170, 0);
v7 = sub_26F735C(Class_System_Exception);
Exception___ctor_20095288(v7, v6, 0);
v8 = ((int (__fastcall *)(int *))il2cpp_raise_exception_0)(v7);
result = (int *)CryptUtils__CalculateMD5FromFileOrEmpty(v8);
}
else
{
switch ( converterType )
{
case 0u:
v3 = sub_26F735C(Class_Torappu_DB_JsonNetConverter);
JsonNetConverter___ctor();
break;
case 1u:
v3 = sub_26F735C(Class_Torappu_DB_BsonNetConverter);
BsonNetConverter___ctor();
break;
case 2u:
v3 = sub_26F735C(Class_Torappu_DB_CrypticConverter_A);
CrypticConverter_A___ctor((int)v3);
break;
case 3u:
v3 = sub_26F735C(Class_Torappu_DB_CrypticConverter_B);
CrypticConverter___ctor((int)v3);
v3[4] = 42;
break;
default:
goto LABEL_10;
}
result = v3;
}
return result;
}
可以发现就是通过converterType
来创建对应的Converter
。那么卡池数据 converterType
的值是多少呢?
查看GachaDB
的初始化方法GachaDB___ctor
看到return ConstTable___ctor(a1, Method_ConstTable_GachaData__GachaDB___ctor__);
,点进去看到伪代码:
int __fastcall ConstTable___ctor(int a1, int a2)
{
int v4; // r6
int v5; // r6
int v6; // r6
int v7; // r1
int result; // r0
int v9; // r0
*(_DWORD *)(a1 + 28) = (***(int (__fastcall ****)(_DWORD))(*(_DWORD *)(a2 + 12) + 84))(0);
if ( a1 )
{
v4 = *(_DWORD *)(*(_DWORD *)(*(_DWORD *)(a2 + 12) + 84) + 8);
sub_26A3FD8(v4);
if ( (*(_BYTE *)(v4 + 178) & 1) != 0 )
{
v5 = *(_DWORD *)(*(_DWORD *)(*(_DWORD *)(a2 + 12) + 84) + 8);
sub_26A3FD8(v5);
if ( !*(_DWORD *)(v5 + 96) )
{
v6 = *(_DWORD *)(*(_DWORD *)(*(_DWORD *)(a2 + 12) + 84) + 8);
sub_26A3FD8(v6);
il2cpp_runtime_class_init_0(v6, v7);
}
}
result = (**(int (__fastcall ***)(int))(*(_DWORD *)(*(_DWORD *)(a2 + 12) + 84) + 4))(a1);
}
else
{
v9 = ((int (*)(void))loc_26E73D0)();
result = ActionDict__get_Item_36179992(v9);
}
return result;
}
噔噔咚,当场去世,先尝试看看DBLoader._DoLoadTable()
吧:
int __fastcall DBLoader___DoLoadTable(int a1, int *abstractTable, int converter, int false)
{
int v7; // r0
int isSuccess; // r7
int isProdMode; // r0
int *v10; // r1
int tableConfig; // r7
int assetPath; // r6
int textAsset; // r6
int v14; // r4
int v15; // r4
int convertSuccess; // r5
int v17; // r4
int v19; // r0
int v20[10]; // [sp+10h] [bp-28h] BYREF
v7 = (unsigned __int8)byte_3023ECF;
if ( !byte_3023ECF )
{
sub_26B1D18(15384);
v7 = (int)&unk_3023E20;
byte_3023ECF = 1;
}
if ( false )
{
if ( !abstractTable )
goto LABEL_39;
goto LABEL_8;
}
if ( abstractTable )
{
v7 = (*(int (__fastcall **)(int *, _DWORD))(*abstractTable + 212))(abstractTable, *(_DWORD *)(*abstractTable + 216));
isSuccess = 1;
if ( v7 )
return isSuccess;
LABEL_8:
isProdMode = AbstractTable__get_isProdMode(v7);
v10 = abstractTable + 3;
if ( isProdMode )
v10 = abstractTable + 4;
tableConfig = *v10;
if ( !*v10 )
goto LABEL_39;
if ( *(_BYTE *)(tableConfig + 8) ) // if(loadFromResource)
{
assetPath = *(_DWORD *)(tableConfig + 12);
if ( (*(_BYTE *)(Class_Torappu_Resource_ResourceManager + 178) & 1) != 0
&& !*(_DWORD *)(Class_Torappu_Resource_ResourceManager + 96) )
{
il2cpp_runtime_class_init_0(Class_Torappu_Resource_ResourceManager, 0);
}
textAsset = ResourceManager__Load(0, assetPath, Method_ResourceManager_Load___TextAsset_);
if ( (*(_BYTE *)(Class_UnityEngine_Object + 178) & 1) != 0 && !*(_DWORD *)(Class_UnityEngine_Object + 96) )
il2cpp_runtime_class_init_0(Class_UnityEngine_Object, 0);
if ( Object__op_Equality(0, textAsset, 0, 0) == 1 )
{
v14 = *(_DWORD *)(tableConfig + 12);
if ( (*(_BYTE *)(Class_string + 178) & 1) != 0 && !*(_DWORD *)(Class_string + 96) )
il2cpp_runtime_class_init_0(Class_string, 0);
isSuccess = 0;
v15 = String__Concat_24998280(0, StringLiteral_11173, v14, 0);
if ( (*(_BYTE *)(Class_UnityEngine_DLog + 178) & 1) != 0 && !*(_DWORD *)(Class_UnityEngine_DLog + 96) )
il2cpp_runtime_class_init_0(Class_UnityEngine_DLog, 0);
DLog__LogError(0, v15, 0);
return isSuccess;
}
}
else
{
textAsset = *(_DWORD *)(tableConfig + 16);
}
convertSuccess = (*(int (__fastcall **)(int *, int, int, _DWORD))(*abstractTable + 228))(
abstractTable,
textAsset,
converter,
*(_DWORD *)(*abstractTable + 232));
if ( (*(_BYTE *)(Class_Torappu_Resource_ResourceManager + 178) & 1) != 0
&& !*(_DWORD *)(Class_Torappu_Resource_ResourceManager + 96) )
{
il2cpp_runtime_class_init_0(Class_Torappu_Resource_ResourceManager, 0);
}
isSuccess = 0;
ResourceManager__UnloadAsset(0, textAsset);
if ( convertSuccess == 1 )
{
if ( (*(_BYTE *)(Class_string + 178) & 1) != 0 && !*(_DWORD *)(Class_string + 96) )
il2cpp_runtime_class_init_0(Class_string, 0);
v17 = String__Format(0, StringLiteral_11174, abstractTable, 0);
Color__get_blue(v20, 0, 0);
if ( (*(_BYTE *)(Class_UnityEngine_DLog + 178) & 1) != 0 && !*(_DWORD *)(Class_UnityEngine_DLog + 96) )
il2cpp_runtime_class_init_0(Class_UnityEngine_DLog, 0);
DLog__Log_40639780(0, v17, v20[0], v20[1], v20[2], v20[3], 0);
isSuccess = 1;
}
return isSuccess;
}
LABEL_39:
v19 = ((int (*)(void))loc_26E73D0)();
return CryptUtils__CalculateMD5FromFileOrEmpty(v19);
}
可以看到核心就在于:
convertSuccess = // 此变量名称为猜测
(*(int (__fastcall **)(int *, int, int, _DWORD))(*abstractTable + 228))(
abstractTable,
textAsset,
converter,
*(_DWORD *)(*abstractTable + 232));
要想找到此方法的地址就需要上动调了,不过在这之前,我们已经可以推断出此方法在AbstractTable
的子/父类中,其参数一定包含一个TextAsset
和一个IConverter
。然后在Torappu.DB.ConstTable
搜索到了疑似方法public override bool Init(TextAsset rawData, IConverter converter);
,在IDA中为int __fastcall ConstTable__Init(int a1, int a2, _DWORD *a3, int a4)
。
另外可以看到DBLoader._DoLoadTable()
中在成功加载后调用了DLog__Log_40639780()
并传进了abstractTable
,推测这个日志打印可能会包含有用的信息,因此我决定直接HookConverterFactory.Create()
获取传递进来的converterType
、Hook log方法来获取日志后根据内容反推converterType
。
node .\hook.js
[*] Script loaded
[*] Message: { type: 'send', payload: '0xc7119000' }
[*] Message: { type: 'send', payload: 'Attach success, base address is 0xc7119000' }
converterType: 0x2
log: B] Inited CharacterDB successfully
log: B] Inited TokenDB successfully
log: B] Inited SkillDB successfully
log: B] Inited BuffDB successfully
log: B] Inited stage_db (Torappu.StageDB) successfully
log: B] Inited game_data_consts_db (Torappu.GameDataConstsDB) successfully
log: B] Inited item_db (Torappu.ItemDB) successfully
log: B] Inited StoryDB successfully
log: B] Inited gacha_db (Torappu.GachaDB) successfully
log: B] Inited audio_db (Torappu.Audio.Middleware.Data.TorappuAudioDB) successfully
log: B] Inited enemy_db (Torappu.EnemyDB) successfully
log: B] Inited RangeDB successfully
converterType: 0x0
log: B] Inited building_db (Torappu.BuildingDB) successfully
log: B] Inited handbook_info_db (Torappu.HandbookInfoDB) successfully
log: B] Inited HandbookTeamDB successfully
log: B] Inited mission_db (Torappu.MissionDB) successfully
log: B] Inited zone_db (Torappu.ZoneDB) successfully
log: B] Inited CharWordDB successfully
log: B] Inited favor_db (Torappu.FavorDB) successfully
log: B] Inited checkin_db (Torappu.CheckInDB) successfully
log: B] Inited tip_db (Torappu.TipDB) successfully
log: B] Inited skin_db (Torappu.SkinDB) successfully
log: B] Inited open_server_db (Torappu.OpenServerDB) successfully
log: B] Inited EnemyHandBookDB successfully
log: B] Inited activity_db (Torappu.ActivityDB) successfully
log: B] Inited clue_db (Torappu.ClueDB) successfully
log: B] Inited shop_client_db (Torappu.ShopClientDB) successfully
converterType: 0x2
从log可以看出数据对应的转换方法,这里关心的是Torappu.GachaDB
,对应0x2
即CRYPTIC_A
。
0x2 解密
CrypticConverter_A
为老熟人AES-128-CBC。先看看CrypticConverter_A
的构造器伪方法:
void *__fastcall CrypticConverter_A___ctor(int a1)
{
int v2; // r5
void *temp; // r0
void *v4; // r1
int chatMask; // r0
int _chatMask; // r5
int *m_rm; // r6
int m_rm2; // r6
int encoding; // r7
int encoding_1; // r6
void *result; // r0
int v12; // r0
int *v13; // r4
if ( !byte_3023EC7 )
{
sub_26B1D18(14805);
byte_3023EC7 = 1;
}
v2 = Class_byte__;
sub_26A3FD8(Class_byte__);
*(_DWORD *)(a1 + 0x14) = il2cpp_array_new_specific_0(v2, 16);// this.m_iv = new byte[16]
CrypticConverter___ctor(a1);
temp = (void *)Singleton__get_instance(0, Method_Singleton_PlayerData__get_instance__);
if ( temp )
{
chatMask = PlayerData__get_chatMask((int)temp);
_chatMask = chatMask;
if ( !chatMask || String__get_Length(chatMask, 0) != 32 )
{
v13 = sub_26F735C(Class_System_Exception);
Exception___ctor_20095288(v13, StringLiteral_11171, 0);
v12 = ((int (__fastcall *)(int *))il2cpp_raise_exception_0)(v13);
return (void *)CryptUtils__CalculateMD5FromFileOrEmpty(v12);
}
m_rm = sub_26F735C(Class_System_Security_Cryptography_RijndaelManaged);
temp = (void *)RijndaelManaged___ctor((int)m_rm);
*(_DWORD *)(a1 + 0x10) = m_rm; // this.m_rm = new RijndaelManaged
if ( m_rm )
{
(*(void (__cdecl **)(size_t))(*m_rm + 0x14C))((size_t)m_rm);
temp = *(void **)(a1 + 0x10); // temp = m_rm
if ( temp )
{
(*(void (__cdecl **)(wint_t))(*(_DWORD *)temp + 348))((wint_t)temp);
m_rm2 = *(_DWORD *)(a1 + 0x10);
if ( (*(_BYTE *)(Class_System_Text_Encoding + 178) & 1) != 0 && !*(_DWORD *)(Class_System_Text_Encoding + 96) )
il2cpp_runtime_class_init_0(Class_System_Text_Encoding, 0);
encoding = Encoding__get_UTF8();
temp = (void *)String__Substring_25026412(_chatMask, 0, 16, 0);
v4 = temp;
if ( encoding )
{
temp = (*(void *(__cdecl **)(size_t))(*(_DWORD *)encoding + 0x104))(encoding);// temp = chatMask[:16]
v4 = temp;
if ( m_rm2 )
{
(*(void (__fastcall **)(int, void *, _DWORD))(*(_DWORD *)m_rm2 + 0x11C))(
m_rm2,
temp,
*(_DWORD *)(*(_DWORD *)m_rm2 + 0x120));
encoding_1 = Encoding__get_UTF8();
temp = (void *)String__Substring_25026412(_chatMask, 16, 16, 0);
v4 = temp;
if ( encoding_1 )
{
result = (*(void *(__cdecl **)(size_t))(*(_DWORD *)encoding_1 + 0x104))(encoding_1);// result = chatMask[16:32]
*(_DWORD *)(a1 + 24) = result; // this.m_ivMask = result
return result;
}
}
}
}
}
}
v12 = ((int (__fastcall *)(void *, void *))loc_26E73D0)(temp, v4);
return (void *)CryptUtils__CalculateMD5FromFileOrEmpty(v12);
}
在这里可以发现chatMask
扮演了重要角色,联想到AES-128的key和iv长度为16,很容易怀疑都与chatMask
有关。chatMask
属于类PlayerData
,难道对于不同玩家来说数值不一致???(剧透:都是一样的)另外注意这里将m_ivMask
设置为了chatMask[16:]
,猜测chatMask[:16]
就是key。
再看看实际进行解密的CrypticConverter_A.DecodeInternal()
伪方法:
int __fastcall CrypticConverter_A__DecodeInternal(int a1, int src, int dst)
{
int src_1; // r9
unsigned int v6; // r0
unsigned int int_16; // r7
signed int v8; // r0
unsigned int int_0; // r4
char _src; // r6
int v11; // r0
int m_ivMask; // r8
int m_iv; // r5
int v14; // r0
char ivMask_xor_src; // r6
int v16; // r0
int RijndaelManaged_m_rm; // r0
int RijndaelManaged_m_rm_1; // r0
_DWORD *v19; // r0
_DWORD *v20; // r5
int v21; // r0
int v22; // r4
int v23; // r3
int v24; // r7
int v25; // r0
int v26; // r0
int v27; // r1
int result; // r0
int v29; // r0
int v30; // r3
int v31; // r4
int v32; // r0
if ( !byte_3023EC9 )
{
sub_26B1D18(14806);
byte_3023EC9 = 1;
}
src_1 = Misc__ReadAllBytes(0, src, 0);
if ( src_1 )
{
v6 = 0x10u; // 16
while ( 1 )
{
int_16 = v6; // 16
v8 = *(_DWORD *)(src_1 + 12);
int_0 = int_16 - 16; // 0
_src = 0;
if ( (int)(int_16 - 16) < v8 )
{
if ( v8 <= int_0 )
{
v11 = sub_26E81BC();
((void (__fastcall *)(int))il2cpp_raise_exception_0)(v11);
}
_src = *(_BYTE *)(src_1 + int_16);
}
m_ivMask = *(_DWORD *)(a1 + 0x18); // *this + 24 = byte[] m_ivMask
if ( !m_ivMask )
break;
m_iv = *(_DWORD *)(a1 + 0x14); // *this + 20 = byte[] m_iv
if ( *(_DWORD *)(m_ivMask + 0xC) <= int_0 )// if(m_ivMask.length <= 0)
{
v14 = sub_26E81BC();
((void (__fastcall *)(int))il2cpp_raise_exception_0)(v14);
}
if ( !m_iv )
break;
ivMask_xor_src = *(_BYTE *)(m_ivMask + int_16) ^ _src;
if ( *(_DWORD *)(m_iv + 0xC) <= int_0 )
{
v16 = sub_26E81BC();
((void (__fastcall *)(int))il2cpp_raise_exception_0)(v16);
}
v6 = int_16 + 1;
*(_BYTE *)(m_iv + int_16) = ivMask_xor_src;// m_iv = ivMask_xor_src
if ( (int)(int_16 - 15) >= 16 )
{
RijndaelManaged_m_rm = *(_DWORD *)(a1 + 0x10);
if ( RijndaelManaged_m_rm )
{
(*(void (__fastcall **)(int, _DWORD, _DWORD))(*(_DWORD *)RijndaelManaged_m_rm + 268))(
RijndaelManaged_m_rm,
*(_DWORD *)(a1 + 0x14), // m_iv
*(_DWORD *)(*(_DWORD *)RijndaelManaged_m_rm + 272));
RijndaelManaged_m_rm_1 = *(_DWORD *)(a1 + 0x10);
if ( RijndaelManaged_m_rm_1 )
{
v19 = (_DWORD *)(*(int (__fastcall **)(int, _DWORD))(*(_DWORD *)RijndaelManaged_m_rm_1 + 356))(
RijndaelManaged_m_rm_1,
*(_DWORD *)(*(_DWORD *)RijndaelManaged_m_rm_1 + 360));
v20 = v19;
if ( v19 )
{
v21 = *v19;
v22 = *(_DWORD *)(src_1 + 12);
if ( *(_WORD *)(*v20 + 170) )
{
v23 = *(_DWORD *)(v21 + 76);
v24 = 0;
while ( *(_DWORD *)(v23 + 8 * v24) != Class_System_Security_Cryptography_ICryptoTransform )
{
if ( ++v24 >= (unsigned int)*(unsigned __int16 *)(*v20 + 170) )
goto LABEL_23;
}
v25 = v21 + 8 * *(_DWORD *)(v23 + 8 * v24 + 4) + 220;
}
else
{
LABEL_23:
v25 = sub_26A6A34(v20, Class_System_Security_Cryptography_ICryptoTransform, (const char *)5);
}
v27 = (*(int (__fastcall **)(_DWORD *, int, int, int, _DWORD))v25)(
v20,
src_1,
16,
v22 - 16,
*(_DWORD *)(v25 + 4));
if ( v27 )
{
if ( dst )
{
result = (*(int (__fastcall **)(int, int, _DWORD, _DWORD, _DWORD))(*(_DWORD *)dst + 340))(
dst,
v27,
0,
*(_DWORD *)(v27 + 12),
*(_DWORD *)(*(_DWORD *)dst + 344));
if ( v20 )
{
v29 = *v20;
if ( *(_WORD *)(*v20 + 170) )
{
v30 = *(_DWORD *)(v29 + 76);
v31 = 0;
while ( *(_DWORD *)(v30 + 8 * v31) != Class_System_IDisposable )
{
if ( ++v31 >= (unsigned int)*(unsigned __int16 *)(*v20 + 170) )
goto LABEL_35;
}
v32 = v29 + 8 * *(_DWORD *)(v30 + 8 * v31 + 4) + 180;
}
else
{
LABEL_35:
v32 = sub_26A6A34(v20, Class_System_IDisposable, 0);
}
result = (*(int (__fastcall **)(_DWORD *, _DWORD))v32)(v20, *(_DWORD *)(v32 + 4));
}
return result;
}
}
}
}
}
break;
}
}
}
v26 = ((int (*)(void))loc_26E73D0)();
return CryptUtils__CalculateMD5FromFileOrEmpty(v26);
}
可以看到第67行用m_ivMask
去异或输入的文件即得到了真正的iv。直接HookPlayerData.get_chatMask()
打印返回值、HookRijndaelManagedTransform
的构造器打印key和iv:
node .\hook.js
[*] Script loaded
[*] Message: { type: 'send', payload: '0xc6f90000' }
[*] Message: { type: 'send', payload: 'Attach success, base address is 0xc6f90000' }
#onEnter:get_chatMask
8OjXUSNSi8yXC0u98mNWvh7MRLGhyEuQ
#onEnter:RijndaelManagedTransform$$ctor
Key:
00000000 38 4f 6a 58 55 53 4e 53 69 38 79 58 43 30 75 39 8OjXUSNSi8yXC0u9
IV:
00000000 7b 0d 0a 20 20 22 63 68 61 72 5f 32 38 35 5f 6d {.. "char_285_m
#onEnter:RijndaelManagedTransform$$ctor
Key:
00000000 38 4f 6a 58 55 53 4e 53 69 38 79 58 43 30 75 39 8OjXUSNSi8yXC0u9
IV:
00000000 7b 0d 0a 20 20 22 74 72 61 70 5f 30 30 34 5f 69 {.. "trap_004_i
#onEnter:RijndaelManagedTransform$$ctor
Key:
00000000 38 4f 6a 58 55 53 4e 53 69 38 79 58 43 30 75 39 8OjXUSNSi8yXC0u9
IV:
00000000 7b 0d 0a 20 20 22 73 6b 63 6f 6d 5f 63 68 61 72 {.. "skcom_char
#onEnter:RijndaelManagedTransform$$ctor
Key:
00000000 38 4f 6a 58 55 53 4e 53 69 38 79 58 43 30 75 39 8OjXUSNSi8yXC0u9
IV:
00000000 7b 0d 0a 20 20 22 73 6c 75 67 67 69 73 68 22 3a {.. "sluggish":
可以发现key就是chatMask
的前16字节(已更换),异或后的iv其实就是明文件的前16字节。
至此,key和iv的算法都拿到了,写个解密程序就万事大吉了。
那么chatMask
对不同玩家来说不同吗?不:
另外咕掉的模拟器(两年前的,插件版本对不上了,连预览都炸了...):
2 条评论
感谢分享,原来是这么得到的
感谢分享 赞一个