记录一次某手游外g登录验证系统及部分功能实现的逆向分析

查看 78|回复 9
作者:jbczzz   
0x0 JAVA层的登录验证分析
首先这个g是破解版,使用的静态注入,游戏使用的引擎是UE4。所以先看看AndroidManifest.xml里应用程序的入口点,入口点果然被修改了,
[XML] 纯文本查看 复制代码
            
               
               
            

        
:此标签用于在AndroidManifest.xml文件中声明活动组件。一个活动代表一个具有用户界面的屏幕。
android:name="com.xxxx.xxx.MainActivity":此属性指定活动的完全限定类名。在这种情况下,活动名为"MainActivity",属于包"com.xxxx.xxx"。
android:exported="true":此属性确定活动是否可以被其他应用程序的组件启动。当设置为"true"时,表示活动对其他应用程序可访问。
:此标签用于指定活动可以响应的意图类型。
:这表示活动是应用程序的入口点。它响应主要操作,通常用于启动应用程序。
:这是一种意图的类别,用于筛选可以直接从主屏幕(启动器)启动的组件。这意味着此活动应出现在启动器中,作为应用程序的入口点。
正常来说UE4游戏的入口点名字应该是com.epicgames.ue4.SplashActivity,所以他是把入口点替换成了卡密验证程序的入口,接下来就根据他这个路径去看看他这个验证程序是怎么写的,从验证MainActivity的onCreate开始逐步分析,最后定位到了验证的关键位置
[Java] 纯文本查看 复制代码recharge.setOnClickListener(new View.OnClickListener() { // from class: com.xxxx.xxx.MainActivity.1
            @Override // android.view.View.OnClickListener
            public void onClick(View view) {
                if (editText.getText().toString().equals("")) {
                    Toast.makeText(context, "卡密不能为空", 0).show();
                    return;
                }
                MainActivity.m54("/storage/emulated/0/keymi", "" + editText.getText().toString());
                Toast.makeText(context, "正在登录等待几秒即可", 0).show();
                String kami = editText.getText().toString();
                String Mac = MainActivity.getAndroidId(context);
                MainActivity.kami(kami);
                MainActivity.jima(Mac);
                ShuanQUtil.ParamsUtil paramsUtil = ShuanQUtil.getParamsUtil();
                paramsUtil.putParam("card", editText.getText().toString());
                paramsUtil.putParam("machine_code", MainActivity.getAndroidId(context));
                Map param = paramsUtil.getParams();
                System.out.println("登录原生请求参数:" + paramsUtil.getParamsOriginal().toString());
                System.out.println("登录实际请求参数:" + paramsUtil.getParams().toString());
                OkhttpUtil.okHttpPost("http://xx.xx.xxx.xxx:xxxxxx/api/card_app/check", param, new CallBackUtil.CallBackString() { // from class: com.xxxx.xxx.MainActivity.1.1
                    @Override // com.xxxx.xxx.common.okhttp.CallBackUtil
                    public void onFailure(Call call, Exception e) {
                        Toast.makeText(context, "登录请求失败请检测网络是否异常", 0).show();
                    }
                    @Override // com.xxxx.xxx.common.okhttp.CallBackUtil
                    public void onResponse(String response) {
                        try {
                            JSONObject object = JSON.parseObject(response);
                            if (!object.isEmpty() && (object.getString("code").equals("1") || object.getString("code").equals("10000"))) {
                                String dataOriginal = object.getString(RemoteMessageConst.DATA);
                                String dataJson = ShuanQUtil.dataDecrypt(dataOriginal);
                                JSONObject data = JSON.parseObject(dataJson);
                                data.getJSONObject("cardInfo");
                                data.getJSONObject("surplusTime");
                                String expireTimeStr = data.getString("expireTimeStr");
                                Toast.makeText(context, "登录成功 到期时间:" + expireTimeStr, 0).show();
                                Intent intent = new Intent();
                                intent.setClassName(BuildConfig.APPLICATION_ID, "com.epicgames.ue4.SplashActivity");
                                context.startActivity(intent);
                                Intent mIntent = new Intent(context, FloatingModMenuService.class);
                                context.startService(mIntent);
                                MainActivity.alertDialogss.dismiss();
                                return;
                            }
                            Toast.makeText(context, "登录失败1" + object.getString("message"), 0).show();
                        } catch (Exception e) {
                            e.printStackTrace();
                            Toast.makeText(context, "登录失败2" + e.getMessage(), 0).show();
                        }
                    }
                });
            }
        });
