Packing malware in Rosetta 2
Table of Contents
How to use DYLD and Rosetta 2's characteristics to pack whatever ya want! If you want the slides, I've appended them to the end of this page. You can use them to follow along with the talk that was listed at SummerCon's recording of the talk. I'm also keen on internships or knowing more folks in security - feel free to reach out! Rosetta 2 is a translation layer that allows macOS to run Mach-O x86_64 binaries on Apple Silicon. To make it short and sweet, it is a transpiler tool that uses static compilation when it can to convert code and cache it away via It will be phased out in the next major MacOS update - with the new tools being the GPTK (Game Porting ToolKit) which packages Windows for game review/porting and then other items of Linux, like Like I mentioned above, we want to first understand how Rosetta 2 leverages both ahead-of-time and just-in-time compilation to keep MachO zippy. Using the If it isn't found, we instead try to perform as much one-for-one x86_64 to AARCH64 translation as possible, and then cache it in a given This is done for all binaries required, and we put it under a directory. If we can't translate a given block, we'll stub a branch that we can figure out later and resolve at runtime - without going into the major differences, this is similar-ish to how dyld works in how we use dyld to resolve dynamic library calls but 'make space' in the This is going to be verbose as we have many differences from ARM64 to x86, and we want to keep state. Why? We need to also append the original x86 instrucitons to a processes' memory so we can refer to them later when needing to find instructions for JIT translation. We'll also want to be quick for first launch- there's details on this on dougallj's blog I have referred to before. To show the AOT section: Cool, so we've got the aot file cached and kept away in a location where SIP is gonna protect it from the nasties. Since we have stubbed any indirect calls, when we end up running the program, we'll need to resolve any stubs at runtime. This is for And the JIT section: We have other cool goodies to help with mimicking all sorts of characteristics that isn't on AARCH64: We aren't going to see this since LLDB and debugging shims the same x86 so we can't see the resulting instructions that get run. It helps the developer that worked on their Intel Mac apps to still debug on Apple Silicon, neato! The AOT Okay, so I already did the bulk of this in a prior talk at Ruxmon Melbourne in 2025. This was initially inspired by Jai Vermas blog on AssaultCube hacks on Intel MacOS, which I thank for the inspo! My updated version for Apple Silicon's AssaultCube hack over Rosetta 2 available at my prior blog post in 2025 about making a game hack - so I won't go over the specifics on how we leverage it, but in short: We can manually add this given library using Why don't you see But Rosetta 2 allows us to run unsigned binaries, which includes loading unsigned libraries into a otherwise signed process. It doesn't check this stuff despite needing these sections to be present! On iOS and for many contexts on MacOS, codesigning is required now we are on Apple Silicon. Keep a pin on In this situation, we would require valid signatures on all libraries and executables, whether ad-hoc or formal to run. DYLD will check this and all Mach-O files carry their signatures as a special section. These tools are part of a system that negotiates around when Gatekeeper would fire on MacOS, where a user would need to additionally verify that this executable is OK to run. From Apple's documentation on Unsigned x86_64 code on Rosetta 2 against how it typically checks for signing: Rosetta 2's loading of AOT sections don't map these load command tables from the original x86, as it's not needed. This means we can understand how Rosetta 2 transpiles code and what is avoided to understand rudimentary attack surfaces: As we know, we review via From the decompilation of Further to this, using Child processes via posix_spawn() can retain the preferred architecture setting. Undocumented registers from Apple are used to ensure further compatibility with Intel is kept, such as This is a novel feature of Apple Silicon chips, which is documented from Asahi Linux. Google Cloud Security's Threat Intelligence went over Rosetta 2 artifacts and instusions refering to PoolRAT, which pulled down a unified binary and deliberately executes the x86_64 verion to use this character of Rosetta 2, which caches part of the translated binary in the Interestingly, they note that Unified Logs also don't catch attribution for PoolRAT by default since So really, we can understand this characteristic to force JIT transition. Instead of using For AssaultCube, it uses the SDL2 (Simple DirectMedia Layer) library to render graphics and handle input events. This is within the AssaultCube app bundle. To confirm, we can open up AssaultCube and see that Finally, we can check that for the full name what Framework is loaded in LLDB: To doubly confirm, we'll want to understand where this function is resolved and used in the actual process - which will define when our swizzle will run. This is important as we could use values given in the process at runtime in a real-world scenario, or we want to simply run our swizzle as early as possible. In this case, the documentation for We can also confirm this via static analysis on the AOT file that translates the call to Slay... looks like this is a viable candidate to swizzle! Swizzling is where we set up our own function pointers to replace the expected, original pointer. In this instance, we'll want to run it and then ensure state is kept to hand it over to the intended call. In short, here's is some dummy code: As you can see here, there is a Using this information, we can create out own dylib to append to AssaultCube - truncating it so we just focus on the swizzle and ensuring state is kept to hand it over to start the game: Then we can use other features of dyld and dynamic linking to leverage constuctors, destructors and that we ourselves resolve We can then use lief or Rosetta 2 is an amazing little gizmo when it comes to how it is absolutely fast-as-hell in terms of using ahead-of-time translation to remove bulk for just-in-time translation to do only what is nessecary. This flow allows for less work to be done when your legacy app is actually running, and ensures all the verbosity that we would need to ensure state for JIT is fast. Using this method of two-step transpilation and preferring verbosity over conciseness or optimisation, additional features can be added since we can always lookup state from one architecture to another. We get all these cute tweaks for memory ordering, RIP-relative addressing and more; that has made Rosetta 2 pretty stable and performant in such a way that game emulation is quite smooth considering - I love playing Guild Wars 2 on the damn thing! Hopefully this blog and talk shows some problems of having a predicate where not all code translated in one spot creates some interesting problems. Just-in-time translation is still something that can run a lot more independently than one initially assumes by enforcing indirect calls along with an assumption that unsigned code is ok. To clear up the post a bit... interposing is an intended function of how many external calls can be shimmed to allow for performance, stability and modularity in many programs. However, this with Rosetta 2 is clearly a great mix for making simple malwares that are harder to detect that you'd think! I'm currently working on more Rosetta 2 research and am interested in MacOS/iOS targets - I am available for internships in lieu of my full-time studies for now - if you're performing research on the same stuff, I'd love to talk! Below are the original slides from SummerCon 2026 in Brooklyn, NYC. TL;DR

