Language-Based Security on Android

Language-Based Security on Android Avik Chaudhuri University of Maryland at College Park [email protected] Abstract In this paper, we initiate a formal...
Author: Camron Robbins
0 downloads 1 Views 169KB Size
Language-Based Security on Android Avik Chaudhuri University of Maryland at College Park [email protected]

Abstract In this paper, we initiate a formal study of security on Android: Google’s new open-source platform for mobile devices. Specifically, we present a core typed language to describe Android applications, and to reason about their dataflow security properties. Our operational semantics and type system provide some necessary foundations to help both users and developers of Android applications deal with their security concerns. Categories and Subject Descriptors D.4.6 [Operating Systems]: Security and Protection—Access controls, Verification; D.3.3 [Programming Languages]: Language Constructs and Features—Control constructs General Terms Security, Languages, Verification Keywords data-flow security, hybrid type system, mobile code, certified compilation

1.

Introduction

Android [3] is Google’s new open-source platform for mobile devices. Designed to be a complete software stack, it includes an operating system, middleware, and core applications. Furthermore, it comes with an SDK [1] that provides the tools and APIs necessary to develop new applications for the platform in Java. Interestingly, Android does not distinguish between its core applications and new applications developed with the SDK; in particular, all applications can potentially interact with the underlying mobile device and share their functionality with other applications. This design is very encouraging for developers and users of new applications, as witnessed by the growing Android “market” [2]. At the same time, it can be a source of concern—what do we understand about security on Android? Indeed, suppose that Alice downloads and installs a new application, developed by Bob, on her Android-based phone.

Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. To copy otherwise, to republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. PLAS ’09 June 15, Dublin, Ireland. c 2009 ACM 978-1-60558-645-8/09/06. . . $10.00 Copyright

Say this application, wikinotes, interacts with a core application, notes, to publish some notes from the phone to a wiki, and to sync edits back from the wiki to the phone. Of course, Alice would not like all her notes to be published, and would not like all her published notes to be edited; for instance, her notes may include intermediate results of her ongoing lab experiments. How does she know whether it is safe to run the application? Can she trust the application to safely access her data? If she cannot, is there still a way to safely run the application? These concerns are important for Alice, because she realizes that running a malicious application on her phone can be disastrous; for instance, it may compromise the records of her experiments. Conversely, it is Bob’s concern to be able to convince Alice that his application can be run safely on her phone. This paper initiates an effort to help Alice and Bob deal with these related concerns, through a unified formal understanding of security on Android. To this end, we envision a recipe inspired by PCC [11]: Bob constructs a safety proof for his application by some conservative analysis of the associated Java code, and Alice verifies the proof before installing the application. Such a recipe requires (at least) two ingredients: (1) a formal operational semantics for application code in an Android environment—this includes, in particular, formal specifications of the APIs provided by the SDK; (2) a static safety analysis for application code based on this semantics—in particular, this analysis may be formalized as a security type system for applications, and a soundness proof for such a system should provide the necessary safety proofs for well-typed applications. We take some initial steps towards composing such a recipe in this paper. • We design a core formal language to describe and reason

about Android applications abstractly. For now, we focus only on constructs that are unique to Android, while ignoring the other usual Java constructs that may appear in Android applications. This simplification allows us to study Android-specific features in isolation. Still, to reason about actual Android applications we must consider the usual Java features in combination with these features, and we plan to extend our language to include those features in the future. • We present an operational semantics for our language.

Our semantics exposes the sophisticated control flows

that underlie Android’s constructs. Indeed, while the official documentation provides only a vague idea of what these constructs mean, a formal understanding of their semantics is crucial for reasoning correctly about the behavior of Android applications. We test our semantics by running our own applications on an Android emulator (included in the SDK). • We present a type system for security in this language.

Our system exploits the access control mechanisms already provided by Android, and enforces “best practices” for developing secure applications with these mechanisms. We develop some new technical concepts, including a special notion of stack types, for this purpose. The resulting guarantees include standard data-flow security properties for well-typed applications described in our language. We expect that these guarantees can be preserved without significant difficulty by extending our type system to handle other usual Java constructs. As future work, we plan to extend our analysis so that it can handle application code in Java, and implement it using existing static analysis tools [10, 12, 7]. As envisioned above, certified installation of Android applications based on such an implementation should help both users and developers deal with their security concerns. More ambitiously, we believe that this setting provides an ideal opportunity to bring language-based security to the mainstream. Indeed, Android applications are usually small and structured, so we expect type inference to scale well on such applications; furthermore, the idea of certified installation should certainly be attractive to a growing and diverse Android community. The rest of the paper is organized as follows. In Section 2 we present an overview of Android, focusing on the application model and security mechanisms. In Section 3 we present our formal language for Android applications. In Section 4, we present our security type system for this language, and outline key properties of typing. Finally, in Section 5, we discuss some related work and conclude.

