请问下大佬们那种图片块被调乱CG图怎么解密?

最近碰到像这种图片被一块块调乱了的CG图,
请问这种图怎么找解密还原的方法?

以下是游戏包,这两游戏都是HCG打成了马赛克,并且live2D导出来打不开
image

游戏包薇薇與魔法之島Ver1.05

游戏包–帝国后宫计划(PC和安卓版)我只下了PC版
解压密码 dpsvip

另外还有这两个游戏加密HCG样版图片各一张
微微与帝国后宫样版图.zip (8.7 MB)

大佬们帮看看这两个游戏的HCG怎么解码,还有live2D怎么导出来打不开

发原件出来我们才能尝试去拼图
CTF杂项题中也存在很多这种拼图题目应该可以使用相同方法解决

大佬 原件和游戏包我都发出来了,有空帮看看

https://blog.csdn.net/m0_47643893/article/details/113778577
一种思路
剩下的等假期过后吧可以先研究一下

1 个赞

看着像是 UTAGE Dicing Textures

1 个赞

看样子应该是你说的这个,有空我研究研究

我今天解游戏时也遇到的这个问题,经过提示,通过修改优化的py解决了。改的比较懒人加简单了。

不过可惜的是上面第一个游戏不是很适配这个方法,因为我没法找到对应的配置文件(含cellSize,padding,atlasTextures,textureDataList字段),也可能不在Monobehavior类中?虽然也是这种剪切方式,可以参考一下。

