Skip to content

Commit 41c185a

Browse files
authored
Merge pull request #1610 from HackTricks-wiki/update_Bypassing_iOS_Frida_Detection_with_LLDB_and_Frida_20251127_124030
Bypassing iOS Frida Detection with LLDB and Frida
2 parents 85c051b + 8cb5517 commit 41c185a

File tree

1 file changed

+75
-1
lines changed

1 file changed

+75
-1
lines changed

src/mobile-pentesting/ios-pentesting/frida-configuration-in-ios.md

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,79 @@ if (ObjC.available) {
358358
}
359359
```
360360

361+
## LLDB-Assisted Frida Detection Bypass & Swift Hooking
362+
363+
### Remote debugging pipeline
364+
365+
Penetration tests against production-like builds often require keeping jailbreak protections enabled while still attaching Frida. A reliable workflow is to pair Apple’s `debugserver` with LLDB over USB multiplexing:
366+
367+
1. Forward SSH so the jailbroken phone is reachable even without Wi-Fi: `iproxy 2222 22 &` followed by `ssh root@localhost -p 2222`.
368+
2. On the device, spawn the debugger stub and make it wait for the target process: `debugserver *:5678 --waitfor <BundleName>` and then launch the app from the SpringBoard.
369+
3. Forward the debugging port and attach LLDB from macOS:
370+
371+
```bash
372+
iproxy 1234 5678 &
373+
lldb
374+
(lldb) process connect connect://localhost:1234
375+
```
376+
377+
4. Use `finish` a few times so constructors return and LLDB can resolve every Swift/ObjC image before you start patching symbols.
378+
379+
Keeping `frida-server` running in parallel now becomes viable even if the app performs anti-instrumentation checks during startup.
380+
381+
### Patching Swift jailbreak / Frida checks
382+
383+
Swift apps frequently centralize jailbreak detection into a boolean helper such as `systemSanityCheck() -> Bool`. With LLDB already attached you can resolve the function name and force it to return `false` without touching the binary:
384+
385+
```bash
386+
(lldb) image lookup -rn 'frida'
387+
(lldb) image lookup -rn 'Check' FridaInTheMiddle.debug.dylib
388+
(lldb) breakpoint set --name 'FridaInTheMiddle.systemSanityCheck'
389+
(lldb) c
390+
(lldb) finish
391+
(lldb) register write x0 0
392+
(lldb) c
393+
```
394+
395+
On arm64 the Swift return value lives in `x0`, so zeroing that register after `finish` makes every caller believe the environment is clean, which keeps the UI alive while `frida-server` remains listening.
396+
397+
### Discovering Swift targets for Frida
398+
399+
Once the detection code is neutralized you can dynamically discover the mangled name of the function that handles sensitive data (e.g. the action behind a “Get Flag” button) instead of guessing:
400+
401+
```bash
402+
frida-trace -U <BundleName> -i "*dummy*"
403+
```
404+
405+
Trigger the UI action and `frida-trace` will log the exact symbol such as `$s16FridaInTheMiddle11ContentViewV13dummyFunction4flagySS_tF`. That string can be fed into `Module.load(<app>.debug.dylib).findExportByName()` inside a Frida script for precise hooking.
406+
407+
### Hooking Swift `String` arguments
408+
409+
Understanding the Swift ABI is essential to rebuild high-level arguments from registers when you intercept pure Swift functions:
410+
411+
- **Small strings (≤15 bytes)** are stored inline and the low byte of `x0` carries the length. The characters themselves are packed in the remainder of `x0`/`x1`.
412+
- **Large strings (>15 bytes)** are heap-backed objects. `x1` holds the pointer to the object header and the UTF‑8 buffer starts at `x1 + 32`.
413+
414+
A single hook can extract both cases without reverse engineering the app’s source:
415+
416+
```javascript
417+
const mod = Module.load('FridaInTheMiddle.debug.dylib')
418+
const fn = mod.findExportByName('$s16FridaInTheMiddle11ContentViewV13dummyFunction4flagySS_tF')
419+
Interceptor.attach(fn, {
420+
onEnter() {
421+
const inlineLen = this.context.x0.and(0xff)
422+
if (inlineLen.toInt32() > 0 && inlineLen.toInt32() <= 15) {
423+
console.log('flag:', this.context.x0.readUtf8String(inlineLen.toInt32()))
424+
return
425+
}
426+
const heapPtr = ptr(this.context.x1).add(32)
427+
console.log('flag:', heapPtr.readUtf8String())
428+
}
429+
})
430+
```
431+
432+
Instrumenting the function at this level means any secret `String` arguments—flags, session tokens, or dynamically generated credentials—can be dumped even when the UI never displays them. Combine this hook with the LLDB patch above to keep the app running under observation despite jailbreak or Frida detections.
433+
361434
## Frida Fuzzing
362435

363436
### Frida Stalker
@@ -1576,7 +1649,8 @@ console.log(" - changeProtection(address, size, 'rwx')")
15761649
## References
15771650

15781651
- [Great Reversing Training](https://reversing.training/ )
1579-
- [https://www.briskinfosec.com/blogs/blogsdetail/Getting-Started-with-Frida](https://www.briskinfosec.com/blogs/blogsdetail/Getting-Started-with-Frida)
1652+
- [Getting Started with Frida](https://www.briskinfosec.com/blogs/blogsdetail/Getting-Started-with-Frida)
1653+
- [Bypassing iOS Frida detection with LLDB and Frida](https://tonygo.tech/blog/2025/8ksec-ios-ctf-writeup)
15801654

15811655

15821656
{{#include ../../banners/hacktricks-training.md}}

0 commit comments

Comments
 (0)