본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[android 보안] 안드로이드 보안 모델 #1

by 돼지왕왕돼지 2018. 4. 14.

[android 보안] 안드로이드 보안 모델


출처 : Android Security Internals 1장

/dev/binder, ahead of time, android runtime, androidism, anonymous shared memory, AOT, apk, Art, ashmem, binder, binder_write_read, binder_write_read 구조체, COM, common object request broker architecture, Component Object Model, CORBA, dalvik, dalvik executable, DEX, ginder, init, init 바이너리, inter process communication, io control, ioctl, IPC, ipc 드라이버, ipc 원리, jar, Java Native Interface, java virtual machine, JNI, Just in time, jvm, low memory killer, odex, oopenbinder, optimized dex, paranoid networking, read_buffer, remote procedure call, RPC, user space, wakelock, wakeup, write_buffer, [android 보안] 안드로이드 보안 모델, 공유 메모리, 공유 메모리 세그먼트, 네이티브 라이브러리, 네이티브 사용자 공간, 네트워크, 달빅, 달빅 jit, 달빅 가상 머신, 달빅 실행 파일, 데몬, 드라이버, 디바이스 종속적 dex, 런타임, 레지스터 기반, 레지스터 기반 vm, 롤리팝, 리눅스 커널, 메모리 부족 킬러, 메시지 큐, 명령어 세트, 바인더, 바인더 원리, 보안, 상위 수준 ipc, 세마포어, 소켓, 스택 기반, 스택 기반 jvm, 스택기반 vm, 시그널, 시스템 서비스, 안드로이드 vm, 안드로이드 아키텍처, 안드로이드 커널, 안드로이디즘, 알람, 원격 인터페이스, 웨이크 락, 익명 공유 메모리, 인텐트, 자바 가상 머신, 자바 런타임 라이브러리, 지역 소켓, 추상 인터페이스, 파라노이드 네트워킹, 파이프, 파일, 파일시스템 접근, 프로레스, 프로세스 간 통신, 프로세스 관리, 프로세스간 통신, 하드웨어

개요 목차


1. 안드로이드 보안 모델

     1.1. 안드로이드의 아키텍처

          1.1.1. 리눅스 커널

          1.1.2. 네이티브 사용자 공간

          1.1.3. 달빅 가상 머신

          1.1.4. 자바 런타임 라이브러리

          1.1.5. 시스템 서비스

          1.1.6. 프로세스 간 통신

          1.1.7. 바인더

          1.1.8. 안드로이드 프레임워크 라이브러리

          1.1.9. 앱


     1.2. 안드로이드 보안 모델

          1.2.1. 앱 샌드박스

          1.2.2. 권한

          1.2.3. IPC

          1.2.4. 코드 서명과 플랫폼 키

          1.2.5. 다중 사용자 지원

          1.2.6. SELinux

          1.2.7. 시스템 업데이트

          1.2.8. 검증된 부트


     1.3. 요약




1.1. 안드로이드의 아키텍처

* 1.1.1. 리눅스 커널


-

커널은 하드웨어, 네트워크, 파일시스템 접근, 프로세스를 관리하기 위한 드라이버를 제공한다.



-

안드로이드 커널은 데스크톱이나 비-안드로이드 임베디드 디바이스에서 볼 수 있는 “일반적인” 리눅스 커널과는 약간 다르다.

이 사소한 차이는 안드로이드를 지원하기 위해 추가된 일련의 새로운 기능 (“안드로이디즘(Androidism)” 이라고 부른다) 때문에 발생한다.


안드로이디즘에는 메모리 부족 킬러(Low Memory Killer), 웨이크 락(Wakelock, 주류 리눅스 커널에서 wakeup 소스를 지원하기 위해 통합된 부분), 익명 공유 메모리(anonymous shared memory, ashmem), 알람, 파라노이드 네트워킹(Paranoid Networking, 호출자 프로세스의 종류에 따라 네트워크 접근을 제한하는 옵션), 바인더(Binder, 안드로이드 프로세스 간 통신 메커니즘) 등이 포함된다.



