U3D游戲的另類漢化思路

(本文是針對U3D編譯為libil2cpp的情況做分析)

  • 現(xiàn)狀

    目前做游戲漢化無非就是在資源文件里面去找到相關(guān)的文字沪哺,將他漢化版重新寫回去,受制于原位置長度酣倾,當然還要考慮很多因素鹦肿。資源文件一般情況在level,assets文件目錄中趴泌,并與global-metadata.dat存在了一定的關(guān)聯(lián)關(guān)系,所以完成此項工作還得對這個幾個文件的二進制結(jié)構(gòu)有一定得理解才能完成
  • 目的

    這篇文章提出一種更加方便的漢化思路拉庶,基于內(nèi)存字節(jié)對比替換實現(xiàn)漢化嗜憔,對于漢化人員理論可以簡單到只是提供一個簡單的字符串映射關(guān)系的文本即可實現(xiàn)游戲漢化
  • 實踐環(huán)境:

cache目錄下一個文本以下格式創(chuàng)建文本

  1. 第一行為手動指定get_text(),set_text()的地址
  2. 左邊列原來的文字,右邊是替換成的文字


    準備文本
  3. 打包出來的so放在lib目錄下重打包apk


    200925-183422.png
  4. 記得重打包的時候加上一句smali
    (代碼里雖然是對加載時機做了判斷氏仗,但實際建議是盡早加載inject吉捶,先于libil2cpp,避免部分漢化無效)
    const-string v1, "inject"
    invoke-static {v1}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
  • 工具

    用到的還是我們的還是我們熟悉的
    Inlinehook --- > 持久化hook
    frida ---> 動態(tài)調(diào)試
  • 原理

    1. 一切渲染在屏幕上的文字都會通過U3D引擎的UnityEngine.UI.Text命名空間下的get_text和set_text(不僅限于以上命名空間get/set)
    2. 通過Hook libil2cpp.so 的 il2cpp_class_get_methods 函數(shù)皆尔,用來找到UnityEngine.UI.Text命名空間下的set_text以及get_text的函數(shù)地址(當然你也可以使用Il2CppDumper去導出再找)
    3. Hook上述找到的兩個地址呐舔,判斷修改函數(shù)參數(shù)以及返回值
  • 實踐

首先是為了找到get_text和set_text的地址,具體可以參見我的另一篇文章 Unity游戲逆向的小工具 后文中用到的腳本
具體為什么是這樣寫的可以參考源碼慷蠕,這里不細說:
Unity\Editor\Data\il2cpp\libil2cpp\vm\Class.cpp
SetupMethods ——> SetupMethodsLocked
里面去找他的結(jié)構(gòu)體即可分析得出以下腳本

//運行以下腳本輸出日志到文本
frida -U -f <pkgName> -l hook.js --no-pause -o c:\temp.txt

function hook(){
    var p_size = Process.pointerSize
    var soAddr = Module.findBaseAddress("libil2cpp.so")
    Interceptor.attach(Module.findExportByName("libil2cpp.so","il2cpp_class_get_methods"),{
        onEnter:function(args){
        },
        onLeave:function(ret){
            console.error("--------------------------------------------------------")
            console.log(hexdump(ret,{length:16}))
            console.log("methodPointer => \t"+ret.readPointer() +"\t ===> \t"+ret.readPointer().sub(soAddr))
            console.log("invoker_method => \t"+ret.add(p_size*1).readPointer() +"\t ===> \t"+ret.add(p_size*1).readPointer().sub(soAddr))
            console.log("MethodName => \t\t"+ret.add(p_size*2).readPointer().readCString())
            var klass = ret.add(p_size*3).readPointer()
            console.log("namespaze => \t\t"+klass.add(p_size*3).readPointer().readCString()+"."
                +klass.add(p_size*2).readPointer().readCString())
        }
    })
}

導出到文本里就可以快速找到實際運行時候的地址以及ida里面分析的地址


