手游2048的破解实战

2019-06-19 约 294 字 预计阅读 2 分钟

声明:本文 【手游2048的破解实战】 由作者 yong夜 于 2019-06-19 09:30:00 首发 先知社区 曾经 浏览数 52 次

感谢 yong夜 的辛苦付出!

[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要看牌是否生成即可,发现第三个并不代表是否生成牌的选项,但是我们了解前两个参数已经够用了。

  • r1:牌生成的位置,取值范围0-15(0-0xF)
  • r2:代表2的指数幂,运算后的结果即牌的点数

外挂实现

实现方案

【1】通过Inline Hook技术将addBoxAtIndexWithLevel的第二个参数(点数)进行修改,即可生成任意数值的牌

【2】hook导入表函数arc4random,获取调用者的信息,如果是addBoxAtIndex就返回0,即可保证第二个参数始终为1,从而只能生成牌面为2的牌

【3】动态/静态patch掉设置牌面点数方法_ZN3Box15setCurrentLevelEi的参数值,直接生成相应点数

【4】异常hook+导入表hook结合起来进行,同第二种方法

方案【1】

这里我们针对方案1进行实现,利用ptrace注入+inline hook技术

实现流程

实现代码

我们先实现hook层,测试成功后接着实现ptrace注入层,这样方便测试

Inline Hook层

  • 构造用户自定义函数replace_addBoxAtIndexWithLevel,实现寄存器值的修改
  • 构造原指令函数

    • 执行原指令addBoxAtIndexWithLevel
    • 跳转回游戏正常指令流程
  • 构造桩函数

    • 保存寄存器值
    • 跳转到用户自定义函数replace_addBoxAtIndexWithLevel
    • 还原寄存器的值
    • 跳转到原指令函数

具体代码细节可以看我上传在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

关键词:[‘安全技术’, ‘二进制安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now