2.

Overview of Android

2.1

The application model

In Android’s application model [1], an application is a package of components, each of which can be instantiated and run as necessary (possibly even by other applications). Components are of the following types: Activity components form the basis of the user interface; usually, each window of the application is controlled by some activity. Service components run in the background, and remain active even if windows are switched. Services can expose interfaces for communication with other applications. Receiver components react asynchronously to messages from other applications.

Provider components store data relevant to the application, usually in a database. Such data can be shared across applications. Consider, e.g., a music-player application for an Androidbased phone. This application may include several components. There may be activities for viewing the songs on the phone, and for editing the details of a particular song. There may be a service for playing a song in the background. There may be receivers for pausing a song when a call comes in, and for restarting the song when the call ends. Finally, there may be a provider for sharing the songs on the phone. Component classes and methods The Android SDK provides a base class for each type of component (Activity, Service, Receiver, and Provider), with methods (callbacks) that are run at various points in the life cycle of the associated component. Each component of an application is defined by extending one of the base classes, and overriding the methods in that class. In particular: • The Activity class has methods that are run when some

activity calls this activity, or returns to this activity. • The Service class has a method that is run when some

component binds to this service. • The Receiver class has a method that is run when a

message is sent to this receiver. • The Provider class has methods to query and update the

data stored by this provider. 2.2

Security mechanisms

As mentioned above, it is possible for an application to share its data and functionality across other applications, by letting such applications access its components. Clearly, these accesses must be carefully controlled for security. We now describe the key access control mechanisms provided by Android [1]. Isolation The Android operating system builds on a Linux kernel, and as such, derives several protection mechanisms from Linux. Every application runs in its own Linux process. Android starts the process when any of the application’s code needs to be run, and stops the process when another application’s code needs to be run. Next, each process runs on its own Java VM, so the application’s code runs in isolation from the code of all other applications. Finally, each application is assigned a unique Linux UID, so the application’s files are not visible to other applications. That said, it is possible for several applications to arrange to share the same UID (see below), in which case their files become visible to each other. Such applications can also arrange to run in the same process, sharing the same VM. Permissions Any application needs explicit permissions to access the components of other applications. Crucially, such permissions are set at install time, not at run time.

The permissions required by an application are declared statically in a manifest. These permissions are set by the package installer, usually via dialogue with the user. No further decisions are made at run time; if the application’s code needs a permission at run time that is not set at install time, it blocks, and its resources are reclaimed by Android. Enforcing permissions can prevent an application from calling certain activities, binding to certain services, sending messages to certain receivers, and receiving messages from other applications or the system, and querying and updating data stored by certain providers. Signatures Finally, any Android application must be signed with a certificate whose private key is held by the developer. The certificate does not need to be signed by a certificate authority; it is used only to establish trust between applications by the same developer. For example, such applications may share the same UID, or the same permissions.

3.

Language

We now proceed to design a core formal language to describe Android applications. To narrow our focus, we do not model general classes and methods; instead, we treat component classes and methods as primitive constructs. Furthermore, we ignore isolation and signatures, since permissions suffice to model the effects of those mechanisms in Android. 3.1

Syntax

We assume a lattice w of permissions (e.g., PERMS). In Android, this lattice is implemented over sets. Components are identified by names. In Android, components are accessed through intents; an intent simply pairs the name of the component to access (action) and a value (parameter) to be passed to the component. Values include names n, variables x and a constant void. Our syntax of programs is as follows. Program syntax v ::= n | x | void i ::= (n, v) t ::= call(i) return(v) bind(i, λx.t) register(SEND, λx.t) send(RECEIVE, i) !n n := v let x = t in t0 t t + t0 v

value intent code call activity return from activity bind to service register new receiver send to receiver read from provider write to provider evaluate fork choice result

We describe the meanings of programs informally below; a formal operational semantics appears in Section 3.2. A program runs in an environment that maps names to component definitions. In Android, such an environment is derived from the set of applications installed on the system.

Application syntax d ::= activity(CALL, PERMS, λx.t, λx.t0 ) service(BIND, PERMS, λx.t) receiver(SEND, PERMS, λx.t) provider(READ, WRITE, v) D ::= ∅ | D, n 7→ d

definition activity service receiver provider hash of definitions

Furthermore, a program runs with a permission (context). In general, the program may be run on a stack of windows (produced by calls to activities), or in a pool of threads (produced by forks). Exceptions are call or return programs, which can only be run on the stack. • The program call((n, v)) checks that n is mapped to an

