모바일 앱

Noir가 Android·iOS 딥링크 진입점을 추출하고 이를 처리하는 코드와 연결하는 방식입니다.

Noir는 Android나 iOS 앱의 진입점을 엔드포인트로 보고합니다. 진입점이란 앱이 외부에 노출하는 딥링크와 exported 컴포넌트입니다. 진입점마다 이를 처리하는 소스 코드도 함께 연결합니다.

딥링크는 전형적인 모바일 공격 표면입니다. 외부에서 주어진 URL이나 인텐트가 앱 안으로 흘러 들어가 WebView, SQL 쿼리, 다른 컴포넌트에 도달할 수 있습니다.

별도 플래그는 없습니다. 평소처럼 앱 소스 트리를 스캔하면 Noir가 아래 파일을 읽습니다:

noir scan path/to/app

Noir가 파싱하는 대상

플랫폼 소스 파일 추출 내용
Android AndroidManifest.xml 커스텀 URL 스킴 딥링크, exported 인텐트 컴포넌트, exported ContentProvider, 검증된 App Link
Android res/values/strings.xml 스킴·호스트·경로에 쓰인 @string/ 참조 해석
Android build.gradle / build.gradle.kts 패키지·컴포넌트 이름·스킴·호스트에 쓰인 ${applicationId}와 커스텀 manifestPlaceholders 해석. 매니페스트에 package 속성이 없으면 패키지도 여기서 가져옵니다
Android res/navigation/*.xml Jetpack Navigation <deepLink app:uri="..."> 딥링크. 소속 destination이 처리 컴포넌트가 됩니다
iOS Info.plist (CFBundleURLTypes를 선언한 모든 *.plist) 커스텀 URL 스킴. .xcconfig / project.pbxproj의 $(VAR) / ${VAR} 플레이스홀더도 해석
iOS *.entitlements associated-domains의 applinks: 유니버설 링크와 appclips: App Clip 실행 URL
Android /.well-known/assetlinks.json 서버 측 App Links 연결 선언(Digital Asset Links)
iOS apple-app-site-association 서버 측 유니버설 링크 paths / components 패턴

앞의 여섯 행은 앱 번들에 선언되는 클라이언트 측입니다. 마지막 두 행은 서버 측으로, OS가 해당 호스트의 URL을 앱으로 열도록 호스트가 게시하는 well-known 파일입니다. 이 연결의 양쪽 모두 같은 출력 모델의 universal-link 엔드포인트를 만듭니다.

엔드포인트 모델

모든 모바일 진입점은 method = "GET"인 엔드포인트입니다. 어떤 종류의 진입점인지는 protocol 필드가 알려 줍니다:

protocol 의미 예시 URL
mobile-scheme 커스텀 URL 스킴 딥링크 myapp://complex/:id
android-intent 인텐트로 도달 가능한 exported 컴포넌트: data URI 없는 action 필터, 또는 intent-filter가 전혀 없는 경우(명시적 인텐트 전용) intent://com.example.app/.SyncService
android-provider 다른 앱이 content://authority URI로 도달할 수 있는 exported ContentProvider content://com.example.app.provider
universal-link 검증된 Android App Link / iOS 유니버설 링크, 또는 서버 측 assetlinks.json / apple-app-site-association 패턴 https://app.example.com/complex/:id, /buy/*

plain 출력에서는 종류마다 SCHEME, INTENT, PROVIDER, UNIVERSAL 프리픽스가 붙습니다.

처리 컴포넌트, 인텐트 action·category, 호스트, 패키지는 엔드포인트별 metadata 맵에 들어갑니다. JSON과 YAML 출력에 포함되며, 일반 HTTP 엔드포인트에는 이 맵이 없습니다.

SCHEME myapp://complex/:id
  ○ path: id
  ○ via: .DeepLinkActivity
  ○ action: android.intent.action.VIEW
  ○ category: android.intent.category.DEFAULT
  ○ host: complex
  ○ package: com.example.myapp

핸들러 코드와의 연결

스캔 대상에 핸들러 소스가 있으면 Noir는 딥링크 엔드포인트마다 이를 처리하는 코드를 연결합니다.

Android

  • 매니페스트의 컴포넌트(예: .DeepLinkActivity)를 해당 .kt / .java 파일로 해석합니다.
  • 인텐트 처리 메서드(onCreate, onNewIntent, onReceive, …)에서 1-hop callee, 즉 그 메서드가 직접 호출하는 함수를 수집합니다.
  • 그 안에서 읽는 입력은 파라미터가 됩니다. uri.getQueryParameter("q") → query 파라미터(URL에 포함), get*Extra 계열 → extra 파라미터.

iOS

딥링크는 중앙에서 디스패치되므로, Noir는 핸들러를 탐색해 종류별로 연결합니다:

  • .onOpenURL / application(_:open:) / scene(_:openURLContexts:) → 커스텀 스킴.
  • userActivity 핸들러 → 유니버설 링크.
  • URLQueryItem 읽기는 query 파라미터가 됩니다.

--ai-context를 사용하면 핸들러 본문이 Noir의 source / sink / guard 추론에 쓰입니다. 그러면 WebView 로드 같은 싱크로 흘러가는 딥링크가 taint 경로(입력이 그 싱크까지 가는 길)와 함께 드러납니다.

출력 동작

출력 모바일 엔드포인트
구조화된 인벤토리(JSON, JSONL, YAML, TOML, SARIF, Markdown table, mermaid, HTML)와 Elasticsearch / webhook export 포함합니다. 앱 표면의 일부이기 때문입니다
HTTP 모양을 가정하는 출력·전송(cURL, HTTPie, PowerShell, OpenAPI 2.0 / 3.0, Postman, 능동 프로빙 / 프록시 전송) 뺍니다. 앱 URL은 보내는 HTTP 요청이 아니라 여는 대상입니다
-f adb(Android)와 -f simctl(iOS) 진입점을 여는 기기 명령으로 내보냅니다. 그대로 실행할 수 있습니다

참고 및 한계

공통

  • 핸들러 연결에는 소스 코드가 필요합니다. 매니페스트·plist만 있는 스캔도 엔드포인트는 추출하지만 callee·파라미터·AI 컨텍스트는 붙지 않습니다.

Android

  • gradle 플레이스홀더: 매니페스트 위쪽에서 가장 가까운 build.gradle(.kts)를 읽습니다. 같은 플레이스홀더가 여러 번 선언되면(예: buildTypes 오버라이드) 먼저 선언된 쪽(관례상 defaultConfig)이 우선합니다. 빌드 variant별 값은 모델링하지 않습니다. gradle에 값이 없는 플레이스홀더는 URL에 그대로 남고, 엔드포인트에 unresolved 태그가 붙습니다.
  • Jetpack Navigation: 스킴 없는 URI(런타임에는 http·https 모두 매칭)는 https 하나로 추출합니다. {arg} 세그먼트는 :arg path 파라미터로, ?key={arg} 쿼리 플레이스홀더는 query 파라미터로 바뀌고, 끝의 .* 와일드카드는 리터럴 프리픽스만 남깁니다.
  • intent-filter가 없는 컴포넌트: intent-filter가 없는 android:exported="true" 컴포넌트는 명시적 인텐트 표면(android-intent, metadata에 explicit/exported/component_type 포함)으로 보고됩니다. action·category·data URI가 없어도, 컴포넌트 이름을 지정한 명시적 인텐트는 여전히 그 컴포넌트에 도달합니다.
    • exported가 아니거나 android:enabled="false"인 필터 없는 컴포넌트는 보고하지 않습니다.
    • 보호용 android:permission은 metadata에 기록되지만 표면을 숨기지는 않습니다. normal/dangerous 권한은 다른 앱이 여전히 획득할 수 있기 때문입니다.
  • ContentProvider: exported <provider>는 android-provider 표면으로 보고되며, android:authorities 항목마다 content://authority 엔드포인트를 하나씩 만듭니다.
    • provider 클래스가 핸들러(via)로 연결되므로 query / insert / openFile 메서드의 callee가 수집됩니다. raw SQL·파일 싱크는 AI 컨텍스트에 드러납니다.
    • 권한은 metadata에 기록만 하고 표면을 숨기지 않습니다: android:readPermission / android:writePermission / android:permission, android:grantUriPermissions(또는 <grant-uri-permission> 자식), 그리고 <path-permission> 오버라이드의 존재 여부.
    • 명시적으로 exported된 provider만 보고합니다(최신 SDK에서 provider 기본값은 not-exported).
    • <path-permission>의 경로별 실효 권한은 표시만 하고 해석하지는 않습니다.

iOS

  • 바이너리(컴파일된) Info.plist는 건너뜁니다. 소스 저장소의 XML plist는 파싱합니다.
  • 앱 자체 스킴에 대해 Swift 코드에 적힌 라우트도 찾습니다. url.host == SomeEnum.case.rawValue처럼 비교하는 라우터 enum(각 case가 라우트가 됨)과 하드코딩된 scheme://host/path 문자열 리터럴입니다.

서버 측 연결 파일

  • assetlinks.json은 패키지 연결만 선언하고 경로는 없으므로 앱당 /* 엔드포인트 하나를 만듭니다.
  • apple-app-site-association(AASA)의 경로(/buy/*, NOT /private/*)와 components는 개별적으로 추출되며, AASA 제외 패턴에는 excluded 태그가 붙습니다.
  • AASA는 plain-JSON 형식만 파싱합니다. CMS로 서명된 AASA 파일(구형 앱)은 건너뜁니다.