서론: 안드로이드 4대 컴포넌트란?
안드로이드 앱은 네 가지 핵심 구성 요소로 이루어져 있습니다. 각 컴포넌트는 명확하게 정의된 역할과 생명주기를 가지고 있으며, 안드로이드 시스템이 이들을 독립적으로 관리합니다.
| 컴포넌트 | 설명 |
|---|---|
| Activity | 사용자 인터페이스를 제공하는 단일 화면입니다. 사용자가 앱에서 보고 터치할 수 있는 모든 화면은 하나의 Activity로 구현됩니다. 로그인 화면, 메인 화면, 설정 화면 등이 각각 독립된 Activity입니다. Activity는 안드로이드 시스템에 의해 생성되고, 화면에 표시되고, 일시 정지되고, 종료되는 생명주기를 가집니다. |
| Service | 사용자 인터페이스 없이 백그라운드에서 장시간 실행되는 작업을 처리하는 컴포넌트입니다. 음악 재생 앱에서 화면을 끄고도 음악이 계속 재생되는 것이 Service를 통해 구현됩니다. Service는 따로 화면을 가지고 있지 않아 사용자가 직접 볼 수 없지만, 시스템에서 독립적으로 관리되는 컴포넌트입니다. |
| Broadcast Receiver | 시스템 전체에 전달되는 브로드캐스트 메시지를 수신하고 처리하는 컴포넌트입니다. 안드로이드 시스템이나 다른 앱이 특정 이벤트가 발생했음을 알리는 브로드캐스트를 전송하면, 해당 이벤트에 관심이 있는 Broadcast Receiver가 이를 수신하여 필요한 동작을 수행합니다. 배터리가 부족할 때, 네트워크 연결이 변경될 때 등의 이벤트를 감지하고 대응할 수 있습니다. |
| Content Provider | 앱의 데이터를 다른 앱과 공유하기 위한 표준화된 인터페이스를 제공하는 컴포넌트입니다. 안드로이드는 기본적으로 각 앱의 데이터를 격리하여 보호하지만, Content Provider를 통해 명시적으로 데이터를 공유할 수 있습니다. 연락처 앱이 저장한 연락처 정보를 메신저 앱이나 이메일 앱에서 읽을 수 있는 것이 모두 Content Provider를 통해 구현됩니다. |
취약점 발생 원인
이렇게 설계된 컴포넌트들이 어떻게 보안 취약점이 발생할까요?
안드로이드 앱의 모든 컴포넌트는 AndroidManifest.xml 파일에 선언되어야 합니다. 이 파일에서 개발자는 각 컴포넌트가 외부에 노출되는지 여부를 exported 속성에 지정할 수 있습니다.
exported="true"로 설정할 경우 해당 컴포넌트는 다른 앱에서도 접근이 가능하지만, exported="false"로 설정하거나 명시하지 않은 경우 기본적으로 같은 앱 내부에서만 접근이 가능합니다.
외부에 노출된 것은 문제가 아닙니다. 많은 앱들은 다른 앱과의 상호작용을 위해 컴포넌트를 의도적으로 노출시킵니다. 예를 들어 카메라 앱은 다른 앱에서 사진 촬영을 요청할 수 있도록 특정 Activity를 노출하도록 설계됩니다.
문제는 컴포넌트를 외부에 노출시키면서도 적절한 보안 검증이 미흡할 때 발생합니다. 인증이 필요한 기능을 담당하는 Activity 외부에 노출하면서 호출 시점에 사용자의 인증 상태를 확인하지 않는다면, 공격자는 정상적인 로그인 절차를 건너뛰고 직접 해당 Activity를 실행할 수 있습니다. 또한 민감한 데이터를 제공하는 Content Provider에 권한 체크를 하지 않으면, 모든 앱이 해당 데이터에 접근할 수 있게 됩니다.
OWASP AndroGoat 앱
이 앱에는 PIN 번호로 보호되어야 하는 청구서 다운로드 기능이 존재합니다. 사용자는 먼저 PIN을 설정하고, 이후 앱을 실행할 때 마다 올바른 PIN을 입력해야만 청구서 다운로드 페이지에 접근이 가능합니다. 하지만 우회를 통해 직접적으로 다운로드하거나 추가적인 공격이 가능합니다.
정상적인 동작 흐름
먼저 정상적인 과정으로 PIN을 설정하는 과정입니다.

앱의 메인 페이지에서 UNPROTECTED ANDROID COMPONENTS 버튼을 클릭한 뒤 임의의 PIN 번호 입력 후 SET PIN 버튼을 클릭합니다. 그러면 정상적으로 PIN 번호가 등록되었습니다.

PIN 번호가 설정되었다면 앱을 종료 후 동일한 버튼을 클릭하면 SET PIN이 아닌 VERIFY PIN 페이지로 들어오게됩니다.
이전에 등록한 PIN 번호를 누르고 버튼을 클릭하면 PIN Verified Toast 메시지와 함께 INVOICE를 다운로드할 수 있는 페이지로 들어오게됩니다.
요약하면,
- PIN 설정 → 페이지 접근 → PIN 인증 → INVOICE 다운로드
의 순서로 진행하게 됩니다.
시나리오 1: Activity Force Browsing
AndroidManifest.xml 파일을 분석하기전, PIN 번호를 인증하는 페이지의 Activity명을 찾아봅니다.

PS C:\Users\WIN11> adb -s R3CM609AZRA shell dumpsys window | findstr "mCurrentFocus"
mCurrentFocus=Window{a4f835 u0 owasp.sat.agoat/owasp.sat.agoat.AccessControlIssue1Activity}
AccessControlIssue1Activity명을 확인했으니 디컴파일 후 코드를 자세히 살펴보겠습니다.

먼저, PIN을 생성하는 함수인 createPIN() 분석부터 진행해보겠습니다.
- getSharedPreferences("pinDetails", 0);
- 앱 내부 저장소에 pinDetails.xml라는 이름의 파일을 생성하거나 불러옴
- 0은 Context.MODE_PRIVATE로 이 앱 외의 다른 앱은 접근하지 못하게 설정했지만, 루팅된 기기에서는 접근 제어가 무의미
- editor1.putBoolean("pinSet", true);
- PIN이 설정되었다는 상태(pinSet)를 true로 저장
- 앱이 시작될 때 이 값을 보고 로그인 화면을 띄울지 결정
- editor1.putString(ContentProviderActivity.PIN, hashPIN(pinValue));
- 사용자가 입력한 pinValue를 그대로 저장하지 않고 hashPIN()함수를 통해 해싱한 뒤 저장
- 단순한 Base64 인코딩이나 MD5라면 복구를 통해 평문 PIN 번호 획득 가능
- editor1.commit();
- 변경된 내용을 파일(pinDetails.xml)에 기록

hashPIN() 함수의 경우 설정한 PIN 번호를 MD5로 암호화하는 것을 확인할 수 있습니다. 이를 복호화하는 것도 좋지만, 현재 시나리오와 부합하지 않아 다른 방법으로 진행할 예정입니다.

verifyPINView()함수를 보면 올바른 PIN 번호를 입력한다면 AccessControl1ViewActivity가 호출되면서 로그인 과정이 진행되는 것을 볼 수 있습니다.
이 Activity가 만약 외부에 노출되어 있다면 PIN 입력 과정을 생략하고 명령어 한줄을 통해 직접적으로 Activity 실행이 가능합니다. 그러기 위해서는 해당 Activity가 외부에 노출되어 있는지 여부를 확인합니다.

메니페스트 파일 확인 시 exported="true"로 설정되어 있어 외부에서 호출이 가능합니다.
adb shell am start -n owasp.sat.agoat/.AccessControl1ViewActivity
Starting: Intent { cmp=owasp.sat.agoat/.AccessControl1ViewActivity }
다음과 같은 명령어를 통해 인증 이후의 페이지를 호출합니다.

