diff --git a/.github/workflows/ios-gadget-spike.yml b/.github/workflows/ios-gadget-spike.yml index 589bd08..b49fe1f 100644 --- a/.github/workflows/ios-gadget-spike.yml +++ b/.github/workflows/ios-gadget-spike.yml @@ -268,6 +268,66 @@ jobs: }, 5000); ''' + # Spike-only diagnostics. native-tls-hook.js could not load libboringssl.dylib at + # startup, and its deferred hook never fired either, though the app plainly does use + # that library later on. Rather than guessing why, report what Frida actually sees: + # + # - two module observers, one retained in a global & one not, since config.js discards + # the handle its own observer returns + # - the modules Frida can see once the app has really made a TLS connection + # - a hook applied by hand at that point, so we learn whether timing is the only + # problem: if the app's later requests are then intercepted, the rest all works + diagnostics = ''' + try { + globalThis.spikeRetainedObserver = Process.attachModuleObserver({ + onAdded(module) { + const name = module.name || module.path || "(unnamed)"; + if (name.indexOf("ssl") !== -1) console.log(`SPIKE-OBSERVER (retained): ${name}`); + } + }); + + Process.attachModuleObserver({ + onAdded(module) { + const name = module.name || module.path || "(unnamed)"; + if (name.indexOf("ssl") !== -1) console.log(`SPIKE-OBSERVER (unretained): ${name}`); + } + }); + + console.log("SPIKE-OBSERVER: both attached"); + } catch (e) { + console.log(`SPIKE-OBSERVER: could not attach: ${e}`); + } + + // Well after the app's first request, so anything TLS-related is definitely loaded: + setTimeout(() => { + try { + const modules = Process.enumerateModules() + .filter((module) => (module.name || "").indexOf("ssl") !== -1); + console.log(`SPIKE-MODULES: ${modules.length} loaded module(s) matching "ssl"`); + modules.forEach((module) => + console.log(`SPIKE-MODULES: ${module.name} @ ${module.path}`)); + } catch (e) { + console.log(`SPIKE-MODULES: could not enumerate: ${e}`); + } + + ["libboringssl.dylib", "/usr/lib/libboringssl.dylib"].forEach((name) => { + try { + const module = Process.getModuleByName(name); + console.log(`SPIKE-MODULES: getModuleByName(${name}) => ${module.path}`); + } catch (e) { + console.log(`SPIKE-MODULES: getModuleByName(${name}) failed: ${e}`); + } + }); + + try { + patchTargetLib(Process.getModuleByName("libboringssl.dylib"), "libboringssl.dylib"); + console.log("SPIKE-TLS-LATE: hooked by hand"); + } catch (e) { + console.log(`SPIKE-TLS-LATE: could not hook by hand: ${e}`); + } + }, 20000); + ''' + # The gadget's console output doesn't reach the simulator log at all (established by the # script mode check below), so we tee it to a file in the app's own data container, # which we can read back afterwards. Without this our scripts fail silently: @@ -300,7 +360,8 @@ jobs: ('native-connect-hook.js', open('native-connect-hook.js').read()), ('the spike additions', send_a_request.replace('__MARKER_PATH__', - os.environ['DATA_DIR'] + '/scripts-loaded.txt')) + os.environ['DATA_DIR'] + '/scripts-loaded.txt')), + ('the spike diagnostics', diagnostics) ] body = '\n'.join( @@ -605,6 +666,12 @@ jobs: "native-connect-hook hooked, with our config applied" check "== Redirecting Network framework connections to 127.0.0.1:8000 ==" \ "ios-connect-hook hooked Network framework" + # N.b. the spike's diagnostics hook libboringssl by hand 20s in, and that path logs the + # same message, so say so plainly rather than reporting a pass that isn't the script's: + if grep -qF "SPIKE-TLS-LATE: hooked by hand" <<< "$OUTPUT"; then + echo "N.b. the spike's diagnostics hooked libboringssl by hand, so the TLS checks" \ + "below show that it can be hooked at all, not that native-tls-hook did it itself" + fi check "== Hooked native TLS lib libboringssl.dylib ==" "native-tls-hook hooked iOS's TLS" # The real question: does traffic actually end up intercepted? Redirection and