总结
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Utage
{
	// Token: 0x02000317 RID: 791
	[Serializable]
	public class DicingTextureData
	{
		// Token: 0x17000622 RID: 1570
		// (get) Token: 0x06001D7A RID: 7546 RVA: 0x0008E96C File Offset: 0x0008CB6C
		public string Name
		{
			get
			{
				return this.name;
			}
		}

		// Token: 0x17000623 RID: 1571
		// (get) Token: 0x06001D7B RID: 7547 RVA: 0x0008E974 File Offset: 0x0008CB74
		public string AtlasName
		{
			get
			{
				return this.atlasName;
			}
		}

		// Token: 0x17000624 RID: 1572
		// (get) Token: 0x06001D7C RID: 7548 RVA: 0x0008E97C File Offset: 0x0008CB7C
		public int Width
		{
			get
			{
				return this.width;
			}
		}

		// Token: 0x17000625 RID: 1573
		// (get) Token: 0x06001D7D RID: 7549 RVA: 0x0008E984 File Offset: 0x0008CB84
		public int Height
		{
			get
			{
				return this.height;
			}
		}

		// Token: 0x06001D7E RID: 7550 RVA: 0x0008E98C File Offset: 0x0008CB8C
		internal List<DicingTextureData.QuadVerts> GetVerts(DicingTextures textures)
		{
			if (this.verts == null)
			{
				this.InitVerts(textures);
			}
			return this.verts;
		}

		// Token: 0x06001D7F RID: 7551 RVA: 0x0008E9A4 File Offset: 0x0008CBA4
		private void InitVerts(DicingTextures atlas)
		{
			if (atlas == null)
			{
				return;
			}
			this.verts = new List<DicingTextureData.QuadVerts>();
			int cellSize = atlas.CellSize;
			int num = cellSize - atlas.Padding * 2;
			int num2 = Mathf.CeilToInt(1f * (float)this.Width / (float)num);
			int num3 = Mathf.CeilToInt(1f * (float)this.Height / (float)num);
			int num4 = atlas.GetTexture(this.AtlasName).width;
			int num5 = atlas.GetTexture(this.AtlasName).height;
			int num6 = Mathf.CeilToInt(1f * (float)num4 / (float)cellSize);
			int num7 = 0;
			for (int i = 0; i < num3; i++)
			{
				float num8 = (float)(i * num);
				float num9 = Mathf.Min(num8 + (float)num, (float)this.Height);
				for (int j = 0; j < num2; j++)
				{
					DicingTextureData.QuadVerts quadVerts = new DicingTextureData.QuadVerts();
					float num10 = (float)(j * num);
					float num11 = Mathf.Min(num10 + (float)num, (float)this.Width);
					quadVerts.v = new Vector4(num10, num8, num11, num9);
					int num12 = this.cellIndexList[num7];
					quadVerts.isAllTransparent = (num12 == this.transparentIndex);
					float num13 = (float)(num12 % num6 * cellSize);
					float num14 = (float)(num12 / num6 * cellSize);
					float x = 1f * (num13 + (float)atlas.Padding) / (float)num4;
					float y = 1f * (num14 + (float)atlas.Padding) / (float)num5;
					float num15 = 1f * (num11 - num10) / (float)num4;
					float num16 = 1f * (num9 - num8) / (float)num5;
					quadVerts.uvRect = new Rect(x, y, num15, num16);
					this.verts.Add(quadVerts);
					num7++;
				}
			}
		}

		// Token: 0x06001D80 RID: 7552 RVA: 0x0008EB64 File Offset: 0x0008CD64
		public void ForeachVertexList(Rect position, Rect uvRect, bool skipTransParentCell, DicingTextures textures, Action<Rect, Rect> function)
		{
			Vector2 scale = new Vector2(position.width / (float)this.Width, position.height / (float)this.Height);
			this.ForeachVertexList(uvRect, skipTransParentCell, textures, delegate(Rect r1, Rect r2)
			{
				r1.xMin *= scale.x;
				r1.xMax *= scale.x;
				r1.x += position.x;
				r1.yMin *= scale.y;
				r1.yMax *= scale.y;
				r1.y += position.y;
				function(r1, r2);
			});
		}

		// Token: 0x06001D81 RID: 7553 RVA: 0x0008EBD0 File Offset: 0x0008CDD0
		public void ForeachVertexList(Rect uvRect, bool skipTransParentCell, DicingTextures textures, Action<Rect, Rect> function)
		{
			if (uvRect.width == 0f || uvRect.height == 0f)
			{
				return;
			}
			if (uvRect.xMin < 0f)
			{
				uvRect.x += (float)Mathf.CeilToInt(-uvRect.xMin);
			}
			if (uvRect.yMin < 0f)
			{
				uvRect.y += (float)Mathf.CeilToInt(-uvRect.yMin);
			}
			bool flag = false;
			if (uvRect.width < 0f)
			{
				uvRect.width *= -1f;
				flag = true;
			}
			bool flag2 = false;
			if (uvRect.height < 0f)
			{
				uvRect.height *= -1f;
				flag2 = true;
			}
			float scaleX = 1f / uvRect.width;
			float fipOffsetX = 0f;
			if (flag)
			{
				scaleX *= -1f;
				fipOffsetX = (float)this.Width;
			}
			float scaleY = 1f / uvRect.height;
			float fipOffsetY = 0f;
			if (flag2)
			{
				scaleY *= -1f;
				fipOffsetY = (float)this.Height;
			}
			float num = uvRect.yMin % 1f;
			float num2 = uvRect.yMax % 1f;
			if (num2 == 0f)
			{
				num2 = 1f;
			}
			float offsetY = 0f;
			bool flag3 = true;
			Rect rect = default(Rect);
			bool flag4;
			do
			{
				rect.yMin = (flag3 ? num : 0f);
				flag4 = (offsetY + 1f - rect.yMin >= uvRect.height);
				rect.yMax = (flag4 ? num2 : 1f);
				float num3 = uvRect.xMin % 1f;
				float num4 = uvRect.xMax % 1f;
				if (num4 == 0f)
				{
					num4 = 1f;
				}
				float offsetX = 0f;
				bool flag5 = true;
				bool flag6;
				do
				{
					rect.xMin = (flag5 ? num3 : 0f);
					flag6 = (offsetX + 1f - rect.xMin >= uvRect.width);
					rect.xMax = (flag6 ? num4 : 1f);
					this.ForeachVertexListSub(rect, skipTransParentCell, textures, delegate(Rect r1, Rect r2)
					{
						r1.xMin *= scaleX;
						r1.xMax *= scaleX;
						r1.x += (offsetX - rect.xMin) * scaleX * (float)this.Width + fipOffsetX;
						r1.yMin *= scaleY;
						r1.yMax *= scaleY;
						r1.y += (offsetY - rect.yMin) * scaleY * (float)this.Height + fipOffsetY;
						function(r1, r2);
					});
					offsetX += rect.width;
					flag5 = false;
				}
				while (!flag6);
				offsetY += rect.height;
				flag3 = false;
			}
			while (!flag4);
		}

		// Token: 0x06001D82 RID: 7554 RVA: 0x0008EF1C File Offset: 0x0008D11C
		private void ForeachVertexListSub(Rect uvRect, bool skipTransParentCell, DicingTextures textures, Action<Rect, Rect> function)
		{
			Texture2D texture = textures.GetTexture(this.AtlasName);
			float num = (float)texture.width;
			float num2 = (float)texture.height;
			List<DicingTextureData.QuadVerts> list = this.GetVerts(textures);
			Rect rect = new Rect(uvRect.x * (float)this.Width, uvRect.y * (float)this.Height, uvRect.width * (float)this.Width, uvRect.height * (float)this.Height);
			for (int i = 0; i < list.Count; i++)
			{
				DicingTextureData.QuadVerts quadVerts = list[i];
				if (!skipTransParentCell || !quadVerts.isAllTransparent)
				{
					float x = quadVerts.v.x;
					float num3 = quadVerts.v.z;
					float y = quadVerts.v.y;
					float num4 = quadVerts.v.w;
					Rect uvRect2 = quadVerts.uvRect;
					if (x <= rect.xMax && y <= rect.yMax && num3 >= rect.x && num4 >= rect.y)
					{
						if (x < rect.x)
						{
							uvRect2.xMin += (rect.x - x) / num;
							x = rect.x;
						}
						if (num3 > rect.xMax)
						{
							uvRect2.xMax += (rect.xMax - num3) / num;
							num3 = rect.xMax;
						}
						if (y < rect.y)
						{
							uvRect2.yMin += (rect.y - y) / num2;
							y = rect.y;
						}
						if (num4 > rect.yMax)
						{
							uvRect2.yMax += (rect.yMax - num4) / num2;
							num4 = rect.yMax;
						}
						function(new Rect(x, y, num3 - x, num4 - y), uvRect2);
					}
				}
			}
		}

		// Token: 0x04001144 RID: 4420
		[SerializeField]
		private string name = "";

		// Token: 0x04001145 RID: 4421
		[SerializeField]
		private string atlasName = "";

		// Token: 0x04001146 RID: 4422
		[SerializeField]
		private int width;

		// Token: 0x04001147 RID: 4423
		[SerializeField]
		private int height;

		// Token: 0x04001148 RID: 4424
		[SerializeField]
		private List<int> cellIndexList = new List<int>();

		// Token: 0x04001149 RID: 4425
		[SerializeField]
		private int transparentIndex;

		// Token: 0x0400114A RID: 4426
		[NonSerialized]
		private List<DicingTextureData.QuadVerts> verts;

		// Token: 0x02000EEF RID: 3823
		public class QuadVerts
		{
			// Token: 0x0400561B RID: 22043
			public Vector4 v;

			// Token: 0x0400561C RID: 22044
			public Rect uvRect;

			// Token: 0x0400561D RID: 22045
			public bool isAllTransparent;
		}
	}
}

