In-memory load of Swift Mach-Os ignores typical ARGC/ARGV arguments
This guest post is from a member of Strontium.io’s penetration testing team. This issue was solved in the latest tamatoa release.
TL;DR
When loading Mach-O executables into memory using direct system calls on Mac OS, Swift binaries operate differently from other language-built executables. Specifically, they ignore the ARGC/ARGV arguments typically passed into a C-style main function.
Problem
This happens when Mach-O’s are loaded through dyld calls. The process executing these calls can be considered the superior process, with the loaded binaries operating as an inferior process. The challenge here is to provide a different set of command line arguments to the inferior process than what is existing in the superior process.
For non-Swift binaries, this is relatively straightforward. Typical binaries respect the arguments provided to their entry point function. For example, this call will provide the given ARGC/ARGV environment via argument variables to the inferior process entry point:
char * new_argv[] = { "./.", "--arg1", NULL };
int new_argc = 2;
inferior_main(new_argc, new_argv);
However, this technique fails when using a Swift binary. A Swift binary loaded in this manner will retain the superior process ARGC/ARGV environment, rather than the arguments provided to the executed entry point. In fact, a Swift binary entrypoint takes no arguments at all.
This issue was uncovered in the development of a new module for tamatoa, my in-memory binary executable loader. slyd0g, a Mac OS security researcher, discovered an issue with tamatoa's argument patching for Swift binaries. I confirmed this issue and started research to understand the differing operation of Swift binaries versus other binary executables.
Context
This is an advanced technique to run arbitrary executables without writing them to disk, useful for evading AV while performing red team operations on MacOS systems.
Solution
Mac OS has a built in C function called _NSGetArgC() and _NSGetArgV() which provide ARGC/ARGV functionality to programs that execute outside of a standard main() entry point. While most executables will have a main(argv, argc) style entrypoint as well as access to NSGetArgC/V, Swift programs rely entirely on these accessors and do not receive main()-style arguments.
Why would you ever do this? Why not allow receiving c-style main(argv, argc) arguments? I don’t know. The Swift authors decided to drop 33 years of consistency in favor of some obscure MacOS-specific accessors.
Now that we have figured out how Swift is retrieving the arguments, we can patch them. In order to patch the arguments, we need to call NSGetArgC/V and overwrite the resulting data. Here is the c program that we load which accomplishes this:
#import <crt_externs.h>
int main(int argc, char * * argv) {
int * nsargcp = _NSGetArgc();
*nsargcp = argc;
char * * * nsargvp = _NSGetArgv();
*nsargvp = argv;
return 0;
}
Conclusion
The Red Team’s goal is to simulate an attacker as accurately as possible. Just like an attacker, the Red Team must develop new tools and techniques to bypass the Blue Teams systems. Tamatoa enables Red Teams to load arbitrary Mach-O’s into memory to prevent AV from scanning them. Swift argument patching is now fully tested and operational in tamatoa. Happy Red Teaming!