涉及:apk、aab、global-metadata.dat、jks密钥文件、APKTool、zipalign
使用7z打开apk文件观察发现有如下3个针对加密的文件。
xxx.apk\assets\bin\Data\Managed\Metadata\global-metadata.dat
xxx.apk\lib\armeabi-v7a\libil2cpp.so
xxx.apk\lib\arm64-v8a\libil2cpp.so
xxx.aab\base\assets\bin\Data\Managed\Metadata\global-metadata.dat
xxx.aab\base\lib\armeabi-v7a\libil2cpp.so
xxx.aab\base\lib\arm64-v8a\libil2cpp.so
如上打包配置:Player Settings - Other Settings - Scripting Backend 选IL2CPP
若选Mono,libil2cpp.so会变成libmono.so,针对.so文件的加密较为复杂可参考libmono.so打包
【Unity3D】unity-mono编译libmono.so成功-CSDN博客
本文章仅针对global-metadata.dat文件加密,global-metadata.dat是一种元数据文件,包含了编译后的IL代码所需要的所有信息,包括类型信息、方法信息、字段信息等等。
它由两部分组成:文件头和元数据。
文件头包含了两个重要的信息:魔数和版本号。
魔数是一个4字节的标志,用于确定这个文件是Unity的元数据文件,它的值是0xFAB11BAF。
版本号是一个4字节的无符号整数,用于表示Unity引擎的版本号,它的值为24(0x18)
HxD Hex Editor工具查看
using UnityEngine;
using UnityEditor;
using System.IO;
public static class EncryptEditor
{
[MenuItem("Tools/加密global-metadata.dat")]
public static void Encrypt()
{
string path = Application.dataPath + "/global-metadata.dat";
string key_char = "abcd ";
byte[] bytes = File.ReadAllBytes(path);
for (int i = 0; i < bytes.Length; i++)
{
int keyCharIndex = i % key_char.Length;
if (keyCharIndex == 4)
{
continue;
}
bytes[i] = (byte)(bytes[i] ^ key_char[i % key_char.Length]);
}
File.WriteAllBytes(path, bytes);
}
}
加密后
找到Unity工程安装目录
\Editor\Data\il2cpp\libil2cpp\vm\MetadataLoader.cpp (选择IL2CPP打包方式)
\Editor\Data\il2cpp\libmono\vm\MetadataLoader.cpp (选择Mono打包方式)
#include "il2cpp-config.h"
#include "MetadataLoader.h"
#include "os/File.h"
#include "os/Mutex.h"
#include "utils/MemoryMappedFile.h"
#include "utils/PathUtils.h"
#include "utils/Runtime.h"
#include "utils/Logging.h"
void* il2cpp::vm::MetadataLoader::LoadMetadataFile(const char* fileName)
{
std::string resourcesDirectory = utils::PathUtils::Combine(utils::Runtime::GetDataDir(), utils::StringView<char>("Metadata"));
std::string resourceFilePath = utils::PathUtils::Combine(resourcesDirectory, utils::StringView<char>(fileName, strlen(fileName)));
int error = 0;
os::FileHandle* handle = os::File::Open(resourceFilePath, kFileModeOpen, kFileAccessRead, kFileShareRead, kFileOptionsNone, &error);
if (error != 0)
{
utils::Logging::Write("ERROR: Could not open %s", resourceFilePath.c_str());
return NULL;
}
//解密相关改动 读取数据长度
int64_t length = 0;
int error2 = 0;
length = os::File::GetLength(handle, &error2);
//解密相关改动
void* fileBuffer = utils::MemoryMappedFile::Map(handle);
os::File::Close(handle, &error);
if (error != 0)
{
utils::MemoryMappedFile::Unmap(fileBuffer);
fileBuffer = NULL;
return NULL;
}
//解密相关改动 拷贝数据至data,解密data
char *data = (char*)malloc(length);
memcpy(data, fileBuffer, length);
void* result = utils::MemoryMappedFile::DecryptFile(data, length);
return result;
//解密相关改动
//注释源代码
//return fileBuffer;
}
\Editor\Data\il2cpp\libil2cpp\utils\MemoryMappedFile.cpp
void* MemoryMappedFile::DecryptFile(char* data, int64_t length)
{
char *result;
result = (char*)malloc(length);
char a[5] = "abcd";
for (int i = 0; i < length; i++)
{
result[i] = data[i] ^ a[i%5];
}
return static_cast<void*>(result);
}
\Editor\Data\il2cpp\libil2cpp\utils\MemoryMappedFile.h
#pragma once
#include <map>
#include "os/File.h"
#include "os/Mutex.h"
#include "os/MemoryMappedFile.h"
namespace il2cpp
{
namespace utils
{
class MemoryMappedFile
{
public:
static void* Map(os::FileHandle* file);
static void* Map(os::FileHandle* file, int64_t length, int64_t offset);
static void* Map(os::FileHandle* file, int64_t length, int64_t offset, int32_t access);
static void* DecryptFile(char* data, int64_t length);
static bool Unmap(void* address);
static bool Unmap(void* address, int64_t length);
};
}
}
修改完毕后,进行打包工程apk,得到test.apk,然后按如下流程进行反编译,加密,重签名打包。
使用APKTool库反编译test.apk得到test文件夹
apktool d test.apk -o test
进入test文件夹,将\test\assets\bin\Data\Managed\Metadata\global-metadata.dat挪到项目里加密,然后再覆盖test文件夹内的,接着进行重打包apk得到ex_test.apk
apktool b test -o ex_test.apk
使用Android Studio(3.5.1)创建keystore_test.jks,alias别名:key3 密码:123456
jarsigner -verbose -keystore keystore_test.jks -signedjar xxx_signed.apk ex_test.apk key3
使用zipalign对签名后的文件进行对齐
zipalign -p -f -v 4 xxx_signed.apk xxx_zipalign.apk
文件对齐后还需要使用apksigner,进行签名
apksigner sign --ks keystore_test.jks --ks-key-alias key3 --ks-pass pass:123456 --v2-signing-enabled true -v --out final.apk xxx_zipalign.apk
注意事项:
针对global-metadata.dat文件的加密和解密写法看着有点奇怪,是不是?
因为C++的char[]数组会末尾自动添加'\0'结束符
char a[5] = "abcd";
for (int i = 0; i < length; i++)
{
result[i] = data[i] ^ a[i%5];
}
上面这个操作,当i%5=4时,取到a[4]是'\0',data[i] ^ '\0' = data[i] 不会发生变化的,因此C#侧加密代码需要如下写法,当i%5=4时,我直接continue跳过,不对bytes[i]做处理。
string key_char = "abcd ";
for (int i = 0; i < bytes.Length; i++)
{
int keyCharIndex = i % key_char.Length;
if (keyCharIndex == 4)
{
continue;
}
bytes[i] = (byte)(bytes[i] ^ key_char[i % key_char.Length]);
}
1、如果apk无法安装,说明没有正常签名以及对齐文件再签名。
2、如果apk可以安装,但运行会闪退说明加密或解密操作出问题,或者真的是项目apk有问题,需确保apk没有解密操作情况下也能正常跑。 (也就是不要修改MetadataLoader.cpp文件 全部还原先)