12558网页游戏私服论坛

 找回密码
 立即注册
游戏开服表 申请开服
游戏名称 游戏描述 开服状态 游戏福利 运营商 游戏链接
攻城掠地-仿官 全新玩法,觉醒武将,觉醒技能 每周新区 经典复古版本,长久稳定 进入游戏
巅峰新版攻 攻城掠地公益服 攻城掠地SF 新兵种、新武将(兵种) 进入游戏
攻城掠地公 散人玩家的天堂 新开 进入游戏
改版攻城掠 上线即可国战PK 稳定新区 全新改版,功能强大 进入游戏
少年江山 高福利高爆率 刚开一秒 江湖水落潜蛟龙 进入游戏
太古封魔录 开服送10亿钻石 福利多多 不用充钱也可升级 进入游戏
神魔之道 签到送元宝 稳定开新区 送豪华签到奖励 进入游戏
神奇三国 统帅三军,招揽名将 免费玩新区 激情国战,征战四方 进入游戏
龙符 三日豪礼领到爽 天天开新区 助你征战无双 进入游戏
王者之师 免费领豪华奖励 免费玩新区 6元送6888元宝 进入游戏
查看: 373|回复: 0

记一次Unity游戏逆向

[复制链接]

52

主题

52

帖子

114

积分

实习版主

Rank: 7Rank: 7Rank: 7

积分
114
发表于 2021-7-18 18:16:37 | 显示全部楼层 |阅读模式
游戏是steam上一款单机音游,难度有点高,在被虐了千百次后,我决定对这个游戏下手。
探秘

各人都知道,unity游戏的重要逻辑都在Assembly-CSharp.dll,只要用dnspy之类的工具就能够轻易的反编译出源码。于是我兴冲冲的掏出了我的dnspy,将Assembly-CSharp.dll拖了进去,然而一片空白的dnspy告诉我事情没这么简单。

利用010 editor打开文件,发现并不是标准的PE格式,DOS头的标志MZ被修改为了ML。

那就老规矩,开启游戏,对mono.dll的mono_image_open_from_data_with_name下断点观察,效果发现游戏并没有在这部门解密PE文件。抱着不想的预感,利用CE搜索了一下这个游戏的前几个字节,效果不出意料。

可以看到游戏并没有直接解密文件,外貌长啥样内存里还是啥样。看来游戏应该是对mono的代码进行了修改,用本身的规则来加载文件。那没有办法,只能老老实实的跟着代码走一遍。
解密

将mono.dll拖进ida,一般加载dll都会走到mono_image_open_from_data_with_name,所以我们直接定位到这里,然后从github下了一份mono的源码作为对照。顺着流程走下去会走到do_mono_image_load函数,这个函数就是用来解析加载dll文件的。
PEHeader部门

