Skip to content

frida-gadget crashes on jailed-no-debugger iOS because config is loaded too late #1126

Description

@comex

If you try to use frida-gadget on jailed iOS (or macOS with hardened runtime) without a debugger attached, the process crashes at launch. This happens even if the config file has "code_signing": "required" as specified in the documentation.

The cause is an initialization order issue. There are two relevant constructor functions: frida_libc_shim_init and frida_on_load. frida-core/lib/gadget/meson.build explicitly orders frida_libc_shim_init before frida_on_load. frida_on_load (-> frida_gadget_load) is responsible for loading the config file and setting gum_code_signing_policy and gum_teardown_requirement. But frida_libc_shim_init calls gum_init_embedded, which triggers several steps that try to patch code or execute unsigned code:

  • gum_try_drive_registry_via_dyld_internals -> gum_create_resident_notifier (image-load hook)
  • gum_try_drive_registry_via_dyld_internals -> interceptor API (image-unload hook)
  • _gum_unwind_broker_backend_activate -> interceptor API

Since frida_gadget_load has not run yet, gum_code_signing_policy and gum_teardown_requirement are both at their default values from gumprocess.c, namely GUM_CODE_SIGNING_OPTIONAL and GUM_TEARDOWN_REQUIREMENT_FULL. It doesn’t matter what the config file says since it hasn’t been loaded yet.

If a debugger is attached then the process starts successfully.

If I change the defaults in gumprocess.c to GUM_CODE_SIGNING_REQUIRED and GUM_TEARDOWN_REQUIREMENT_MINIMAL, then the process starts successfully without a debugger. Of course this is not a real fix.

Reproduction

I'm pretty sure I'm using frida-gadget the way it's meant to be used, but if I'm doing something wrong, let me know and I will hang my head in shame. :)

I reproduced this on iOS (27 beta, 24A5355q) as well as macOS (26.5, 25F71) using the hardened runtime.

For iOS, I used the tool Cyan to inject frida-gadget.dylib and frida-gadget.config into a preexisting app. The tool adds a LC_LOAD_WEAK_DYLIB command to the main executable to load frida-gadget.dylib, and re-signs both binaries.

For macOS, I created an artificial test case as follows. For this to work properly, you will need SIP enabled, and the codesigning certificate should be the same one used to build frida-gadget.

mkdir test Frameworks
echo 'int main() {}' > test/test.c
echo '<dict><key>com.apple.security.get-task-allow</key><true/></dict>' > test/entitlements.plist
rsync ~/frida/build/subprojects/frida-core/lib/gadget/frida-gadget.dylib Frameworks/FridaGadget.dylib
echo '{"interaction":{"type":"connect","address":"127.0.0.1","port":27052},"code_signing":"required"}' > Frameworks/FridaGadget.config
clang -o test/test test/test.c Frameworks/FridaGadget.dylib
codesign -f -s 8477F0AB655B6F922C20C214CE0E5A19AB97B222 -o runtime test/test --entitlements test/entitlements.plist
test/test

This should show test being killed, but it should succeed if launched under lldb.

Backtrace

With unmodified Frida (831acfffc87483c4a1d665b9b6ae89e66bcefaa2), the first crash happens here (.ips crashlog symbolicated with LLDB):

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=50, address=0x104de8000)
  * frame #0: 0x000000018ee1067c libsystem_platform.dylib`sys_icache_invalidate + 40 [synthetic]
    frame #1: 0x0000000109177763 FridaGadget.dylib`gum_clear_cache + 23 [synthetic]
    frame #2: 0x000000010915b917 FridaGadget.dylib`gum_code_allocator_commit + 211 [synthetic]
    frame #3: 0x0000000109161327 FridaGadget.dylib`gum_interceptor_transaction_end + 75 [synthetic]
    frame #4: 0x000000010916186f FridaGadget.dylib`gum_interceptor_replace_with_type + 155 [synthetic]
    frame #5: 0x00000001091617c7 FridaGadget.dylib`gum_interceptor_replace + 63 [synthetic]
    frame #6: 0x000000010917c89b FridaGadget.dylib`_gum_unwind_broker_backend_activate + 119 [synthetic]
    frame #7: 0x0000000109160143 FridaGadget.dylib`gum_unwind_broker_obtain at gumunwindbroker.c:126:5 [synthetic] [opt]
    frame #8: 0x0000000109160e27 FridaGadget.dylib`gum_interceptor_obtain + 123 [synthetic]
    frame #9: 0x0000000109159317 FridaGadget.dylib`gum_init_embedded + 219 [synthetic]
    frame #10: 0x000000010924b1d3 FridaGadget.dylib`frida_libc_shim_init + 35 [synthetic]
    frame #11: 0x000000018ea74e2f dyld`invocation function for block in dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const::$_0::operator()() const + 179 [synthetic]
    frame #12: 0x000000018ea7fef3 dyld`invocation function for block in dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const + 171 [synthetic]
    frame #13: 0x000000018eab685f dyld`invocation function for block in mach_o::UnsafeHeader::forEachSection(void (mach_o::UnsafeHeader::SectionInfo const&, bool&) block_pointer) const + 311 [synthetic]
    frame #14: 0x000000018eab3393 dyld`mach_o::UnsafeHeader::forEachLoadCommand(void (load_command const*, bool&) block_pointer) const + 207 [synthetic]
    frame #15: 0x000000018eab4c3b dyld`mach_o::UnsafeHeader::forEachSection(void (mach_o::UnsafeHeader::SectionInfo const&, bool&) block_pointer) const + 123 [synthetic]
    frame #16: 0x000000018ea86af3 dyld`dyld3::MachOFile::forEachInitializerPointerSection(Diagnostics&, void (unsigned int, unsigned int, bool&) block_pointer) const + 159 [synthetic]
    frame #17: 0x000000018ea7fbb3 dyld`dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const + 431 [synthetic]
    frame #18: 0x000000018ea6f7d3 dyld`dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const + 527 [synthetic]
    frame #19: 0x000000018ea6840b dyld`dyld4::JustInTimeLoader::runInitializers(dyld4::RuntimeState&) const + 35 [synthetic]
    frame #20: 0x000000018ea6feef dyld`dyld4::Loader::runInitializersBottomUp(dyld4::RuntimeState&, lsl::Vector<AuthenticatedValue<dyld4::Loader const*>>&, lsl::Vector<AuthenticatedValue<dyld4::Loader const*>>&) const + 567 [synthetic]
    frame #21: 0x000000018ea6fde3 dyld`dyld4::Loader::runInitializersBottomUp(dyld4::RuntimeState&, lsl::Vector<AuthenticatedValue<dyld4::Loader const*>>&, lsl::Vector<AuthenticatedValue<dyld4::Loader const*>>&) const + 299 [synthetic]
    frame #22: 0x000000018ea74ec7 dyld`dyld4::Loader::runInitializersBottomUpPlusUpwardLinks(dyld4::RuntimeState&) const::$_0::operator()() const + 115 [synthetic]
    frame #23: 0x000000018ea701d7 dyld`dyld4::Loader::runInitializersBottomUpPlusUpwardLinks(dyld4::RuntimeState&) const + 715 [synthetic]
    frame #24: 0x000000018ea4938f dyld`dyld4::prepare(dyld4::APIs&, mach_o::UnsafeHeader const*) + 3879 [synthetic]
    frame #25: 0x000000018ea4844b dyld`dyld4::start(dyld4::KernelArgs*, void*, void*, unsigned long long)::$_1::operator()() const + 319 [synthetic]
    frame #26: 0x000000018ea47da7 dyld`start + 6903 [synthetic]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions