First version of the Android was built over the Linux 2.6 kernel and is customised to embedded (smartphone or tablet) needs. This article provides a basic overview of the Android security architecture and mentions some important changes that may have an impact while porting your native code to Android.
Android applications are written in Java. However, the Android framework for executing applications, which is written in Java, does not take the traditional JVM/JRE approach. Instead, the Java code is compiled into byte-code (as in a pure JVM environment), which is then compiled into another Android-specific format known as the Dalvik EXecutable (DEX) format. This is run in an Android-specific virtual machine, known as the Dalvik VM, which is the heart of Android and the biggest diversion from the pure Java-JVM approach. The Dalvik VM is highly optimised, both in size and execution efficiency. The different approach is because of three primary factors:
Size: A .dex file is composed of several .class files; this gives an opportunity for duplicate data or code to be merged and shared among multiple .class files, which results in a smaller DEX file than possible with multiple .class files.
Run-time efficiency: The Dalvik VM is highly optimised to execute DEX code a major factor in the market’s considerations of usable perating systems. We will later look at how this is achieved with zygotes.
An extra layer of security: DEX code is like the next level of abstraction over Java byte-code.
The Dalvik VM and the Androidised Linux kernel constitute the Android framework for applications, providing full separation between various applications every application has its own unique instance of the Dalvik VM. This constitutes a very secure design. This means that for every application, Android starts a new instance of Dalvik. At first, this design may appear inefficient. However, a very important concept known as Zygote is primarily meant to address this problem. Zygote, which is always running in the background, is responsible for speeding up Dalvik instance creation as it has a pre-initialised footprint of Dalvik core components always ready in memory. Whenever a new instance of Dalvik needs to be created, Zygote speeds it up by using the pre-initialised core.
Android end-user applications and exposed libraries are written in Java. The application developer selects suitable Java classes that expose the required functionality or service.
The Native Development Kit (NDK)
Native code is written in C. So how can native code be integrated in the Android framework, which is Java dominant? Android has a solution for this Native Development (ND). All code written in C (or in other words, all code that can directly run on Linux, outside Dalvik) is called native code. You compile your native code through the NDK provided by Android, and generate a Linux shared library (not DEX format). This shared library can be used in Java applications through the Java Native Interface (JNI) mechanism. It does not run in the Dalvik VM, of course, but directly on the Linux kernel.
The Android security framework
From the developer’s perspective, Android provides two levels of security to applications. Before discussing the details, let’s look at how Android applications are shipped and installed on the device.
An Android application package is a file with the extension .apk. When a user installs such a file, the Android Package Manager (PM) identifies all the native libraries, along with the other components in the package, and puts these libraries in some package-specific location. Also, this application is assigned a unique UID (Linux user ID) and GID (group ID), based on the developer who signed the application package. From then onwards, this application is identified by the Linux kernel as a unique user-a one-app-one-user philosophy. Thus, all the separation provided to one user from another is now applicable to the Android application! Also, permissions to the location where all application data (including native libraries) resides, are as per the assigned UID/GID.
Now let’s come back to the security framework.
Application sand-boxing
In Android, this term is used to describe the encapsulation provided to an application, with the help of Linux user-level (unique UID/GID) security. Each application runs in its own sandbox, and the Linux kernel maintains that separation, making every application’s code and data accessible to that application only. Since this sand-boxing is directly by the kernel, it is very secure.
However, there is an exception to this rule: application developers can choose to share some data or native libraries between multiple applications. The restriction is that such applications should be created and signed by the same developer. In this case, more than one application (owned by the same vendor or developer) is assigned the same UID/GID by the Android package manager. This means that the default application sand-boxing can be broken by the developers, making everything shared among their applications.
Let’s look at how this model affects native code. Let us suppose an application is assigned a user ID <X>, and the application data storage location is <PATH>. Now, only <PATH> has the desired Linux permissions for the user ID <X>. So, if native code running as a part of this application tries to access anything at some other location, the Linux kernel will ensure that this access is denied!
Android permission framework
At the top level, which has the Android (Java) APIs, Android also has another security layer. Android defines a large set of security types (also known as capabilities), which are necessary for an application to exhibit in order to use a specific service from the Android runtime. For example, a permission with the name INTERNET is necessary if the application wants access to network-related resources. If an application tries to use a service or resource for which it has no permission, then the service or resource is denied. The particular permission or capability is declared by the application developer, in the application’s Manifest file. This level of security is over and above application sand-boxing. The important point here is that the Android security framework works evenly for Java code and native code, even though the native code is running outside the Dalvik VM.
Let’s look at an example of how this model influences native code. When native code makes any calls to the Linux API socket, it needs to have the Android permission INTERNET. Similarly, if native code tries to write to the SD card, it should have a permission known as WRITE_EXTERNAL_STORAGE, and so the list goes on.
The Android filesystem hierarchy
Traditional UNIX distributions follow a filesystem standard known as the Filesystem Hierarchy Standard (FHS). However, Android does not follow this or any other existing standard, as far as file system hierarchy is concerned. Various Android-specific file systems are mounted by the kernel at start-up, at not-very-standard mount points. So, native code should avoid making any assumptions about the availability of a specific mount point. Even if a particular mount point (or path like /dev, /tmp etc.) exists on the Android device, we will still not be sure if this is used in the same way it is used in Linux.
As mentioned earlier, Android provides application sand-boxing at the kernel level, which is implicitly applicable to the file system as well. For example, only that part of the file system is accessible to the application that has appropriate permissions, as determined by the application developer. However, there exists a concept of ‘rooting’ an Android device. This mechanism allows the user to escalate to root user privileges. Once this is done, the user can access or modify anything on the file system. The reason for this is that the root user is not subject to any constraints or restrictions, even at the kernel level in line with traditional Linux.
Here are some points to consider:
1. Although Android provides an application separation mechanism, the developer has an option to override this behaviour. You, as a developer, can decide whether your application needs to use this feature.
2. SysV IPC is not supported in the Android C run-time library because of security considerations; so native-code locking primitives that depend on SysV IPC, if any, need to be changed to use an alternative mechanism.
3. Android never stops a (Linux) process, unless the system is extremely low on resources. So, you should optimise the system to minimise memory consumption.
4. Android has tweaked the Linux kernel’s timer mechanism (along with other features not directly related here) in order to cater for the sleep/suspend/wake-up functionality of the device. If your native code uses any such mechanism, then make sure you check with the latest Android documentation.
5. Since any access to protected resources or services is guarded by the application permission framework, all access in native code needs to be analysed, and the required permissions should be identified. Whatever permissions the native code may need should be published for developers, so that they can include these permissions in their applications Manifest file.
6. Native code should not rely on any code or mechanism that needs root access, as this won’t be available on standard Android. Native code should rely only on the permissions governed by Android’s application permission framework which can be specified in the application Manifest file.
7. Any hard-coded paths, like /tmp, which is in itself a bad practice, need to be removed, because such a path may not be available on the Android device, as discussed earlier. Yet, such code is still written, so you should scan your code to be sure!
8. Android is designed for the smartphone and tablet market. These devices need to have a very reliable user-response mechanism. If an application does not respond within a specified time period, it’s possible that the user is unable to switch to another application which is annoying! In Android, this has been taken care of very well. If an application does not respond within a specified period of time (the default is 10 seconds-I am not sure if this is configurable or not) then Android assumes that there is something wrong with the application, and prompts the user with a message like, Application so-and-so not responding, do you want to stop it forcibly?. So, it’s very important that native code does not wait for too long, like in locks, etc.
As mentioned at the start, this is not a complete description of the Android security model. However, I believe this should give readers enough insight into the Android security framework for one to understand how it works, especially for native code developers.
References
[1] http://developer.android.com/guide/index.html
[2] http://developer.android.com/reference/android/Manifest.permission.html
Quote from the article:
” Even if a particular mount point (or path like /dev, /tmp etc.) exists
on the Android device, we will still not be sure if this is used in the
same way it is used in Linux.”
I think I know what this means, but it seems to be drawing a comparison between hardware, as in “the … device” and an operating system, as with the word “Linux”. So in reading this sentence, I experienced a bit of ‘logical disconnect’ that made me momentarily focus on the words, rather than the meaning behind them.
Is this what the sentence really meant, if reworded to maintain a ‘like-with-like’ comparison?
“Even if a particular mount point (or path like /dev, /tmp, etc.) exists
in Android/Linux, we will still not be sure if this is used in the
same way as it is in GNU/Linux.”
If the answer to that question is ‘yes’ — then I will be certain I completely understood. I find it easier to think of mount points as existing within an operating system, rather than existing within a ‘device’.
And, if the reference was to something other than GNU/Linux, please correct my misunderstanding.
Dear BeloSol,
Yes, your point is correct. The original text in article does compare an OS and a device – which is wrong.
Thanks for taking time to raise this bug!
-Raman
good description mate