At the end of last year Patrick Wardle published a blog post demonstrating how to load a shared library into memory on macOS without leaving any traces on disk. This technique, commonly referred to as reflective or in-memory code loading, is well worth reading, as it lays the foundation for the topic discussed here.
For those of us in the red team community—or anyone developing macOS-targeted malware—this capability is incredibly valuable. When building a C2 framework, the ability to load additional functionality through dynamic libraries is a powerful feature. Leveraging in-memory loading helps evade detection, as the injected libraries never touch disk, reducing the risk of being flagged by EDRs monitoring for malicious file artifacts.
To that purpose, I cloned the repository and integrated it into our internal macOS C2 implant. The provided C++ example library loaded without issues and worked as expected. Encouraged by the results, I then attempted to load an Objective-C library we had developed for macOS-specific functionality. Unfortunately, it failed with an error about an unrecognized selector—an issue that closely resembled linker errors I’ve encountered in C/C++.

As someone still relatively new to macOS development, I spent some time researching the error—assuming it was related to certain Objective-C libraries not being loaded into the host process, or perhaps a variation of a linker issue. I revisited several older articles on macOS reflective loading, including Adam Chester’s post hoping to uncover some clues. Luckily, at the very end of his article, I found a screenshot showing the exact same error I was encountering. He mentioned that the next part of his blog would cover how he resolved it.
Unfortunately that second blog post wasn’t ever written… I decided to message @_xpn_ and ask if he would mind passing on what the issue may be. No luck there either.

After spending more time experimenting without success, I had to put the issue on hold for a few months until I had some free time again. Initially, I tried various LLMs—ChatGPT, Claude, Gemini—for help with the error. I vaguely recall framing the problem in terms of linking, since I assumed that was the root cause. As a result, all the responses focused on ensuring that both the C2 binary and the module libraries were correctly linked against the Objective-C runtime.
This time, driven mostly by frustration, I decided to try a different approach. I loaded the Reflective Loader project into GitHub Copilot, included the loader code as context, pasted in the error message, and provided the LLM with the one line clue that Adam Chester had teased at the end of his blog post. To my utter amazement, it provided the following answer.

A new lead! With this fresh insight, I used the updated context to ask more targeted questions in the LLM. I decided to aim straight for a solution and asked Copilot to generate the necessary code. While it didn’t give me a working implementation, it did provide valuable hints that helped guide my next round of research.
In the example code, I noticed a function call to objc_registerClassPair
, which seemed related to registering classes with the Objective-C runtime. Curious, I Googled the function name to see if any projects were using it—figuring anything that specific might be doing something similar to what I needed. A few results in, I came across an article discussing how to load Objective-C and Swift into memory in LLVM.
The article was written by Stanislav Pankevich in 2018, and they ran into the exact same problem I was having. What’s even better was they solved it.
The best part of the article was its depth—it detailed how the loader was implemented, the challenges encountered along the way, and even included a GitHub link to the library containing the actual code.
I reviewed the code and integrated the Objective-C registration logic into the Reflective Loader project. After loading it into our implant, the error disappeared, and we were able to successfully execute code from the dynamically loaded library. I submitted a pull request to Patrick Wardle’s Reflective Loader GitHub repository to share the improvements with the community and he was kind enough to merge them in.
The key takeaway from this experience: be creative with your LLM prompts. Sometimes, the most unexpected questions can lead to breakthrough results.