Dumping Encrypted Viltrox Lens Firmware - two times

In my quest to understand the internals of Sony’s E-Mount camera/lens communication protocol I wanted to take a look at a few different implementation on the lens side. Viltrox makes very interesting, out of the ordinary, lenses and one of their prime virtues is that they are relatively cheap. This makes them a perfect speciment for poking and proding at the electronics.

I got two Viltrox branded lenses, dumped their firmware and reverse engineered their firmware update mechanism. The lenses on my operating table are 28mm F4.5 pancake and 25mm F1.7 E. Firmware updates for these are easily available, but it turned out they were encrypted.

both lenses

I started my investigation on the pancake lens hoping its implementation would be the simplest. After breaking the microcontroller readout protection, the firmware encryption mechanism is easily reverse engineered and firmware plaintext was ready for Ghidra.

On the other hand, I couldn’t use the same encryption keys to decrypt firmware of other Viltrox lenses. Keys were product specific. I then acquired the 25mm lens to dump it the same way, obtain its encryption key and hopefully find some corelation that would enable me to decrypt other models, too. This was not the case as it turns out these two lenses, although both branded Viltrox, seem to be made by different ODMs. First one by LyLens, the second by RAIKAGE.

Spoiler alert: Second lens required an interesting exploit, so read on!

Before screwdriver

I’ve started with the 28mm F4.5 pancake lens as it is quite unusual. Fixed aperture, no manual focus, built in lens protector, as small as possible. And above all, it’s just about $80!

Curiosuly, this lens has a USB-C port. Usually, firmware update on E-mount lenses is done via camera, so this is unusual. I was hoping it uses the standard DFU mode for firmware update, but after testing it out, it was obvious that it implements a custom update protocol on top of HID. Firmware update files are freely available through Viltrox’s app, but they were obviously encrypted.