-

안드로이디즘에서 가장 중요한 부분은 “바인더”“파라노이드 네트워킹” 이다.


바인더는 IPC 및 관련된 보안 메커니즘을 구현.

파라노이드 네트워킹은 특정 권한을 가진 앱의 네트워크 소켓 접근을 제한한다.




* 1.1.2. 네이티브 사용자 공간


-

커널 위에는 네이티브 사용자 공간(user space) 계층이 있다.

여기에는 init 바이너리(최초로 시작되는 프로세스로서 다른 프로세스들을 기동시킨다.), 여러 네이티브 데몬, 전체 시스템이 사용하는 수백 개의 네이티브 라이브러리가 들어간다.




* 1.1.3. 달빅 가상 머신


-

안드로이드의 상당 부분은 자바로 구현되어 있으므로 자바 가상 머신(Java Virtual Machine, JVM)에 의해 실행된다.

현재 안드로이드의 자바 VM 구현을 달빅(Dalvik) 이라고 하며, 네이티브 사용자 공간 위에 있다.

달빅은 모바일 디바이스를 염두에 두고 설계되었으며, 자바 바이트코드(.class 파일)를 직접 실행할 수 없다.

달빅이 실행할 수 있는 코드 포맷을 “달빅 실행 파일(Dalvik Executable, DEX)” 라고 하며, .dex 파일 안에 패키징된다.

결국 .dex 파일은 시스템 자바 라이브러리(JAR 파일)나 안드로이드 앱(APK 파일)안에 다시 패키징된다.



-

달빅 JVM 은 레지스터에 기반을 두고 있는 데 반해 오라클의 JVM 은 스택에 기반을 두고 있으며, 명령어 세트도 다르다.

달빅은 동일한 결과를 얻기 위해 더 적은 수의 명령어를 사용한다.

일반적으로 레지스터 기반 VM 이 스택 기반 VM 보다 명령어 수는 더 적지만 생성된 코드는 더 길다.

그렇지만 대부분의 아키텍처에서 코드 로딩은 명령어 실행보다 간단한 연산이므로 레지스터 기반의 VM 이 더 효율적으로 인터프리트된다.


cf) 스택 기반의 JVM 은 CPU 에 대한 제약을 덜 받는다.

     아마도 그러한 이유로 오라클의 JVM 은 스택 기반을 유지하는 듯 하다.



-

상용 제품에는 디바이스 독립적인 DEX 대신 디바이스 종속적인 포맷인 최적화된(optimized) DEX 인 .odex 형태로 저장되어 있다.

이 녀석들은 일반적으로 상위의 JAR 나 APK 파일과 동일한 디렉터리에 저장된다.



cf)

안드로이드 5.0/롤리팝 버전부터 ART(Android Runtime)가 달빅을 대신해 기본 런타임이 되었다.

두 런타임 모두 중간코드로 .dex 를 사용한다.

가장 큰 차이는 달빅이 .dex 를 실행 시에 2진 코드로 컴파일(Just-In-Time, JIT) 하는 데 반해, ART 는 앱 설치 시에 컴파일(Ahead-of-Time, AOT) 하는 방식으로 성능 향상을 꾀하였다.






* 1.1.4. 자바 런타임 라이브러리


-

핵심 라이브러리 대부분은 자바로 개발되었지만 일부는 아직도 네이티브 코드에 의존하고 있다.

네이티브 코드는 자바 코드와 네이티브 코드를 서로 호출할 수 있게 해주는 자바 네이티브 인터페이스(Java Native Interface, JNI)를 통해 안드로이드의 자바 라이브러리에 연결되어 있다.




* 1.1.5. 시스템 서비스


-

몇몇 서비스를 제외한 각 서비스는 다른 서비스나 앱이 호출할 수 있는 원격 인터페이스를 제공한다.

바인더(Binder)가 제공하는 서비스 발견, 중재, 프로세스 간 통신과 결합해 실제로는 리눅스 위에서 시스템 서비스가 객체지향 운영체제를 구현하고 있는 것이다.




* 1.1.6. 프로세스 간 통신


