[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

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