吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2853|回复: 24
收起左侧

[Android 原创] 记一次简单的游戏逆向与修改

[复制链接]
lyldalek 发表于 2024-8-22 23:01

最近沉迷于一个老游戏,FC版的《三国志-霸王的大陆》,有大佬做了个Android版的,还添加了很多东西,比如技能系统。

游戏文件:

https://tieba.baidu.com/f/good?cid=0&ie=utf-8&kw=霸王的大陆

我想刷一个组合出来,但是靠随机不太可能:

于是就想用修改存档的方式来改下技能。

直接覆盖

首先,刷新前保存一次存档,刷新后再保存一次存档,用 010 对比一下,发现有4处不同,猜想这4处就是储存技能的位置了。

先尝试直接替换第2处不同,看看能够修改第二个技能。记载替换后的存档,失败了,不仅第2个技能没替换,其他的技能也变化了。再重新加载一次,发现技能又不一样了,于是确定是技能数据修改有问题。

所以直接替换是不行的,应该是游戏对技能数据做了校验,发现被修改之后,就又重新随机了。

反编译游戏

该游戏作者在贴吧做了介绍,是使用 godot 做的,查一下官方文档:

https://docs.godotengine.org/en/stable/

看着非常像Unity,解压一下base.apk,看一下asset下的文件结构,发现有个 script 文件夹,里面有很多 .gdc 文件。

网上搜索一下 gdc decompiler:

https://github.com/bruvzg/gdsdecomp

按照项目的说明,直接使用命令反编译 base.apk,再用 code 打开:

非常的nice,gd 是 gdscript,还是能看懂的,与源码几乎没区别了。

查看 sav 存档

游戏的存档,使用 010 打开后,明显可以看出是一个 json 文件:

直接使用 code 打开,定位到之前对比过的有区别的位置:

diy_skills 是做了校验的。我们需要搞定它。

查找 diy_skills 逻辑

安装一下 gdscript 的插件,可以高亮代码。

一通搜索之后,找到如下逻辑:

            var encrypted = PoolByteArray(Global.hex_decode(skills))
            var sign_version = encrypted[encrypted.size() - 1]
            var signed_len = encrypted[encrypted.size() - 2]
            var signed = encrypted.subarray(encrypted.size() - signed_len - 2, encrypted.size() - 3)
            encrypted = encrypted.subarray(0, encrypted.size() - signed_len - 3)
            if not StaticManager.crypto_verify_short(encrypted, signed):
                return {}
            if sign_version == 1:
                decrypted = clAes.ECB_Decrypted(encrypted)
                priorVersion = true
            elif sign_version == 2:
                encrypted = Global.hex_decode(encrypted.get_string_from_ascii())
                decrypted = clAes.ECB_Decrypted(encrypted)
            else :
                return {}

可以看到是使用了 AES 的 ECB 加密。

数据段的最后一个字节是 sign_version。

数据段的倒数第二个字节是 signed_len。

数据段的真实数据长度是 size - 2 - signed_len。

数据段校验逻辑:

func crypto_verify_short(data:PoolByteArray, signature:PoolByteArray)->bool:
    crypto_init()
    var hashed = data.get_string_from_utf8().md5_buffer()
    return crypto.verify(HashingContext.HASH_MD5, hashed, signature, signer)

md5 校验,还加了私钥签名,非常的合理

编写解密脚本

一通搜索与实验,作者使用的 godot 是 3.x 的版本,4.0 api有些变化,找到了一个online playground(我最讨厌加密的地方就是每个平台算法都可能不一致,所以最好找一样的环境进行算法逆向):

https://gdscript-online.github.io/

解密脚本如下:

extends Node

func _ready():
    var data = "6163656664396236353766666537393731353866303334613136656562666339363734393235333431626362646562323732656234313732316562613862613138353535653737323162333661353635356133386333643035336532326431383365623231373765326237633961366430363434613765323639653261326637333834363638376239393533346133386630316437363638633362623932616266373037316261333434303664323937313366393765656135343663333334326163633233336436363836653839636563326336626338393662306563333630c314c379b14f65cf9830e28af007b4411382d678a0133bf521a37ed32e1db8e3a1e754217315abd656f8e5dbcb1a509c54483bbd39328c50bbf5b7d150f7f7834002"
    var d = decrypt_skill_data(data)
    print(d)

