首页
社区
课程
招聘
[原创]记录一次Unity加固的探索与实现
发表于: 2025-11-4 11:27 7710

[原创]记录一次Unity加固的探索与实现

2025-11-4 11:27
7710

正值某比赛出题,一道困难题不知道要怎么出才好,突然想起了il2cpp在安卓平台的加密,但是本人又不太会这方面,只好从学习一下il2cpp的原理并且尝试进行加固,本文记录我的出题过程。

要直到如何保护libil2cpp.so首先需要知道这个so文件是在什么时候被载入的,根据il2cpp安卓端的启动流程,我们可以发现载入位置,其流程图如下:

在此处我们可以看到libunity.so通过dlopen加载libil2cpp.so

光保护il2cpp.so大抵是不够的,很多加固方案肯定都会选择加密global-metadata.dat,接下来我们看看il2cpp.so中负责加载global-metadata.dat的代码位置吧,如下是加载metadata的流程图
file
源码分析可知加载global-metadata.dat的代码位置在

具体代码如下:

正如上文所提到的,加固global-metadata.dat主要是在

,理论上修改了此处的代码之后,再写一个脚本去加密metadata再打包回去,就可以运行了,但是如果直接修改MetadataLoader.cpp会导致后面如果不需要加固的项目每一次编译都需要加密global-metadata.dat才能运行,这样的话岂不是非常的不方便

自然,这里先说一下一个可能的解决方案,也是我在NSSCTF 4th中出过的一个pyinstaller打包项目加固的原理,我们可以通过设置一个标记,比如MHY0,我们再魔改MetadataLoader的时候通过识别是否存在MHY0这个标识符来确定是否需要解密,这样就不会影响后续打包的项目直接运行。

当然,还有更加优秀的办法,我们看到如下GitHub项目:
9ecK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6T1j5h3c8m8M7s2m8D9k6e0l9H3x3g2)9J5c8V1W2D9x3X3y4H3M7p5g2F1j5%4u0@1P5i4m8@1

我们在Unity Hub编写完主体代码之后就可以开始考虑加固了,接下来讲述一下Il2cppEncrtypt 这个项目构建时加固的原理。

Unity有一个很有意思的机制叫做Editor Scripting (Unity 编辑器扩展系统)暨所有放在 Assets/Editor/ 或任何以 Editor 命名的文件夹里的脚本,都会被编译进一个 编辑器专用的程序集(Editor Assembly),不会进入打包的游戏。

与此同时,Unity还存在IPostprocessBuildWithReport这个接口有什么用呢,来看一下介绍

IPostprocessBuildWithReport:Unity 的构建管线接口,OnPostprocessBuild 会在构建完成后自动被调用,参数是 BuildReport,包含构建结果、输出路径、平台等信息。

(上面的字那么多看的怪枯燥的吧,让GPT生成了一张图,润色一下,看个乐呵

同时在这个接口的上下文中,我们可以获取到打包的路径,从而进行对global-metadata.dat的加固

那么其实我们就可以扩展这个接口,并且重写OnPostprocessBuild,就可以实现在构建项目的时候一并完成对global-metadata.dat加固了。

以下代码摘自Il2cppEncrtypt:

诚然,如果我们直接使用Unity Hub编译一个可以直接运行的unity app的话默认使用的是Unity Editor中的代码,这个十分坑爹,也许是我不知道如何修改,反正最后试了很久也是没招了。

导出Unity 项目之后通过Android Studio直接编译:

导出项目在装好安卓的SDK之后,Build Setting界面,直接就可以看到导出了。

file

导出之后在导出目录下可以看到两个包launcher和unityLibrary,其中unityLibrary主要加载il2cpp的内容

file

同时我们检查一下Metadata是否加密
其位置在于

file

可以看到这里原本应该是有意义的symbol,但此刻变成了乱码,即Metadata成功被加密

接下来我们需要在项目中的loader写入解密代码,但你会发现Android studio 好像无法完美识别到关于il2cpp.so的代码,我们需要手动在

中修改,这里需要注意的是确保和我们加密代码一致,不然会导致崩溃。

如下代码中所示,主要的点也就是在拿到fileBuffer 之后做解密即可。

至此成功的完成了Metadata的加密,但是这够吗,显然远远不够,我们准备开始下一步探索,加密libil2cpp.so

在上文中提到了,libil2cpp.so 是被libunity.so加载的,但是我们在libUnity的编译脚本中可以看见似乎build.gradle是没有编译libunity.so的逻辑的,代码如下

同时我们编译一次项目也会发现JniLibs的目录出现了时间差

file

种种迹象也说明了libunity.so似乎不再我们可控范围内,经过搜索得知这个unity.so是核心引擎,属于Unity的闭源部分,因此我们没有办法通过修改unity.so来拦截il2cpp的加载从而实现动态解密。

既然无法从代码层面进行修改ilbunity.so,那么根据上文提到的il2cpp.so被加载流程,最后加载libunity.so肯定是要走dlopen的,那么是不是说我们只需要在这之前注册一个hook,但dlopen打开的是libil2cpp的时候我们进行加密呢,接下来我们开始尝试

这里的Hook有很多方法,Github已经开源了很多好用的Hook框架,我这里使用dobby hook
c9dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6B7L8i4m8W2N6%4y4Q4x3V1k6p5L8$3u0T1P5g2)9K6c8Y4c8S2j5W2)9K6c8s2u0W2j5h3c8E0k6g2)9J5k6r3!0$3i4K6u0V1k6X3W2D9k6b7`.`.

