praydog / July 2023 (9023 Words, 51 Minutes)
Disclaimer: Most of what is talked about here is not new, and has been used in the security industry for a long time. This article is meant to show how these techniques can be used to locate functions, variables, and structure offsets within games, rather than identify malware.
Disclaimer 2: This article assumes an x86-64 binary target, compiled under Windows, using the Microsoft Visual C++ compiler. Some of the techniques may not be applicable to other platforms/compilers, or may require some modifications.
The sphere of game hacking and modding has a rich history, tightly knit with a well-known practice called AOB (Array of Bytes) scanning, also known as signature scanning. This method, similarly employed within the realm of security, is a traditional approach used to detect malicious code or, in the context of modding and hacking, locate specific areas within a program’s memory.
In the realm of security, there exists a suite of techniques encompassing static, dynamic, and heuristic analysis. For the purpose of this discussion, we’ll refer to this group of methods as “binary analysis”. The question: could we leverage these techniques to create more robust code scanning techniques? This isn’t about superseding signature scanning; instead, it’s about broadening our capabilities, adding more depth to our strategies.
Automated binary analysis, static and dynamic, while not a novel approach, is sometimes applied within the game hacking community but is less frequently discussed. However, when used appropriately, these techniques can offer a powerful enhancement to - or even an alternative for - traditional AOB scanning. There are other reasons why these techniques would be used, but that is not the focus of this article.
Many of the techniques described here require the reader to have already identified areas of interest via something like IDA Pro, x64dbg, Cheat Engine, or other tools. From there, we will use automated techniques to locate these areas of interest, such as functions, global variables, or structure offsets.
Let’s start by introducing a practical project that demonstrates these advanced techniques in action. The Unreal Engine Virtual Reality project (UEVR) is a real-world initiative I’ve been working on that uses many of the techniques described in this article.
Theory is great, but it’s not very useful without practical application.
That’s why there is a project I have been working on that uses many of these techniques. It’s meant to work on almost any Unreal Engine game. It adds 6DOF VR support to as many Unreal Engine games as possible, with little to no intervention required for the most part on the user’s end (kind of like an emulator in an ideal state).
Using many of the techniques described here, it is possible to make such a project. It’s called UEVR. This project has been my playground for experimenting with these techniques, and it’s been a great learning experience.
For information on just how many games (hundreds confirmed at the time of writing) it works on, and to what degree, see the compatibility list.
UEVR uses the following libraries:
-emulatestereo which “emulates” a stereo device, splitting the desktop screen in two with two cameras at separate positions-emulatestereo uses a built-in class called FFakeStereoRendering which inherits from IStereoRendering, which UEVR aims to hookIStereoRendering contains virtuals relevant to creating a proper stereoscopic image, these include: FFakeStereoRendering interface does not implement thisfalse, the engine falls back to its own texture creation methodGEngine global object "CALIBRATEMOTION" string has always been referenced near a reference to GEngine since early in UE4 developmentUGameEngine instanceStereoRenderingDevice object among other thingsInitializeHMDDevice to create an FFakeStereoRendering instanceFFakeStereoRendering FFakeStereoRenderingLEA instruction via static analysisIStereoRendering vtable and use that insteadUGameEngine::InitializeHMDDevice "emulatestereo" and a reference to the CVar for r.EnableStereoEmulation in this functionFFakeStereoRendering instance without telling the user to use -emulatestereo r.EnableStereoEmulation to 1-emulatestereo was passed in as a command-line argument via code-patchingFFakeStereoRendering vtable we found earlier inside of the global GEngine object to obtain the object and offsetUGameEngine for the StereoRenderingDevice for manual creation if either one of these processes failFFakeStereoRendering vtable and identifies the relevant vtable indices (which has changed a lot over the years of Unreal Engine development)jmp wrapper to multiple layers of control flow obfuscation to simply return truetanf or tannullptr in a row preceding ittrue or false in a row preceding itnullptr and true/false returns, forming a unique patternthis pointerPostInitProperties function GEngine global, it’s the only function that does this within the vtable -emulatestereo immediately when the game starts, but not at any point afterwardsIStereoRenderTargetManager::AllocateRenderTargetTexture, placing the mid hook right before the call to the creation function, as well as afterUGameEngine::Tick in a few ways: UEngine::Tick pure virtual as a reference pointUGameViewportClient::Draw hook if all fails"causeevent=" UGameEngine’s virtual functions (after we’ve found GEngine)UGameViewportClient::Draw "CanvasObject" in the function"CanvasObject" string because: FViewport::Draw by analyzing the call stack from a UGameViewportClient::Draw hookFSlateRHIRenderer::DrawWindow_RenderThread by: Slate.DrawToVRRenderTarget CVar is referencedIStereoRendering::RenderTexture_RenderThread which is directly called within FSlateRHIRenderer::DrawWindow_RenderThreadViewExtensions member of UGameEngine: StereoRenderingDevice member offset StereoRenderingDevice offset as an anchor pointViewExtensions member: ViewFamily passed to BeginRenderViewFamily and others FSceneView constructor "r.TranslucentSortPolicy" and "vr.InstancedStereo""r.TranslucentSortPolicy" is actually referenced in multiple functions"vr.InstancedStereo" string isn’t guaranteed to be at the same call stack level as the other string "r.TranslucentSortPolicy" references, and then analyze each function to see if it contains the "vr.InstancedStereo" string "vr.InstancedStereo" stringIConsoleVariable vtable SetGetIntGetFloatExecute (For IConsoleCommand)ID3D12Resource pointer) GUObjectArray global variable GUObjectArray global variable FUObjectItem elements via bruteforce scanning for valid pointers to UObjectBaseUObject, FField, FProperty, etc… structures FName::ToString() function AOB scanning, or signature scanning, have long been used as a means to locate specific areas in a program’s memory. The technique consists of identifying unique sequences of bytes that correspond to certain operations or data. This practice, which started in the field of antivirus software, quickly found its way to the modding/hacking community, where it became an essential tool. These sequence of bytes can be a function prologue, a magic number, a specific string, and many other things.
One such industry tool that employs signatures is YARA, a versatile malware identification and classification tool. YARA leverages signatures as part of its sets of rules that identify malware based on binary or textual patterns. The use of signatures, while effective in some scenarios, often encounters issues when dealing with complex game updates, function changes, engine differences, or compiler changes, mainly due to its rigidity and specificity.
Unlike AOBs, binary analysis isn’t dependent on specific byte patterns. Instead, it examines the structural and behavioral patterns in a program. In cybersecurity, binary analysis is used to detect unknown viruses or malware by looking at how they behave or how they are structured, instead of looking for a known virus signature. It’s also used to perform formal verification of software, which is the process of proving or disproving the correctness of a program with respect to a certain formal specification or property. Software like BAP is used to perform formal verification of software.
The implementation of binary analysis is broad and ranges from simple static binary analysis to more sophisticated techniques such as emulation, symbolic execution, taint analysis, control flow analysis, and more. These techniques can be used to locate specific areas of interest in a way that is resistant to updates and changes.
Historically, the game hacking community is well-known for using techniques seen in the wild well before research from the security industry is released. However, despite the potential benefits, the adoption of automated binary analysis for locating key areas has been slow: it is complex, and some may see it as overkill.
The community often prefers AOBs for their simplicity, but advanced modders might find binary analysis a potent addition to their arsenal, especially if working with a prolific game engine, or if the game is frequently updated and more robust techniques are needed to locate areas of interest.
A particularly compelling use case for automated binary analysis is with specific game engines that power many games. The technique’s ability to locate shared engine functions can exceed that of a signature if used correctly. Automated binary analysis is generally more resilient to game updates in general, as it’s not dependent on specific byte patterns. However, it’s not a silver bullet, and it’s not without its own set of challenges. It can also be quite slow if not done with care.
Localized emulation of functions, symbolic execution, general disassembly (static analysis) using libraries, data flow analysis, and string reference analysis, offer a solid addition to the modder’s arsenal.
While these techniques are powerful on their own, when some, or all of these techniques are combined, they become powerful tools for locating functions, addresses, and structure offsets that can withstand the test of time.
The first thought that usually comes to mind when thinking about emulation is the use of a virtual machine to run a program, or a program to emulate a game console. However, emulation can also be used to run a function in a controlled environment and observe its behavior. This technique is particularly useful when dealing with functions that are difficult to analyze statically.
Given an anchor instruction to start from (this assumes you know how to find this instruction in the first place, for example with an AOB or string reference lookup, even the entry point of the program), we can emulate the function (or set of instructions) and observe its behavior. Along the way, we can look for function calls to other functions of interest, look for specific memory operations (such as accesses of global variables), look for specific values in registers, look for accesses into structures to find specific field offsets, or just trace the flow of data in general.
One can argue that some of those things can just be found simply with a disassembly pass, but here are some counterpoints:
In C/C++, libraries such as Triton, Unicorn, and bddisasm’s bdshemu can be used to emulate functions.
In Python, libraries such as miasm, Unicorn, qiling, or Triton can be used to emulate functions.
Symbolic execution is similar to emulation in that it runs a function in a controlled environment. However, instead of running the function, it analyzes the function’s code and generates a set of constraints that represent the function’s behavior. This particular technique is not used here, but worth mentioning.
The simplest example I can give here is that symbolic execution has the ability to figure out what inputs can drive a specific function or program to end up at a certain instruction. Example being, product key activation functions, where you can figure out what key will make the program think it’s valid.
The only similar technique we use to this is the enhancement to static analysis to explore all paths.
In C++, libraries such as Triton can be used to symbolically execute functions.
In Python, miasm, angr can be used to symbolically execute functions.
Data flow analysis is the process of tracking the flow of data in a program. This technique is commonly used in the security industry to track user input and identify potential vulnerabilities. However, it can also be used to track the flow of data in a program and identify specific operations or functions of interest.
In this particular context, given an anchor point (such as a function or instruction), we can “taint” a register or memory location with a magic number and track its flow throughout the code using emulation. This can be used for a few things, such as:
The same libraries used for emulation can be used for data flow and/or taint analysis.
Structural analysis, in this context, refers to the analysis of structures (or classes) in a program. This is particularly useful when dealing with C++ programs, as C++ classes are often represented as structures in the binary. This technique is used to locate specific fields in a structure, such as offsets, sizes, and types. It can also just be used to verify that a structure is what we expect it to be.
If we know the general layout of a structure beforehand, we can probe specific areas of memory to see if the structure layout matches. If a structure is supposed to have a valid pointer at 0x10, we can check the offset at 0x10 from a specific pointer, check if it points to valid memory. Combining this with several other checks, like making sure integers look like valid integers, and strings look like valid strings, we can verify that the structure is what we expect it to be.
This technique also works if you know the general layout of the structure, but not the exact offsets within the structure. Using that information, we can perform bruteforce-like checks against the entire structure to obtain the offsets of the fields we are looking for.
An example being, in the UStruct structure in Unreal Engine 4/5, there is a field called SuperStruct. If we have full access to the GUObjectArray already, we can find the:
UStruct classUField classUObject classUsing the pointers to these classes, we can scan over the UStruct structure until we see a pointer to the UField class. If we do, we perform the same check on the UField structure until we see a pointer to the UObject class. If we do, we can assume that the SuperStruct field is at the given offset, because all of these classes are supposed to point to eachother via the SuperStruct field.
Static analysis is the process of analyzing a program’s code without running it. Disassemblers such as IDA, Ghidra, or Binary Ninja are commonly used to perform static analysis. However, in the context of runtime binary analysis, static analysis can be used to analyze a function’s code and look for specific patterns or behavior at runtime using libraries such as:
Using libraries such as these, we can not only perform simple linear disassembly, we can also perform more complex disassembly techniques such as recursive traversal of code paths.
An example, we could disassemble forward, and by using a virtual instruction pointer and list of branches to analyze:
This traversal technique is particularly useful because:
String reference scanning is a technique that involves combing through the program’s memory for references to specific strings. In conventional software analysis tools like IDA or x64dbg, cross-references to strings are often used as a reliable method for locating functions during manual analysis. However, leveraging string references within the context of automated runtime binary analysis in the game hacking community is less frequently discussed.
String reference scanning is a form of signature scanning, but due to its reliance on human-readable data, it is more reliable than traditional signature scanning, which usually looks for byte patterns of instructions which may change for a variety of reasons that the original programmer was not even aware of.
Deploying string reference scanning can accomplish several goals:
Disassemblers such as IDA commonly employ this technique to display cross-references to strings. The difference here is it would be performed at runtime. We wouldn’t have the luxury of disassembling the entire program though; this would be too slow, it would have to be a purpose-built scan that employs localized disassembly.
There are some complexities involved because string references on x86-64 are usually relative as part of an instruction operand. So, a special scanning technique that scans the memory byte-by-byte, treating 4 bytes as the “relative” offset, would be required.
// This would need to be used at every address that is scanned for a string reference
// where customOffset is 4
// This assumes that the immediate is the last operand, which is sometimes not the case
// but it's the most common case so it's not really a big deal
uintptr_t calculate_absolute(uintptr_t address, uint8_t customOffset /*= 4*/) { auto offset = *(int32_t*)address; return address + customOffset + offset;
}
There’s also the issue that this would only give you the address within the middle of the instruction, where the relative operand is. If you need the start of the instruction, you need to get creative. For example, you could have a list of valid prefixes for address references, and scan backwards until you find one of those prefixes, or you could use the technique(s) described earlier that can get you the start of the containing function. From there, you can disassemble forward until the bounds of the instruction encapsulate the address reference.
This same technique can also just be applied to references to any data in general, not just strings. But, since strings are the easiest to scan for from the get-go, it’s a good starting point. If trying to find references to data in general, you would need to know how to find that data in the first place (such as by using or combining the other techniques talked about here).
There is an IDA plugin I wrote a while back which allows the user to click on any address, and it will look for any string references that are near a reference to that particular address. It will only display areas where only the first occurrence of the string reference is near the address. It orders them and displays them in a list based on distance to the target address reference.
Result of using this plugin on the FName constructor in the Unreal Engine
/*
4.27:
[address hidden] + 0x8 NumNetGUIDsPending
[address hidden] + 0x7 SkeletalMeshComponentClothTick
[address hidden] + 0x7 DefaultModulationPlugin
[address hidden] + 0x4 Oculus Quest2
[address hidden] + 0x4 FSequencerPlayerAnimSequence
[address hidden] + 0x4 EndPhysicsTick
[address hidden] + 0x4 StartPhysicsTick
[address hidden] + 0x4 TickAnimationSharing
[address hidden] + 0x4 LakeCollisionComponent
[address hidden] + 0x4 SkeletalMeshComponentEndPhysicsTick
[address hidden] + 0x4 Behavior
[address hidden] + 0x4 FSlateMouseEventsMetaData
[address hidden] + 0x4 FSlateCursorMetaData
[address hidden] + 0x4 SoundExporterWAV
[address hidden] + 0x4 FReflectionMetaData
[address hidden] + 0x4 GameDefaultMap
[address hidden] + 0x4 Test FName
[address hidden] + 0x4 WaterBodyCollision
[address hidden] + 0x4 WidgetTree
[address hidden] + 0x4 FTagMetaData
[address hidden] + 0x4 FSlateToolTipMetaData
[address hidden] + 0x4 ParticleSystemManager
[address hidden] + 0x4 Plugins
[address hidden] + 0x4 FNavigationMetaData
[address hidden] + 0x4 FSceneViewExtensionContext
*/ /*
4.14:
[address hidden] + 0x8 bIsPlaying
[address hidden] + 0x8 FLandscapeUniformShaderParameters
[address hidden] + 0x8 STAT_ColorList
[address hidden] + 0x7 OPUS
[address hidden] + 0x4 LightComponent
[address hidden] + 0x4 FPostProcessMaterialNode
[address hidden] + 0x4 SoundExporterWAV
[address hidden] + 0x4 Component
[address hidden] + 0x4 STextBlock
[address hidden] + 0x4 FLightPropagationVolumeSettings
[address hidden] + 0x4 CraneCameraMount
[address hidden] + 0x4 Priority
[address hidden] + 0x4 FTagMetaData
[address hidden] + 0x4 FNavigationMetaData
*/ /*
Present in both (and 5.0.3+):
+ 0x4 FTagMetaData
+ 0x4 FNavigationMetaData
*/
Heuristic analysis is essentially inferring something about a program given specific patterns or behavior. This is a very broad term, and can be used to describe a lot of things, and some of the techniques talked about so far can fall under this umbrella, or be used in the process of heuristic analysis. It can be used to describe rule of thumb techniques which make educated assumptions, can have a good chance of working, though are not guaranteed to work.
For example, a heuristic for locating a specific global variable in a game engine could be to look for a function that contains a specific string reference, and checking the most accessed global variable inside the function using static analysis, and assuming this is what we’re looking for. We came up with these specific rules by analyzing a few different sample binaries, and the source code of the engine.
This can describe the offline process (such as manual analysis in IDA) AND the automated techniques we will need to use at the same time, providing instructions to two different groups of reverse engineers. This very specific example will be explored down below.
Another powerful technique that is especially useful for finding anchors to use for other techniques is call stack analysis. We can use Windows functions like RtlCaptureStackBackTrace to do this.
The call stack shows us the chain of calls that got us to a particular function. It’s a list of return addresses. Note, there must be hook on this function or the thread must be intercepted in some way to get the call stack.
Using these return addresses we can:
Anchors, or reference points, in the context of both exploit development and game hacking, refer to specific points or references within a codebase or memory space that serve as starting points or ‘fixed’ locations for further code analysis or manipulation. These anchors could be functions, global variables, return addresses, or essentially any identifiable piece of code or data.
In the realm of exploit development, anchor points play a crucial role, especially when dealing with mitigations like Address Space Layout Randomization (ASLR). ASLR randomizes the memory locations of key data areas, making it more difficult for attackers to predict where in memory their payloads or shellcode will reside. Here, the concept of an anchor or ‘fixed’ point is invaluable, allowing an attacker to navigate the memory space in a deterministic manner, despite the randomization.
When talking about signatures in the context of game hacking, they are usually used to scan through an entire module. If scanning from an anchor point, and limiting the size, it can drastically reduce false positives, and can also allow the signature to be smaller.
Anchor points can also apply to offsets or virtuals. We can sometimes assume that an offset to a member, or a virtual index can sometimes precede or be ahead of another offset/virtual index based on previous analysis.
One may ask: “Well, how do I find an anchor in the first place?”
There’s a few different ways:
An interesting note: when enough anchors are gathered, the need for signatures is reduced. This is because the anchors can be used to find other anchors, and so on. This is a very powerful concept, and is the basis for a lot of the techniques described here.
Take this function for example, which is robust enough to work across hundreds of UE4/5 games. It’s a function that finds the IConsoleManager interface, which is used to interact with the game’s console.
namespace detail {
sdk::FConsoleManager** try_find_console_manager(const std::wstring& string_candidate) { SPDLOG_INFO("Finding IConsoleManager..."); const auto now = std::chrono::steady_clock::now(); const auto core_module = sdk::get_ue_module(L"Core"); const auto candidate_string = utility::scan_string(core_module, string_candidate.c_str()); if (!candidate_string) { SPDLOG_ERROR("Failed to find {} string", utility::narrow(string_candidate)); return nullptr; } const auto candidate_stringref = utility::scan_displacement_reference(core_module, *candidate_string); if (!candidate_stringref) { SPDLOG_ERROR("Failed to find {} stringref", utility::narrow(string_candidate)); return nullptr; } SPDLOG_INFO("Found {} stringref: {:x}", utility::narrow(string_candidate), (uintptr_t)*candidate_stringref); // This might need to be improved a bit, it can be insanely slow on obfuscated builds const auto containing_function = utility::find_function_start_with_call(*candidate_stringref); if (!containing_function) { SPDLOG_ERROR("Failed to find containing function"); return nullptr; } // Disassemble the function and look for references to global variables std::unordered_map<uintptr_t, size_t> global_variable_references{}; std::optional<std::tuple<uintptr_t, size_t>> highest_global_variable_reference{}; utility::exhaustive_decode((uint8_t*)*containing_function, 20, [&](INSTRUX& ix, uintptr_t ip) -> utility::ExhaustionResult { if (std::string_view{ix.Mnemonic}.starts_with("CALL")) { return utility::ExhaustionResult::STEP_OVER; } // Checks if the instruction can possibly return a displacement, and if it does, resolve it // an example could be "mov rax, [rip + 0x12345678]" where the displacement is rip + 0x12345678 (an absolute address) const auto displacement = utility::resolve_displacement(ip); if (!displacement) { return utility::ExhaustionResult::CONTINUE; } // hinges on IConsoleManager actually being constructed and assigned if (!IsBadReadPtr((void*)*displacement, sizeof(void*)) && !IsBadReadPtr(*(void**)*displacement, sizeof(void*))) { global_variable_references[*displacement]++; if (!highest_global_variable_reference || global_variable_references[*displacement] > std::get<1>(*highest_global_variable_reference)) { highest_global_variable_reference = std::make_tuple(*displacement, global_variable_references[*displacement]); } } return utility::ExhaustionResult::CONTINUE; }); if (!highest_global_variable_reference) { SPDLOG_ERROR("Failed to find any references to global variables"); return nullptr; } SPDLOG_INFO("Found IConsoleManager**: {:x}", (uintptr_t)std::get<0>(*highest_global_variable_reference)); SPDLOG_INFO("Points to IConsoleManager*: {:x}", *(uintptr_t*)std::get<0>(*highest_global_variable_reference)); const auto diff = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - now).count(); SPDLOG_INFO("Took {}ms to find IConsoleManager", diff); return (sdk::FConsoleManager**)std::get<0>(*highest_global_variable_reference);
}
} namespace sdk {
FConsoleManager* FConsoleManager::get() { static auto result = []() -> FConsoleManager** { std::vector<std::wstring> candidates { L"r.DumpingMovie", L"vr.pixeldensity" }; const auto now = std::chrono::steady_clock::now(); for (const auto& candidate : candidates) { SPDLOG_INFO("Trying to find IConsoleManager with candidate: {}", utility::narrow(candidate)); auto result = detail::try_find_console_manager(candidate); if (result) { SPDLOG_INFO("Took {}ms to search through all candidates", std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - now).count()); return result; } } SPDLOG_ERROR("Failed to find IConsoleManager"); return nullptr; }(); if (result == nullptr) { return nullptr; } return *result;
}
}
Let’s break this down:
"r.DumpingMovie" or "vr.pixeldensity" in this case)From a manual perspective, recreating this entire process from the perspective of a reverse engineer would look something like this:
utility::scan_string in the code)


