DISCLAIMER: This blog is aimed towards educative purposes. In no way is it endorsing nor encouraging software piracy.

Hacking a Harley's Tuner — Part 3

After nearly a year of absence, it is now time to conclude this adventure. Most of what will be described here will seem out of topic if you haven't read:

Summary

In Part 1, we decrypted and retrieved the full firmware of the programmer. In Part 2, we reverse engineered the communication protocol on the USB link. Now holding all the cards, we can achieve our primary goal: bypassing the licensing system in place!

Licensing functions

The ultimate goal of the exercise is to be able to use the PowerVision without a valid license. It can be achieved in many different ways: forging a license, disabling the verification it is subjected to, or deleting/ignoring the VIN locks. For neophytes, the Vehicle Identification Number is a unique identifier stored in the ECU. A VIN Lock is therefore essentially just a VIN stored in the programmer, which is used to ensure the device will not be used to program anything else. Here are a few sample VINs for Harley Davidson:

  • 1HD1KED10HB661265 — 2017 Harley-Davidson FLHTK / Ultra Limited (1.8L), Touring
  • 1HD1BFV14EB015825 — 2014 Harley-Davidson FXSB-103 Breakout (1.7L V2), Custom
  • 1HD1FC413AB618635 — 2010 Harley-Davidson FLHTCU (1584CC), Touring

License

For the Dynojet PowerVision 1, a license file is something of the form:

XML
<PVLicense>
   <Name>PV1</Name>
   <Company>UNISEC</Company>
   <Email>none</Email>
   <LicenseCode>1234</LicenseCode>
   <ExpireVer>2.1.0</ExpireVer>
   <Cmd>VL:<VIN></Cmd>
   <Signature>
q8EgYRN+XZ/88wEyYfAOQEkZ7GPoV/JbtvuYYsUEOhEWH1cyN1i9OvHPyaj945+f...
   </Signature>
</PVLicense>

The cmd part here contains the command VL, that indicates to the PowerVision which VIN it is coupled to. Controlling the value of this field is the ultimate jackpot, as it enables forging licenses for arbitrary VINs. The signature, however, is here to prevent exactly this. It contains the SHA1 of the XML file, encrypted with Dynojet's private key. In order to verify the signature, the PowerVision stores the public key in an encrypted database. It then proceeds to hash the file, and performs a memcmp on the resulting hash and the one obtained using decryption. Here is the overview of the license verification function:

License verification function overview
Overview of the license verification function

By patching the following code segment, we can easily anticipate how the verification bypass could be implemented:

License memcmp comparison
The memcmp that decides the fate of the license

While reversing the license verification function, we realized that there could be many other ways to forge a license than just changing the VIN. In fact, the cmd field can contain several other functions than the VIN locking one, and they could probably be abused too. Also, one of the easiest ways to defeat the single VIN restriction would be creating a dealer license, as they are not submitted to the same constraints.

VIN Locks

Another way to solve our problem would be to "unmarry" the PowerVision. To do that, we can take two paths:

  • Locate and modify the locks
  • Patch the get_locks function

The first choice was quickly abandoned for the following reason: the PowerVision stores the locks in NVRAM. We already had experienced that issue when trying to locate the firmware encryption key, and the NVRAM can't be read directly from /dev:

NVRAM access attempt
NVRAM cannot be read directly from /dev

It actually uses a low-level API, developed by the hardware manufacturer (Drew Technologies). It seems to be linked to the files:

  • usr/lib/libPP2534.so
  • lib/modules/2.6.30/kernel/drivers/char/ermine_arm7_ldisc.ko

A guess is that we'd need the API to communicate with a kernel module that has the capacity to read and write from the NVRAM. Sounds like a hassle, doesn't it? We have better ways...

Let's take the lazy path, and consider the functions that interpret the results gathered from the NVRAM:

get_locks function
The get_locks function and its lock structure

The get_locks function gathers data from the NVRAM API, and stores lock data in the following structure:

C
lockptr         struc ; (sizeof=0x10)
  active_mask     DCD ?
  used_mask       DCD ?
  first_unused    DCD ?
  total           DCB ?
  used            DCB ?
  free            DCB ?
  max             DCB ?
lockptr         ends

From there, it becomes quite easy to hardcode some of the values in the get_locks function, so that it always returns with a certain amount of free locks!

Micropatching

These are basic patching tricks used under certain conditions. It is far from exhaustive.

Unconditional jump

When getting the amount of active VIN locks, we would like the code to always jump to the block "Tuning: Not Locked to vehicle".

Conditional branch before patch
Before: conditional branch (BEQ)

In order to do that, we replace the Branch if equal instruction by an unconditional Branch instruction:

Unconditional branch after patch
After: unconditional branch (B)

Condition negation

When verifying the license's signature, we would like the code to always jump to the "Verified: YES" location:

Branch if not equal before patch
Before: Branch if Not Equal (BNE)

To do so, we can negate the Branch if not equal instruction, and replace it by Branch if equal:

Branch if equal after patch
After: Branch if Equal (BEQ)