-

바인더는 프로세스 간 통신(Inter-process Communication, IPC) 매커니즘이다.

표준 IPC 메커니즘은 파일, 시그널, 소켓, 파이프, 세마포어, 공유 메모리, 메시지 큐 등의 방법이 있다.

안드로이드는 이 중에서 지역 소켓(local socket) 등 일부는 지원하지만 세마포어, 공유 메모리 세그먼트, 메시지 큐와 같은 시스템 V IPC 는 지원하지 않는다.




* 1.1.7. 바인더


-

표준 IPC 매커니즘은 융통성과 신뢰성이 떨어지므로, 안드로이드에서는 바인더라는 IPC 메커니즘을 새로 만들었다.

안드로이드의 바인더는 새로 구현된 것이긴 하지만, 그 구조와 개념은 OpenBinder 에서 가져왔다.



-

바인더는 추상 인터페이스에 기반해 분산 컴포넌트 아키텍처를 구현한다.

윈도우의 COM(Component Object Model)이나 유닉스의 CORBA(Common Object Request Broker Architecture)와 비슷하지만, 이런 프레임워크와는 달리 네트워크에서 RPC(Remote Procedure Call)을 지원하지 않는다.

(그렇지만 바인더 위에서 RPC 를 구현할 수는 있다.)



** 바인더 구현


-

유닉스 계열 시스템에서 프로세스는 다른 프로세스의 메모리에 접근할 수 없다.

그렇지만 커널은 모든 프로세스를 제어하고 있으므로 IPC 할 수 있는 인터페이스를 제공할 수 있다.

바인더에서는 바인더 커널 드라이버로 구현된 /dev/binder 디바이스가 이 인터페이스를 제공한다.

바인더 드라이버는 프레임워크의 핵심 객체로서 모든 IPC 호출은 이 드라이버를 거쳐간다.

IPC 는 단 한번의 ioctl() 호출을 통해 구현되며, binder_write_read 구조체를 통해 데이터를 보내고 받는다.

binder_write_read 구조체는 드라이버에 보낼 명령을 담은 write_buffer 와 사용자 공간이 처리해야 할 명령을 담은 read_buffer 로 구성된다.



cf) ioctl 은 I/O control 의 준말로 사용자 공간의 코드가 하드웨어 장치, 커널 구성 요소와 통신하는 인터페이스이다.



-

바인더 프로세스는 각 프로세스의 주소 공간의 일부를 관리한다.

바인더 드라이버가 관리하는 메모리는 프로세스 입장에서는 읽기 전용이며, 모든 쓰기 작업은 커널 모듈이 수행한다.

프로세스가 다른 프로세스에 메시지를 보낼 때 커널은 대상 프로세스의 메모리에 공간을 할당하고 보내는 프로세스의 메시지 데이터를 이 공간에 복사한다.

그런 다음 받는 프로세스에 보내고자 하는 짧은 메시지를 큐에 넣고 받을 메시지가 어디에 있는지 알려준다.

받을 메시지가 자신의 메모리 공간에 있기 때문에 받는 프로세스는 이 메시지에 바로 접근할 수 있다.

프로세스는 메시지 처리를 끝낸 후 해당 메모리를 해제해도 된다고 바인더 드라이버에 알려준다.



-

안드로이드가 제공하는 상위 수준의 IPC 인 인텐트(Intent, 다른 프로세스에 있는 컴포넌트에 전달하는, 데이터와 결합된 명령), 메신저(Messager, 프로세스 간에 메시지 기반으로 통신할 수 있게 해주는 객체), 콘텐트 제공자(ContentProvider, 다른 프로세스에 데이터 관리 인터페이스를 제공하는 컴포넌트)는 바인더를 이용해 구현된다.



-

다른 프로세스에 제공하려는 서비스 인터페이스는 안드로이드 인터페이스 정의 언어(Android Interface Definition Language, AIDL)을 이용해 정의하는데, AIDL 을 이용하면 클라이언트가 원격 서비스를 마치 자신의 자바 객체인 것처럼 호출할 수 있다.


SDK 와 함께 제공되는 aidl 도구는 스텁(stub)과 프락시(Proxy)를 자동으로 생성해준다.

스텁은 원격 객체를 클라이언트 관점에서 표현한 것이며, 프락시는 인터페이스 메서드를 바인더에서 제공하는 하위 수준의 transact() 메서드로 대응시키고 파라미터를 바인더가 전송할 수 있는 포맷으로 변환시켜준다.

이 때 네트워크로 전송할 수 있도록 파라미터를 변환하는 것을 마샬링(Marshalling), 네트워크에서 받은 데이터를 파라미터로 복원하는 것을 언마샬링(Unmarshalling)이라고 한다.




** 바인더 보안


-

상위 수준에서 보면 바인더 프레임워크를 통해 접근할 수 있는 객체는 IBinder 인터페이스를 구현하며 이를 ‘바인더 객체’ 라고 한다.

바인더 객체 호출은 바인더 트랜잭션 안에서 수행되며, 바인더 트랜잭션에는 타깃 객체, 실행할 메서드 아이디, 데이터 버퍼에 대한 참조가 들어 있다.


바인더 드라이버는 호출하는 프로세스 아이디(Process ID, PID) 와 유효 사용자 아이디(Effective User ID, EUID)를 트랜잭션 데이터에 자동으로 추가한다.


호출받는 프로세스(Callee)는 호출하는 프로세스(Caller) 의 PID 와 EUID 를 조사하고 내부 논리나 호출하는 앱에 대한 시스템 전반적인 메타데이터를 기반으로 요청된 메서드를 실행할지 판단한다.



-

PID 와 EUID 는 커널이 채워넣으므로, 호출하는 프로세스는 시스템이 허용한 권한보다 더 많은 권한을 갖고자 자신의 정체를 속일 수 없다.

즉 바인더는 권한 상승(Privilege Escalation)을 금지한다.

이 개념이 안드로이드 보안 모델의 핵심 중 하나며, 권한(Permission)과 같은 상위 수준에서의 개념은 여기에 기반을 두고 있다.

호출하는 프로세스(Caller, 호출자)의 PID 와 EUID 는 안드로이드 공개 API 의 일부분인 android.os.Binder 클래스의 getCallingPid() 와 getCallingUid() 메서드를 통해 알 수 있다.




** 바인더 정체


-

바인더 객체의 중요 속성 중 하나는 여러 프로세스를 거치더라도 바인더의 고유한 정체(identity)가 유지된다는 점이다.

바인더를 전달받는 입장에서는 바인더 객체에 대한 핸들(Handle)만 받는다. ( 메모리 직접 접근은 안되므로 )



-

바인더 객체와 다른 프로세스에서 사용하는 핸들은 커널에서 대응시킨다.

바인더 객체의 정체는 고유하며 커널에서 관리하므로, 사용자 공간 프로세스가 임의로 바인더 객체를 복사할 수 없다.

IPC 로 전달받지 않은 바인더 객체를 참조할 수도 없다.

바인더 객체는 고유하고, 위조할 수 없고, 쉽게 전달할 수 있어 보안 토큰(Security Token)으로 활용할 수 있으며, 안드로이드에서 기능(Capability) 기반 보안을 구현할 수 있게 해준다.




** 기능 기반 보안


-

기능 기반 보안 모델(Capability-based Security Model)에서는 프로그램에 위조할 수 없는 기능을 할당하는 방식으로 프로그램이 특정 자원에 접근할 수 있게 허용한다.

기능은 타깃 객체를 참조하며 이 객체에 대한 일련의 접근 권한을 담고 있다.

기능은 위조할 수 없으므로 프로그램이 기능을 소유하고 있다는 사실만으로 타깃 리소스에 대한 접근을 허용하기에 충분하기 때문에, 접근 제어 리스트(Access Control List, ACL) 같은 데이터 구조체를 리소스에 유지할 필요가 없다.




** 바인더 토큰


-

안드로이드에서 바인더 객체는 기능으로 취급할 수 있으며, 이 경우 바인더 객체를 “바인더 토큰(Binder Token)” 이라고 한다.

바인더 토큰을 소유한 프로세스는 바인더 객체에 자유롭게 접근할 수 있으며, 타깃 객체에 바인더 트랜잭션을 수행할 수 있다.

바인더 트랜잭션에 전달된 액션(Action)파라미터에 기반해 다양한 처리를 제공하는 바인더 객체의 경우, 호출자는 바인더 객체에 다양한 액션을 수행할 수 있다.



-

일반적으로 안드로이드에서 시스템(UID 1000) 이나 루트(UID 0)로 실행되는 호출자는 어떤 액션이라도 수행할 수 있게 허용한다.

하지만, 이외 프로세스에 대해서는 추가적으로 권한을 검사한다.



-

공개 API 를 통해 볼 수 있는 바인더 토큰 사례로는 윈도우 토큰(Window Token)이 있다.

각 액티비티의 최상위 윈도우는 바인더 토큰에 연결되어 있는데, 이 토큰을 “윈도우 토큰” 이라고 하고 안드로이드의 윈도우 매니저(앱 윈도우를 관리하는 시스템 서비스)가 이를 관리한다.

앱은 자신의 윈도우 토큰은 가져올 수 있지만 다른 앱의 윈도우 토큰에는 접근할 수 없다.

일반적으로 다른 앱이 자신의 앱 위에 윈도우를 추가하거나 삭제하는 것을 원치 않을 것이다.






** 바인더 객체에 접근


-

보안을 위해 안드로이드가 바인더 객체로의 접근을 통제하며 바인더 객체의 참조를 얻어야만 바인더 객체와 통신할 수 있지만, 일부 바인더 객체(주로 시스템 서비스)는 시스템 어디에서나 접근할 수 있어야 한다.

그래서 프로세스가 필요에 따라 시스템 서비스를 찾아내고 참조를 가져올 수 있게 해주는 메커니즘이 필요하다.



-

서비스를 찾아낼 수 있도록 하기 위해 바인더 프레임워크는 단 하나의 컨텍스트 매니저(Context Manager)를 갖고 있으며,

이는 바인더 객체들에 대한 참조를 관리한다.

안드로이드는 서비스 매니저(Service Manager)라는 네이티브 데몬으로 컨텍스트 매니저를 구현한다.

서비스 관리자 데몬은 시스템 부팅 초기 단계에 시작되므로 다른 시스템 서비스들은 기동되면서 자신을 등록할 수 있다.


서비스는 서비스명과 바인더 참조를 서비스 관리자에 전달함으로써 등록된다.

일단 서비스가 등록되면 어떠한 클라이언트라도 서비스명으로 바인더 참조를 가져올 수 있다.



-

일단 서비스가 서비스 관리자에 등록되면 누구나 서비스의 바인더 객체에 대한 참조를 가져올 수 있으므로, 화이트 리스트에 등록된 특정 시스템 프로세스만 시스템 서비스로 등록할 수 있다.


예를 들어 UID 1002(AID_BLUETOOTH)로 실행하는 프로세스만 블루투스 시스템 서비스를 등록할 수 있다.



-

service list 명령을 실행하면 등록된 서비스명과 구현된 IBinder 인터페이스를 보여준다.




** 그 외 바인더 특징


-

바인더에는 레퍼런스 카운팅(Reference Counting)과 데스 노티피케이션(Death Notification, 혹은 링크 투 데스(Link-to-Death)) 이라는 특징이 있다.


퍼런스 카운팅은 바인더 객체를 아무도 참조하고 있지 않으면 그 객체를 메모리에서 자동으로 해제해주며 BC_INCREFS, BC_ACQUIRE, BC_RELEASE, BC_DECREFS 명령을 이용해 커널 드라이버에서 구현한다.


데스 노티피케이션은 바인더 객체를 제공하는 프로세스가 커널에 의해 강제 종료되었을 때 해당 바인더 객체를 사용하는 앱에 통지하여 앱에서 적절히 정리할 수 있게 해준다.

