使用Riru修改手游

2019-06-25 20,758 ℃

前篇

手游的注入与修改

使用VirtualXposed修改手游

前言

时隔半年终于把这坑填上了,一开始只是打算随便写写,不过现在看来刚好可以凑出一个系列,之前的文章也重新编辑了一下,感兴趣的人可以翻回去看看。

这次要介绍的Riru模块对于一些人应该并不陌生,Riru+EdXposed是现在的安卓9以上Xposed环境的解决方案之一。Riru的原理是通过替换会被Zygote加载的libmemtrack.so从而实现Zygote注入,而安卓应用进程都是从Zygote fork的,注入了Zygote也就等同于注入了接下来会启动的游戏,也就可以轻松实现修改了。

Riru是Magisk的模块,所以首先要安装Magisk,不过现在手机root应该都是选择Magisk了,然后去Riru的Github的Releases页面下载最新的riru-core包,在magisk里安装,安装好Riru后就可以动手写自己的修改模块了。

写自己的Riru模块

去Riru的Github上下载所有代码,根据官方README,先给自己的模块取个名字,比如perfare,然后修改riru-module-template/jni/main/Android.mk中的模块名字

LOCAL_MODULE := libriru_perfare

接着修改riru-module-template/build.gradle中的模块信息

def moduleName = "perfare"

这两个地方的模块名字一定要一样,其他模块信息自己看着修改就好,修改好后就开始写代码吧,打开riru-module-template/jni/main/main.cpp

在main.cpp中本身已经写好了一些函数,我们只需要关注两个函数
nativeForkAndSpecializePre 通过解析参数appDataDir从而得到当前启动的进程包名,判断是不是我们要修改的游戏
nativeForkAndSpecializePost 在这里创建新线程进行修改

首先写个函数判断包名

static int enable_hack;
static const char* game_name = "com.aniplex.fategrandorder";

int isGame(JNIEnv *env, jstring appDataDir) {
if (!appDataDir)
return 0;

const char *app_data_dir = env->GetStringUTFChars(appDataDir, NULL);

int user = 0;
static char package_name[256];
if (sscanf(app_data_dir, "/data/%*[^/]/%d/%s", &user, package_name) != 2) {
if (sscanf(app_data_dir, "/data/%*[^/]/%s", package_name) != 1) {
package_name[0] = '\0';
LOGW("can't parse %s", app_data_dir);
return 0;
}
}
env->ReleaseStringUTFChars(appDataDir, app_data_dir);
if (strcmp(package_name, game_name) == 0) {
LOGD("detect game: %s", package_name);
return 1;
}
else {
return 0;
}
}

然后在nativeForkAndSpecializePre里调用这个函数

void nativeForkAndSpecializePre(
JNIEnv *env, jclass clazz, jint *_uid, jint *gid, jintArray *gids, jint *runtime_flags,
jobjectArray *rlimits, jint *_mount_external, jstring *se_info, jstring *se_name,
jintArray *fdsToClose, jintArray *fdsToIgnore, jboolean *is_child_zygote,
jstring *instructionSet, jstring *appDataDir, jstring *packageName,
jobjectArray *packagesForUID, jstring *sandboxId) {
// packageName, packagesForUID, sandboxId exists from Android Q
enable_hack = isGame(env, *appDataDir);
}

之后就可以在nativeForkAndSpecializePost里根据enable_hack来修改了,这里选择启动一个新线程来修改

int nativeForkAndSpecializePost(JNIEnv *env, jclass clazz, jint res) {
if (res == 0) {
// in app process
if (enable_hack) {
int ret;
pthread_t ntid;
if ((ret = pthread_create(&ntid, NULL, hack_thread, NULL))) {
LOGE("can't create thread: %s\n", strerror(ret));
}
}
} else {
// in zygote process, res is child pid
// don't print log here, see https://github.com/RikkaApps/Riru/blob/77adfd6a4a6a81bfd20569c910bc4854f2f84f5e/riru-core/jni/main/jni_native_method.cpp#L55-L66
}
return 0;
}

至于hack_thread里要怎么修改游戏就看你自己了,这里随便示范一个简单的libil2cpp.so修改,通过不断读取/proc/self/maps确定libil2cpp.so的载入和获取基址,然后直接通过指针修改text段代码

