Abstract/BLUF
The goal of this project is to analyze the Citymapper app on Android and search for forensic artifacts present in its configuration and storage files. Citymapper is a navigation app focused on pedestrian and public transit navigation in intracity areas. Citymapper only requires a logical extraction to obtain its configuration files and main database. This means that the device does not need to be rooted to obtain a full file system image like may be required for other apps. The ADB backups of this app contain preference files with fields that include the user’s first name, last name, email address, phone number, and device location when the app was first opened. Additionally, the main Citymapper SQLite database contains tables describing the user’s favorited navigation trips, favorited locations, history of locations searched for, and recent trips taken with the app. These tables and fields can be used by a forensic examiner to determine basic information about the user, their work and home addresses, and their location history throughout time if the app was actively used to navigate. For an overview of the forensic artifacts present in this data, skip to the Key Forensic Artifacts section.
Table of Contents
- Abstract/BLUF
- Concept
- Testing
- Acquisition
- Analysis
- Key Forensic Artifacts
- Conclusion
- Downloads
- Acknowledgment
- References
Concept
As part of my DFOR 762 project, I analyzed the forensic artifacts that can be found in the Citymapper app for Android. Citymapper is a navigation map centered around public transit through city centers and urban areas. The Google Play store lists the app as having over 10 million installs, which makes it a plausible source for recovering forensic artifacts from a seized device1. From my research, I have not found any other articles about forensic data extraction from the Citymapper app on Android, so this is a good source of information for any forensic analysts that come across this app while investigating Android mobile devices.
Testing
The testing phase started with preparing the Android device for generating test data. The test device used is a Samsung Galaxy S8, model SM-G950U1. I verified the phone was updated to the latest software release and installed any updates for apps currently installed on the device. I cleared the data and cache from Citymapper and uninstalled it from the phone to clear any old data from the app. Then, I downloaded a new copy of Citymapper from the Google Play Store. I also installed the “Mock GPS” app from the Play Store, which I will use to spoof my location. This app will be used to make this test data more realistic and detailed without going on real transit trips with the phone because it does not have a data plan. Additionally, it will prevent accidental exposure of my exact location. All the system information and versions of the used apps can be seen in table 1.
Item | Version |
---|---|
Make | Samsung Galaxy S8 |
Model | SM-G950U1 |
Android Version | 9.0 |
Android Security Patch Level | December 1, 2020 |
Google Play Store Version | 27.3.15-21 [0] [PR] 398783257 |
Citymapper Version | 10.43 and 10.44 |
Mock GPS Version | 1.2.2.0 |
Table 1: System information and app versions
Generating Test Data
To have relevant test data to analyze, I experimented with using various features in the app to generate as much data as possible so that I can determine the purpose for all the fields and tables in the forensic images. Before opening the Citymapper app for the first time, I started Mock GPS and set my location to the Johnson Center, the main student center on George Mason University’s Fairfax campus. This can be seen in figure 1, including the Mock GPS joystick that is overlayed on the screen in the bottom right in all subsequent screenshots for actively controlling the location of the device. Then, I opened the newly installed version of Citymapper and allowed access to location permissions. The app correctly identified my location at GMU and identified this as the “DC-Baltimore” area. The app has a list of supported metropolitan areas you can choose from and autodetects one based on your location when opening the app for the first time. If you do not allow location permissions, the app lets you manually pick a region that you would like to use. After loading, it correctly showed the current spoofed GPS location in the main map interface as seen in figure 2.
Citymapper has an account system that you can sign up for and use, so I went to the app settings and created an account using my GMU email address and randomized password. After verifying my email, I added a first and last name (“TestFirstName” and “TestLastName”) to the account. Additionally, I added a U.S. phone number and verified it, shown in figure 3. Next, I set my work location arbitrarily to the Washington Monument in Washington, D.C. (2 15th St NW, Washington, D.C. 20024), and set my home location to The Hub at GMU (10423 Rivanna River Way, Fairfax, VA 22030). Finally, I saved a custom place named “Dulles Airport” at 1 Saarinen Cir, Sterling, VA 20166. The purpose of saving these locations and creating an account is to see if they are accessible inside the app’s database in the extracted image.
Figure 1: GPS spoofing app.
Figure 2: Spoofed location in Citymapper.
Figure 3: Account created.
Once the saved places were populated, I changed my spoofed location to be at the Vienna/Fairfax-GMU metro station. At 2021-10-17 17:45 EDT, I started a new trip with a destination of the West Falls Church-VT/UVA metro station and selected the option to take the Orange Line, shown in figure 4. I pressed the star button at the top to save this trip and then started the trip to begin navigation. To simulate riding the metro, I began to change my location in the app, both by using the on-screen joystick for fine controls and manually moving my location roughly 1,500 ft at a time in the Mock GPS app for larger steps. Once I reached the Dunn Loring-Merrifield station, I received a notification in Citymapper that my stop was coming up soon. Once I arrived at the destination, the screen showed a checkmark in the top right which I tapped to end the journey, shown in figure 5.
Figure 4: Starting trip on the orange line.
Figure 5: Arriving at the destination.
While still at West Falls Church, I started another trip, this time to CVS Pharmacy at 1150 W Broad St, Falls Church, VA 22046, and selected the option to walk. I navigated partially on this journey but canceled the trip before I finished to see if the app tracks incomplete journeys. I also saved this trip as a favorite. Finally, I set my location to the CVS with the Mock GPS app and set the destination in Citymapper to the West Falls Church station again. I started the trip and once I reached the cross streets of Falls Church Dr and Haycock Rd, I closed the app. After reopening the app, the trip was not still in progress; however, there was still an active notification to press in the notification drawer of the phone, which resumes the navigation. I opened the “issues” button on the main page, then tapped the notification to resume the navigation, and then closed the app for the final time. To ensure the app was fully closed before beginning acquisition, I used the force quit option in the Android settings.
This is the end of the main test data that I generated and will be made available for download. Throughout my analysis of the database and preference files, I also cleared the app multiple times and produced additional test data to ensure different database fields behaved the way I thought they did.
Acquisition
To begin acquisition, I installed all the tools in table 2 that I needed to create the image from the phone. To be safe and ensure the tools were working correctly, I used three different acquisition tools to verify the information returned is the same.
Item | Version |
---|---|
Magnet ACQUIRE | 2.43.0.27145 |
Android Debug Bridge (ADB) | 1.0.41 (SDK 31.0.3-7562133) |
Android Backup Extractor (ABE) | 20211009062527-4c55371 |
Andriller | 3.5.3 |
HashCheck | 2.1.11 |
Table 2: Acquisition tools
I began with Magnet ACQUIRE by connecting the phone with a data cable to the computer and selecting the SM-G950U1 from the list of devices. For the image type, the device is not rooted so the only available image type is “quick”, as seen in figure 6. I specified where to store the evidence folder and pressed “ACQUIRE” to begin the acquisition. The imaging took around 9 minutes to complete and afterward the summary screen in figure 7 was shown. Inside the produced quick image ZIP file, there is an adb-data.tar file containing the backup of the apps, including Citymapper (com.citymapper.app.release). I extracted this out of the archive for analysis in the next phase.
Figure 6: Selecting the Quick image type in ACQUIRE
Figure 7: ACQUIRE summary screen after imaging completed
To confirm the Magnet ACQUIRE data was accurate, I tested acquisition with two other tools. The first is Andriller. Andriller was able to detect the phone and I ran the extraction, shown in figure 8. This produced a few reports as well as a backup.ab file, which was pre-extracted into a data directory and contained the backup data for the installed apps. This data appears to be identical to the ACQUIRE adb-data.tar data, except for the shared data directory (where camera pictures, music, and other user-accessible data are stored). This makes sense because both tools are ultimately just using Android backups to create their images.
Figure 8: Andriller extraction
The last extraction method I tested was using ADB directly. Since ADB is more granular in terms of its available options, I was able to select Citymapper to backup and exclude all the other apps, as shown in the commands issued in Figure 9 below2. This significantly decreases the backup time.
Figure 9: ADB backup
To test the validity of these backup methods, I compared the extracted files from each of the images together using HashCheck. To do this, I copied the com.citymapper.app.release folder out of the ADB backup and generated a SHA-1 hash of the folder. I then ran this hash against the Andriller backup folder for Citymapper which completely matched. I finally compared the hash to the ACQUIRE backup which matched except for one file, _manifest, which differed by a single number. This difference can be ignored since I will not be analyzing that file, and it most likely changed due to the different times I acquired the image from the device. For simplicity, I will be using the ADB backups in the analysis section due to their ease of use and acquisition, since it has been shown they contain the same data as the Andriller and Magnet ACQUIRE images and are the quickest to acquire.
After acquiring this baseline test image for analysis, which is provided for download below, I cleared the data for Citymapper and reinstalled it again. To obtain a clean image of the app when it has no user-defined data, I opened the app briefly to let it detect my location and then immediately closed it again. I took an ADB backup of the app in this state and will use it as a reference point to see what values change between installations and what changes if you’re not logged in and have no history. It is evident that the Citymapper app has Android backups enabled for its data, so I do not believe there is a need to root the device to acquire any additional data since it appears I am getting back most of what the app stores already. However, there is room for future research into what additional data, if any, can be obtained from a full file system image.
Analysis
The normal files present in a logical image of the Citymapper app initialized in the US-DC region are the following:
- com.citymapper.app.release/
- db/
- citymapper.db
- citymapper.db-journal
- sp/
- MapResourceState.xml
- preferences.xml
- regionCode.xml
- us_dc_region_preferences.xml
- _manifest
- db/
XML Files
Citymapper’s XML configuration files are included in the sp folder of the ADB backup archive. There are four files present in this directory, and all of them are present in the archive regardless of the state of the app except for MapResourceState.xml which is only generated once maps are used by the user.
MapResourceState.xml
This file contains the last position and scale of the PDF maps included in the app for the local transit services (Metrorail, MARC, and Baltimore’s transit in this case). An example of this file from the image can be seen in Figure 10. The -Scale floats range from a low decimal value when zoomed all the way out to 1.0 when zoomed all the way in. The -Position strings are coordinates for the position the user was last viewing in the PDF. An entry is not included in the MapResourceState.xml file unless the map has been viewed, so it can be assumed that a user has not viewed a map if there is no entry for it in this file.
1
2
3
4
5
6
7
8
9
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<float name="us-dc-metrorail-pdf-map.pdf-Scale" value="0.036764707" />
<string name="us-dc-metrorail-pdf-map.pdf-Position">19584.0,21888.0</string>
<string name="us-dc-marc-pdf-map.pdf-Position">8640.0,8640.0</string>
<float name="us-dc-marc-pdf-map.pdf-Scale" value="0.083333336" />
<float name="us-dc-baltimore-pdf-map.pdf-Scale" value="0.065789476" />
<string name="us-dc-baltimore-pdf-map.pdf-Position">10944.0,15552.0</string>
</map>
Figure 10: Example MapResourceState.xml
preferences.xml
The preferences.xml file is the main preferences file and is the most useful XML file for forensics. The following are the properties seen inside this file that may have forensic value:
- The earliestSeenVersionCode and earliestSeenVersion fields contain the earliest version of the app installed on the device (without considering any data clears or device wipes).
- The latestSeenVersionCode and latestSeenVersion fields contain the latest version of the app installed and should match what was the last active version of the app on the device.
- The wasRegionSet field will be true if the app has been opened and the region was either manually set or automatically set by allowing location permission.
- The firstRunLocation field will contain coordinates in decimal degrees format of the device’s location when the app was first run and location permissions were allowed.
- The Dismissed God Messages field will contain UUIDv4 formatted IDs for any alert messages that were dismissed by the user.
- The isCommuteOnboarded field is set to true when the user’s commute has been saved, which requires setting the home and work locations and the commute option on the main page has been expanded at least once.
- The Has viewed go screen field will be true if the user has pressed “Go” to start a trip before.
- The loggedInProvider field will be set to different values depending on how a user has logged in. It will be “MAGICLINK” if logged into a regular Citymapper account and used a magic link email to login, “GOOGLE” if the user logged in with their Google account, or “FACEBOOK” if the user logged in with their Facebook account.
- The loggedInUser field contains an HTML encoded JSON object with various information about the user if they have logged into the app:
- id: A 12-character string representing an internal ID for the user.
- email: The email registered to the user or the Google or Facebook account.
- first_name and last_name: First and last name of the user as specified when signing up or provided by Google of Facebook.
- logged_in_with: Will contain “magiclink”, “google”, or “facebook” depending on how the user logged in.
- contact_phone_number: The user’s phone number provided when they signed up. Only appears to be set when the account is a Citymapper account, not when they are logged in with Google or Facebook.
- pass_subscription: This will probably contain some information if the user has purchased a Citymapper CLUB membership, but was not tested.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<long name="earliestSeenVersionCode" value="1043070" />
<boolean name="Has viewed go screen" value="true" />
<string name="lastSeenVersion">10.44</string>
<boolean name="isCommuteOnboarded" value="true" />
<string name="RECENT_SCENARIO_IDS">["","","","","classic","classic"]</string>
<boolean name="syncRequired" value="false" />
<string name="loggedInUser">{"id":"4NGsIb80k6Qo","email":"[email protected]","first_name":"TestFirstName","last_name":"TestLastName","is_interested_in_pass":false,"has_started_pass_signup":false,"logged_in_with":"magiclink","pass_subscription":{"subscription_status":"NONE","jetpack_status":"UNKNOWN","api_subscription_state":"Not started or unknown"},"contact_phone_number":"+17035550123"}</string>
<string name="first_run_location">38.829972403960795,-77.30742417275906</string>
<boolean name="has_checked_install_referrer" value="true" />
<string name="Camera mode">camera 2d</string>
<boolean name="Logged Device Features" value="true" />
<long name="lastSeenVersionCode" value="1043070" />
<set name="Dismissed God Messages">
<string>9dfb67e3-3a7f-4800-a59f-5089a49e9098</string>
</set>
<string name="loggedInProvider">MAGICLINK</string>
<string name="earliestSeenVersion">10.43</string>
<boolean name="wasRegionSet" value="true" />
</map>
Figure 11: Example preferences.xml
regionCode.xml
The regionCode.xml file contains the current region that the app is in, stored in the code field. For the D.C. region, the code is “us-dc”.
us_dc_region_preferences.xml
The region preferences file is prefixed with the region code it describes and contains a few statistics about the saved home/work commutes.
Database
The main SQLite database for the app is citymapper.db and is located in the db directory in the ADB backup. The database contains 10 tables that are already loaded into the database when the app is first opened. There are no database or XML files present until the app is opened for the first time.
The standard time format used by the Citymapper database for all fields are strings in the format “YYYY-MM-DD HH:MM:SS.SSS000” where the seconds are stored with three decimal places of precision followed by three zeroes. No time zone information is included, and the timestamps are recorded in local time. All data in the database is stored as text, including integers, floats, timestamps, etc.
The tables in citymapper.db include:
alertentry
This table has been empty during all of my testing.
android_metadata
This table contains a “locale” column with a single row containing “en_US”, which appears to be set when the app is first launched.
calendarevent
This table has been empty during all of my testing.
favoriteentry
This table contains a list of favorited navigation routes. Deleted routes are kept inside the table with the deleted column filled with the timestamp they were deleted. The columns are:
- color: Contains the primary color of a route, such as the metro line color. This is an RGB color stored in four bytes. The first byte is always 0xFF, representing the alpha value. The next three bytes are red, green, and blue respectively. This value is then converted into a signed integer, which is usually negative due to the leading 0xFF. This is representative of a standard android.graphics.Color object3. Finally, the integer is converted into an ASCII-encoded string and stored in the SQL database. For example, the RGB hex color “#00ae4c” would be encoded as “-16732596” inside the database.
- created: A timestamp for when the route was saved by the user.
- deleted: A timestamp for when the route was deleted by the user.
- id: A UUIDv4 formatted random ID for the route.
- isDirty: Unknown meaning but stores either a 0 or a 1.
- latitude and longitude: Coordinates in decimal degrees format for the stop. Only stored for “route” types.
- mergedRouteIconNames: Name for the route icon (e.g. us-dc-GR for the D.C. Metro Green Line).
- modificationDate: Possibly a modification date for the favorite but doesn’t appear to change.
- name: A human-readable name for the favorite.
- primaryBrand: An internal value for what transit system the favorite is associated with (e.g. WMATAMetro, WMATAMetrobus, CapitalBikeShare)
- regionCode: The region code for the region the location is in (e.g. “us-dc”).
- targetId: An internal value for the metro line, bus route, etc. (e.g. WMATAMetroGreen, DCStation_ViennaFairfaxGMU, CapitalBikeshare_692).
- textColor: Color for text in the same format as color.
- type: The type of favorite saved (e.g. Route, Stop, CycleStation).
- shortName: Through my testing, has always been NULL.
locationhistoryentry
This table contains a list of previously entered or selected addresses, transit stations, and locations. There is a maximum of 25 locations stored in this table. If this limit is reached, the oldest row will be deleted. If a previously entered location is reentered, it will be removed from the table and inserted at the end as a new entry. The following columns are present:
- address: Contains the street address of the location, which can be null.
- date: Contains the timestamp for when that address was searched for by the user.
- id: An integer starting at 1 and increases each time a new location is added to this table.
- lat and lng: Correspond to coordinates in decimal degrees format of the location.
- searchResultType: Contains the type of result the location was, which includes place, address, station, and **unknown. A place has the value **unknown when it was a location manually moved to on the map by the user, rather than being searched for, and will typically be a street.
- name: Contains a friendly name of the location that is shown to the user.
- placeId: Contains an identifier for the location. This will either start with “citymapper:” followed by a station name (e.g. citymapper:DCStation_CharlesCenter), start with “google:” followed by the Google Maps ID for the location (e.g. google:ChIJvX5Ce2BOtokRRHVV5lOmZtc), or contain no prefix and just the location ID (e.g. “NationalAirport” for Reagan National Airport). Locations with a searchResultType of __unknown will have a NULL placeId.
- regionCode: The region code for the region the location is in (e.g. “us-dc”).
- brandIds, categoryIconUrl, categoryNames, mobileDetailsUrl, role, routeIconNames, source, and thumbnailUrl: Through my testing, all of these columns have always been NULL.
placeentry
This table stores custom places saved by the user. In addition to custom values, the home and work locations are also saved here. The columns are:
- address: Contains the street address of the location, which can be null.
- created: A timestamp for when the place was saved by the user.
- deleted: A timestamp for when the place was deleted by the user.
- editability: Unknown but is 2 for built-in places like home and work, and 1 for user-defined places.
- id: A UUIDv4 formatted random ID for the route.
- isDirty: Unknown meaning but stores either a 0 or a 1.
- lat, lng: Correspond to coordinates in decimal degrees format of the location.
- modified: A timestamp for when the place was modified by the user.
- myPlacesOrder: The order in which saved places are displayed. Home and work are incremented to the highest value automatically when new places are saved.
- name: User-specified or default human-readable label for the place.
- populated: Contains 0 if the place has not been populated with data yet, otherwise 1. User-specified places will always be 1.
- regionCode: The region code for the region the location is in (e.g. “us-dc”).
- role: Will be NULL except for the built-in places “home” and “work” which can be set on the main screen.
- searchPlaceId: The same as placeId in the locationhistoryentry table.
- type: Through my testing, this has always been 2.
- lastUse, templatePlaceId: Through my testing, all of these columns have always been NULL.
placehistoryentry
This table has been empty during all of my testing.
savedtripentry
This table contains saved and recent trips which display on the home screen of the app. The columns are:
- commuteType: Will be either be NULL, “HOME_TO_WORK”, or “WORK_TO_HOME” depending on the type of trip.
- created: A timestamp for when the trip was saved by the user.
- deleted: A timestamp for when the trip was deleted by the user.
- documentId: A UUIDv4 formatted random ID for the trip.
- endLat and endLng: Correspond to coordinates in decimal degrees format of the trip ending location.
- homeLat and homeLng: If commuteType is HOME_TO_WORK or HOME_TO_HOME, will correspond to coordinates in decimal degrees format of the user’s saved home location. Otherwise, it will be NULL.
- workLat and workLng: If commuteType is HOME_TO_WORK or HOME_TO_HOME, will correspond to coordinates in decimal degrees format of the user’s saved work location. Otherwise, it will be NULL.
- id: An integer starting at 1 and increases each time a new trip is added to this table.
- isDirty: Unknown meaning but stores either a 0 or a 1.
- manuallySaved: Appears to always be 0.
- originalSignature: JSON data containing information about the original trip when it was started. This contains information about the trip, including the source, destination, car to be taken, and legs of the journey.
- signature: JSON data containing information about the actual trip that was taken, which may differ from originalSignature if the trip was rerouted. See originalSignature.
- startLat and startLng: Correspond to coordinates in decimal degrees format of the trip starting location.
- tripData: JSON data containing information about the trip, including the source, destination, geometry for the exact route to be taken, and instruction for each stop on the trip.
- tripListData: From my testing, appears to always contain either NULL or “[]”.
- tripType: Contains COMMUTE_TRIP if the trip was between the work and home locations. Contains SAVED_TRIP if the trip was saved by the user manually. Contains RECENT if the trip was recently taken by the user. Contains CURRENT_TRIP if the trip is still being actively taken by the user.
- tripOrder, slug, userName: Through my testing, all of these columns have always been NULL.
sqlite_sequence
This table contains two columns, “name” and “seq”, and corresponds to sequence numbers for other tables. Specifically, the “locationhistoryentry” and “savedtripentry” tables both have a corresponding row in this table with their name and the maximum sequence number used in the “seq” column. This number is incremented the next time a row is added to one of these tables.
synceddocumentary
This table has been empty during all of my testing.
Key Forensic Artifacts
The following is a summary of the key forensic artifacts that can be found in the Citymapper logical image:
- Home and work addresses can be found in the placeentry table in the citymapper.db database with the role column set to “home” or “work”.
- The user’s location when the app was first opened can be found in the preferences.xml file in the first_run_location field.
- The user’s first name, last name, email address, and phone number can be found in the preferences.xml file in the loggedInUser field if they have logged into the app.
- Recent trips the user has gone on can be found in the savedTripEntry table in the citymapper.db database and can be identified by looking for “RECENT” in the tripType column.
- Search history in the app can be found in the locationhistoryentry table in the citymapper.db database.
- Favorited routes can be found in the favoriteentry table in the citymapper.db database.
- Custom places saved by the user can be found in the placeentry table in the citymapper.db database.
Conclusion
The Citymapper app on Android can be a good source of forensic artifacts about a user and their location history if the app has been actively used. It is especially useful since rooting the device is not required to obtain this information; a logical image or ADB backup is enough to extract all of the app’s data. If the user has used the app to navigate on trips, then their trip history can be recovered to obtain a general timeline of their location. Saved trips and locations can also be recovered which may indicate places of interest, including their home and work addresses. A sample logical image of the app’s data has been provided and the steps performed to generate it are described in the Test Data section of this post.
Downloads
The logical image of the test data I created in the Generating Test Data section can be downloaded below.
citymapper.tar.gz
MD5 | 0AA1EBFCFBA5321C8011D449CACE358F |
SHA-1 | 2A9F102210FB967164E6507E1B68974B3F3391A2 |
SHA-256 | FEAD1EC1BD63FD5C2779DE4326A59BC381AE495D484C72A4574B8B6D1211C51A |
Acknowledgment
Thank you to Jessica Hyde (@B1N2H3X) for her excellent teaching in DFOR 762 at GMU and her mentorship throughout this project.
References
-
“Citymapper: The Ultimate Transport App,” Google Play. https://play.google.com/store/apps/details?id=com.citymapper.app.release (accessed Nov. 13, 2021). ↩
-
J.-C. Vassort, “Backup android app, data included, no root needed, with adb,” GitHub Gist, Oct. 06, 2019. https://gist.github.com/AnatomicJC/e773dd55ae60ab0b2d6dd2351eb977c1. ↩
-
“Color,” Android Developers, Feb. 24, 2021. https://developer.android.com/reference/android/graphics/Color ↩