mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-07-20 22:40:58 +03:00
Add 30 new production-grade cybersecurity skills: AI security, supply chain, firmware, cloud-native, compliance, deception, crypto, threat hunting, purple team, OT, privacy
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by the Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding any notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. Please do not remove or change
|
||||
the license header comment from a contributed file except when
|
||||
necessary.
|
||||
|
||||
Copyright 2026 mukul975
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,268 @@
|
||||
---
|
||||
name: performing-ios-app-security-assessment
|
||||
description: >
|
||||
Performs comprehensive iOS application security assessments using Frida for dynamic
|
||||
instrumentation, Objection for runtime exploration, SSL pinning bypass for traffic
|
||||
interception, keychain extraction for credential analysis, and IPA static analysis
|
||||
for binary-level review. Use when conducting authorized iOS penetration tests, evaluating
|
||||
mobile app security posture against OWASP MASTG, or assessing iOS app data protection
|
||||
and transport security controls. Activates for requests involving iOS app pentesting,
|
||||
Frida-based iOS instrumentation, mobile app SSL pinning bypass, or IPA reverse engineering.
|
||||
domain: cybersecurity
|
||||
subdomain: mobile-security
|
||||
author: mukul975
|
||||
tags: [mobile-security, ios, frida, objection, ssl-pinning, keychain, ipa-analysis, owasp-mastg]
|
||||
version: 1.0.0
|
||||
license: Apache-2.0
|
||||
---
|
||||
# Performing iOS App Security Assessment
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This skill is intended for authorized security testing, penetration testing engagements, CTF competitions, and educational purposes only. Unauthorized access to applications or devices is illegal. Always obtain written authorization before performing any security assessment. Misuse of these techniques may violate computer fraud and abuse laws in your jurisdiction.
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this skill when:
|
||||
- Conducting authorized penetration tests of iOS applications against OWASP MASVS/MASTG criteria
|
||||
- Performing dynamic analysis of iOS apps using Frida instrumentation and Objection runtime exploration
|
||||
- Bypassing SSL/TLS certificate pinning to intercept and analyze app network traffic through a proxy
|
||||
- Extracting and auditing iOS Keychain contents for insecure credential storage practices
|
||||
- Performing static analysis of IPA packages to identify hardcoded secrets, entitlements, and binary protections
|
||||
- Assessing jailbreak detection and anti-tampering controls in iOS applications
|
||||
|
||||
**Do not use** against applications without explicit written authorization. Do not use on production devices containing real user data unless the engagement scope permits it.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10+ with pip
|
||||
- Frida toolkit: `pip install frida-tools frida`
|
||||
- Objection: `pip install objection`
|
||||
- Target iOS device (jailbroken with frida-server, or non-jailbroken with patched IPA)
|
||||
- macOS with Xcode command-line tools (recommended for code signing and ideviceinstaller)
|
||||
- Burp Suite or mitmproxy for traffic interception after SSL pinning bypass
|
||||
- For jailbroken devices: SSH access and frida-server running on the device
|
||||
- For non-jailbroken devices: Apple Developer certificate for IPA re-signing
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: IPA Static Analysis
|
||||
|
||||
Extract and analyze the IPA binary before runtime testing:
|
||||
|
||||
```bash
|
||||
# Unzip IPA for static analysis
|
||||
unzip target.ipa -d target_app/
|
||||
|
||||
# Check binary architectures and protections
|
||||
otool -hv target_app/Payload/*.app/AppExecutable
|
||||
otool -l target_app/Payload/*.app/AppExecutable | grep -A4 LC_ENCRYPTION
|
||||
|
||||
# Extract Info.plist for entitlements and URL schemes
|
||||
plutil -p target_app/Payload/*.app/Info.plist
|
||||
|
||||
# Search for hardcoded secrets in binary strings
|
||||
strings target_app/Payload/*.app/AppExecutable | grep -iE "api[_-]?key|secret|password|token|firebase"
|
||||
|
||||
# Check embedded provisioning profile
|
||||
security cms -D -i target_app/Payload/*.app/embedded.mobileprovision
|
||||
|
||||
# Identify linked frameworks
|
||||
otool -L target_app/Payload/*.app/AppExecutable
|
||||
```
|
||||
|
||||
### Step 2: Environment Setup and Frida Attachment
|
||||
|
||||
```bash
|
||||
# For jailbroken device: verify Frida server is running
|
||||
frida-ps -U
|
||||
|
||||
# Spawn target app with Frida
|
||||
frida -U -f com.target.app --no-pause
|
||||
|
||||
# For non-jailbroken device: patch IPA with Frida Gadget
|
||||
objection patchipa --source target.ipa --codesign-signature "Apple Development: tester@example.com"
|
||||
|
||||
# Install patched IPA
|
||||
ideviceinstaller -i target-patched.ipa
|
||||
|
||||
# Attach Objection to running app
|
||||
objection --gadget "com.target.app" explore
|
||||
```
|
||||
|
||||
### Step 3: SSL Pinning Bypass
|
||||
|
||||
Bypass certificate pinning to enable traffic interception:
|
||||
|
||||
```bash
|
||||
# Using Objection's built-in bypass
|
||||
objection --gadget "com.target.app" explore --startup-command "ios sslpinning disable"
|
||||
|
||||
# Using Frida script for more comprehensive bypass
|
||||
frida -U -f com.target.app -l ssl_pinning_bypass.js --no-pause
|
||||
|
||||
# Verify bypass by configuring device proxy to Burp Suite
|
||||
# Device Settings -> Wi-Fi -> HTTP Proxy -> Manual -> <burp_ip>:8080
|
||||
# Install Burp CA certificate on device via http://<burp_ip>:8080/cert
|
||||
```
|
||||
|
||||
The Frida SSL pinning bypass script hooks into NSURLSession, NSURLConnection, and
|
||||
AFNetworking/Alamofire trust evaluation delegates to override certificate validation
|
||||
at the TLS handshake level.
|
||||
|
||||
### Step 4: Keychain Extraction and Credential Analysis
|
||||
|
||||
```bash
|
||||
# Dump all accessible keychain items via Objection
|
||||
ios keychain dump
|
||||
|
||||
# Dump keychain with raw data output
|
||||
ios keychain dump --json
|
||||
|
||||
# Check keychain item accessibility attributes
|
||||
# Items with kSecAttrAccessibleAlways or kSecAttrAccessibleAfterFirstUnlock
|
||||
# are accessible without device unlock - this is a finding
|
||||
|
||||
# Search for specific credential types
|
||||
ios keychain dump | grep -i "password\|token\|secret\|oauth"
|
||||
|
||||
# Inspect NSUserDefaults for sensitive data leaks
|
||||
ios nsuserdefaults get
|
||||
|
||||
# Check for sensitive data in app cookies
|
||||
ios cookies get
|
||||
```
|
||||
|
||||
### Step 5: Runtime Method Hooking and Analysis
|
||||
|
||||
```bash
|
||||
# List all loaded classes
|
||||
ios hooking list classes
|
||||
|
||||
# Search for security-relevant classes
|
||||
ios hooking search classes Auth
|
||||
ios hooking search classes Crypto
|
||||
ios hooking search classes Biometric
|
||||
ios hooking search classes Jailbreak
|
||||
|
||||
# Hook authentication methods to observe parameters and return values
|
||||
ios hooking watch method "+[AuthManager validateCredentials:password:]" --dump-args --dump-return
|
||||
|
||||
# Monitor biometric authentication (LocalAuthentication framework)
|
||||
ios hooking watch class LAContext
|
||||
|
||||
# Bypass jailbreak detection
|
||||
ios jailbreak disable
|
||||
|
||||
# Search memory for sensitive strings
|
||||
memory search "Bearer " --string
|
||||
memory search "password" --string
|
||||
|
||||
# Dump loaded modules for third-party library identification
|
||||
memory list modules
|
||||
```
|
||||
|
||||
### Step 6: Data Storage Assessment
|
||||
|
||||
```bash
|
||||
# List files in app sandbox
|
||||
env
|
||||
|
||||
# Check for SQLite databases with sensitive data
|
||||
sqlite connect Documents/app.db
|
||||
sqlite execute query "SELECT name FROM sqlite_master WHERE type='table'"
|
||||
|
||||
# Inspect plist files for cached credentials
|
||||
ios plist cat Library/Preferences/com.target.app.plist
|
||||
|
||||
# Check for sensitive data in app caches
|
||||
find Library/Caches/ -type f
|
||||
|
||||
# Monitor pasteboard for credential leakage
|
||||
ios pasteboard monitor
|
||||
|
||||
# Check binary cookies
|
||||
ios cookies get
|
||||
```
|
||||
|
||||
### Step 7: Network and Transport Security Assessment
|
||||
|
||||
After SSL pinning bypass, analyze intercepted traffic:
|
||||
|
||||
```bash
|
||||
# Verify App Transport Security (ATS) configuration in Info.plist
|
||||
# Check for NSAllowsArbitraryLoads = true (disables ATS)
|
||||
ios plist cat Info.plist | grep -A5 NSAppTransportSecurity
|
||||
|
||||
# Hook URL session delegates to monitor all network calls
|
||||
ios hooking watch class NSURLSession
|
||||
ios hooking watch class NSURLSessionConfiguration
|
||||
|
||||
# Check for certificate transparency validation
|
||||
ios hooking search classes CT
|
||||
ios hooking search classes Certificate
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
| Term | Definition |
|
||||
|------|-----------|
|
||||
| **Frida** | Dynamic instrumentation toolkit that injects a JavaScript engine into target processes, enabling runtime hooking, tracing, and modification of iOS app behavior |
|
||||
| **Objection** | Runtime mobile exploration toolkit built on Frida providing pre-built commands for common security tests including keychain dump, SSL pinning bypass, and method hooking |
|
||||
| **SSL Pinning** | Client-side certificate validation that restricts which TLS certificates the app trusts, preventing proxy-based traffic interception; bypassed by hooking trust evaluation functions |
|
||||
| **Keychain** | iOS secure storage API for credentials and tokens; items have accessibility attributes that control when they can be read (e.g., only when device is unlocked) |
|
||||
| **IPA** | iOS App Store Package; a ZIP archive containing the app binary, frameworks, assets, and provisioning profile that can be extracted for static analysis |
|
||||
| **OWASP MASTG** | Mobile Application Security Testing Guide; comprehensive methodology for iOS and Android security testing organized by MASVS verification categories |
|
||||
| **Frida Gadget** | Shared library (.dylib) injected into IPA for non-jailbroken testing; enables Frida instrumentation without requiring a jailbroken device |
|
||||
| **Method Swizzling** | Objective-C runtime technique that exchanges method implementations at runtime; used by Frida to intercept and modify method behavior |
|
||||
|
||||
## Tools & Systems
|
||||
|
||||
- **Frida**: Dynamic instrumentation framework for injecting JavaScript into native app processes at runtime
|
||||
- **Objection**: High-level Frida-powered mobile security toolkit with pre-built exploration commands
|
||||
- **frida-tools**: CLI utilities including frida-ps (process listing), frida-trace (method tracing), frida-discover (API discovery)
|
||||
- **Burp Suite**: HTTP/HTTPS interception proxy used to analyze app traffic after SSL pinning bypass
|
||||
- **ideviceinstaller**: Cross-platform CLI tool for installing and managing iOS apps over USB
|
||||
- **otool / rabin2**: Binary analysis tools for inspecting Mach-O headers, linked libraries, and encryption info
|
||||
- **Cycript / Frida REPL**: Interactive consoles for exploring Objective-C runtime and modifying objects in memory
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Frida detection crashes the app**: Some apps implement Frida detection by scanning for frida-server process names, Frida's RPC ports, or gadget signatures. Use `--startup-command` to hook detection checks before they execute, or rename frida-server binary.
|
||||
- **Keychain scope limitation**: Objection can only access keychain items within the app's keychain access group. System-wide keychain items require jailbreak-level tools like keychain-dumper.
|
||||
- **Swift name mangling**: Swift method names are mangled in the Objective-C runtime. Use `ios hooking list classes` and grep for demangled names, or use frida-trace with wildcard patterns.
|
||||
- **App Transport Security enforcement**: ATS may block your proxy connections even after SSL pinning bypass. Verify the Info.plist ATS configuration allows your proxy's certificate chain.
|
||||
- **Code signing invalidation**: Patching an IPA with Frida Gadget invalidates the original code signature. You need a valid Apple Developer certificate to re-sign the patched IPA.
|
||||
- **Non-persistent modifications**: All Frida/Objection hooks are runtime-only and reset when the app restarts. Document findings and capture evidence immediately.
|
||||
|
||||
## Output Format
|
||||
|
||||
```
|
||||
## Finding: Insecure Keychain Storage with kSecAttrAccessibleAlways
|
||||
|
||||
**ID**: IOS-001
|
||||
**Severity**: High (CVSS 7.5)
|
||||
**OWASP MASTG**: MASTG-TEST-0055 (Testing Data Storage)
|
||||
**MASVS Category**: MASVS-STORAGE
|
||||
|
||||
**Description**:
|
||||
The application stores OAuth refresh tokens in the iOS Keychain with
|
||||
the accessibility attribute kSecAttrAccessibleAlways, making them
|
||||
readable even when the device is locked or after a reboot without
|
||||
user authentication.
|
||||
|
||||
**Proof of Concept**:
|
||||
1. Attach Objection to com.target.app: objection --gadget com.target.app explore
|
||||
2. Execute: ios keychain dump
|
||||
3. Observe refresh_token item with Accessible: kSecAttrAccessibleAlways
|
||||
4. Token value is accessible without device unlock
|
||||
|
||||
**Impact**:
|
||||
An attacker with physical access to a locked device or forensic
|
||||
image can extract OAuth refresh tokens and gain persistent access
|
||||
to the user's account without knowing device passcode.
|
||||
|
||||
**Remediation**:
|
||||
Store sensitive credentials with kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
||||
and enable biometric protection via kSecAccessControlBiometryCurrentSet.
|
||||
```
|
||||
@@ -0,0 +1,78 @@
|
||||
# API Reference: iOS App Security Assessment Agent
|
||||
|
||||
## Overview
|
||||
|
||||
Automates iOS application security testing using Frida dynamic instrumentation, Objection runtime exploration, SSL pinning bypass, keychain extraction, and IPA static analysis. Covers OWASP MASVS categories including STORAGE, NETWORK, AUTH, RESILIENCE, and PLATFORM. For authorized penetration testing only.
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| frida | >=16.0 | Dynamic instrumentation framework for iOS process injection |
|
||||
| frida-tools | >=12.0 | CLI utilities (frida-ps, frida-trace) for device interaction |
|
||||
| objection | >=1.11 | High-level Frida-powered mobile security exploration toolkit |
|
||||
|
||||
## CLI Usage
|
||||
|
||||
```bash
|
||||
# Static IPA analysis only
|
||||
python agent.py --ipa target.ipa --output-dir ./analysis
|
||||
|
||||
# Dynamic testing with SSL pinning bypass and keychain dump
|
||||
python agent.py --bundle-id com.target.app --ssl-bypass --keychain --output report.json
|
||||
|
||||
# Full assessment with jailbreak bypass
|
||||
python agent.py --bundle-id com.target.app --ipa target.ipa \
|
||||
--ssl-bypass --keychain --jailbreak-bypass \
|
||||
--device usb --frida-timeout 45 --output full_report.json
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
| Argument | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `--bundle-id` | Conditional | Target app bundle identifier for dynamic testing |
|
||||
| `--ipa` | Conditional | Path to IPA file for static analysis |
|
||||
| `--device` | No | Frida device type: `usb`, `remote`, `local` (default: `usb`) |
|
||||
| `--ssl-bypass` | No | Execute SSL pinning bypass Frida script |
|
||||
| `--keychain` | No | Dump and analyze keychain item security |
|
||||
| `--jailbreak-bypass` | No | Execute jailbreak detection bypass script |
|
||||
| `--frida-timeout` | No | Frida script execution timeout in seconds (default: 30) |
|
||||
| `--output` | No | Output report file path (default: `ios_assessment_report.json`) |
|
||||
| `--output-dir` | No | Directory for IPA extraction artifacts (default: `.`) |
|
||||
|
||||
At least one of `--bundle-id` or `--ipa` is required.
|
||||
|
||||
## Key Functions
|
||||
|
||||
### `analyze_ipa_static(ipa_path, output_dir)`
|
||||
Extracts and statically analyzes an IPA package. Checks Info.plist for ATS configuration, URL schemes, and background modes. Scans binary strings for hardcoded API keys, secrets, AWS credentials, Firebase URLs, and private keys. Inspects provisioning profile for debug entitlements.
|
||||
|
||||
### `run_frida_script(target_bundle, script_source, device_type, timeout_sec)`
|
||||
Executes a Frida JavaScript payload against a target iOS app. Attempts to attach to a running process first, falls back to spawning the app. Collects messages sent from the Frida script via the `send()` API.
|
||||
|
||||
### `run_objection_command(bundle_id, command)`
|
||||
Runs a single Objection command against the target app using subprocess. Returns stdout, stderr, and return code. Handles timeout and missing installation gracefully.
|
||||
|
||||
### `assess_keychain_security(bundle_id)`
|
||||
Dumps keychain items via Objection and analyzes accessibility attributes. Flags items using insecure attributes (kSecAttrAccessibleAlways, kSecAttrAccessibleAfterFirstUnlock) and passwords lacking biometric/passcode access control.
|
||||
|
||||
### `generate_report(findings, target_app, output_path)`
|
||||
Aggregates all findings into a JSON report with severity breakdown (critical/high/medium/low) and metadata including timestamp and target identifier.
|
||||
|
||||
## Frida Script Payloads
|
||||
|
||||
| Script | Target APIs | Purpose |
|
||||
|--------|-------------|---------|
|
||||
| `SSL_PINNING_BYPASS_SCRIPT` | SecTrustEvaluate, SecTrustEvaluateWithError, AFSecurityPolicy, TSKPinningValidator | Bypasses certificate pinning across system and third-party frameworks |
|
||||
| `KEYCHAIN_DUMP_SCRIPT` | SecItemCopyMatching | Enumerates keychain item classes and counts accessible items |
|
||||
| `JAILBREAK_DETECTION_BYPASS_SCRIPT` | NSFileManager, UIApplication canOpenURL, fork() | Hides jailbreak indicators from filesystem, URL scheme, and process checks |
|
||||
|
||||
## OWASP MASVS Coverage
|
||||
|
||||
| MASVS Category | Tests | Functions |
|
||||
|----------------|-------|-----------|
|
||||
| MASVS-STORAGE | MASTG-TEST-0055, 0058 | `assess_keychain_security`, `analyze_ipa_static` |
|
||||
| MASVS-NETWORK | MASTG-TEST-0066, 0068 | SSL pinning bypass, ATS configuration check |
|
||||
| MASVS-RESILIENCE | MASTG-TEST-0079, 0083 | Jailbreak bypass, debug entitlement check |
|
||||
| MASVS-PLATFORM | MASTG-TEST-0075 | URL scheme analysis |
|
||||
@@ -0,0 +1,616 @@
|
||||
#!/usr/bin/env python3
|
||||
# For authorized penetration testing and lab environments only
|
||||
"""iOS App Security Assessment Agent - Automates Frida-based iOS security testing including
|
||||
SSL pinning bypass, keychain extraction, IPA static analysis, and runtime method hooking."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import plistlib
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Frida JavaScript payloads for iOS instrumentation
|
||||
SSL_PINNING_BYPASS_SCRIPT = r"""
|
||||
'use strict';
|
||||
|
||||
// Hook NSURLSessionDelegate certificate challenge methods
|
||||
if (ObjC.available) {
|
||||
try {
|
||||
// Bypass NSURLSession certificate pinning
|
||||
var NSURLSessionTask = ObjC.classes.NSURLSessionTask;
|
||||
var resolveMethod = ObjC.classes.NSURLSession['- URLSession:didReceiveChallenge:completionHandler:'];
|
||||
|
||||
Interceptor.attach(
|
||||
ObjC.classes.NSURLSession['- URLSession:didReceiveChallenge:completionHandler:'].implementation, {
|
||||
onEnter: function(args) {
|
||||
send('[SSL Bypass] Intercepted NSURLSession challenge');
|
||||
}
|
||||
});
|
||||
} catch(e) {}
|
||||
|
||||
// Hook SecTrustEvaluate to always return success
|
||||
try {
|
||||
var SecTrustEvaluate = Module.findExportByName('Security', 'SecTrustEvaluate');
|
||||
if (SecTrustEvaluate) {
|
||||
Interceptor.attach(SecTrustEvaluate, {
|
||||
onLeave: function(retval) {
|
||||
retval.replace(0); // errSecSuccess
|
||||
send('[SSL Bypass] SecTrustEvaluate -> success');
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// Hook SecTrustEvaluateWithError (iOS 12+)
|
||||
try {
|
||||
var SecTrustEvaluateWithError = Module.findExportByName('Security', 'SecTrustEvaluateWithError');
|
||||
if (SecTrustEvaluateWithError) {
|
||||
Interceptor.attach(SecTrustEvaluateWithError, {
|
||||
onLeave: function(retval) {
|
||||
retval.replace(1); // true = trusted
|
||||
send('[SSL Bypass] SecTrustEvaluateWithError -> trusted');
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// Hook SecTrustGetTrustResult
|
||||
try {
|
||||
var SecTrustGetTrustResult = Module.findExportByName('Security', 'SecTrustGetTrustResult');
|
||||
if (SecTrustGetTrustResult) {
|
||||
Interceptor.attach(SecTrustGetTrustResult, {
|
||||
onLeave: function(retval) {
|
||||
retval.replace(0);
|
||||
send('[SSL Bypass] SecTrustGetTrustResult -> proceed');
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// Bypass AFNetworking pinning
|
||||
try {
|
||||
var AFSecurityPolicy = ObjC.classes.AFSecurityPolicy;
|
||||
if (AFSecurityPolicy) {
|
||||
Interceptor.attach(AFSecurityPolicy['- setSSLPinningMode:'].implementation, {
|
||||
onEnter: function(args) {
|
||||
args[2] = ptr(0); // AFSSLPinningModeNone
|
||||
send('[SSL Bypass] AFNetworking pinning disabled');
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// Bypass TrustKit pinning
|
||||
try {
|
||||
var TrustKit = ObjC.classes.TSKPinningValidator;
|
||||
if (TrustKit) {
|
||||
Interceptor.attach(TrustKit['- evaluateTrust:forHostname:'].implementation, {
|
||||
onLeave: function(retval) {
|
||||
retval.replace(0); // TSKTrustEvaluationSuccess
|
||||
send('[SSL Bypass] TrustKit pinning bypassed');
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
send('[SSL Bypass] All hooks installed successfully');
|
||||
} else {
|
||||
send('[SSL Bypass] Objective-C runtime not available');
|
||||
}
|
||||
"""
|
||||
|
||||
KEYCHAIN_DUMP_SCRIPT = r"""
|
||||
'use strict';
|
||||
|
||||
if (ObjC.available) {
|
||||
var SecItemCopyMatching = Module.findExportByName('Security', 'SecItemCopyMatching');
|
||||
var results = [];
|
||||
|
||||
// Enumerate keychain item classes
|
||||
var kSecClasses = [
|
||||
'genp', // kSecClassGenericPassword
|
||||
'inet', // kSecClassInternetPassword
|
||||
'cert', // kSecClassCertificate
|
||||
'keys', // kSecClassKey
|
||||
'idnt' // kSecClassIdentity
|
||||
];
|
||||
|
||||
var classNames = {
|
||||
'genp': 'GenericPassword',
|
||||
'inet': 'InternetPassword',
|
||||
'cert': 'Certificate',
|
||||
'keys': 'CryptoKey',
|
||||
'idnt': 'Identity'
|
||||
};
|
||||
|
||||
for (var i = 0; i < kSecClasses.length; i++) {
|
||||
try {
|
||||
var query = ObjC.classes.NSMutableDictionary.alloc().init();
|
||||
query.setObject_forKey_(kSecClasses[i], 'class');
|
||||
query.setObject_forKey_(ObjC.classes.__NSCFBoolean.numberWithBool_(true), 'r_Ref');
|
||||
query.setObject_forKey_('m_LimitAll', 'm_Limit');
|
||||
query.setObject_forKey_(ObjC.classes.__NSCFBoolean.numberWithBool_(true), 'r_Data');
|
||||
query.setObject_forKey_(ObjC.classes.__NSCFBoolean.numberWithBool_(true), 'r_Attributes');
|
||||
|
||||
var resultPtr = Memory.alloc(Process.pointerSize);
|
||||
var status = new NativeFunction(SecItemCopyMatching, 'int', ['pointer', 'pointer']);
|
||||
var ret = status(query.handle, resultPtr);
|
||||
|
||||
if (ret === 0) {
|
||||
var resultObj = new ObjC.Object(resultPtr.readPointer());
|
||||
send({
|
||||
type: 'keychain',
|
||||
class: classNames[kSecClasses[i]],
|
||||
count: resultObj.count ? resultObj.count() : 1
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
send({type: 'keychain_error', class: classNames[kSecClasses[i]], error: e.toString()});
|
||||
}
|
||||
}
|
||||
send({type: 'keychain_complete'});
|
||||
}
|
||||
"""
|
||||
|
||||
JAILBREAK_DETECTION_BYPASS_SCRIPT = r"""
|
||||
'use strict';
|
||||
|
||||
if (ObjC.available) {
|
||||
// Hook NSFileManager fileExistsAtPath: to hide jailbreak indicators
|
||||
var NSFileManager = ObjC.classes.NSFileManager;
|
||||
var fileExistsAtPath = NSFileManager['- fileExistsAtPath:'];
|
||||
|
||||
var jailbreakPaths = [
|
||||
'/Applications/Cydia.app',
|
||||
'/Applications/Sileo.app',
|
||||
'/Library/MobileSubstrate/MobileSubstrate.dylib',
|
||||
'/bin/bash', '/usr/sbin/sshd', '/etc/apt',
|
||||
'/usr/bin/ssh', '/private/var/lib/apt/',
|
||||
'/private/var/lib/cydia', '/private/var/tmp/cydia.log',
|
||||
'/var/lib/dpkg/info', '/usr/libexec/cydia/'
|
||||
];
|
||||
|
||||
Interceptor.attach(fileExistsAtPath.implementation, {
|
||||
onEnter: function(args) {
|
||||
this.path = ObjC.Object(args[2]).toString();
|
||||
},
|
||||
onLeave: function(retval) {
|
||||
for (var i = 0; i < jailbreakPaths.length; i++) {
|
||||
if (this.path.indexOf(jailbreakPaths[i]) !== -1) {
|
||||
retval.replace(0); // false
|
||||
send('[JB Bypass] Hidden: ' + this.path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Hook canOpenURL to block cydia:// scheme detection
|
||||
var UIApplication = ObjC.classes.UIApplication;
|
||||
Interceptor.attach(UIApplication['- canOpenURL:'].implementation, {
|
||||
onEnter: function(args) {
|
||||
this.url = ObjC.Object(args[2]).toString();
|
||||
},
|
||||
onLeave: function(retval) {
|
||||
if (this.url.indexOf('cydia://') !== -1 || this.url.indexOf('sileo://') !== -1) {
|
||||
retval.replace(0);
|
||||
send('[JB Bypass] Blocked URL scheme: ' + this.url);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Hook fork() to prevent fork-based jailbreak detection
|
||||
var fork = Module.findExportByName(null, 'fork');
|
||||
if (fork) {
|
||||
Interceptor.attach(fork, {
|
||||
onLeave: function(retval) {
|
||||
retval.replace(-1); // fork fails on non-jailbroken
|
||||
send('[JB Bypass] fork() returned -1');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
send('[JB Bypass] All jailbreak detection bypasses installed');
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def analyze_ipa_static(ipa_path, output_dir):
|
||||
"""Perform static analysis on an IPA file."""
|
||||
findings = []
|
||||
extract_dir = os.path.join(output_dir, "ipa_extracted")
|
||||
|
||||
if not zipfile.is_zipfile(ipa_path):
|
||||
logger.error("Not a valid IPA/ZIP file: %s", ipa_path)
|
||||
return findings
|
||||
|
||||
with zipfile.ZipFile(ipa_path, "r") as zf:
|
||||
zf.extractall(extract_dir)
|
||||
logger.info("Extracted IPA to %s", extract_dir)
|
||||
|
||||
# Find the .app directory
|
||||
payload_dir = os.path.join(extract_dir, "Payload")
|
||||
if not os.path.isdir(payload_dir):
|
||||
logger.error("No Payload directory found in IPA")
|
||||
return findings
|
||||
|
||||
app_dirs = [d for d in os.listdir(payload_dir) if d.endswith(".app")]
|
||||
if not app_dirs:
|
||||
logger.error("No .app bundle found in Payload")
|
||||
return findings
|
||||
|
||||
app_path = os.path.join(payload_dir, app_dirs[0])
|
||||
logger.info("Analyzing app bundle: %s", app_dirs[0])
|
||||
|
||||
# Parse Info.plist
|
||||
info_plist_path = os.path.join(app_path, "Info.plist")
|
||||
if os.path.exists(info_plist_path):
|
||||
with open(info_plist_path, "rb") as f:
|
||||
try:
|
||||
plist_data = plistlib.load(f)
|
||||
except Exception:
|
||||
plist_data = {}
|
||||
|
||||
# Check App Transport Security configuration
|
||||
ats = plist_data.get("NSAppTransportSecurity", {})
|
||||
if ats.get("NSAllowsArbitraryLoads", False):
|
||||
findings.append({
|
||||
"id": "IOS-STATIC-001",
|
||||
"severity": "Medium",
|
||||
"title": "App Transport Security Disabled",
|
||||
"detail": "NSAllowsArbitraryLoads is set to true, allowing cleartext HTTP traffic",
|
||||
"masvs": "MASVS-NETWORK",
|
||||
"mastg": "MASTG-TEST-0066",
|
||||
})
|
||||
|
||||
# Check URL schemes for deep link hijacking potential
|
||||
url_types = plist_data.get("CFBundleURLTypes", [])
|
||||
custom_schemes = []
|
||||
for url_type in url_types:
|
||||
schemes = url_type.get("CFBundleURLSchemes", [])
|
||||
custom_schemes.extend(schemes)
|
||||
if custom_schemes:
|
||||
findings.append({
|
||||
"id": "IOS-STATIC-002",
|
||||
"severity": "Informational",
|
||||
"title": "Custom URL Schemes Registered",
|
||||
"detail": f"App registers URL schemes: {', '.join(custom_schemes)}. "
|
||||
"Test for deep link hijacking and parameter injection.",
|
||||
"masvs": "MASVS-PLATFORM",
|
||||
"mastg": "MASTG-TEST-0075",
|
||||
})
|
||||
|
||||
# Check for background modes that could leak data
|
||||
bg_modes = plist_data.get("UIBackgroundModes", [])
|
||||
if bg_modes:
|
||||
findings.append({
|
||||
"id": "IOS-STATIC-003",
|
||||
"severity": "Informational",
|
||||
"title": "Background Execution Modes Enabled",
|
||||
"detail": f"Background modes: {', '.join(bg_modes)}. "
|
||||
"Verify sensitive operations are not exposed in background.",
|
||||
"masvs": "MASVS-STORAGE",
|
||||
"mastg": "MASTG-TEST-0058",
|
||||
})
|
||||
|
||||
# Scan binary strings for hardcoded secrets
|
||||
executable_name = plist_data.get("CFBundleExecutable", app_dirs[0].replace(".app", ""))
|
||||
executable_path = os.path.join(app_path, executable_name)
|
||||
|
||||
if os.path.exists(executable_path):
|
||||
secret_patterns = [
|
||||
(r'(?i)api[_-]?key\s*[:=]\s*["\'][A-Za-z0-9_\-]{16,}', "Hardcoded API Key"),
|
||||
(r'(?i)secret\s*[:=]\s*["\'][A-Za-z0-9_\-]{8,}', "Hardcoded Secret"),
|
||||
(r'(?i)password\s*[:=]\s*["\'][^"\']{4,}', "Hardcoded Password"),
|
||||
(r'https?://[a-zA-Z0-9._\-]+\.firebaseio\.com', "Firebase URL"),
|
||||
(r'AIza[0-9A-Za-z_\-]{35}', "Google API Key"),
|
||||
(r'AKIA[0-9A-Z]{16}', "AWS Access Key ID"),
|
||||
(r'-----BEGIN (?:RSA )?PRIVATE KEY-----', "Embedded Private Key"),
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["strings", executable_path],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
binary_strings = result.stdout
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
binary_strings = ""
|
||||
try:
|
||||
with open(executable_path, "rb") as f:
|
||||
raw = f.read()
|
||||
binary_strings = raw.decode("ascii", errors="ignore")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for pattern, label in secret_patterns:
|
||||
matches = re.findall(pattern, binary_strings)
|
||||
if matches:
|
||||
findings.append({
|
||||
"id": "IOS-STATIC-004",
|
||||
"severity": "High",
|
||||
"title": f"{label} Found in Binary",
|
||||
"detail": f"Pattern match for {label}: {len(matches)} occurrence(s) found. "
|
||||
f"Sample: {matches[0][:60]}...",
|
||||
"masvs": "MASVS-STORAGE",
|
||||
"mastg": "MASTG-TEST-0058",
|
||||
})
|
||||
|
||||
# Check for embedded provisioning profile
|
||||
provision_path = os.path.join(app_path, "embedded.mobileprovision")
|
||||
if os.path.exists(provision_path):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["security", "cms", "-D", "-i", provision_path],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if "get-task-allow" in result.stdout and "<true/>" in result.stdout:
|
||||
findings.append({
|
||||
"id": "IOS-STATIC-005",
|
||||
"severity": "Medium",
|
||||
"title": "Debug Entitlement Enabled (get-task-allow)",
|
||||
"detail": "The provisioning profile has get-task-allow=true, "
|
||||
"indicating a development/debug build that allows attaching debuggers.",
|
||||
"masvs": "MASVS-RESILIENCE",
|
||||
"mastg": "MASTG-TEST-0083",
|
||||
})
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
# Check for unprotected frameworks
|
||||
frameworks_dir = os.path.join(app_path, "Frameworks")
|
||||
if os.path.isdir(frameworks_dir):
|
||||
frameworks = [f for f in os.listdir(frameworks_dir) if f.endswith(".framework")]
|
||||
if frameworks:
|
||||
findings.append({
|
||||
"id": "IOS-STATIC-006",
|
||||
"severity": "Informational",
|
||||
"title": "Embedded Frameworks Inventory",
|
||||
"detail": f"App embeds {len(frameworks)} frameworks: {', '.join(frameworks[:10])}. "
|
||||
"Review for known vulnerable versions.",
|
||||
"masvs": "MASVS-RESILIENCE",
|
||||
"mastg": "MASTG-TEST-0083",
|
||||
})
|
||||
|
||||
logger.info("Static analysis complete: %d findings", len(findings))
|
||||
return findings
|
||||
|
||||
|
||||
def run_frida_script(target_bundle, script_source, device_type="usb", timeout_sec=30):
|
||||
"""Execute a Frida script against a target iOS application."""
|
||||
messages = []
|
||||
|
||||
try:
|
||||
import frida
|
||||
except ImportError:
|
||||
logger.error("Frida Python bindings not installed: pip install frida")
|
||||
return messages
|
||||
|
||||
def on_message(message, data):
|
||||
if message["type"] == "send":
|
||||
payload = message["payload"]
|
||||
messages.append(payload)
|
||||
logger.info("Frida: %s", payload)
|
||||
elif message["type"] == "error":
|
||||
logger.error("Frida error: %s", message.get("description", ""))
|
||||
|
||||
try:
|
||||
if device_type == "usb":
|
||||
device = frida.get_usb_device(timeout=10)
|
||||
elif device_type == "remote":
|
||||
device = frida.get_remote_device()
|
||||
else:
|
||||
device = frida.get_local_device()
|
||||
|
||||
logger.info("Attached to device: %s", device.name)
|
||||
|
||||
# Try to attach to running process first
|
||||
try:
|
||||
pid = device.get_process(target_bundle).pid
|
||||
session = device.attach(pid)
|
||||
logger.info("Attached to running process PID %d", pid)
|
||||
except frida.ProcessNotFoundError:
|
||||
# Spawn the app
|
||||
pid = device.spawn([target_bundle])
|
||||
session = device.attach(pid)
|
||||
device.resume(pid)
|
||||
logger.info("Spawned and attached to PID %d", pid)
|
||||
|
||||
script = session.create_script(script_source)
|
||||
script.on("message", on_message)
|
||||
script.load()
|
||||
|
||||
import time
|
||||
time.sleep(timeout_sec)
|
||||
|
||||
script.unload()
|
||||
session.detach()
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Frida execution failed: %s", e)
|
||||
messages.append({"error": str(e)})
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
def run_objection_command(bundle_id, command):
|
||||
"""Execute an Objection command against a target app."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["objection", "--gadget", bundle_id, "run", command],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
return {"command": command, "stdout": result.stdout, "stderr": result.stderr,
|
||||
"returncode": result.returncode}
|
||||
except FileNotFoundError:
|
||||
logger.error("Objection not found: pip install objection")
|
||||
return {"command": command, "error": "objection not installed"}
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning("Objection command timed out: %s", command)
|
||||
return {"command": command, "error": "timeout"}
|
||||
|
||||
|
||||
def assess_keychain_security(bundle_id):
|
||||
"""Dump and analyze keychain items for insecure storage practices."""
|
||||
findings = []
|
||||
result = run_objection_command(bundle_id, "ios keychain dump --json")
|
||||
|
||||
if result.get("error"):
|
||||
logger.warning("Keychain dump failed: %s", result.get("error"))
|
||||
return findings
|
||||
|
||||
try:
|
||||
items = json.loads(result["stdout"])
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
logger.warning("Could not parse keychain dump output")
|
||||
return findings
|
||||
|
||||
insecure_accessibility = [
|
||||
"kSecAttrAccessibleAlways",
|
||||
"kSecAttrAccessibleAlwaysThisDeviceOnly",
|
||||
"kSecAttrAccessibleAfterFirstUnlock",
|
||||
]
|
||||
|
||||
for item in items:
|
||||
accessible = item.get("accessible", "")
|
||||
if any(a in accessible for a in insecure_accessibility):
|
||||
findings.append({
|
||||
"id": "IOS-KEYCHAIN-001",
|
||||
"severity": "High",
|
||||
"title": "Insecure Keychain Accessibility Attribute",
|
||||
"detail": f"Keychain item '{item.get('service', 'unknown')}' uses "
|
||||
f"accessibility '{accessible}'. Data may be accessible "
|
||||
"without device unlock.",
|
||||
"masvs": "MASVS-STORAGE",
|
||||
"mastg": "MASTG-TEST-0055",
|
||||
})
|
||||
|
||||
# Check for unprotected password items
|
||||
if item.get("class") == "genp" and not item.get("accessControl"):
|
||||
findings.append({
|
||||
"id": "IOS-KEYCHAIN-002",
|
||||
"severity": "Medium",
|
||||
"title": "Keychain Password Without Access Control",
|
||||
"detail": f"Generic password '{item.get('service', 'unknown')}' lacks "
|
||||
"biometric or passcode access control constraints.",
|
||||
"masvs": "MASVS-STORAGE",
|
||||
"mastg": "MASTG-TEST-0055",
|
||||
})
|
||||
|
||||
logger.info("Keychain analysis: %d findings from %d items", len(findings), len(items))
|
||||
return findings
|
||||
|
||||
|
||||
def generate_report(findings, target_app, output_path):
|
||||
"""Generate comprehensive iOS security assessment report."""
|
||||
critical = [f for f in findings if f.get("severity") == "Critical"]
|
||||
high = [f for f in findings if f.get("severity") == "High"]
|
||||
medium = [f for f in findings if f.get("severity") == "Medium"]
|
||||
low = [f for f in findings if f.get("severity") in ("Low", "Informational")]
|
||||
|
||||
report = {
|
||||
"assessment": "iOS Application Security Assessment",
|
||||
"target": target_app,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"summary": {
|
||||
"total_findings": len(findings),
|
||||
"critical": len(critical),
|
||||
"high": len(high),
|
||||
"medium": len(medium),
|
||||
"low_informational": len(low),
|
||||
},
|
||||
"findings": findings,
|
||||
}
|
||||
|
||||
with open(output_path, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
logger.info("Report saved to %s (%d findings)", output_path, len(findings))
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="iOS App Security Assessment Agent - Frida/Objection-based testing"
|
||||
)
|
||||
parser.add_argument("--bundle-id", help="Target app bundle identifier (e.g., com.target.app)")
|
||||
parser.add_argument("--ipa", help="Path to IPA file for static analysis")
|
||||
parser.add_argument("--device", choices=["usb", "remote", "local"], default="usb",
|
||||
help="Frida device type (default: usb)")
|
||||
parser.add_argument("--ssl-bypass", action="store_true",
|
||||
help="Run SSL pinning bypass script")
|
||||
parser.add_argument("--keychain", action="store_true",
|
||||
help="Dump and analyze keychain security")
|
||||
parser.add_argument("--jailbreak-bypass", action="store_true",
|
||||
help="Run jailbreak detection bypass")
|
||||
parser.add_argument("--frida-timeout", type=int, default=30,
|
||||
help="Frida script execution timeout in seconds")
|
||||
parser.add_argument("--output", default="ios_assessment_report.json",
|
||||
help="Output report file path")
|
||||
parser.add_argument("--output-dir", default=".",
|
||||
help="Directory for extracted IPA and artifacts")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.bundle_id and not args.ipa:
|
||||
parser.error("Provide --bundle-id for dynamic testing or --ipa for static analysis")
|
||||
|
||||
findings = []
|
||||
|
||||
# Static analysis of IPA
|
||||
if args.ipa:
|
||||
logger.info("=== IPA Static Analysis ===")
|
||||
findings.extend(analyze_ipa_static(args.ipa, args.output_dir))
|
||||
|
||||
# Dynamic analysis with Frida/Objection
|
||||
if args.bundle_id:
|
||||
if args.ssl_bypass:
|
||||
logger.info("=== SSL Pinning Bypass ===")
|
||||
msgs = run_frida_script(args.bundle_id, SSL_PINNING_BYPASS_SCRIPT,
|
||||
args.device, args.frida_timeout)
|
||||
if any("success" in str(m).lower() for m in msgs):
|
||||
findings.append({
|
||||
"id": "IOS-NET-001",
|
||||
"severity": "Informational",
|
||||
"title": "SSL Pinning Successfully Bypassed",
|
||||
"detail": "Certificate pinning was bypassed using Frida hooks on "
|
||||
"SecTrustEvaluate, SecTrustEvaluateWithError, and framework-specific "
|
||||
"trust evaluation methods. Traffic can be intercepted via proxy.",
|
||||
"masvs": "MASVS-NETWORK",
|
||||
"mastg": "MASTG-TEST-0068",
|
||||
})
|
||||
|
||||
if args.jailbreak_bypass:
|
||||
logger.info("=== Jailbreak Detection Bypass ===")
|
||||
msgs = run_frida_script(args.bundle_id, JAILBREAK_DETECTION_BYPASS_SCRIPT,
|
||||
args.device, args.frida_timeout)
|
||||
if any("bypass" in str(m).lower() for m in msgs):
|
||||
findings.append({
|
||||
"id": "IOS-RES-001",
|
||||
"severity": "Medium",
|
||||
"title": "Jailbreak Detection Bypassed at Runtime",
|
||||
"detail": "Jailbreak detection checks (file existence, URL scheme, fork) "
|
||||
"were all bypassed using Frida method hooks.",
|
||||
"masvs": "MASVS-RESILIENCE",
|
||||
"mastg": "MASTG-TEST-0079",
|
||||
})
|
||||
|
||||
if args.keychain:
|
||||
logger.info("=== Keychain Security Analysis ===")
|
||||
findings.extend(assess_keychain_security(args.bundle_id))
|
||||
|
||||
target_name = args.bundle_id or args.ipa or "unknown"
|
||||
report = generate_report(findings, target_name, args.output)
|
||||
|
||||
print(json.dumps(report["summary"], indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user