Android System & AOSP Engineering/AOSP Framework & Custom Services

AOSP 플랫폼 보안: Custom System Service를 위한 SELinux 정책(sepolicy) 구성 매뉴얼

임베디드 친구 2025. 6. 9. 19:21
반응형

안드로이드 프레임워크 소스코드를 수정해 멋지게 나만의 시스템 서비스를 코딩하고 부팅 시퀀스에 빌트인하는 데 성공하셨나요? 하지만 기쁜 마음도 잠시, 단말기를 켜고 내 서비스를 호출해 보면 높은 확률로 먹통이 되거나 로그캣에 avc: denied라는 차가운 보안 거부 에러 메시지가 화면을 가득 채우는 것을 보게 됩니다.

안드로이드 운영체제는 리눅스 커널 상단에 SELinux(Security-Enhanced Linux)라는 초강력 강제 접근 통제(MAC) 가드레일을 촘촘히 치고 있습니다. 아무리 root 권한이나 system 권한을 가진 특권 프로세스라 할지라도, 미리 정의된 보안 정책 명세서(sepolicy)에 "이 서비스는 누구와 소통해도 좋다"라고 명시적으로 도장을 찍어주지 않으면 커널 단에서 통신 포트를 가차 없이 차단해 버리죠. 오늘은 내 커스텀 시스템 서비스가 안드로이드 보안 샌드박스를 안전하게 통과해 정상 가동되도록 돕는 SELinux 도메인 설계 공식과 정책 컴파일 툴체인 적용법을 실무 아키텍트 관점에서 알기 쉽게 정리해 보겠습니다.

Generated by Gemini AI.

📌 핵심 요약 3줄

  1. SystemServer.java에 이식되는 Java 기반 서비스는 service_contexts 등록만으로 충분하지만, 독립 C++ 네이티브 서비스는 고유 도메인(.te)과 실행 컨텍스트까지 모두 정의해야 합니다.
  2. 상용 양산 단말 빌드 시 구글 순정 데이터 영역(system_data_file)을 커스텀 도메인으로 무단 접근하면 neverallow 컴파일 에러가 터지므로 전용 레이아웃 타입을 파야 합니다.
  3. 정책 변경 후에는 m selinux_policy 명령어로 변경된 보안 컨텍스트 파일만 빠르게 갱신하여 에뮬레이터나 실기기에 실시간으로 이식 및 테스트할 수 있습니다.

1. 서비스 형태에 따른 SELinux 정책 구성 필수 컴포넌트 대조

내가 프레임워크에 추가한 서비스의 하드웨어 구동 형태에 따라 어떤 보안 구성 요소를 설계해야 하는지 명확히 정리한 테이블입니다.

보안 정책 구성 파일 경로 Java 기반 프레임워크 시스템 서비스 C++ 기반 독립 네이티브 데몬 서비스
private/service_contexts 필수 등록 (서비스 매니저 문자열 주소 키값과 system_server 컨텍스트 매핑) 필수 등록 (C++ 데몬이 등록할 고유 바인더 이름표와 전용 도메인 매핑)
private/file_contexts 불필요 (SystemServer 내부 메모리 쓰레드로 상주하므로 파일 컨텍스트 없음) 필수 등록 (/system/bin/에 위치할 바이너리 실행 파일의 소유권 타입 선언)
private/custom_service.te 불필요 (기존 순정 system_server.te 보안 도메인 범주 안에서 자동 제어) 필수 작성 (데몬 전역의 실행 자격, 바인더 스레드 허용, I/O 접근 권한 기술)
보안 경계의 주체(Subject) u:r:system_server:s0 권한을 그대로 수용 오직 나만을 위해 개설된 u:r:custom_service:s0 독점 도메인 권한

2. 유형별 커스텀 시스템 서비스 SELinux 정책 작성 가이드

안드로이드 소스 트리 내 보안 정책 저장소인 system/sepolicy/ 경로에 타깃 정책을 올바르게 심는 아키텍처 명세 방식입니다.

2.1 [루트 A] Java 프레임워크 내장 서비스 등록 (service_contexts)

frameworks/base/ 내부에서 자바 클래스로 생성해 ServiceManager.addService("my_custom_java_service", ...) 형태로 등록한 서비스라면 아래 파일 하나만 수정해주면 즉시 바인더 통로가 개방됩니다.

Plaintext
 
# system/sepolicy/private/service_contexts
# 내 서비스의 등록명과 시스템 서버 컨텍스트를 1:1로 결합해 줍니다.
my_custom_java_service                   u:object_r:system_server_service:s0

2.2 [루트 B] C++ 독립 네이티브 데몬 서비스 등록 정책

독립 프로세스로 빌드하여 /system/bin/my_native_daemon 파일로 가동되는 C++ 서비스는 아래와 같이 3종 세트 정책을 생성 및 결합해야 합니다.

① 실행 파일 보안 라벨 지정

Plaintext
 
# system/sepolicy/private/file_contexts
# 시스템 바이너리 폴더에 탑재될 실행 파일에 네이티브 서비스 전용 실행 컨텍스트(exec) 라벨을 붙여줍니다.
/system/bin/my_native_daemon             u:object_r:my_native_daemon_exec:s0

② 서비스 매니저 주소록 라벨 지정

Plaintext
 
# system/sepolicy/private/service_contexts
# C++ 데몬 내부 main() 함수에서 addService할 때 쓰일 문자열 이름표에 고유 서비스 보안 타입을 부여합니다.
my_native_daemon_hardware                u:object_r:my_native_daemon_service:s0

③ 도메인 접근 허용 상세 명세서 작성 (.te 파일)

Plaintext
 
# system/sepolicy/private/my_native_daemon.te
# 1. 타입 및 독립 보안 도메인 선언
type my_native_daemon, domain, coredomain;
type my_native_daemon_exec, exec_type, file_type, system_file_type;
type my_native_daemon_service, service_manager_type;

# 2. 리눅스 init 프로세스가 부팅 시 이 바이너리를 실행하여 my_native_daemon 도메인으로 안전하게 전환(Transition)되도록 허용합니다.
init_daemon_domain(my_native_daemon)

# 3. 바인더 IPC 드라이버를 원활하게 사용할 수 있도록 기본 권한 규칙 매핑
binder_use(my_native_daemon)
binder_call(my_native_daemon, system_server)

# 4. 내 네이티브 서비스를 서비스매니저 주소록에 정식 등록(add)할 수 있도록 허가합니다.
allow my_native_daemon my_native_daemon_service:service_manager add;

# 5. [중요] 자바 앱이나 시스템 서버가 내 네이티브 서비스를 주소록에서 찾을(find) 수 있도록 권한을 개방합니다.
allow system_server my_native_daemon_service:service_manager find;
allow platform_app my_native_daemon_service:service_manager find;

3. 상위 레이어 권한 통제 모델 결합 (AndroidManifest.xml)

SELinux 가드레일을 통과해 바인더 연결 통로가 열렸다면, 악성 서드파티 앱이 내 특권 서비스를 함부로 호출하지 못하도록 프레임워크 상위 레이어에서 앱 서명(Signature) 기준의 2차 방어벽을 설계해야 합니다.

XML
 
<!-- frameworks/base/core/res/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="android">

    <!-- 우리 가문(제조사)의 서명 키로 사인된 시스템 앱들만 이 서비스를 부를 수 있도록 권한 강도를 지정합니다. -->
    <permission
        android:name="android.permission.ACCESS_CUSTOM_HARDWARE_ENGINE"
        android:protectionLevel="signature|privileged"
        android:label="커스텀 특권 하드웨어 엔진 접근 권한"
        android:description="제조사 커스텀 하드웨어를 직접 제어하고 전역 스트리밍을 인출하는 특권 권한입니다." />
</manifest>

이렇게 선언해 둔 뒤, 앞서 작성했던 자바 시스템 서비스 소스코드 내부 메서드 진입점에 mContext.enforceCallingOrSelfPermission("android.permission.ACCESS_CUSTOM_HARDWARE_ENGINE", "Denied!"); 검증 코드를 한 줄 얹어두면 완벽한 2중 보안 구조가 완성됩니다.


4. SELinux 정책 고속 컴파일 및 실기기 플래싱 공정

보안 소스를 수정할 때마다 몇 시간씩 걸리는 전체 OS 이미지를 다시 빌드하는 것은 비효율적이므로, 보안 정책 모듈만 빠르게 증분 컴파일하여 런타임 단말에 주입하는 팁입니다.

Bash
 
# 1. 빌드 환경 초기화 및 타깃 디바이스 세팅
source build/envsetup.sh
lunch aosp_arm64-userdebug

# 2. [시간 단축 팁] 전체 system.img를 다 빌드하지 말고, 오직 selinux 정책 파일만 집어내어 고속 증분 빌드합니다.
m selinux_policy

# 3. 단말기 쓰기 권한 확보 공정 실행
adb root
adb remount

# 4. 컴파일되어 나온 최신 sepolicy 바이너리 팩을 단말기 내부 보안 설정 저장소 경로에 다이렉트로 덮어씁니다.
adb push out/target/product/가상디바이스명/system/etc/selinux/. /system/etc/selinux/
adb push out/target/product/가상디바이스명/vendor/etc/selinux/. /vendor/etc/selinux/

# 5. 단말기 소프트 재부팅을 통해 최신 보안 아키텍처를 시스템 커널에 이식 완료합니다.
adb reboot