當然我們inlinehook自然不可能使用js代碼珊拼,那就翻譯成c代碼
  • 首先是對函數(shù)地址的計算
    我們從函數(shù)的導出函數(shù)中只能找到il2cpp_class_get_methods,然而il2cpp_class_get_methods函數(shù)只有一條指令砌们,是不能支持我們使用inlinehook的(inlinehook工作會替換前兩條指令)
    只有一條指令的il2cpp_class_get_methods

    所以我們需要計算一下他這條跳轉(zhuǎn)指令真實的跳轉(zhuǎn)位置杆麸,用跳轉(zhuǎn)過去的位置作為hook點
    真實的il2cpp_class_get_methods函數(shù)

    先明確他的跳轉(zhuǎn)指令是如何構(gòu)成的
    ea == b eb == bl e1 == bx
    地址的偏移 = (目的地址 - 當前地址 - 8 )÷ 4
    驗證一下:
    (1B3AE0 - 193B64 - 8 )÷ 4 = 7FDD
    下面用c來實現(xiàn)計算

    左移八位去掉操作碼搁进,右移八位還原浪感,+ 8 再乘以 4 計算偏移
    拿到正常地址我們?nèi)ook再做點判斷即可拿到上述兩個函數(shù)地址

    我們再使用frida去看看看這兩個函數(shù)究竟是什么參數(shù)
    函數(shù)參數(shù)或者返回值數(shù)據(jù)結(jié)構(gòu)
  • 前面十二位管他是什么保留就行
    其實這里看得出來第9-12字節(jié)是存放的大小,只是因為實踐中這個值對實際沒有什么影響饼问,也就懶得單獨去操作了影兽,這里就把前12位直接原封不動的搬下來用作返回值的拼接,至于最前面的8位歡迎大佬來補充解釋一下是啥意思
  • 然后的數(shù)據(jù)就是UnionCode小端存儲
  • 最后的八位補齊8個0(中文補齊四個0)
知道這些后我們就用c去申請空間莱革,按照這樣的規(guī)則去構(gòu)造一塊內(nèi)存區(qū)域峻堰,替換原先的返回值或者參數(shù)指針即可實現(xiàn)內(nèi)容的替換
void *new_func_set(void *arg, void *arg1, void *arg2, void *arg3) {
    LOGD("Enter new_func_set %d",current_get_index);
    current_set_index++;
    //set的時候第二個參數(shù)可能為0讹开,就像get的時候返回值可能為0一樣
    if (arg1 == 0) return old_func_set(arg, arg1, arg2, arg3);
    try {
        memset(header_set, 0, HeaderSize);
        memcpy(header_set, arg1, HeaderSize);
        memset(end, 0, EndSize);
        memset(middle_set, 0, SplitSize);
        //以八個0作為結(jié)束,拷貝以返回值偏移12個字節(jié)的作為開始的內(nèi)存數(shù)據(jù)捐名,其實就是中間文字部分
        memccpy(middle_set, (char *) arg1 + sizeof(char) * HeaderSize, reinterpret_cast<int>(end), SplitSize);
        void* p_le =memchr(middle_set,reinterpret_cast<int>(end),SplitSize);
        //原返回值中間文字部分的長度
        int src_length = (char*)p_le - (char*)middle_set;

        int current_lines = 0;
        //初始化解析文本以“|”作為分割左邊 右邊部分緩存指針
        char *left = static_cast<char *>(calloc(SplitSize, sizeof(char)));
        char *right = static_cast<char *>(calloc(SplitSize, sizeof(char)));

        //讀取文件后刪除了源文件的旦万,從這里的buffer拷貝一個備份來操作
        char *temp_buffer = (char *) malloc(sizeof(char) * file_size + sizeof(int));
        memcpy(temp_buffer, buffer, sizeof(char) * file_size + sizeof(int));
        char *p = strtok(temp_buffer, "\r\n");
        while (p != NULL) {
            memset(left, 0, SplitSize);
            memset(right, 0, SplitSize);
            char *s = strstr(p, "|");
            static_cast<char *>(memcpy(left, p, strlen(p) - strlen(s)));
            right = strcpy(right, s + sizeof(char));
            if (current_lines != 0) {
                char *convert_str = static_cast<char *>(malloc(strlen(left) * 2));
                memset(convert_str, 0, strlen(left) * 2);
                int length = UTF8_to_Unicode(convert_str, left);
                //length == (src_length - EndSize) &&
//                LOGD("src_length - EndSize  %d" , src_length - EndSize);
                //內(nèi)存字節(jié)的比較
                if (memcmp(middle_set, convert_str, src_length - EndSize) == 0) {
                    LOGE("---> called set_text replace %s to %s   times:%d",left,right,current_set_index);
                    LOGD("Original str hex at %p === >",&middle_set);
                    hexDump(reinterpret_cast<const char *>(middle_set), src_length);
                    void *p = malloc(strlen(right) * 2);
                    int le = UTF8_to_Unicode(static_cast<char *>(p), right);
                    LOGD("Replacement str hex at %p === >",&le);
                    hexDump(reinterpret_cast<const char *>(p), le);
                    //申請空間來重新組合返回值
                    void *temp = malloc(static_cast<size_t>(HeaderSize + le + EndSize));
                    memset(temp, 0, static_cast<size_t>(HeaderSize + le + EndSize));
                    memcpy(temp, header_set, HeaderSize);
                    memcpy((char *) temp + HeaderSize, p, static_cast<size_t>(le));
                    memcpy((char *) temp + HeaderSize + le, end, EndSize);
                    LOGD("Return str hex at %p === >",&temp);
                    hexDump(static_cast<const char *>(temp), static_cast<size_t>(HeaderSize + le + EndSize));
                    free(convert_str);
                    free(left);
                    free(right);
                    free(temp_buffer);
                    return old_func_set(arg, temp, arg2, arg3);
                }
            }
            p = strtok(NULL, "\r\n");
            current_lines++;
        }
        free(left);
        free(right);
        free(temp_buffer);
        return old_func_set(arg, arg1, arg2, arg3);
    }catch (...){
        LOGE("ERRR MENORY");
        return old_func_set(arg, arg1, arg2, arg3);
    }
}