dobby hook编译好后使用非常的简单啊
我的建议是编译成静态链接库
libdobby.a,dobby.h
静态链接库编译之后会直接融入到编译出来的so中,这样不容易被看出来用了hook框架。

如何在项目中使用dobby hook呢?

首先将libdobby.a放到Jnilibs中,然后把dobby.h放到include中
接下来给一份我的Cmakelist.txt 自行理解一下,其中加入了对静态链接库符号的去除

编译完dobby hook之后我们会发现导出来的项目是没有自己的cpp代码的,我们需要自己添加

file

这一步Android Studio 会帮我们完成

但是 到了这一步 ,如果用Unity Editor目录里的NDK的话,编译的时候会各种报错,这个就非常离谱了,应为路径中带空格,并且Java 版本 以及ndk版本种种问题,导致在这一步卡了很久,但最后还是成功的编译出来了,(也许你不会出现这个BUG)

另外Cmake的版本也会影响,我在用高版本的Cmake的时候一直在报错,奇奇怪怪的,这里把launcher的build.gradle分享出来,大家遇到奇奇怪怪的报错也可以参考参考

接下来就是正式开始写Hook 代码了

我们在Unity palyer中需要加载我们用于加固的lib库,后通过JNI_Onload 或者 init_array 都可以,但JNI_Onload只有System.loadLibary才有效,如果是so中dlopen 比如自定义linker的话是需要自己给JNI_Onload传递JVM并且自己调用的,其实最推荐的还是卸载init_array,给我们的加载函数修饰成构造函数即可。

UnityPlayerActivity 中载入加固SO文件:
file

通过构造函数来调用Init_Hook:

使用 dobby hook 来 hook lib库的导出符号我们首先可以通过dlopen 这个lib库,然后通过dlsym来通过符号获取我们需要hook的导入函数的地址,随后初始化hook即可,这里展示一下我们hook libdl.so 获取dlopen 地址的办法。

Init_Hook 实现代码:

这里我们知道dlopen的参数格式:dlopen("path/to/libX.so", flags),似乎我们这样Hook只能够获取到加载so的路径,如果我们读取路径动态解密的话,解密的so就落地了,这并不是一种好方法,接下来我们思考解决这个问题的办法。

另外提一嘴,大多数加固厂商在此处可能就替换成自己的linker了,要写一个稳定的linker对于我目前的实力来说还是差点意思,所以这次时间我采用整体加密的方案,而不是用难度更高的linker。

言归正传我们要采用的技术为memfd(memfd 是内核提供的一种特殊文件描述符机制,它创建的文件不在磁盘上,而是在内存中。)

换句话说,这个文件是“存在于内存里的临时文件”,但是对系统来说它依然是一个合法的文件对象,可以被 dlopen 或 android_dlopen_ext 识别并加载。

memfd_create 返回的是一个文件描述符(fd),你可以对它 write 写数据、lseek、甚至 mmap。
当我们把解密后的 ELF 数据写进这个 memfd 之后,就等价于往一个真实文件里写入了一份 so。
而内核又在 /proc/self/fd/ 下提供了一个伪路径映射,比如 /proc/self/fd/37,指向这个 fd。
所以当我们在 dlopen 中传入这个路径时,loader 实际上是去读我们内存中的 ELF 数据,这样整个加载过程完全脱离了磁盘。

因此我们能在 Hook 的时候“偷梁换柱”——先拦截住目标 so 的加载,再把它替换成从 memfd 中加载。
在这个过程中,动态链接器完全不会察觉到区别,因为对它来说,只要能读到符合 ELF 格式的内容,它就能照常加载。