🛠️ 개발을 위한 팁 (Tips)

  1. setenforce 0 (Permissive 모드)로 삽질 시간 동결: 새 서비스를 등록했는데 로그캣에 에러가 나면서 아예 서비스 가동조차 안 될 때는, 이게 소스코드 자체의 버그인지 SELinux 가드레일에 막힌 것인지 헷갈릴 때가 많습니다. 이럴 때는 터미널 창에 adb shell setenforce 0 명령을 날려보세요. 보안 모드가 '허용 모드(Permissive)'로 전환되어, 규칙에 어긋난 행동을 해도 로그만 남기고 차단은 하지 않습니다. 전환 후 서비스가 잘 돌아간다면 100% SELinux 규칙 설계 누락이 원인이니 소스 탓을 하지 말고 정책 파일로 돌아와 집중 디버깅하시면 됩니다.
  2. audit2allow 유틸리티 적극 활용: 커널 로그(dmesg)에 찍히는 난해한 avc: denied { read } for pid=451 ... 형태의 보안 거부 에러 메시지를 사람이 읽기 편한 .te 파일 전용 allow 규칙 문법으로 자동 변환해 주는 구글 내장 파이썬 도구입니다. 단말기 로그를 긁어 터미널에 adb shell dmesg | audit2allow -p out/target/product/디바이스명/root/sepolicy를 실행하면, 지금 당장 내 .te 파일에 어떤 허용 한 줄을 추가해야 하는지 인공지능처럼 정답 코드를 뱉어주므로 실무에서 가장 유용하게 쓰입니다.

⚠️ 흔히 하는 실수 (Common Mistakes)

  1. system_data_file에 무단으로 쓰기 권한 추가 (neverallow 위반): 플랫폼 개발을 하면서 편의상 서비스 내부에서 가볍게 설정값이나 텍스트 로그 파일을 파일 시스템에 입출력하려고 일반 순정 구역인 allow my_native_daemon system_data_file:file write; 같은 코드를 냅다 집어넣는 실수입니다. 안드로이드 보안 컴파일러는 단말기의 메인 유저 데이터 저장소가 오염되는 것을 막기 위해 강력한 절대 금지 규칙인 neverallow 규칙을 소스 트리 내부 깊숙이 선언해 두었습니다. 이 규칙을 침범하면 AOSP 빌드 마감 단계에서 컴파일러가 거대한 빌드 크래시를 내뿜으며 아예 빌드를 거부합니다. 파일 I/O가 필요하다면 나만의 고유 데이터 폴더 라벨(type my_service_data_file, file_type, data_file_type;)을 별도로 선언해 우회 설계해야 정석입니다.
  2. service_contexts 내 문자열 태그와 addService 이름의 불일치: 오타나 관리 소홀로 빈번히 일어나는 실수입니다. C++ 소스 내부나 자바 내부에서 ServiceManager.addService("my_cool_hardware", ...)라고 코드를 짜놓고, 정작 보안 정책 파일인 service_contexts 안에는 이름 표기를 약간 다르게 적거나 누락하는 경우죠. 이름이 단 한 자라도 매칭되지 않으면 안드로이드 바인더 드라이버는 해당 서비스를 '미등록 불법 컴포넌트'로 간주하여 실행 즉시 프로세스를 강제 종료(Abort) 시켜 버리니, 소스 코드 내부의 고유 문자열 태그와 보안 설정 파일의 매핑 텍스트가 대소문자까지 완벽하게 일치하는지 항상 교차 체크해야 합니다.

5. 결론

안드로이드 Open Source Project(AOSP)의 광활한 영토 위에서 내가 만든 서비스가 안전하고 당당하게 특권 자원을 다루게 하려면, 커널의 철통 보안관인 SELinux와 올바른 동맹 계약을 체결해 주어야 합니다.

자바 레이어 기반의 프레임워크 내장형 구조인지, 리눅스 루트 기반의 단독 C++ 데몬형 아키텍처인지 내 컴포넌트의 신분을 정확하게 식별하고, 그에 부합하는 service_contexts 및 전역 도메인 정책 명세서를 질서정연하게 설계해 나가야 구글 CTS 보안 적합성 기준을 단 한 번에 통과하는 견고한 커스텀 운영체제를 배출할 수 있습니다. 보안 정책을 빌드 레이어에 증분 이식하는 도중 족보를 알 수 없는 neverallow 컴파일 오류 파괴 현상을 만나 소스 트리가 락에 걸렸거나, audit2allow 변환 툴이 추천해 준 규칙을 적용했음에도 여전히 바인더 링크가 차단되어 곤혹스러우시다면 주저 말고 하단 댓글에 구체적인 AVC 로그 메시지를 복사해 주세요. 하부 리눅스 커널 보안 맥락에서 명쾌하게 디버깅 실마리를 짚어드리겠습니다!

반응형