我个人喜欢分为三个阶段进行破解
- 找关键函数并通过这些关键函数去确定金币所属地址
- 尝试修改并调用ui更新相关函数
- 找保存函数并尝试保存,重启游戏测试
第一部分
(这里有个小想法: GameGuardian 去查找到金币所在的地址,然后可以使用frida去追踪这个地址修改的函数调用堆栈,实现快速的定位函数调用,emmm,frida也可以内存搜索,好像有点画蛇添足了 ....)
找到游戏内购触发点,如果是google游戏,一般情况会调用一个 BuyProductID(String ID) 的方法,这里传入的c#string 可以用作区分激励的起点,或者是一些其他的函数这个很好区分,实在再不行走下下册,hook掉onpointerclick,根据gameobj以及父级判断点击的是不是我们的内购按钮(这种操作在某些情况是有奇效的,一般用不到)
找到我们需要修改的激励点
众所周知,单机游戏不管是道具还是金币或者是其他,本质就是内存中的一段数据,我们只要找到这段数据修改它就完了,这里还是比较推荐使用我写的frida脚本 Ufun.js
对游戏中函数进行批量断点,快速的定位到关键的一些金币操作,钻石操作的函数
但是对于一般的游戏不用细化到这种程度,往往都是有相关的函数可以给你直接调用的,类似于下图
对于上图中的UserProfileManager_TypeInfo必然是一个bss段的值,所以你也可以手动去操作指针去通过这个位置加偏移定位到待修改的位置,直接去修改它也是可的(取决于这个addGems有没有其他乱七八糟的操作,有的话就自己去算偏移并修改,没有当然最好,直接调用就是)
简单举例一个:
这里的 get_current 是一个静态方法,所以我们可以直接调用就行
(如果我们想要调用一个类中的方法,但是又不知道这个类的实例地址,我们可以考虑去hook .ctor,其中的第一个参数即为当前类的实例地址,这里举例这个是因为有静态方法获取当然就直接用就是了
下面这个是c中对指针的操作来读写内存地址
void *(*get_Instance)(void *, void *, void *, void *) = reinterpret_cast<void *(*)(void *, void *, void *, void *)>(base + 0x36E70C);
void *(*GemsChanged)(void *) = reinterpret_cast<void *(*)(void *)>(base + 0x338ECC);
void *(*MoneyChanged)(void *) = reinterpret_cast<void *(*)(void *)>(base + 0x338E08);
void *(*Save)(void *) = reinterpret_cast<void *(*)(void *)>(base + 0x36F594);
if (args[0] == nullptr ) args[0] = get_Instance(nullptr,nullptr,nullptr,nullptr);
char* MoneyManager = static_cast<char *>(args[0]);
int* gems = reinterpret_cast<int *>(MoneyManager + 0x30);
int* MoneyUnit = reinterpret_cast<int *>(MoneyManager + 0x2C);
char* temp = reinterpret_cast<char *>(*MoneyUnit);
double* money = reinterpret_cast<double *>(temp + 0x10);
第二部分
有UI操作的话直接在当前线程操作是会报错的,在inlinehook中直接体现为闪退,在frida脚本中体现为 0x8 的报错,对于这个问题我考虑的是直接Hook使用他的Update函数,往里面添加我们自己要做的事情
使用frida脚本体现为
/**
* U3D中的update会被循环一直调用,这里的目的是让函数跑在ui线程里面
* B("Update") 拿到函数 update 的 pointer 填入第一个参数
* @param {Pointer} UpDatePtr
* @param {Function} Callback
*/
function runOnMain(UpDatePtr,Callback){
if (Callback ==undefined || UpDatePtr == undefined) return
Interceptor.attach(checkPointer(UpDatePtr),{
onEnter:function(args){
if (Callback != undefined && Callback != null){
try{
Callback()
}catch(e){
LOG(e,LogColor.RED)
}
Callback = null
}
},
onLeave:function(ret){
}
})
}
使用inlinehook体现为
//用作u3d主线程轮询调用函数的 hook update 函数地址
long func_y_1 = base + 0x4b5df4;
if (func_y_1 != base)
DobbyHook((void *) func_y_1, (void *) new_func_y_1, (void **) &old_func_y_1) == RS_SUCCESS ?
LOGD("Success Hook func_y_1 at 0x%x", func_y_1) : LOGE("Fail Hook func_y_1 at 0x%x", func_y_1);
//涉及到主线程操作,这里可以hook掉一个OnUpdate/OnAnimatorIK .... 等等运行在主线程并循环调用的函数以便于我们通知修改
//通过对一个标志位静态变量的判断来实现类似于标志位,标志函数的调用
void *new_func_y_1(void *arg, void *arg1, void *arg2, void *arg3) {
payUtils->GetRewards();
return old_func_y_1(arg, arg1, arg2, arg3);
}
class PayUtils {
......
void GetRewards() {
if (!paySuccess) return;
void *(*TestBeginner)() = reinterpret_cast<void *(*)()>(base + 0x3D5E44);
void *(*TestAdvanced)() = reinterpret_cast<void *(*)()>(base + 0x3D5F90);
void *(*TestBoss)() = reinterpret_cast<void *(*)()>(base + 0x3D60DC);
void *(*TesUltimate)() = reinterpret_cast<void *(*)()>(base + 0x3D6238);
void *(*TestNoAds)() = reinterpret_cast<void *(*)()>(base + 0x3D55CC);
void *(*AddGems)(void*,int) = reinterpret_cast<void *(*)(void*,int)>(base + 0x3D5550);
void *(*OfflineHours)(void*,int) = reinterpret_cast<void *(*)(void*,int)>(base + 0x3D56A0);
void *(*MoneyManagerBonus)(void*,int) = reinterpret_cast<void *(*)(void*,int)>(base + 0x3D5A10);
if (payUpStr == Rewards01) {
AddGems(nullptr,30);
} else if (payUpStr == Rewards00) {
AddGems(nullptr,70);
} else if (payUpStr == Rewards01) {
TestNoAds();
}
paySuccess = false;
payUpStr = "";
}
};
......
};
第三部分
这一部分一般不会做太多特殊的处理,多数情况跟进他的添加金币,添加宝石,获取...等等函数都能找到一些踪迹
或者使用 B("Save") HookPlayerPrefs() 都可以做一定的辅助判断