(老规矩,GPT来一张,方便理解

file

接下来是我的完整代码实现

至此,我们实现了一个简单的加密lib2cpp.so的功能,正如我如上代码,我们还加入了ELF头校验,这样我们在debug的时候一样可以运行。

这一步的话完全可以使用ollvm来编译,但是有没有更简单的呢,显然是有的

我们可以利用现成的项目
b84K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6S2j5K6y4K6M7K6m8J5i4K6u0r3L8$3u0X3N6i4y4Z5k6h3q4V1k6i4u0Q4x3X3g2Z5
通过头文件来对项目代码进行混淆

项目生成的源代码的位置在

我们直接导入obfusheader.h即可,但是这里可能obfusheader.h的部分写法会与你的编译版本冲突,需要人为修改的情况,这里你结合AI和报错看看哪些语法需要修改即可,多尝试几次。

至此也就完成了一个简单的Unity加固,从萌生这个想法到实现前前后后花了几天,上班连续时间比较短,但现在AI发展趋势感觉学习的成本越来越低了,中途也各种编译报错给我整的犯恶心过,但好在都解决了。

最后附上这道题的逆向过程吧

il2cpp的app尝试il2cpp dumper

file

直接报错

发现libil2cpp.so是被加密的

file

在just.so 中发现了是hook了dlpen

file

找到dobbyhook真实函数并给他命名

file

file

注册的hook在这里

file

逆向发现是rc4^0x33

file
密钥是这个

file

file

解密Libil2cpp.so

检查metadata可以发现是加密的

file
那么只能去il2cpp里面看了,那么metadata-loader(il2cpp官方源代码)中有一个字符串可以帮助我们定位逻辑

file

file

所以直接可以看到else后面的逻辑就是开始载入metadata了

file

看到解密逻辑,直接写解密脚本

file

解密后的改名回global-metadata.dat

然后使用il2cppdumper

file

dump成功载入符号,然后找到flagcheck,直接看逻辑

file

file

密文在这里下断点获取

file

file

自然Tea的key也需要调试获取或者frida Hook

file

但需要过Frida check

或者根据il2cpp特性

数据在这里初始化

file

file

4个int是tea key

40个byte是密文

C8E4E9E3F34C25560172B0D40B6DF4823260AA87EC6866054AA4691711E5D7BF

找到哈希

在il2cppdumper生成的dump.cs中查找

file

找到offset,然后直接静态dump

file

还原出来的加密逻辑就如下

写解密逻辑如下:

拿到flag:flag{unitygame_I5S0ooFunny_Isnotit?????}

至此全文完。

E:\Unity Edit\2020.3.48f1c1\Editor\Data\il2cpp\libil2cpp\vm\MetadataLoader.cpp
E:\Unity Edit\2020.3.48f1c1\Editor\Data\il2cpp\libil2cpp\vm\MetadataLoader.cpp
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;
}
 
void* fileBuffer = utils::MemoryMappedFile::Map(handle);
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;
}
 
void* fileBuffer = utils::MemoryMappedFile::Map(handle);
E:\Unity Edit\2020.3.48f1c1\Editor\Data\il2cpp\libil2cpp\vm\MetadataLoader.cpp
E:\Unity Edit\2020.3.48f1c1\Editor\Data\il2cpp\libil2cpp\vm\MetadataLoader.cpp
void IPostprocessBuildWithReport.OnPostprocessBuild( BuildReport report )
{
    SetDisplayLog( LogMessageFromCpp );
 
    if ( report.summary.platform == UnityEditor.BuildTarget.Android )
    {
        Debug.Log(report.summary.outputPath);
        EncryptionCode( Marshal.StringToHGlobalAnsi( report.summary.outputPath ) );
        OverrideLoader( Marshal.StringToHGlobalAnsi( report.summary.outputPath ) );
    }
    else if ( report.summary.platform == UnityEditor.BuildTarget.iOS )
    {
 
    }
    Debug.Log( "执行扩展程序完成" );
}
void IPostprocessBuildWithReport.OnPostprocessBuild( BuildReport report )
{
    SetDisplayLog( LogMessageFromCpp );
 
    if ( report.summary.platform == UnityEditor.BuildTarget.Android )
    {
        Debug.Log(report.summary.outputPath);
        EncryptionCode( Marshal.StringToHGlobalAnsi( report.summary.outputPath ) );
        OverrideLoader( Marshal.StringToHGlobalAnsi( report.summary.outputPath ) );
    }
    else if ( report.summary.platform == UnityEditor.BuildTarget.iOS )
    {
 
    }
    Debug.Log( "执行扩展程序完成" );
}
unityLibrary\src\main\assets\bin\Data\Managed\Metadata
unityLibrary\src\main\assets\bin\Data\Managed\Metadata
unityLibrary\src\main\Il2CppOutputProject\IL2CPP\libil2cpp\vm\MetadataLoader.cpp
unityLibrary\src\main\Il2CppOutputProject\IL2CPP\libil2cpp\vm\MetadataLoader.cpp
void *il2cpp::vm::MetadataLoader::LoadMetadataFile(const char *fileName)
{
#if IL2CPP_TARGET_ANDROID && IL2CPP_TINY_DEBUGGER && !IL2CPP_TINY_FROM_IL2CPP_BUILDER
    std::string resourcesDirectory = utils::PathUtils::Combine(utils::StringView<char>("Data"), utils::StringView<char>("Metadata"));
 
    std::string resourceFilePath = utils::PathUtils::Combine(resourcesDirectory, utils::StringView<char>(fileName, strlen(fileName)));
 
    int size = 0;
    return loadAsset(resourceFilePath.c_str(), &size, malloc);
#elif IL2CPP_TARGET_JAVASCRIPT && IL2CPP_TINY_DEBUGGER && !IL2CPP_TINY_FROM_IL2CPP_BUILDER
    return g_MetadataForWebTinyDebugger;
#else
    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;
    }
 
    void *fileBuffer = g_cacheFileHeader = utils::MemoryMappedFile::Map(handle);
 
    int ero;
    int64_t length = os::File::GetLength(handle, &ero);
    void *decBuffer = g_cacheDecodeHeader = PromiseAntiencryption(fileBuffer, length);
 
    os::File::Close(handle, &error);
    if (error != 0)
    {
        utils::MemoryMappedFile::Unmap(fileBuffer);
        fileBuffer = NULL;
        return NULL;
    }
 
    return decBuffer;