unsigned long get_module_base(const char* module_name)
{
FILE *fp;
unsigned 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)) {
pch = strtok(line, "-");
addr = strtoul(pch, NULL, 16);
if (addr == 0x8000)
addr = 0;
break;
}
}
fclose(fp);
}
return addr;
}

void *hack_thread(void *arg)
{
LOGD("hack thread :%d", gettid());
unsigned long base_addr;
while (true)
{
base_addr = get_module_base("libil2cpp.so");
if (base_addr != 0) {
break;
}
}
LOGD("detect libil2cpp.so %lx", base_addr);
LOGD("hack game begin");

unsigned long hack_addr = base_addr + 偏移;

//设置属性可写
void* page_start = (void*)(hack_addr - hack_addr % PAGE_SIZE);
if (-1 == mprotect(page_start, PAGE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC)) {
LOGE("mprotect failed(%d)", errno);
return NULL;
}

unsigned char* tmp = (unsigned char*)(void*)hack_addr;
tmp[0] = 0x00;
tmp[1] = 0x00;
tmp[2] = 0x00;
tmp[3] = 0x00;

LOGD("hack game finish");
return NULL;
}

到这里代码就写完了,可以使用gradlew.bat assembleMagiskRelease命令直接编译或者用Android Studio,在release文件夹下就会生成zip包,在Magisk安装即可

后记

这个系列到这里就暂时结束了,虽然只介绍了两个方案,不过这两个方案对付现在市面上99%的游戏都是毫无问题的,所以其他偏门方案也就没有介绍的必要了。接下来可能会摸些详细介绍修改的文章,比如在本博客评论区突然火起来的NetHTProtect,对付这玩意实际上就可以考虑使用dll注入反射修改的方式,这样就可以直接无视它的加密以及其他一些检测,当然文章感觉大概率还是会咕咕咕。另外最近已经有些保护厂商做了内存中text段的检验,如何找到并绕过这些检测可能会成为接下来修改游戏的重点。

一个过手游反作弊中.text段校验的方法

原本打算下个月再水的,不过转念一想,反正也不可能做到每个月都水一篇,而且现在放开以后准备到处去玩了,所以还是赶紧写完了发出来。 偷梁换柱 思路...

阅读全文

在模拟器上使用Zygisk修改游戏

前言 没想到距离上次发一篇教程性质的文章已经过去整整一年了,刚好最近搞了标题里提到的东西觉得还是可以写一下的,于是就来水了这一篇。 之前我一直...

阅读全文

使用Zygisk和Il2Cpp Api来修改游戏吧

在两年前发布Riru版Il2CppDumper的时候,就打算发布这篇配套的文章,结果码了一半就被我丢到了一旁,后来每次想起来的时候总是以”既然代码都发布了大概也不...

阅读全文

25 条评论

  1. 大佬问下,riru那个模块模板里使用try catch啊,网上普遍去android.mk里去加,但是现在的模板里没有这个配置文件夹了

  2. 大佬写下绕过网易检测外挂的方法吧!gg一修改过一会就被服务端给封号了

  3. 大佬,Magisk模块有没有免重启调式模块的方法呀,每次调试都要重启浪费好多时间

  4. 膜拜大佬,光遇在昨日游戏更新了,官方加强so防GG,大佬还望能去看看。目前我也不知道要怎么过检

  5. 能否问一下写出来的模块不加载是怎么回事?
    按照教程弄出来的东西编译安装都没问题
    结果调试的时候log什么都不输出

    adb logcat -s “xxxx”
    ——— beginning of system
    ——— beginning of main

    看magisk本身自己的so又已经加载了

    1. 建议检查一下游戏内是否有自己的so,换几个地方打log,如果还有问题也只能你自己检查

  6. mark :mrgreen:
    个人感觉网易盾的自己写dll去反射玩法限制太低了。而且有so注入检测最新网易盾。NetHTProtect,我研究到网易盾的我直接还原方法体后修改放回去加载。干掉了替换检测。也可以写Va进行注入。xposed!都行。有兴趣可以扣扣聊

  7. 有点好奇实时检验内存的Text段不会降低执行效能吗?
    还是有什麽我不懂的黑科技 :lol:

欢迎留言

5 + 5 =