Disclaimer: The following article is for informational and awareness purposes only. Any notable findings during this investigation have been reported using the appropriate channels.

Intro

I have a friend that loves cars and enjoys tuning cars

Some time ago, a friend of mine was working on swapping his Honda Civic K20 engine. The engineer used a program to create the maps for all the engine parameters and my friend wanted to know the exact values used to configure its engine.

The engineer used PCLink to configure the ECU but a password was needed to access the data from the .pclx file that contained the paramenters.

This blog post is a full reverse engineer of how .pclx format ecncrypts data and how to decrypt it easily.

what the hell is a .pclx?

A .pclx file is a tuning map used by PCLink for Link ECUs from generations like G4X and G5. It stores everything you need to rebuild a calibration: parameters, tables, input/output config, CAN, firmware and hardware metadata, stats, fault codes, and a bunch of other stuff.

Even though the extension screams “fully proprietary binary format”, the actual logical content of a PCLX is just an XML document called LinkStorage. The thing stopping you from opening it in a text editor is a binary wrapper encrypted with Block TEA, better known as XXTEA.

There are three different things we need to keep straight from the get-go:

  1. The PCLX container: encrypts the whole XML with XXTEA and a fixed key baked into PCLink.
  2. Password Protection: stores the user’s password inside the XML and lets PCLink decide whether the map is locked or unlocked.
  3. Save as Encrypted: on top of that, uses a validation value tied to a specific ECU. It’s a separate feature from Password Protection.

Here’s the architecture I ended up with:

flowchart LR
    A[.pclx file] --> B[XXTEA with fixed key]
    B --> C[12-byte header]
    C --> D[XML LinkStorage]
    D --> E[D2294: Master Password G4]
    D --> F[D2295: User Password G4]
    E --> G[PCLink access logic]
    F --> G

The goal of this whole thing was to untangle how .pclx files get encrypted and figure out whether we could actually pull the data out of saved maps.

The binary I looked at was PCLink.exe version 7.8.2, grabbed straight from their main site.

A quick first look at the tool shows it’s written in Delphi, an old friend :)

1. First look at a PCLX

Open a .pclx file and you’re greeted with random-looking garbage, i.e. the file looks encrypted. This is already covered in articles like https://ecurecovery.com/blog/how-to-remove-password-link-g4-ecu, which explain how to recover the key set by the user, what we’ll call “Password Protection”.

That article mentions that from G4X onwards, .pclx files are encrypted with XXTEA, so they can’t be modified or read anymore. But here’s the thing: when you open one of the sample files that ships with PCLink itself, it’s also encrypted. So does that mean the encryption key is static and lives inside the executable? Or in one of its libraries?

To decrypt something you need a key, and that key has to be somewhere, right?

Let’s go find it.

The first entry point was to search for the strings the user actually sees in the interface:

Setup Password Protection
Unlock Password Protection
Enter Password
Confirm Password

A minimal radare2 session to inventory the binary and hunt for those strings could look like this:

r2 -q -e bin.relocs.apply=true PCLink.exe
[0x00400000]> aaa
[0x00400000]> iI
[0x00400000]> izz~Password
[0x00400000]> izz~TunerPassword
[0x00400000]> izz~UnlockPassword

The method table and the VCL resources contain, among others, these names:

TunerPassword1Click
UnlockPasswordProtection1Click
OKSetPasswordButClick
Password1Edit
Password2Edit

Following the references and the form method tables got me to two main handlers:

Address Function
0x004C334C TunerPassword1Click: Password Protection setup
0x004C35A8 UnlockPasswordProtection1Click: unlock

These functions let you study the password logic, but first I needed to understand how PCLink turns the opaque file into the XML that those components actually consume.

3. Spotting XXTEA by its constant and its structure

TEA, XTEA and XXTEA are usually easy to recognize thanks to the constant 0x9E3779B9, derived from the golden ratio (go check Wikipedia, this cipher uses that constant). In little-endian it shows up in the executable as B9 79 37 9E.

[0x00400000]> /x b979379e
0x00451717 hit0_0 b979379e
0x00451802 hit0_1 b979379e
0x004518c4 hit0_2 b979379e