Introduction
oahd_helper and for branching out to indirect calls, it will hand it over to JIT compilation.VZVirtualMachine. Who knows how it will be architected, but this is a nice little tool that has been used to smooth out the transition to M-series chips! Understanding how Rosetta 2 ends up giving us AARCH64
Ahead-of-time
oahd-helper daemon, we first check any exec call that is made against a given x86_64 target, calculate a hash and then check it against a known cache at /var/db/oah/. If we find it, no problem.exec.aot file. We also do this for any required frameworks that are needed by the executable.
LAZY_SYMBOL_POINTERS section of a given MachO file.
Just-in-time
extern calls, indirect branching available in x86_64 and anything else we can't really deterministically resolve. runtime from Rosetta 2 will lookup the original instructions we mentioned before and walks down a red-black binary tree of these to resolve new instructions and hand it back to the TEXT segment responsible for executing code on your Macbook!
What else is in Rosetta 2?

shared_cache and runtime will work together to review and resolve all function starts and code blocks to combat the lack of heavy optimisation - this isn't investigated with regards to of exec primitives. There's a variety of protections available in malloc to prevent cross-pollution of x86 and AARCH64 memory. Gamehacking over Rosetta 2
Setting up the game cheat
DYLD_INSERT_LIBRARIES environment variable, which is similar to LD_PRELOAD on Linux - we use it to ask dyld to load our library before any other library.DYLD_INSERT_LIBRARIES used much? Well, dyld also checks for sections of a library for codesigning - if the library is not signed, dyld will not load it.
Apple Silicon security paradigms in Rosetta 2

