1 of 74

Riccardo Mori <rmori@quarkslab.com>�Robin David <rdavid@quarkslab.com>

Alexandre Chazal <achazal@quarkslab.com>

Abusing EV Chargers

through Bluetooth and USB

Recon 2025

1

2 of 74

Bio

2

2

RICCARDO

PICTURE

Security Research at Quarkslab

also, trainer and R&D Manager.

Security Research at Quarkslab

also, CTF & pwn2own participant.

Security Research at Quarkslab

working on automotive and embedded devices.

Riccardo Mori

Alex Chazal

Robin David

3 of 74

Electric Vehicles

3

3

29%

Region

Sales (Millions)

Year-over-Year Growth

China

3.3

+35%

Europe

1.2

+25%

North America

0.6

+5%

Rest of World

0.5

+37%

Global

5.6

+29%

Growth of electric vehicle sold from Jan-April 2025 (over 2024 same period)

*

https://rhomotion.com/news/global-ev-sales-up-29-in-first-third-2025/

4 of 74

EVSE: EV Supply Equipment

4

4

Level

Voltage

Use Case

Charging Speed

Level 1

120V

Home

Level 2

208/240V

Home/Public

+

Level 3

400V+ DC

Commercial

& Highways

+++

Charging power

Connector Type

Today’s focus

aka EV Charger

5 of 74

EVSE’s Today

5

5

Recent now connected to internet

  • LAN: Ethernet, WiFi
  • WAN: LTE, 4G etc

    • smart charging (push messages app iOS/Android)
    • billing features
    • analytics, FOTA
    • smart grids (V2G)

Bridge between your home and your car.

6 of 74

Pwn2Own Automotive 2025

6

6

⇒ Focus on the ones part of pwn2own automotive

7 of 74

Pwn2Own Automotive 2025

7

7

⇒ Focus on the ones part of pwn2own automotive

⇒ Bug bounty competition for automotive equipments

  • Exploitation: 3 attempts of 10 minutes
  • Need proof of exploitation

The target we selected!

⇒ Already present in pwn2own 2024 (5 CVE reported)

8 of 74

Autel MaxiCharger AC Wallbox

8

8

Specs:

  • Both residential & commercial
  • Connector Type: IEC 62196-2
  • Autel Cloud (Smart Charging)
  • Up to 22KW of output power
  • OCPP 1.6j (Open Charge Point Protocol)

USB

Ethernet

NFC

WiFi

Bluetooth

RS-485

9 of 74

Firmware RE

9

10 of 74

Firmware Acquisition

10

10

Download

(online from manufacturer)

Autel does not provide any download link

Dump flash

(directly from memory

Winbond flash)

Firmware OTA (FOTA) (Intercepting update)

16Mb of (ciphered) bytes,

no Binwalk match

(half)

Through Android app

(FOTA over bluetooth)

11 of 74

Intercepting Firmware

11

11

Autel Cloud

Android App

  • Limited control over the device
  • No reproducibility
  • Not guaranteed to work (certificate pinning)

Did not manage to trigger OTA update query

Need to reverse the application!

12 of 74

Intercepting Firmware: Attacking the app

12

12

⇒ Packed with SecNeo

⇒ Frida hooking is still possible.

Methodology:

    • Dump app memory (using Frida)
    • Search for dex headers
    • Collect DEXes and reconstruct the original app (7 remaining files)

$ ls -lah

-rw-r--r-- 1 x x 16M Nov 1 2024 classes09.dex

-rw-r--r-- 1 x x 13M Nov 1 2024 classes11.dex

-rw-r--r-- 1 x x 13M Nov 1 2024 classes13.dex

-rw-r--r-- 1 x x 12M Nov 1 2024 classes15.dex

-rw-r--r-- 1 x x 9.3M Nov 1 2024 classes17.dex

-rw-r--r-- 1 x x 14M Nov 1 2024 classes19.dex

-rw-r--r-- 1 x x 2.1M Nov 1 2024 classes23.dex

13 of 74

Interception with Frida: Recovering OTA update

13

13

#1 Finding endpoints

TOKEN = "UNb2g%2BMonYmgKnIeCgm3e1TrLLDbvqvT..."�URL = "https://gateway-eneprodapac.autel.com"

ENDP ="/api/data-service/device/pile/version/upgrade/ota"

s = requests.Session()

s.headers["X-Model"] = "Pixel 8 Pro"

# [snip]

r = s.get(URL + ENDP, params={"sn": "AE0022..."})

#3 URL retrieval