All the hits landed inside a single function starting at 0x004516D4:

[0x00400000]> pd 220 @ 0x004516d4

Three details confirmed this wasn’t a coincidence:

  1. At 0x004516F1 it loads 52, divides by the number of words, and adds 6.
  2. At 0x00451713 it adds 0x9E3779B9 in the encryption branch.
  3. At 0x004518C0 it subtracts 0x9E3779B9 in the decryption branch.

The rounds line up with XXTEA:

rounds = 6 + 52 / n

The reconstructed mixing function matches XXTEA too:

MX = (((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4)))
   ^ ((sum ^ y) + (key[(p & 3) ^ e] ^ z));

The function bundles encryption and decryption together. A positive word count triggers the encrypt branch; a negative one triggers decrypt. The read wrapper negates the counter right before calling it:

0x004519e8  neg edx
0x004519ec  call 0x4516d4

So the core function was pinned down as:

0x004516D4  XXTEA encrypt/decrypt over a DWORD vector

4. Rebuilding the container around the XML

The stream encryption function starts at 0x004518D8. The disassembly shows it takes the length of the XML stream, works out how many DWORDs to allocate, prepares three header words, copies the XML starting at byte 12, and calls the XXTEA routine:

0x004518f5  sar edi, 2
0x004518f8  add edi, 4
...
0x00451921  mov dword [ebx], eax       ; random DWORD
0x00451929  mov dword [ebx + 4], eax   ; XML length
0x0045192c  mov dword [ebx + 8], ebp   ; validation
...
0x00451955  call 0x4516d4              ; XXTEA

The allocation works out to:

word_count = floor(xml_length / 4) + 4

That leaves between one and four bytes of padding after the XML. The first DWORD is built from a random value multiplied by 1234567 (well, well), with the float literal showing up at 0x00451984.

The full structure before encryption looks like this:

Offset Size Content
0x00 4 bytes Random DWORD
0x04 4 bytes Exact XML length
0x08 4 bytes Validation value
0x0C variable UTF-8 LinkStorage XML
end 1–4 bytes Padding up to the allocated size

The whole block is then treated as a single DWORD vector and encrypted with XXTEA.

The inverse function starts at 0x00451988. Its steps are symmetric:

  1. Gets file_size / 4.
  2. Reads the whole file into memory.
  3. Negates the counter and calls 0x004516D4.
  4. Checks the validation DWORD at +8.
  5. Reads the length from +4.
  6. Copies exactly that many bytes from +12 into the output stream.

The general-purpose routines wiring this wrapper up to LinkStorage live at 0x0041A104 for reading and 0x0041B404 for writing.

5. Finding the container’s fixed key

The XXTEA function takes a pointer to four DWORDs, i.e. a 128-bit key. Next step was to check its callers from the load and save routines.

At 0x0041A127 the pointer shows up during the read:

0x0041a127  push 0x8eda90
0x0041a132  call 0x451988

And on save, the same address pops up again:

0x0041b485  mov ecx, 0x8eda90

Searching for the little-endian pointer confirmed all its references:

[0x00400000]> /x 90da8e00
0x0041a128 hit0_0 90da8e00
0x0041b486 hit0_1 90da8e00
0x00514bc7 hit0_2 90da8e00
0x00516251 hit0_3 90da8e00

And finally, the four DWORDs:

[0x00400000]> pxw 16 @ 0x008eda90
0x008eda90  0x00004d5c 0x0000b091 0x0091fcc8 0x0007c4fe

So the container key for this version is:

00004D5C 0000B091 0091FCC8 0007C4FE

This is the fixed (or master) container key, not the password the user types in. PCLink needs it to open any PCLX in this format, which is exactly why it’s baked right into the executable.

There was also another 16-byte block at 0x008EDAB0 and an RC4 routine at 0x00451568. Both looked like candidates at first. Instead of just assuming they belonged to the PCLX, I tested them and validated the output. The key at 0x008EDA90 produced a sensible length and valid XML; the one at 0x008EDAB0 produced an impossible length and random garbage. The RC4 routine belonged to a different internal path.

