(本文是針對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)建文本
- 第一行為手動指定get_text(),set_text()的地址
-
左邊列原來的文字,右邊是替換成的文字
準備文本 -
打包出來的so放在lib目錄下重打包apk
200925-183422.png - 記得重打包的時候加上一句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)試 -
原理
- 一切渲染在屏幕上的文字都會通過U3D引擎的UnityEngine.UI.Text命名空間下的get_text和set_text(不僅限于以上命名空間get/set)
- 通過Hook libil2cpp.so 的 il2cpp_class_get_methods 函數(shù)皆尔,用來找到UnityEngine.UI.Text命名空間下的set_text以及get_text的函數(shù)地址(當然你也可以使用Il2CppDumper去導出再找)
- 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修改提交共同完善這個小工具淆两,這里不適合貼太多代碼,詳細代碼移步