mono打包的Unity游戏,存在该DLL:Game_Data\Managed\Utage.dll的,下面可以还原。

还原png图片
import os
import json
from PIL import Image
import UnityPy
import math

def scan_directory(dir_path, required_fields):
    valid_jsons = []
    for root, _, files in os.walk(dir_path):
        for file in files:
            if file.lower().endswith('.json'):
                json_path = os.path.join(root, file)
                try:
                    with open(json_path, 'r', encoding='utf-8') as f:
                        data = json.load(f)
                    if all(field in data for field in required_fields):
                        valid_jsons.append(json_path)
                except Exception:
                    continue
    return valid_jsons

def import_unity_resources(path):
    import_results = []
    if os.path.isfile(path):
        files = [path]
    elif os.path.isdir(path):
        files = []
        for root, dirs, filenames in os.walk(path):
            for filename in filenames:
                files.append(os.path.join(root, filename))
    else:
        print(f"路径 {path} 不存在。")
        return import_results

    for file in files:
        try:
            env = UnityPy.load(file)
            import_results.append((file, True))
        except Exception as e:
            import_results.append((file, False, str(e)))
    return import_results

def export_texture_by_path_id(env, path_id, export_dir):
    for obj in env.objects:
        if obj.path_id == path_id and obj.type.name == "Texture2D":
            data = obj.read()
            if "Atlas" in data.m_Name:
                path = os.path.join(export_dir, f"{data.m_Name}.png")
                try:
                    data.image.save(path)
                    print(f"成功导出 Texture2D 资源到 {path}")
                    return path  # 返回保存的文件路径
                except Exception as e:
                    print(f"导出 Texture2D 资源时出错: {e}")
            return None
    print(f"未找到 path_id 为 {path_id} 且文件名包含 'Atlas' 的 Texture2D 资源。")
    return None

def reconstruct_from_json(json_path, atlas_dir, output_dir):
    with open(json_path, 'r', encoding='utf-8') as f:
        data = json.load(f)

    cell_size = data['cellSize']
    padding = data['padding']
    texture_list = data.get('textureDataList', [])

    atlas_images = {}
    for td in texture_list:
        atlas_name = td['atlasName']
        if atlas_name not in atlas_images:
            img_path = os.path.join(atlas_dir, f"{atlas_name}.png")
            if not os.path.isfile(img_path):
                raise FileNotFoundError(f"Atlas image not found: {img_path}")
            atlas_images[atlas_name] = Image.open(img_path).convert("RGBA")

    os.makedirs(output_dir, exist_ok=True)
    for td in texture_list:
        name = td['name']
        atlas_name = td['atlasName']
        width, height = td['width'], td['height']
        indices = td['cellIndexList']
        transparent_index = td['transparentIndex']

        atlas_img = atlas_images[atlas_name]
        atlas_w, atlas_h = atlas_img.size

        content_size = cell_size - 2 * padding
        cols = math.ceil(width / content_size)
        rows = math.ceil(height / content_size)
        cells_per_row = atlas_w // cell_size

        result = Image.new('RGBA', (width, height), (0, 0, 0, 0))

        patches = []
        positions = []
        idx = 0
        for row in range(rows):
            for col in range(cols):
                ci = indices[idx]
                idx += 1
                if ci == transparent_index:
                    continue  

                cell_col = ci % cells_per_row
                cell_row = ci // cells_per_row
                crop_w = min(content_size, width - col * content_size)
                crop_h = min(content_size, height - row * content_size)
                x0 = cell_col * cell_size + padding
                y0_unity = cell_row * cell_size + padding
                y0 = atlas_h - y0_unity - crop_h

                box = (x0, y0, x0 + crop_w, y0 + crop_h)
                patch = atlas_img.crop(box)

                dst_x = col * content_size
                dst_y = height - (row * content_size + crop_h)

                patches.append(patch)
                positions.append((dst_x, dst_y))

        for patch, pos in zip(patches, positions):
            result.paste(patch, pos, patch)

        base_name = f"{atlas_name}_{name}.png"
        out_path = os.path.join(output_dir, base_name)
        number = 1
        while os.path.exists(out_path):
            out_path = os.path.join(output_dir, f"{os.path.splitext(base_name)[0]}_{number}.png")
            number += 1

        result.save(out_path)
        print(f"Saved: {out_path}")

def main():
    json_directory = input("请输入要扫描的 JSON 文件目录路径: ").strip('"\'')
    unity_resources_directory = input("请输入要扫描的 Unity 资源文件目录路径: ").strip('"\'')
    output_directory = input("请输入输出目录路径: ").strip('"\'')
    required_fields = ['cellSize', 'padding', 'textureDataList', 'atlasTextures']

    if not os.path.isdir(json_directory):
        print(f"错误: 目录 '{json_directory}' 不存在")
        return

    if not os.path.isdir(unity_resources_directory):
        print(f"错误: 目录 '{unity_resources_directory}' 不存在")
        return

    os.makedirs(output_directory, exist_ok=True)

    valid_jsons = scan_directory(json_directory, required_fields)

    if valid_jsons:
        print(f"找到 {len(valid_jsons)} 个包含所有必要字段的 JSON 文件:")
        
        for sum_num, json_path in enumerate(valid_jsons):
            print(f"- {json_path}")
            
            json_output_dir = os.path.join(output_directory, f"{sum_num}")
            os.makedirs(json_output_dir, exist_ok=True)
            
            with open(json_path, 'r', encoding='utf-8') as f:
                data = json.load(f)
            path_id = data['atlasTextures'][0]['m_PathID']

            import_results = import_unity_resources(unity_resources_directory)

            exported_files = []
            for result in import_results:
                if result[1]:
                    file = result[0]
                    env = UnityPy.load(file)
                    exported_file = export_texture_by_path_id(env, path_id, json_output_dir)
                    if exported_file:
                        exported_files.append(exported_file)

            reconstruct_from_json(json_path, json_output_dir, json_output_dir)

            for file in exported_files:
                if os.path.exists(file):
                    os.remove(file)
                    print(f"已删除临时文件: {file}")

    else:
        print(f"未找到包含所有必要字段 ({', '.join(required_fields)}) 的 JSON 文件")


if __name__ == "__main__":
    main()

先导出所有的Monobehavior类(第一次导出会选两次文件夹,其中有一次是选Game_Data\Managed文件夹,如果是ilcpp打包则需要il2cppdumper dump出dll),

如果不选择对dll的话导出来全是1kb的没用的json。

然后打开py,
输入剪切png的特征字符,留空则不选择。
输入导出的Monobehavior类文件夹地址,
输入unity资源文件的文件夹地址,
输入输出文件夹即可自动开始,

就是有点慢,而且因为加载了unity的资源文件会占内存(但是搜索导出的png的话会有重名的就不好检索了)。

live2d用缝合版就可以导出。

1 个赞