00000000: 4c59 6c65 6e73 0000 0000 0000 0000 0000  LYlens..........
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 4146 2032 382f 342e 3520 4645 0000 0000  AF 28/4.5 FE....
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000040: 71b1 2176 5b3f 20f4 0f47 9c71 c48c f746  q.!v[? ..G.q...F
00000050: 7631 2e30 3300 0000 0000 0000 0000 0000  v1.03...........
00000060: 3230 3234 3038 3133 3136 3034 3032 0000  20240813160402..
00000070: e09a 0200 05ea 3df1 0100 0000 0000 0000  ......=.........
00000080: 7b3f e710 b048 8e68 59c5 3b92 f383 c385  {?...H.hY.;.....
00000090: c98a a216 b256 0a8b d90b cf59 7e26 92e6  .....V.....Y~&..
000000a0: 3efa d46b f2d0 0044 8182 362c aeec 89e6  >..k...D..6,....
000000b0: 67ea bc77 9bd5 bfe8 41e1 f7dc 78e8 65ef  g..w....A...x.e.
000000c0: bb79 52f6 3367 ec2f 67d8 487d fde3 ff0a  .yR.3g./g.H}....
000000d0: 6be6 5b2a 7774 2b0e 28d8 72f4 2a5d 5399  k.[*wt+.(.r.*]S.
000000e0: 5428 65d7 f301 0572 860a f8f3 659d 9371  T(e....r....e..q
000000f0: 919a 14dc d518 72fa cdeb d613 9d7e dca0  ......r......~..
00000100: c90e b504 9b9c e871 3e08 b3a9 37ad ab9f  .......q>...7...
00000110: a86c 0e5c b692 fb84 f96d e805 2b27 4856  .l.\.....m..+'HV
00000120: b822 4c05 5b3c 0d2e c08c 99ec 4928 5117  ."L.[<......I(Q.
00000130: d807 d54d 52d2 339f a202 b654 e182 10e2  ...MR.3....T....
00000140: d058 24e4 ae97 180d a6e6 8d05 c125 200c  .X$..........% .
00000150: 3816 e11c d731 1152 0b82 236f 8ee3 7c74  8....1.R..#o..|t
00000160: 2d04 8bbf 68c3 d47e 41bc d741 bc16 00e6  -...h..~A..A....
00000170: 4fd5 4ee3 eea7 0a4c 2004 aed6 512d f32a  O.N....L ...Q-.*
00000180: cac4 0e10 a170 9b97 f030 9533 e045 3f34  .....p...0.3.E?4
00000190: 0d57 8ec5 6942 3e25 b4c0 7368 4051 6642  .W..iB>%..sh@QfB
000001a0: d828 f4c7 9a04 e3ca 6e46 6ffc 2376 4552  .(......nFo.#vER
000001b0: 09b5 d476 569e 0c4e aa53 d4af ab8b 9d9f  ...vV..N.S......
000001c0: 68e6 e69b ed50 7914 475c 902a 95b5 3d1a  h....Py.G\.*..=.
000001d0: 6c2d 039f 156e 7126 7cf0 5b83 6993 8493  l-...nq&|.[.i...
000001e0: c9be 5dff b887 14a2 0eb8 74dd 9294 1972  ..].......t....r
000001f0: 63a7 6160 dcb4 c85e 3417 0752 d66b b8fe  c.a`...^4..R.k..
00000200: f4df 200a be48 67b2 a448 3109 5e0b 3ece  .. ..Hg..H1.^.>.
00000210: 2638 d37a cdec 8e0d a167 0aed 161c fb77  &8.z.....g.....w
00000220: 6fc5 61b8 de7d 1bcf 3deb d804 f800 e223  o.a..}..=......#
00000230: 9062 fdc0 869b f581 7839 6427 9890 fe41  .b......x9d'...A
00000240: de5e 4f34 4edc 0d22 8fa9 20e6 caf5 980d  .^O4N..".. .....
00000250: f42b d3cb 6578 1ece 8264 383e 7df7 d5f0  .+..ex...d8>}...
00000260: de61 86e5 813d e7a1 ba40 4fee b1ca 4e64  .a...=...@O...Nd
00000270: cb3d dcdb 843f cbcb ba89 cae4 141a 7f47  .=...?.........G
00000280: 222a bad2 ba74 ae8d ff77 24e1 bb51 bec8  "*...t...w$..Q..
00000290: 3f83 c88f daf9 2349 f01e 6a0a fdad 89da  ?.....#I..j.....
000002a0: 94a6 b79a 2d71 1395 bb91 fa31 d7e9 2a9d  ....-q.....1..*.
000002b0: 560e c764 e226 eb87 a8a7 3f6c 602e 22a6  V..d.&....?l`.".
000002c0: c057 5a6f 6e85 e0e6 34dc 2c2e b4dc f91e  .WZon...4.,.....
000002d0: 2cad 1b52 c4fb 96b2 13aa c354 880f 8d91  ,..R.......T....

The hexdump hints at the encryption method. There’s what appears to be an IV in the simple header that describes the firmware and its size which is then followed by actual encrypted data. There are no repeating blocks anywhere in the ciphertext and it’s an even multiple of 16 bytes. An educated guess is that we are dealing with AES-128 in CBC mode. Comparing a few different versions of the firmware further strenghtens that conclusion: the IV changes completely and so does the ciphertext.

The firmware update app also didn’t contain anything exciting. It just copies the encrypted firmware chunk by chunk to the lens. The lens does the decryption and update. The decryption key is most likely somewhere in the custom bootloader.

Under the flange

The lens is easy to open. A single PCB has a large microconroller labeled VILTROX. I noticed previously that the USB vendor IDs hint at the microcontroller most likely being an STM32 of some family. Lucky for us, one side of the PCB apepars to have debug pins broken out. My first guess, and hope, was that these 4 pins were for SWD.

28mm lens board

SWD, OpenOCD and readout protection bypass

Taking a guess at the SWD pinout, I’ve wired Tigard and fired up OpenOCD.

28mm lens board with tigard attached

Simple OpenOCD config confirms things are working:

interface ftdi
transport select swd

ftdi_vid_pid 0x0403 0x6010
ftdi_channel 1
adapter_khz 200

ftdi_layout_init 0x0028 0x002b
ftdi_layout_signal SWD_EN -data 0
ftdi_layout_signal nSRST -data 0x0020

swd newdap chip cpu -expected-id 0x0
dap create chip.dap -chain-position chip.cpu
target create chip.cpu cortex_m -dap chip.dap

init
dap info

As output, we get:

Open On-Chip Debugger 0.11.0
Licensed under GNU GPL v2
For bug reports, read
      http://openocd.org/doc/doxygen/bugs.html
DEPRECATED! use 'adapter driver' not 'interface'
Info : FTDI SWD mode enabled
DEPRECATED! use 'adapter speed' not 'adapter_khz'
Info : clock speed 200 kHz
Info : SWD DPIDR 0x2ba01477
Info : chip.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : starting gdb server for chip.cpu on 3333
Info : Listening on port 3333 for gdb connections
AP ID register 0x24770011
      Type is MEM-AP AHB3
MEM-AP BASE 0xe00ff003
      Valid ROM table present
              Component base address 0xe00ff000
              Peripheral ID 0x04000bb4c4
              Designer is 0x4bb, ARM Ltd
              Part is 0x4c4, Cortex-M4 ROM (ROM Table)
              Component class is 0x1, ROM table
              MEMTYPE system memory present on bus
      ROMTABLE[0x0] = 0xfff0f003
              Component base address 0xe000e000
              Peripheral ID 0x04000bb00c
              Designer is 0x4bb, ARM Ltd
              Part is 0xc, Cortex-M4 SCS (System Control Space)
              Component class is 0xe, Generic IP component
      ROMTABLE[0x4] = 0xfff02003
              Component base address 0xe0001000
              Peripheral ID 0x04003bb002
              Designer is 0x4bb, ARM Ltd
              Part is 0x2, Cortex-M3 DWT (Data Watchpoint and Trace)
              Component class is 0xe, Generic IP component
      ROMTABLE[0x8] = 0xfff03003
              Component base address 0xe0002000
              Peripheral ID 0x04002bb003
              Designer is 0x4bb, ARM Ltd
              Part is 0x3, Cortex-M3 FPB (Flash Patch and Breakpoint)
              Component class is 0xe, Generic IP component
      ROMTABLE[0xc] = 0xfff01003
              Component base address 0xe0000000
              Peripheral ID 0x04003bb001
              Designer is 0x4bb, ARM Ltd
              Part is 0x1, Cortex-M3 ITM (Instrumentation Trace Module)
              Component class is 0xe, Generic IP component
      ROMTABLE[0x10] = 0xfff41003
              Component base address 0xe0040000
              Invalid CID 0x00000000
      ROMTABLE[0x14] = 0xfff42003
              Component base address 0xe0041000
              Peripheral ID 0x04000bb925
              Designer is 0x4bb, ARM Ltd
              Part is 0x925, Cortex-M4 ETM (Embedded Trace)
              Component class is 0x9, CoreSight component
              Type is 0x13, Trace Source, Processor
      ROMTABLE[0x18] = 0x0
              End of ROM table

Info : Listening on port 6666 for tcl connections

With some aditional probing, we can gather that the MCU is a Cortex-M4 (CPUID 0x410fc241) with IDCODE 0x8fb84511. What’s more, the chip appears to be in RDP1 mode. Flash reads fail, but we have control over the CPU and SRAM.

The standard operating procedure in this case is to execute shellcode that copies flash into SRAM so that we can read it out. Shellcode:

.syntax unified
.cpu cortex-m4
.thumb
.global _start
_start:
loop:
    ldr  r3, [r0], #4      @ load word from flash (CPU can read it)
    str  r3, [r1], #4      @ store to SRAM buffer, post-inc
    subs r2, r2, #1        @ decrement word count
    bne  loop
    bkpt #0                @ halt so the debugger regains control

Then a slightly more complicated OpenOCD config:

interface ftdi
transport select swd
ftdi_vid_pid 0x0403 0x6010
ftdi_channel 1
adapter_khz 200
ftdi_layout_init 0x0028 0x002b
ftdi_layout_signal SWD_EN -data 0
ftdi_layout_signal nSRST -data 0x0020
swd newdap chip cpu -expected-id 0x0
dap create chip.dap -chain-position chip.cpu
target create chip.cpu cortex_m -dap chip.dap

init

proc write_stub {} {
    mww 0x20000000 0x3b04f850
    mww 0x20000004 0x3b04f841
    mww 0x20000008 0xd1f93a01
    mww 0x2000000c 0x0000be00
}

# returns 1 on success, 0 if the copy faulted (e.g. read past flash end)
proc run_copy {src buf words} {
    halt
    reg r0 $src
    reg r1 $buf
    reg r2 $words
    reg primask 1
    reg pc 0x20000001
    resume
    if {[catch {wait_halt 5000}]} { return 0 }
    return 1
}

set BUF 0x20002000
halt
write_stub

set BASE 0x08000000
set TOTAL 0x100000
set CHUNK 0x8000
set WORDS [expr {$CHUNK/4}]
set idx 0
for {set off 0} {$off < $TOTAL} {incr off $CHUNK} {
    if {[run_copy [expr {$BASE+$off}] $BUF $WORDS] == 0} {
        echo [format "FAULT at flash 0x%08x (likely flash end) - stopping" [expr {$BASE+$off}]]
        break
    }
    dump_image [format "%s/fwchunk_%02d.bin" $DIR $idx] $BUF $CHUNK
    echo [format "chunk %d: flash 0x%08x OK" $idx [expr {$BASE+$off}]]
    incr idx
}
echo "DUMP DONE"
shutdown

This is fairly quick and works like a charm. To confirm we got plaintext:

$ strings -n 10  fw_dump.bin
Power by Francis for LYlens
20241129140900
`8F@lX`8F@l
`>H@lWhx@X`;H@l
 h0` hp` h
LYlens Ltd.
LYlens Ltd.
AF 28/4.5 FE
20241114135155
Power by Francis for LYlens
20241114135155
LYLENS                          AF 28/4.5 FE
50A1101AM5J1
20250313140910
20241129140900
20241114135155
AF 28/4.5 FE
20251218153756
 TaskCommSony
%+FTM)hRM*hSMSM
$H@h$IH`"H@i
 TaskCalibrat
 Max = F%2.1f
Min = F%2.1f
Aperture %s %s
ResetFront
FullRunStart
FullRunStop
Max err %d %f %f
SetFront err
SetFront ack
SetFront err %d %f %f
SetBack ack
SetBack err
SetBack err %d %f %f
 ResetFront ack
 DeviceId %s
 %sFnumCalOn
DateTime ack
MfRingVal=%d
MtftRingVal=%d
AfPosSenseAdcRange=%d,%d
ApPosSenseAdcRange=%d,%d
FullRunStart ack
FullRunStop ack
ToNear ack
ToMiddle ack
FrontMargin=%f
FrontMarginNear=%f
FrontMarginInf=%f
FrontMargin=%d
BackMargin=%f
BackMarginNear=%f
BackMarginInf=%f
BackMargin=%d
AfMotorCal=%d
 FrontMotorCal=%d
BackMotorCal=%d
FrontMotorPos=%d
BackMotorPos=%d
AfNowPos=%d
ApNowPos=%d
PosRange=%d %d %d %d
SetAfPosErr
SetAfPosAck
SetApPosErr
SetApPosAck
EncryptState=%d
Err Motor Reset
Err Motor Reset AP
Err Motor Reset AF
Err Motor Running
Err Motor Running AP
Err Motor Running AF
Err Lens Info
Err Motor margin check
Err Motor margin check front
Err Motor margin check back

LYLens, Francis, questions! But most importantly, there seem to be some useful debug strings in there. Taking a peek at this firmware will be instructional.

For completeness - firmware update decryption

Plaintext firmware obtained via SWD didn’t have any obvious AES implementation magic numbers; it turned out the MCU has a built in AES engine that the firmware uses. After a bit of reversing, function FUN_08000de8 was found to be the entrypoint for firmware update process. It reads the firmware update, runs a CRC check on it and proceeds to AES-CBC decrypt it.

It implements a somewhat complicated key derivation scheme. The lens’ MCU containes two separate secret values: a magic const (16 bytes) and what appears to be a chip UID (16 bytes at 0x1FFFF7C0: 29110136 57345004 30313636 18500630), but evidently is the same on all the chips. Actual encryption key is derived like so:

KEY = AES128CBC_decrypt(data=CONST, key=UID, iv=IV_derive)

Where IV_derive is CRC of UID repeated 4 times. Once the key is derived, the firmware is decrypted like so:

AES128CBC_decrypt(fw_update[0x80:0x80+bodylen], KEY, iv=hdr[0x40:0x50])

The actual magic value is left out as an exercise for the reader as it can only be used to decrypt firmware updates for this one lens. Simple python script to do the decryption is:

#!/usr/bin/env python3
"""Decrypt Viltrox/LYlens firmware containers offline.

  cipher      : AES-128-CBC (hardware engine @0x40024000 on the GD32/APM32 clone)
  body key    : a fixed 16-byte constant in the bootloader, itself AES-CBC-
                decrypted with the vendor-provisioned UID as key:
                    KEY = AES128CBC_dec(data=CONST, key=UID, iv=CRC(UID)x4)
                The UID (@0x1FFFF7C0) is fixed for the whole product line
                (bootloader enforces CRC(UID)==0x82460ff9), so KEY is constant
                body iv     : the per-file IV in the container header at offset 0x40
  body        : container[0x80:], 16-byte aligned; AES128CBC_dec(body, KEY, iv)
"""
import struct, sys, glob, os
from Cryptodome.Cipher import AES

CONST = struct.pack('<4I', 0xeaeaeaea,0xeaeaeaea,0xeaeaeaea,0xeaeaeaea,) # plug in your dumped key
UID   = struct.pack('<4I', 0x29110136, 0x57345004, 0x30313636, 0x18500630) # different lens probably has different UID

def stm32_crc_words(words):
    crc = 0xFFFFFFFF
    for w in words:
        crc ^= w
        for _ in range(32):
            crc = ((crc << 1) ^ 0x04C11DB7) & 0xFFFFFFFF if crc & 0x80000000 else (crc << 1) & 0xFFFFFFFF
    return crc

def derive_key():
    crc = stm32_crc_words(struct.unpack('<4I', UID))
    iv = struct.pack('<4I', crc, crc, crc, crc)
    return AES.new(UID, AES.MODE_CBC, iv).decrypt(CONST)

KEY = derive_key()

def decrypt(path, outdir):
    d = open(path, "rb").read()
    ver   = d[0x50:0x60].split(b'\0')[0].decode('latin1')
    iv    = d[0x40:0x50]
    blen  = struct.unpack_from('<I', d, 0x70)[0]
    body  = d[0x80:0x80+blen]
    body  = body[:len(body)//16*16]
    pt    = AES.new(KEY, AES.MODE_CBC, iv).decrypt(body)
    sp, rv = struct.unpack_from('<II', pt, 0)
    ok = 0x20000000 <= sp <= 0x20040000 and 0x08000000 <= rv <= 0x08080000 and (rv & 1)
    out = os.path.join(outdir, os.path.basename(path).replace('.dat', '.bin'))
    open(out, "wb").write(pt)
    print(f"{os.path.basename(path):20s} {ver:7s} SP={sp:#010x} reset={rv:#010x} "
          f"base~{rv & 0xFFFFF000:#010x} {'OK' if ok else 'CHECK'} -> {os.path.basename(out)}")
    return ok

if __name__ == "__main__":
    print("derived AES-128 key:", KEY.hex())
    outdir = sys.argv[1] if len(sys.argv) > 1 else "decrypted"
    os.makedirs(outdir, exist_ok=True)
    files = sorted(glob.glob("fw/UPD2845FEV*.dat"))
    n_ok = sum(decrypt(f, outdir) for f in files)
    print(f"\n{n_ok}/{len(files)} decrypted to valid Cortex-M images in {outdir}/")

The second lens - 25mm F1.7 E

Since that was pretty fun, I thought there might be a chance they’ve somehow made the encryption keys not completely random and that if I saw the constants from another lens, I could maybe figure out a general rule and be able to decrypt all of them. Long story short, that was not the case, but dumping the firmware from this second lens turned out to be even more interesting.

I bought the 25mm lens because it was genuinely interesting. Very fast, very wide. Even before I got it, I knew chances were slim that the same scheme would work. The firmware update process is completely different. Instead of custom HID protocol, the lens shows up as a storage device, and the firmware is simply copied onto it.

The firmware format was somewhat different, too:

00000000: 5241 494b 4147 4527 5320 5052 4f44 5543  RAIKAGE'S PRODUC
00000010: 5420 4649 524d 5741 5245 0000 0000 0000  T FIRMWARE......
00000020: 3230 3235 3031 3037 3039 3537 3233 0000  20250107095723..
00000030: 6461 796f 2e74 6f6e 6740 676d 6169 6c2e  dayo.tong@gmail.
00000040: 636f 6d00 0000 0000 0000 0000 0000 0000  com.............
00000050: 7631 2e30 3000 0000 0000 0000 0000 0000  v1.00...........
00000060: 7631 2e30 3200 0000 0000 0000 0000 0000  v1.02...........
00000070: 5c84 f0f0 00a2 0e9c d1d7 e0a6 f42e 2b0f  \.............+.
00000080: 7e07 292b 9a01 0000 0078 3501 0000 0000  ~.)+.....x5.....
00000090: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000100: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000110: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000120: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000130: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000140: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000150: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000160: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000170: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000180: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000190: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000200: a7f9 d9dc d6de ff29 3fd1 d3a1 aed1 f79f  .......)?.......
00000210: 3ba5 0fd2 396c fa92 979d 084b 00ef 9a8f  ;...9l.....K....
00000220: e0a5 7d3b 27e9 77e0 43e2 1d01 5aab ad25  ..};'.w.C...Z..%
00000230: f68c 8d6d 029b 96b5 d4bb 6b96 739f f089  ...m......k.s...
00000240: ac77 7365 6b42 73fa da36 5fd6 826e 9e5c  .wsekBs..6_..n.\
00000250: fabf b06f 11fa 6a67 6876 1bce e52d 48fa  ...o..jghv...-H.
00000260: 4bc0 f33a 823c 30b8 cf73 d6e8 1cd8 3343  K..:.<0..s....3C
00000270: bce9 48c6 2955 3a83 0404 d658 0332 25c0  ..H.)U:....X.2%.
00000280: f152 ca26 0ab3 ca4e f1a7 b438 d522 8653  .R.&...N...8.".S
00000290: a5cd 6d9d f55d 59de b061 69d9 2fe4 043e  ..m..]Y..ai./..>
000002a0: 0be3 26e3 c8a6 fdd4 5836 b11c 4f81 2699  ..&.....X6..O.&.
000002b0: c600 eb91 b932 13c2 6fd6 ee6e 5551 8375  .....2..o..nUQ.u
000002c0: 8ff3 df58 34a4 5dfd 810d 37af 6c81 6c87  ...X4.]...7.l.l.
000002d0: 5c6c e05c f919 54da 5d69 93a4 7254 1322  \l.\..T.]i..rT."
000002e0: e1b1 30cd ec4d 23e6 e7bc 3e87 426a 98d9  ..0..M#...>.Bj..
000002f0: 4d75 bf73 5112 136e 1573 5e3e db36 6a7a  Mu.sQ..n.s^>.6jz
00000300: 5e3c 57c0 3bed 400e 4762 666a 4ae4 9dd6  ^<W.;.@.GbfjJ...
00000310: 913a 6dd1 c88e 3d6a 550a 99ef cb08 9f92  .:m...=jU.......
00000320: d6d4 da8c 5f3e dee5 f1b6 7198 d008 6a7c  ...._>....q...j|
00000330: 4c56 d6e5 3c25 b56c cefe 6618 c50d 1c0e  LV..<%.l..f.....
00000340: 59e5 714e d28a ad3a 9c61 0655 c8d3 2d45  Y.qN...:.a.U..-E
00000350: 5e65 daac bb44 0377 2114 70e9 d507 6005  ^e...D.w!.p...`.
00000360: dd4d b6ac 7932 e9b9 b9c4 1d88 477d a7f3  .M..y2......G}..
00000370: 6d9b 378c 0629 a7ef 2f1f 35a7 ce42 f05d  m.7..)../.5..B.]
00000380: b9c6 b2dd 2697 3518 9224 4d61 58ef 7d99  ....&.5..$MaX.}.
00000390: f6fc cea8 7cdb b2df 1bc4 bd08 54b4 f6a3  ....|.......T...
000003a0: 7566 9aa5 ce54 8296 9302 f9ad 5431 9111  uf...T......T1..
000003b0: 6b39 5cdd 9e57 1ae3 fc97 58a0 55d6 8576  k9\..W....X.U..v
000003c0: 5884 e8d1 fe7c b3db a05e c5a4 90f6 f8b1  X....|...^......
000003d0: a1b6 58a5 2baf 9548 077f 9c81 97ed 8305  ..X.+..H........
000003e0: 776f d4db 0194 04a9 cbd5 15b8 8671 3a34  wo...........q:4
000003f0: d290 b2f6 3200 eda3 289c 95c3 9252 0535  ....2...(....R.5
00000400: 0abd 6c1f 016d 089f 4784 5175 b67d 3d71  ..l..m..G.Qu.}=q
00000410: 45fc 14e1 037f 5fec 44c2 8362 2733 92dd  E....._.D..b'3..
00000420: aed4 7c9d abe3 15c4 ff28 ffd2 abd5 9008  ..|......(......
00000430: 71e2 d63d a49a 42c4 6f91 84d8 9aaf 3ff6  q..=..B.o.....?.
00000440: d843 9432 2b07 85d7 a9a9 3c94 6487 b3b2  .C.2+.....<.d...

This is either a much older generation of a product or simply a different ODM selling under Viltrox brand. Wonder who RAIKAGE is. How did they get the e-mount specs?

Anyway, time for the screwdriver:

25mm lens board

Very similar layout to 28mm lens, even the same looking debug header. The main microcontroller is unbranded, though. Hooked it up to Tigard and fired up OpenOCD as before:

openocd -f tigard-id.cfg -c "init" -c "reset halt" \
  -c "echo ==IDCODE==" -c "mdw 0xE0042000" \
  -c "echo ==CPUID=="  -c "mdw 0xE000ED00" \
  -c "echo ==FLASHSIZE_dbg(blocked?)==" -c "mdw 0x1FFFF7E0" \
  -c "echo ==FLASH_CR==" -c "mdw 0x40022010" \
  -c "shutdown"

...
...

...
Info : SWD DPIDR 0x2ba01477
target halted ... pc: 0x080053f0 msp: 0x20005000
==IDCODE==
0xe0042000: 20036410
==CPUID==

This appears to be a STM32F103-class device, different then the previous one. My guess is that it’s another STM32 clone. This time around, however, we seem to be in a stronger readout protection mode. Even the CPU can’t read flash while debugger is attached:

==MAINFLASH==
0x08000000: bf004770 e000ed00 bf004770 e000ed00 
==ALIAS0==
0x00000000: bf004770 e000ed00 
==BOOTROM==
0x1ffff000: bf004770 e000ed00 

Above shows that trying reads of various common addresses returns the same pattern indicating protection is on. Reading and writing SRAM works just as before, but if we try the shellcode to read the flash, it fails. We get zeros. We’ll need a different trick.

Interrupt Jigsaw

There were a few tricks to try but I remembered a neat exploit documented in Travis Goodspeed’s Microcontroller Exploits book. Described in Travis’ book as “Interrupt jigsaw” vulnerability, it was discovered and published by Marc Schink & Johannes Obermaier and is documented as exceptional failure.

In essence, since running code can’t read flash when the debugger is attached, we can’t put our shellocde stub into SRAM and expect it to work. But, instruction fetches aren’t data reads and what we can do is change the location of the interrup table. If we then trigger an interrupt and single step, the address of the apparent interrupt handler will be loaded into PC. Since we control the location of the interrupt table, we can use this to leak a few bytes at a time. To make the exploit practical, you need to trigger all the interrupts to leak the most bytes. Some are inevitably not triggerable, or non existant, which leaves gaps, but through some additional cleverness, Obermaier&Schink’s exploit manages to leak out up to 95% of flash. We can work with that!

With OpenOCD set up and running, their original exploit actually worked like a charm:

# python3 stm32f1-firmware-extractor/main.py  --value 0x41 --binary  --host localhost --port 6666 0x8000000 0x20000 > 25mm_f17_jigsaw.bin

08000008: 08004035
0800000c: 08004039
08000010: 0800403d
08000014: 08004041
08000018: 08004045
0800002c: 08004049
08000030: 0800404d
08000038: 08004051
0800003c: 08004055
08000040: 080053ed
...
...

The full exploit run takes about 40 minutes but once it’s done we can see the following:

00000000: 4100 0000 4100 0000 3540 0008 3940 0008  A...A...5@..9@..
00000010: 3d40 0008 4140 0008 4540 0008 4100 0000  =@..A@..E@..A...
00000020: 4100 0000 4100 0000 4100 0000 4940 0008  A...A...A...I@..
00000030: 4d40 0008 4100 0000 5140 0008 5540 0008  M@..A...Q@..U@..
00000040: ed53 0008 ed53 0008 ed53 0008 ed53 0008  .S...S...S...S..
00000050: ed53 0008 ed53 0008 ed53 0008 ed53 0008  .S...S...S...S..
00000060: ed53 0008 ed53 0008 ed53 0008 ed53 0008  .S...S...S...S..
00000070: ed53 0008 ed53 0008 ed53 0008 ed53 0008  .S...S...S...S..
00000080: ed53 0008 ed53 0008 ed53 0008 ed53 0008  .S...S...S...S..
00000090: 5940 0008 ed53 0008 ed53 0008 ed53 0008  Y@...S...S...S..
000000a0: ed53 0008 ed53 0008 ed53 0008 ed53 0008  .S...S...S...S..
000000b0: ed53 0008 ed53 0008 ed53 0008 ed53 0008  .S...S...S...S..
000000c0: ed53 0008 ed53 0008 ed53 0008 ed53 0008  .S...S...S...S..
000000d0: ed53 0008 ed53 0008 ed53 0008 ed53 0008  .S...S...S...S..
000000e0: ed53 0008 ed53 0008 ed53 0008 0000 0000  .S...S...S......
000000f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000100: 0000 0000 0000 0000 5ff8 08f1 0000 0000  ........_.......
00000110: 0203 0405 0607 0809 0a0b 0c0d 0e0f 1010  ................
00000120: 0006 0000 0000 0000 0000 0000 0000 0000  ................
00000130: 0000 0003 4100 0000 0000 0000 0000 0000  ....A...........
00000140: 0102 0304 0607 0809 0000 0000 0102 0304  ................
00000150: 0002 0102 0080 0202 1f00 0000 5241 494b  ............RAIK
00000160: 4147 4520 5072 6f64 7563 7420 2020 2020  AGE Product
00000170: 2020 2020 302e 3031 637c 777b f26b 6fc5      0.01c|w{.ko.
00000180: 3001 672b fed7 ab76 ca82 c97d fa59 47f0  0.g+...v...}.YG.
00000190: add4 a2af 9ca4 72c0 b7fd 9326 363f f7cc  ......r....&6?..
000001a0: 34a5 e5f1 71d8 3115 04c7 23c3 1896 059a  4...q.1...#.....
000001b0: 0712 80e2 eb27 b275 0983 2c1a 1b6e 5aa0  .....'.u..,..nZ.
000001c0: 523b d6b3 29e3 2f84 53d1 00ed 20fc b15b  R;..)./.S... ..[
000001d0: 6acb be39 4a4c 58cf d0ef aafb 434d 3385  j..9JLX.....CM3.
000001e0: 45f9 027f 503c 9fa8 51a3 408f 929d 38f5  E...P<..Q.@...8.
000001f0: bcb6 da21 10ff f3d2 cd0c 13ec 5f97 4417  ...!........_.D.
00000200: 4100 0000 4100 0000 6081 4fdc 222a 9088  A...A...`.O."*..
00000210: 46ee b814 de5e 0bdb e032 3a0a 4100 0000  F....^...2:.A...
00000220: 4100 0000 4100 0000 4100 0000 8dd5 4ea9  A...A...A.....N.
00000230: 6c56 f4ea 4100 0000 ba78 252e 1ca6 b4c6  lV..A....x%.....
00000240: e8dd 741f 4bbd 8b8a 703e b566 4803 f60e  ..t.K...p>.fH...
00000250: 6135 57b9 86c1 1d9e e1f8 9811 69d9 8e94  a5W.........i...
00000260: 9b1e 87e9 ce55 28df 8ca1 890d bfe6 4268  .....U(.......Bh
00000270: 4199 2d0f b054 bb16 8d01 0204 0810 2040  A.-..T........ @
00000280: 801b 3600 5209 6ad5 3036 a538 bf40 a39e  ..6.R.j.06.8.@..
00000290: 81f3 d7fb 7ce3 3982 9b2f ff87 348e 4344  ....|.9../..4.CD
000002a0: c4de e9cb 547b 9432 a6c2 233d ee4c 950b  ....T{.2..#=.L..
000002b0: 42fa c34e 082e a166 28d9 24b2 765b a249  B..N...f(.$.v[.I
000002c0: 6d8b d125 72f8 f664 8668 9816 d4a4 5ccc  m..%r..d.h....\.
000002d0: 5d65 b692 6c70 4850 fded b9da 5e15 4657  ]e..lpHP....^.FW
000002e0: a78d 9d84 90d8 ab00 8cbc d30a f7e4 5805  ..............X.
000002f0: b8b3 4506 d02c 1e8f ca3f 0f02 c1af bd03  ..E..,...?......
00000300: 0113 8a6b 3a91 1141 4f67 dcea 97f2 cfce  ...k:..AOg......
00000310: f0b4 e673 96ac 7422 e7ad 3585 e2f9 37e8  ...s..t"..5...7.
00000320: 1c75 df6e 47f1 1a71 1d29 c589 6fb7 620e  .u.nG..q.)..o.b.
00000330: aa18 be1b 4100 0000 c6d2 7920 9adb c0fe  ....A.....y ....
00000340: 78cd 5af4 1fdd a833 8807 c731 b112 1059  x.Z....3...1...Y
00000350: 2780 ec5f 6051 7fa9 19b5 4a0d 2de5 7a9f  '.._`Q....J.-.z.
00000360: 93c9 9cef a0e0 3b4d ae2a f5b0 c8eb bb3c  ......;M.*.....<
00000370: 8353 9961 172b 047e ba77 d626 e169 1463  .S.a.+.~.w.&.i.c
00000380: 5521 0c7d 8000 c1ff 0e00 c1ff 0008 0000  U!.}............
00000390: 0010 0000 0000 0000 0000 0000 0000 0000  ................
000003a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000003b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000003c0: 0000 0000 55aa 0000 0000 0000 0000 0000  ....U...........
000003d0: 0000 0000 eb3c 9052 4149 4b41 4745 0000  .....<.RAIKAGE..
000003e0: 0201 0600 0200 0200 10f8 1000 0100 0100  ................
000003f0: 0008 0000 0000 0000 8000 295c 9d3e 2c44  ..........)\.>,D
00000400: 4100 0000 4100 0000 2020 4641 5431 3620  A...A...  FAT16
00000410: 2020 0000 55aa 0000 f8ff ffff 4100 0000    ..U.......A...
00000420: 4100 0000 4100 0000 4100 0000 0000 0000  A...A...A.......
00000430: 0000 0000 4100 0000 1117 0000 0000 0000  ....A...........
00000440: 4274 0069 006f 006e 002e 000f 00e1 7400  Bt.i.o.n......t.
00000450: 7800 7400 0000 ffff ffff 0000 ffff ffff  x.t.............
00000460: 0144 0065 0076 0069 0063 000f 00e1 6500  .D.e.v.i.c....e.
00000470: 4900 6e00 6600 6f00 7200 0000 6d00 6100  I.n.f.o.r...m.a.
00000480: 4445 5649 4345 7e31 5458 5405 0000 0000  DEVICE~1TXT.....
00000490: 1117 1117 0000 0000 1117 0300 bc00 0000  ................
000004a0: 4261 0074 0000 00ff ffff ff0f 00ab ffff  Ba.t............
000004b0: ffff ffff ffff ffff ffff 0000 ffff ffff  ................
000004c0: 0155 0073 0065 0072 0050 000f 00ab 7200  .U.s.e.r.P....r.
000004d0: 6f00 6600 6900 6c00 6500 0000 2e00 6400  o.f.i.l.e.....d.
000004e0: 5553 4552 5052 7e31 4441 5422 0000 0000  USERPR~1DAT"....
000004f0: 1117 1117 0000 0000 1117 0400 0002 0000  ................
00000500: 4465 7669 6365 496e 666f 726d 6174 696f  DeviceInformatio
00000510: 6e20 3d20 0d0a 0000 4861 7264 7761 7265  n = ....Hardware
00000520: 5665 7273 696f 6e20 3d20 0000 426f 6f74  Version = ..Boot
00000530: 6c6f 6164 4100 0000 7273 696f 6e20 3d20  loadA...rsion =
00000540: 536f 6674 7761 7265 5665 7273 696f 6e20  SoftwareVersion
00000550: 3d20 0000 536f 6674 7761 7265 5265 6c65  = ..SoftwareRele
00000560: 6173 6544 6174 6520 3d20 0000 a325 253d  aseDate = ...%%=
00000570: 23a3 3a75 5354 4d69 6372 6f65 6c65 6374  #.:uSTMicroelect
00000580: 726f 6e69 6373 0000 5354 4d33 3220 4d61  ronics..STM32 Ma
00000590: 7373 2053 746f 7261 6765 0000 3030 3030  ss Storage..0000
000005a0: 3030 3030 3030 3141 0000 0000 4d53 4320  0000001A....MSC
000005b0: 436f 6e66 6967 0000 4d53 4320 496e 7465  Config..MSC Inte
000005c0: 7266 6163 6500 0000 5241 494b 4147 4527  rface...RAIKAGE'
000005d0: 5320 5052 4f44 5543 5420 4649 524d 5741  S PRODUCT FIRMWA
000005e0: 5245 0000 0348 044b 8342 02d0 034b 03b1  RE...H.K.B...K..

