모바일 앱

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

Noir는 모바일 앱이 외부에 노출하는 진입점(딥링크와 exported 컴포넌트)을 엔드포인트로 추출하고, 이를 처리하는 소스 코드와 연결합니다. 딥링크는 전형적인 모바일 공격 표면입니다. 외부에서 주어진 URL이나 인텐트가 앱 내부로 흘러 들어가 WebView, SQL 쿼리, 다른 컴포넌트에 도달할 수 있기 때문입니다.

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-domainsapplinks: 유니버설 링크와 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 protocol과 출력 모델을 통해 처리됩니다.

엔드포인트 모델

모바일 진입점은 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
  ○ via: .DeepLinkActivity
  ○ action: android.intent.action.VIEW
  ○ host: complex
  ○ package: com.example.myapp

핸들러 코드와의 연결

스캔에 핸들러 소스가 포함되어 있으면, Noir는 각 딥링크 엔드포인트를 이를 처리하는 코드와 연결합니다.

  • Android: 매니페스트의 컴포넌트(예: .DeepLinkActivity)를 해당 .kt / .java 파일로 해석합니다. 인텐트 처리 메서드(onCreate, onNewIntent, onReceive, …)에서 1-hop callee를 수집하고, 그 안에서 읽는 입력을 파라미터로 만듭니다. uri.getQueryParameter("q")query 파라미터(URL에 baking), get*Extra 계열 → extra 파라미터.
  • iOS: 딥링크는 중앙에서 디스패치되므로, Noir는 핸들러를 탐색해 종류별로 연결합니다. .onOpenURL / application(_:open:) / scene(_:openURLContexts:)는 커스텀 스킴에, userActivity 핸들러는 유니버설 링크에 붙입니다. URLQueryItem 읽기는 query 파라미터가 됩니다.

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

출력 동작

모바일 엔드포인트는 구조화된 인벤토리(JSON, JSONL, YAML, SARIF, Markdown, mermaid, HTML)와 Elasticsearch / webhook export에는 그대로 포함됩니다. 앱 표면의 일부이기 때문입니다. 반면 HTTP 모양을 가정하는 출력·전송(cURL, HTTPie, PowerShell, OpenAPI 2.0 / 3.0, 능동 프로빙 / 프록시 전송)에서는 제외됩니다. 앱 URL은 보내는 HTTP 요청이 아니라 여는 대상이기 때문입니다.

참고 및 한계

  • 핸들러 연결에는 소스 코드가 필요합니다. 매니페스트·plist만 있는 스캔도 엔드포인트는 추출하지만 callee·파라미터·AI 컨텍스트는 붙지 않습니다.
  • 바이너리(컴파일된) Info.plist는 건너뜁니다. 소스 저장소의 XML plist는 파싱합니다.
  • gradle 플레이스홀더는 매니페스트 위쪽에서 가장 가까운 build.gradle(.kts)를 읽어 해석합니다. 같은 플레이스홀더가 여러 번 선언되면(예: buildTypes 오버라이드) 먼저 선언된 쪽(관례상 defaultConfig)이 우선하며, 빌드 variant별 값은 모델링하지 않습니다. gradle에 값이 없는 플레이스홀더는 URL에 그대로 남고 엔드포인트에 unresolved 태그가 붙습니다.
  • 스킴 없는 Jetpack Navigation URI(런타임에는 http·https 모두 매칭)는 https 하나로 추출합니다. {arg} 세그먼트는 :arg path 파라미터로, ?key={arg} 쿼리 플레이스홀더는 query 파라미터로 바뀌고, 끝의 .* 와일드카드는 리터럴 프리픽스만 남깁니다.
  • 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 권한은 다른 앱이 여전히 획득할 수 있기 때문입니다.
  • exported <provider>(ContentProvider)는 android-provider 표면으로 보고됩니다. android:authorities 항목마다 content://authority 엔드포인트 하나씩. provider 클래스가 핸들러(via)로 연결되므로 query / insert / openFile 메서드의 callee가 수집되어, raw SQL·파일 싱크가 AI 컨텍스트에 드러납니다. android:readPermission / android:writePermission / android:permission, android:grantUriPermissions(또는 <grant-uri-permission> 자식), 그리고 <path-permission> 오버라이드의 존재 여부가 metadata에 실립니다. 모두 기록만 하고 숨기지 않습니다. 명시적으로 exported된 provider만 보고합니다(최신 SDK에서 provider 기본값은 not-exported). <path-permission>의 경로별 실효 권한은 표시만 하고 해석하지는 않습니다.
  • assetlinks.json은 패키지 연결만 선언하고 경로는 없으므로 앱당 /* 엔드포인트 하나를 만듭니다. apple-app-site-association의 경로(/buy/*, NOT /private/*)와 components는 개별적으로 추출되며, AASA 제외 패턴은 excluded 태그가 붙습니다.
  • apple-app-site-association은 plain-JSON 형식만 파싱합니다. CMS로 서명된 AASA 파일(구형 앱)은 건너뜁니다.