6. Writing a decryption script.

With the algorithm and key nailed down, I threw together a small script. The decryption core reproduces the operations I’d seen, pretty much verbatim:

uint rounds = (uint)(6 + 52 / words.Length);
uint sum = rounds * 0x9E3779B9u;
uint y = words[0];

while (sum != 0)
{
    uint e = (sum >> 2) & 3;

    for (int p = words.Length - 1; p > 0; p--)
    {
        uint z = words[p - 1];
        y = words[p] -= Mix(sum, y, z, p, e, key);
    }

    uint last = words[words.Length - 1];
    y = words[0] -= Mix(sum, y, last, 0, e, key);
    sum -= 0x9E3779B9u;
}

Accepting an output just because it had some printable character in it would’ve been a weak validation. So the script demanded four conditions:

  1. The input file had to be a multiple of four bytes.
  2. The length stored in the second DWORD had to fit inside the container.
  3. The payload had to start with an XML declaration.
  4. The XML had to be well-formed and have LinkStorage as its root.

The sample Honda map spat out this:

random DWORD:     0x000B865E
payload length:   563701
validation DWORD: 0x00000000
payload:          <?xml version="1.0"?>
                  <LinkStorage Version="100" Type=".xml"> ...
padding:          00 00 00

That result confirmed the algorithm, the key, and the header structure all at once. The alternative key returned, for example, a length of several million for a half-megabyte file, so I could rule it out objectively.

As an extra sanity check, I decrypted ECU.cfgr, which installs alongside PCLink. It used the same container and revealed an XML parameter base of 2,438,771 bytes. This file turned out to be the key to putting names on the password fields.

7. Locating the password inside LinkStorage

The ECUrecovery article I mentioned earlier might already get you there, but its technique is a bit janky, it basically just searches for literals that look like a password. What I wanted was to automate this with zero human input.

So once I had the XML, I searched for password-related terms and looked up specific definitions by DataId in ECU.cfgr. Four relevant parameters showed up:

DataId Name in the parameter base Representation
2294 Master Password G4 Vector of 20 values between 0 and 255
2295 User Password G4 Copies the definition from 2294
2303 Password Settings Internal state and options
2324 Password Protection State visible in PCLink

The definition for 2294 specifies one row, twenty columns, min zero, max 255, and a default made of twenty zeros. On a map with no password it looks like this:

<D2294 DataId="2294" Type="1" RowCount="1" ColCount="20" Count="20">
  <Row0 Value="0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" />
</D2294>
<D2295 DataId="2295" Type="1" RowCount="1" ColCount="20" Count="20">
  <Row0 Value="0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" />
</D2295>

On the protected map I used as a sample, D2294 contained:

112,101,112,101,49,50,51,0,0,0,0,0,0,0,0,0,0,0,0,0

Converting each non-zero value straight to a character gives you:

112 -> p
101 -> e
112 -> p
101 -> e
 49 -> 1
 50 -> 2
 51 -> 3

The test password, pepe123, was recovered with zero brute force. No hash, no salt, no key derivation function, just the numeric character codes sitting inside the XML, even though the whole XML was covered by the XXTEA layer.

Conclusions

The PCLX format I looked at has two layers you really don’t want to mix up. The first is an XXTEA container with a fixed 128-bit key baked into PCLink. Its practical job is to stop the file from exposing the XML directly. The second is Password Protection, implemented through two vectors inside that XML.

The password the user types in doesn’t derive the XXTEA key, isn’t stored as a hash, and doesn’t individually encrypt the calibration. PCLink stores it as character codes in D2294 and figures out the state by comparing it against D2295. Because of that, anyone who can reproduce the outer container layer can recover the value and rebuild an unprotected copy.

Save as Encrypted adds an ECU-tied check via the third header DWORD, but it’s still a separate path from Password Protection.

The result is specific to PCLink G5 7.8.2. An update could change the key, the addresses, the parameter IDs, or even the whole format. The methodology, though, still holds up: inventory the binary, hunt for constants and strings, follow the calls, come up with hypotheses, validate them against real files, and demand a round-trip proof before you call the format “understood”. </content> </invoke>