Cheers! Obvious gaps (I should have used 0x41414141 instead of 0x41 so the gaps would be more noticable) but this is good enough. With some luck, we’ll have enough to recover the encryption scheme.

The encryption scheme

With some convincing, Ghidra was able to properly process the binary firmware. Once again we are dealing with AES-128-CBC, but this time implemented in software. There doesn’t appear to be a complex key derivation mechanism this time; it just uses a preprogrammed secret key. The IV for CBC is specified in the firmware update file header.

"""Decrypt Viltrox "RAIKAGE" ODM lens firmware 
  cipher  : AES-128-CBC, software AES (S-box @0x08000178 in the firmware)
  key     : FIXED 16-byte constant hardcoded in flash @0x0800578c:
  IV      : per-file, 16 bytes at container header offset 0x75.
  layout  : container header 0x00..0x1FF (plaintext: "RAIKAGE'S PRODUCT FIRMWARE"
            @0x00, build date @0x20, author email @0x30, min-ver @0x50, ver @0x60,
            per-build field @0x70; 0x90..0x1EF zero padding). Encrypted app image
            starts at 0x200, runs to the last non-zero byte, 16-byte aligned.
            Decrypted image is the APP, loaded at flash 0x08006000 (the bootloader
            0x08000000..0x08006000 is not in the .dat).
"""
import sys, struct
from Cryptodome.Cipher import AES

