拆弹专家,原谷歌游戏
downloadUrl:https://ww.lanzous.com/id06yjc(有广告版本)
本文只作为思路分享文章,无逆向成品,仅供学习交流
拿到apk首先还是解压一下看一下文件目录,看看游戏引擎
这里就先借用一下Perfare大神的工具:Il2CppDumper
该工具用来分析so里面游戏逻辑的方法枚举,虽然没有函数体,但是通过函数名很大程度上还是可以帮助我们分析游戏的逻辑,工具很香,可以配合IDA使用,了解详情的可以自行查看GitHub文档
(7.2版本IDA直接去运行ida.py并加载script.json即可实现方法名的导入)
其次就是对反编译libil2cpp的dll文件可以使用Reflector或者dnSpy来查看(其实和查看dump.cs没有太大的区别,主要的差别在于使用Reflector可以直接去查看空命名空间的代码,过掉一些系统级别代码)
举例一下Reflector结合Frida的使用
这里我们可以知道偏移地址(实际地址=基地址+偏移地址+ thumb指令?1:0))
- showDilog() → 0x547484
- showMessage() → 0x547F30
通过这种方式我们就可以轻松的批量断点我们希望断点的方法了
以下为一个简单的批量断点脚本示例:
function start(){
//com.izyplay.defusethebomb.bazhang
var arrayAddr = [0x54728C,0x547310,0x54745C,0x547DF8,0x547484,0x548218,0x547F30,0x55DF40
,0x679798,0x6798B4,0x687428,0x687350];
var arrayName = ["AndroidDialog Create","AndroidDialog Create1","AndroidDialog init"
,"AndroidMessage Create","showDialog","CallStatic","showMessage","SetPressedState"
,"NativeDialog","NativeMessage","ToggleButton","OnClick"];
var soAddr = Module.findBaseAddress("libil2cpp.so");
console.error('\nsoAddr:' + soAddr + "\n");
for (var index = 0; index < arrayAddr.length; index++) {
console.log("-------------------------");
var currentAddr = soAddr.add(arrayAddr[index]);
console.log('currentAddr:' + currentAddr);
funcTmp(currentAddr,soAddr,index,arrayName);
console.log("\t\t---->"+index,arrayAddr[index]+" is prepared ");
}
console.log("\n")
}
function funcTmp(currentAddr,soAddr,index,arrayName){
Interceptor.attach(currentAddr, {
onEnter: function(args){
console.log("called : "+arrayName[index]+" ----- addr : " + currentAddr.sub(soAddr) +"\n");
},
onLeave: function(retval){
}
});
}
已上是对so的一个简单处理分析
我们知道Unity游戏与Java的通信是通过UnitySendMessage()之类的函数来实现的
不同的代码可能写法不一样,但是这里注意几个关键词就是了
“Unity”,“Send”,“Message”,“Reward”,“Video”(拿到国内的谷歌游戏都是添加了广告的,自然是有一个video来展示广告,获取奖励Reward等等)自己排列组合,总能发现点东西
随便点进去一个跟进代码不难发现其实最终就是去调用了Native方法
这里就不继续跟进了,回到初衷是要搞这个游戏的奖励
这里游戏原来的处理逻辑是点击观看广告视频,然后就可以成功获取奖励,上面说了,游戏与unity的通信是通过UnitySendMessage来实现的,这里我们只用再找到在哪里打开视频,打开视频看完了必然也会有一个成功回调,修改smali处理一下这个逻辑就搞定,于是我们又重新搜索关键字Reward,Video ... 发现如下
这里可以看到每种广告播放状态都向Unity发了一条消息(UnitySendMessage 是一个 public static方法),简单分析一下逻辑,成功后向Unity发的是什么消息,剩下的就是对这个smali为所欲为了
public static void UnitySendMessage(String str, String str2, String str3) {
if (!m.c()) {
f.Log(5, "Native libraries not loaded - dropping message for " + str + "." + str2);
return;
}
try {
nativeUnitySendMessage(str, str2, str3.getBytes("UTF-8"));
} catch (UnsupportedEncodingException unused) {
}
}
const-string v0, "onRewardedVideoAdRewarded"
const-string v1, "123"
invoke-direct {p0, v0, v1}, Lcom/ironsource/unity/androidbridge/AndroidBridge;->sendUnityEvent(Ljava/lang/String;Ljava/lang/String;)V
至于以上我们是怎么找到这些关键点的话,我们还是使用我们的Frida大法,使用基于Frida的Objection来完成Class批量断点,非常好用,当然你也可以选择手写Frida批量下断脚本
想进行方法调用的测试,我们可以使用Frida的远程方法调用,静态变量值的获取等等
Java.choose("com.ironsource.unity.androidbridge.AndroidBridge",{
onMatch: function(obj){
var ss = {"reward_amount":1,"placement_name":"DefaultRewardedVideo","reward_name":"Virtual Item"};
obj.sendUnityEvent("onRewardedVideoAdRewarded","onRewardedVideoAdRewarded");
},
onComplete: function(){
}
});
Java.choose("com.ironsource.mediationsdk.model.RewardedVideoConfigurations",{
onMatch: function(obj){
console.log("主动调用onRewardedVideoAdRewarded")
var mRVPlacements = obj.mRVPlacements.value;
console.log(mRVPlacements.size());
console.log(mRVPlacements.get(0));
},
onComplete: function(){
}
});
最后来一个小tips:想不看广告只用对app重新签名就是,但是广告播放成功的回调自然也是失效了,所以还是需要稍微改改smali
以上就是这个Unity游戏的简单逆向过程
总结一下:
一般游戏逆向分为unity游戏,cocos游戏,或者一些自己写的游戏
unity主要可以看成两类,dll游戏和libil2cpp游戏,dll游戏比较简单,由于c#类似Js的语言特性,几乎就是可以明文随便篡改(dpy/),为了安全性的提升,所以才应运而生了libil2cpp用来转换dll to so ,但实际上逆向的时候使用ida分析libil2cpp的时候也差不多吧,有点汇编基础基本不难看懂,或者是结合frida去动态短点一些位置,或者是使用dwarf去动态调试一些位置,so我们只能修改不能新增指令,解决这个问题我们可以考虑用inlinehook完成新增。对于libil2cpp的情况,咋们可以用我分享的工具dps.py查找关键词函数,使用dpoint.js批量断点我们想查看的函数,动态断点方便我们快速找到想要hook的关键点后,使用inlinehook对其实现本地化为所欲为。对于一些自己写的游戏没有了dumper,但是我们可以考虑使用frida脚本枚举导出函数进行批批量hook,同时筛选函数名实现上述类似工作,至于cocos游戏也可以参照上述思路,咋们就简单分个类 cocos js 和 cocos lua,由于这篇文章主要介绍unity游戏,这里就不多说cocos,大概就这样哇 ~