首先看到pe_image_load_pe_data,这个函数是用来解析PE Header部门的,通过对比可以看出这个函数与源码不一样,是被修改过的,ida F5代码如下
_BOOL8 __fastcall pe_image_load_pe_data(__int64 image){  char *header; // rdi  __int64 v2_image; // rbx  signed int section_table_offset; // eax  _BOOL8 result; // rax  char data[128]; // [rsp+20h] [rbp-88h]  header = *(char **)(image + 0x50);  v2_image = image;  result = 0;  if ( *(_DWORD *)(image + 24) >= 0x80u )       // raw_data_len  {    memmove(data, *(const void **)(image + 16), 0x80ui64);// raw_data    if ( data[0] == 'M' && data[1] == 'L' )    {      section_table_offset = do_load_header(v2_image, header, *(_DWORD *)&data[0x3C] - 0x4D4C);// NtHeader offset - 0x4D4C      if ( section_table_offset >= 0 )      {        if ( (unsigned int)load_section_tables(v2_image, (__int64)header, section_table_offset) )          result = 1;      }    }  }  return result;首先可以看到被修改过的mono在识别DOS头标志的时候用的不是MZ而是ML,与修改过的dll文件一致,然后在读取0x3c位置也就是NtHeader偏移值的时候减去了0x4D4C。do_load_header重要是记录一下IMAGE_NT_HEADERS结构,与源码没什么太大的差异,唯一的区别就是在识别NtHeader标志的时候用的不是PE而是ML,与修改过的dll一致。
signed __int64 __fastcall do_load_header(__int64 image, char *header, int e_lfanew){  __int64 v3; // rdi  char *v4_header; // rbx  __int64 v5; // rsi  unsigned int section_table_offset; // edi  int v8; // er11  int v9; // eax  char Dst; // [rsp+20h] [rbp-D8h]  int v11; // [rsp+50h] [rbp-A8h]  int v12; // [rsp+58h] [rbp-A0h]  int v13; // [rsp+5Ch] [rbp-9Ch]  __int16 v14; // [rsp+60h] [rbp-98h]  __int16 v15; // [rsp+62h] [rbp-96h]  __int16 v16; // [rsp+64h] [rbp-94h]  __int16 v17; // [rsp+66h] [rbp-92h]  __int16 v18; // [rsp+68h] [rbp-90h]  __int16 v19; // [rsp+6Ah] [rbp-8Eh]  int v20; // [rsp+6Ch] [rbp-8Ch]  int v21; // [rsp+70h] [rbp-88h]  int v22; // [rsp+74h] [rbp-84h]  int v23; // [rsp+78h] [rbp-80h]  __int16 v24; // [rsp+7Ch] [rbp-7Ch]  __int16 v25; // [rsp+7Eh] [rbp-7Ah]  int v26; // [rsp+80h] [rbp-78h]  int v27; // [rsp+88h] [rbp-70h]  int v28; // [rsp+90h] [rbp-68h]  int v29; // [rsp+98h] [rbp-60h]  int v30; // [rsp+9Ch] [rbp-5Ch]  char Src; // [rsp+A0h] [rbp-58h]  v3 = e_lfanew;  v4_header = header;  v5 = image;  if ( e_lfanew + 248i64 > (unsigned __int64)*(unsigned int *)(image + 24) )    return 0xFFFFFFFFi64;  memmove(header, (const void *)(e_lfanew + *(_QWORD *)(image + 16)), 0xF8ui64);// raw_data  if ( *v4_header != 'M' || v4_header[1] != 'L' )// NtHeader signature    return 0xFFFFFFFFi64;  if ( *((_WORD *)v4_header + 12) == 0x10B )    // PE32  {    section_table_offset = v3 + 248;    if ( *((_WORD *)v4_header + 10) != 0xE0 )   // SizeOfOptionalHeader      return 0xFFFFFFFFi64;  }  else  {    if ( *((_WORD *)v4_header + 12) != 523 || *((_WORD *)v4_header + 10) != 240 )      return 0xFFFFFFFFi64;    memmove(&Dst, (const void *)(v3 + *(_QWORD *)(v5 + 16)), 0x108ui64);    section_table_offset = v3 + 264;    memmove(&Dst, v4_header, 0xF4ui64);    v8 = v11;    *((_DWORD *)v4_header + 24) = v23;    *((_DWORD *)v4_header + 25) = v26;    *((_DWORD *)v4_header + 26) = v27;    *((_DWORD *)v4_header + 27) = v28;    v9 = v12;    *((_DWORD *)v4_header + 13) = v8;    *((_DWORD *)v4_header + 14) = v9;    *((_DWORD *)v4_header + 15) = v13;    *((_WORD *)v4_header + 32) = v14;    *((_WORD *)v4_header + 33) = v15;    *((_WORD *)v4_header + 34) = v16;    *((_WORD *)v4_header + 35) = v17;    *((_WORD *)v4_header + 36) = v18;    *((_WORD *)v4_header + 37) = v19;    *((_DWORD *)v4_header + 19) = v20;    *((_DWORD *)v4_header + 20) = v21;    *((_DWORD *)v4_header + 21) = v22;    *((_DWORD *)v4_header + 22) = v23;    *((_WORD *)v4_header + 46) = v24;    *((_WORD *)v4_header + 47) = v25;    *((_DWORD *)v4_header + 28) = v29;    *((_DWORD *)v4_header + 29) = v30;    memmove(v4_header + 120, &Src, 0x80ui64);  }  return section_table_offset;}接下来看看load_section_tables
signed __int64 __fastcall load_section_tables(__int64 image, __int64 iinfo, unsigned int offset){  __int64 v3_image; // r13  unsigned int v4_offset; // er12  int v5_number_of_sections; // eax  __int64 v6_iinfo; // r14  __int64 v7; // r15  __int64 v8; // rbx  int v9; // esi  __int64 v10_index; // rdi  __int64 v11_current_cli_section_table; // rbp  v3_image = image;  v4_offset = offset;  v5_number_of_sections = *(unsigned __int16 *)(iinfo + 6) + 1;// section+1  v6_iinfo = iinfo;  v7 = v5_number_of_sections;  *(_DWORD *)(iinfo + 248) = v5_number_of_sections;// cli_section_count  *(_QWORD *)(iinfo + 256) = g_try_calloc(40i64 * v5_number_of_sections);// cli_section_tables  v8 = 0i64;  *(_QWORD *)(v6_iinfo + 264) = g_try_calloc(8 * v7);// cli_sections  if ( v7  *(unsigned int *)(v3_image + 24) )      break;    memmove(      (void *)(v10_index + *(_QWORD *)(v6_iinfo + 256)),      (const void *)(*(_QWORD *)(v3_image + 16) + v4_offset),      0x28ui64);    ++v8;    v4_offset += 40;    v10_index += 40i64;    *(_DWORD *)(v11_current_cli_section_table + 20) += -0x4D4Cu - v9;// PointerToRawData - (i+1)*0x4D4C    v9 += 0x4D4C;    if ( v8 >= v7 )      return 1i64;  }  return 0i64;}这个函数重要是用来解析SectionHeader部门的。首先可以看到在读取NumberOfSections时加上了1,然后再接着解析SectionHeader。解析SectionHeader的时候对其中的PointerToRawData也动了手脚,操纵是PointerToRawData-(i+1)*0x4D4C,其中i是SectionHeader的索引(从0开始)。
PEHeader部门有改动的解析就结束了,总结一下这个游戏对PEHeader的处置惩罚就是:
1、修改了DosHeader和NtHeader的标志,将MZ和PE修改成了ML。
2、修改了指向NtHeader的偏移值。
3、将NumberOfSections减去1。
4、修改了SectionHeader中的PointerToRawData。
按着修改方式逆处置惩罚一下就算把PEHeader修复完了,利用010 editor的模板也能正知识别了。于是我高高兴兴的将修复后的文件再次扔进dnspy,发现事情远远没有这么简单。

没办法,只能老老实实的接着往下看了。
CLIHeader部门

这一部门就是.net CLI文件特有的部门了,在开始着手这个游戏之前我对这部门基本没有相识,只能现学现卖了。网上关于这部门的中文资料基本等于没有,只好阅读官方文档ECMA 335了,在这里我简单的先容一下,CLI文件的概览如下

可以看到除了有传统的PE文件部门之外,尚有CLI特有的部门,比如CLIHeader。那这个CLIHeader位于文件中的哪里呢?答案就在PEHeader的OptionalHeader->DataDirectory[14]中,文档的说明如下

所以我们可以在这里获取CLIHeader的RVA。再来看看CLIHeader的结构

其中比较重要的就是MetaData元数据了,比如程序中每个方法的IL都可以通过元数据找到,元数据的具体先容各人可以本身百度或者阅读文档,我这个半吊子就不在这里献丑了。通过CLIHeader中的MetaData我们可以找到MetadataRoot,也就是描述Metadata几个table的地方,下面是MetadataRoot的结构

对CLI文件格式的先容临时到这里,有爱好的可以自行翻阅文档,接下来回到代码当中。在执行完pe_image_load_pe_data后,mono会执行pe_image_load_cli_data来解析CLIHeader部门。通过对比发现与源码中差别的部门在load_metadata_ptrs中,mono的源码对MetadataRoot中signature的判断是这样的

而游戏中的mono是这样的

看了一下dll中的signature也是WSML(我是Mengluu?),与游戏中的mono对应的上。
将文件中的WSML修改为BSJB后再次丢进dnspy,令人惊喜的发现可以看到东西了

正当我高兴的开始预备翻阅的时候,现实又给了我当头一棒。

函数反编译失败了。
opcode部门

将反编译方式切换至IL可以发现应该是opcode被替换了

这就麻烦了,在百度+谷歌了一段时间过后得出的结论就是通过阅读mono_method_to_ir,人肉识别出被修改的opcode与原opcode的对应关系。看了一下mono_method_to_ir的源代码,我心态刹时崩了。

尝试着用ida F5了一下该函数,decompile了半天才出来效果,F5出来的伪代码一眼看去接近两万行,任意改个变量名都要卡半天。没办法,只能把F5抠掉看汇编了。分析opcode没什么好讲的,纯粹就是体力活。在分析了大概几十个opcode之后才发现了规律(我太菜了),原来就是把opcode 0xB3-0xC1插到了0x00的前面,用源码中的opcode.def文件来表示的话大概就是这样
/* GENERATED FILE, DO NOT EDIT. Edit cil-opcodes.xml instead and run "make opcode.def" to regenerate. */OPDEF(CEE_NOP, "nop", Pop0, Push0, InlineNone, 0, 1, 0xFF, 0x00, NEXT)OPDEF(CEE_CONV_OVF_I1, "conv.ovf.i1", Pop1, PushI, InlineNone, 0, 1, 0xFF, 0xB3, NEXT)OPDEF(CEE_CONV_OVF_U1, "conv.ovf.u1", Pop1, PushI, InlineNone, 0, 1, 0xFF, 0xB4, NEXT)OPDEF(CEE_CONV_OVF_I2, "conv.ovf.i2", Pop1, PushI, InlineNone, 0, 1, 0xFF, 0xB5, NEXT)OPDEF(CEE_CONV_OVF_U2, "conv.ovf.u2", Pop1, PushI, InlineNone, 0, 1, 0xFF, 0xB6, NEXT)OPDEF(CEE_CONV_OVF_I4, "conv.ovf.i4", Pop1, PushI, InlineNone, 0, 1, 0xFF, 0xB7, NEXT)OPDEF(CEE_CONV_OVF_U4, "conv.ovf.u4", Pop1, PushI, InlineNone, 0, 1, 0xFF, 0xB8, NEXT)OPDEF(CEE_CONV_OVF_I8, "conv.ovf.i8", Pop1, PushI8, InlineNone, 0, 1, 0xFF, 0xB9, NEXT)OPDEF(CEE_CONV_OVF_U8, "conv.ovf.u8", Pop1, PushI8, InlineNone, 0, 1, 0xFF, 0xBA, NEXT)OPDEF(CEE_UNUSED50, "unused50", Pop0, Push0, InlineNone, 0, 1, 0xFF, 0xBB, NEXT)OPDEF(CEE_UNUSED18, "unused18", Pop0, Push0, InlineNone, 0, 1, 0xFF, 0xBC, NEXT)OPDEF(CEE_UNUSED19, "unused19", Pop0, Push0, InlineNone, 0, 1, 0xFF, 0xBD, NEXT)OPDEF(CEE_UNUSED20, "unused20", Pop0, Push0, InlineNone, 0, 1, 0xFF, 0xBE, NEXT)OPDEF(CEE_UNUSED21, "unused21", Pop0, Push0, InlineNone, 0, 1, 0xFF, 0xBF, NEXT)OPDEF(CEE_UNUSED22, "unused22", Pop0, Push0, InlineNone, 0, 1, 0xFF, 0xC0, NEXT)OPDEF(CEE_UNUSED23, "unused23", Pop0, Push0, InlineNone, 0, 1, 0xFF, 0xC1, NEXT)OPDEF(CEE_BREAK, "break", Pop0, Push0, InlineNone, 0, 1, 0xFF, 0x01, ERROR)OPDEF(CEE_LDARG_0, "ldarg.0", Pop0, Push1, InlineNone, 0, 1, 0xFF, 0x02, NEXT)OPDEF(CEE_LDARG_1, "ldarg.1", Pop0, Push1, InlineNone, 1, 1, 0xFF, 0x03, NEXT)OPDEF(CEE_LDARG_2, "ldarg.2", Pop0, Push1, InlineNone, 2, 1, 0xFF, 0x04, NEXT)OPDEF(CEE_LDARG_3, "ldarg.3", Pop0, Push1, InlineNone, 3, 1, 0xFF, 0x05, NEXT)OPDEF(CEE_LDLOC_0, "ldloc.0", Pop0, Push1, InlineNone, 0, 1, 0xFF, 0x06, NEXT)OPDEF(CEE_LDLOC_1, "ldloc.1", Pop0, Push1, InlineNone, 1, 1, 0xFF, 0x07, NEXT)OPDEF(CEE_LDLOC_2, "ldloc.2", Pop0, Push1, InlineNone, 2, 1, 0xFF, 0x08, NEXT)OPDEF(CEE_LDLOC_3, "ldloc.3", Pop0, Push1, InlineNone, 3, 1, 0xFF, 0x09, NEXT)OPDEF(CEE_STLOC_0, "stloc.0", Pop1, Push0, InlineNone, 0, 1, 0xFF, 0x0A, NEXT)OPDEF(CEE_STLOC_1, "stloc.1", Pop1, Push0, InlineNone, 1, 1, 0xFF, 0x0B, NEXT)OPDEF(CEE_STLOC_2, "stloc.2", Pop1, Push0, InlineNone, 2, 1, 0xFF, 0x0C, NEXT)OPDEF(CEE_STLOC_3, "stloc.3", Pop1, Push0, InlineNone, 3, 1, 0xFF, 0x0D, NEXT)...以下省略感谢作者没有完全打乱,否则不知道要看到猴年马月。
opcode修复

没想到这一步卡了我很久,在找了半天符合的轮子未果后(不得不说我的搜索能力实在是不太行),在坛友@艾莉希雅的帮助下,我找到了ilasm和ildasm这两个工具,可以在github的coreclr里找到这两个工具的源码。首先修改源码中的opcode.def为上面提到的样子之后编译ildasm,用修改过的ildasm反编译游戏的dll文件为IL,再用正常的ilasm编译刚才生成的IL,即可得到opcode精确的dll文件。将这个新的dll拖入dnspy后即可正常反编译。

至此,这个游戏的dll文件应该就被正常解密了
结语

通过这个unity游戏的逆向学习了一波CLI文件,并且亲自分析了一遍opcode(以前都是云的),感觉劳绩还蛮大的。然而解密dll后顿时索然无味,为啥不好好玩游戏呢,然后就没有然后了。尚有,关于替换opcode或者说修复opcode这一点我很好奇如果是各人会怎么做,希望各位不吝见教。


附上学习CLI文件时写的010 editor template文件供各人参考(现实上只写了一点点,并且尚有已知的BUG)
CLI.rar

折腾这游戏浪费了好几天ff14的月卡 呜呜呜
来源:http://www.12558.net
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
楼主热帖
回复

使用道具 举报

*滑块验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|12558网页游戏私服论坛 |网站地图

GMT+8, 2024-4-24 20:10 , Processed in 0.093750 second(s), 31 queries .

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表