KEY = bytes.fromhex("") # add your own recovered key 

def decrypt(path, out=None):
    d = open(path, "rb").read()
    vendor = d[0x00:0x20].split(b"\0")[0].decode("latin1", "replace")
    ver    = d[0x60:0x70].split(b"\0")[0].decode("latin1", "replace")
    end    = len(d.rstrip(b"\0"))
    IV     = d[0x75:0x85]           # per-file 16-byte IV at header offset 0x75
    body   = d[0x200:end]
    body   = body[: len(body) // 16 * 16]
    pt = AES.new(KEY, AES.MODE_CBC, IV).decrypt(body)
    sp, rv = struct.unpack_from("<II", pt, 0)  # app vector table (SP, reset)
    print(f"{path}")
    print(f"  vendor='{vendor}'  version='{ver}'  enc-body={len(body)}B")
    if out is None:
        out = path.rsplit("/", 1)[-1].replace(".dat", "_dec.bin")
    open(out, "wb").write(pt)
    print(f"  saved {out}")
    return pt

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("usage: decrypt_raikage.py <UPD*.dat> [out.bin]"); sys.exit(1)
    decrypt(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None)

With this, we get a byte-perfect copy of the lens firmware sans bootloader. As before, the secret key appears to be per product so we can’t directly decrypt ALL of firmwarae of this type…

25mm lens E-mount implementation

A cursory look at this firmware in Ghidra reveals that it’s a completely different implementation from the one used on 28mm lens, further suggesting different ODMs or developers. Interesting in itself. Sadly, this firmware, while more featureful, is less verbose than the 28mm lens one, but I was able to gather some other useful info.

I’ll be documenting the information about e-mount protocol in the next writeup.