旅行青蛙破解分析从内存到存档再到改包

最近朋友圈里出现了一款日本的游戏,十分火爆,于是忍不住想去破解看看。分析后发现这个游戏的破解并不难,但是可以多种思路进行,是个很好的学习样本,于是决定写一篇文章分享给初学者们。

本文分三个方向进行破解分析,分别为内存修改,存档修改,apk修改。文章涉及的修改较为简单,主要目的是给大家提供多元的分析思路,接下来我们一个一个来进行具体分析。

所使用样本为 旅行青蛙 1.0.4版本(目前最新版本)。 链接: https://pan.baidu.com/s/1dSqHK6 密码: qmvg


目录

0x1.内存修改→ GG修改器修改数值,需root

0x2.存档修改→ 存档十六进制修改,无需root;原创apk用于修改存档,无需root

0x3.apk修改  → Unity3D游戏脚本修改,无需root

0x4.总结        → 文章整体思路和方向概况


正文


0x1.内存修改

思路:这个方式是用在已经root的手机上,也就是我们接触比较多的修改器通过搜索来确认关键数值的内存地址,然后将其修改,达到破解目的。

工具:GG修改器 / 需要ROOT权限


因为比较简单,这部分尽量简要讲。

打开GG修改器和游戏,进游戏后查看当前三叶草数量,GG修改器附加游戏进程,并搜索该数量。


    


附加后我们进行搜索,搜索37这个数值。


    


搜索结果比较多,我们需要筛选,回到游戏使用三叶草买东西,数值变化为27,然后我们搜索27来确认三叶草数量的内存地址。


    


修改最终搜索到的值为27000,回到游戏就可以看到三叶草数量已经变化。


    


其他物品及抽奖券等数量均可用该方式修改,请大家自己尝试。

这种方式非常方便,但是有个弊端就是需要我们有ROOT权限,对于目前大部分安卓手机来讲,ROOT权限的获取越来越难,接下来我们来分析不需要ROOT权限的两种修改方法。


0x2.存档修改

思路:通过存档文件分析和修改完成关键数值修改

工具:十六进制编辑器


单机游戏都会有存档,旅行青蛙当然也不例外,我们按照常规路径去找一下,发现游戏的存档都在Tabikaeru.sav文件中,路径请看图:




我们使用十六进制编辑器将其打开,编辑器可以用PC端的也可以用手机端的,自行选择。

打开后我们根据目前的三叶草数量27000进行搜索,27000的十六进制为0x6978,所以我们在十六进制文件中可以进行hex搜索,搜索 69 78 或 78 69。

(通常在十六进制中的数值都是倒序记录,比如0x6978会保存为 78 69,在旅行青蛙1.0.1版本的存档中就是这么保存的,不过在1.0.4版本的存档中,已经变为了正序,即69 78)




经过搜索我们找到了三叶草的数量,接下来我们将其修改验证一下,将69 78 修改为 FF FF,保存后放回手机中存档的文件夹中,重新启动,发现三叶草数量已经变更:




其他数值修改,比如抽奖券或者其他物品数量等,均可依照此方法进行,此处不再赘述,请大家自己尝试。另外还可以在每次数值有较明显变化后保存存档文件,进行对比分析,来找到更多物品的数值。

为了更简便的进行修改,我们做一个专用修改器apk用来在未root手机上专门完成此修改过程,源码如下(完整project参考附件):


package com.example.frog;

