Original link: https://ksmeow.moe/unity-build-and-game-security/
Note: This article is a technical sharing made by the blogger in the student team. The blogger itself is not engaged in construction or security work, and has not been exposed to many related work cases. There are inferences and associations in many contents, and its feasibility needs to be verified in actual projects, so the content is only for readers’ reference.
Making independent games in the student team, the topic I want to share today is out of daily life. For our own packaging needs, the build options in Unity are actually sufficient, and the operation is very simple (except for pits such as the need to accurately match the small version of Android NDK). As for the two topics discussed today – Unity construction and client security, more application scenarios will be in large-scale games and online games. A good build system can improve the productivity of the production team, while security is used to maintain fairness in online games. Choosing this topic to share is just to broaden your horizons, and it does not require you to actually apply relevant knowledge and technology in independent games. I hope you know.
Unity Game Build
Build (build) is a common concept in software engineering. It usually refers to the process of compiling, integrating, and packaging a software project from source code and original resources, and finally obtaining files that can be directly run on the target platform. For example, for a C++ project, you can write a cmake file to specify the information and process required for the build, use it to compile, and then place the library files, resource files, etc. in the corresponding directory structure to complete a build.
Build pipelines and CI/CD
In typical web application projects, builds are often connected with the concept of CI/CD. CI/CD here actually refers to three things: continuous integration, continuous delivery, and continuous deployment. A good CI/CD can enable developers and testers to concentrate more on the work of the business itself in their respective scope of work, and automate inspection, testing, construction, delivery, etc., and hand them over to the CI/CD pipeline for execution. Notify the relevant personnel to intervene only when there is a problem with the process. Build pipelines are part of the CI/CD system concept.
Continuous Integration (CI) refers to automation for developers. This includes a series of checks, tests and branch management operations such as compile checks, configuration checks, unit tests, smoke tests, automatic merging of branches and conflict resolution, etc. Only the submissions that have passed this series of processes will enter the official branch, so as to ensure the health of the official branch.
Continuous delivery (CD) means that developers’ modifications will be automatically built into ready-to-deploy applications and tested. The tested version will be managed by the pipeline for deployment. Continuous deployment (another CD) refers to automatically deploying the completed version in the production environment for users to use. These automation measures are mainly to solve the complexity of the construction and deployment of large-scale projects, write these complex processes as construction and deployment scripts, and manage the configuration and products of complex work through pipelines, so as to save the energy of developers and improve work efficiency , and assist in the realization of agile development. Compared with CI, which assists the work during development, CD assists the work after the development is completed.
The core concept of CI/CD is the encapsulation and automation of complex work outside of business development, and the build pipeline plays an important role in this. The build pipeline is responsible for branch/version management, build deployment task distribution, build product management, automated testing, etc. In large-scale projects, there will be dedicated development and QA personnel to maintain and monitor the operation of the build pipeline to avoid workflow being blocked by the pipeline Fault blocking.
Before understanding the construction pipeline scheme of the Unity project, it is necessary to introduce the work that will be performed when the Unity project is built. This topic can start with the structure of the Unity game packaging product.
File structure of a Unity game
Taking a weekly build of the team project Common Brain as an example, the files included in the game are shown above. The files that can be seen at this level are
- LettersAdvProjectFiles.exe: The launcher of the game, the exe automatically generated by Unity
- UnityCrashHandler64.exe: A tool for reporting information when the game crashes
- UnityPlayer.dll: Unity engine library
- MonoBleedingEdge: Files needed for Mono to run
- LettersAdvProjectFiles_Data: the game’s own resources and program files
Under the LettersAdvProjectFiles_Data folder, you can also see the following files (only part of the screenshot is taken)
- Managed: Code and libraries written in C#, where Assembly-CSharp.dll contains the game logic, and the rest are used libraries
- Plugins: native libraries, such as libraries that are directly compiled into target platform machine codes such as C++, will appear here
- StreamingAssets: a folder with the same name in the same project directory
- The rest of the files are the packaged products of art, audio and other resources
Except for some files that every Unity game has (such as launcher exe, UnityPlayer.dll), the rest can be roughly divided into two parts: code and resources. Next, we will introduce the construction process of code and resources respectively.
Construction of the C# code
How the C# code works
Before understanding the construction of C# code, we need to introduce how the C# code runs. Although C# is also a compiled language, it does not directly compile to the machine code of the target platform, but requires a runtime environment similar to the JVM to run.
The C# code will first be compiled into an intermediate language (IL, intermediate language) by the C# compiler, and this intermediate language can be quickly converted into the machine code of the running platform during actual runtime. The program products expressed in the intermediate language still use extensions such as .exe and .dll, which look like normal executable programs or libraries, but to run them, the assistance of the runtime environment is required.
C# belongs to the .NET family of languages and uses the common runtime environment CLR (Common Language Runtime) of the .NET family. There are actually multiple implementations of the CLR. Microsoft’s official implementation is called the .NET Framework, and Unity also uses Mono. These runtime environments actually need to perform a conversion from IL to machine code, which is also a compilation process, so it is called JIT compilation (Just-in-Time compilation).
The figure above shows a program and library written in C# in action. During the running process, the runtime will compile IL into machine code, write the product into a new memory area, and then make the CPU execute the product machine code.
Mono (JIT)
Currently, the C# runtime environment used by Unity is Mono. As mentioned above, this is a way of JIT compilation. Mono is an open source, cross-platform CLR implementation. When Unity began to use C# as a scripting language, Microsoft’s .NET Framework was not yet open source and cross-platform, and could only run on the Windows platform. For Unity’s multi-platform build ability, Mono was chosen as the JIT scripting backend for C#.
Like the .NET Framework, Mono also has its own set of build toolchains that generate ECMA-compliant CIL products. In addition, tool libraries such as Mono.Cecil can also be used to analyze Mono CIL products, such as obtaining type and function information, modifying and generating code, etc.
The CIL code of the JIT is converted into machine code at runtime, so after the software is built, the actual distribution is the CIL binary file, which will bring us some convenience
- The same CIL binary can be used for different platforms, as long as the platform has the corresponding CLR to run
- When building on multiple platforms, C# can be compiled to CIL only once, and products of different platforms can be generated by simply modifying the structure of the binary file, saving construction time
- Compared with machine code, CIL is more convenient for static analysis, code modification and generation, and can easily implement injection management, etc.
But these conveniences can also come with inconveniences, such as
- Compiling the CLR to machine code at runtime consumes a certain amount of CPU time, which may result in poor performance
- The distributed CIL files are easily reversed and modified by users, affecting the normal operation of the game, or easily used to make plug-ins, etc.
Since performance and security are usually high priorities, Mono is not widely used in commercial games, and is more seen in development environments (development builds) or small indie games, etc.
IL2CPP (AOT)
In addition to Mono, Unity also provides an alternative C# scripting backend, which is IL2CPP. This name includes two names of IL and CPP. You can guess from the name. Its working method is to convert CIL into C++ code, and then compile it directly to the machine code of the target platform.
First of all, you may think about such a question, how to turn C# code into CIL? Remember our Mono, you can directly use the C# compiler in the Mono toolchain to compile the source code to CIL. Therefore, IL2CPP itself is an AOT compiler (Ahead-of-Time compiler), which can convert CIL into machine code before it actually runs, but the intermediate process is to convert it into C++ code first. The position of IL2CPP in the construction process can be understood from the following figure
Unlike Mono, the built product no longer contains any CIL code, but is compiled directly to machine code. Therefore, there is no Managed folder under the XXX_Data folder of the game build to save all CIL .dll files. Instead, there is GameAssenbly.dll in the root directory, which is the machine code library file compiled after converting CIL to C++.
However, C# still needs to provide functions such as reflection and GC at runtime, so IL2CPP products also need to include a part of the way to provide such functions, which is jointly completed by baselib.dll and XXX_Data/il2cpp_data folders.
The benefits of AOT compilation basically correspond to defects and JIT, and its benefits include
- The final distributed file is the machine code binary file of the target platform, which can be run directly without the performance loss caused by JIT compilation
- Native binary files are more difficult to reverse, and can be further processed by packing and other methods to enhance the binary security of the game
while the flaws include
- Since the game logic is also compiled into the machine code of the target platform, if a build package needs to run on multiple platforms, the corresponding binary files need to be provided separately, which will increase the size of the package body, which is important when building an Android apk file very obvious
- If you use IL2CPP to compile to machine code during development, it will become difficult to modify the C# code; but this can be achieved by adding a modification phase during the construction process and completing the injection of CIL before the IL2CPP phase. Effect
- Since IL2CPP actually adds several stages to compile to machine code after the Mono build, this will make a single build time longer
Construction of resources such as art and sound effects
Games are much more than source code. The number of resources in a game project is far greater than the number of codes. The resources themselves also have complex types, formats, and construction processes. Therefore, resource management and construction are more complicated than codes. The construction of resources mainly refers to the organization and packaging of resources. Unity provides a variety of ways to load resources, and their construction is different. Therefore, the construction of resources will be introduced according to the way Unity loads resources.
Directly referenced by GUID
In Unity’s Inspector, you can directly drag a resource file into a slot, at which point the scene creates a GUID reference to a resource file. The resources referenced in this way will be directly loaded into memory when the scene is opened. Therefore, if all resources in a scene are configured in this way, it will take a long time to open the scene, and all resources will be loaded at the beginning , it will not be unloaded until the scene is closed.
The information of the scene itself will be packaged into a levelX file, and the resources referenced by the scene may be packaged in files such as resources.assets/sharedassetsX.assets.
Resources class
If resources are placed in the Assets/Resources folder, they can be dynamically loaded through the Resource.Load method, which is the easiest way for Unity to dynamically load resources. These resources are kept in the resources.assets file, so a project’s resources.assets file can be quite large.
The above figure shows the structure of the resources.assets file, which is actually a package of several resources. The file contains continuous resource blocks. The file header indicates the block corresponding to each resource, so the corresponding block can be indexed through the file path. block, and then read resources from the block into memory.
The packaging of these resources is automatically completed during the Unity build process. Unity does not provide an interface to modify the build process, so it is difficult to control the resource build from the script level.
Asset Bundle
Asset Bundle (hereinafter referred to as Asb) is another way provided by Unity to load resources from files. Compared with Resources, the biggest advantage of Asb is controllability. When organizing resources, you can select the Asb corresponding to the resource in the Inspector. Asb files can be built from scripts via BuildPipeline.BuildAssetBundles. When loading Asb, you only need to provide the file path. You can build the game body and Asb separately, and put Asb into StreamingAssets when publishing, so that the game body can read Asb. In addition, Asb can also be distributed directly through the network, and the game body can be downloaded to StreamingAssets and then read. The encryption of Asb is also easy to handle. Unity provides an interface to load Asb from the stream, so only the decrypted stream is provided, and Unity can still load Asb as usual.
Asb has a file structure similar to resources.assets, but an Asb package actually contains multiple files, and the serialized data and some original resources are separated. Asb also supports loading only part of the resources, and it also has file header information similar to resources.assets.
Most large-scale games will choose the resource management and construction method of Asb, because it can be built separately from the game body, which is convenient for distribution and update, which can reduce the minimum package size of the game, so that more resources can be distributed through the network, and players You can also choose the quantity and quality of the downloaded resources.
value, configuration
Values and configurations are usually directly configured in scenes, Prefabs or ScriptableObjects during the development of interesting projects, and can be packaged together with Unity references or Asbs.
We tried to introduce numerical files managed by Excel tables in the team project Emoji , and provided tools such as exporting values from Excel tables, generating data classes and serialization codes for Excel rows.
For these self-implemented numerical constructions, you can export the configuration before packaging, or customize the packaging process, and add this link before packaging. The self-management of numerical configuration is also convenient for updating operations such as numerical patch in commercial games. In addition, a numerical verification link can be added to the Excel table or the construction process to prevent developers from submitting wrong numerical configurations and making the project unable to run normally.
Unity game build pipeline
Unity provides a build pipeline interface, the BuildPipeline class. In addition to opening the build window in the editor, you can also use the pipeline interface to customize the build script, and separate the build configuration into a configuration file for switching.
Implementing build scripts by yourself usually has the following categories and steps
- Build the game ontology
- read build configuration
- Export the resources required by the game body, such as values, minimum package resources, etc.
- code obfuscation etc.
- BuildPipeline. BuildPlayer
- Binary packing protection, etc.
- Run smoke tests on base game and recent Asb builds
- If the smoke passes, upload the build to the build management platform
- Upload debug files to the build management platform
- Build Asb
- read build configuration
- Export the resources required by Asb
- BuildPipeline. BuildAssetBundles
- If there is a self-designed Asb encryption or regrouping strategy, execute the corresponding packaging process
- Run smoke tests on Asb builds and recent ontology builds
- If the smoke passes, upload the build to the build management platform
Once the build scripts and build configurations are implemented, the build pipeline can be configured using a CI/CD platform such as Jenkins, TeamCity, etc. However, unlike typical application CI/CD pipelines, a build of a game takes too long, with large projects often taking up to 10 hours or more to complete a full build. This makes the CI/CD work of the game difficult to perform at high frequency, and the efficiency improvement effect brought by automation work is greatly reduced.
A game’s CI solution usually consists of stronger checks and weaker tests, since work such as numerical checks, compilation checks, etc. may be completed in minutes, but it may take longer to start the game for testing, and one commit If you cannot enter the project through the CI process in a short period of time, there will be a high probability of conflicts, and developers’ time will be wasted in dealing with conflicts. One solution is that submitting automatically triggered pipeline tasks only includes inspection, and automated testing is not a necessary condition for submission, but when a version of the test fails to check the revision of the version. The inspection task of the pipeline can be triggered by the submission trigger of the project management platform. The mainstream version management systems (such as Git, Perforce, etc.) all have the function of triggering scripts before and after submission.
While the development team could modify and package the game’s build process into easy-to-use build scripts, the cost of a single build was too high to run consistently. In addition, if the target of the build is a mobile device, there may be special build platform requirements, such as iOS applications need to be built on a Mac, which increases the number and types of machines managed by the build pipeline. These complexities make the construction of the game can only be triggered manually in the end, and its use is limited in the team. Otherwise, no matter how large-scale the team has a construction cluster, it is difficult to meet the needs of developers and requirements for construction. Therefore, compared to the automatic triggering and high-frequency operation of the CI process, the game CD pipeline has to choose a strategy of manual triggering and on-demand operation due to time-consuming reasons.
In a student team like Ice Rock Workshop, we don’t have the ability to build such a robust build pipeline. However, a game that can’t build every day is in big trouble, we can still build it manually at a certain frequency, continuously package, test and provide it to players for trial play. This is also a spur to the development team, allowing us to produce and accept content at fixed time intervals to maintain the vitality of the project.
Unity Game Client Security
Client security is not a problem that only games face. Desktop applications and mobile applications also face the same problem. For ordinary applications, the security requirements mainly focus on anti-crawlers, anti-reversing, anti-privacy leaks, etc.; for games, in addition to these requirements, security is more prominently reflected in the game experience issues, and plug-ins and other game-specific The client-side attack tool severely damaged the player’s gaming experience. Just imagine, if you are a Battlefield 1 (Battlefield 1) player, you are enjoying the fun of the game, and the result is output by the plug-in dog riding your face, which makes you no longer able to have fun in this game, so you start to spit EA plays dead and does not deal with cheating issues.
Looking at security issues from the perspective of the development team is more of a question of cost-effectiveness. For example, for stand-alone games where PVP is weak or basically non-existent, the security requirement is mainly anti-cracking/anti-piracy, and the harm of plug-ins that modify game data is very small; but for games with strong competition, a data modification/anti-piracy The plug-in of the auxiliary game will seriously damage the fairness of the game. It is necessary to add security measures for reading/modifying memory in the client, but the anti-cheating of this kind of PVP game can also start from the perspective of the server.
This sharing mainly introduces some strategies/measures for client security. The application scenarios are different, and the specific implementation measures need to be determined according to the actual situation of each project.
Anti-reverse: Packing
Most of the game logic developed by Unity is written in C#, and mechanisms such as CIL or reflection in C# can easily bring convenience to attackers. Therefore, in order to enhance the security of the Unity game client, the IL2CPP scripting backend is usually used and the binary files are packed.
Packing refers to compressing, segmenting, or adding unreadable or redundant logic to the original program code, so that the original program code can run normally, while the results obtained by disassembly are confusing to a certain extent, increasing the reverse work difficulty.
VMP shell (VMProtect) is a packing protection scheme that transfers the operation of the original program code to the VM implemented by the shell. VMP 2 will convert the original x86 assembly instructions to the RISC instruction set that the shell VM can run, and encrypt the operands of the instructions, so as to achieve the purpose of obfuscating the original assembly code. After packing, the reverse work must start from the VM itself to find the entry point, and study the translated RISC instructions, rather than simply reverse the x86 instructions. Due to the large amount of new VM-related logic that needs to be added, the VMP shell can bloat the size of the binary many times and introduce additional runtime overhead.
UPX shell (Ultimate Packer for eXecutables) is a packing protection scheme that compresses the original program code and adds decompression logic to the program. After UPX is packed, the program code will be compressed by a compression algorithm, which will be quite different from the original content at the binary level. For reverse work, the decompression algorithm must be studied first, and the program can only be analyzed after decompressing the original program code. Some security can be gained if the compression/decompression algorithm is adjusted such that it is difficult to reverse the algorithm. The UPX shell will reduce the size of the binary file, but it will also bring additional runtime overhead and reduce program performance.
Anti-reversing: obfuscation
Binary files generated by IL2CPP can be protected by packing, but the metadata that C#’s reflection mechanism relies on will also leave a reverse breakthrough for attackers. Code obfuscation during build is one solution.
In the actual logic, there are not many classes and fields involved in reflection, and most class names, variable names, and function names can be modified arbitrarily, so confusion can be made—that is, replacing the original meaningful name with a name that does not have An actual random string. During the obfuscation process, the obfuscator will maintain a name mapping table to find and replace the names in the code one by one. The readability of the replaced program code is greatly reduced, and the names in the metadata become illegible, and only some names are used for reflection. Retained, increasing the difficulty of reverse.
Anti-reversing: modifying the virtual machine
Languages such as C# and Lua run in a virtual machine, and the generated IL can be different from the conventionally used one by modifying the virtual machine, increasing the difficulty of reverse engineering. A common modification method is to change the opcode of the IL instruction. After the modification, the attacker can only guess the corresponding relationship between the opcode and the actual instruction through the characteristics. This work will consume a lot of time and increase the difficulty of reverse engineering.
Anti-hijacking: file integrity check
Attackers can replace some of the library files that the game relies on to run with modified versions by means of file replacement, thereby adding attack logic to the interface, hijacking game logic, or obtaining resource data from the game, etc.
You can add a file integrity check when the game starts to analyze whether the files to be used have been modified. For example, add a checksum check when distributing, and obtain the checksum of the file online. If the file has been modified, download the original file online and start the game.
Anti-unpacking: custom resource encryption method
Adding a custom resource packaging and encryption method during the packaging process, instead of directly using a general or common encryption method, can increase the difficulty for attackers to analyze the package body structure, prevent the client resource package from being easily cracked, and the original resources are leaked and stolen.
contradictory security work
There are many contradictions in the security work of the game. For example, the binary-related security measures have too much impact on the game software or affect the performance of the game. It is difficult to identify the clear watermark and blind watermark due to the leakage of the picture and the door lock. Design and verify the encryption scheme The cycle is very long, but the attacker may crack the encryption scheme faster (after all, the attacker has too much time than the worker), and so on.
The ultimate goal of security measures can be difficult to determine to prevent attacks from happening, and it is even more difficult to do so for game projects. Therefore, the actual implementation of security must consider cost-effectiveness and feasibility issues. For example, the watermark on the resource is enough to prevent most resources from being stolen, and the anti-reverse measures are enough to prevent most players from modifying the client. It can be considered that the purpose of the security solution has been achieved.
In addition, the security solution is not only carried out on the technical level, but on the client side. For example, for networked game clients, data verification can also be performed on the server, or actively identify cheating players and modified clients; for resources that need to be kept secret, sign a confidentiality agreement with relevant personnel to clarify confidentiality obligations, and Use legal means to solve the problem at an appropriate time.
epilogue
This sharing involves some topics that are difficult to touch in daily game development of interest projects. It is intended to broaden your horizons and provide more horizontal knowledge for understanding game development work. I am not an expert engaged in these tasks, and my understanding of relevant knowledge is relatively shallow. If you have any mistakes or suggestions, welcome to communicate.
The ongoing projects of the Bingyan Workshop game team include the Brain mentioned in this article, Emoji (code name) and the Newland Hub that has been launched on TapTap (there is a link in the reference material!), welcome everyone to support our game projects. Emoji, which the author is participating in, is a 3D platform jumping game. It is currently in the prototype stage of development, and I look forward to meeting you in the future.
References
Some of the materials and pictures used in this article come from the Internet, thanks to the original author for sharing.
- Mike McShaffry, David Graham, Game Coding Complete (Fourth Edition), Course Technology
- Ret Hat, What is CI/CD?. https://www.redhat.com/en/topics/devops/what-is-ci-cd
- Kristijan Crnac (dev.to), .NET compilation process explained (C#), https://dev.to/kcrnac/net-execution-process-explained-c-1b7a
- Mono Project, https://www.mono-project.com/
- Josh Peterson (Unity), An introduction to IL2CPP internals, https://blog.unity.com/engine-platform/an-introduction-to-ilcpp-internals
- imadr (GitHub), Unity Game Hacking Guide, https://github.com/imadr/Unity-game-hacking
- Ryan Caltabiano (Unity), Asset Bundles vs. Resources: A Memory Showdown, https://blog.unity.com/technology/asset-bundles-vs-resources-a-memory-showdown
- Zhihu, how to evaluate the recent Battlefield 1 external large-scale bombing incident? , https://www.zhihu.com/question/578918874
- IDontCode (Back Engineering), VMProtect 2 – Detailed Analysis of the Virtual Machine Architecture, https://back.engineering/17/05/2021/
- TapTap, Newland Hub, https://www.taptap.cn/app/287001
The post Unity Build and Client Security first appeared on KSkun’s Blog .
This article is transferred from: https://ksmeow.moe/unity-build-and-game-security/
This site is only for collection, and the copyright belongs to the original author.