#endif
}
void *il2cpp::vm::MetadataLoader::LoadMetadataFile(const char *fileName)
{
#if IL2CPP_TARGET_ANDROID && IL2CPP_TINY_DEBUGGER && !IL2CPP_TINY_FROM_IL2CPP_BUILDER
    std::string resourcesDirectory = utils::PathUtils::Combine(utils::StringView<char>("Data"), utils::StringView<char>("Metadata"));
 
    std::string resourceFilePath = utils::PathUtils::Combine(resourcesDirectory, utils::StringView<char>(fileName, strlen(fileName)));
 
    int size = 0;
    return loadAsset(resourceFilePath.c_str(), &size, malloc);
#elif IL2CPP_TARGET_JAVASCRIPT && IL2CPP_TINY_DEBUGGER && !IL2CPP_TINY_FROM_IL2CPP_BUILDER
    return g_MetadataForWebTinyDebugger;
#else
    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;
    }
 
    void *fileBuffer = g_cacheFileHeader = utils::MemoryMappedFile::Map(handle);
 
    int ero;
    int64_t length = os::File::GetLength(handle, &ero);
    void *decBuffer = g_cacheDecodeHeader = PromiseAntiencryption(fileBuffer, length);
 
    os::File::Close(handle, &error);
    if (error != 0)
    {
        utils::MemoryMappedFile::Unmap(fileBuffer);
        fileBuffer = NULL;
        return NULL;
    }
 
    return decBuffer;
#endif
}
def BuildIl2Cpp(String workingDir, String targetDirectory, String architecture, String abi, String configuration) {
    exec {
        commandLine(workingDir + "/src/main/Il2CppOutputProject/IL2CPP/build/deploy/netcoreapp3.1/il2cpp.exe",
            "--compile-cpp",
            "--incremental-g-c-time-slice=3",
            "--avoid-dynamic-library-copy",
            "--profiler-report",
            "--libil2cpp-static",
            "--platform=Android",
            "--architecture=" + architecture,
            "--configuration=" + configuration,
            "--outputpath=" + workingDir + targetDirectory + abi + "/libil2cpp.so",
            "--cachedirectory=" + workingDir + "/build/il2cpp_"+ abi + "_" + configuration + "/il2cpp_cache",
            "--additional-include-directories=" + workingDir + "/src/main/Il2CppOutputProject/IL2CPP/external/bdwgc/include",
            "--additional-include-directories=" + workingDir + "/src/main/Il2CppOutputProject/IL2CPP/libil2cpp/include",
            "--tool-chain-path=" + android.ndkDirectory,
            "--map-file-parser=" + workingDir + "/src/main/Il2CppOutputProject/IL2CPP/MapFileParser/MapFileParser.exe",
            "--generatedcppdir=" + workingDir + "/src/main/Il2CppOutputProject/Source/il2cppOutput",
            "--baselib-directory=" + workingDir + "/src/main/jniStaticLibs/" + abi,
            "--dotnetprofile=unityaot")
        environment "ANDROID_SDK_ROOT", getSdkDir()
    }
    delete workingDir + targetDirectory + abi + "/libil2cpp.sym.so"
    ant.move(file: workingDir + targetDirectory + abi + "/libil2cpp.dbg.so", tofile: workingDir + "/symbols/" + abi + "/libil2cpp.so")
}
def BuildIl2Cpp(String workingDir, String targetDirectory, String architecture, String abi, String configuration) {
    exec {
        commandLine(workingDir + "/src/main/Il2CppOutputProject/IL2CPP/build/deploy/netcoreapp3.1/il2cpp.exe",
            "--compile-cpp",
            "--incremental-g-c-time-slice=3",
            "--avoid-dynamic-library-copy",
            "--profiler-report",
            "--libil2cpp-static",
            "--platform=Android",
            "--architecture=" + architecture,
            "--configuration=" + configuration,
            "--outputpath=" + workingDir + targetDirectory + abi + "/libil2cpp.so",
            "--cachedirectory=" + workingDir + "/build/il2cpp_"+ abi + "_" + configuration + "/il2cpp_cache",
            "--additional-include-directories=" + workingDir + "/src/main/Il2CppOutputProject/IL2CPP/external/bdwgc/include",
            "--additional-include-directories=" + workingDir + "/src/main/Il2CppOutputProject/IL2CPP/libil2cpp/include",
            "--tool-chain-path=" + android.ndkDirectory,
            "--map-file-parser=" + workingDir + "/src/main/Il2CppOutputProject/IL2CPP/MapFileParser/MapFileParser.exe",
            "--generatedcppdir=" + workingDir + "/src/main/Il2CppOutputProject/Source/il2cppOutput",
            "--baselib-directory=" + workingDir + "/src/main/jniStaticLibs/" + abi,
            "--dotnetprofile=unityaot")
        environment "ANDROID_SDK_ROOT", getSdkDir()
    }
    delete workingDir + targetDirectory + abi + "/libil2cpp.sym.so"
    ant.move(file: workingDir + targetDirectory + abi + "/libil2cpp.dbg.so", tofile: workingDir + "/symbols/" + abi + "/libil2cpp.so")
}
cmake_minimum_required(VERSION 3.10.2)
 
project("just")
 
# ========================
# 源文件
# ========================
add_library(${CMAKE_PROJECT_NAME} SHARED
        just.cpp
        detectFrida.cpp
        )
 
