2018-12-20 20,880 ℃
前篇
前言
这篇后续文章原本是打算很快就写完的,但是不知怎么一转眼就已经12月了,眼看今年都要过了,还是赶紧把这篇文章水出来吧。
在上篇文章说道的修改的核心思路,就是注入so到游戏中,而这次用到的VirtualXposed,其实就是作为注入器一样的存在。VirtualXposed这款软件已经不是什么新奇的东西了,在手游上被用来做外挂,做mod也是非常常见了。VirtualXposed原本的功能是利用VirtualApp做到无需root使用Xposed,而我们要使用的其实只是其中VirtualApp的部分,因为VirtualApp已经商业化开源部分已不再更新,兼容性存在问题,所以就直接使用VirtualXposed了。
既然VirtualXposed能用于修改手游,那被检测也是很正常的一件事,虽然也有办法对抗各种保护,但是这篇文章就不会说到这些东西了,所以为了保证本文还有点用处,我测试了中日热门的和新出的几款手游。结果倒是有点出乎我的意料,日游的话基本上是没法在VirtualXposed上运行,倒是国内游戏基本都能跑。稍微看了下日游现在大部分都有上保护,有保护的就凉了,而国内的游戏上了保护的也还是能正常运行,虽然不懂具体是什么情况,不过至少证明了用VirtualXposed做修改还是适合现在国内这个版本的。
好了,接下来就进入正题吧。
编译VirtualXposed
编译之前要做的事就是先安装好编译环境啦,包括java sdk,android sdk以及ndk,也可以选择安装Android Studio。搞定了编译环境之后就可以去github上clone源码了,作者的wiki上已经说明了应该如何正确clone源码,按他给的命令操作就行了,clone源码到本地之后,我们还需要修改一下源码从而达到注入so的目的。
源码要修改三个文件,其中两个是
VirtualXposed\VirtualApp\app\src\main\java\io\virtualapp\VCommends.java
和
VirtualXposed\VirtualApp\lib\src\main\jni\Jni\VAJni.cpp
在这两个文件里可以看到两个被注释“君子坦荡荡,小人常戚戚”的函数,是作者做签名验证用的,直接注释掉就行了。
VirtualXposed\VirtualApp\lib\src\main\java\com\lody\virtual\client\NativeEngine.java
在这个文件里可以看到一行代码
System.loadLibrary("va++");
这里就会让每个从VirtualXposed里启动的app都load一个libva++.so,所以我们直接在后面多加一句System.loadLibrary自己的so名就能实现注入了。
修改完源码就可以编译了,可以用Android Studio,也可以直接使用
gradlew.bat assembleRelease
命令进行编译,值得一提的是最新版(3311c0f)的代码编译x86的时候似乎有点问题,可以修改
VirtualXposed\VirtualApp\lib\build.gradle
这个文件的第21行删除x86配置来绕过,或者切换到7d051f0这个版本
当然编译中可能还会遇到各种各种的错误,这个时候就请善用百度啦。
编译so
这次要演示的是使用hook来修改unity3d dll的游戏,hook的话就是hook libmono下mono_image_open_from_data_with_name这个函数了。而从so被调用到进行hook,我们要做的事情就是判断是否是需要修改的游戏,然后获取到libmono在内存中的基址,这段代码在修改其他游戏的时候是可以通用的:
long get_module_base(const char* module_name, const char* package_name) { FILE *fp; long addr = 0; char *pch; char filename[32]; char line[1024]; snprintf(filename, sizeof(filename), "/proc/self/maps"); fp = fopen(filename, "r"); if (fp != NULL) { while (fgets(line, sizeof(line), fp)) { if (strstr(line, module_name) && strstr(line, package_name)) { pch = strtok(line, "-"); addr = strtoul(pch, NULL, 16); if (addr == 0x8000) addr = 0; break; } } fclose(fp); } return addr; } void *thread_hack(void *arg) { int count = 0; while (true) { long mono = get_module_base("libmono.so", "游戏包名"); if (mono != 0) { hack_game(mono); break; } count++; if (count > 5) { break; } sleep(1); } return NULL; } __attribute__((constructor)) void entry() { int ret; pthread_t ntid; if ((ret = pthread_create(&ntid, NULL, thread_hack, NULL))) { LOGE("can't create thread: %s\n", strerror(ret)); } } JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env; if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } return JNI_VERSION_1_6; }
这里我用的是非常简单的读取maps的方式判断游戏和获取需要修改的so的基址,每过一秒判断一次,超过5次就会自动退出,虽然这种方式不是什么很好的做法,不过能用就行了。
上面hack_game这个函数就是修改游戏的部分了,我们就在这个函数里hook mono_image_open_from_data_with_name。这次用的是简单的inlinehook,就不需要什么hook框架了
typedef void* (*GAME_PROXY)(char *data, int data_len, int need_copy, void *status, int refonly, char* name); GAME_PROXY old_game_proxy = 0; char g_HookCode_game_proxy[8] = { 0 }; char g_OrigCode_game_proxy[16] = { 0 }; void inlineHook_game_proxy(void* currentFunc, void* targetFunc) { //保存原函数头 char *tmp = (char*)currentFunc; for (int i = 0; i < 8; i++) { g_OrigCode_game_proxy[i] = tmp[i]; } //函数头设置属性可写 void* page_start = (void*)((long)tmp - (long)tmp % PAGE_SIZE); if (-1 == mprotect(page_start, PAGE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC)) { LOGE("mprotect failed(%d)", errno); return; } //生成跳转指令 memcpy(g_HookCode_game_proxy, "\x04\xf0\x1f\xe5\x00\x00\x00\x00", 8); *(unsigned long *)&(g_HookCode_game_proxy[4]) = (unsigned long)targetFunc; //替换函数头,把跳转指令写进去 for (int i = 0; i < 8; i++) { tmp[i] = g_HookCode_game_proxy[i]; } //跳回原函数指令 memcpy(&g_OrigCode_game_proxy[8], "\x04\xf0\x1f\xe5\x00\x00\x00\x00", 8); *(unsigned long *)&(g_OrigCode_game_proxy[12]) = (unsigned long)currentFunc + 8; //设置可执行权限 page_start = (void*)((long)g_OrigCode_game_proxy - (long)g_OrigCode_game_proxy % PAGE_SIZE); if (-1 == mprotect(page_start, PAGE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC)) { LOGE("mprotect failed(%d)", errno); return; } old_game_proxy = (GAME_PROXY)(g_OrigCode_game_proxy); } void* new_game_proxy(char *data, int data_len, int need_copy, void *status, int refonly, char *name) { if (strstr(name, "Assembly-CSharp.dll")) { FILE * pFile = fopen("/data/local/tmp/Assembly-CSharp.dll", "r"); fseek(pFile, 0, SEEK_END); int len= ftell(pFile); rewind(pFile); char * buffer = (char *)malloc(sizeof(char)*len); fread(buffer, 1, len, pFile); fclose(pFile); data = buffer; data_len = len; } return old_game_proxy(data, data_len, need_copy, status, refonly, name); } void hack_game(long addr) { long mono_image_open_from_data_with_name = addr + 偏移; inlineHook_game_proxy((void*)mono_image_open_from_data_with_name, (void*)new_game_proxy); }
这个的修改本质其实就是在调用mono_image_open_from_data_with_name之前修改传入的参数,从而载入我们修改好的dll,对于一些有加密的游戏,可以选择先调用mono_image_open_from_data_with_name,然后修改返回值MonoImage结构体的raw_data和raw_data_len。
写完so后就可以用ndk进行编译了,然后塞进上面编译好的VirtualXposed,签名后就可以安装使用啦。
结束啦
文章到这里就结束啦,虽然只演示了修改unity3d的dll这一种,但是其他il2cpp或者cocos2dx游戏修改so的,其实就是直接操作指针修改内存了,参考inlinehook部分就能很容易写出来了。
大佬,我按照你的方法添加了so,但是在so的
snprintf(filename, sizeof(filename), “/proc/self/maps”);
fp = fopen(filename, “r”);
这里的 /proc/self/maps 进入的是VirtualXposed的线程,而不是游戏,所以没有能找到要注入的so如il2cpp、从而导致无法修改
我该如何去修改呢
是否是因为我的so是从另一个项目里编译好直接复制到VirtualXposed的lib里呢?如果是这样的话我改如何在VirtualXposed中修改
望大佬发一下光遇国服的过检测
抱歉我回错帖子了
这个方法能对付il2cpp.so的程序吗
现在好多游戏都在检测VX(或者该说VA了),不是花式闪退(比如明日方舟)就是在某个点卡住无法进入游戏(cgss)。国内日厂都测试过不少可能还剩下20%左右的游戏能用
这个只能针对一些目前还放得开没检测的游戏了
我文中也说了va本来就被很多保护厂商盯上,你要做的就是研究各个保护厂商的检测方式,然后给va做对应的修改就行了,当然你手机如果已经root的话我还是更推荐使用riru
我可以解决,你懂c++嘛。3214455610我的联系方式
为什么向游戏注入so之后,游戏不会自动执行OnLoad函数,反而是框架执行了这个函数,这样搞得我没办法进行修改内存,好难受。。。
“long get_module_base(const char* module_name, const char* package_name)
{”
我可以把这个文件放在哪里?
VirtualXposed Android 9.0 可以 hook 成功吗我这里用的Substrate框架一直 hook 失败 换到Android 6.0 就ok
大佬,我还不会这个方法,可以加练习方式学习吗?
我的Q508325606
vxp跟你hook代码又没有关系,本身vxp能跑就是能跑,hook不成功要自己找原因
大佬,可否留个联系方式。3214455610我现在很多用到的东西都跟你发表的各项挂钩。有机会可以私下看看我的成品。
我就是想找个傻瓜化一点的软件用来改游戏金币,现在居然还需要学代码啊,好难啊。
虽然不懂,感觉还是好牛逼,我连上面的代码是写在哪个文件里面的都不清楚。
想问下,这里的 g_OrigCode_game_proxy用来恢复现场,为什么没有写入内存去执行,求大神指定
注入后g_OrigCode_game_proxy本来就是在内存中的,设置可执行权限后就能直接调用
我基础比较渣 ,但是很想看懂这个,应该是误解了代码的意思,大佬能解释下这三行代码吗:
1.这个是只想函数地址的指针的指针吗?
typedef void* (*GAME_PROXY)(char *data, int data_len, int need_copy, void *status, int refonly, char* name);
2.调用new_game_proxy函数时,没传入name参数,这个 strstr(name, “Assembly-CSharp.dll”) 怎么对比呢
3.这句怎么理解呢
return old_game_proxy(data, data_len, need_copy, status, refonly, name);
大佬能加下QQ吗,我想搞懂你写的这个代码,麻烦了,
你这不懂的问题太多了,还是自学吧。。。
我的QQ:3388722890,烦请大佬加下,谢谢谢谢谢
我想hook 王者荣耀,但libmono并没有找到mono_image_open_from_data_with_name,最神奇的是,logcat里面,王者荣耀居然有log出mono_image_open_from_data_with_name,网上说是王者自己hook自己的mono_image_open_from_data_with_name,但so的export函数里面没有mono_image_open_from_data_with_name,求大神指点。
大佬把我想学的全写出来啦 感谢
因为我对unity不熟 一直不知道要怎麽找dll的指针位置(原来是要先hook mono)
之前自己写了so来修改偶像大师millionlive
这个游戏在去年就用了appguard这个壳加密libil2cpp.so
现在很多游戏也开始用这个壳了 会改的人比较少(也几乎找不到教程)
但好笑的是它没有检测xposed(我用的甚至不是virtual版)
我的思路是写一个libdump.so注入游戏
然後dump解密过的libil2cpp.so
看大佬是写个thread等5秒 我就直接等到死 哈
从记忆体dump出的so档需要用工具修复 不然ida分析会有section错误
再配合大佬写的il2cppdumper汇出dump.cs档
ida里可以直接用keypatch这个plugin
直接打arm的assmebly就可以帮你转成hex
得到了要修改的hex自己再写个so注入游戏就可以直接patch libil2cpp.so
自动打歌就完成啦 (死)
所以严格来说用修改so还比修改dll简单一些
毕竟不需要懂unity的东西
其实如果能把appguard的加解密搞懂就不用那麽麻烦了
但是我太弱鸡又懒所以就算了
很棒,加油
能请教下如何patch的么?
已经dump出so, 找到要修改的地址, 修改内存中该地址的代码就会被appguard检测到.
没想到有人回应会寄mail通知阿…
如果你都有办法注入自己的so到程序里不被appguard抓到
照道理自己写个so去进patch内存应该没事阿
例如你想把0xE2C654这个内存位置改成 BX LR
我自写的so里会有个类似
hook_by_addr_hex(“libil2cpp.so”, 0xE2C654, “E12FFF1E”);
就是会去找libil2cpp.so在内存的位置 例如0x40000000
那hook_by_addr_hex就会把0x400E2C654 patch成 E12FFF1E
还是说appguard又进化了吗?其实我只改过millionlive音游目前都还行
感谢回复. 今天又研究了下, appguard好像还对so的.text节进行了CRC校验
这边说的patch是改内存 so档本身的内容并没有动到阿
所以不管是md5还是crc校验都没有变的
前面我举例的0x40E2C654是so档load进内存的映射位址
这基本上跟你用什麽烧饼还是gg修改器改内存是一个道理
请问能不能问一下关键点在哪里,现在东西是弄出来了却不知道该从哪里下手
可以留个联系方式嘛!3214455610
大佬可以留个联系方式吗。
天天来看首页是不是有更新文章,盼天盼地,今天终于更新文章了。。
然后被加点东西就能做成一个月X元的……
绝究级神兵启动!
笑死233
绝巴哈启动!
结果我被神兵吊锤,甚至被欧米伽打的找不着北
找朱雀老婆安慰一下就好啦
日常膜p大!感谢分享
蕉老婆( ´▽`)
???