LC_LOAD_DYLIB/LC_LOAD_WEAK_DYLIB for later... WINK!!!
Attack Surfaces in Rosetta 2
dlopen and libraries,uname from the kernel to define x86/ARM execution paths, AOT Checksums
oahd to check for AOT caches on exec for a given Intel executable. Project Champollion details the characteristics of how these checksums are created, and are relatively abritrary if we keep in mind that we should ensure the relative virtual address the x86_64 code section is kept the same along with relative paths from the main executable.oahd (cred to Koh Nakagawa): 
otool we get a Mach-O load command, which... yes, loads the AOT files into memory.
Code like you're on x86_64 (for malware)
ACTLR_EL1, which flips the first bit to enable total-store ordering, meaning any store operations str are ordered as they would be on x86_64, and doesn't use any tricky ARM weakly-ordered memory model - more on ARM memory ordering here! Examples of this in North Korean malwares
/var/db/oah directory, but we know it doesn't handle everything.oahd omits names, and a custom profile needed to be set up to catch it. Oopsie!!!!!(void)(void*) and dynamic execution means we'll need dynamic analysis to see what Rosetta 2 spits out - and in the case of PoolRAT, it removes itself - so you don't get the whole corpus of code that ended up executing with just the .aot files. Setting up a sneakier attack on Rosetta 2
Finding a target on a dependent library
DYLD_INSERT_LIBRARIES to enforce an arbitrary library to run ahead, we should try to leverage how dyld works by resolving these external library calls at the LAZY_SYMBOL_POINTERS section. This is known as swizzling.
SDL_Init is in the LAZY_SYMBOL_POINTERS section of the file:

Understanding time of resolution and use
SDL_Init will have it run to start the SDL subsystem, and we can confirm this via a breakpoint!
SDL_Init along with it's relative path @rpath/SDL2.framework/...
Swizzling with your own function pointers
// clang -arch x86_64 -dynamiclib interpose.c -o
#include <stdio.h>
// re-use it wherever this way!!! clang will fix it up
#define DYLD_INTERPOSE(_replacement, _replacee)
__attribute__((used)) static struct {
const void* replacement;
const void* replacee;
} _interpose_##_replacee __attribute__ ((section("__DATA, __interpose"))) = {
(const void*) (unsigned long) &_replacement,
(const void*) (unsigned long) &_replacee
};
int my_printf(const char *format, ...)
{
int ret = printf("Hello from interpose... uh oh!!!\n");
return ret;
}
DYLD_INTERPOSE(my_printf,printf);DYLD_INSERT_LIBRARIES=./interpose.dylib ./hello
Hello from interpose... uh oh!!!
__DATA, __interpose section in the binary that holds the interpose information that is used for interposing function calls, which we then enforce by telling clang to not strip the code (as it will, being empty after analysis).
SDL_Init to hijack the resolved pointer, and enforce any calls go to us first: 
Ensuring it's cosy in our applications directory with all the other libraries
clang to instead of using DYLD_INSERT_LIBRARIES, we can use DylibCommands to either enforce strong (required to continue) or weak (optional to load, no worries if it's not there!) loading of our swizzling dylib. This uses install_name to specify the path of the dylib to load, which is relative to executable path... remember that image path from LLDB!clang -arch x86_64 \
-shared \
-install_name @executable_path/../Frameworks/evil.dylib \
-F assaultcube.app/Contents/Frameworks \
-Wl, -ld_classic \
-framework SDL2 \
-o assaultcube.app/Contents/Frameworks/evil.dylib \
evil.cimport lief
SDL2 = "assaultcube.app/Contents/Frameworks/SDL2.framework/Versions/A/SDL"
binary = lief.()
binary.(...(
"@executable_path/../Frameworks/evil.dylib"
))
binary.()
binary.() OK FINE here's the calc
Conclusion and afterthoughts
Slides from SummerCon