Using Volatility on Android

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 and the recovery image

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).

Volatility for Android

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.

Note

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:

  • The 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.
  • The 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 command
  • linux_ifconfig: This plugin simulates the Linux ifconfig command
  • linux_route_cache: It reads and prints the route cache that stores the recently used routing entries in a hash table
  • linux_proc_maps: This plugin acquires memory mappings of each individual process

If 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.

Reconstructing data for Android

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.

Call history

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:

  • Type (incoming, outgoing, or missed)
  • Duration
  • Date and time
  • Telephone number
  • Contact name
  • Assigned photo of the contact

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 class

If 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.

Keyboard cache

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset