mirror of
https://github.com/httptoolkit/frida-interception-and-unpinning.git
synced 2026-10-23 00:03:41 +02:00
WIP: test different frida versions
This commit is contained in:
@@ -17,8 +17,11 @@ on:
|
||||
- 'native-*.js'
|
||||
|
||||
env:
|
||||
FRIDA_VERSION: 17.16.4
|
||||
FRIDA_OLD_VERSION: 16.7.19 # Only to tell version-specific breakage from the rest
|
||||
# Gadget 17.16.4 segfaults in its own initialisation on this simulator, with no script of ours
|
||||
# loaded at all, while 16.7.19 does not. So rather than pinning a version, we walk back from
|
||||
# the newest until one survives, which also tells us where it broke.
|
||||
GADGET_VERSIONS: >-
|
||||
17.16.4 17.14.0 17.12.0 17.10.0 17.8.0 17.6.0 17.4.0 17.2.0 17.0.0 16.7.19
|
||||
FRIDA_TOOLS_VERSION: 14.10.4 # Matched to the pin in ci.yml, for the ObjC bridge below
|
||||
APP_NAME: SpikeApp
|
||||
BUNDLE_ID: com.httptoolkit.gadget-spike
|
||||
@@ -144,22 +147,6 @@ jobs:
|
||||
# Confirms the simulator accepted the bundle, rather than finding out at launch:
|
||||
xcrun simctl listapps "$UDID" | grep -q "$BUNDLE_ID"
|
||||
|
||||
- name: Download the Frida gadget for the simulator
|
||||
run: |
|
||||
# N.b. the simulator build specifically - the normal iOS gadget is built for devices,
|
||||
# and won't load here. Two versions, so that a crash in the gadget itself can be told
|
||||
# apart from one specific to the current release:
|
||||
fetch() {
|
||||
curl -sSfL -o "gadget-$1.dylib.xz" \
|
||||
"https://github.com/frida/frida/releases/download/$1/frida-gadget-$1-ios-simulator-universal.dylib.xz"
|
||||
unxz "gadget-$1.dylib.xz"
|
||||
mv "gadget-$1.dylib" "$2"
|
||||
file "$2"
|
||||
}
|
||||
|
||||
fetch "$FRIDA_VERSION" "$RUNNER_TEMP/frida-gadget.dylib"
|
||||
fetch "$FRIDA_OLD_VERSION" "$RUNNER_TEMP/frida-gadget-old.dylib"
|
||||
|
||||
- name: Start a proxy to intercept through
|
||||
run: |
|
||||
# A real proxy, so we can prove interception end to end rather than just that our hooks
|
||||
@@ -207,6 +194,12 @@ jobs:
|
||||
# Only for its copy of the ObjC bridge - see below:
|
||||
python3 -m pip install --quiet --break-system-packages "frida-tools==$FRIDA_TOOLS_VERSION"
|
||||
|
||||
# The app's own data container: somewhere it can definitely write, so our scripts can
|
||||
# report that they ran without depending on log output being captured:
|
||||
DATA_DIR="$(xcrun simctl get_app_container "$UDID" "$BUNDLE_ID" data)"
|
||||
echo "DATA_DIR=$DATA_DIR" >> $GITHUB_ENV
|
||||
export DATA_DIR
|
||||
|
||||
python3 - <<'EOF'
|
||||
import os, re, pathlib, frida_tools
|
||||
|
||||
@@ -235,6 +228,16 @@ jobs:
|
||||
send_a_request = '''
|
||||
console.log("SPIKE-MARKER: all scripts loaded");
|
||||
|
||||
// Written as well as logged, since whether the gadget's console output reaches the
|
||||
// simulator log is a separate question from whether our scripts ran:
|
||||
try {
|
||||
const marker = new File("__MARKER_PATH__", "w");
|
||||
marker.write("all scripts loaded\\n");
|
||||
marker.close();
|
||||
} catch (e) {
|
||||
console.log(`SPIKE-MARKER: could not write the marker file: ${e}`);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (!ObjC.available) return console.log("SPIKE-REQUEST: no ObjC runtime");
|
||||
|
||||
@@ -274,7 +277,7 @@ jobs:
|
||||
open('ios/ios-disable-detection.js').read(),
|
||||
open('native-tls-hook.js').read(),
|
||||
open('native-connect-hook.js').read(),
|
||||
send_a_request
|
||||
send_a_request.replace('__MARKER_PATH__', os.environ['DATA_DIR'] + '/scripts-loaded.txt')
|
||||
]
|
||||
|
||||
with open(os.environ['RUNNER_TEMP'] + '/spike.js', 'w') as output:
|
||||
@@ -298,101 +301,136 @@ jobs:
|
||||
- name: Check the app runs without the gadget
|
||||
run: |
|
||||
# Otherwise a broken app and broken injection are indistinguishable, since both show up
|
||||
# as nothing at all in the logs:
|
||||
xcrun simctl launch "$UDID" "$BUNDLE_ID"
|
||||
# as nothing at all in the logs.
|
||||
#
|
||||
# N.b. checked by pid throughout: matching the app by name also matches our own log
|
||||
# stream commands, whose predicate contains the app's name.
|
||||
xcrun simctl launch "$UDID" "$BUNDLE_ID" | tee "$RUNNER_TEMP/baseline.log"
|
||||
sleep 5
|
||||
pgrep -fl "$APP_NAME"
|
||||
ps -p "$(awk '{print $2}' "$RUNNER_TEMP/baseline.log")"
|
||||
xcrun simctl terminate "$UDID" "$BUNDLE_ID"
|
||||
|
||||
- name: Work out which gadget configurations the app survives
|
||||
- name: Find a gadget version the app survives
|
||||
run: |
|
||||
# The gadget segfaulted inside its own initialisation last run - every frame was in
|
||||
# frida-gadget.dylib and the app's main was never reached - so these launches separate
|
||||
# the remaining possibilities: the gadget itself, this gadget version, script mode,
|
||||
# where the script lives, and our own scripts.
|
||||
#
|
||||
# Whether the app survives is the signal, since unlike log output it can't be lost.
|
||||
# Gadget 17.16.4 segfaults during its own initialisation here: listen mode with no
|
||||
# script of ours loaded crashes identically to a full run, and 16.7.19 doesn't crash at
|
||||
# all. So rather than pinning a version, walk back from the newest until one survives.
|
||||
# That gets us running, and shows where this broke.
|
||||
BUNDLE_DIR="$(xcrun simctl get_app_container "$UDID" "$BUNDLE_ID" app)"
|
||||
echo "BUNDLE_DIR=$BUNDLE_DIR" >> $GITHUB_ENV
|
||||
echo "Installed at: $BUNDLE_DIR"
|
||||
|
||||
# Inside the bundle, which is where a gadget would normally be shipped, and is
|
||||
# somewhere the app can definitely read from:
|
||||
# Inside the bundle, which is where a gadget is normally shipped, and is somewhere the
|
||||
# app can definitely read from:
|
||||
mkdir -p "$BUNDLE_DIR/Frameworks"
|
||||
cp "$RUNNER_TEMP/frida-gadget.dylib" "$BUNDLE_DIR/Frameworks/FridaGadget.dylib"
|
||||
cp "$RUNNER_TEMP/frida-gadget-old.dylib" "$BUNDLE_DIR/Frameworks/FridaGadgetOld.dylib"
|
||||
cp "$RUNNER_TEMP/spike.js" "$BUNDLE_DIR/Frameworks/spike.js"
|
||||
echo 'console.log("SPIKE-TRIVIAL: script mode works");' > "$RUNNER_TEMP/trivial.js"
|
||||
cp "$RUNNER_TEMP/trivial.js" "$BUNDLE_DIR/Frameworks/trivial.js"
|
||||
|
||||
# The bundle's contents just changed, so its seal needs redoing:
|
||||
# Listen mode with on_load resume: the gadget initialises, but runs nothing of ours and
|
||||
# doesn't hold the app waiting for a client, so this tests the gadget by itself.
|
||||
printf '%s\n' '{ "interaction": { "type": "listen", "on_load": "resume" } }' \
|
||||
> "$BUNDLE_DIR/Frameworks/FridaGadget.config"
|
||||
|
||||
WORKING=""
|
||||
for VERSION in $GADGET_VERSIONS; do
|
||||
echo
|
||||
echo "### Frida $VERSION"
|
||||
|
||||
if ! curl -sSfL -o "$RUNNER_TEMP/gadget.dylib.xz" \
|
||||
"https://github.com/frida/frida/releases/download/$VERSION/frida-gadget-$VERSION-ios-simulator-universal.dylib.xz"
|
||||
then
|
||||
echo " no simulator gadget published for this version"
|
||||
continue
|
||||
fi
|
||||
|
||||
rm -f "$RUNNER_TEMP/gadget.dylib"
|
||||
unxz "$RUNNER_TEMP/gadget.dylib.xz"
|
||||
cp "$RUNNER_TEMP/gadget.dylib" "$BUNDLE_DIR/Frameworks/FridaGadget.dylib"
|
||||
codesign --force --sign - "$BUNDLE_DIR" # The bundle's contents just changed
|
||||
|
||||
xcrun simctl terminate "$UDID" "$BUNDLE_ID" > /dev/null 2>&1 || true
|
||||
|
||||
if ! SIMCTL_CHILD_DYLD_INSERT_LIBRARIES="$BUNDLE_DIR/Frameworks/FridaGadget.dylib" \
|
||||
xcrun simctl launch "$UDID" "$BUNDLE_ID" > "$RUNNER_TEMP/attempt.log" 2>&1
|
||||
then
|
||||
echo " launch refused: $(cat "$RUNNER_TEMP/attempt.log")"
|
||||
continue
|
||||
fi
|
||||
|
||||
# N.b. by pid: matching the app by name also matches our own log stream commands,
|
||||
# whose predicate contains the app's name.
|
||||
PID="$(awk '{print $2}' "$RUNNER_TEMP/attempt.log")"
|
||||
sleep 10
|
||||
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
echo " the app survived (pid $PID)"
|
||||
WORKING="$VERSION"
|
||||
xcrun simctl terminate "$UDID" "$BUNDLE_ID" > /dev/null 2>&1 || true
|
||||
break
|
||||
fi
|
||||
|
||||
echo " the app crashed (pid $PID)"
|
||||
done
|
||||
|
||||
if [ -z "$WORKING" ]; then
|
||||
echo
|
||||
echo "No gadget version worked at all - see the crash reports below"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Using Frida $WORKING"
|
||||
echo "GADGET_VERSION=$WORKING" >> $GITHUB_ENV
|
||||
|
||||
- name: Check script mode works, and whether its output reaches the log
|
||||
run: |
|
||||
# Two separate questions: will the gadget run a script of ours at all, and does its
|
||||
# console output reach the simulator log? The second decides how much the log-based
|
||||
# checks below are worth, so the marker file is the signal that can't be lost.
|
||||
cat > "$BUNDLE_DIR/Frameworks/trivial.js" <<EOF
|
||||
console.log("SPIKE-TRIVIAL: script mode works");
|
||||
|
||||
try {
|
||||
const file = new File("$DATA_DIR/trivial-ran.txt", "w");
|
||||
file.write("ran\n");
|
||||
file.close();
|
||||
console.log("SPIKE-TRIVIAL: wrote the marker file");
|
||||
} catch (e) {
|
||||
console.log(\`SPIKE-TRIVIAL: could not write the marker file: \${e}\`);
|
||||
}
|
||||
EOF
|
||||
|
||||
printf '%s\n' \
|
||||
"{ \"interaction\": { \"type\": \"script\", \"path\": \"$BUNDLE_DIR/Frameworks/trivial.js\", \"on_change\": \"ignore\" } }" \
|
||||
> "$BUNDLE_DIR/Frameworks/FridaGadget.config"
|
||||
codesign --force --sign - "$BUNDLE_DIR"
|
||||
|
||||
rm -f "$DATA_DIR/trivial-ran.txt"
|
||||
|
||||
xcrun simctl spawn "$UDID" log stream --level debug \
|
||||
--predicate "processImagePath CONTAINS \"$APP_NAME\"
|
||||
OR eventMessage CONTAINS \"SPIKE-\"" > diagnose.log 2>&1 &
|
||||
--predicate 'eventMessage CONTAINS "SPIKE-"' > trivial.log 2>&1 &
|
||||
LOG_PID=$!
|
||||
sleep 5
|
||||
|
||||
attempt() {
|
||||
local description="$1" dylib="$2" config="$3"
|
||||
local config_path="${dylib%.dylib}.config"
|
||||
|
||||
echo
|
||||
echo "### $description"
|
||||
|
||||
rm -f "$config_path"
|
||||
if [ -n "$config" ]; then printf '%s\n' "$config" > "$config_path"; fi
|
||||
|
||||
xcrun simctl terminate "$UDID" "$BUNDLE_ID" > /dev/null 2>&1 || true
|
||||
|
||||
if SIMCTL_CHILD_DYLD_INSERT_LIBRARIES="$dylib" \
|
||||
xcrun simctl launch "$UDID" "$BUNDLE_ID" > "$RUNNER_TEMP/attempt.log" 2>&1
|
||||
then
|
||||
echo " launch: accepted ($(cat "$RUNNER_TEMP/attempt.log"))"
|
||||
else
|
||||
echo " launch: REFUSED ($(cat "$RUNNER_TEMP/attempt.log"))"
|
||||
return
|
||||
fi
|
||||
|
||||
sleep 10
|
||||
if pgrep -f "$APP_NAME" > /dev/null; then
|
||||
echo " result: the app is still running"
|
||||
else
|
||||
echo " result: the app died"
|
||||
fi
|
||||
|
||||
xcrun simctl terminate "$UDID" "$BUNDLE_ID" > /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# Listen mode with on_load resume: the gadget sets itself up, but runs nothing of ours
|
||||
# and doesn't hold the app at startup waiting for a client to attach.
|
||||
LISTEN='{ "interaction": { "type": "listen", "on_load": "resume" } }'
|
||||
script_config() {
|
||||
printf '{ "interaction": { "type": "script", "path": "%s", "on_change": "ignore" } }' "$1"
|
||||
}
|
||||
|
||||
attempt "Gadget $FRIDA_VERSION alone, no script of ours" \
|
||||
"$BUNDLE_DIR/Frameworks/FridaGadget.dylib" "$LISTEN"
|
||||
|
||||
attempt "Gadget $FRIDA_OLD_VERSION alone, no script of ours" \
|
||||
"$BUNDLE_DIR/Frameworks/FridaGadgetOld.dylib" "$LISTEN"
|
||||
|
||||
attempt "Trivial script, from inside the app bundle" \
|
||||
"$BUNDLE_DIR/Frameworks/FridaGadget.dylib" \
|
||||
"$(script_config "$BUNDLE_DIR/Frameworks/trivial.js")"
|
||||
|
||||
attempt "Trivial script, from outside the bundle" \
|
||||
"$RUNNER_TEMP/frida-gadget.dylib" "$(script_config "$RUNNER_TEMP/trivial.js")"
|
||||
|
||||
attempt "Our full script" \
|
||||
"$BUNDLE_DIR/Frameworks/FridaGadget.dylib" \
|
||||
"$(script_config "$BUNDLE_DIR/Frameworks/spike.js")"
|
||||
xcrun simctl terminate "$UDID" "$BUNDLE_ID" > /dev/null 2>&1 || true
|
||||
SIMCTL_CHILD_DYLD_INSERT_LIBRARIES="$BUNDLE_DIR/Frameworks/FridaGadget.dylib" \
|
||||
xcrun simctl launch "$UDID" "$BUNDLE_ID"
|
||||
sleep 10
|
||||
|
||||
kill $LOG_PID 2>/dev/null || true
|
||||
echo
|
||||
echo "=== log output during the attempts:"
|
||||
cat diagnose.log
|
||||
xcrun simctl terminate "$UDID" "$BUNDLE_ID" > /dev/null 2>&1 || true
|
||||
|
||||
echo "=== log output:"
|
||||
cat trivial.log
|
||||
if grep -q "SPIKE-TRIVIAL" trivial.log; then
|
||||
echo "=> the gadget's console output does reach the simulator log"
|
||||
else
|
||||
echo "=> the gadget's console output does NOT reach the simulator log, so only the" \
|
||||
"marker file & proxy evidence mean anything below"
|
||||
fi
|
||||
|
||||
# Fatal, because nothing else below can work without it:
|
||||
test -f "$DATA_DIR/trivial-ran.txt"
|
||||
echo "=> the gadget ran our script"
|
||||
|
||||
- name: Launch the app with the gadget injected
|
||||
run: |
|
||||
@@ -428,16 +466,18 @@ jobs:
|
||||
exit 1
|
||||
}
|
||||
cat launch.log
|
||||
APP_PID="$(awk '{print $2}' launch.log)"
|
||||
echo "APP_PID=$APP_PID" >> $GITHUB_ENV
|
||||
|
||||
echo "=== app process immediately after launch:"
|
||||
pgrep -fl "$APP_NAME" || echo "(no - it did not survive startup)"
|
||||
ps -p "$APP_PID" || echo "(gone - it did not survive startup)"
|
||||
|
||||
# The app requests every 10s, so this covers several:
|
||||
sleep 45
|
||||
kill $LOG_PID 2>/dev/null || true
|
||||
|
||||
echo "=== app process still running?"
|
||||
pgrep -fl "$APP_NAME" || echo "(no - the app is not running)"
|
||||
ps -p "$APP_PID" || echo "(gone - the app is not running)"
|
||||
|
||||
- name: Report what happened
|
||||
if: always() # Especially when the launch itself failed
|
||||
@@ -509,6 +549,15 @@ jobs:
|
||||
# The marker proves every script ran to completion; the rest prove they did something.
|
||||
# N.b. these match the scripts' success messages specifically - "libboringssl.dylib"
|
||||
# alone would also match the message logged when hooking it fails:
|
||||
# The marker file, rather than the log line, is what proves our scripts ran to
|
||||
# completion: it doesn't depend on the gadget's output being captured at all.
|
||||
if [ -f "$DATA_DIR/scripts-loaded.txt" ]; then
|
||||
echo "PASS: our scripts ran to completion under the gadget"
|
||||
else
|
||||
echo "FAIL: our scripts did not run to completion (no marker file written)"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
check "SPIKE-APP: launched" "the app started with the gadget injected"
|
||||
check "SPIKE-MARKER: all scripts loaded" "our scripts ran under the gadget"
|
||||
check "== Redirecting all TCP connections to 127.0.0.1:8000 ==" \
|
||||
@@ -526,7 +575,7 @@ jobs:
|
||||
check "PROXY-SAW: https://example.com/spike" "our script's HTTPS request was intercepted"
|
||||
check "SPIKE-REQUEST: got status 200" "our script got the proxy's response back"
|
||||
|
||||
if ! pgrep -f "$APP_NAME" > /dev/null; then
|
||||
if ! ps -p "$APP_PID" > /dev/null 2>&1; then
|
||||
echo "FAIL: the app is not running - it may have crashed (see the report above)"
|
||||
FAILED=1
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user