# ========================
# 包含路径
# ========================
include_directories(
        dobby
)
 
# ========================
# 导入静态库 libdobby.a
# ========================
add_library(local_dobby STATIC IMPORTED)
set_target_properties(local_dobby PROPERTIES
        IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/arm64-v8a/libdobby.a
        )
 
# ========================
# 编译优化与符号隐藏
# ========================
target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE
        -fvisibility=hidden
        -fvisibility-inlines-hidden
        -fdata-sections
        -ffunction-sections
        -O3
        )
 
# 宏:可用于标记显式导出的函数
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE EXPORT_SYMBOLS)
 
# 控制符号可见性
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
        CXX_VISIBILITY_PRESET hidden
        VISIBILITY_INLINES_HIDDEN ON
        POSITION_INDEPENDENT_CODE ON
        )
 
# ========================
# 生成 version script (控制导出符号)
# ========================
set(EXPORTS_FILE "${CMAKE_CURRENT_BINARY_DIR}/exports.map")
file(WRITE ${EXPORTS_FILE} "{
    global:
        Java_*;
        JNI_OnLoad;
        JNI_OnUnload;
    local:
        *;
};
")
 
# ========================
# 修正后的链接参数(去掉分号问题)
# ========================
set(MY_EXTRA_LINKER_FLAGS
        "-Wl,--version-script=${EXPORTS_FILE}"
        "-Wl,--exclude-libs,ALL"
        "-Wl,--gc-sections"
        "-s"
        )
 
# 把列表转换为空格分隔字符串(防止 CMake 用 ;)
string(REPLACE ";" " " MY_EXTRA_LINKER_FLAGS_STR "${MY_EXTRA_LINKER_FLAGS}")
 
# 追加到共享库链接参数
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${MY_EXTRA_LINKER_FLAGS_STR}")
 
message(STATUS "CMAKE_SHARED_LINKER_FLAGS = ${CMAKE_SHARED_LINKER_FLAGS}")
 
# ========================
# 链接阶段
# ========================
target_link_libraries(${CMAKE_PROJECT_NAME}
        android
        local_dobby
        log
        )
cmake_minimum_required(VERSION 3.10.2)
 
project("just")
 
# ========================
# 源文件
# ========================
add_library(${CMAKE_PROJECT_NAME} SHARED
        just.cpp
        detectFrida.cpp
        )
 
# ========================
# 包含路径
# ========================
include_directories(
        dobby
)
 
# ========================
# 导入静态库 libdobby.a
# ========================
add_library(local_dobby STATIC IMPORTED)
set_target_properties(local_dobby PROPERTIES
        IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/arm64-v8a/libdobby.a
        )
 
# ========================
# 编译优化与符号隐藏
# ========================
target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE
        -fvisibility=hidden
        -fvisibility-inlines-hidden
        -fdata-sections
        -ffunction-sections
        -O3
        )
 
# 宏:可用于标记显式导出的函数
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE EXPORT_SYMBOLS)
 
# 控制符号可见性
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
        CXX_VISIBILITY_PRESET hidden
        VISIBILITY_INLINES_HIDDEN ON
        POSITION_INDEPENDENT_CODE ON
        )
 
# ========================
# 生成 version script (控制导出符号)
# ========================
set(EXPORTS_FILE "${CMAKE_CURRENT_BINARY_DIR}/exports.map")
file(WRITE ${EXPORTS_FILE} "{
    global:
        Java_*;
        JNI_OnLoad;
        JNI_OnUnload;
    local:
        *;
};
")
 
# ========================
# 修正后的链接参数(去掉分号问题)
# ========================
set(MY_EXTRA_LINKER_FLAGS
        "-Wl,--version-script=${EXPORTS_FILE}"
        "-Wl,--exclude-libs,ALL"
        "-Wl,--gc-sections"
        "-s"
        )
 
# 把列表转换为空格分隔字符串(防止 CMake 用 ;)
string(REPLACE ";" " " MY_EXTRA_LINKER_FLAGS_STR "${MY_EXTRA_LINKER_FLAGS}")
 
# 追加到共享库链接参数
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${MY_EXTRA_LINKER_FLAGS_STR}")
 
message(STATUS "CMAKE_SHARED_LINKER_FLAGS = ${CMAKE_SHARED_LINKER_FLAGS}")
 
# ========================
# 链接阶段
# ========================
target_link_libraries(${CMAKE_PROJECT_NAME}
        android
        local_dobby
        log
        )
// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN
 
apply plugin: 'com.android.application'
 
dependencies {
    implementation project(':unityLibrary')
    }
 
android {
    compileSdkVersion 33
    buildToolsVersion '30.0.2'
 
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
 
    defaultConfig {
        minSdkVersion 28
        targetSdkVersion 33
        applicationId 'com.DefaultCompany.just'
        ndk {
            abiFilters 'arm64-v8a'
        }
        versionCode 1
        versionName '1.0'
    }
    externalNativeBuild {
        cmake {
            version "3.10.2"
            path = "src/main/cpp/CMakeLists.txt"
        }
    }
    aaptOptions {
        noCompress = ['.ress', '.resource', '.obb'] + unityStreamingAssets.tokenize(', ')
        ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~"
    }
 
    lintOptions {
        abortOnError false
    }
 
    buildTypes {
        debug {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt')
            signingConfig signingConfigs.debug
            jniDebuggable true
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt')
            signingConfig signingConfigs.debug
        }
    }
 
    packagingOptions {
        doNotStrip '*/arm64-v8a/*.so'
    }
 
    bundle {
        language {
            enableSplit = false
        }
        density {
            enableSplit = false
        }
        abi {
            enableSplit = true
        }
    }
}
// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN
 
apply plugin: 'com.android.application'
 
dependencies {
    implementation project(':unityLibrary')
    }
 
android {
    compileSdkVersion 33
    buildToolsVersion '30.0.2'
 
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
 
    defaultConfig {
        minSdkVersion 28
        targetSdkVersion 33
        applicationId 'com.DefaultCompany.just'
        ndk {
            abiFilters 'arm64-v8a'
        }
        versionCode 1
        versionName '1.0'
    }
    externalNativeBuild {
        cmake {
            version "3.10.2"
            path = "src/main/cpp/CMakeLists.txt"
        }
    }
    aaptOptions {
        noCompress = ['.ress', '.resource', '.obb'] + unityStreamingAssets.tokenize(', ')
        ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~"
    }
 
    lintOptions {
        abortOnError false
    }
 
    buildTypes {
        debug {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt')
            signingConfig signingConfigs.debug
            jniDebuggable true
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt')
            signingConfig signingConfigs.debug
        }
    }
 
    packagingOptions {
        doNotStrip '*/arm64-v8a/*.so'
    }
 
    bundle {
        language {
            enableSplit = false
        }
        density {
            enableSplit = false
        }
        abi {
            enableSplit = true
        }
    }
}
__attribute__((constructor))
static void OnLoad() {
    LOGI("libjust.so loaded — initializing hook...");
    InitHook();
}
__attribute__((constructor))
static void OnLoad() {
    LOGI("libjust.so loaded — initializing hook...");
    InitHook();
}
void* libdl = dlopen("libdl.so", RTLD_NOW);
if (!libdl) {
    LOGE("InitHook: dlopen(libdl.so) failed");
    return;
}
 
void* sym_dlopen = dlsym(libdl, "dlopen");
if (sym_dlopen) {
    if (DobbyHook(sym_dlopen, (void*)my_dlopen, (void**)&orig_dlopen) == 0) {
        LOGI("InitHook: Hooked dlopen");
    } else {
        LOGE("InitHook: DobbyHook dlopen failed");
    }
} else {
    LOGE("InitHook: dlsym dlopen failed");
}
void* libdl = dlopen("libdl.so", RTLD_NOW);
if (!libdl) {
    LOGE("InitHook: dlopen(libdl.so) failed");
    return;
}
 
void* sym_dlopen = dlsym(libdl, "dlopen");
if (sym_dlopen) {
    if (DobbyHook(sym_dlopen, (void*)my_dlopen, (void**)&orig_dlopen) == 0) {
        LOGI("InitHook: Hooked dlopen");
    } else {
        LOGE("InitHook: DobbyHook dlopen failed");
    }
} else {
    LOGE("InitHook: dlsym dlopen failed");
}
// 用于表示我们是否把解密数据写进了 memfd
struct PreparedMem {
    int fd;
    bool used_memfd;
    PreparedMem(): fd(-1), used_memfd(false) {}
};
 
// 当且仅当 filename 可直接 open(包含 '/' 或 access 可读)时,读取文件、RC4 解密并写入 memfd(不落地)
static PreparedMem prepare_memfd_if_local_path(const char* filename) {
    PreparedMem ret;
    if (!filename) return ret;
 
    bool has_slash = strchr(filename, '/') != nullptr;
    if (!has_slash) {
        if (access(filename, R_OK) != 0) {
            // basename 且不可直接访问 — 放过 loader 去寻找实际路径
            return ret;
        }
    }
 
    int fd = open(filename, O_RDONLY);
    if (fd < 0) {
     //   LOGE("prepare_memfd_if_local_path: open(%s) failed: %s", filename, strerror(errno));
        return ret;
    }
 
    uint8_t header[4] = {0};
    ssize_t rn = pread(fd, header, sizeof(header), 0);
    if (rn == (ssize_t)sizeof(header) && is_elf_header(header, sizeof(header))) {
        close(fd);
       // LOGI("prepare_memfd_if_local_path: %s is already ELF", filename);
        return ret;
    }
 
    struct stat st;
    if (fstat(fd, &st) != 0 || st.st_size <= 0) {
       // LOGE("prepare_memfd_if_local_path: fstat failed or zero-size for %s", filename);
        close(fd);
        return ret;
    }
 
    size_t size = (size_t)st.st_size;
    std::vector<uint8_t> buf(size);
    ssize_t got = pread(fd, buf.data(), size, 0);
    close(fd);
    if (got != (ssize_t)size) {
       // LOGE("prepare_memfd_if_local_path: read failed %zd/%zu", got, size);
        return ret;
    }
 
  //  LOGI("prepare_memfd_if_local_path: %s appears encrypted (len=%zu), decrypting to memfd...", filename, size);
    // 用纯 RC4 解密(与你 Python 脚本一致)
    rc4_crypt(buf.data(), buf.size(), (const uint8_t*)RC4_KEY, strlen(RC4_KEY));
 
    int memfd = try_memfd_create("dec_il2cpp");
    if (memfd < 0) {
       // LOGE("prepare_memfd_if_local_path: memfd_create failed");
        return ret;
    }
 
    ssize_t wrote = write(memfd, buf.data(), buf.size());
    if (wrote != (ssize_t)buf.size()) {
       // LOGE("prepare_memfd_if_local_path: write memfd failed %zd/%zu", wrote, buf.size());
        close(memfd);
        return ret;
    }
    lseek(memfd, 0, SEEK_SET);
 
    // 验证 memfd 首 4 字节是 ELF(仅作 debug 保证)
    uint8_t check_head[4] = {0};
    ssize_t rn2 = pread(memfd, check_head, sizeof(check_head), 0);
    if (rn2 == (ssize_t)sizeof(check_head)) {
        if (is_elf_header(check_head, 4)) {
           // LOGI("prepare_memfd_if_local_path: memfd contains valid ELF header");
        } else {
//            LOGE("prepare_memfd_if_local_path: memfd header NOT ELF: %02x %02x %02x %02x",
//                 check_head[0], check_head[1], check_head[2], check_head[3]);
            // 但仍继续让 loader尝试(以便打印错误),不立即关闭 memfd here.
        }
    }
 
    ret.fd = memfd;
    ret.used_memfd = true;
    LOGI("prepare_memfd_if_local_path: decrypted content written to memfd fd=%d", memfd);
    return ret;
}
 
