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
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
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/
source: Global EV Outlook 2025, IEA
⇒
⇒
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
EVSE’s Today
5
5
Recent now connected to internet
Bridge between your home and your car.
Pwn2Own Automotive 2025
6
6
⇒ Focus on the ones part of pwn2own automotive
Pwn2Own Automotive 2025
7
7
⇒ Focus on the ones part of pwn2own automotive
⇒ Bug bounty competition for automotive equipments
The target we selected!
⇒ Already present in pwn2own 2024 (5 CVE reported)
Autel MaxiCharger AC Wallbox
8
8
Specs:
USB
Ethernet
NFC
WiFi
Bluetooth
RS-485
Firmware RE
9
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)
⇓
Intercepting Firmware
11
11
Autel Cloud
Android App
Did not manage to trigger OTA update query
Need to reverse the application!
⇒
Intercepting Firmware: Attacking the app
12
12
⇒ Packed with SecNeo
⇒ Frida hooking is still possible.
Methodology:
$ 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
⇒
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
14
We have the Firmware!
But it’s encrypted!
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)
Entropy Checks
16
16
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 !
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
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)
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"
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
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 [..]
Step #2: More Complex Operation
23
23
Goal:
ciphith = (key1ith ◇1 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!
⇒
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
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 '
Step #4: Refining plain-ciphered samples
26
26
Goal: providing more samples until the SMT returns a single solution
-----BEGIN CERTIFICATE-----
Step #4: Refining plain-ciphered with Certificates
27
27
Methodology:
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
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 '
Step #5: Refining with Hex
29
29
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
31
Vulnerability
Research
32
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
Firmware analysis
34
34
Recon:
Used internal tool for function similarity ⇒ Recognized all known FreeRTOS functions
Wifi_Ble_HAL_TASK
App_Business_Task
Module_Wifi_Ble_Task
UsbCommTask
Vulnerabilities
35
35
2 independent
RCE
chains
BLE
Vuln #1
(BoF cmds)
BLE
Vuln #2
(AT inject)
Bluetooth Stack
USB Stack
USB
Vuln #1
Bluetooth
36
36
AT commands
BLE
messages
Translate BLE message into AT commands:
+WRITE:0,0,0,0,4,DATA
size
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:
Chain #1 - Buffered AT command parsing
38
38
Observations:
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
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
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
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
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
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
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 !
Chain #1 - Full Exploit
45
45
.data
@ble_tmp_buff
heap
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)
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 | |
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.
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?
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
⇒
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
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
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
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
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)
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)
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!)
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)
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 !)
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
Chain #1 - Full Exploit
61
61
heap
CreateTask
Shellcode
@stage1
Task
Function
@stage2
⇒ Execution flow restored properly
(In reality the task code is copied at another location than the heap that might be overridden).
Chain #1 - Full Exploit
62
62
heap
CreateTask
Shellcode
@stage1
Task
Function
@stage2
FreeRTOS creates the task and jumps on the final stage
DEMO
64
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
66
Device is compromised
and what now ?
(besides participating to pwn2own)
⇒ Full internal device pivoting capabilities
(can flash any of the internal firmwares)
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:
⇒ Enjoy unlimited persistency
Lateral Movement
68
68
Pivoting
LAN / WAN
#1
Via WiFi, ETH..
OCPP Provider
#2
Via any WAN
CAR
#3
Via IEC 62196 Type2
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..)
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 !
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:
Targeting backend protocol (reuse auth)
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 …)
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:
Thank You !