PIN 인증과 상관 없이 Welcome User!라는 메시지와 함께 Invoice 다운로드 버튼에 접근할 수 있었습니다.
시나리오 2: 보호되지 않은 Service
Service는 사용자 인터페이스 없이 백그라운드에서 실행됩니다. 만약 보호되지 않는다면 공격자는 사용자가 모르는 사이에 파일 다운로드나 심지어 암호화폐 채굴 같은 비용이 많이 드는 작업을 시작할 수 있습니다.
이 앱에는 사용자가 인보이스를 요청할 때만 실행되어야 하는 DownloadInvoiceService가 있습니다. 코드를 보면 인증이나 권한 체크가 전혀 없습니다. 이는 심각한 보안 결함입니다.

AccessControl1ViewActivity 코드를 살펴보면 실제로 Invoice 다운로드를 시도하면 DownloadInvoiceService 서비스가 실행됩니다.

DownloadInvocieService 코드를 살펴보면 실제로 별도의 권한 체크가 존재하지 않습니다. 터미널에서 다음 명령어로 이 서비스를 수동으로 시작할 수 있습니다.
PS C:\Users\WIN11> adb shell am startservice -n owasp.sat.agoat/.DownloadInvoiceService
Starting service: Intent { cmp=owasp.sat.agoat/.DownloadInvoiceService }
am startservice 명령어는 Service를 시작하는 명령입니다. Activity와 마찬가지로 -n 플래그로 정확한 컴포넌트를 지정할 수 있습니다.
서비스가 백그라운드에서 시작되고, 토스트 메시지가 나타나며 우리가 앱의 UI와 상호작용 없이 백그라운드 로직을 트리거할 수 있음을 확인할 수 있습니다.

프록시의 History를 확인하면 다운로드가 실제로 동작하는 것을 확인할 수 있습니다.

실제 환경에서는 공격자가 악성 앱을 통해 이러한 Service를 반복적으로 호출하여 사용자의 데이터 요금을 소진시키거나, 불필요한 파일을 다운로드하여 저장 공간을 낭비하게 만들 수 있습니다.
시나리오 3: 보호되지 않은 Content Provider (민감 정보 유출)
Content Provider는 구조화된 데이터를 관리하고 다른 앱과 공유하기 위한 메커니즘입니다. 데이터베이스처럼 작동하지만, 앱 간 경계를 넘어 데이터에 접근할 수 있게 해주는 표준화된 인터페이스를 제공합니다.

<provider
android:name="owasp.sat.agoat.ContentProviderActivity"
android:exported="true"
android:authorities="owasp.sat.agoat.provider.userpinsprovider"/>
메니페스트 파일을 보면 ContentProvider가 외부로 노출되어 있습니다.
android:exported="true"로 설정되어 있어 아무런 권한 없이 모든 앱이 이 데이터베이스에 접근할 수 있습니다.authorities속성은 Content Provider의 고유 식별자로, 이를 통해 다른 앱이 이 Provider에 접근할 수 있습니다. 이름이userpinsprovider인 것으로 보아, 사용자가 설정한 PIN 번호(또는 해시값)를 저장하고 관리하는 데이터베이스일 가능성이 매우 높습니다.
AccessControlIssue1Activity.kt 코드를 보면 PIN을 SharedPreferences에 저장하지만, 종종 ContentProvider가 이 데이터를 래핑해서 제공하는 구조로 개발되기도 합니다. 이는 데이터 접근을 중앙화하고 일관성 있는 인터페이스를 제공하기 위한 설계 패턴입니다.

ContentProviderActivity의 코드를 확인하면 URI를 확인할 수 있습니다. 단순히 query를 전송하여 데이터베이스 내부에 존재하는 데이터 추출이 가능합니다.
adb shell content query --uri content://owasp.sat.agoat.provider.userpinsprovider/user_pins