activity of the form activity(CALL, PERMS, λx.t, λx.t0 ), and that the current context has permission CALL. A new window is pushed on the stack, and the program t is run with permission PERMS and with x bound to v. • Dually, return(v) pops the current window off the stack and returns control to the previous activity; if that activity is of the form shown above, the program t0 is run with permission PERMS and with x bound to v. • The program bind((n, v), λx.t0 ) checks that n is mapped

to a service of the form service(BIND, PERMS, λx.t), and that the current context has permission BIND. The program t is run with permission PERMS and with x bound to v, and the result v 0 is passed back to the current context; then, t0 is run with x bound to v 0 in the current context. In Android, v 0 is typically an interface to some functionality exposed by the service. Below, we encode away such services with receivers. • The program register(SEND, λx.t) creates a fresh name

n, maps it to the receiver receiver(SEND, PERMS, λx.t) in the environment, and returns n; here, PERMS is the permission of the current context. • Dually, send(RECEIVE, (n, v)) checks that n is mapped to a receiver of the form receiver(SEND, PERMS, λx.t), that the current context has permission SEND, and that PERMS includes RECEIVE. The program t is run with permission PERMS, and with x bound to v. • The program !n checks that n is mapped to a provider of

the form provider(READ, WRITE, v), and that the current context has permission READ; the value v is returned. • Dually, n := v checks that n is mapped to a provider of the form provider(READ, WRITE, v 0 ), and that the current context has permission WRITE; n is then mapped to provider(READ, WRITE, v) in the environment. • The program let x = t in t0 evaluates t, and then evaluates

t0 with x bound to the result. • The program  t forks a thread that runs t. • The program t + t0 evaluates either t or t0 . In Android,

such choices arise in the user interface.

Local reduction D; e; E → D0 ; e0 ; E 0 (Red let-distr) (Red let-eval) (Red let-return) (Red fork) (Red read) (Red write) (Red register) (Red send)

D; [PERMS] let x = t in t0 ; E → D; let x = [PERMS] t in [PERMS] t0 ; E D; e; E → D0 ; e0 ; E 0 D; let x = e in [PERMS] t; E → D0 ; let x = e0 in [PERMS] t; E 0 D; let x = [PERMS0 ] v in [PERMS] t; E → D; [PERMS] t{v/x}; E E 0 = E, [PERMS] t D; [PERMS]  t; E → D; [PERMS] void; E 0 D(n) = provider(READ, , v)

PERMS w READ

D; [PERMS] !n; E → D; [PERMS] v; E D(n) = provider(READ, WRITE, )

D0 = D[n 7→ provider(READ, WRITE, v)]

PERMS w WRITE 0

D; [PERMS] n := v; E → D ; [PERMS] void; E n fresh

0

D = D, n 7→ receiver(SEND, PERMS, λx.t)

D; [PERMS] register(SEND, λx.t); E → D0 ; [PERMS] n; E D(n) = receiver(SEND, PERMS0 , λx.t)

PERMS0 w RECEIVE

PERMS w SEND 0

D; [PERMS] send(RECEIVE, (n, v)); E → D; [PERMS ] t{v/x}; E

(Red choice-l)

D; [PERMS] t + t0 ; E → D; [PERMS] t; E

(Red choice-r)

D; [PERMS] t + t0 ; E → D; [PERMS] t0 ; E

Global reduction D; S; E −→ D0 ; S; E 0 (Red local) (Red call) (Red return) (Red thread)

D; e; E → D0 ; e0 ; E 0 D; he, λx.e00 i :: S; E −→ D0 ; he0 , λx.e00 i :: S; E 0 D(n) = activity(CALL, PERMS0 , λx.t, λx.t0 )

PERMS w CALL

S 0 = h[PERMS] void, λx.ei :: S

0

D; h[PERMS] call((n, v)), λx.ei :: S; E −→ D; h[PERMS ] t{v/x}, λx.[PERMS0 ] t0 i :: S 0 ; E S = h[PERMS0 ] void, λx.e0 i :: S 0 D; h[PERMS] return(v), λx.ei :: S; E −→ D; he0 {v/x}, λx.e0 i :: S 0 ; E D; e; E → D0 ; e0 ; E 0 D; S; E, e −→ D0 ; S; E 0 , e0

Figure 1. Small-step operational semantics 3.2

Semantics

We now present a formal small-step operational semantics. Since services can be encoded with receivers (as follows), we do not consider services any further in our development. Encodings

Internal syntax e ::= [PERMS] t let x = e in e0 E ::= ∅ | E, e S ::=  | he, λx.e0 i :: S

expression encapsulate evaluate pool of threads stack of windows

service(BIND, PERMS, λx.t) , receiver(BIND, PERMS, λx.t) bind((n, v), λx.t) , let x = send(⊥, (n, v)) in t

