Reversing the CyberPatriot National Competition Scoring Engine

Edit 4/12/2019

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.

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

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