데이터베이스에 하드코딩된 관리자 계정 정보가 들어있습니다. 이 취약점은 보호되지 않은 Content Provider를 통해 내부 데이터베이스의 민감 정보가 평문으로 노출되는 전형적인 사례입니다.
시나리오 4: Broadcast Spoofing
Broadcast는 안드로이드 시스템에서 이벤트를 전파하는 pub-sub(발행-구독) 메커니즘입니다. 한 컴포넌트가 브로드캐스트를 발행하면, 관심 있는 모든 Receiver가 그것을 구독하여 받을 수 있습니다. 문제는 누구나 브로드캐스트를 보낼 수 있다는 점입니다.

메니페스트 파일 확인시 ShowDataReceiver 리시버가 expotred="true" 속성을 가지고 있습니다.
<receiver
android:name="owasp.sat.agoat.ShowDataReceiver"
android:enabled="true"
android:exported="true"/>
공격자는 임의의 가짜 방송(Broadcast Intent)을 보내 이 리시버를 강제로 동작시킬 수 있습니다. 리시버의 이름으로 보아, 특정 데이터를 화면에 띄우거나 로그를 남기는 기능으로 유추됩니다.
PS C:\Users\WIN11> adb shell am broadcast -n owasp.sat.agoat/.ShowDataReceiver
Broadcasting: Intent { flg=0x400000 cmp=owasp.sat.agoat/.ShowDataReceiver }
Broadcast completed: result=0

am broadcast 명령어는 브로드캐스트 인텐트를 전송하는 명령입니다. -n 플래그로 특정 Receiver를 직접 타겟팅할 수 있습니다.

명령어를 실행하자 화면 하단에 Toast 메시지로 숨겨진 자격 증명이 노출되었습니다. 이는 Receiver가 민감한 정보를 처리하면서 발신자의 신원을 확인하지 않았기 때문에 발생한 문제입니다. 실제 앱에서는 이런 식으로 암호화 키, API 토큰, 사용자 자격 증명 등이 유출될 수 있습니다.
시나리오 5: Deep Link를 통한 원격 공격
매니페스트 파일을 분석하면 AccessControl1ViewActivity(PIN 인증 후 진입해야 하는 화면)에 Deep Link가 설정되어 있는 것을 확인할 수 있습니다.

<activity
android:label="@string/activity"
android:name="owasp.sat.agoat.AccessControl1ViewActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data
android:scheme="androgoat"
android:host="vulnapp"/>
</intent-filter>
android:scheme="androgoat"와 android:host="vulnapp" 설정은 기기 내에서 androgoat://vulnapp이라는 주소가 호출되면 즉시 이 액티비티를 연결하겠다는 의미입니다. 해당 액티비티 진입 시 isLoggedIn 여부나 PIN 인증을 확인하는 로직이 없으므로, 공격자는 이 URL을 통해 보안 절차를 건너뛸 수 있습니다.
이 취약점은 물리적인 접근(ADB)뿐만 아니라 원격 공격이 가능합니다.
공격자는 피싱 이메일이나 웹사이트에 <a href="androgoat://vulnapp">접근</a>과 같은 링크를 심어둡니다. 사용자가 크롬 브라우저 등에서 이 링크를 클릭하는 순간, 앱이 설치되어 있다면 즉시 PIN 입력 화면을 건너뛰고 송장 다운로드 화면으로 이동하게 됩니다.

