瑞芯微 RK27/RK28 固件的图片资源提取工具。已测试 OPPO S33/S39 MP4 固件
https://github.com/he55/RK28UIAssets
- 图片预览
- 图片透明通道显示
- 图片批量保存
- 拖拽打开文件
- 拖拽保存图片
背景
十年前 OPPO 还在做 MP4 的时候(这也是 OPPO 最后一年做 MP4,之后都做手机业务了)。那时我还在上高中看上了 OPPO 的一款 MP4,我还记得型号是 S33(和这个一起的还有一个型号 S39)。在那时流行玩 MP4 的年代,班上同学基本上一半的人都有一台。当时被 OPPO 这款 MP4 外观和 UI 惊艳到了,于是攒了好几个月的钱买了一台。
我当时就觉得 UI 非常好看,就想把固件里面的资源提取出来,好在官方出了一个固件美化工具。这个工具可以修改固件的图片,但是并没有导出图片的功能。我研究一番发现,软件右边显示的图片会缓存到用户的临时文件夹,文件名为 tmp.bmp
的文件。这下总算可以拿到固件里面的图片资源了,但是如果图片有透明背景保存的图片就有问题。实际上这个软件本身解析的图片就不正确,当时我并没有继续探究原因(那时我还不会写代码,也不懂数据结构)。
最近我又想起了十年前困扰我的这个问题,这次我成功解决了之前的问题。
固件文件
OPPO S33 的固件文件实际上是 FAT16
文件系统镜像文件,用 7z
可以查看固件里面的文件结构。存放位图资源的文件路径 RESOURCE\BID\BMP0.BIN
,所有图片资源都在这个文件里面。这个文件就是要解析出图片资源的文件。
提取 BMP0.BIN
文件
BMP0.BIN
文件可以用 7z
手动提取,但官方的固件美化工具可以直接从固件中提取 BMP0.BIN
文件。用 IDA
分析官方的固件美化工具,发现是调用了当前目录下的 RK28FSDll.dll
文件。
使用 DLL Export Viewer
查看导出函数,只有 5 个函数
使用 IDA
反编译 RK28FSDll.dll
文件,分析出了调用参数。导出函数的 C# 代码
public static class RK28FS
{
public const string RK28FSDll = "RK28FSDll.dll";
[DllImport(RK28FSDll)]
public static extern int FS_Initialize(string imagePath, int arg2, int arg3);
[DllImport(RK28FSDll)]
public static extern int FS_DeInitialize();
[DllImport(RK28FSDll)]
public static extern int FS_GetLoaderPath(IntPtr ptr);
[DllImport(RK28FSDll)]
public static extern int FS_WriteFileToImg(string inputPath, string resPath);
[DllImport(RK28FSDll)]
public static extern int FS_WriteFileToPC(string resPath, string outputPath);
}
提取文件代码。FS_WriteFileToPC
方法第一个参数是镜像文件里的资源文件路径,要以 C:\
开头
RK28FS.FS_Initialize("OPPO_S33_10.828.img", 0, 0);
RK28FS.FS_WriteFileToPC("C:\\RESOURCE\\BID\\BMP0.BIN", "BMP0.BIN");
RK28FS.FS_DeInitialize();
BMP0.BIN
文件数据结构
public class RKRS_H
{
public int offset;
public short size;
public short count;
public ushort value;
public List<BID_H> _bids;
}
public class BID_H
{
/// <summary>
/// 长度
/// </summary>
public int length;
/// <summary>
/// 偏移
/// </summary>
public int offset;
public int _bi;
public int _bid;
public BIDD_H _bidd;
public string Id => $"BID_{_bid:X}";
public string Name => "";
public string Size => $"{_bidd.width}*{_bidd.height}";
public string Offset => $"0x{offset:X}";
public string Length => $"{length - 14}";
public string D3 => $"0x{_bidd.d3:X}";
public string D4 => $"0x{_bidd.d4:X}";
public string D5 => $"0x{_bidd.d5:X}";
}
public class BIDD_H
{
/// <summary>
/// 图片宽度
/// </summary>
public short width;
/// <summary>
/// 图片高度
/// </summary>
public short height;
/// <summary>
/// 颜色深度
/// </summary>
public short d3;
/// <summary>
/// 压缩模式
/// </summary>
public short d4;
public ushort d5;
public ushort d6;
public ushort d7;
}
BMP0.BIN
文件解析
public static RKRS_H ReadFile(string filePath)
{
using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
using (BinaryReader binaryReader = new BinaryReader(fileStream))
{
if (binaryReader.ReadInt32() != 0x00000028 || binaryReader.ReadInt32() != 0x53524b52)
{
throw new Exception("二进制文件格式错误");
}
RKRS_H rkrs = new RKRS_H();
rkrs._bids = new List<BID_H>();
fileStream.Position = 0x30;
rkrs.offset = binaryReader.ReadInt32();
fileStream.Position = 0x3c;
rkrs.size = binaryReader.ReadInt16();
rkrs.count = binaryReader.ReadInt16();
fileStream.Position = 0x42;
rkrs.value = binaryReader.ReadUInt16();
fileStream.Position = rkrs.offset;
for (int i = 0; i < rkrs.count; i++)
{
BID_H bid = new BID_H();
bid.length = binaryReader.ReadInt32();
bid.offset = binaryReader.ReadInt32();
bid._bi = i;
bid._bid = rkrs.value + i;
rkrs._bids.Add(bid);
}
foreach (BID_H bid in rkrs._bids)
{
fileStream.Position = bid.offset;
BIDD_H bidd = new BIDD_H();
bid._bidd = bidd;
bidd.width = binaryReader.ReadInt16();
bidd.height = binaryReader.ReadInt16();
bidd.d3 = binaryReader.ReadInt16();
bidd.d4 = binaryReader.ReadInt16();
bidd.d5 = binaryReader.ReadUInt16();
bidd.d6 = binaryReader.ReadUInt16();
bidd.d7 = binaryReader.ReadUInt16();
}
return rkrs;
}
}
源码
https://github.com/he55/RK28UIAssets