(depends geographic regions (US/EMEA/Asia)

#2 Function hooking

Hook z6.b.k and dump parameters. Repeat for sending and receiving functions.

$ wget -O Firmware_ECC01_V1.51.00.aut https://s3.eu-central-1.amazonaws.com/...

#4 Fetching Firmware

Firmware_ECC01_V1.51.00.aut

14 of 74

14

14

We have the Firmware!

But it’s encrypted!

15 of 74

Entropy Checks

15

15

Encrypted firmware analysis

Entropy

Byte Distribution

Pass: 2 Fail: 26

[] Frequency (Monobit) Test

[] Test For Frequency Within A Block

[] Runs Test

[] Random Binary Matrix Rank Test, 32 x 32

[] Random Binary Matrix Rank Test, 320 x 320

[] Discrete Fourier Transform (Spectral) Test

[] Linear Complexity Test

...

[] Random Excursions Variant Test (L = 10016)

[] RandomWalk1 C (L = 10016)

TestU01

(to assess PRNG)

16 of 74

Entropy Checks

16

16

17 of 74

Inferring Key Size: Kasiski/Babbage approach

17

17

Principle:

⇒ Plaintext ciphered with the same key portion generates the same ciphertext

⇒ Thus theirs distance is a multiple of key length

⇒ The GCD of distances should be multiple of key length

Guess #1: Simple operation (+, -, x, ^) with a repeating key

Charles Babbage

(1791-1871)

I solved it in 1854

Friedrich Kasiski

(1805-1881)

Me too in 1863 !

18 of 74

Inferring Key Size: Example

18

18

9338 ad70 4e8d b17b 8b30 c5a9 5084 7e69

ad90 b07b 0880 c0bc 5442 e022 524d ad87

5239 9531 978e 7972 a731 b877 6d6c bb94

9b8d 9078 98ac b482 d87c 72bb aa3f 7465

75c3 6577 757b bc88 8784 9568 999d 7088

177e 7703 935f b07b 0880 c0bc e573 bfb6

2e82 1f72 a926 b479 1078 b83c 917e f572

5f3f b47f 3888 7b55 9b3b 1e65 a453 b485

a425 b57f 9105 d49b c18f ba36 7e6a 370a

ba94 72c6 c668 7c36 3667 b32a 9b66 7776

dd23 7492 9092 8d85 a1f5 b57f 9105 d476

9a32 7457 eb74 c377 4c77 7265 7df8 b576

9006 aa74 4536 2d3f 5ba6 4e5c 7566 53bf

6133 0a45 4e5c 7566 8b6b d03c 9f46 b487

7294 c068 9181 4f90 a103 cb20 db24 879c

d285 aa89 4c25 bf7e a476 aa9d 5745 8e81

60

40

10

dist:

dist:

dist:

Gcd(60, 40, 10) ⇒ 10 (key length)

(greatest common divider)

19 of 74

19

19

10: [1001] ['9: 10', '8: 10', '8: 10', '8: 40', '8: 10', '8: 20', '8: 10', '9: 20', '9: 640', '10: 40', '11: 20', '8: 10', '9: 10', '8: 40', '8: 10', '7: 40',...]

11: [884] ['5: 11', '5: 11', '4: 11', '4: 22', '4: 11', '4: 352', '5: 11', '5: 11', '4: 11', '5: 11', '5: 11', '6: 1408', '4: 11', '4: 88', '4: 11', '3: 11',....]

12: [823] ['13: 12', '18: 12', '17: 48', '16: 12', '15: 24', '13: 48', '15: 384', '14: 12', '14: 96', '13: 96', '15: 24', '13: 12', '14: 48', '15: 12', '13: 24',..]

13: [612] ['3: 26', '3: 13', '3: 416', '4: 13', '4: 26', '4: 13', '3: 13', '3: 13', '4: 13', '3: 26', '3: 13', '3: 208', '3: 13', '3: 26', '3: 13', '3: 52', '3: 13',...]

14: [689] ['5: 28', '6: 56', '7: 28', '7: 14', '6: 224', '5: 28', '6: 14', '6: 14', '5: 14', '6: 112', '7: 14', '6: 28', '6: 14', '5: 14', '5: 14', '7: 28', '5: 14',..]

[snip]

29: [17] ['4: 29', '3: 116', '4: 58', '3: 29', '3: 29']

30: [126] ['3: 30', '4: 30', '4: 120', '3: 30', '3: 60', '3: 30', '3: 3840', '3: 30', '3: 60', '3: 30', '3: 120', '3: 30', '3: 60', '3: 30', '3: 240', '3: 30', '4: 60', '4: 30', '3: 120', '3: 30', '3: 60', '3: 30', '3: 480', '3: 60', '3: 30', '3: 240', '3: 30', '4: 120', '3: 30', '3: 60', '3: 30', '3: 30', '4: 30', '3: 30', '3: 30', '3: 30', '3: 30', '3: 30', '3: 120', '3: 30']

31: [6] ['3: 31', '3: 93']

32: [284] ['39: 64', '34: 32', '37: 128', '34: 32', '35: 32', '36: 256', '33: 32', '36: 64']

33: [6] ['3: 33', '3: 66']

[snip]

52: [108] ['3: 208', '3: 52', '3: 104', '3: 52', '3: 416', '3: 52', '3: 104', '3: 52', '3: 208', '3: 52', '3: 104', '3: 52', '3: 3328', '3: 52', '3: 104', '4: 52', '4: 208', '3: 52', '3: 104', '3: 52', '3: 416', '3: 52', '3: 104', '3: 52', '3: 208', '3: 52', '3: 104', '3: 52', '4: 832', '3: 52', '3: 104', '3: 52', '3: 104', '3: 52', '3: 52']

56: [154] ['6: 112', '5: 56', '5: 224', '5: 56', '5: 112', '5: 56', '5: 448', '5: 56', '5: 112', '5: 56', '5: 224', '5: 56', '5: 112', '5: 56', '5: 896', '6: 56', '6: 112', '6: 56', '5: 224', '5: 56', '4: 112', '4: 56', '4: 448', '4: 56', '5: 112', '4: 56', '4: 224', '4: 56', '4: 112', '5: 56', '4: 1792', '4: 56']

58: [3] ['3: 58']

60: [43] ['3: 120', '3: 60', '3: 3840', '3: 60', '3: 120', '3: 60', '3: 240', '4: 60', '3: 120', '3: 60', '3: 60', '3: 240', '3: 120', '3: 120']

64: [134] ['33: 128', '35: 64', '33: 256', '33: 64']

68: [9] ['3: 136', '3: 68', '3: 136']

72: [118] ['4: 288', '4: 72', '4: 144', '4: 72', '4: 576', '4: 72', '4: 144', '4: 72', '4: 288', '4: 72', '4: 144', '6: 72', '4: 2304', '4: 72', '4: 144', '4: 72', '4: 288', '4: 72', '4: 144', '3: 72', '3: 576', '3: 72', '3: 144', '3: 72', '3: 288', '3: 72', '3: 144', '3: 72', '3: 1152', '3: 72', '4: 144', '3: 72']

76: [6] ['3: 76', '3: 76']

80: [104] ['7: 160', '7: 80', '7: 1280', '7: 80', '7: 160', '7: 80', '7: 320', '7: 80', '6: 160', '6: 80', '6: 640', '6: 80', '6: 160', '6: 80', '6: 320', '6: 80']

88: [91] ['3: 88', '4: 704', '3: 88', '3: 176', '3: 88', '3: 352', '3: 88', '3: 176', '3: 88', '3: 1408', '3: 88', '3: 176', '3: 88', '3: 352', '3: 88', '3: 176', '3: 88', '3: 704', '3: 88', '3: 176', '3: 88', '3: 352', '3: 88', '3: 176', '3: 88', '3: 2816', '3: 88', '3: 176', '3: 88', '3: 352']

96: [87] ['12: 96', '11: 768', '11: 96', '11: 192', '11: 96', '11: 384', '10: 96', '10: 192']

104: [45] ['3: 208', '3: 104', '3: 416', '3: 104', '3: 208', '3: 104', '3: 3328', '3: 104', '3: 208', '3: 104', '3: 416', '3: 104', '3: 208', '3: 104', '3: 832']

112: [75] ['5: 112', '5: 224', '5: 112', '5: 448', '5: 112', '5: 224', '5: 112', '5: 896', '6: 112', '5: 224', '4: 112', '4: 448', '4: 112', '4: 224', '4: 112', '4: 1792']

120: [15] ['3: 120', '3: 3840', '3: 120', '3: 240', '3: 120']

128: [65] ['33: 128', '32: 256']

144: [57] ['4: 288', '4: 144', '4: 576', '4: 144', '4: 288', '4: 144', '4: 2304', '4: 144', '4: 288', '3: 144', '3: 576', '3: 144', '3: 288', '3: 144', '3: 1152', '3: 144']

160: [52] ['7: 160', '7: 1280', '7: 160', '7: 320', '6: 160', '6: 640', '6: 160', '6: 320']

176: [42] ['3: 704', '3: 176', '3: 352', '3: 176', '3: 1408', '3: 176', '3: 352', '3: 176', '3: 704', '3: 176', '3: 352', '3: 176', '3: 2816', '3: 176']

192: [42] ['11: 768', '11: 192', '10: 384', '10: 192']

208: [21] ['3: 208', '3: 416', '3: 208', '3: 3328', '3: 208', '3: 416', '3: 208']

224: [36] ['5: 224', '5: 448', '5: 224', '5: 896', '4: 224', '4: 448', '4: 224', '4: 1792']

240: [6] ['3: 3840', '3: 240']

256: [32] ['32: 256']

288: [28] ['4: 288', '4: 576', '4: 288', '4: 2304', '3: 288', '3: 576', '3: 288', '3: 1152']

Key length (block length)

Duplicate occurrences

Matches (and GCD)

20 of 74

Inferring Key Size: Index of Coincidence (IC)

20

20

William Frederick Friedman

(1891-1969)

Principle: Counting character occurrences at every indexes (and drawing correlation). Can also computes most probable key length!

Technique invented in 1920 and published in "The Index of Coincidence and its Applications in Cryptography"

21 of 74

Cryptanalysis: Step #1 (First Attempt)

21

21

Guess #2: The only 256 bytes repeating block is 0’s in the plaintext.

Steps:

⇒ Take the repeating block and apply it on the whole firmware with by testing various operators (+, -, x, &, |, ^) (might revert operation)

0 + key = key

0 ^ key = key

e.g

22 of 74

Cryptanalysis: Step #1 (First Attempt)

22

22

⇒ Conclusion: Might not be only one operator

Guess #2: The only 256 bytes repeating block is 0’s in the plaintext.

Steps:

⇒ Take the repeating block and apply it on the whole firmware with by testing various operators (+, -, x, &, |, ^) (might revert operation)

⇒ Explore “semi-deciphed” to find any readable strings

ASCII

UTF-16

b'i>IW;xLUKcW?Rz/DO?58`MR'

b"KZDI<||'\\BCVn;R`dt"

b"R+AVoFI<rG'XSRCX9["

b'`=MK\\=?GrB#MZCH(38'

b'x=IT K<?z>AW7??`pU'

b'IC4SE3XzENHOt;>;L\\'

add (+)

(284 matches)

b"r'[ d%_ j/Un 3lr areaTc fail!"

b'L=EEFABLE"$"sco": "RE04?C=U'

...

b'"Xl<l,2Cp9'

b'U2sG1Pl\\jE'

b'tLk*&5WjCT'

sub (-)

(12 matches)

b'0lcaSe us] app or ah_r]ifg c_r\\ lg stgp ch_ree.'

b'<]vice ^aulty& Pl]as[ ag^t_cl _dSiViqlratgr&'

b'AgmekfYc_lgof crRor milh cWnlr_l bgard'

b'This chaR_er h_s a\\r[a\\y b[]V r]sern[\\'

b'Inqerl cUffeatUr iV char]] pert.'

b'Car\\ read]R cgmmknWc_tWgf [rrgr'

sub (-)

(89 matches)

(no match for other operators)

This charset has already been seen [..]

23 of 74

Step #2: More Complex Operation

23

23

Goal:

  • Testing combinatorial of any two operators (+, -, x, &, |, ^)

  • by SMT solving:
    • Enforce plain-ciphered constraints using operators
    • Solve to obtain: key1 and key2

ciphith = (key1ith1 plainith ) ◇2 key2ith

Guess #3: Algorithm shall involve multiple operators such as:

candidate: add | add

candidate: add | sub

candidate: add | mul

candidate: add | and_

candidate: add | or_

candidate: add | xor

candidate: sub | add

candidate: sub | sub

candidate: sub | mul

candidate: sub | and_

candidate: sub | or_

candidate: sub | xor

candidate: mul | add

candidate: mul | sub

candidate: mul | mul

candidate: mul | and_

candidate: mul | or_

candidate: mul | xor

candidate: and_ | add

candidate: and_ | sub

candidate: and_ | mul

candidate: and_ | and_

candidate: and_ | or_

candidate: and_ | xor

candidate: or_ | add

...

candidate: xor | and_

candidate: xor | or_

candidate: xor | xor

total: 35/36

They all match!

24 of 74

Step #3: Better Known-Plaintext Attack

24

24

Guess #4: The firmware shall

contain crypto materials

⇒ Need more plaintext for KPA

(Known-Plaintext Attacks)

Searching AES SBOX

(In “semi-deciphered” obtain from sub)

Windowed search

25 of 74

Step #3: SMT Results

25

25

samples = [

(b"\x00"*256, repeating_block),

(rotated_sbox, ciphered_sbox),

(rotated_inv_sbox, ciphered_inv_sbox)

]

candidates = solve_ops_combinatorial_smt(samples)

candidate: add | xor

candidate: sub | xor

total: 2/36

Strings

(In “semi-deciphered”)

b'8p r1JPx%n8p r260x=>8p RSJPH%.80 24'

b'eq"*["r"<"ivt","-1"]d "Ti]eOffse'

b'--=WarrYno!!!}--=FLASH_Evase(5) '

b'{"amd":"FIRMwAREGVMRSIWN_nCPKRT"'

b'atiov*%l pe2iodDurati[b:=d va\\i'

b'%.2fV, Wu|CR=eR3fA, outOW==>?fA'

b'uaee() case OLD_tCtPAC[AGE`ODD_'

b'T1KKySe97eKnDIr1]vnscFAZMej2Yc'

b'VendI\\Ta_":+&rwb,"strYbg"<."]'

b'CovlonePowernL3: %d; m_w'

b'Id":REPLACEgCONVECTURID,",i+'

b'1e 5 stwp > fa5lt w appoYbt '

b'cOARDS","ppqta4us.:bvclue"}}'

b"#'###@@@@@@##c### con^ectorS"

b'mmpr!t}r%Zed %d =d %d %d %d '

b':ed tp:0x%08x ppsz0x%08x sts'

b'0x%.xp r7:0x%28p r8:px%.8p '

b'i$TwZ%l,u startSchcdu,e:%ll'

b'=5%---m-=-->packkt %rrkr`#d'

...

b'TLS-ECLH-EC<SA-WITH-AEs-16HmG_K-WHA2'

b'tIME"4"seq": "REPLACEgSEQ"`,>lala":'

b'WDVR0*BFgwFoASA95QFVbRTLtmXKPiGxz\\'

b'tSN #s, uWmB %d, mGmM %d4`m[rPsRt '

26 of 74

Step #4: Refining plain-ciphered samples

26

26

Goal: providing more samples until the SMT returns a single solution

-----BEGIN CERTIFICATE-----

27 of 74

Step #4: Refining plain-ciphered with Certificates

27

27

Methodology:

  • Enforce strict equality for header / footers

  • Enforce Base64 charset in between ⇒ Easy to do in SMT!

def is_valid_base64(c):

charset = [('a','z'), ('A','Z'), ('0','9')]

ascii_constraints = [z3.And(c >= ord(x[0]), c <= \

ord(x[1])) for x in charset]

xtra_chars = ['+', '/', '=', b'\x0d', b'\x0a']

xtra_const = [c == ord(x) for x in xtra_chars]

return z3.Or(*ascii_constraints, *xtra_const)

28 of 74

28

28

b'{\t"AuthorizationCacheEnabled":["rw","boolean",&false"],\t"AuthorizeRemoteTxRequests":["rw","boolean","true"],\t"AllowOfflijeTxForUnknownId":["rw","boolean","false"],\t"ClockAlignedDataInterval":["rw","int","1800"],\t"ConnectionTimeOut":["rw","int","30"],\t"GetConfigurationMaxKeys":["r","int","10"],\t"HeartbeatInterval":["rw.,"int","14400"],\t"LocalAuthorizeOffline":["rw","boolean","true"],\t"LocalPreAuthorize":["rw","boolean","true"],\t"MeterValuesAlignedData":]"rw","string","Energy.Active.Import.Register,Voltage,Current.E'

b'port,Power.Offered,Current.Import,Powev.Active.Import"],\t"MeterValuesSampledData":["rw","string","Energy.Active.Import.Register,Voltage,Current.Export,Power.Offered,Current.Import,Power.Active.Import"],\t"MeterValueSampleInterval":["rw","int","4"],\t"NumberOfConnectors":["r","int","1"],\t"ResetRetries":["rw","int.,"1"],\t"ConnectorPhaseRotation":["rw","string","RST"],\t"otopTransactionOnEVSideDisconnect":["rw","boolean","true"],\t"StopTransactionOnInvalidId":["rw","boolean","false"],\t"StopTxnAlignedData":["rw","'

b'%s:connId:%d LocalId:%s nemoteId:%s startIdTag:%s stopIdTag:%s startMode:%d chargeMode:%d chargeParam:%d chargeTimeSec:%d stopTime:%d meterStart:%d meterEnd:%d energy:%d stopReason:%d accountFlag:%d svcEndFlag:%d startTranFlag:%d stopTranFlag:%d svcStartTime:[%04d-%02d-%02dT%02d:%02d:%02d] svcEndTime:[%04d-%02d-%02dT%02d:%02d:%02d]'

b'connId:%d LocalId:%s RemateId:%s startIdTag:%s stopIdTag:%s startMode:%d chargeMode:%d chargeParam:%d chargeTimeSec:%d stopTime:%d meterStart:%d meterEnd:%d energy:%d stopReason:%d accountFlag:%d svcEjdFlag:%d startTranFlag:%d stopTranFlag:%d svcStartTime:[%04d-%02d-%02dT%02d:%02d:%02d] svcEndTime:[%04d-%02d-%02dT%02d:%02d:%02d]'

b'n m_onlineDevCount %d ; m_chargingDevCsunt %d ; m_onlinePower %d ; m_offLineSumPower %d ; m_groupDevCount %d ; m[chargingPowerSum %d; m_selfCheckGridPhase %d ; m_gridRST 0x%x ; m_meterPower %d ; m_curPower %lld ; 11KWChargingCount %d ; curMaxLimitPower %d ;m_minLimitPower %d ;m_meterEnabled %d ;StopChargingDevOount %d'

b'{"cmd":"RANDOM_DELAY_TIME","seq": "REPLACE_SEQ" ,"data"Z {"pileNum":"REPLACE_SN", "connectsrId":REPLACE_CONJECTORID,"delayLimitMax":REPLACE_LIMITMAX, "delayLimit>:REPLACE_LIMIT,"delayOn":REPLACE_DELAYON,"delayRunning":REPLACE_RUNNING,"timeLeft":REPLACE_TIMELEFT, "timestamp":"REPLACE_TIMESTAMP"}}'

b'comStatus:%d,runStatus:%d,faultCode:%x,faultData:%x,cpVolt:%d,cpVoltJ:%d,ppVolt:%d,eElFbStatus:%d,pwm:%d,gridFreq:%d,gridVoltR:%d,gridVoltS:%d,gridValtT:%d,currR:%d,currS:%d,currT:%d,meterErrFlag:%d,meterEnd:%d,ChargeTime:%d,Energy:%d,neVolt:%d,PlugtsCharge:%d,temprature:%d %d %d %d %d %d %d %d'

b'm_gridRST 0x%x; gridRST %d ; Cur power: %lld ;pawer sum : %d ;outPowerR: %d ;outPowerS: %d ; outPowerT %d; outCurrR: %d ; outCurrS %d ; outCurrT %d ? outVoltR: %d ; kutVoltS %d ; outVoltT %d ; gridVoltR: %d ; gridVoltS %d 7 gridVoltT %d'

b' <%s:%u>: Node startInterval:%llu endInterval:%llu validFram:%llu validTo:%llu startSchedule:%llu duration:%d PeriodSize:%d connectorId:%d Level:%d Purpose:%d PvofileKind:%d recurrencyKind:%d sResult startSec:%llu duration:%d periodDu'

b'-------------DLB ReadFlash sucess,m_inDlbGroup %d , m_DevRule %d , m_DlbOnOff %d , m_totalDevCount %d , m_totalPower %d , m_offLineMaxLimitPower %d, m_defaultTotalPower %d , m[masterSN %s, m_meterBaudrate %d, m_meterEnabled %d, m_re'

b"AppGetGroupDevInfo m_meterPower %d ; m_ojlinePower %d ; m_onlineDevCount %d ; m_chargingDevCount %d ; m_offLineSumPower 'd ; m_groupDevCount %d ; m_chargingPowerSum %d; m_selfCheckGridPhase %d ; m_gridRST 0x%x ; m_curPower %lld"

b' <%s:%u>: i:%d dst:%d node startPeriod:%d startPerkodPos:%d dstStartPeriod:%d dstEndPeriod:%d recurrencyIndex:%d durationNode:%d recurrencyStepLen:%d dstEndPeriod:%d recurrencyIndex:%d (*it) startPeriod:%d duration:%d periodDu'

b' m_nTSpOhgDCt %d ;m_onlDCt %d ; m_chgDCt %d ; m_onlPw %d ; m_offLSumPw %d ; m_grpDCt %d ; m_chgPwSum %d; m_sCGP %d ; m_mPw %d ; m_cPw %lld ; 11MWChgCt %d ; cMLPw %d ;m_mLPw %d ;m_mE %d; L1:%d; L2:%d; L3:%d;'

b' <%s:%u>: dst:%d node startPeriod:%d startPeriodPks:%d dstStartPeriod:%d dstEndPeriod:%d recurrencyIndex:%d (*it) startPeriod:%d duration:%d periodDuration:%d validDuration:%d dst end:%llu endInterval:%llu '

b' m_nR %d ; m_onlDCt %d ; m_chgDCt %d ; m_onlPw %d ; m_offLSumPw %d ; m_grpDCt %d ; m_chgPwSum %d; m_sCGP %d ; m_gRST 0x%x ; m_mPw %d ; m_curPower %lld ; 11KWChgCt %d ; cMLPw %d ;m_mLPw %d ;m_mE %d'

b'@WWJ m_almRunType:%d m_maxAvailablePower:%d m_availablePower:%d m_DynPower:%d m_meterPower:%d m_totalDevCount: %d m_chargingDevCount.sum: %d totalPower:%d m_onlinePower.sum:%d m_loadPower:%d'

b' <%s:%u>: i:%d dst:%d node startPeriod:%d startPeriodPos:%d recuvrencyStepLen:%d dstEndPeriod:%d recurrencyIndex:%d (*it) startPeriod:%d duration:%d periodDuration:%d validDuration:%d'

b' <%s:%u>: retF%d dst:%d node startPeriod:%d startPeriodPos:%d dstStartPeriod:%d dstEndPeriod:%d recurrencyIndex:%d (*it) startPeriod:%d duration:%d periodDuration:%d validDuration:%d'

b'SN:%s; role:%d; net status:%d; group status:%d;charging status:%d; default power:%d; max limit pkwer:%d; cur power:%lld ;gridIsSinglePhase:%d; carIsSinglePhase:%d; RST_value:%d '

29 of 74

Step #5: Refining with Hex

29

29

30 of 74

Step #6: Iterative Refinement

30

30

⇒ Problem: At each iteration the key bytes chosen by the solver can change making some faulty strings to appear

⇒ Thus need to refine up until no faults were found

⇒ Did: 4 - 5 iterations

⇒ Finally found the two key parts making the whole firmware valid (with ADD | XOR)

(Turns out the real algorithm was a bit different with a key and a constant [↗] )

31 of 74

31

31

32 of 74

Vulnerability

Research

32

33 of 74

Hardware

Overview

33

33

CAN BUS

STM32F407

Power board

Levetop

LT7689

UART

LED

Winbond Flash

Control board

SWD

BLE, WiFi

USB

STM32F407

ESP32-WROOM-32D

UART

Ethernet

UART

SIM slot

LTE board

EC25-EUX

PP

CP

34 of 74

Firmware analysis

34

34

Recon:

  • Loading address 0x08010000 (bootloader not part of FOTA?, secureboot?)

  • OS: FreeRTOS
    • No symbols available
    • No mitigations/protections (ASLR, NX..)
    • Task-based OS: identified interesting ones

Used internal tool for function similarity ⇒ Recognized all known FreeRTOS functions

Wifi_Ble_HAL_TASK

App_Business_Task

Module_Wifi_Ble_Task

UsbCommTask

35 of 74

Vulnerabilities

35

35

2 independent

RCE

chains

BLE

Vuln #1

(BoF cmds)

BLE

Vuln #2

(AT inject)

Bluetooth Stack

USB Stack

USB

Vuln #1

36 of 74

Bluetooth

36

36

AT commands

BLE

messages

Translate BLE message into AT commands:

  • BLECONN
  • BLEAUTHCMPLT
  • READ
  • BLEAUTHCMPT
  • BLEDISCONN
  • ready
  • WRITE
  • ...

+WRITE:0,0,0,0,4,DATA

size

37 of 74

Chain #1 - Vuln #1: Stack BoF in BLECFGMTU

37

37

But as an external user we can’t directly send BLECFGMTU commands

Goal: Trying to see if we can forge a message that would result in a BLECFGMTU command

uint8_t size = min(size, 100);

uint16_t len = 0;

char tmp_buf[0x8] = {0};

if (strstr("+BLECFGMTU:", ble_buf, size, &pos) != 0) {

for (int i = 0; i < size; ++i) {

if (ble_buf[pos + i + 13] == '\r' && ble_buf[pos + i + 14] == '\n') {

len = i;

break

}

}

if (len != 0)

memcpy(&tmp_buf, &ble_buf[pos + 13], len);

Limited to 100 bytes

+BLECFGMTU:0,115200\r\n

Example of valid command:

38 of 74

Chain #1 - Buffered AT command parsing

38

38

Observations:

  • Commands are buffered! (thus they can be chained)
  • There is an order in which commands are evaluated (with str_str)

Can reliably force the ESP32 to send two commands in the same message.

+BLESECREQ:0\r\n+BLEAUTHCMPL:0,0\r\n

+IPD [:8]

+WRITE [:10]

+WRITE [:8]

+BLECONN [:100]

ble_process_esp32_msg()

+BLESECREQ [:100]

+BLEAUTHCMPLT

+BLEAUTHCMPLT

+BLECFGMTU

...

+BLECFGMTU

Y

N

39 of 74

Chain #1 - Vuln #2: AT command injection

39

39

+READ:0,0\r\n+WRITE:0,0,0,116,+BLECFGMTU:0,115200\0[PAYLOAD_BOF_STK]\r\n

+IPD [:8]

+WRITE [:10]

+WRITE [:8]

+BLECONN [:100]

ble_process_esp32_msg()

+BLESECREQ [:100]

+BLEAUTHCMPLT

+BLEAUTHCMPLT

+BLECFGMTU

...

+BLECFGMTU

Y

N

8

40 of 74

Chain #1 - Vuln #2: AT command injection

40

40

+READ:0,0\r\n+WRITE:0,0,0,116,+BLECFGMTU:0,115200\0[PAYLOAD_BOF_STK]\r\n

+IPD [:8]

+WRITE [:10]

+WRITE [:8]

+BLECONN [:100]

ble_process_esp32_msg()

+BLESECREQ [:100]

+BLEAUTHCMPLT

+BLEAUTHCMPLT

+BLECFGMTU

...

+BLECFGMTU

Y

N

10

41 of 74

Chain #1 - Vuln #2: AT command injection

41

41

+IPD [:8]

+WRITE [:10]

+WRITE [:8]

+BLECONN [:100]

ble_process_esp32_msg()

+BLESECREQ [:100]

+BLEAUTHCMPLT

+BLEAUTHCMPLT

+BLECFGMTU

...

+BLECFGMTU

Y

N

+READ:0,0\r\n+WRITE:0,0,0,116,+BLECFGMTU:0,115200\0[PAYLOAD_BOF_STK]\r\n

8

42 of 74

Chain #1 - Vuln #2: AT command injection

42

42

+IPD [:8]

+WRITE [:10]

+WRITE [:8]

+BLECONN [:100]

ble_process_esp32_msg()

+BLESECREQ [:100]

+BLEAUTHCMPLT

+BLEAUTHCMPLT

+BLECFGMTU

...

+BLECFGMTU

Y

N

+READ:0,0\r\n+WRITE:0,0,0,116,+BLECFGMTU:0,115200\0[PAYLOAD_BOF_STK]\r\n

100

43 of 74

Chain #1 - Vuln #2: AT command injection

43

43

+IPD [:8]

+WRITE [:10]

+WRITE [:8]

+BLECONN [:100]

ble_process_esp32_msg()

+BLESECREQ [:100]

+BLEAUTHCMPLT

+BLEAUTHCMPLT

+BLECFGMTU

...

+BLECFGMTU

Y

N

+READ:0,0\r\n+WRITE:0,0,0,116,+BLECFGMTU:0,115200\0[PAYLOAD_BOF_STK]\r\n

44 of 74

Chain #1 - Vuln #2: AT command injection

44

44

+READ:0,0\r\n+WRITE:0,0,0,116,+BLECFGMTU:0,115200\0[PAYLOAD_BOF_STK]\r\n

+IPD [:8]

+WRITE [:10]

+WRITE [:8]

+BLECONN [:100]

ble_process_esp32_msg()

+BLESECREQ [:100]

+BLEAUTHCMPLT

+BLEAUTHCMPLT

+BLECFGMTU

...

+BLECFGMTU

Y

N

Call the command handler with injected command and trigger stack BoF !

WRITE arguments improperly considered as a command !

found !

45 of 74

Chain #1 - Full Exploit

45

45

.data

@ble_tmp_buff

heap

46 of 74

Chain #1 - Full Exploit

46

46

.data

@stage1

@ble_tmp_buff

Step #1: send valid BLE +WRITE command

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

data stored in heap

code keep pointer to data (fixed .data address)

47 of 74

Chain #1 - Full Exploit

47

47

stack frame

.data

@stage1

@ble_tmp_buff

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

Step #2: Use vuln#2 to inject BLECFGMTU command and execute faulty function.

buff[8]

r4

r5

r6

r7

r8

r9

r10

r11

@ret

48 of 74

Chain #1 - Full Exploit

48

48

buff[8]

r4

r5

r6

r7

r8

r9

r10

r11

@ret

stack frame

100 bytes

.data

@stage1

@ble_tmp_buff

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

Step #3: Use vuln#1 to trigger stack BoF on buf

Can’t put exploit on stack as address non-deterministic (depends Task launch order).

⇒ Not reliable enough for pwn2own.

49 of 74

Chain #1 - Full Exploit

49

49

buff[8]

r4

r5

r6

r7

r8

r9

r10

r11

@ret

stack frame

100 bytes

.data

@stage1

@ble_tmp_buff

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

Step #3: Use vuln#1 to trigger stack BoF on buf

All registers restored before return: pop {r4-11}

Can’t ROP as overflow can override @ret but not beyond

JOP?

50 of 74

Chain #1 - Full Exploit

50

50

buff[8]

r4

r5

r6

r7

r8

r9

r10

r11

@ret

stack frame

100 bytes

.data

@stage1

@ble_tmp_buff

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

@stage1 points to data, in THUMB, LSB should be set to jump on code

Step #4: Write a JOP chain to jump on @stage1

51 of 74

Chain #1 - Full Exploit

51

51

buff[8]

-

-

r4

@ble_tmp_buff

r5

@gadget2

r6

@scratch

r7

@scratch+0xc

r8

@gadget3

r9

-

r10

-

r11

-

@ret

@gadget1

stack frame

100 bytes

.data

@stage1

@ble_tmp_buff

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

.text

ldr r0,[r4]

mov r1, #0

blx r5

@gadget1

addr r0, #1

str r0,[r6,#0c]

mov r2, #1

ldr r0, [r6]

mov r1, #0

blx r8

@gadget2

ldr r1, [r7]

blx r1

@gadget3

JOP chain

r0

r1

registers state

52 of 74

Chain #1 - Full Exploit

52

52

buff[8]

-

-

r4

@ble_tmp_buff

r5

@gadget2

r6

@scratch

r7

@scratch+0xc

r8

@gadget3

r9

-

r10

-

r11

-

@ret

@gadget1

stack frame

100 bytes

.data

@stage1

@ble_tmp_buff

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

.text

ldr r0,[r4]

mov r1, #0

blx r5

@gadget1

addr r0, #1

str r0,[r6,#0c]

mov r2, #1

ldr r0, [r6]

mov r1, #0

blx r8

@gadget2

ldr r1, [r7]

blx r1

@gadget3

JOP chain

r0

@stage1

r1

registers state

53 of 74

Chain #1 - Full Exploit

53

53

buff[8]

-

-

r4

@ble_tmp_buff

r5

@gadget2

r6

@scratch

r7

@scratch+0xc

r8

@gadget3

r9

-

r10

-

r11

-

@ret

@gadget1

stack frame

100 bytes

.data

@stage1

@ble_tmp_buff

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

.text

ldr r0,[r4]

mov r1, #0

blx r5

@gadget1

addr r0, #1

str r0,[r6,#0c]

mov r2, #1

ldr r0, [r6]

mov r1, #0

blx r8

@gadget2

ldr r1, [r7]

blx r1

@gadget3

JOP chain

r0

@stage1

r1

#0

registers state

54 of 74

Chain #1 - Full Exploit

54

54

buff[8]

-

-

r4

@ble_tmp_buff

r5

@gadget2

r6

@scratch

r7

@scratch+0xc

r8

@gadget3

r9

-

r10

-

r11

-

@ret

@gadget1

stack frame

100 bytes

.data

@stage1

@ble_tmp_buff

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

.text

ldr r0,[r4]

mov r1, #0

blx r5

@gadget1

addr r0, #1

str r0,[r6,#0c]

mov r2, #1

ldr r0, [r6]

mov r1, #0

blx r8

@gadget2

ldr r1, [r7]

blx r1

@gadget3

JOP chain

r0

@stage1 + 1

r1

#0

registers state

55 of 74

Chain #1 - Full Exploit

55

55

buff[8]

-

-

r4

@ble_tmp_buff

r5

@gadget2

r6

@scratch

r7

@scratch+0xc

r8

@gadget3

r9

-

r10

-

r11

-

@ret

@gadget1

stack frame

100 bytes

.data

@stage1

@ble_tmp_buff

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

.text

ldr r0,[r4]

mov r1, #0

blx r5

@gadget1

addr r0, #1

str r0,[r6,#0c]

mov r2, #1

ldr r0, [r6]

mov r1, #0

blx r8

@gadget2

ldr r1, [r7]

blx r1

@gadget3

JOP chain

r0

@stage1 + 1

r1

#0

registers state

(stores at @scratch a tmp address)

56 of 74

Chain #1 - Full Exploit

56

56

buff[8]

-

-

r4

@ble_tmp_buff

r5

@gadget2

r6

@scratch

r7

@scratch+0xc

r8

@gadget3

r9

-

r10

-

r11

-

@ret

@gadget1

stack frame

100 bytes

.data

@stage1

@ble_tmp_buff

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

.text

ldr r0,[r4]

mov r1, #0

blx r5

@gadget1

addr r0, #1

str r0,[r6,#0c]

mov r2, #1

ldr r0, [r6]

mov r1, #0

blx r8

@gadget2

ldr r1, [r7]

blx r1

@gadget3

JOP chain

r0

@stage1 + 1

r1

#0

registers state

(junk)

57 of 74

Chain #1 - Full Exploit

57

57

buff[8]

-

-

r4

@ble_tmp_buff

r5

@gadget2

r6

@scratch

r7

@scratch+0xc

r8

@gadget3

r9

-

r10

-

r11

-

@ret

@gadget1

stack frame

100 bytes

.data

@stage1

@ble_tmp_buff

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

.text

ldr r0,[r4]

mov r1, #0

blx r5

@gadget1

addr r0, #1

str r0,[r6,#0c]

mov r2, #1

ldr r0, [r6]

mov r1, #0

blx r8

@gadget2

ldr r1, [r7]

blx r1

@gadget3

JOP chain

r0

xxxxxxxxxx

r1

#0

registers state

(erase our address!)

58 of 74

Chain #1 - Full Exploit

58

58

buff[8]

-

-

r4

@ble_tmp_buff

r5

@gadget2

r6

@scratch

r7

@scratch+0xc

r8

@gadget3

r9

-

r10

-

r11

-

@ret

@gadget1

stack frame

100 bytes

.data

@stage1

@ble_tmp_buff

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

.text

ldr r0,[r4]

mov r1, #0

blx r5

@gadget1

addr r0, #1

str r0,[r6,#0c]

mov r2, #1

ldr r0, [r6]

mov r1, #0

blx r8

@gadget2

ldr r1, [r7]

blx r1

@gadget3

JOP chain

r0

xxxxxxxxxx

r1

#0

registers state

(junk)

59 of 74

Chain #1 - Full Exploit

59

59

buff[8]

-

-

r4

@ble_tmp_buff

r5

@gadget2

r6

@scratch

r7

@scratch+0xc

r8

@gadget3

r9

-

r10

-

r11

-

@ret

@gadget1

stack frame

100 bytes

.data

@stage1

@ble_tmp_buff

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

.text

ldr r0,[r4]

mov r1, #0

blx r5

@gadget1

addr r0, #1

str r0,[r6,#0c]

mov r2, #1

ldr r0, [r6]

mov r1, #0

blx r8

@gadget2

ldr r1, [r7]

blx r1

@gadget3

JOP chain

r0

xxxxxxxxxx

r1

@stage1 + 1

registers state

(read it back !)

60 of 74

Chain #1 - Full Exploit

60

60

buff[8]

-

-

r4

@ble_tmp_buff

r5

@gadget2

r6

@scratch

r7

@scratch+0xc

r8

@gadget3

r9

-

r10

-

r11

-

@ret

@gadget1

stack frame

100 bytes

.data

@stage1

@ble_tmp_buff

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

.text

ldr r0,[r4]

mov r1, #0

blx r5

@gadget1

addr r0, #1

str r0,[r6,#0c]

mov r2, #1

ldr r0, [r6]

mov r1, #0

blx r8

@gadget2

ldr r1, [r7]

blx r1

@gadget3

JOP chain

r0

xxxxxxxxxx

r1

@stage1 + 1

registers state

61 of 74

Chain #1 - Full Exploit

61

61

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

  1. Calls FreeRTOS: xTaskCreate(@stage2, “exploit”, …)
  2. Returns where BLECFGMTU function should have

⇒ Execution flow restored properly

(In reality the task code is copied at another location than the heap that might be overridden).

62 of 74

Chain #1 - Full Exploit

62

62

heap

CreateTask

Shellcode

@stage1

Task

Function

@stage2

FreeRTOS creates the task and jumps on the final stage

  • 100% stable
  • do not cause any service interruption

63 of 74

DEMO

64 of 74

64

65 of 74

Chain #2: USB

65

65

⇒ RE uneasy (plenty callbacks function pointers) but likely based on an OSS library

bmRequestType

bRequest

wValue

wIndex

wLength

0

1

2

3

4

Header

Missing validation of message length ⇒ Buffer overread/overwrite

66 of 74

66

66

Device is compromised

and what now ?

(besides participating to pwn2own)

⇒ Full internal device pivoting capabilities

(can flash any of the internal firmwares)

67 of 74

Persistency

67

67

if a new magic: fetch fw from external flash and decrypt it into its memory.

FIRMWARE

(encrypted)

size

update magic

crc32 cksum

0x864000

0x865000

FIRMWARE

(max 960 kb)

bootloader

0x08000000

0x08010000

0x08100000

STM32 (internal flash)

Winbond (external flash)

Solution #1: Patch either STM32 or Winbond firmware (Winbond requires forcing update) + update crc32.

⇒ Not persistent to OTA update

Solution #2: Write the implant in bootloader area:

  • not affected by OTA update
  • bootloader only use ~24Kb of 64Kb ⇒ 40Kb available!
  • Patch bootloader to execute implant

⇒ Enjoy unlimited persistency

68 of 74

Lateral Movement

68

68

Pivoting

LAN / WAN

#1

Via WiFi, ETH..

  • Can interact with LAN
  • Can interact with WAN
  • All primitives to perform network queries

OCPP Provider

#2

Via any WAN

  • Internet made to interact with Autel OCPP servers
  • OCPP provides sharing EVSE and billing electricity consumption

CAR

#3

Via IEC 62196 Type2

  • Connected through power board
  • CP / PP dial ports
  • V2G in latest OCPP versions

69 of 74

Pivot #1: LAN / WAN (Botnet)

69

69

Device used both by enterprises and domestic users

WiFi: exposes all connected devices, (smart TVs , speakers, NAS, smart fridge, cameras , routers )

BLE: can use EVSE to actively connect to reachable devices (smart watches, speakers, …)

LAN

⇒ Expose internal network otherwise

unreachable.

Botnet C2

WAN

(internet)

⇒ Device always connected to internet (4G/WiFi).

(almost anything doable with internet connection..)

70 of 74

Finding the devices

70

70

Find the Devices — There's a Map for That!

Only public EVSE are shown. Private ones don’t appear.

Autel

Gotta Exploit ‘em all !

71 of 74

Pivot #2: Backend Infrastructure (OCPP, etc)

71

71

OCPP Open Charge Point Protocol (communication protocol between the EVSE and the Central Management Server)

Possible Abuse

Free charging (electricity), by impersonating users / getting them billed!

Functionalities:

  • starting/stopping a charge

  • billing users energy consumption

  • vendor-specific protocol extensions

Targeting backend protocol (reuse auth)

72 of 74

Pivot #3: Car (side)

72

72

Proximity Pilot

Control Pilot

Abusing EVSE for

Free Electricity

Complete control of Power Board

Interacting with the car

⇒ Functionality depends on OCPP version

(defined in IEC 61851-1)

Determine if connected and supported current

PWM signal for EV state (ready, charging …)

73 of 74

Conclusion & Takeaways

73

73

⇒ If you wonder we failed at pwn2own

⇒ Connected EVSE are a bridge between the LAN, the CAR and to Internet

⇒ pwning the device brings interesting

abuse scenarios (free electricity…)

⇒ We did not score at pwn2own

(regional version reasons) but obtained:

  • CVE-2025-5826 (ZDI-CAN-26368)
  • CVE-2025-5827 (ZDI-CAN-26369)

74 of 74

Thank You !