Next, we introduce some internal syntactic categories to describe intermediate states of an Android system. Recall that a program runs with a permission, and may be run on a stack of windows or in a pool of threads. An expression denotes code running with a particular permission (context). A thread is simply an expression. A window is of the form he, λx.e0 i, where e is the expression currently running in the window, and λx.e0 is the callback invoked when a window returns control to this window.

Now, a state is a tuple D; S; E, where D is an environment, S is a stack of windows, and E is a pool of threads. Figure 1 shows reduction rules that formalize the semantics explained in Section 3.1. A global reduction relation, −→, describes the reduction of states. This relation depends on a local reduction relation, →, that describes the reduction of expressions under an environment and a pool of threads. In particular, call and return programs reduce under −→, and all other code reduces under →. Of specific interest are (Red let-return), (Red send), (Red call), and (Red return), that can cause data flows across contexts.

A typical initial configuration of the system is of the form D; he, λx.ei; ∅, where D is the environment defined by the set of installed applications, x is fresh, and e is of the form [>] t; for example, t may be a choice of calls to the main activities of the installed applications, modeling code running in the “home” window of an Android-based phone.

4.

Well-typed code Γ `PERMS t : T (Typ read)

(Typ write)

Type system

Next, we present a system of security types for our language.

(Typ register)

Types τ ::= Any(READ, WRITE) Activity(CALL, τ → τ 0 ⇒ τ 00 ) Receiver(SEND, τ → T ) Provider(READ, WRITE) Stuck T ::= τ τ ⇒ τ0

data type any activity receiver provider stuck type data type stack type

(Typ send) (Typ let) (Typ fork) (Typ choice)

Roughly, the type Any(READ, WRITE) is given to data that may flow from contexts with at most permission WRITE to contexts with at least permission READ. This type is the basis for security specifications in the language (see Theorem 4.2). For example, Any(>, >) types secret trusted data, Any(⊥, >) types public trusted data, and Any(⊥, ⊥) types public tainted data. Next, we have types for each component class, that are given to names bound in the environment. The meanings of these types are given in the rules for well-formed environments below. Besides typing information, these types record the permissions required to access the associated components, so that programs that block due to access control can be identified. Such programs are vacuously safe, and are given the type Stuck. The treatment of Stuck closely follows previous work [8], and we omit the details in the sequel. Finally, we introduce stack types. A stack type τ ⇒ τ 0 is given to an expression running at the top of a stack; values returned by any window to this window have type τ , and values returned by this window have type τ 0 . Note that the code run by an activity must have a stack type, since it is always run on a stack; in contrast, the code run by a receiver may or may not have a stack type. 4.1

(Typ val-hyp) (Typ val-void)

(Typ call) (Typ return)

(Typ activity)

(Typ receiver)

(Typ provider)

(Typ encap) (Typ eval)

(Sub any)

(Typ stack)

WRITE w WRITE0 0

Γ `PERMS n := v : Any(⊥, >) Γ, x : τ `PERMS t : T T 0 = Receiver(SEND, τ → T ) Γ `PERMS register(SEND, λx.t) : T 0 Γ `PERMS n : Receiver( , τ → T ) Γ `PERMS v : τ Γ `PERMS send(RECEIVE, (n, v)) : T Γ `PERMS t : τ

Γ, x : τ `PERMS t0 : T

Γ `PERMS let x = t in t0 : T Γ `PERMS t : τ Γ `PERMS  t : Any(⊥, >) Γ `PERMS t : T

Γ `PERMS t0 : T

Γ `PERMS t + t0 : T v:τ ∈Γ Γ `PERMS v : τ Γ `PERMS void : Any(⊥, >) Γ `PERMS n : Activity(CALL, τ → ⇒ τ 0 ) Γ `PERMS v : τ Γ ` [PERMS] call((n, v)) : τ 0 ⇒ Γ `PERMS v : τ Γ ` [PERMS] return(v) : ⇒ τ

D(n) = activity(CALL, PERMS, λx.t, λx.t0 ) Γ, x : τ `PERMS t : τ 0 ⇒ τ 00 Γ, x : τ 0 `PERMS t0 : τ 0 ⇒ τ 00 Γ, n : Activity(CALL, τ → τ 0 ⇒ τ 00 ) ` D D(n) = receiver(SEND, PERMS, λx.t) Γ, x : τ `PERMS t : T Γ, n : Receiver(SEND, τ → T ) ` D D(n) = provider(READ, WRITE, v) Γ ` v : Any(READ, WRITE) Γ, n : Provider(READ, WRITE) ` D

Well-typed expression, stack Γ ` e : T , Γ ` S : τ ⇒ τ 0

Subtyping Γ ` T