Files
r2flutter/doc/tuto-authpass.md

7.8 KiB

AuthPass.app Dart AOT TLS tutorial

This tutorial uses the iOS fixture:

test/bins/ios/AuthPass.app

Run commands from the repository root. The Dart AOT image is not the top-level Runner executable; it is:

IOS_BUNDLE=test/bins/ios/AuthPass.app
IOS_APP="$IOS_BUNDLE/Frameworks/App.framework/App"

Rebuild the tools and plugin before following the r2 steps:

make
make r2
make user-install

Challenge 1: recover Dart AOT method names and map the TLS handshake path

Start by verifying that r2flutter recognizes the snapshot:

bin/r2flutter -j -H "$IOS_BUNDLE"

Expected important values:

"dart_version":"3.2.5"
"hash":"f71c76320d35b65f1164dbaa6d95fe09"
"it_len":37092
"tag_style":"CID_SHIFT1"

Dump recovered functions and resolve the TLS methods by semantic name. Do not hardcode the private suffix; AuthPass uses 14069316, but that value changes between snapshots.

FUNCS=$(mktemp)
bin/r2flutter -q -f "$IOS_BUNDLE" > "$FUNCS"

BAD_CERT_ADDR=$(awk '/method\._RawSecureSocket_.*\._onBadCertificateWrapper_/ { print $1; exit }' "$FUNCS")
REGISTER_ADDR=$(awk '/method\._SecureFilterImpl_.*\.registerBadCertificateCallback$/ { print $1; exit }' "$FUNCS")
REGISTER_IMPL_ADDR=$(awk '/method\._SecureFilterImpl_.*\._registerBadCertificateCallback_/ { print $1; exit }' "$FUNCS")
HANDSHAKE_ADDR=$(awk '/method\._RawSecureSocket_.*\._secureHandshake_/ { print $1; exit }' "$FUNCS")
MARK_TRUSTED_ADDR=$(awk '/method\._SecureFilterImpl_.*\._markAsTrusted_/ { print $1; exit }' "$FUNCS")

printf 'wrapper=%s register=%s register_impl=%s handshake=%s mark=%s\n' \
  "$BAD_CERT_ADDR" "$REGISTER_ADDR" "$REGISTER_IMPL_ADDR" "$HANDSHAKE_ADDR" "$MARK_TRUSTED_ADDR"
rm -f "$FUNCS"

Expected output:

wrapper=0x3199d4 register=0x3194ec register_impl=0x31954c handshake=0x31749c mark=0x31925c

Apply r2flutter flags in r2 and confirm that the same method flags exist:

r2 -q -e scr.color=0 -e bin.relocs.apply=true \
  -c 'r2flutter -A' \
  -c 'f~onBadCertificateWrapper' \
  -c 'f~registerBadCertificateCallback' \
  -c 'f~secureHandshake' \
  -c q "$IOS_APP"

Expected method flags:

0x003199d4 0 method._RawSecureSocket_14069316._onBadCertificateWrapper_14069316
0x003194ec 0 method._SecureFilterImpl_14069316.registerBadCertificateCallback
0x0031954c 0 method._SecureFilterImpl_14069316._registerBadCertificateCallback_14069316
0x0031749c 0 method._RawSecureSocket_14069316._secureHandshake_14069316

Now disassemble the handshake:

r2 -q -e scr.color=0 -e bin.relocs.apply=true \
  -c 'r2flutter -A' \
  -c "s ${HANDSHAKE_ADDR:-0x31749c}" \
  -c 'pd 104' \
  -c q "$IOS_APP"

Useful lines:

0x003174e4  bl method._SecureFilterImpl_14069316.handshake
0x00317514  bl method._RawSecureSocket_14069316._secureHandshake_14069316
0x0031752c  add x1, x22, 0x30
0x0031753c  bl method._RawSecureSocket_14069316._readSocket_14069316
0x00317548  bl method._RawSecureSocket_14069316._writeSocket_14069316
0x00317554  bl method._RawSecureSocket_14069316._scheduleFilter_14069316
0x00317588  bl method._RawSecureSocket_14069316._reportError_14069316
0x003175ac  add x0, x22, 0x20

The boolean constants are visible in this cluster:

  • x22 + 0x20 is Dart true.
  • x22 + 0x30 is Dart false.

Challenge 2: patch _RawSecureSocket._onBadCertificateWrapper

Disassemble the wrapper:

r2 -q -e scr.color=0 -e bin.relocs.apply=true \
  -c 'r2flutter -A' \
  -c "s ${BAD_CERT_ADDR:-0x3199d4}" \
  -c 'pd 24' \
  -c q "$IOS_APP"

Important instructions:

0x003199d4  stp x29, x30, [x15, -0x10]!
0x003199ec  ldr x0, [x29, 0x18]
0x003199f0  ldur x1, [x0, 0x5f]
0x003199f4  cmp x1, x22
0x003199f8  b.eq 0x319a28
0x00319a00  stp x16, x1, [x15]
0x00319a0c  ldur x2, [x0, 0x37]
0x00319a10  blr x2
0x00319a1c  ret