// 使用 memfd 加载:优先使用 android_dlopen_ext + ANDROID_DLEXT_USE_LIBRARY_FD。
// 注意:不要在 loader 调用前关闭 fd,loader 返回后再 close。
static void* load_from_memfd(const char* orig_path, int flag, PreparedMem &pm) {
    if (!pm.used_memfd || pm.fd < 0) return nullptr;
    void* handle = nullptr;
 
    if (orig_android_dlopen_ext) {
        android_dlextinfo info;
        memset(&info, 0, sizeof(info));
        // 设置 flags 为请求从 fd 加载
        info.flags = ANDROID_DLEXT_USE_LIBRARY_FD;
        // 将 memfd 放进正确字段
#if defined(HAVE_ANDROID_DLEXT_H)
        // 当系统头已包含时字段布局与系统一致
        info.library_fd = pm.fd;
#else
        // 当使用我们的兼容 typedef 时同样写入 library_fd
        info.library_fd = pm.fd;
#endif
        handle = orig_android_dlopen_ext(orig_path, flag, &info);
        if (handle) {
            LOGI("load_from_memfd: loaded via android_dlopen_ext (fd=%d)", pm.fd);
        } else {
            LOGE("load_from_memfd: android_dlopen_ext failed: %s", dlerror());
        }
    } else if (orig_dlopen) {
        // fallback: dlopen("/proc/self/fd/N")
        char procpath[64];
        snprintf(procpath, sizeof(procpath), "/proc/self/fd/%d", pm.fd);
        handle = orig_dlopen(procpath, flag);
        if (handle) {
            LOGI("load_from_memfd: loaded via dlopen(procfd) (fd=%d)", pm.fd);
        } else {
            LOGE("load_from_memfd: dlopen(procfd) failed: %s", dlerror());
        }
    } else {
        LOGE("load_from_memfd: no original loader available");
    }
 
    return handle;
}
 
// Hooked dlopen
extern "C" void* my_dlopen(const char* filename, int flag) {
    LOGI("my_dlopen intercept: %s", filename ? filename : "NULL");
 
    PreparedMem pm = prepare_memfd_if_local_path(filename);
    void* handle = nullptr;
 
    if (pm.used_memfd && pm.fd >= 0) {
        handle = load_from_memfd(filename, flag, pm);
        // loader 返回后才 close fd
        close(pm.fd);
        pm.fd = -1;
        pm.used_memfd = false;
        if (handle) return handle;
    }
 
    if (orig_dlopen) {
        handle = orig_dlopen(filename, flag);
    } else {
        LOGE("my_dlopen: orig_dlopen is null");
    }
    return handle;
}
// 用于表示我们是否把解密数据写进了 memfd
struct PreparedMem {
    int fd;
    bool used_memfd;
    PreparedMem(): fd(-1), used_memfd(false) {}
};
 
