本帖最后由 dhdajun 于 2024-08-25 16:37 编辑
最近沉迷于一个老游戏,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
注:若转载请注明大神论坛来源(本贴地址)与作者信息。
|