BC_REQUEST_DEATH_NOTIFICATIOn 과 BC_CLEAR_DEATH_NOTIFICATION 명령, 프레임워크에서 IBinder 인터페이스의 linkToDeath() 와 unlinkToDeath() 메서드로 구현된다.




* 1.1.8. 안드로이드 프레임워크 라이브러리


-

프레임워크는 표준 자바 런타임( java.*, javax.* 등 )을 제외한 모든 자바 라이브러리를 포함하며, 대부분은 android. 로 시작하는 패키지다.

프레임워크에는 액티비티, 서비스, 콘텐트 제공자를 구현하기 위한 상위 클래스 android.app.* 패키지, GUI 위젯 android.view.* 및 android.widget 패키지, 파일과 데이터베이스 접근을 위한 클래스 android.database.* 및 android.content.* 패키지 등 안드로이드 앱을 만들기 위한 기본적인 클래스들이 있다.



-

커널보다 높은 수준에 있는 거의 모든 안드로이드 OS 기능이 시스템 서비스로 구현되어 있지만, 프레임워크에서는 이를 직접 노출하지 않고 매니저(Manager)라는 퍼사드 클래스(Facade Class)를 통해 접근하도록 했다.




* 1.1.9. 앱


** 시스템 앱


-

시스템 앱은 OS 이미지에 포함되어 있으며, 디바이스에서 읽기 전용으로 설정되어 있다.

일반적으로 /system 폴더에 마운트되어 있어서 사용자가 제거하거나 변경할 수 없다.

이 앱들은 안전하다고 가정하며 사용자 설치 앱보다 더 많은 권한을 갖는다.



-

이전 안드로이드 버전에서는 /system 폴더에 설치된 모든 앱을 동일하게 처리했지만, 안드로이드 4.4(Kitkat) 및 이후 버전에서는 /system 폴더에 설치된 모든 앱이 아니라 /system/priv-app/ 폴더에 설치된 앱만 특권을 가진 앱으로 처리하고, 특권을 가진 앱들은 signatureOrSystem 보호 수준을 부여받는다.


/system 폴더에 설치되지 않았더라도 플랫폼 서명 키로 서명한 앱에는 signature 보호 수준을 가진 시스템 권한을 부여한다.



-

시스템 앱은 제거하거나 변경할 수는 없지만 동일한 개인 키로 서명한 앱으로 업데이트할 수 있으며, 일부 앱은 사용자 설치 앱으로 덮어쓸 수도 있다. ( 기본 런처를 다른 런처로 대체 )




** 사용자 설치 앱


-

사용자 설치 앱은 통상 /data 폴더에 마운트되는 읽기-쓰기 파티션에 설치된다.

이 파티션에는 사용자 데이터가 저장되고 앱을 마음대로 제거할 수 있다.

각 앱은 전용 보안 샌드박스(Sandbox)에 설치되어 다른 앱에 영향을 주거나 데이터를 가져올 수 없다.

게다가 앱은 권한을 가진 리소스에만 접근할 수 있다.

“권한 분리(Privilege Separation)” 과 “최소 권한 원칙(Principle of Least Privilege)” 은 안드로이드 보안 모델의 핵심이다.




** 안드로이드 앱 컴포넌트


-

AndroidManifest.xml 도 대부분의 안드로이드 리소스 파일과 마찬가지로 크기를 줄이고 파싱 속도를 높이기 위해 이 파일은 앱 패키지(APK) 파일에 번들로 묶기 전에 ASN.1 과 비슷한 2진 XML 파일로 컴파일된다.



-

AndroidManifest.xml 파일은 앱을 설치할 때 파싱하며, 이 파일에서 정의한 패키지와 컴포넌트가 시스템에 등록된다.

안드로이드는 개발자의 키를 이용해 앱을 서명하도록 요구한다.

이렇게 해서 설치된 앱은 함부로 동일한 패키지명의 다른 앱으로 교체되지 않도록 보장한다.

같은 키로 서명된 앱으로 업데이트할 경우에만 동일 패키지명의 앱으로 교체할 수 있다.





댓글0