utility::exhaustive_decode in the code)


This kind of approach is going for more of a human-oriented approach, where we’re trying to replicate the process a human would go through to find the IConsoleManager instance. Doing it like this allows us to easily maintain the code.
While more complex than an AOB, this set of techniques can be more robust, as it’s not reliant on a specific set of instruction bytes being present in the game’s executable other than the string data itself. Rather, it’s dependent on these strings being referenced by the game’s code, which is much more likely to remain consistent across different versions of the game, no matter how they’re referenced, whether with a LEA instruction, a MOV instruction, or something else entirely. The same goes for the global variables: we’re checking for global displacement references, which can be several different kinds of instructions.
If it fails at any point, we have multiple options to use as the IConsoleManager instance is used in many, many other places near unique strings, and we can simply try again with a different string.
In contrast, while an AOB could work here, it may require multiple variants to be written, not necessarily for each version of the game, but to account for function changes, edge cases, different compiler versions, obfuscated code, and more. It’s also much more likely to break, as it’s dependent on a specific set of bytes (a specific instruction) being present in the game’s executable, which is more likely to change for the reasons stated, and sometimes updates to the game in general can break AOBs. I would argue for using an AOB though if there was a magic constant being used in the function though.
To explain how I came up with these strings to look for, I looked through the source code, and through the disassembly listing in IDA in several samples. I found that these strings were referenced in the same function that the IConsoleManager instance was referenced in.
While it may seem as if this article is trying to dissuade from using signatures, that’s not the case. Signatures are still a very useful tool in the reverse engineer’s arsenal, and can be used in conjunction with the techniques discussed in this article to improve their effectiveness.
I would still very much advocate for signatures if there was a magic constant being used somewhere in an instruction as well. An example being:
mov eax, 0xdeadbeef
While it’s possible the mnemonic may change, or the bytes may partially change, the constant referenced within it may remain the same, and can be used as a signature. I could see a form of obfuscation breaking this assumption, however.
The techniques described here are usually going to be slower than a straight signature scan, and may not be feasible in some cases. The degree varies, and can range anywhere from a few milliseconds to a few seconds, depending on the complexity of the binary. Moreso if it’s obfuscated in some way, which not only bloats the size of the executuable, but also messes up the code, and other things like the Windows exception directory. However, this may be a tradeoff between speed and robustness.
Signatures may also be the only option if the target encrypts all of its strings, has very minimal strings in the first place, there’s no feasible anchor points to work with, or doing deep automated analysis may eat up too much time. These methods are in no way easy to come up with an attack plan either, it may just be easier and quicker to write a signature, it may be the case that there’s not enough samples to come up with a broad set of heuristics either.
There’s also more advanced signature scanning techniques which operate on “nibbles” (4 bits) instead of whole bytes, which can be used to make signatures more robust, and less likely to break. Tools like YARA also have “jump” sequences for their signatures, which can be used to skip over N amount of bytes, within a specific range, which can be used to make signatures more robust as well.
The entire point I am trying to get across is that the process of locating certain functions or data structure by a reverse engineer manually in IDA, can be automated in the form of runtime code. If the process taken to get to a specific location manually is broken down by how it was achieved, the same can be achieved in code.
Case in point: If a reverse engineer finds a string reference in IDA that leads them to the function they want, this same exact process can be recreated in code. Even going an extreme step further: If they had to step through the code in a debugger to find the function they want, this same exact process can be recreated in code in the form of emulation or disassembly.
The techniques discussed in this article aren’t intended to displace traditional tools such as AOB scanning, which undeniably hold their unique advantages and a place of importance in the hacker’s toolkit.