안드로이드 AOSP 환경에서 나만의 시스템 서비스를 설계하고 빌드하는 데 성공했다면, 기쁨도 잠시 곧바로 거대한 장벽을 마주하게 됩니다. 바로 보안(Security)과 권한 관리입니다. 안드로이드 OS의 심장부인 system_server 프로세스 내부에서 동작하는 프레임워크 서비스는 시스템 내부의 최고 권한(Root 레벨에 준하는 권한)을 손에 쥐고 움직입니다.
만약 보안 필터를 꼼꼼하게 설계하지 않고 API 엔드포인트를 열어두면, 악의적인 서드파티 앱이 내 커스텀 서비스를 브릿지 삼아 단말기의 하드웨어를 무단 제어하거나 기밀 데이터를 탈취하는 치명적인 보안 취약점(Privilege Escalation)이 발생할 수 있습니다. 구글 역시 플랫폼 버전이 올라갈 때마다 이 보안 관문을 극단적으로 좁히고 있죠. 이번 포스팅에서는 AndroidManifest.xml을 통한 권한 레벨 제어부터, 호출자 프로세스를 필터링하는 바인더 권한 검증 로직, 그리고 시스템을 방어하는 최후의 보루인 SELinux(sepolicy) 매핑 규칙까지 실무 플랫폼 개발에서 필수적으로 요구되는 시스템 보안 아키텍처를 자세히 다뤄보겠습니다.

