(本文是针对U3D编译为libil2cpp的情况做分析)
-
现状
目前做游戏汉化无非就是在资源文件里面去找到相关的文字,将他汉化版重新写回去,受制于原位置长度,当然还要考虑很多因素。资源文件一般情况在level,assets文件目录中,并与global-metadata.dat存在了一定的关联关系,所以完成此项工作还得对这个几个文件的二进制结构有一定得理解才能完成 -
目的
这篇文章提出一种更加方便的汉化思路,基于内存字节对比替换实现汉化,对于汉化人员理论可以简单到只是提供一个简单的字符串映射关系的文本即可实现游戏汉化 -
实践环境:
cache目录下一个文本以下格式创建文本
- 第一行为手动指定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 ---> 动态调试 -
原理
- 一切渲染在屏幕上的文字都会通过U3D引擎的UnityEngine.UI.Text命名空间下的get_text和set_text(不仅限于以上命名空间get/set)
- 通过Hook libil2cpp.so 的 il2cpp_class_get_methods 函数,用来找到UnityEngine.UI.Text命名空间下的set_text以及get_text的函数地址(当然你也可以使用Il2CppDumper去导出再找)
- Hook上述找到的两个地址,判断修改函数参数以及返回值
-
实践
首先是为了找到get_text和set_text的地址,具体可以参见我的另一篇文章 Unity游戏逆向的小工具 后文中用到的脚本
具体为什么是这样写的可以参考源码,这里不细说:
Unity\Editor\Data\il2cpp\libil2cpp\vm\Class.cpp
SetupMethods ——> SetupMethodsLocked
里面去找他的结构体即可分析得出以下脚本
//运行以下脚本输出日志到文本
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代码
- 首先是对函数地址的计算
我们从函数的导出函数中只能找到il2cpp_class_get_methods,然而il2cpp_class_get_methods函数只有一条指令,是不能支持我们使用inlinehook的(inlinehook工作会替换前两条指令)
只有一条指令的il2cpp_class_get_methods
所以我们需要计算一下他这条跳转指令真实的跳转位置,用跳转过去的位置作为hook点
真实的il2cpp_class_get_methods函数
先明确他的跳转指令是如何构成的
ea == b eb == bl e1 == bx
地址的偏移 = (目的地址 - 当前地址 - 8 )÷ 4
验证一下:
(1B3AE0 - 193B64 - 8 )÷ 4 = 7FDD
下面用c来实现计算
左移八位去掉操作码,右移八位还原,+ 8 再乘以 4 计算偏移
拿到正常地址我们去hook再做点判断即可拿到上述两个函数地址
我们再使用frida去看看看这两个函数究竟是什么参数
函数参数或者返回值数据结构 - 前面十二位管他是什么保留就行
其实这里看得出来第9-12字节是存放的大小,只是因为实践中这个值对实际没有什么影响,也就懒得单独去操作了,这里就把前12位直接原封不动的搬下来用作返回值的拼接,至于最前面的8位欢迎大佬来补充解释一下是啥意思 - 然后的数据就是UnionCode小端存储
- 最后的八位补齐8个0(中文补齐四个0)
知道这些后我们就用c去申请空间,按照这样的规则去构造一块内存区域,替换原先的返回值或者参数指针即可实现内容的替换
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的时候第二个参数可能为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作为结束,拷贝以返回值偏移12个字节的作为开始的内存数据,其实就是中间文字部分
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);
//内存字节的比较
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是动态加上去的,按照目前的代码是处理不了这种情况的,可能需要在读取文本中单独加上一个标识,分类讨论嘛
大概就这意思,平时空余时间写的demo,欢迎大佬来fork修改提交共同完善这个小工具,这里不适合贴太多代码,详细代码移步
网友评论