Warning: this is a relatively unstable way of patching. Bear in mind that the code will proceed successfully with any license presenting an invalid signature, but will abort if presented with a valid signature.

NOP

After the license verification, the register R4 contains a boolean, indicating whether the verification has failed or not. If it has failed, it jumps to an error handler and aborts:

Branch if equal to error handler
Before: BEQ to error handler

By replacing the Branch if Equal instruction by a NOP, we make sure this error case is never reached, and the code continues executing after the CMP/NOP instructions:

NOP replacing the branch
After: NOP (MOV R0, R0)

As this code is ARM (little endian), the NOP used was:

ASM
00 00 A0 E1    MOV R0, R0

Firmware update process

Now that we have a patched version of the main binary, we can package it to install it on the PowerVision. In Part 1 we analyzed the content of PVU (PowerVision Update) files, they contain:

  • PVU_TYPE: firmware, recovery, tunes...
  • PVU_FILE: encrypted file containing
    • NEW_ROOTFS.bin: squashfs file system, readonly
    • NEW_KERNEL.bin: kernel update
  • PVU_CERT: update file signature (ouch)

In the code, we discovered that there is an integrity check on uploaded firmware updates. The encrypted databases contain an UPDATES_PK entry, that is used to decrypt the PVU_CERT file. Of course, we could rebuild a database embedding our own key, or even bypass the check using micropatching. But again, we are lazy. While reversing the firmware update function, we discovered a very useful tool:

ASM
0015B3A4  "ubiupdatevol /dev/ubi0_0 /flash/NEW_ROOTFS.BIN"

Connecting with the shell we obtained previously, we were able to confirm the existence and purpose of the ubiupdatevol binary. Like its name indicates, it is used to write directly on the /dev/ubi devices, which contain the squashfs file system we want to patch. The only thing needed is a way to upload a file on the PowerVision device. But we have that too:

Python
def send_file(path, content):
    pvlink = CDLL("./PVLink.dll")
    sendfile = pvlink.PVSendFile
    r = sendfile(path, len(content), content)
    if r != 0:
        print("Error")
    else:
        print("Wrote /flash/storage/rootfs_patch.sqsh")

if __name__ == "__main__":
    f = open("rootfs_patch.sqsh", "rb")
    print("running...")
    send_file("updates:rootfs_patch.sqsh", f.read())

Now we just need to connect, and use the ubiupdatevol tool with the patched file we uploaded. This bypasses the signature verification, as we are now writing directly to the device!

To summarize, the steps are:

  1. Patch main binary
  2. Copy binary to squashfs tree structure, and keep destination file attributes
  3. mksquashfs
  4. Send patched squashfs to the device using the PVLink API
  5. Connect through UART and write to the UBI volume

After a little bit of pimping, we can see that the PowerVision does print the correct "Tuning: Not Locked to vehicle" message:

PowerVision showing Not Locked message
The PowerVision displaying "Tuning: Not Locked to vehicle"

As some of you may have guessed, this message does not necessarily mean that we have bypassed the lock verification. In fact, it only proves that we are able to modify the firmware. To prove that our modifications have deeper implications, we can get the device's information using pvinfo:

Python
def do_soap(request):
    pvlink = CDLL("./PVLink.dll")
    dosoap = pvlink.PVDoSoapEx
    dosoap.argtypes = [c_int, c_char_p, POINTER(c_char_p), c_char_p, c_int, c_int, c_int, c_int]
    ref = c_char_p()
    a = 0
    point = ''
    dosoap(len(request), request, byref(ref), point, 1, 5, a, a)
    return ref.value

def reqtype(typestring):
    req = "<request><type>" + typestring + "</type><ver>1</ver><summary></summary></request>"
    return do_soap(req)

if __name__ == "__main__":
    print(reqtype("pvinfo"))

And here is a comparison of the two responses (before and after patching):

JSON diff before and after patching
pvinfo response comparison — before (left) and after (right) patching

Conclusion

The time we spent on this analysis speaks for itself (over a year). The protections in place are quite robust and well thought out, and the overall firmware's architecture is complex and interesting. The PowerVision 1 has definitely been a worthy opponent, and we had lots of fun working on it.

This last part is mainly to show examples of software bypasses (for licenses, but also security features like anti-VM/anti-debugging functions) that are generic. The fact that we were able to perform them on this device is not a vulnerability in itself, but merely the resulting possibilities of two factors:

  • Being able to read and understand the firmware
  • Being able to write the firmware

We encourage readers interested in such work to report vulnerabilities and bypasses they find to the original software publishers. Good work is always valued!

WARNING: Modifying the PV1 firmware will leave the device in an unstable state. This PoC is not intended to be reproduced, and should NEVER be used against a real bike. It can damage the ECU, and will likely brick the PowerVision.

References

I wish to thank all of you readers, we had great feedback and interesting discussions over the last year about this topic. Stay classy netsecurios!


Read Part 1 · Read Part 2


← Back to articles

Need a security audit or tailored cybersecurity support?

Explore our services →