这段代码通过匿名内部类的方式定义了点击事件监听器:
1.当用户点击名为 recharge 的视图时,将触发这个点击事件监听器,使用 OkhttpUtil 类的静态方法 okHttpPost() 发送一个 POST 请求,并传入参数。
2.在请求的回调方法中,首先检查响应的数据。如果数据不为空且包含特定的代码(code),则执行以下操作:
解析响应数据,并获取到期时间字符串。显示一个 "登录成功,到期时间:" 的提示消息,并启动com.epicgames.ue4.SplashActivity",并关闭一个对话框。
3.如果响应数据中的 code 不符合预期,则显示一个相应的错误提示消息。
java层的验证很容易过,把登录失败的处理都跳转到登录成功即可,但是跳过验证之后发现游戏确实有悬浮窗了但是里面功能都是无效的。
再回看一下上面这段代码,分析后发现MainActivity.kami(kami)和MainActivity.jima(Mac)都是调用的native注册的函数,通过当前Activity里System.loadLibrary("xxx");即可定位所用的libxxx.so。接下来就需要去native分析验证逻辑
0x1 Native层的登录验证分析
进到native发现好像跟kami和jima没啥关系,这两个只是单纯的jason转换成cstr和获取机器码而已,然后经过分析发现真正验证的地方是在刚才启动的这个服务Intent mIntent = new Intent(context, FloatingModMenuService.class);里注册的native函数intt进行验证和解密。


1.png (52.84 KB, 下载次数: 0)
下载附件
2024-5-11 10:57 上传

这个玩意就是外挂的初始化函数了,经过分析,Z9___v里是hook的dlsym,只允许去访问指定的符号。bulletTrackHook里是通过hook游戏里的calcshoot实现的子弹追踪
CNM(),CNM1(),CNM2(),_Z10__kamiv()就是验证相关的函数,里面都长得很像,大概逻辑是把需要发送的报文(有验证服务器状态的,验证卡密的等)通过各种乱七八糟的与异或,然后调RC4加密函数加密后发送给服务器,然后接收服务器响应回来的值,再通过加密反过来的步骤解密一次。 最后可以得到服务器返回的验证结果以及一些游戏外挂功能相关的初始化参数。比较坑的是他后两个函数是一定要验证通过之后才会跟着cardtoken一起返回的,所以单纯在native过掉验证还是没效果,不过只要买一次卡密就能通过hook RC4Decrpt获取到他所有的游戏外挂功能相关的初始化参数的具体数值,只要手动去初始化这些全局变量就有功能了。
RC4Decrpt的部分输出:


2.png (80.5 KB, 下载次数: 0)
下载附件
2024-5-11 10:57 上传

content里就是服务器返回的数值
0x2 小结
以上,实现了过卡密验证 ,以及分析出了他实现外g功能修改读取和hook的地方

入口, 应用程序

iooioox   

你要脱敏的话,截图里的ip还是漏了
oneline111   

感谢分享
yixianliu   

真的很感谢分享
jbczzz
OP
  

看的不太懂,跟不上了!!
Tom3030   


正己 发表于 2024-5-10 19:13
你要脱敏的话,截图里的ip还是漏了

好的,那我明天再重传一下吧
zyastc521   


jbczzz 发表于 2024-5-10 21:25
好的,那我明天再重传一下吧

图片好像都不见了
张道陵   

感谢分享
张道陵   

学习了,感谢分享!
yixianliu   

谢谢分享
您需要登录后才可以回帖 登录 | 立即注册

返回顶部