To analyze volatile memory from Android devices, you will first need LiME. LiME is a Loadable Kernel Module (LKM) that gives access to the whole RAM of the device and can dump it to a physical SD card or network. After acquiring the volatile memory dump with LiME, we will show you how to install and configure Volatility to parse the RAM dump. In the last section, we will demonstrate how to get specific information out of the RAM dump.
LiME is a Loadable Kernel Module (LKM) that allows for volatile memory acquisition from Linux and Linux-based devices, such as Android. This makes LiME unique, as it is the first tool that allows for full memory captures on Android devices. It also minimizes its interaction between user and kernel space processes during acquisition, which allows it to produce memory captures that are more forensically sound than those of other tools designed for Linux memory acquisition.
In order to use LiME on Android, it has to be cross-compiled for the used kernel on the device in question. In the following sections, we will see how these steps are performed for a Nexus 4 with Android 4.4.4 (however, this approach can be adapted to every Android-based device for which the kernel—or at least the kernel configuration—is available as open source).
First of all, we have to install some additional packages on our lab system, as follows:
user@lab:~$ sudo apt-get install bison g++-multilib git gperf libxml2-utils make python-networkx zlib1g-dev:i386 zip openjdk-7-jdk
After installing all the required packages, we now need to configure the access to USB devices. Under GNU/Linux systems, regular users directly can't access USB devices by default. The system needs to be configured to allow such access. This is done by creating a file named /etc/udev/rules.d/51-android.rules
(as the root user) and inserting the following lines in it:
# adb protocol on passion (Nexus One) SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e12", MODE="0600", OWNER="user" # fastboot protocol on passion (Nexus One) SUBSYSTEM=="usb", ATTR{idVendor}=="0bb4", ATTR{idProduct}=="0fff", MODE="0600", OWNER="user" # adb protocol on crespo/crespo4g (Nexus S) SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e22", MODE="0600", OWNER="user" # fastboot protocol on crespo/crespo4g (Nexus S) SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e20", MODE="0600", OWNER="user" # adb protocol on stingray/wingray (Xoom) SUBSYSTEM=="usb", ATTR{idVendor}=="22b8", ATTR{idProduct}=="70a9", MODE="0600", OWNER="user" # fastboot protocol on stingray/wingray (Xoom) SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="708c", MODE="0600", OWNER="user" # adb protocol on maguro/toro (Galaxy Nexus) SUBSYSTEM=="usb", ATTR{idVendor}=="04e8", ATTR{idProduct}=="6860", MODE="0600", OWNER="user" # fastboot protocol on maguro/toro (Galaxy Nexus) SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e30", MODE="0600", OWNER="user" # adb protocol on panda (PandaBoard) SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d101", MODE="0600", OWNER="user" # adb protocol on panda (PandaBoard ES) SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="d002", MODE="0600", OWNER="user" # fastboot protocol on panda (PandaBoard) SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d022", MODE="0600", OWNER="user" # usbboot protocol on panda (PandaBoard) SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d00f", MODE="0600", OWNER="user" # usbboot protocol on panda (PandaBoard ES) SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d010", MODE="0600", OWNER="user" # adb protocol on grouper/tilapia (Nexus 7) SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e42", MODE="0600", OWNER="user" # fastboot protocol on grouper/tilapia (Nexus 7) SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e40", MODE="0600", OWNER="user" # adb protocol on manta (Nexus 10) SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4ee2", MODE="0600", OWNER="user" # fastboot protocol on manta (Nexus 10) SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4ee0", MODE="0600", OWNER="user"
Now the most time consuming part is coming—checking the source code of the Android version that is used. Depending on the speed of the hard drive and Internet connection, this step can take several hours so plan it in advance. Furthermore, keep it in mind that the source code is pretty big so use a second partition with at least 40 GB of free space. We install the source code for Android 4.4.4 as follows:
user@lab:~$ mkdir ~/bin user@lab:~$ PATH=~/bin:$PATH user@lab:~$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo user@lab:~$ chmod a+x ~/bin/repo user@lab:~$ repo init -u https://android.googlesource.com/platform/manifest -b android-4.4.4_r1 user@lab:~$ repo sync
After we have installed the source code for Android 4.4.4, we now need the sources for the kernel running on the device in question. For the Nexus 4 that we are using here, the right kernel is the mako kernel. A list of all available kernels for Google phones can be found at http://source.android.com/source/building-kernels.html.
user@lab:~$ git clone https://android.googlesource.com/device/lge/mako-kernel/kernel user@lab:~$ git clone https://android.googlesource.com/kernel/msm.git
Now that we have all the sources needed to cross-compile LiME, it is time to get LiME itself:
user@lab:~$ git clone https://github.com/504ensicsLabs/LiME.git
After cloning the git
repository to our lab machine, now we have to set some environmental variables that are needed during the build process:
user@lab:~$ export SDK_PATH=/path/to/android-sdk-linux/ user@lab:~$ export NDK_PATH=/path/to/android-ndk/ user@lab:~$ export KSRC_PATH=/path/to/kernel-source/ user@lab:~$ export CC_PATH=$NDK_PATH/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86/bin/ user@lab:~$ export LIME_SRC=/path/to/lime/src
Next, we need to get the current kernel configuration from the device in question and copy it to the correct location in the LiME source. On our Nexus 4, this is possible by entering the following command:
user@lab:~$ adb pull /proc/config.gz user@lab:~$ gunzip ./config.gz user@lab:~$ cp config $KSRC_PATH/.config user@lab:~$ cd $KSRC_PATH user@lab:~$ make ARCH=arm CROSS_COMPILE=$CC_PATH/arm-eabi-modules_prepare
Before we can build the LiME kernel module, we need to write our customized Makefile:
obj-m := lime.o lime-objs := main.o tcp.o disk.o KDIR := /path/to/kernel-source PWD := $(shell pwd) CCPATH := /path/to/android-ndk/toolchains/arm-linux-androideabi-4.4.4/prebuilt/linux-x86/bin/ default: $(MAKE) ARCH=arm CROSS_COMPILE=$(CCPATH)/arm-eabi- -C $(KDIR) M=$(PWD) modules
With the help of this Makefile, we can build the kernel module that is needed to get the volatile memory from an Android device. Entering make
can start this process.
In the following example, we will demonstrate how to push our newly generated kernel module to the device in question and dump the whole volatile memory to our lab environment through TCP.
If you have a device on which the kernel doesn't allow loading modules on the fly, you should consider creating your own recovery image (for example, a custom version of TWRP or CWM), include the LiME kernel module and flash it to the device in question. If you are fast enough during the flashing operation, there is nearly no data lost (for more information, refer to https://www1.informatik.uni-erlangen.de/frost).
The LiME module offers three different image formats that can be used to save a captured memory image on the disk: raw, padded, and lime. The third format—lime— is discussed in detail as it is our format of choice. The lime format has been especially developed to be used in conjunction with Volatility. It is supposed to allow easy analysis with Volatility and a special address space has been added to deal with this format. Every memory dump that is based on the lime format has a fixed size header, containing specific address space information for each memory range. This eliminates the need to have additional paddings just to fill up unmapped or memory mapped I/O regions. The LiME header specification is listed in the following:
typedef struct { unsigned int magic; // Always 0x4C694D45 (LiME) unsigned int version; // Header version number unsigned long long s_addr; // Starting address of physical RAM unsigned long long e_addr; // Ending address of physical RAM unsigned char reserved[8]; // Currently all zeros } __attribute__ ((__packed__)) lime_mem_range_header;
To get such a dump from the Android device in question, connect to the Android device through adb
and enter the following commands:
user@lab:~$ adb push lime.ko /sdcard/lime.ko user@lab:~$ adb forward tcp:4444 tcp:4444 user@lab:~$ adb shell nexus4:~$ su nexus4:~$ insmod /sdcard/lime.ko "path=tcp:4444 format=lime"
On the lab machine, enter the following command to accept the data sent through TCP port 4444 from the Android device to the local lab machine:
user@lab:~$ nc localhost 4444 > nexus4_ram.lime
If the preceding commands are executed successfully, you will now have a RAM dump that can be further analyzed with the help of Volatility or other tools (refer to the next section).
After acquiring a dump file that represents the physical memory of the target system with the tools that we created in the previous section, we intend to extract data artifacts from it. Without an in-depth analysis of Android's memory structures, we would only be able to extract known file formats such as JPEG, or just the JPEG headers with the EXIF data (with tools such as PhotoRec) or simple ASCII strings, which are stored in a contiguous fashion (with common Linux tools such as strings) that could be used to brute force passwords on the devices in question. This approach is very limited as it can be used for any disk or memory dump but does not focus on OS and application-specific structures. As we intend to extract whole data objects from the Android system, we will make use of the popular forensic investigation framework for volatile memory: Volatility.
In this section, we will use a version of Volatility with ARM support (you need version 2.3 at least). Given a memory image, Volatility can extract running processes, open network sockets, memory maps for each process, and kernel modules.
Before a memory image can be analyzed, a Volatility profile must be created that is passed to the Volatility framework as a command line parameter. Such Volatility profile is a set of vtype definitions and optional symbol addresses that Volatility uses to locate sensitive information and parse it.
Basically, a profile is a compressed archive that contains two files, as follows:
System.map
file contains symbol names and addresses of static data structures in the Linux kernel. In case of Android, this file is found in the kernel source tree after the kernel compilation.module.dwarf
file emerges on compiling a module against the target kernel and extracting the DWARF debugging information from it.In order to create a module.dwarf
file, a utility called dwarfdump
is required. The Volatility source tree contains the tools/linux
directory. If you run make
in this directory, the command compiles the module and produces the desired DWARF file. Creating the actual profile is done by simply running the following command:
user@lab $ zip Nexus4.zip module.dwarf System.map
The resulting ZIP file needs to be copied to volatility/plugins/overlays/linux
. After successfully copying the file, the profile shows up in the profiles section of the Volatility help output.
Although the support of Android in Volatility is quite new, there is a large amount of Linux plugins that are working perfectly on Android too. For example:
linux_pslist
: It enumerates all running processes of a system similar to the Linux ps commandlinux_ifconfig
: This plugin simulates the Linux ifconfig
commandlinux_route_cache
: It reads and prints the route cache that stores the recently used routing entries in a hash tablelinux_proc_maps
: This plugin acquires memory mappings of each individual processIf you are interested in how to write custom Volatility plugins and parse unknown structures in Dalvik Virtual Machine (DVM), please take a look at the following paper written by me and my colleagues: Post-Mortem Memory Analysis of Cold-Booted Android Devices (refer to https://www1.informatik.uni-erlangen.de/filepool/publications/android.ram.analysis.pdf).
In the next section, we will exemplarily show how to reconstruct the specific application data with the help of LiME and Volatility.
Now, we will see how to reconstruct application data with the help of Volatility and custom made plugins. Therefore, we have chosen the call history and keyboard cache. If you are investigating on a common Linux or Windows system, there is already a large amount of plugins that are available, as you will see in the last section of this chapter. Unfortunately, on Android, you have to write your own plugins.
One of our goals is to recover the list of recent incoming and outgoing phone calls from an Android memory dump. This list is loaded when the phone app is opened. The responsible process for the phone app and call history is com.android.contacts
. This process loads the PhoneClassDetails.java
class file that models the data of all telephone calls in a history structure. One instance of this class is in memory per history entry. The data fields for each instance are typical meta information of a call, as follows:
To automatically extract and display this metadata, we provide a Volatility plugin called dalvik_app_calllog
, which is shown as follows:
class dalvik_app_calllog(linux_common.AbstractLinuxCommand): def __init__(self, config, *args, **kwargs): linux_common.AbstractLinuxCommand.__init__(self, config, *args, **kwargs) dalvik.register_option_PID(self._config) dalvik.register_option_GDVM_OFFSET(self._config) self._config.add_option('CLASS_OFFSET', short_option = 'c', default = None, help = 'This is the offset (in hex) of system class PhoneCallDetails.java', action = 'store', type = 'str') def calculate(self): # if no gDvm object offset was specified, use this one if not self._config.GDVM_OFFSET: self._config.GDVM_OFFSET = str(hex(0x41b0)) # use linux_pslist plugin to find process address space and ID if not specified proc_as = None tasks = linux_pslist.linux_pslist(self._config).calculate() for task in tasks: if str(task.comm) == "ndroid.contacts": proc_as = task.get_process_address_space() if not self._config.PID: self._config.PID = str(task.pid) break # use dalvik_loaded_classes plugin to find class offset if not specified if not self._config.CLASS_OFFSET: classes = dalvik_loaded_classes.dalvik_loaded_classes(self._config).calculate() for task, clazz in classes: if (dalvik.getString(clazz.sourceFile)+"" == "PhoneCallDetails.java"): self._config.CLASS_OFFSET = str(hex(clazz.obj_offset)) break # use dalvik_find_class_instance plugin to find a list of possible class instances instances = dalvik_find_class_instance.dalvik_find_class_instance(self._config).calculate() for sysClass, inst in instances: callDetailsObj = obj.Object('PhoneCallDetails', offset = inst, vm = proc_as) # access type ID field for sanity check typeID = int(callDetailsObj.callTypes.contents0) # valid type ID must be 1,2 or 3 if (typeID == 1 or typeID == 2 or typeID == 3): yield callDetailsObj def render_text(self, outfd, data): self.table_header(outfd, [ ("InstanceClass", "13"), ("Date", "19"), ("Contact", "20"), ("Number", "15"), ("Duration", "13"), ("Iso", "3"), ("Geocode", "15"), ("Type", "8") ]) for callDetailsObj in data: # convert epoch time to human readable date and time rawDate = callDetailsObj.date / 1000 date = str(time.gmtime(rawDate).tm_mday) + "." + str(time.gmtime(rawDate).tm_mon) + "." + str(time.gmtime(rawDate).tm_year) + " " + str(time.gmtime(rawDate).tm_hour) + ":" + str(time.gmtime(rawDate).tm_min) + ":" + str(time.gmtime(rawDate).tm_sec) # convert duration from seconds to hh:mm:ss format duration = str(callDetailsObj.duration / 3600) + "h " + str((callDetailsObj.duration % 3600) / 60) + "min " + str(callDetailsObj.duration % 60) + "s" # replace call type ID by string callType = int(callDetailsObj.callTypes.contents0) if callType == 1: callType = "incoming" elif callType == 2: callType = "outgoing" elif callType == 3: callType = "missed" else: callType = "unknown" self.table_row( outfd, hex(callDetailsObj.obj_offset), date, dalvik.parseJavaLangString(callDetailsObj.name.dereference_as('StringObject')), dalvik.parseJavaLangString(callDetailsObj.formattedNumber.dereference_as('StringObject')), duration, dalvik.parseJavaLangString(callDetailsObj.countryIso.dereference_as('StringObject')), dalvik.parseJavaLangString(callDetailsObj.geoCode.dereference_as('StringObject')), callType)
This plugin accepts the following command line parameters:
-o
: For an offset to the gDvm object-p
: For a process ID (PID)-c
: For an offset to the PhoneClassDetails classIf some of these parameters are known and passed on to the plugin, the runtime of the plugin reduces significantly. Otherwise, the plugin has to search for these values in RAM itself.
Now, we want to have a look at the cache of the default keyboard application. Assuming that no further inputs were given after unlocking the screen and the smartphone is protected by a PIN, this PIN is equal to the last user input, which can be found in an Android memory dump as a UTF-16 Unicode string. The Unicode string of the last user input is created by the RichInputConnection
class in the com.android.inputmethod.latin
process and is stored in a variable called mCommittedTextBeforeComposingText
. This variable is like a keyboard buffer, that is, it stores the last typed and confirmed key strokes of the on-screen keyboard. To recover the last user input, we provide a Volatility plugin called dalvik_app_lastInput
, as follows:
class dalvik_app_lastInput(linux_common.AbstractLinuxCommand): def __init__(self, config, *args, **kwargs): linux_common.AbstractLinuxCommand.__init__(self, config, *args, **kwargs) dalvik.register_option_PID(self._config) dalvik.register_option_GDVM_OFFSET(self._config) self._config.add_option('CLASS_OFFSET', short_option = 'c', default = None, help = 'This is the offset (in hex) of system class RichInputConnection.java', action = 'store', type = 'str') def calculate(self): # if no gDvm object offset was specified, use this one if not self._config.GDVM_OFFSET: self._config.GDVM_OFFSET = str(0x41b0) # use linux_pslist plugin to find process address space and ID if not specified proc_as = None tasks = linux_pslist.linux_pslist(self._config).calculate() for task in tasks: if str(task.comm) == "putmethod.latin": proc_as = task.get_process_address_space() self._config.PID = str(task.pid) break # use dalvik_loaded_classes plugin to find class offset if not specified if not self._config.CLASS_OFFSET: classes = dalvik_loaded_classes.dalvik_loaded_classes(self._config).calculate() for task, clazz in classes: if (dalvik.getString(clazz.sourceFile)+"" == "RichInputConnection.java"): self._config.CLASS_OFFSET = str(hex(clazz.obj_offset)) break # use dalvik_find_class_instance plugin to find a list of possible class instances instance = dalvik_find_class_instance.dalvik_find_class_instance(self._config).calculate() for sysClass, inst in instance: # get stringBuilder object stringBuilder = inst.clazz.getJValuebyName(inst, "mCommittedTextBeforeComposingText").Object.dereference_as('Object') # get superclass object abstractStringBuilder = stringBuilder.clazz.super.dereference_as('ClassObject') # array object of super class charArray = abstractStringBuilder.getJValuebyName(stringBuilder, "value").Object.dereference_as('ArrayObject') # get length of array object count = charArray.length # create string object with content of the array object text = obj.Object('String', offset = charArray.contents0.obj_offset, vm = abstractStringBuilder.obj_vm, length = count*2, encoding = "utf16") yield inst, text def render_text(self, outfd, data): self.table_header(outfd, [ ("InstanceClass", "13"), ("lastInput", "20") ]) for inst, text in data: self.table_row( outfd, hex(inst.obj_offset), text)
Actually, this plugin not only recovers PINs but also arbitrary user inputs that were given last; this might be an interesting artifact of digital evidence in many cases. Similar to the preceding plugin, it accepts the same three command line parameters: gDvm offset
, PID
, and class file offset
. If none, or only some, of these parameters are given, the plugin can also automatically determine the missing values.