#1 - 2023-1-26 18:51
nusfb
我爱好收藏但不会编程,以往都是收藏别人提取好的,前两天看到一篇报道里这款游戏的图片,搜不到解包就试着解解看,碰到png打不开一冲动就提问了,没想能在 xu_zh 大佬的帮助下成功解密了!趁着兴奋劲写好了网页,把资源上传到了 GitHub,第一次弄,有什么可以改进的地方还请大佬指出!像素小人及其他的动画之后会添加。(不知道为什么网页版唯独10400读不出,我用同样是3.8.x的 jar 版查看器就能打开(bgm116)

仓库地址:https://github.com/memo-db/pixel-heroes-spine
在线查看1:https://pixel-heroes-spine.pages.dev
在线查看2:https://pixel-heroes-spine.vercel.app
在线查看3:https://pixel-heroes-spine.netlify.app

您可以在线查看,或将仓库 clone 到本地,使用现代浏览器打开 index.html 文件即可查看 Spine 动画。点击表头的id/c/r可按角色id/内容评级/“我”的评分进行排序。

--------原文--------

本月新出的手游《马赛克英雄》角色不错,我想提取 Spine 动画来收藏。
游戏资源不用另下都在安装包里,安装包下载(800MB),但png图片加密了,有大佬解答吗?我传了一份单角色的资源(600KB),有兴趣的可以点击下载。
https://files.catbox.moe/rdm56x.zip
#2 - 2023-1-26 20:29
先来个比较朴素的思路,不保证对(

看了一眼文件头,89 57 BA 8D 0D 0D EE C0,和正常png的89 50 4E 47 0D 0A 1A 0A对比
怀疑是每4字节为单位,异或00 07 f4 ca来加密
但是第0x42字节起出现了PLTE字样,怀疑后面是未加密原文,无脑搜索IDAT块,看到64个结果,前63个都是每chuck 0x200c字节,和整个文件510K比较吻合,猜测应该没错
比较奇怪的是文件结尾并不是IEND块而是下面这个玩意

但这毕竟不是数据块,对于现代的软件来说应该不会影响正常打开图片,不知道有没有大佬能再解答一下

那么解密就是前0x42字节依次循环异或00 07 f4 ca,后面保持原始数据,待会用python写一下试试
#2-1 - 2023-1-26 20:31
东坡Des1ope
好硬核,小白看傻了
#2-2 - 2023-1-26 20:42
xu_zh

with open("10100.png", "rb") as f:
    data = f.read()

key = [0x00, 0x07, 0xf4, 0xca]
bound = 0x42
dec = []

for i in range(bound):
    kbit = key.pop(0)
    key.append(kbit)
    bit = data[i]
    dec.append(bit ^ kbit)

dec = bytes(dec) + data[bound:]

with open("10100_decoded.png", "wb") as f:
    f.write(dec)

测了一下意外的发现还真的能用(
https://https://files.catbox.moe/h7ovs9.png
#2-3 - 2023-1-26 21:27
nusfb
xu_zh 说:
with open("10100.png", "rb") as f:
    data = f.read()

key = [0x00, 0x07, 0xf4, 0xca]
bo...
喔喔,大佬强强强!不过解密后的图片有些软件能读,有些软件读取会出错,比如 Chrome 可以,Firefox 不行(
#2-4 - 2023-1-26 21:38
nusfb
xu_zh 说:
with open("10100.png", "rb") as f:
    data = f.read()

key = [0x00, 0x07, 0xf4, 0xca]
bo...
另外,我用 Spine Web Player 播放动画会显示“Error: Could not load skeleton binary.name cannot be null.”,是骨骼文件也加密过了吗?
#2-5 - 2023-1-26 21:50
xu_zh
nusfb 说: 喔喔,大佬强强强!不过解密后的图片有些软件能读,有些软件会读取出错,比如 Chrome 可以,Firefox 不行(
那么你可以尝试强行把尾部12字节换成正确的IEND,例如

dec = bytes(dec) + data[bound:-12] + bytes([0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82])
#2-6 - 2023-1-26 21:51
原野
%%% ctf 佬
#2-7 - 2023-1-26 21:52
xu_zh
nusfb 说: 另外,我用 Spine Web Player 播放动画会显示“Error: Could not load skeleton binary.name cannot be null.”,是骨骼文件也加密过...
骨骼文件我不熟(bgm38),但看着报错不像是加密导致的
#2-8 - 2023-1-26 22:09
nusfb
xu_zh 说: 那么你可以尝试强行把尾部12字节换成正确的IEND,例如

dec = bytes(dec) + data[bound:-12] + bytes([0x00, 0x00, 0x00, 0x00, 0x...
谢大佬!图片能正常读取了!骨骼文件我去下个 Spine 编辑器看看能正常导入不(bgm38)
#2-9 - 2023-1-26 22:48
热爱所热爱
佬。不过加密方式最后回过头看,感觉很朴实啊(bgm38)
#2-10 - 2023-1-27 00:29
XBan
想请教下异或的密钥是怎么看出来的,纯经验吗(另解密过程真的很精彩!)
#2-11 - 2023-1-27 01:39
nusfb
xu_zh 说: 骨骼文件我不熟,但看着报错不像是加密导致的
确实没加密,是因为 Spine 不同版本间的兼容性较差,之前我没用对版本所以打不开(bgm38)
#2-12 - 2023-1-27 02:04
冒泡ioa
这xor的还正好的是这个图片的字节数(bgm39)
#2-13 - 2023-1-27 10:22
xu_zh
XBan 说: 想请教下异或的密钥是怎么看出来的,纯经验吗(另解密过程真的很精彩!)
确定了异或运算以后密钥是显然的:
根据异或运算的特性A^0=A,A^A=0以及结合律,可以知道既然密文是原文^密钥,那么原文^密文=原文^原文^密钥=密钥
#2-14 - 2023-1-27 10:34
xu_zh
xu_zh 说: 确定了异或运算以后密钥是显然的:根据异或运算的特性A^0=A,A^A=0以及结合律,可以知道既然密文是原文^密钥,那么原文^密文=原文^原文^密钥=密钥
至于怎么确定的异或运算,emm,大概算是背景知识+经验&直觉+尝试&验证+亿点点运气
具体来说
背景知识:png图片的文件结构,或者说数据格式要求头部一些字节是确定的,直接百度搜一下有很多大佬解析过,这样就相当于知道了一部分原文,由此结合密文反推加密方式和密钥是比干瞪眼或者暴力简单很多的
经验:这种给游戏图片加密的方式(大概是出于性能之类的考虑?)通常不会很复杂
直觉:把原文和密文摆在一起瞪眼,第0、4字节完全相同,第1、5字节都满足异或7。最符合直觉的方式就是4字节一组的异或加密
尝试&验证:算出密钥,写一下解密程序,得到的结果文件头里面包含有sRGB这样的字段,符合正常png图片特征。还有些步骤之前提了就不重复了orz
#2-15 - 2023-1-27 11:40
XBan
xu_zh 说: 至于怎么确定的异或运算,emm,大概算是背景知识+经验&直觉+尝试&验证+亿点点运气具体来说背景知识:png图片的文件结构,或者说数据格式要求头部一些字节是确定的,直接百度搜一下有很多大佬解析过,这样就相当于知道了一部分原文,由此结合密文反推加密方式和密钥是比干瞪眼或者暴力简单很多的经验:这种给游戏图片加密的方式(大概是出于性能之类的考虑?)通常不会很复杂直觉:把原文和密文摆在一起瞪眼,第0、4字节完全相同,第1、5字节都满足异或7。最符合直觉的方式就是4字节一组的异或加密尝试&验证:算出密钥,写一下解密程序,得到的结果文件头里面包含有sRGB这样的字段,符合正常png图片特征。还有些步骤之前提了就不重复了orz
orz,感谢详细解释,异或加密特性和直觉学习了
#2-16 - 2023-1-27 12:36
糸色企鹅
xu_zh 说: xu_zh 说: 确定了异或运算以后密钥是显然的:根据异或运算的特性A^0=A,A^A=0以及结合律,可以知道既然密文是原文^密钥,那么原文^密文=原文^原文^密钥=密钥至于怎么确定的异或运算,emm...
牛逼 感谢讲解
#2-17 - 2023-1-27 17:25
nusfb
xu_zh 说: 确定了异或运算以后密钥是显然的:
根据异或运算的特性A^0=A,A^A=0以及结合律,可以知道既然密文是原文^密钥,那么原文^密文=原文^原文^密钥=密钥
出现了新的问题,这个解密脚本不能套用在其他图片上,也许每张图的密钥是各自生成的,大佬能把代码完善下吗?谢谢!我传了六张图片资源以供验证。
https://files.catbox.moe/pltixt.zip
#2-18 - 2023-1-27 19:51
xu_zh
nusfb 说: xu_zh 说: 确定了异或运算以后密钥是显然的:
根据异或运算的特性A^0=A,A^A=0以及结合律,可以知道既然密文是原文^密钥,那么原文^密文=原文^原文^密钥=密钥出现了新的问题,这个解密脚本...
有道理,是我想少了一步,另外我注意到结束加密的边界其实也是不确定的,暂时还没有发现和密钥或者文件名有什么联系。可能在数据包的其它位置藏了每个文件对应的密钥和边界吧
另外针对10102这一个例子,它结束加密的边界在PLTE块的数据段,这一段的内容我不是很了解,而且图片之间不一致,没有可比对的原文
简单点可以直接暴力,生成边界在一定范围内的所有图片,然后人工看哪个能打开。稍微复杂点就验证一下CRC校验的结果
但这太蠢了,让我再研究下
#2-19 - 2023-1-27 20:05
xu_zh
xu_zh 说: nusfb 说: xu_zh 说: 确定了异或运算以后密钥是显然的:
根据异或运算的特性A^0=A,A^A=0以及结合律,可以知道既然密文是原文^密钥,那么原文^密文=原文^原文^密钥=密钥出现了新的...
额,我大概意识到文件末尾为什么不是标准IEND了
从目前给我的文件来看,倒数第5个字节的值就是异或加密边界的位置(问就是干瞪眼看出来的),倒数第4、第1字节的值和key是相同的,怀疑是把key异或了某个常数存在最后,但倒数第3字节不符合这个规律,我得再想想
改进后的代码:

import os

def decode(filename):
    print(f"Decode {filename}.png")
    with open(f"{filename}.png", "rb") as f:
        data = f.read()

    key = [original ^ cipher for original, cipher in zip([0x89, 0x50, 0x4E, 0x47], data[:4])]
    bound = data[-5]
    print(f" -> guess key: {[hex(k) for k in key]}, bound: {hex(bound)}")
    dec = []

    for i in range(bound):
        kbyte = key.pop(0)
        key.append(kbyte)
        dbyte = data[i]
        dec.append(dbyte ^ kbyte)

    dec = bytes(dec[:bound]) + \
          data[bound:-12] + \
          bytes([0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82])

    with open(f"decode/{filename}.png", "wb") as f:
        f.write(dec)

if not os.path.exists("decode"):
    os.mkdir("decode")

mode = input("input filename (leave empty for all): ")
if not mode:
    for filename in os.listdir("."):
        if not filename.endswith(".png"):
            continue
        decode(filename[:-4])
else:
    decode(mode)

另外为了方便使用(和调试)我加了一个交互,输入文件名回车(不含.png后缀)会尝试解码该文件,直接回车会尝试解码当前文件夹下所有文件
#2-20 - 2023-1-27 20:20
xu_zh
xu_zh 说: xu_zh 说: nusfb 说: xu_zh 说: 确定了异或运算以后密钥是显然的:
根据异或运算的特性A^0=A,A^A=0以及结合律,可以知道既然密文是原文^密钥,那么原文^密文=原文^原文^密...
小学找规律:


屑,中间两字节不是异或8而是带进位的加8
0x915+0x8 = 0x91d
...
0x5fd+0x8=0x605

不过其实已经利用原文^密文反推密钥了,结尾这4字节用不用也无所谓了
#2-21 - 2023-1-27 20:58
nusfb
xu_zh 说: xu_zh 说: nusfb 说: xu_zh 说: 确定了异或运算以后密钥是显然的:
根据异或运算的特性A^0=A,A^A=0以及结合律,可以知道既然密文是原文^密钥,那么原文^密文=原文^原文^密...
好耶!请问这是最终代码了吗?是的话,我弄好网页查看器后把处理好的资源传到 github 上方便大家欣赏动画。
#2-22 - 2023-1-27 21:04
xu_zh
nusfb 说: 好耶!请问这是最终代码了吗?是的话,我弄好网页查看器后把处理好的资源传到github上方便大家欣赏动画。
如果你测试都能解码就行了,仓库链接记得给一个我去点star(bgm47)
#2-23 - 2023-1-28 03:24
nusfb
xu_zh 说: 如果你测试都能解码就行了,仓库链接记得给一个我去点star
弄好了!第一次弄,有什么可以改进的地方还请大佬指出!像素小人及其他的动画之后会添加。
仓库地址:https://github.com/memo-db/pixel-heroes-spine
在线查看1:https://pixel-heroes-spine.pages.dev
在线查看2:https://pixel-heroes-spine.vercel.app
在线查看3:https://pixel-heroes-spine.netlify.app
#3 - 2023-1-26 22:17
(DD雷达搜寻中...?)
万能班
#4 - 2023-1-26 22:54
(愛讀者諸孃は御賛成下さいまし)
万能班
#5 - 2023-1-27 00:43
学到了,无论是加密方法还是分析都很有学习价值。
#6 - 2023-1-27 00:59
学到了(bgm38)这过程跟我想得不太一样啊,竟然不是有一个密钥然后加密整个文件,就加密了个头
#6-1 - 2023-1-27 11:12
烈之斩
解码整个文件太耗时/资源
#6-2 - 2023-1-27 20:13
頂上ノ月🌙
烈之斩 说: 解码整个文件太耗时/资源
所以sc的spine那张图片干脆不加密
#7 - 2023-1-27 10:33
删除了回复
#8 - 2023-1-27 11:23
(可愛くなりたい / 可愛くてごめん)
学习了(bgm39)
#9 - 2023-1-27 12:32
(Never knows best.)
万能班
#10 - 2023-1-27 13:12
万能班(bgm38)
#11 - 2023-1-27 13:15
(The Laughing Man)
万能班(bgm38)
#12 - 2023-1-27 16:13
(没有想好要成为什么样的大人)
看傻了
#13 - 2023-1-27 18:45
(都是异端!)
太牛逼了
#13-1 - 2023-1-30 20:21
#14 - 2023-1-27 21:03
万能班(bgm38)
#15 - 2023-1-28 01:02
(小圣杯邀请码: whyjxz14#576501)
以前我看了好久明日方舟拆包那个人的博客。看了好久都没看懂(bgm38)
#16 - 2023-1-30 17:55
(还我穷鬼套餐!)
牛逼疯了,学到了(bgm38)
#17 - 2023-1-30 22:07
(迷子でもいい、迷子でも進め。)
学到了
#18 - 2023-2-12 16:21
看傻了,之前也遇到过解不开的png,最后放弃了……
#19 - 2023-2-12 17:39
狠狠得mark