📌 핵심 요약 3줄
- 커스텀 시스템 서비스는 아무나 접근할 수 없도록 AndroidManifest.xml에 protectionLevel="signature" 등급의 전용 권한을 선언해 보호해야 합니다.
- 바인더 트랜잭션 수신 시 enforceCallingPermission() 등의 API를 활용해 실제 호출 앱이 해당 권한을 획득했는지 PID/UID 단에서 강제 검증해야 합니다.
- 자바 스택 제어가 끝나면 커널 보안 모듈이 차단하지 않도록 sepolicy/private/service_contexts 스크립트에 시스템 서비스 보안 문맥을 명시해 주어야 합니다.
1. 안드로이드 시스템 서비스의 3단계 보안 레이어 비교
안드로이드 플랫폼 커스텀 시 외부 앱의 무단 접근을 차단하기 위해 계층적으로 적용해야 하는 보안 핵심 요소들입니다.
| 보안 계층 | 적용 스택 위치 | 주요 적용 파일 및 메서드 | 실무 관점의 방어 메커니즘 |
| 1단계: 선언 레이어 | 프레임워크 매니페스트 | frameworks/base/core/ res/AndroidManifest.xml |
전역에서 쓸 커스텀 권한 식별자를 정의하고, 제조사 서명(signature)과 일치하는 앱만 권한을 획득하도록 규정 |
| 2단계: 런타임 레이어 | 자바 바인더 서비스 내부 | Binder.getCallingUid() enforceCallingPermission() |
API 메서드가 호출되는 순간, 요청을 보낸 프로세스의 UID를 심사하여 권한이 없는 앱일 경우 SecurityException 발생 |
| 3단계: 커널 레이어 | SELinux (Mandatory Access Control) | device/제조사/보드/sepolicy/ service.te, service_contexts |
리눅스 커널 레벨에서 system_server와 일반 앱 프로세스 간의 바인더 통신 통로 자체를 엄격히 감시하고 허용된 규칙만 통과시킵니다. |
2. AndroidManifest.xml 권한 정의 및 보호 수준 설정
가장 먼저 내 커스텀 서비스에 접근할 수 있는 통행증(Permission)의 스펙을 규정해야 합니다. AOSP 프레임워크 통합 빌드 환경에서는 frameworks/base/core/res/AndroidManifest.xml 소스에 내 커스텀 권한을 심어놓는 것이 표준입니다.
2.1 매니페스트 권한 선언
<!-- frameworks/base/core/res/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="android">
<!-- 제조사 서명 키와 일치하는 특권 앱만 가져갈 수 있도록 signature 레벨로 선언합니다. -->
<permission android:name="com.custom.hardware.permission.ACCESS_MY_SERVICE"
android:protectionLevel="signature" />
</manifest>
2.2 protectionLevel 속성 가이드라인
- normal: 매니페스트에 선언만 하면 사용자 동의 없이 100% 자동 승인되는 낮은 단계의 권한입니다. 시스템 서비스 제어용으로는 절대 쓰이지 않습니다.
- dangerous: 위치 정보나 카메라처럼 앱 실행 중에 사용자에게 팝업을 띄워 동의를 받아야 하는 권한입니다.
- signature: (강력 추천) 앱을 빌드할 때 사용한 암호화 서명 키가 OS 이미지의 서명 키와 완벽히 일치하는 시스템 앱이나 제조사 순정 앱에만 OS가 권한을 부여합니다. 보안성이 가장 뛰어납니다.
- signatureOrSystem (최신 버전은 privileged로 분분): 단말기의 /system/priv-app/ 경로에 탑재된 특권 레이어 앱이거나 서명이 같은 앱에 권한을 배포할 때 씁니다.
3. 자바 레벨 system_server 내부에서의 API 호출자 권한 검증
서비스를 SystemServer.java에 안착시킨 후, 바인더 인터페이스 구현부 안쪽에서 호출자의 자격을 실시간 검증하는 코드를 심어야 합니다.
// frameworks/base/services/core/java/com/android/server/custom/CustomService.java
package com.android.server.custom;
import android.content.Context;
import android.os.Binder;
import android.os.IYourService;
import android.os.RemoteException;
import com.android.server.SystemService;
public class CustomService extends SystemService {
private final Context mContext;
public CustomService(Context context) {
super(context);
mContext = context;
}
@Override
public void onStart() {
// system_server의 바인더 맵에 내 서비스를 퍼블리싱합니다.
publishBinderService("my_framework_service", new BinderImpl());
}
// 실제 바인더 인터페이스 내부 통신 처리부
private final class BinderImpl extends IYourService.Stub {
@Override
public void triggerSecureHardware() throws RemoteException {
// [보안 핵심] 이 API를 호출한 클라이언트 앱이 매니페스트에 정의한 signature 권한을 가졌는지 검사합니다.
// 권한이 없다면 즉시 SecurityException을 던져 하부 비즈니스 로직 진입을 완벽히 차단합니다.
mContext.enforceCallingPermission(
"com.custom.hardware.permission.ACCESS_MY_SERVICE",
"해당 커스텀 하드웨어를 제어할 권한이 없습니다."
);
// 추가적인 방어벽: 호출한 앱의 실제 시스템 UID를 뽑아와 로그에 남기거나 특정 시스템 UID(예: SYSTEM_UID = 1000)인지 체크할 수도 있습니다.
int callingUid = Binder.getCallingUid();
// 안전성이 검증된 상태에서만 하드웨어 레지스터 제어 실행
executeHardwareLogic();
}
}
}
4. 최후의 보루: SELinux (sepolicy) 보안 정책 바인딩
아무리 자바 소스 단에서 권한 체크 코드를 꼼꼼히 짜두어도, 리눅스 커널 단의 강제 접근 제어 메커니즘인 SELinux 정책에 내 서비스를 등록해 주지 않으면 바인더 통신 자체가 커널 레이어에서 영구 격리(Denied)됩니다. sepolicy 스크립트를 수정해 주어야 합니다.
4.1 서비스 도메인 유형 정의 (service.te)
커스텀 서비스 문자열 키값이 시스템 서비스라는 정체성을 가질 수 있도록 타입을 지정합니다.
# device/제조사/보드/sepolicy/service.te
# 내 커스텀 서비스를 시스템 API 서비스 규격 속성(system_api_service) 그룹으로 묶어 선언합니다.
type my_framework_service, app_api_service, system_server_service, service_manager_type;
4.2 바인더 서비스 컨텍스트 매핑 명시 (service_contexts)
ServiceManager에 등록할 때 쓰는 텍스트 키값과 위에서 정의한 SELinux 타입을 1대1로 결합해 줍니다.
# device/제조사/보드/sepolicy/service_contexts
# 자바 소스 단에서 publishBinderService("my_framework_service", ...)할 때 쓴 문자열 키와 정확히 일치시켜 줍니다.
my_framework_service u:object_r:my_framework_service:s0
이렇게 구성해 주면 일반 앱 도메인(untrusted_app)이나 시스템 앱 도메인(system_app)이 바인더 드라이버를 거쳐 my_framework_service 타입에 정상적으로 트랜잭션을 날릴 수 있도록 안전한 통로가 확보됩니다.
🛠️ 개발을 위한 팁 (Tips)
- clearCallingIdentity()와 restoreCallingIdentity() 쌍 활용하기: 내 커스텀 서비스 내부 로직 도중, 안드로이드 순정 OS의 다른 핵심 서비스(예: PackageManager나 SettingsProvider)의 API를 호출해야 하는 경우가 빈번히 생깁니다. 이때 내 커스텀 서비스를 호출했던 일반 서드파티 앱의 UID 권한 그대로 순정 서비스를 호출하면 권한 부족으로 크래시가 납니다. 이럴 때는 호출부 직전에 long token = Binder.clearCallingIdentity();를 호출하여 현재 스레드의 권한 런타임 주체를 클라이언트 앱이 아닌 system_server(Root 등급)로 일시 격상시킨 뒤 처리를 끝내고, finally 블록에서 Binder.restoreCallingIdentity(token);으로 복구해 주는 구조를 취해야 안전하게 프레임워크 연쇄 호출이 가능해집니다.
- SELinux 허용 모드(Permissive)로 우선 테스트: 처음부터 완벽한 sepolicy 규칙을 타이핑하는 것은 불가능에 가깝습니다. 보안 정책 때문에 내 서비스가 막히는 것인지 자바 소스 에러인지 헷갈릴 때는 셸 창에 adb shell setenforce 0 명령을 날려 SELinux를 경고만 내뿜는 허용(Permissive) 모드로 변경해 보세요. 이 상태에서 정상 작동한다면 소스 코드는 문제가 없는 것이니 오직 sepolicy 빌드 스크립트 튜닝에만 집중하시면 됩니다.
⚠️ 흔히 하는 실수 (Common Mistakes)
- enforceCallingPermission 호출 시 자격 증명 누락: 바인더 인터페이스 내부에서 mContext.enforceCallingPermission()을 호출할 때 간혹 오동작하는 경우가 있습니다. 이 메서드는 호출한 외부 프로세스의 권한만 심사하기 때문에, 만약 외부 앱이 아니라 system_server 자기 자신 내부의 다른 쓰레드가 내 커스텀 메서드를 다이렉트로 호출하게 되면 호출자 권한이 '자신'이 되어 권한이 없다는 에러를 뿜게 됩니다. 자기 자신(프로세스 내부 호출)의 요청도 받아들여야 하는 아키텍처라면 mContext.enforceCallingPermission() 보다는 Binder.getCallingPid()가 내 프로세스 PID와 같은지 먼저 예외 처리를 해주는 방어 코드가 필요합니다.
- file_contexts와 service_contexts 스크립트 오지정: 독립형 네이티브 C++ 데몬 파일과 자바 프레임워크 서비스를 구별하지 못해 발생하는 단골 실수입니다. SystemServer.java에 올라가는 자바 기반 서비스는 메모리 런타임 자원이므로 반드시 service_contexts 파일에 한 줄 선언해 주어야 합니다. 파일 시스템 경로를 지정하는 file_contexts 파일에 열심히 적어봤자 시스템 빌드 시 SELinux 컴파일러가 매핑 테이블을 찾지 못해 빌드 드롭이 나거나 런타임에 서비스 인식이 완전히 먹통이 됩니다.
5. 결론
안드로이드 OS 커스텀 환경에서 내가 추가한 자바 프레임워크 서비스에 보안 레이어를 덧입히는 작업은 시스템 전체의 무결성을 지키는 핵심 방화벽을 세우는 일입니다.
단순히 비즈니스 로직을 잘 짜는 단계를 넘어 매니페스트 권한 규격화, 바인더 레벨에서의 UID 심사, 그리고 리눅스 커널이 신뢰할 수 있도록 SELinux 보안 문맥(service_contexts)까지 매끄럽게 연결해 주어야 비로소 커머셜 상용 제품에 탑재될 수 있는 완성도 높은 임베디드 플랫폼 OS가 완성됩니다. 보안 정책을 반영한 뒤 avc: denied 커널 거부 메시지가 쏟아지거나 자바 단에서 SecurityException 트러블슈팅이 풀리지 않는다면, 주저하지 말고 하단 댓글 창에 로그를 복사해 남겨주세요. 아키텍처 보안 장벽을 완벽히 넘을 수 있도록 같이 머리를 맞대보겠습니다!
'Android System & AOSP Engineering > AOSP Framework & Custom Services' 카테고리의 다른 글
| AOSP 고급 디버깅: dumpsys 덤프 분석부터 service call 트랜잭션 주입까지 (0) | 2025.05.30 |
|---|---|
| 안드로이드 앱에서 Custom Framework Service 호출하기: AIDL 래핑과 전역 매니저 연동 실전 (0) | 2025.05.29 |
| AOSP 빌드 및 디버깅 가이드: Custom Framework Service 플래싱부터 dumpsys 검증까지 (0) | 2025.05.27 |
| Android AOSP 가이드: AIDL 생성부터 Context.getSystemService() 등록까지 전 과정 (0) | 2025.05.26 |
| AOSP 커스텀 서비스 개발: 사용자 정의 Framework Service 설계부터 Android.bp 빌드까지 (0) | 2025.05.24 |