IDA Pro Reverse Engineering Guide - Unity IL2CPP (C++) & Non-Unity Games

Wait 5 sec.

Unity & Non-Unity Game Reverse EngineeringDifficulty: Beginner / Intermediate Platform: iOS / ARM64 Tools: IDA Pro, dnSpy / ILSpy, IL2CPP Dumper, LLDB IntroductionI've spent quite a bit of time messing around with IDA Pro and reverse engineering games, so I figured I'd put together a guide covering some of the stuff I've learned. This is mainly for people who are new to IDA and don't really know where to start once they've opened a game in it. I'll be covering both Unity and non-Unity games, including how to find functions, understand addresses, read pseudocode, follow function calls, and work out what certain parts of the game are doing. Most of my examples will be based on iOS and ARM64, but a lot of the IDA basics apply to other platforms too. I'm still learning myself, so if anyone notices something I've missed or explained incorrectly, feel free to correct me. Quick note: I used Grammarly to mass-fix the spelling and grammar throughout this guide because my typing is all over the place LOL. The information itself is based on what I've learned while working with IDA. 1. What You'll NeedYou don't need every tool listed here. It depends on the game you're working with. ToolWhat it's used forIDA ProAnalyzing native game codednSpy / ILSpyBrowsing managed C# assembliesIL2CPP DumperRecovering Unity IL2CPP metadataLLDBDebugging native applicationsHex EditorViewing binary dataARM64 AssemblerUnderstanding and assembling ARM64 instructionsFor Unity IL2CPP games, you'll normally use dnSpy or ILSpy alongside reconstructed assemblies and IDA Pro. For non-Unity games, you might only need IDA and sometimes a debugger. I'd also recommend keeping an untouched backup of any binary you're analyzing. 2. Unity vs Non-Unity GamesBefore doing anything, it's worth finding out what engine the game uses. Different engines store their code and metadata differently. Game typeWhat you might findUnity IL2CPPUnityFramework, global-metadata.datUnity MonoManaged .dll assembliesUnreal EngineUnreal symbols, reflection data, .pak / .ucas filesCocos2d-xcocos2d classes and C++ symbolsObjective-CObjective-C classes and selectorsSwiftSwift-mangled symbolsCustom C/C++Native functions without an obvious engineThese are only clues. A game might strip symbols, rename files, or use several frameworks. What's the difference?Unity IL2CPP You can often recover class names, methods, and fields from the game's metadata. This makes it easier to locate functions in IDA. Non-Unity You might not have any reconstructed assemblies or method lists. Instead, you'll rely more on strings, imports, symbols, cross-references, and native function analysis. Both can be analyzed in IDA Pro. 3. Getting Started with IDA ProOpen the game's native executable or framework in IDA. For iOS games, you'll normally be working with a Mach-O binary. Let IDA finish its initial analysis before jumping around. Useful IDA shortcutsShortcutWhat it doesGJump to an addressF5Open pseudocodeXShow cross-referencesShift + F3Open Functions windowShift + F12Open Strings windowSpaceSwitch between graph and text viewsNRename a symbolYChange a type;Add a commentSome shortcuts can vary depending on your version or keyboard configuration. The main windowsFunctions Shows functions IDA has identified. Strings Shows recognized strings found inside the binary. Imports Shows functions imported from other libraries. Disassembly Shows the native machine instructions. Pseudocode Shows IDA's C-like interpretation of the machine code, if you have a supported decompiler. You don't need to understand everything immediately. I'd recommend getting comfortable with the Functions, Strings, and Pseudocode windows first. 4. What Do Names Like sub_100123450 Mean?You'll probably see a lot of names like these: sub_100123450sub_100123A80sub_100124C10These are names IDA generates when it doesn't know the original function names. For example: sub_100123450Breaking it down: sub_ - Short for subroutine, basically a function. 100123450 - The function's address in hexadecimal. 0x100123450 - The same address written with a hexadecimal prefix. The name doesn't tell you what the function actually does. It also doesn't automatically mean the function is obfuscated. Other common IDA namesNameMeaningsub_Functionloc_Location or branch targetlocret_Return locationoff_Data containing an address or pointerbyte_8-bit dataword_16-bit datadword_32-bit dataqword_64-bit dataunk_Unknown or undefined dataasc_String dataVariables and registersNameMeaninga1, a2Decompiler-generated argument namesv1, v2Decompiler-generated local variablesX0 to X30ARM64 64-bit registersW0 to W30Lower 32-bit views of X registersSPStack pointerPCProgram counterLRLink register, usually X30XZRZero registerRenaming functionsLet's say you find: __int64 __fastcall sub_100123450(__int64 a1){ return sub_100124000(a1);}After looking through the code, you work out that the function loads player data. You can press N and rename it to something like: LoadPlayerDataThat makes it easier to understand when you come across it again. Just don't rename everything based on guesses. 5. Unity IL2CPP ExplainedUnity games commonly use Mono or IL2CPP. With Mono, managed assemblies contain intermediate language that tools like dnSpy can inspect. With IL2CPP, Unity converts the managed C# code into C++ and compiles it into native machine code. This means the original managed method bodies aren't normally available in reconstructed DummyDll files. Important Unity filesDummyDll Contains reconstructed class names, fields, methods, and other metadata. global-metadata.dat Contains IL2CPP metadata describing managed types and methods. UnityFramework / native executable Contains the compiled native code. How they work togetherThe reconstructed assemblies help you find a method. IDA helps you understand its native implementation. For example, you might find a method called: public void AddMoney(double amount){}You can investigate the method further in IDA or test its behavior using tools such as iGameGod. However, finding a method named AddMoney doesn't automatically mean you've found the code responsible for changing the actual balance. It might only update the interface or call another function. That's why it's worth inspecting what the method actually does. 6. Finding Unity Methods with dnSpyOnce you've generated your reconstructed assemblies, open them in dnSpy or ILSpy. Search for the class or method you're interested in. For example: [Token(Token = "0x6001234")][Address( RVA = "0x123456", Offset = "0x123456", VA = "0x123456")]public void AddMoney(double amount){}Note: These addresses are only examples. They aren't real offsets from a specific game. Actual RVA, VA, and file-offset values can differ. The empty body is expected with reconstructed IL2CPP assemblies. What you're interested inClass name Namespace Method name Parameters Return type RVA / VA Other related methods Moving from dnSpy to IDAOnce you've found a method: Check its RVA or VA. Make sure you're using the matching native binary. Open that binary in IDA. Press G to jump to the corresponding address. Press F5 to inspect the pseudocode. Depending on how your dump was generated, you may need to calculate the correct IDA address. Don't assume the displayed RVA and VA are always interchangeable. 7. Understanding RVA, VA and OffsetsThese are worth learning properly because you'll use them constantly. TermMeaningRVARelative Virtual AddressVAVirtual AddressFile OffsetLocation inside the binary fileImage BaseBase address used to map the binaryASLR SlideRuntime address adjustmentExampleImage Base: 0x100000000RVA: 0x00123456VA: 0x100123456The basic relationship is: VA = Image Base + RVAFor example: 0x100000000 + 0x123456 = 0x100123456What about iOS?iOS uses ASLR, so the actual runtime address may differ from the static address shown in IDA. For a typical Mach-O image: Runtime Address = Unslid VA + ASLR SlideThe file offset is another thing entirely. It's the position of bytes inside the file, and you may need the binary's segment mapping to convert between file offsets and virtual addresses. Also remember that different frameworks have their own address spaces and mappings. 8. Reading Pseudocode in IDAThis is one of the most useful parts of IDA. Press F5 while viewing a function. If you have a compatible decompiler, IDA will show a C-like version of the machine code. For example: void ShowMenu(void* instance){ void* gameObject = GetGameObject(instance); if (gameObject == nullptr) return; SetActive(gameObject, true);}This is fairly easy to understand. The function gets an object, checks whether it exists, and activates it. But most of the time, you won't get such nice names. You might see: __int64 __fastcall sub_123456(__int64 a1){ __int64 v2; v2 = sub_654321(a1); if (v2 == 0) return 0; return sub_789ABC(v2);}Now you've got a bunch of unnamed functions and variables. What I normally look atFirst, I check what arguments the function receives. Then I look at any conditions, comparisons, or early returns. After that, I follow the functions being called. If I figure out what something does, I'll rename it or add a comment. You can also press Y to change types, which sometimes makes the pseudocode much easier to read. Just remember that pseudocode isn't the original C++ source. It's IDA's reconstruction of the native instructions. 9. Understanding ARM64Most modern iOS games use ARM64 or ARM64e. Here are some instructions you'll regularly see. InstructionMeaningMOVMove a valueLDRLoad from memorySTRStore to memoryADDAddSUBSubtractCMPCompareBBranchBLCall a functionBRBranch to an address in a registerBLRCall an address in a registerCBZBranch if zeroCBNZBranch if not zeroTBZTest a bit and branch if zeroTBNZTest a bit and branch if nonzeroRETReturnNOPNo operationSTPStore a register pairLDPLoad a register pairADRPCalculate a page-relative addressExampleMOV X0, X19MOV X1, #0BL ExampleFunctionHere, values are being placed into registers before another function is called. ARM64 argument registersFor common integer and pointer arguments: X0 = First argumentX1 = Second argumentX2 = Third argumentX3 = Fourth argumentX4 = Fifth argumentX5 = Sixth argumentX6 = Seventh argumentX7 = Eighth argumentReturn values commonly use X0. Floating-point arguments use separate registers. IL2CPP methods may also have native metadata arguments that aren't obvious from the original C# method signature. 10. Function Prologues and ReturnsYou might see something like: STP X29, X30, [SP,#-0x20]!MOV X29, SPThis is an example of a function prologue. It's saving registers and setting up a stack frame. Near the end, you might see: LDP X29, X30, [SP],#0x20RETThis restores the registers and returns. Not every function uses the same structure. The main thing to understand is that these instructions can be important for keeping the stack and registers in the correct state. 11. Cross-References in IDACross-references are probably one of the features I use most. Select a function or address and press X. IDA will show references it has identified. Direct function callBL ExampleFunctionThis directly calls the function. Data referenceDCQ ExampleFunctionThis stores a function address as data. You might see this in a callback table or method table. A data reference doesn't necessarily mean the function is being called directly. Why cross-references matterLet's say you find a function that looks interesting. Instead of only looking at that function, check where it's referenced. You might find that it's called by another function responsible for initializing an object, handling a button, or processing an event. Following those references can help you understand how the different parts of the game connect. 12. Indirect Function CallsNot every function call uses BL. You might see: LDR X8, [X19,#0x20]LDR X8, [X8,#0xC0]LDR X8, [X8,#0x20]BLR X8Here, the function address is being loaded into X8. BLR X8 then calls the address stored in that register. If you want to know what function is being called, you'll need to trace where X8 gets its value. You'll come across this with virtual methods, callbacks, and some IL2CPP generic method dispatch. 13. Unity Objects and Field OffsetsWhen analyzing Unity IL2CPP, you'll often see code accessing fields through object pointers. For example: void* view = *(void**)(manager + 0x18);Breaking it down: manager = Base object pointer0x18 = Field offset*(void**) = Read a pointerview = ResultIt's reading a pointer stored at offset 0x18 inside an object. This is useful when trying to understand how Unity manager classes store references to other objects. Why this mattersThe pointer might be null. The object might not have been initialized. Or the object could have been destroyed. That's why simply finding the correct function address isn't always enough to understand how a Unity system works. 14. Why Native Function Calls Can CrashOne thing I've learned is that finding a function address doesn't automatically mean you can call it from anywhere. For example: void ShowMenu(MenuView* instance){ instance->Show();}This function expects a valid MenuView object. If something passes the wrong object, the function may access invalid memory and crash. Before calling a native function, you need to understand what arguments it expects and whether its required objects have been initialized. With IL2CPP, you may also need to account for method metadata or generic context. The address might be correct, but the call itself might still be invalid. 15. Analyzing Non-Unity GamesNow for games that don't use Unity. You won't normally have IL2CPP DummyDll files to help you find methods. Instead, you'll need to do more of the investigation directly inside IDA. Start with the Strings windowOpen the Strings window using Shift + F12. You might find strings like: Purchase FailedNot Enough CurrencyLevel CompleteReward CollectedSave FailedDouble-click a string and press X to see where it's referenced. This can lead you to functions related to the feature you're investigating. Keep in mind that the first function you find might only display the message. Check imported functionsThe Imports window can also be useful. You might find: objc_msgSendmallocmemcpydispatch_asyncThese can give you clues about how the application interacts with system libraries. Follow the codeOnce you find something interesting: Open the function. Press F5 to read its pseudocode. Check its arguments. Follow any important function calls. Use X to inspect references. Rename functions once you understand them. It's basically the same process as Unity, just without the extra metadata to guide you. 16. Objective-C GamesSome native iOS games use Objective-C. If the runtime metadata is available, you might find readable methods such as: -[GameManager saveProgress]-[ShopController purchaseItem:]-[PlayerData currentBalance]Objective-C commonly uses objc_msgSend for method dispatch. For a typical ARM64 Objective-C message call: X0 = Receiver (self)X1 = Selector (_cmd)X2 = First explicit argumentX3 = Second explicit argumentThis is worth remembering when you're reading Objective-C functions in IDA. The first two registers are normally used for the receiver and selector rather than ordinary explicit method arguments. 17. Swift GamesSwift applications can contain mangled symbols. For example: $s4Game13PlayerManagerC11saveProgressyyFThese names encode information about modules, types, and methods. You can use a Swift demangler to make them easier to read. Swift also has its own ABI and calling conventions, so don't assume every Swift function works exactly like a normal C function. If the binary has stripped symbols, you'll need to rely more on IDA's native analysis. 18. Unreal Engine GamesUnreal Engine games work differently from Unity IL2CPP. You generally won't use IL2CPP Dumper or DummyDll files. Instead, you may encounter: UObject UFunction Unreal reflection metadata Native C++ methods Unreal class structures Engine-specific assets Depending on the build, Unreal's reflection information or remaining symbols can make some functions easier to identify. Otherwise, you'll be working through the native C++ code directly in IDA. 19. Cocos2d-x and Custom EnginesCocos2d-x games are commonly written in C++. You might find classes like: cocos2d::Nodecocos2d::Scenecocos2d::Spritecocos2d::DirectorIf those symbols are available, they can help you recognize engine-related code. Custom engines might not have any obvious symbols. In that case, strings, imports, cross-references, and debugging become even more useful. It takes more manual work, but the same IDA fundamentals still apply. 20. Finding Gameplay-Related FunctionsWhether you're working with Unity or a non-Unity game, searching for relevant names and strings can help. For example: MoneyCurrencyCoinsGemsGoldBalanceWalletRewardSpendPurchaseMultiplierIn a Unity IL2CPP dump, you might find methods such as: GetBalance()AddMoney()SpendMoney()CollectReward()UpdateCurrencyUI()SaveCurrency()For a non-Unity game, you might find similar names in symbols or strings. Just remember that a function called UpdateCurrencyUI might only change the displayed balance. It doesn't necessarily change the actual stored value. Online games may also handle important values on the server. Don't assume that every function with an interesting name is responsible for the actual gameplay state. 21. ARM64 Hex and Byte OrderARM64 instructions are normally four bytes long. For example: NOPThe instruction word is: D503201FIn little-endian byte order: 1F 20 03 D5Another example: RETThe instruction word is: D65F03C0Little-endian bytes: C0 03 5F D6Make sure you understand whether your tool expects the instruction word or the actual byte sequence. Some ARM64 branch instructions are also PC-relative. That means the address where an instruction is assembled can affect its encoded destination. 22. Common MistakesAssuming every function name is accurate A method name is a clue, not proof of what the function does. Confusing RVA and VA Always check which module and address format you're working with. Assuming sub_ means obfuscated IDA often generates these names because the original symbols aren't available. Not checking cross-references Sometimes the function you're looking at is only one part of a much larger process. Ignoring object initialization Some functions need valid objects or other systems to be initialized first. Assuming a correct address means a valid function call Arguments and calling conventions matter just as much. Changing instructions without understanding them You could accidentally break stack setup, register preservation, or other important behavior. Not keeping notes Write down what you've already checked. It'll save you time later. 23. My Usual WorkflowThis is roughly how I approach analyzing a game. Unity IL2CPPIdentify the Unity version and IL2CPP files. Recover usable metadata and reconstructed assemblies. Open the assemblies in dnSpy or ILSpy. Search for the relevant class or method. Locate the native implementation in IDA. Read the pseudocode and check the arguments. Follow cross-references and related functions. Verify what the function actually does. Non-UnityIdentify the engine or framework. Load the native executable into IDA. Check the Functions, Strings, and Imports windows. Look for useful symbols or strings. Follow cross-references to relevant functions. Read the pseudocode. Rename functions and document your findings. Verify your assumptions using debugging. The main difference is how you find the functions in the first place. Once you're looking at native code in IDA, many of the same skills apply. 24. From Reverse Engineering to Game ModificationsNow that we've covered finding functions and understanding what they do, let's go over how that knowledge can be used to modify games. Finding an address is one thing. Knowing what to do with it is another. There are a few different approaches depending on the game, its engine, and whether the values are handled locally or by a server. 24.1 - Client-Side vs Server-SideBefore attempting to modify anything, you need to understand where the game handles its data. Client-side The game handles the value locally. This is common with offline games, visual effects, UI elements, and some gameplay systems. Server-side The server controls the actual value. This is common with online currencies, inventories, purchases, and multiplayer statistics. For example, changing the displayed currency value doesn't necessarily change the real balance. You might see the number change on screen, but the server still has the original value. This is why it's worth checking what a function actually controls. 24.2 - ARM64 Patching BasicsARM64 patching involves replacing existing machine instructions with different instructions. For example, imagine we've made a small offline test application containing this function: int GetTestValue(){ return 10;}The compiled ARM64 instructions might look something like: MOV W0, #10RETThe first instruction places the value 10 into W0. The second instruction returns to the caller. If we wanted our own test function to return 100 instead, the equivalent instructions would be: MOV W0, #100RETThat's a basic example of changing a function's return value. Real game functions can be much more complicated. They might rely on objects, memory, other functions, or server responses. 24.3 - Understanding NOP and RETTwo instructions you'll probably come across quite often are NOP and RET. NOP NOPThis performs no operation and continues to the next instruction. ARM64 instruction word: D503201FLittle-endian bytes: 1F 20 03 D5RET RETThis returns control to the caller. ARM64 instruction word: D65F03C0Little-endian bytes: C0 03 5F D6One thing worth mentioning is that these instructions aren't interchangeable. Replacing an instruction with NOP doesn't automatically disable a function. Likewise, replacing something with RET doesn't guarantee that the function will return safely. You need to understand the surrounding instructions first. 24.4 - Using iGameGod Live Offset PatcherIf you're working with a jailbroken iOS device, you've probably come across iGameGod's Live Offset Patcher. This can be used to experiment with instruction changes in a running application. Before using it, make sure you understand: Which binary or framework contains the function. Whether the address is an RVA, VA, or runtime address. What the original instructions do. What bytes the replacement instructions represent. Whether the function relies on other code. For example, an address belonging to UnityFramework shouldn't automatically be treated as an address belonging to the main executable. Also, don't assume every address shown in dnSpy can be entered directly into a memory tool. For learning, I'd recommend starting with your own offline test application where you already know the function's behavior. 24.5 - Function Hooking with C++Patching isn't the only way to modify native behavior. Another approach is function hooking. Instead of simply replacing instructions, a hook redirects execution through your own function. Here's a basic C++ example: using GetValueFn = int (*)();GetValueFn originalGetValue = nullptr;int HookedGetValue(){ if (originalGetValue == nullptr) { return 0; } int original = originalGetValue(); return original + 5;}In this example, the wrapper calls the original function and changes the returned result. I've also included a null-pointer check so the example doesn't attempt to call an uninitialized function pointer. Returning 0 is just an example fallback and might not be appropriate for every function. This doesn't install a hook by itself. It only demonstrates the wrapper logic. A working hook needs a correctly resolved function address, the right calling convention, and an appropriate hooking framework. On iOS, you'll commonly see developers working with Theos, Objective-C++, and native hooking frameworks. 24.6 - Unity vs Non-Unity ModificationsThe overall idea is similar, but the process of finding and understanding functions can be different. Unity IL2CPPNon-UnityOften has recoverable metadataMay require manual native analysisMethods can be found through DummyDllFunctions may need to be found through IDAUses IL2CPP object structuresObject structures depend on the engineMay require MethodInfo metadataDepends on the language and calling conventionOften analyzed through UnityFrameworkCode may be in the main executable or other frameworksWith Unity IL2CPP, you might already have a method name and address from the reconstructed assemblies. With non-Unity games, you might need to find the function through strings, symbols, or cross-references. Once you've found the native function, many of the same IDA skills apply. 24.7 - Why Some Patches Don't WorkThis is something you'll probably run into when experimenting. Wrong address You might be modifying the wrong function or using the wrong address format. Wrong binary The function might belong to a framework rather than the main executable. Incorrect instructions The replacement bytes might not represent the instructions you intended. Invalid object pointer A function might expect an object that isn't available or initialized. Incorrect return type Returning an integer when the caller expects a floating-point value or pointer can cause problems. Server-side behavior A local modification might have no effect on the actual game state. Game updates Addresses and instructions can change between versions. 24.8 - Testing Your ChangesWhen experimenting with your own offline test application, I'd recommend changing one thing at a time. Keep a backup of the original binary. Find the function in IDA. Read the pseudocode and assembly. Check the arguments and return type. Record the original instruction bytes. Make one controlled change. Test the specific behavior. Check for crashes or unexpected results. Restore the original code before trying something else. Changing multiple things at once makes it harder to work out what caused a problem. Also, just because the game launches doesn't mean your modification worked. The function might not have executed at all. 24.9 - My AdviceIf you're just getting started, don't focus entirely on finding hex codes. Spend time understanding what the instructions do, how functions receive arguments, and how different systems connect. Knowing why something works is much more useful than copying an address and hoping for the best. I'd also recommend practicing with your own offline test applications before moving on to anything more complicated. Final NotesI'm still learning IDA Pro myself, but these are some of the things that have helped me understand it a lot better. If you're just starting out, I'd recommend getting comfortable with pseudocode, cross-references, and ARM64 registers before worrying too much about modifying anything. At first, IDA can look like a complete mess, especially when everything is named sub_ and you've got no idea what half the instructions mean. Once you start understanding how functions connect, it gets a lot easier. Hopefully this helps anyone else who's getting started with Unity or non-Unity game reverse engineering. If I've missed something or got anything wrong, feel free to point it out. And yes, Grammarly probably saved everyone from having to read my original spelling LOL.