arm64 architecture handler
arm64 architecture handler.
It uses unicorn and libffi to run iOS arm64 binaries on x86_64 macOS, with varying degrees of success.
Most things will fail to launch because they need frameworks/symbols that aren't available on macOS.
To run iOS apps, aah relies on the Mac Catalyst frameworks that are present on macOS 10.15.
The sample app is based on UIKitCatalog from the Apple sample code, with an integrated capstone disassembler to inspect functions and methods.
TestApp
target. This builds an arm64 iOS app (TestApp.app
), and makes a copy with the Mach-O header changed to be emulatable (TestApp-aah.app
, in the same output directory).aah-TestApp
scheme, select the TestApp-app.app
executable by choosing “Other...” from the Executable drop-down menu (under Run > Info) and selecting it from the filesystem. Otherwise Xcode will complain that the target doesn't match the current platform.aah-TestApp
scheme.Steps 1 and 2 are only necessary before the first run.
libaah.dylib
is inserted with DYLD_INSERT_LIBRARIES
.reserved
field in the header.EXC_BAD_ACCESS
exception when it's executed.Transitions between host and emulated execution is handled by libffi
at function entry/exit points, and requires the function signature to be known. This is done by keeping a mapping of entry points and their signatures (see cif.c
).
The file SymbolTable.plist
contains the signatures for supported functions (using Objective-C type encoding), and it's added as a section to the libaah.dylib
binary. The key objc shims
is used by the objc_msgSend
shim to call variadic methods. This file is added as a section to the libaah.dylib
binary at build time.
When new entry points are found at runtime, they are added with cif_cache_add
or cif_cache_add_new
. This is used for Objective-C methods, pthreads, blocks and function pointers.
The format of the method signature determines how the call is handled:
$
+ shim name: A shim that will be called when emulated code calls the function. The shim is defined with the SHIMDEF
macro, it receives an emulator context where it can access the registers, and can return SHIM_RETURN
or an address to continue execution. Examples:
printf
or NSLog
(see nslog.m
).objc_msgSend
, setjmp
.<
+ method signature + >
+ wrapper name: Defines wrapper(s) that will be called after and/or before the native function is called. The wrappers are defined with the WRAP_EMULATED_TO_NATIVE
and WRAP_NATIVE_TO_EMULATED
macros, and have arguments rvalue
and avalues
that work like those of ffi_call
. See libdispatch.c
for examples.You will need a thin non-encrypted arm64 app to start with.
Repackage with this modified version of marzipanify.
This will do the following:
/System/iOSSupport
libaah
to be emulated (0x456D400C
in the reserved
field of the header).MH_PIE
flag (probably not be needed, but makes debugging easier).LC_VERSION_MIN_IPHONEOS
load command with LC_BUILD_VERSION
, or update LC_BUILD_VERSION
.Most binaries won't launch because of missing libraries or symbols. optool might help:
After modifying the binary, it will have to be resigned:
If a library is present on macOS but missing some symbols, it's possible to build a stub library with the missing symbols, and make it export all of the original library by adding -Xlinker -reexport_library /path/to/original.dylib
to its linker flags.
For functions in a native library to be called from emulated code, they must have an entry in SymbolTable.plist
, as an Objective-C method signature. This can be automated with the msdecl
tool from CParser, how to do this is somewhat documented in SymbolTable/make_symbol_table.sh
.
If the app links to libc++, package it with the arm64 version included in lib/libc++em.dylib
, and rename the linked library:
optool rename /usr/lib/libc++.1.dylib @executable_path/../lib/libc++em.dylib --target /path/to/package.app/Contents/MacOS/executable
Run the patched executable inserting libaah.dylib
:
$ DYLD_INSERT_LIBRARIES=/path/to/libaah.dylib /path/to/executable
Some useful environment variables recognised by aah
:
PRINT_DISASSEMBLY=1
will print disassembled instructions as they are executed by the emulator.PRINT_REGS=1
will print the registers after and before function calls, or before printing each executed instruction (when combined with PRINT_DISASSEMBLY
).To debug, you'll need a custom build of debugserver that doesn't catch EXC_BAD_ACCESS
exceptions, as this prevents them from being caught as signals in libaah:
Download the llvm source:
$ git clone [email protected]:llvm/llvm-project.git
Patch lldb/tools/debugserver/source/MacOSX/MachTask.mm
:
diff --git a/lldb/tools/debugserver/source/MacOSX/MachTask.mm b/lldb/tools/debugserver/source/MacOSX/MachTask.mm
index 6aa4fb23754..6148b628119 100644
--- a/lldb/tools/debugserver/source/MacOSX/MachTask.mm
+++ b/lldb/tools/debugserver/source/MacOSX/MachTask.mm
@@ -601,7 +601,7 @@ bool MachTask::StartExceptionThread(DNBError &err) {
// Set the ability to get all exceptions on this port
err = ::task_set_exception_ports(
- task, m_exc_port_info.mask, m_exception_port,
+ task, m_exc_port_info.mask & ~EXC_MASK_BAD_ACCESS, m_exception_port,
EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE);
if (DNBLogCheckLogBit(LOG_EXCEPTIONS) || err.Fail()) {
err.LogThreaded("::task_set_exception_ports ( task = 0x%4.4x, "
Build debugserver with Xcode and install to /Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/Resources/debugserver
. You might want to save a backup of your original debugserver.
In the aah
project, edit the aah
scheme and choose an executable/app to debug.
Make sure the environment variables include DYLD_INSERT_LIBRARIES
with $(TARGET_BUILD_DIR)/libaah.dylib
first.
Build & run
It will hit a SIGBUS
when it first encounters code to emulate. To prevent it, run this in the lldb console:
There is already a shared breakpoint in the project that does this in init_aah
.
(lldb) process handle --pass true --stop false --notify true SIGBUS
(lldb) continue