// 当且仅当 filename 可直接 open(包含 '/' 或 access 可读)时,读取文件、RC4 解密并写入 memfd(不落地)
static PreparedMem prepare_memfd_if_local_path(const char* filename) {
    PreparedMem ret;
    if (!filename) return ret;
 
    bool has_slash = strchr(filename, '/') != nullptr;
    if (!has_slash) {
        if (access(filename, R_OK) != 0) {
            // basename 且不可直接访问 — 放过 loader 去寻找实际路径
            return ret;
        }
    }
 
    int fd = open(filename, O_RDONLY);
    if (fd < 0) {
     //   LOGE("prepare_memfd_if_local_path: open(%s) failed: %s", filename, strerror(errno));
        return ret;
    }
 
    uint8_t header[4] = {0};
    ssize_t rn = pread(fd, header, sizeof(header), 0);
    if (rn == (ssize_t)sizeof(header) && is_elf_header(header, sizeof(header))) {
        close(fd);
       // LOGI("prepare_memfd_if_local_path: %s is already ELF", filename);
        return ret;
    }
 
    struct stat st;
    if (fstat(fd, &st) != 0 || st.st_size <= 0) {
       // LOGE("prepare_memfd_if_local_path: fstat failed or zero-size for %s", filename);
        close(fd);
        return ret;
    }
 
    size_t size = (size_t)st.st_size;
    std::vector<uint8_t> buf(size);
    ssize_t got = pread(fd, buf.data(), size, 0);
    close(fd);
    if (got != (ssize_t)size) {
       // LOGE("prepare_memfd_if_local_path: read failed %zd/%zu", got, size);
        return ret;
    }
 
  //  LOGI("prepare_memfd_if_local_path: %s appears encrypted (len=%zu), decrypting to memfd...", filename, size);
    // 用纯 RC4 解密(与你 Python 脚本一致)
    rc4_crypt(buf.data(), buf.size(), (const uint8_t*)RC4_KEY, strlen(RC4_KEY));
 
    int memfd = try_memfd_create("dec_il2cpp");
    if (memfd < 0) {
       // LOGE("prepare_memfd_if_local_path: memfd_create failed");
        return ret;
    }
 
    ssize_t wrote = write(memfd, buf.data(), buf.size());
    if (wrote != (ssize_t)buf.size()) {
       // LOGE("prepare_memfd_if_local_path: write memfd failed %zd/%zu", wrote, buf.size());
        close(memfd);
        return ret;
    }
    lseek(memfd, 0, SEEK_SET);
 
    // 验证 memfd 首 4 字节是 ELF(仅作 debug 保证)
    uint8_t check_head[4] = {0};
    ssize_t rn2 = pread(memfd, check_head, sizeof(check_head), 0);
    if (rn2 == (ssize_t)sizeof(check_head)) {
        if (is_elf_header(check_head, 4)) {
           // LOGI("prepare_memfd_if_local_path: memfd contains valid ELF header");
        } else {
//            LOGE("prepare_memfd_if_local_path: memfd header NOT ELF: %02x %02x %02x %02x",
//                 check_head[0], check_head[1], check_head[2], check_head[3]);
            // 但仍继续让 loader尝试(以便打印错误),不立即关闭 memfd here.
        }
    }
 
    ret.fd = memfd;

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 53
支持
分享
最新回复 (30)
雪    币: 204
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
小弟膜拜膜拜你~
2025-11-4 11:31
0
雪    币: 7529
活跃值: (7613)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
3
进军unity
2025-11-4 11:44
0
雪    币: 228
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
强如swdd~
2025-11-4 11:49
0
雪    币: 7582
活跃值: (3401)
能力值: (RANK:166 )
在线值:
发帖
回帖
粉丝
5
学到啦!
2025-11-4 12:23
0
雪    币: 43
活跃值: (2579)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
66666
2025-11-4 14:02
0
雪    币: 6605
活跃值: (5640)
能力值: ( LV12,RANK:280 )
在线值:
发帖
回帖
粉丝
7
好文章!!!感謝分享
2025-11-4 14:03
0
雪    币: 3032
活跃值: (3884)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
学习一下
2025-11-4 14:57
0
雪    币: 396
活跃值: (2983)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
9
学习一下
2025-11-4 15:16
0
雪    币: 104
活跃值: (7159)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
tql
2025-11-4 15:57
0
雪    币: 71
活跃值: (1773)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
666
2025-11-4 16:39
0
雪    币: 2513
活跃值: (2234)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
12
狠狠学习
2025-11-4 18:19
0
雪    币: 5611
活跃值: (3919)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
666
2025-11-6 09:53
0
雪    币: 157
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
14
好文
2025-11-6 10:18
0
雪    币: 143
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
15
6666
2025-11-6 10:31
0
雪    币: 184
活跃值: (432)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16

学习学习

最后于 2025-11-6 16:06 被fei3ei编辑 ,原因:
2025-11-6 16:06
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
17
666
2025-11-6 16:26
0
雪    币: 717
活跃值: (1145)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
66666
2025-11-7 09:02
0
雪    币: 9
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
19
样本在哪里
2025-11-9 14:40
0
雪    币: 4170
活跃值: (4969)
能力值: ( LV12,RANK:250 )
在线值:
发帖
回帖
粉丝
20
mb_vhwdzyqo 样本在哪里
强网拟态Mobile方向Just赛题
2025-11-9 22:20
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
21
666
2025-11-10 09:53
0
雪    币: 279
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
22
学习
2025-11-10 11:47
0
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
23
66
2025-11-10 13:52
0
雪    币: 2
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
24

很实用!从构建阶段就考虑防护很专业,自动化加密和内存防泄密的设计也很巧妙。详细的技术分析可以参考思维导图!


最后于 2025-11-11 10:52 被mb_cizqyhuh编辑 ,原因:
2025-11-11 10:38
1
雪    币: 15
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
25
55555
2025-11-16 15:41
0
游客
登录 | 注册 方可回帖
返回