func decrypt_skill_data(skills:String) -> String :
    var encrypted = PoolByteArray(hex_decode(skills))
    var sign_version = encrypted[encrypted.size() - 1]
    var signed_len = encrypted[encrypted.size() - 2]
    var signed = encrypted.subarray(encrypted.size() - signed_len - 2, encrypted.size() - 3)
    encrypted = encrypted.subarray(0, encrypted.size() - signed_len - 3)
    encrypted = hex_decode(encrypted.get_string_from_ascii())
    var decrypted = ECB_Decrypted(encrypted)

    return decrypted.get_string_from_ascii()

func ECB_Decrypted(encrypted:PoolByteArray)->PoolByteArray:
    if encrypted.empty():
        return PoolByteArray([])
    var aes = AESContext.new()
    var key = "gd secret key!!!"

    aes.start(AESContext.MODE_ECB_DECRYPT, key.to_utf8())
    var decrypted:PoolByteArray = aes.update(encrypted)
    aes.finish()
    return decrypted

func hex_decode(data:String)->PoolByteArray:
    var ret = []
    var b:int = 0
    var odd = true
    for c in data.to_lower():
        if c in "abcdef":
            b += ord(c) - ord("a") + 10
        else :
            b += int(c)
        if odd:
            b *= 16
            odd = false
        else :
            ret.append(b)
            odd = true
            b = 0
    return ret

神奇的事情出现了,发现 diy_skils 里面解出来的全部是同一个字符串:

{"LV1":"","LV2":"","LV3":"","LV4":"","LV5":"","LV6":"","LV7":"","LV8":""}

有点不对劲,打印hex数据出来看看,果然有区别,这个在线编辑器真辣鸡:

[123, 34, 76, 86, 49, 34, 58, 34, 229, 190, 183, 229, 138, 169, 34, 44, 34, 76, 86, 50, 34, 58, 34, 34, 44, 34, 76, 86, 51, 34, 58, 34, 233, 149, 191, 229, 133, 181, 34, 44, 34, 76, 86, 52, 34, 58, 34, 34, 44, 34, 76, 86, 53, 34, 58, 34, 230, 153, 186, 232, 191, 159, 34, 44, 34, 76, 86, 54, 34, 58, 34, 34, 44, 34, 76, 86, 55, 34, 58, 34, 230, 188, 171, 229, 176, 132, 34, 44, 34, 76, 86, 56, 34, 58, 34, 34, 125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[123, 34, 76, 86, 49, 34, 58, 34, 230, 128, 165, 230, 148, 187, 34, 44, 34, 76, 86, 50, 34, 58, 34, 34, 44, 34, 76, 86, 51, 34, 58, 34, 229, 136, 154, 231, 155, 180, 34, 44, 34, 76, 86, 52, 34, 58, 34, 34, 44, 34, 76, 86, 53, 34, 58, 34, 229, 164, 135, 230, 136, 152, 34, 44, 34, 76, 86, 54, 34, 58, 34, 34, 44, 34, 76, 86, 55, 34, 58, 34, 231, 153, 189, 230, 175, 166, 34, 44, 34, 76, 86, 56, 34, 58, 34, 34, 125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
  {    "   L   V   1   "  :    "   x    x    x    x    x    x    "   ,  
使用 java 打印数组:
{"LV1":"�攻","LV2":"","LV3":"刚直","LV4":"","LV5":"备战","LV6":"","LV7":"白毦","LV8":""}

可以看到上面标记 x 的地方应该就是技能的 id 之类的了,所以我们需要替换这个id,再加密后覆盖应该就可以。

找两个有同样技能的存档验证一下,解密出来比较一下数组,发现技能相同的数组值确实是一样的,这样就确定可以直接替换了。

先尝试搜索一下这些技能数组,看看源码里面有没有,尝试无果。只能自己手动刷技能,然后填进去了。

替换数据

首先,拿到存档里面的技能数据,然后拼接成字节数组:

[123, 34, 76, 86, 49, 34, 58, 34]

// 智迟
skill1 = [230, 153, 186, 232, 191, 159]

[34, 44, 34, 76, 86, 50, 34, 58, 34, 34, 44, 34, 76, 86, 51, 34, 58, 34]

// 藤甲
skill2 = [232, 151, 164, 231, 148, 178]

[34, 44, 34, 76, 86, 52, 34, 58, 34, 34, 44, 34, 76, 86, 53, 34, 58, 34]

// 白毦
skill3 = [231, 153, 189, 230, 175, 166]

[34, 44, 34, 76, 86, 52, 34, 58, 34, 34, 44, 34, 76, 86, 53, 34, 58, 34]

// 玄阵
skill4 = [231, 142, 132, 233, 152, 181]

[34, 44, 34, 76, 86, 56, 34, 58, 34, 34, 125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

编写加密逻辑:

func ECB_Encrypted(data:PoolByteArray)->PoolByteArray:
    var aes = AESContext.new()
    var key = "gd secret key!!!"
    while data.size() % 16 != 0:
        data.append(0)
    aes.start(AESContext.MODE_ECB_ENCRYPT, key.to_utf8())
    var encrypted:PoolByteArray = aes.update(data)
    aes.finish()
    return encrypted

编写签名逻辑:

func crypto_sign_short(data:PoolByteArray)->PoolByteArray:
    var crypto = Crypto.new()
    var signer = CryptoKey.new()
    signer.load_from_string("""
    -----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAOTW56JN2BCV/G//PQn4/Kz06h92jmdbUIM+KmzQrbvNVwiobwEd
3VvEsmDa6pQ0JFgVY8dr66Hc18HLShwJEq8CAwEAAQJADMHUQO6RBH+wBnhWqUcp
ouS2ZpGf57AmAWMGT3GktcrmOR+W4vjS9B2iFH/JhJDBMkQ+5py9+fMCE5gc0gMS
RQIhAPZUCEJAAl6y1FggoiVpaSUT9g9TdBYJfr/6wOPfqXebAiEA7dMVioDfqQ5t
zH2KySLtEVe2ANWroJLwL8Ts3vUVxH0CIQC7hlWTOe+T8Eg/nvhRyuHE3GFiYYHq
lOftdxQJZmg5KQIgWg+fjq2zBSAzsEaycezJ/dFLWRGRRuOeFVjropsJPTkCIQDt
p9I6LkIJqfid8y4YC1mSFF0g4ClEoAIv918R47hAEA==
-----END RSA PRIVATE KEY-----
    """, false)
    var hashed = data.get_string_from_utf8().md5_buffer()
    return crypto.sign(HashingContext.HASH_MD5, hashed, signer)

测试签名逻辑是否正确:

func test():
    var skills_data = "6336336237386135643733383165303730626537356434643035633430373237363734393235333431626362646562323732656234313732316562613862613134333632323035333335646265613765303838336432303637643639623239363633356637653430313730346238616462643564623530383764326261383138333834363638376239393533346133386630316437363638633362623932616233626634623139663539613231316664366663356166306635303232393735326163633233336436363836653839636563326336626338393662306563333630"

    skills_data = hex_decode(skills_data)

    var sign_data = "ba172a595d70137e650f34567238b2c6112bf15a8f1a238d4c0f9502a5b4bc5366235529243de0cb3e5cd797b9d85f5b36b7be899cda27065b0a505b722e9ddc"

    var my_sign_data = crypto_sign_short(skills_data)

    print(my_sign_data.hex_encode())

运行是匹配的,完美。

最后,生成技能代码:

func generate_skills_data():
    var result_data = PoolByteArray([])
    var skills_data = get_skills();
    skills_data = ECB_Encrypted(skills_data).hex_encode().to_ascii()
    result_data.append_array(skills_data)
    var sign_data = crypto_sign_short(skills_data)
    result_data.append_array(sign_data)
    result_data.append(64)
    result_data.append(2)
    print(result_data)
    var hex_string = result_data.hex_encode()
    print(hex_string)

替换存档数据,查看:

嗯???有bug,再重新读档一下,发现技能没变,说明签名逻辑没问题,数据也没问题。看来是技能id还有些东西没搞出来,需要再研究研究。

技能修正

突然想到游戏逻辑的一段代码,猜测技能代码里面的那6个字节数组其实是汉字:

var skillsData = JSON.print(skills).to_utf8()

测试一下,那个 godot 在线编辑器居然不能输出汉字,辣鸡。

使用 js 测试一下看看:

果然如此,那我们就可以重构代码了,在 4.x 版本的编辑器(https://gd.tumeo.space/#)里输出数组对比一下:

extends Node

func _ready():
#   var data = "6336336237386135643733383165303730626537356434643035633430373237363734393235333431626362646562323732656234313732316562613862613134333632323035333335646265613765303838336432303637643639623239363633356637653430313730346238616462643564623530383764326261383138333834363638376239393533346133386630316437363638633362623932616233626634623139663539613231316664366663356166306635303232393735326163633233336436363836653839636563326336626338393662306563333630ba172a595d70137e650f34567238b2c6112bf15a8f1a238d4c0f9502a5b4bc5366235529243de0cb3e5cd797b9d85f5b36b7be899cda27065b0a505b722e9ddc4002"
#   var d = decrypt_skill_data(data)
#   print(d)
    var skills = {
            "LV1":"智迟", 
            "LV2":"", 
            "LV3":"藤甲", 
            "LV4":"", 
            "LV5":"白毦", 
            "LV6":"", 
            "LV7":"玄阵", 
            "LV8":""
        };
    var skillsData = JSON.stringify(skills).to_utf8_buffer()
  print(PackedByteArray(skillsData))

与我们自己拼接的是一样的,看来我们的数据是没错。

补充

后来将技能调整了下顺序就可以了,有点奇怪,懒得调试了,查了下神武是默认值。使用字符串来生成数组的方式没有出过错了。上面代码可用。

完整代码

extends Node

func _ready():
#   var data = "6336336237386135643733383165303730626537356434643035633430373237363734393235333431626362646562323732656234313732316562613862613134333632323035333335646265613765303838336432303637643639623239363633356637653430313730346238616462643564623530383764326261383138333834363638376239393533346133386630316437363638633362623932616233626634623139663539613231316664366663356166306635303232393735326163633233336436363836653839636563326336626338393662306563333630ba172a595d70137e650f34567238b2c6112bf15a8f1a238d4c0f9502a5b4bc5366235529243de0cb3e5cd797b9d85f5b36b7be899cda27065b0a505b722e9ddc4002"
#   var d = decrypt_skill_data(data)
#   print(d)
    test()
    generate_skills_data()

func test():
    var skills_data = "6336336237386135643733383165303730626537356434643035633430373237363734393235333431626362646562323732656234313732316562613862613134333632323035333335646265613765303838336432303637643639623239363633356637653430313730346238616462643564623530383764326261383138333834363638376239393533346133386630316437363638633362623932616233626634623139663539613231316664366663356166306635303232393735326163633233336436363836653839636563326336626338393662306563333630"

    skills_data = hex_decode(skills_data)

    var sign_data = "ba172a595d70137e650f34567238b2c6112bf15a8f1a238d4c0f9502a5b4bc5366235529243de0cb3e5cd797b9d85f5b36b7be899cda27065b0a505b722e9ddc"

    var my_sign_data = crypto_sign_short(skills_data)

    print(my_sign_data.hex_encode())

func decrypt_skill_data(skills:String) -> String :
    var encrypted = PoolByteArray(hex_decode(skills))
    var sign_version = encrypted[encrypted.size() - 1]
    var signed_len = encrypted[encrypted.size() - 2]
    var signed = encrypted.subarray(encrypted.size() - signed_len - 2, encrypted.size() - 3)
    encrypted = encrypted.subarray(0, encrypted.size() - signed_len - 3)
    encrypted = hex_decode(encrypted.get_string_from_ascii())
    var decrypted = ECB_Decrypted(encrypted)
    print(decrypted)
    return decrypted.get_string_from_ascii()

func ECB_Encrypted(data:PoolByteArray)->PoolByteArray:
    var aes = AESContext.new()
    var key = "gd secret key!!!"
    while data.size() % 16 != 0:
        data.append(0)
    aes.start(AESContext.MODE_ECB_ENCRYPT, key.to_utf8())
    var encrypted:PoolByteArray = aes.update(data)
    aes.finish()
    return encrypted

func ECB_Decrypted(encrypted:PoolByteArray)->PoolByteArray:
    if encrypted.empty():
        return PoolByteArray([])
    var aes = AESContext.new()
    var key = "gd secret key!!!"

    aes.start(AESContext.MODE_ECB_DECRYPT, key.to_utf8())
    var decrypted:PoolByteArray = aes.update(encrypted)
    aes.finish()
    return decrypted

func hex_decode(data:String)->PoolByteArray:
    var ret = []
    var b:int = 0
    var odd = true
    for c in data.to_lower():
        if c in "abcdef":
            b += ord(c) - ord("a") + 10
        else :
            b += int(c)
        if odd:
            b *= 16
            odd = false
        else :
            ret.append(b)
            odd = true
            b = 0
    return ret

func generate_skills_data():
    var result_data = PoolByteArray([])
    var skills_data = get_skills();
    skills_data = ECB_Encrypted(skills_data).hex_encode().to_ascii()
    result_data.append_array(skills_data)
    var sign_data = crypto_sign_short(skills_data)
    result_data.append_array(sign_data)
    result_data.append(64)
    result_data.append(2)
    print(result_data)
    var hex_string = result_data.hex_encode()
    print(hex_string)

func get_skills()->PoolByteArray:
    var skill1 = [230, 153, 186, 232, 191, 159]
    var skill2 = [232, 151, 164, 231, 148, 178]
    var skill3 = [231, 153, 189, 230, 175, 166]
    var skill4 = [231, 142, 132, 233, 152, 181]

    var skills = [123, 34, 76, 86, 49, 34, 58, 34]
    skills.append_array(skill1)
    skills.append_array([34, 44, 34, 76, 86, 50, 34, 58, 34, 34, 44, 34, 76, 86, 51, 34, 58, 34])
    skills.append_array(skill2)
    skills.append_array([34, 44, 34, 76, 86, 50, 34, 58, 34, 34, 44, 34, 76, 86, 51, 34, 58, 34])
    skills.append_array(skill3)
    skills.append_array([34, 44, 34, 76, 86, 50, 34, 58, 34, 34, 44, 34, 76, 86, 51, 34, 58, 34])
    skills.append_array(skill4)
    skills.append_array([34, 44, 34, 76, 86, 56, 34, 58, 34, 34, 125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
    return PoolByteArray(skills)

func crypto_sign_short(data:PoolByteArray)->PoolByteArray:
    var crypto = Crypto.new()
    var signer = CryptoKey.new()
    signer.load_from_string("""
    -----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAOTW56JN2BCV/G//PQn4/Kz06h92jmdbUIM+KmzQrbvNVwiobwEd
3VvEsmDa6pQ0JFgVY8dr66Hc18HLShwJEq8CAwEAAQJADMHUQO6RBH+wBnhWqUcp
ouS2ZpGf57AmAWMGT3GktcrmOR+W4vjS9B2iFH/JhJDBMkQ+5py9+fMCE5gc0gMS
RQIhAPZUCEJAAl6y1FggoiVpaSUT9g9TdBYJfr/6wOPfqXebAiEA7dMVioDfqQ5t
zH2KySLtEVe2ANWroJLwL8Ts3vUVxH0CIQC7hlWTOe+T8Eg/nvhRyuHE3GFiYYHq
lOftdxQJZmg5KQIgWg+fjq2zBSAzsEaycezJ/dFLWRGRRuOeFVjropsJPTkCIQDt
p9I6LkIJqfid8y4YC1mSFF0g4ClEoAIv918R47hAEA==
-----END RSA PRIVATE KEY-----
    """, false)
    var hashed = data.get_string_from_utf8().md5_buffer()
    return crypto.sign(HashingContext.HASH_MD5, hashed, signer)

Android 版技能修改器

后来,在Android 里面测试了一下算法,是通用的,所以就写了一个Android版的修改器,再也不用开网页修改了。

https://github.com/aprz512/sgz-gd-save-editor

免费评分

参与人数 12威望 +1 吾爱币 +29 热心值 +12 收起 理由
junjia215 + 1 + 1 用心讨论,共获提升!
kasabulanka888 + 1 + 1 我很赞同!
xuxian106 + 1 + 1 用心讨论,共获提升!
1282155866 + 1 热心回复!
zzyzy + 1 + 1 谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!
ykynb + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
唐小样儿 + 1 + 1 我很赞同!
vmoranv + 1 热心回复!
正己 + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
SaberMason + 1 + 1 我很赞同!
wsdx233 + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

bigmojin 发表于 2024-8-24 07:23
大佬:简单的游戏逆向;我:拿小板凳、笔记本:这个什么?
rustzig 发表于 2024-8-23 18:51
amwquhwqas128 发表于 2024-8-23 22:44
Feichong1995 发表于 2024-8-23 22:46
真的很牛啊
zdmin 发表于 2024-8-23 23:35
技术很牛
assuller 发表于 2024-8-23 23:40
这个是什么天书
netxboy 发表于 2024-8-24 00:56
有这技术,,,还玩什么游戏。。
skyparis 发表于 2024-8-24 07:37
非常厉害
等到烟火也清凉 发表于 2024-8-24 08:57
体验技能的话 直接改技能文件更好

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-24 09:56

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表