<progress id="yueoz"><code id="yueoz"><xmp id="yueoz">

      1. 吾愛破解 - LCG - LSG |安卓破解|病毒分析|www.13ee.cn

         找回密碼
         注冊[Register]

        QQ登錄

        只需一步,快速開始

        搜索
        查看: 2345|回復: 41
        上一主題 下一主題

        [游戲安全] 記一次Unity游戲逆向

          [復制鏈接]
        跳轉到指定樓層
        樓主
        二娃 發表于 2020-6-20 22:29 回帖獎勵
        本帖最后由 二娃 于 2020-6-22 12:00 編輯

        游戲是steam上一款單機音游,難度有點高,在被虐了千百次后,我決定對這個游戲下手。

        探秘

        大家都知道,unity游戲的主要邏輯都在Assembly-CSharp.dll,只要用dnspy之類的工具就能夠輕易的反編譯出源碼。于是我興沖沖的掏出了我的dnspy,將Assembly-CSharp.dll拖了進去,然而一片空白的dnspy告訴我事情沒這么簡單。

        3

        使用010 editor打開文件,發現并不是標準的PE格式,DOS頭的標志MZ被修改為了ML。

        1

        那就老規矩,開啟游戲,對mono.dll的mono_image_open_from_data_with_name下斷點觀察,結果發現游戲并沒有在這部分解密PE文件。抱著不想的預感,使用CE搜索了一下這個游戲的前幾個字節,結果不出意料。

        2

        可以看到游戲并沒有直接解密文件,外面長啥樣內存里還是啥樣?磥碛螒驊撌菍ono的代碼進行了修改,用自己的規則來加載文件。那沒有辦法,只能老老實實的跟著代碼走一遍。

        解密

        將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 <= 0 )
            return 1i64;
          v9 = 0;
          v10_index = 0i64;
          while ( 1 )                                   // 填充cli_section_tables
          {
            v11_current_cli_section_table = v10_index + *(_QWORD *)(v6_iinfo + 256);
            if ( (unsigned __int64)v4_offset + 40 > *(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,發現事情遠遠沒有這么簡單。

        4

        沒辦法,只能老老實實的接著往下看了。

        CLIHeader部分

        這一部分就是.net CLI文件特有的部分了,在開始著手這個游戲之前我對這部分基本沒有了解,只能現學現賣了。網上關于這部分的中文資料基本等于沒有,只好閱讀官方文檔ECMA 335了,在這里我簡單的介紹一下,CLI文件的概覽如下

        5

        可以看到除了有傳統的PE文件部分之外,還有CLI特有的部分,比如CLIHeader。那這個CLIHeader位于文件中的哪里呢?答案就在PEHeader的OptionalHeader->DataDirectory[14]中,文檔的說明如下

        6

        所以我們可以在這里獲取CLIHeader的RVA。再來看看CLIHeader的結構

        7

        其中比較重要的就是MetaData元數據了,比如程序中每個方法的IL都可以通過元數據找到,元數據的具體介紹大家可以自己百度或者閱讀文檔,我這個半吊子就不在這里獻丑了。通過CLIHeader中的MetaData我們可以找到MetadataRoot,也就是描述Metadata幾個table的地方,下面是MetadataRoot的結構

        8

        對CLI文件格式的介紹暫時到這里,有興趣的可以自行翻閱文檔,接下來回到代碼當中。在執行完pe_image_load_pe_data后,mono會執行pe_image_load_cli_data來解析CLIHeader部分。通過對比發現與源碼中不同的部分在load_metadata_ptrs中,mono的源碼對MetadataRoot中signature的判斷是這樣的

        9

        而游戲中的mono是這樣的

        10

        看了一下dll中的signature也是WSML(我是Mengluu?),與游戲中的mono對應的上。

        將文件中的WSML修改為BSJB后再次丟進dnspy,令人驚喜的發現可以看到東西了

        11

        正當我高興的開始準備翻閱的時候,現實又給了我當頭一棒。

        12

        函數反編譯失敗了。

        opcode部分

        將反編譯方式切換至IL可以發現應該是opcode被替換了

        13

        這就麻煩了,在百度+谷歌了一段時間過后得出的結論就是通過閱讀mono_method_to_ir,人肉識別出被修改的opcode與原opcode的對應關系?戳艘幌耺ono_method_to_ir的源代碼,我心態瞬間崩了。

        14

        嘗試著用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后即可正常反編譯。

        15

        至此,這個游戲的dll文件應該就被正常解密了

        結語

        通過這個unity游戲的逆向學習了一波CLI文件,并且親自分析了一遍opcode(以前都是云的),感覺收獲還蠻大的。然而解密dll后頓時索然無味,為啥不好好玩游戲呢,然后就沒有然后了。還有,關于替換opcode或者說修復opcode這一點我很好奇如果是大家會怎么做,希望各位不吝賜教。



        附上學習CLI文件時寫的010 editor template文件供大家參考(實際上只寫了一點點,并且還有已知的BUG)
        CLI.rar (12.83 KB, 下載次數: 13)

        折騰這游戲浪費了好幾天ff14的月卡 嗚嗚嗚

        免費評分

        參與人數 28吾愛幣 +33 熱心值 +28 收起 理由
        siuhoapdou + 1 + 1 用心討論,共獲提升!
        fengbolee + 1 + 1 用心討論,共獲提升!
        zycode + 1 + 1 謝謝@Thanks!
        iqixi + 1 用心討論,共獲提升!
        xiong_online + 1 + 1 用心討論,共獲提升!
        海盜小K + 3 + 1 感謝發布原創作品,吾愛破解論壇因你更精彩!
        虔來學習 + 1 用心討論,共獲提升!
        gunxsword + 1 + 1 666666
        huiyuanaidexuan + 1 + 1 用心討論,共獲提升!
        子晗。 + 1 + 1 歡迎分析討論交流,吾愛破解論壇有你更精彩!
        whitehack + 1 + 1 用心討論,共獲提升!
        asq56747277 + 1 + 1 感謝發布原創作品,吾愛破解論壇因你更精彩!
        LOLQAQ + 1 + 1 我很贊同!
        david.136 + 1 + 1 謝謝@Thanks!
        65302666 + 2 + 1 二娃牛脾
        XhyEax + 2 + 1 我很贊同!
        xzqsr + 1 + 1 感謝發布原創作品,吾愛破解論壇因你更精彩!
        弗雷迪 + 1 + 1 熱心回復!
        濤之雨 + 3 + 1 用心討論,共獲提升!
        Dboykey + 1 + 1 用心討論,共獲提升!
        liloooo + 1 + 1 我很贊同!
        鶴舞九月天 + 1 + 1 謝謝@Thanks!
        gh0st_ + 1 謝謝@Thanks!
        qaz003 + 1 + 1 二娃腦力和體力一級棒。。哈哈
        52xjh + 1 + 1 用心討論,共獲提升!
        alicc + 1 + 1 鼓勵轉貼優秀軟件安全工具和文檔!
        生有涯知無涯 + 1 + 1 感謝發布原創作品,吾愛破解論壇因你更精彩!
        CrazyNut + 3 + 1 用心討論,共獲提升!

        查看全部評分

        本帖被以下淘專輯推薦:

        發帖前要善用論壇搜索功能,那里可能會有你要找的答案或者已經有人發布過相同內容了,請勿重復發帖。

        推薦
        huiyuanaidexuan 發表于 2020-6-22 20:27
        當事人前來圍觀(ML),沒想到竟然有人愿意花時間在這上面,明明是個小眾游戲來著hhh
        其實那個出現多次的0x4D4C數字就是“ML”,的確是Mengluu的縮寫
        最后替換opcode有個方便的方法,git上有個開源庫dnlib可以讀取.net執行文件進行修改,編譯兩套opcode分別讀取寫出就行
        我也是之前被cytus2的加密方式惡心到了才幫別人做保護的時候寫了這個,可惜現在cytus2換了il2cpp,保護力度反而下降了

        免費評分

        參與人數 1吾愛幣 +2 熱心值 +1 收起 理由
        二娃 + 2 + 1 謝謝@Thanks!

        查看全部評分

        3#
         樓主| 二娃 發表于 2020-6-20 22:37 |樓主
        4#
        見風消 發表于 2020-6-20 22:44
        解完密后怎么修改的游戲,也發一些唄,學習學習
        5#
        赤座燈里 發表于 2020-6-20 22:53
        在其他論壇也看到過替換opcode的文章,也是一個個還原的,看來這方法真惡心
        6#
        yunkof 發表于 2020-6-20 23:20
        可惜我現在的程度看不懂。
        7#
        alicc 發表于 2020-6-20 23:38
        這是個什么游戲的
        8#
        52xjh 發表于 2020-6-20 23:44
        我愛惜爹雅鹿,樓主。&#128536;
        9#
         樓主| 二娃 發表于 2020-6-20 23:48 |樓主
        見風消 發表于 2020-6-20 22:44
        解完密后怎么修改的游戲,也發一些唄,學習學習

        已經索然無味了
        10#
        拉瑪西亞 發表于 2020-6-21 00:07
        某款國產游戲研究有著落了
        11#
        艾莉希雅 發表于 2020-6-21 00:09
        體力活,如果上了攪屎棍的話,這個體力活就能讓肥宅變成死宅
        您需要登錄后才可以回帖 登錄 | 注冊[Register]

        本版積分規則 警告:本版塊禁止灌水或回復與主題無關內容,違者重罰!

        快速回復 收藏帖子 返回列表 搜索

        RSS訂閱|小黑屋|聯系我們|吾愛破解 - LCG - LSG ( 京ICP備16042023號 | 京公網安備 11010502030087號 )

        GMT+8, 2020-6-28 21:03

        Powered by Discuz!

        Copyright © 2001-2020, Tencent Cloud.

        快速回復 返回頂部 返回列表
        快三开奖结果