[TOC]
本篇文章主旨是通过破解2048这款游戏来入门游戏破解,学习ptrace注入和inlinehook组合使用技术
手指向上滑动,所有带有数字的牌向上移动
遇见相同数字牌会数字相加融合成一张牌
直到融合出一张数字为2048的牌即可赢得胜利
【1】破坏计算逻辑,修改成不相同的牌面也可以相加
【2】修改加法运算,任意两个牌面相加即可得到2048的牌面
【3】直接生成一张2048的牌面
【4】修改通关逻辑,未合成2048牌面也可通关
打开2048.apk安装包,发现只有一个lib\armeabi\libcocos2dcpp.so文件,可以看出游戏由Cocos2d-x引擎进行开发,主要编程语言是C++,这里因为反编译后java曾代码只有一些UI显示,所以可以判断游戏逻辑都在Native层。
首先我们打开IDA导入libcocos2dcpp.so,使用shift+F12搜索类似win、game over的字样,随意点进去一个字段,并使用x进行交叉引用,看有哪些方法调用了这个字段,可以从类名中看出这些代码都没有隐藏符号,并且从下面得到的结果和字面上意思,可以合理猜测这个类应该是控制游戏逻辑的相关类,接着继续查看这个类的具体情况
我们在函数窗口继续看这个类相关方法,发现了有关游戏过关、结束等逻辑的方法名
继续对这个wonTheGame进行跟踪,发现Playground::checkPlaygroundForEvents类方法对其进行了调用,这里我们可以从名称中看出Playground是playgroundcontroller的基类
再次搜索基类Playground的相关方法,可以看见都是游戏逻辑相关的方法名,有分数、牌移动、游戏开始结束等相关内容,可以确定游戏逻辑由基类Playground以及其派生类控制,下面看一下
在熟悉游戏的时候,大致每次生成牌的数字都在2和4中并且在16方格中随机出现,可以在的逻辑控制类中找找看是否存在random这样的字段,Playground::addBoxRandom方法,从名称中判断应该是添加一个随机牌面相关的方法,进去看看
从下面函数名中可以猜测可能是在指定位置添加牌吧,下面我们具体通过动态调试验证一下
aapt.exe d bading 2048.apk |findstr package
即可输出包名com.estoty.game2048,然后使用IDA进行attach连接,在addBoxAtIndex下断点,然后在屏幕上向任意方向滑动后,都会执行到这里一次并产生一个新的牌
接着我们在addBoxAtIndex内部的Playground::addBoxAtIndexWithLevel(int,int,bool)方法上下断点,r0是this,r1-r3存储着三个参数。我们在上面猜测这个函数是在指定位置生成牌,那么这三个参数可能包含位置、点数、是否生成这三种情况,执行完这里后,屏幕上第四个方格中生成一张点数为2的牌,如果下标从0开始那么这个3代表的位置就对上了,但是第二个1和点数2对不上,为了验证它我们将r2的值修改成0xB,结果生成了一个点数为2048的牌,可以判断r2的值是2的指数幂。验证r3所代表的是否是我们猜测的内容也很简单,修改r3为0要看牌是否生成即可,发现第三个并不代表是否生成牌的选项,但是我们了解前两个参数已经够用了。
【1】通过Inline Hook技术将addBoxAtIndexWithLevel的第二个参数(点数)进行修改,即可生成任意数值的牌
【2】hook导入表函数arc4random,获取调用者的信息,如果是addBoxAtIndex就返回0,即可保证第二个参数始终为1,从而只能生成牌面为2的牌
【3】动态/静态patch掉设置牌面点数方法_ZN3Box15setCurrentLevelEi的参数值,直接生成相应点数
【4】异常hook+导入表hook结合起来进行,同第二种方法
这里我们针对方案1进行实现,利用ptrace注入+inline hook技术
我们先实现hook层,测试成功后接着实现ptrace注入层,这样方便测试
Inline Hook层
构造原指令函数
构造桩函数
具体代码细节可以看我上传在github上的源码https://github.com/xiongchaochao/InlineHook,欢迎start
我们hook这里0x7587ECE4 ,减去so模块基地址偏移0xa1ce4,对github上的源码进行小改即可,改动如下。就是如果在下一条指令下hook就会覆盖半条BL指令
这里对r2,r3可随意修改,如下r2=1,r3=0,那么HOOK完之后,执行了ADCS R2,R3,R2的值就变成了2(有进位),再经过下面的加一,就变成了3,所以最后每次生成的牌面都为8
/** * 用户自定义的回调函数,修改r0寄存器大于300 */ void EvilHookStubFunctionForIBored(pt_regs *regs) { LOGI("In Evil Hook Stub."); regs->uregs[2] = 0x1; regs->uregs[3] = 0x0; } /** * 1.Hook入口 */ void ModifyIBored() { LOGI("In IHook's ModifyIBored."); void* pModuleBaseAddr = GetModuleBaseAddr(-1, "libcocos2dcpp.so"); LOGI("libnative-lib.so base addr is 0x%X.", pModuleBaseAddr); if(pModuleBaseAddr == 0) { LOGI("get module base error."); return; } //模块基址加上HOOK点的偏移地址就是HOOK点在内存中的位置 uint32_t uiHookAddr = (uint32_t)pModuleBaseAddr + 0xa1ce4; LOGI("uiHookAddr is %X", uiHookAddr); LOGI("uiHookAddr instructions is %X", *(long *)(uiHookAddr)); LOGI("uiHookAddr instructions is %X", *(long *)(uiHookAddr+4)); //HOOK函数 InlineHook((void*)(uiHookAddr), EvilHookStubFunctionForIBored); }
ptrace注入层
详细注入功能代码,参考我上传到github上的项目:https://github.com/xiongchaochao/ptraceInject 欢迎start
这里只附上,入口代码,主要是最上面三个参数的修改:
int main(int argc, char *argv[]) { char InjectModuleName[MAX_PATH] = "/data/libIHook.so"; // 注入模块全路径 char RemoteCallFunc[MAX_PATH] = "ModifyIBored"; // 注入模块后调用模块函数名称 char InjectProcessName[MAX_PATH] = "com.estoty.game2048"; // 注入进程名称 // 当前设备环境判断 #if defined(__i386__) LOGD("Current Environment x86"); return -1; #elif defined(__arm__) LOGD("Current Environment ARM"); #else LOGD("other Environment"); return -1; #endif pid_t pid = FindPidByProcessName(InjectProcessName); if (pid == -1) { printf("Get Pid Failed"); return -1; } printf("begin inject process, RemoteProcess pid:%d, InjectModuleName:%s, RemoteCallFunc:%s\n", pid, InjectModuleName, RemoteCallFunc); int iRet = inject_remote_process(pid, InjectModuleName, RemoteCallFunc, NULL, 0); //int iRet = inject_remote_process_shellcode(pid, InjectModuleName, RemoteCallFunc, NULL, 0); if (iRet == 0) { printf("Inject Success\n"); } else { printf("Inject Failed\n"); } printf("end inject,%d\n", pid); return 0; }
【1】使用代码android.os.Build.CPU_ABI
或者使用shell命令访问/proc/cpuinfo来看手机CPU架构,可以在附录中的ABI管理中查看不同架构的CPU对应的指令集,abi为armeabi-v7a对应ARMv7
【2】Thumb-2指令集是4字节长度的Thumb指令集和2字节长度的Thunb指令集的混合使用
【3】在4字节长度的Thumb指令中,若该指令对PC寄存器的值进行了修改,那么这条指令所在的地址一定要能整除4,否则程序会崩溃,所以如果指令地址不能整除4时我们通过NOP(BF00:Thumb-2)填充第一条指令,让我们覆盖跳转指令的地方可以被4整除,这个时候需要保存的原指令就是10字节长度了
【4】执行完保存的原指令后,我们跳转回去的地址需要加1让其为奇数,防止切换回arm指令集,让编译器编译出错
【5】调用RestroeThumbHookTarget/RestroeArmHookTarget删除断点的时候,是不能调用InitThumbHookInfo函数来初始化hook点信息的,这样会用修改后的指令覆盖被保存的指令
【1】ARM指令集切换到Thumb是通过分支执行条内存地址为奇数的指令,当然这条指令存储的位置还是偶数的,只是将这个奇数减一就可以得到指令真实地址。之后如果通过PC寄存器每次加2来得到奇数的内存地址来执行指令,所以一直都是Thumb模式,如果出现分支跳转到一个偶数指令,就会切回ARM指令集
介绍 | 内容 |
---|---|
相关代码应用下载链接 | https://gslab.qq.com/portal.php?mod=attachment&id=2050 |
Android Arm Inline Hook | http://ele7enxxh.com/Android-Arm-Inline-Hook.html |
Android Inline Hook中的指令修复详解 | https://gtoad.github.io/2018/07/13/Android-Inline-Hook-Fix/ |
ABI 管理 | https://developer.android.com/ndk/guides/abis.html?hl=zh-cn |
InlineHook功能代码 | https://github.com/xiongchaochao/InlineHook |
ptrace注入功能代码 | https://github.com/xiongchaochao/ptraceInject |