import android.content.Context;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class MainActivity extends AppCompatActivity {
    private EditText editText;
    private EditText editText2;
    private Button button;
    private InputMethodManager inputMethodManager;
    private static final String FILE_PATH = Environment.getExternalStorageDirectory() + File.separator + "Android/data/jp.co.hit_point.tabikaeru/files/Tabikaeru.sav";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        editText = (EditText) findViewById(R.id.editText);
        editText2 = (EditText) findViewById(R.id.editText2);
        button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (editText.getText().toString().equals("") || editText2.getText().toString().equals("")) {
                    return;
                }
                String cloverHex = String.format("%06X",  Integer.valueOf(editText.getText().toString()));
                String couponHex = String.format("%06X",  Integer.valueOf(editText2.getText().toString()));
                Log.d("123", " " + cloverHex);
                Log.d("123", " " + couponHex);
                writeToFile(cloverHex, couponHex);
            }
        });
    }

    public void writeToFile(String cloverHex, String couponHex) {
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        File file = new File(FILE_PATH);
        File newFile = new File(FILE_PATH);
        byte[] cloverByteArray = hexStringToByte(cloverHex);
        byte[] couponByteArray = hexStringToByte(couponHex);
        if (!file.exists()) {
            Log.d("123", "未找到文件Tabikaeru.sav");
            return;
        }
        try {
            fileInputStream = new FileInputStream(file);
            byte[] arrayOfByte = new byte[fileInputStream.available()];
            Log.d("123", "文件大小" + arrayOfByte.length);
            fileInputStream.read(arrayOfByte);
            if (arrayOfByte.length > 29) {
                file.delete();
                Log.d("123", "删除旧文件");
                createFile(newFile);
                //三叶草
                arrayOfByte[23] = cloverByteArray[0];//Byte.valueOf(cloverHex.substring(0, 2));
                arrayOfByte[24] = cloverByteArray[1];//Byte.valueOf(cloverHex.substring(2, 4));
                arrayOfByte[25] = cloverByteArray[2];//Byte.valueOf(cloverHex.substring(4, 6));

                //抽奖券
                arrayOfByte[27] = couponByteArray[0];//Byte.valueOf(couponHex.substring(0, 2));
                arrayOfByte[28] = couponByteArray[1];//Byte.valueOf(couponHex.substring(2, 4));
                arrayOfByte[29] = couponByteArray[2];//Byte.valueOf(couponHex.substring(4, 6));
                Log.d("123", " " + arrayOfByte.length);
                for (int i = 0; i <arrayOfByte.length; i++) {
                    Log.d("123", " " + arrayOfByte[i]);
                }
                fileOutputStream = new FileOutputStream(newFile);
                fileOutputStream.write(arrayOfByte);
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            Toast.makeText(this, getString(R.string.saved), Toast.LENGTH_SHORT).show();
            hideSoftInput();
            try {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void createFile(File file){
        try{
            file.getParentFile().mkdirs();
            file.createNewFile();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    public void hideSoftInput(){
        if(inputMethodManager == null) {
            inputMethodManager = (InputMethodManager)this.getSystemService(Context.INPUT_METHOD_SERVICE);
        }
        inputMethodManager.hideSoftInputFromWindow(editText.getWindowToken(), 0);
        editText.clearFocus();

        inputMethodManager.hideSoftInputFromWindow(editText2.getWindowToken(), 0);
        editText2.clearFocus();
    }

    /**
     * 把16进制字符串转换成字节数组
     */
    public static byte[] hexStringToByte(String hex) {
        int len = (hex.length() / 2);
        byte[] result = new byte[len];
        char[] achar = hex.toCharArray();
        for (int i = 0; i < len; i++) {
            int pos = i * 2;
            result[i] = (byte) (toByte(achar[pos]) << 4 | toByte(achar[pos + 1]));
            if (result[i] == 0) {
                result[i] = 00;
            }
        }
        return result;
    }

    private static int toByte(char c) {
        byte b = (byte) "0123456789ABCDEF".indexOf(c);
        return b;
    }
}

上述代码实现了存档的直接修改,界面如下,不需要ROOT权限:




输入数值后,点击修改即可完成三叶草及抽奖券的修改,更多物品修改请自行尝试。


0x3.apk修改

思路:分析apk包,找到脚本文件,反编译后找到关键method进行修改,然后重新打包

工具:Android Killer,DnSpy


Android Killer相关操作这里不再赘述,反编译后我们发现这是一个mono框架的Unity3D游戏,Unity3D游戏的脚本文件都存放在Assembly-CSharp.dll或Assembly-CSharp-firstpass.dll文件中,很显然,旅行青蛙的脚本文件位于Assembly-CSharp.dll,我们使用Dnspy进行分析看看。




我们搜索三叶草的英文clover,发现getCloverPoint可能是我们需要找的关键method。




根据getCloverPoint的源码,我们发现这个method的功能是在三叶草数量发生变化时在三叶草数量进行增减运算,那么我们可以对函数内部增加数量的这句代码进行修改,修改为发生变化增加固定数量的三叶草,比如10000。

(抽奖券相关修改也在SuperGameMaster中可以找到,method名为getTicket,此处不作演示,请大家自己尝试修改) 




修改后函数变更为:




保存后打包apk运行,只要三叶草数量发生变化(如收割三叶草或者购买物品),三叶草的数量就会增加10000。


0x4.总结

本文通过多种思路对旅行青蛙的修改进行了分析,内容较为简单,主要目的是分享游戏破解分析的思路,有兴趣的可以尝试更多物品数量的修改。


附件下载