其實這里還有問題沒考慮到(隨便舉例一個):

  • 游戲中很多文本是組合的
    比如 " Time:12:00 " ,time是一個常量后面的12:00是動態(tài)加上去的,按照目前的代碼是處理不了這種情況的镶蹋,可能需要在讀取文本中單獨加上一個標識成艘,分類討論嘛

大概就這意思,平時空余時間寫的demo贺归,歡迎大佬來fork修改提交共同完善這個小工具淆两,這里不適合貼太多代碼,詳細代碼移步

UnityGameLocalizationDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拂酣,一起剝皮案震驚了整個濱河市秋冰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌婶熬,老刑警劉巖剑勾,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異尸诽,居然都是意外死亡甥材,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門性含,熙熙樓的掌柜王于貴愁眉苦臉地迎上來洲赵,“玉大人,你說我怎么就攤上這事商蕴〉迹” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵绪商,是天一觀的道長苛谷。 經(jīng)常有香客問我,道長格郁,這世上最難降的妖魔是什么腹殿? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮例书,結(jié)果婚禮上锣尉,老公的妹妹穿的比我還像新娘。我一直安慰自己决采,他們只是感情好自沧,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著树瞭,像睡著了一般拇厢。 火紅的嫁衣襯著肌膚如雪爱谁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天孝偎,我揣著相機與錄音访敌,去河邊找鬼。 笑死衣盾,一個胖子當著我的面吹牛捐顷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播雨效,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼迅涮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了徽龟?” 一聲冷哼從身側(cè)響起叮姑,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎据悔,沒想到半個月后传透,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡极颓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年朱盐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片菠隆。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡兵琳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出骇径,到底是詐尸還是另有隱情躯肌,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布破衔,位于F島的核電站清女,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏晰筛。R本人自食惡果不足惜嫡丙,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望读第。 院中可真熱鬧曙博,春花似錦、人聲如沸卦方。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盼砍。三九已至尘吗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浇坐,已是汗流浹背睬捶。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留近刘,地道東北人擒贸。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像觉渴,于是被迫代替她去往敵國和親介劫。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359