PS C:\Users\WIN11> adb shell am start -W -a android.intent.action.VIEW -d "androgoat://vulnapp"
Starting: Intent { act=android.intent.action.VIEW dat=androgoat://vulnapp }
Status: ok
LaunchState: WARM
Activity: owasp.sat.agoat/.AccessControl1ViewActivity
TotalTime: 61
WaitTime: 71
Complete
명령어 상세를 살펴보면, -a android.intent.action.VIEW는 뷰(View) 액션을 요청합니다. 이것은 브라우저가 링크를 열 때 사용하는 액션입니다. -d "androgoat://vulnapp"는 데이터(Data)로 타겟 URI를 전달합니다.

실행 결과를 보면, 명령어 실행 즉시 스마트폰 화면이 PIN 입력창(AccessControlIssue1Activity)을 거치지 않고, AccessControl1ViewActivity(인증 완료 화면)로 전환됩니다.
Mitigation
앞서 살펴본 다섯 가지 취약점 시나리오는 모두 개발 단계에서 적절한 보안 조치를 취하지 않아 발생한 문제들입니다. 이제 각 취약점에 대한 구체적인 대응 방안을 살펴보겠습니다.
1. Activity 보호하기
Activity Force Browsing을 방지하기 위해서는 세 가지 핵심 원칙을 따라야 합니다.
첫째, exported 속성을 신중하게 설정해야 합니다. 외부 앱과의 상호작용이 필요하지 않은 Activity는 반드시 exported="false"로 설정하거나 아예 명시하지 않아야 합니다. AndroidManifest.xml에서 다음과 같이 설정할 수 있습니다.
<activity
android:name=".AccessControl1ViewActivity"
android:exported="false">
</activity>
둘째, Activity가 시작될 때마다 사용자의 인증 상태를 확인해야 합니다. 심지어 exported가 false로 설정되어 있더라도, 방어적 프로그래밍 차원에서 인증 체크를 구현하는 것이 좋습니다. Activity의 onCreate() 메서드에서 다음과 같이 인증 상태를 확인할 수 있습니다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 인증 상태 확인
val sharedPref = getSharedPreferences("pinDetails", Context.MODE_PRIVATE)
val isAuthenticated = sharedPref.getBoolean("isAuthenticated", false)
if (!isAuthenticated) {
// 인증되지 않았다면 로그인 화면으로 이동
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
finish()
return
}
setContentView(R.layout.activity_main)
}
셋째, 권한 기반 접근 제어를 구현해야 합니다. 특정 Activity에 접근하려면 커스텀 권한을 요구하도록 설정할 수 있습니다.
<!-- 커스텀 권한 정의 -->
<permission
android:name="com.example.myapp.permission.ACCESS_SECURE_ACTIVITY"
android:protectionLevel="signature"/>
<!-- Activity에 권한 적용 -->
<activity
android:name=".SecureActivity"
android:permission="com.example.myapp.permission.ACCESS_SECURE_ACTIVITY">
</activity>
2. Service 보호하기
Service도 Activity와 마찬가지로 외부 접근을 제한하고 인증을 구현해야 합니다.
먼저 Service의 exported 속성을 false로 설정합니다.
<service
android:name=".DownloadInvoiceService"
android:exported="false">
</service>
Service가 시작될 때 호출자의 권한을 확인하는 로직을 추가해야 합니다.
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// 인증 토큰 확인
val token = intent?.getStringExtra("auth_token")
if (!validateToken(token)) {
Log.w("DownloadService", "Unauthorized access attempt")
stopSelf()
return START_NOT_STICKY
}
// 실제 작업 수행
downloadInvoice()
return START_NOT_STICKY
}
private fun validateToken(token: String?): Boolean {
// 토큰 유효성 검증 로직
val sharedPref = getSharedPreferences("auth", Context.MODE_PRIVATE)
val validToken = sharedPref.getString("current_token", null)
return token != null && token == validToken
}
중요한 작업을 수행하는 Service의 경우, 사용자에게 명시적인 동의를 받는 것도 좋은 방법입니다. Notification을 통해 사용자에게 현재 진행 중인 작업을 알리고, 필요시 중단할 수 있는 옵션을 제공합니다.
3. Content Provider 보호하기
Content Provider는 데이터 공유를 목적으로 하기 때문에 더욱 세심한 보안 조치가 필요합니다.
먼저 권한을 설정해야 합니다. AndroidManifest.xml에서 읽기와 쓰기 권한을 각각 정의할 수 있습니다.
<!-- 커스텀 권한 정의 -->
<permission
android:name="com.example.myapp.permission.READ_USER_DATA"
android:protectionLevel="signature"/>
<permission
android:name="com.example.myapp.permission.WRITE_USER_DATA"
android:protectionLevel="signature"/>
<!-- Content Provider에 권한 적용 -->
<provider
android:name=".SecureContentProvider"
android:authorities="com.example.myapp.provider"
android:exported="true"
android:readPermission="com.example.myapp.permission.READ_USER_DATA"
android:writePermission="com.example.myapp.permission.WRITE_USER_DATA">
</provider>
민감한 데이터는 절대로 평문으로 저장하면 안 됩니다. 암호화를 적용해야 합니다.
class SecureContentProvider : ContentProvider() {
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
// 호출자의 UID 확인
val callingUid = Binder.getCallingUid()
if (!isAuthorized(callingUid)) {
throw SecurityException("Unauthorized access")
}
// SQL Injection 방지를 위해 파라미터화된 쿼리 사용
val db = dbHelper.readableDatabase
return db.query(
TABLE_NAME,
projection,
selection,
selectionArgs, // 직접 문자열 연결 대신 파라미터 사용
null,
null,
sortOrder
)
}
private fun isAuthorized(uid: Int): Boolean {
// 허용된 앱의 패키지명 목록
val allowedPackages = listOf("com.example.trustedapp")
val packageManager = context?.packageManager
val packages = packageManager?.getPackagesForUid(uid)
return packages?.any { it in allowedPackages } ?: false
}
}
SQL Injection 공격을 방지하기 위해서는 사용자 입력을 직접 쿼리 문자열에 연결하지 말고, 반드시 파라미터화된 쿼리를 사용해야 합니다. 위의 코드에서 selectionArgs를 사용하는 것이 그 예시입니다.
4. Broadcast Receiver 보호하기
Broadcast Receiver는 시스템 전체에 전파되는 메시지를 받기 때문에, 신뢰할 수 있는 발신자로부터 온 메시지인지 검증하는 것이 중요합니다.
먼저 Receiver에도 권한을 설정할 수 있습니다.
<!-- 커스텀 권한 정의 -->
<permission
android:name="com.example.myapp.permission.SEND_BROADCAST"
android:protectionLevel="signature"/>
<!-- Receiver에 권한 적용 -->
<receiver
android:name=".SecureDataReceiver"
android:exported="true"
android:permission="com.example.myapp.permission.SEND_BROADCAST">
<intent-filter>
<action android:name="com.example.myapp.ACTION_SHOW_DATA"/>
</intent-filter>
</receiver>
Receiver 내부에서도 발신자의 신원을 확인해야 합니다.
class SecureDataReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// 인텐트의 출처 확인
if (intent?.action != "com.example.myapp.ACTION_SHOW_DATA") {
Log.w("SecureReceiver", "Unknown action received")
return
}
// 발신자 검증
if (!isFromTrustedSource(context)) {
Log.w("SecureReceiver", "Untrusted broadcast source")
return
}
// 데이터 무결성 검증
val data = intent.getStringExtra("data")
val signature = intent.getStringExtra("signature")
if (!verifySignature(data, signature)) {
Log.w("SecureReceiver", "Invalid signature")
return
}
// 안전하게 데이터 처리
processData(data)
}
private fun isFromTrustedSource(context: Context?): Boolean {
// 시스템 브로드캐스트인지 확인하거나
// 알려진 신뢰할 수 있는 앱인지 확인
return true
}
private fun verifySignature(data: String?, signature: String?): Boolean {
// 데이터와 서명을 검증하는 로직
return true
}
}
가능하다면 로컬 브로드캐스트를 사용하는 것이 더 안전합니다. LocalBroadcastManager를 사용하면 브로드캐스트가 앱 내부에서만 전파되므로 외부 공격을 원천적으로 차단할 수 있습니다.
// 브로드캐스트 전송
val intent = Intent("com.example.myapp.ACTION_SHOW_DATA")
intent.putExtra("data", "sensitive information")
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
// 브로드캐스트 수신
LocalBroadcastManager.getInstance(context).registerReceiver(
receiver,
IntentFilter("com.example.myapp.ACTION_SHOW_DATA")
)
5. Deep Link 보호하기
Deep Link는 사용자 경험을 향상시키는 강력한 기능이지만, 제대로 보호하지 않으면 심각한 보안 위협이 될 수 있습니다.
첫째, Deep Link로 진입한 Activity에서 반드시 인증 상태를 확인해야 합니다.
class SecureActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Deep Link로 진입했는지 확인
val data: Uri? = intent?.data
if (data != null) {
// Deep Link로 진입한 경우 인증 확인
val sharedPref = getSharedPreferences("auth", Context.MODE_PRIVATE)
val isAuthenticated = sharedPref.getBoolean("isAuthenticated", false)
if (!isAuthenticated) {
// 인증되지 않았다면 로그인 화면으로 리디렉션
// 원래 요청한 URL을 저장하여 로그인 후 복귀 가능하도록 함
val intent = Intent(this, LoginActivity::class.java)
intent.putExtra("redirect_url", data.toString())
startActivity(intent)
finish()
return
}
}
setContentView(R.layout.activity_secure)
}
}
둘째, Deep Link URL의 파라미터를 검증해야 합니다. 공격자가 악의적인 데이터를 URL에 포함시킬 수 있기 때문입니다.
private fun validateDeepLinkParameters(data: Uri): Boolean {
// URL 스킴 확인
if (data.scheme != "androgoat") {
return false
}
// 호스트 확인
if (data.host != "vulnapp") {
return false
}
// 파라미터 검증
val userId = data.getQueryParameter("user_id")
if (userId != null) {
// 사용자 ID가 현재 로그인한 사용자와 일치하는지 확인
val currentUserId = getCurrentUserId()
if (userId != currentUserId) {
Log.w("DeepLink", "User ID mismatch")
return false
}
}
return true
}
셋째, App Links를 사용하여 도메인 소유권을 검증하는 것이 좋습니다. Android 6.0 이상에서는 App Links를 통해 특정 도메인의 링크가 자동으로 앱에서 열리도록 설정할 수 있으며, 이는 도메인 소유권 검증을 거치기 때문에 더 안전합니다.
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:scheme="https"
android:host="www.example.com"
android:pathPrefix="/secure"/>
</intent-filter>
웹 서버에 .well-known/assetlinks.json 파일을 배치하여 도메인 소유권을 증명해야 합니다.
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints": [
"14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"
]
}
}]
6. 추가적인 보안 강화 방법
앞서 언급한 개별 컴포넌트 보호 외에도 전반적인 앱 보안을 강화할 수 있는 방법들이 있습니다.
ProGuard나 R8을 사용한 코드 난독화는 리버스 엔지니어링을 어렵게 만듭니다. build.gradle 파일에서 다음과 같이 설정할 수 있습니다.
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
루팅 탐지를 구현하여 루팅된 기기에서의 실행을 제한할 수 있습니다. 루팅된 기기는 안드로이드의 보안 메커니즘이 무력화되어 있어 더 큰 위험에 노출됩니다.
fun isDeviceRooted(): Boolean {
// su 바이너리 존재 확인
val paths = arrayOf(
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su"
)
for (path in paths) {
if (File(path).exists()) {
return true
}
}
return false
}
SSL Pinning을 구현하여 중간자 공격(Man-in-the-Middle Attack)을 방지할 수 있습니다. 이는 앱이 특정 인증서만 신뢰하도록 설정하는 것입니다.
val certificatePinner = CertificatePinner.Builder()
.add("example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
Comments
Sign in with GitHub to leave a comment.