AuthPass differs from the Android samples: on the normal path the wrapper returns the app-provided bad-certificate callback result unchanged. The bypass target is still the wrapper entry. Returning Dart true before the callback logic makes the certificate decision succeed.

For this fixture, rabin2 -S "$IOS_APP" reports equal __text physical and virtual addresses:

rabin2 -S "$IOS_APP" | rg '__TEXT.__text'

Expected:

0   0x00005a80  0x6f8bd0 0x00005a80  0x6f8bd0 -r-x 0x0   REGULAR  0.__TEXT.__text

That means the recovered method virtual address is also the raw file offset for this fixture. If this is not true for another Mach-O, translate with:

section_paddr + (method_vaddr - section_vaddr)

Patch a copy first:

cp "$IOS_APP" "$IOS_APP.bak"

r2 -q -w -n -e scr.color=0 \
  -c "s ${BAD_CERT_ADDR:-0x3199d4}" \
  -c 'wx c0820091c0035fd6' \
  -c q "$IOS_APP"

Patch bytes:

c0820091 c0035fd6

Decoded AArch64:

add x0, x22, 0x20
ret

Verify:

r2 -q -n -a arm -b 64 -e scr.color=0 \
  -c "s ${BAD_CERT_ADDR:-0x3199d4}" \
  -c 'p8 8' \
  -c 'pd 2' \
  -c q "$IOS_APP"

Expected:

c0820091c0035fd6
add x0, x22, 0x20
ret

Restore the fixture after testing:

cp "$IOS_APP.bak" "$IOS_APP"

You can also use the generic qjs helper on a copy:

TMP_APP=$(mktemp /tmp/r2flutter-authpass-app.XXXXXX)
cp "$IOS_APP" "$TMP_APP"
r2 -q -w -e bin.relocs.apply=true \
  -i scripts/dart_cert_bypass.qjs \
  "$TMP_APP"
r2 -q -n -a arm -b 64 -e scr.color=0 \
  -c 's 0x3199d4' \
  -c 'p8 8' \
  -c 'pd 2' \
  -c q "$TMP_APP"
rm -f "$TMP_APP"

The script resolves method._RawSecureSocket_*._onBadCertificateWrapper_* through r2flutter flags, translates the method address to a file offset using section metadata, and writes the same two AArch64 instructions. It expects the actual Dart image to be opened in r2: Android libapp.so or iOS Frameworks/App.framework/App.

Challenge 3: trace callback registration and direct xrefs

Disassemble callback registration:

r2 -q -e scr.color=0 -e bin.relocs.apply=true \
  -c 'r2flutter -A' \
  -c "s ${REGISTER_ADDR:-0x3194ec}" \
  -c 'pd 44' \
  -c q "$IOS_APP"

Useful lines:

0x0031950c  stur x0, [x1, 0xf]
0x00319530  bl method._SecureFilterImpl_14069316._registerBadCertificateCallback_14069316
0x00319534  mov x0, x22
0x00319540  ret

Run lightweight reference analysis and query the resolved TLS addresses:

r2 -q -e scr.color=0 -e bin.relocs.apply=true \
  -c 'r2flutter -A' \
  -c 'aar' \
  -c "axt @ ${BAD_CERT_ADDR:-0x3199d4}" \
  -c "axt @ ${REGISTER_ADDR:-0x3194ec}" \
  -c "axt @ ${REGISTER_IMPL_ADDR:-0x31954c}" \
  -c "axt @ ${HANDSHAKE_ADDR:-0x31749c}" \
  -c q "$IOS_APP"

Observed direct xrefs:

0x3199bc -> 0x3199d4  _RawSecureSocket._anon_closure -> _onBadCertificateWrapper
0x316cd8 -> 0x3194ec  _RawSecureSocket constructor -> registerBadCertificateCallback
0x319530 -> 0x31954c  registerBadCertificateCallback -> _registerBadCertificateCallback
0x317028 -> 0x31749c  _RawSecureSocket constructor -> _secureHandshake
0x317514 -> 0x31749c  _secureHandshake -> _secureHandshake
0x3178b4 -> 0x31749c  _tryFilter -> _secureHandshake
0x317e04 -> 0x31749c  _closeHandler -> _secureHandshake

The call into the app-provided certificate callback inside the wrapper is indirect (blr x2), so it is not expected to produce a stable direct axt target. The stable evidence is the callback registration, the wrapper body, the TLS method cluster, and the direct references to the wrapper.

Regression tests

The tutorial workflow is covered by:

r2r -u test/db/cmd/authpass-r2-tutorial

The tests validate:

  • r2 plugin flag recovery for the AuthPass TLS methods.
  • wrapper and handshake disassembly landmarks.
  • direct xref/callsite sources without running full analysis in the test.
  • the generic qjs bypass script on a temporary copy.

Patching the embedded iOS framework invalidates the bundle code signature. A real deployment requires re-signing with the appropriate identity/provisioning workflow.