作弊(主要是弱联网休闲游戏的作弊)的影响,比如改变排行榜(包括全局排名、地区排名和好友排名),不仅会影响用户留存,还会影响广告投放,带来经济损失。弱联网游戏可能存在的安全问题:存档被篡改和复制;本地配置被篡改;帐号被盗和恶意修改;内存被修改;网络协议被破解和重放攻击;工程被逆向破译。现在有很多技术高超的人,可以反汇编你的代码,然后把你的工程逆向;二进制程序可能被修改等。

1. 内存修改

主流游戏内存修改器有三大内存搜索功能:

  • 基础数值搜索
  • 模糊搜索
  • 反加密搜索

基础数值搜索是这么一个工作流程:首先搜一个特定的数值,比如金钱100就搜100,待金钱变化后,比如110就再搜110,如此重复3到4步操作,内存修改器就能定位到金钱所在的内存地址,接着就可以进行修改了。

模糊搜索是针对看不到具体数值的数值的修改(比如柱状血量、柱状气槽以及某些加密了的数值等)。模糊搜索是这么一个工作流程:首先启动模糊搜索,然后在游戏中等待要修改的数值发生变化,搜索“变大”、“变小”、“变化了”、“没变化”等操作,进行内存筛选,如此重复7到10步操作,就能定位到要修改的数值的内存地址,接着就可以进行修改了。

反加密搜索是更为强悍的内存搜索功能,和基础数值搜索是同样一个使用流程,但根据反加密强度,可以轻松破解多种常见的数值加密算法:

  • 倍值加密算法,y = n * x + m。其中x为加密前的数值,y为内存中的数值,n为任意正负整数。 y = (x << n) + m也属于这种算法,m为任意正负整数。
  • 异或加密算法,y = n ^ x + m。其中x为加密前的数值,y为内存中的数值,n为任意正负整数,m为任意正负整数。

前述三大内存搜索功能,都基于一个前提:要修改的数值,在内存中的地址是不变的或者在定位过程中是不变的。

所以,防御内存修改器的方法是:不断变化敏感数值的内存存储地址,敏感数值不留明文并随时更换密钥(比如将读写变量的方法封装成set和get两方法, 调用set方式时自动切换变量存储地址、变化加密密钥等)。

此外,游戏修改器在找不到直接修改游戏中的金钱、血量、经验值等敏感数据的时候,通常还采用“曲线救国”的方法:比如修改商店中物品的价格,改为负数,买商品,越买钱越多;比如修改每一局游戏中打到的怪物数量,结算的时候就可以获得更多的奖励;比如修改某一物品的数量,然后定位到该物品所在地址,修改该物品代码,把垃圾装备变成黄金装备等。这些数据,都是要格外注意保护的。

下面是一种简单的内存加密的实现方式(Java 示例):

/**
 * 加密Int
 */
public class EncryptInt {

    private int value;
    private byte[] values = new byte[4];

    /**
     * 根据加密的值返回原始值
     */
    public int getValue() {
        return (int) ((values[0] & 0xFF)
                | ((values[2] & 0xFF) << 8)
                | ((values[1] & 0xFF) << 16)
                | ((values[3] & 0xFF) << 24));
    }

    /**
     * 加密存储的值
     */
    public void setValue(int value) {
        this.value = value;
        values = new byte[4]; // 改变变量存储地址
        values[0] = (byte) (value & 0xFF);
        values[2] = (byte) ((value >> 8) & 0xFF);
        values[1] = (byte) ((value >> 16) & 0xFF);
        values[3] = (byte) ((value >> 24) & 0xFF);
    }

    /**
     * 校验值是否被篡改
     */
    public boolean isValid() {
        return value == getValue();
    }

}

2. 存档篡改

防止存档篡改需要考虑加密算法和存档协议设计:

加密算法:存档加密建议强加密,必须使用安全的对称加密算法,建议用 AES 256 CBC PKCS#7

首先,密钥不要直接保存在代码里面;密钥是生成的;第二,密钥不要放在存档里面,这是很危险的。第三,密钥每次加密当场现算;第四,密钥的生成需要有随机度。

存档协议设计:存档内容需要包含存档类型、存档版本号、源数据哈希、初始向量和加密数据。源数据哈希可以用于校验解密后的数据是否被篡改,初始向量可以参与加密算法中 IV 的生成,防止存档被转移。

AES 是当今世界上使用最广泛的对称密钥加密算法,用于加密和解密数据。AES 密钥生成是 AES 算法的关键步骤之一,密钥的长度可以是 128 位、192 位或 256 位,根据密钥的长度的不同,加密强度也不同。可以使用 UDID(唯一设备识别符)参与密钥和 IV 的生成,这样不仅可以防止存档被篡改,也可以防止存档被转移。

下面是一种存档读/写的实现方式(Java 示例):

/**
 * 存档管理
 */
public class ArchiveManager {

    private static final String DIR_ARCHIVE = "Archive";

    /**
     * 写入存档
     *
     * @param context  上下文
     * @param data     存档数据
     * @param fileName 文件名
     */
    public static boolean writeArchive(Context context, String data, String fileName) {
        String aesKey = getAesKey(context);
        String rawIV = generateRawIV(context);
        String aesIv = getAesIv(context, rawIV);
        String hash = md5(data);
        String encryptData = AES.encrypt(aesKey, aesIv, data);
        StringBuilder builder = new StringBuilder();
        builder.append("1");            // 存档类型
        builder.append("|");
        builder.append("10");           // 版本号
        builder.append("|");
        builder.append(hash);           // 源数据哈希
        builder.append("|");
        builder.append(rawIV);          // 初始向量
        builder.append(System.lineSeparator());
        builder.append(encryptData);    // 加密数据
        String content = builder.toString();
        try {
            File file = getFile(context, fileName);
            BufferedWriter writer = new BufferedWriter(new FileWriter(file));
            writer.write(content);
            writer.flush();
            writer.close();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 读取存档
     *
     * @param context  上下文
     * @param fileName 文件名
     */
    public static String readArchive(Context context, String fileName) {
        try {
            String line;
            String header = null;
            String data = null;
            File file = getFile(context, fileName);
            BufferedReader reader = new BufferedReader(new FileReader(file));
            while ((line = reader.readLine()) != null) {
                if (header == null) {
                    header = line;
                } else if (data == null) {
                    data = line;
                }
            }
            reader.close();
            if (header != null && data != null) {
                String[] split = header.split("\\|");
                String hash = split.length > 2 ? split[2] : null;  // 源数据哈希
                String rawIV = split.length > 3 ? split[3] : null;  // 初始向量
                String aesKey = getAesKey(context);
                String aesIv = getAesIv(context, rawIV);
                String decryptData = AES.decrypt(aesKey, aesIv, data);
                if (Objects.equals(hash, md5(decryptData))) {   // 校验存档内容哈希值
                    return decryptData;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static File getFile(Context context, String fileName) {
        File filesDir = context.getExternalFilesDir(DIR_ARCHIVE);
        if (!filesDir.exists() && !filesDir.mkdirs()) {
            filesDir = new File(context.getFilesDir(), DIR_ARCHIVE);
        }
        return new File(filesDir, fileName);
    }

    private static String getAesKey(Context context) {
        String key = Build.FINGERPRINT + '/' + context.getPackageName() + '/' + getAndroidId(context);
        return md5(key);
    }

    private static String getAesIv(Context context, String rawIv) {
        String iv = rawIv + '/' + getAndroidId(context);
        return md5(iv).substring(0, 16);
    }

    private static String generateRawIV(Context context) {
        return md5(context.getPackageName() + '/' + System.currentTimeMillis());
    }

    private static String getAndroidId(Context context) {
        try {
            return Settings.System.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

    private static String md5(String data) {
        return digest("MD5", data);
    }

    private static String sha256(String data) {
        return digest("SHA-256", data);
    }

    private static String digest(String algorithm, String data) {
        try {
            MessageDigest digest = MessageDigest.getInstance(algorithm);
            byte[] bytes = digest.digest(data.getBytes(StandardCharsets.UTF_8));
            return byte2Hex(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static String byte2Hex(byte[] bytes) {
        String hex;
        StringBuilder builder = new StringBuilder();
        for (byte b : bytes) {
            hex = Integer.toHexString(b & 0XFF);
            if (hex.length() == 1) {
                builder.append("0");
            }
            builder.append(hex);
        }
        return builder.toString();
    }

}

results matching ""

    No results matching ""