문제 정보
Prompt: I downloaded a firmware update for RAMN’s ECU B and C from a secret OTA portal in development. Can you help me sign my own firmware files? I have attached a caringcaribou UDS RDBI dump log file, if that is any help.
(Note: flag is secret key in decimal format – not hexadecimal. It is NOT the password for the .zip file).
2 files are provided:
1. challenge_signed_firmware_files.zip
2. challenge4_RDID_dump.log
문제 풀이 요약
- 주어진 log 파일 내에 0x7e3, 0x7eb CAN id를 사용하는 UDS가 존재함
- 0x22 명령을 이용해서 브루투포싱하는 것으로 보임
- UDS 프레임 내에 public key를 추출하여 pub.pem 파일로 저장
- ECUB.hex.sig, ECUC.hex.sig 파일을 sigB.der, sigC.der 파일로 변환
- pub.pem과 der 파일들을 이용해 ECU*.hex 파일 검증 수행
- sig*.der 파일의 서명 바이트 구조를 확인하여 동일한 난수(r)값 사용 식별
- 연산을 통해 개인키(d)를 추출
문제 풀이 상세
1. 주어진 log 파일 내에 0x7e3, 0x7eb CAN ID를 사용하는 UDS frame이 있으며 did을 brute-force하여 출력하는 동작을 수행하고 있습니다.

• 출력된 주요 did를 정리해보면 아래와 같습니다.
22 1d f2
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEzWvXZ0MMevWSHMMCzdJKOq2uXr1kg45NS1z0w8ZWc0Lr
n2qYn0QsXdR3aTXK9kDmPx2fH3wxt9OkhrwHnl+Hsg==
-----END PUBLIC KEY-----
22 1d f3
1234567890123456789012345678901234567890
22 1d f4
115792089237316195423570985008687907852837564279074904382605163141518161494337
22 1d f5
SECP256k1
2. 식별된 공캐키를 복구하여 검증을 수행합니다.
a. byte를 추출하여 pub.pem 파일로 저장
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEzWvXZ0MMevWSHMMCzdJKOq2uXr1kg45NS1z0w8ZWc0Lr
n2qYn0QsXdR3aTXK9kDmPx2fH3wxt9OkhrwHnl+Hsg==
-----END PUBLIC KEY-----
b. pub.pem 파일이 어떤 곡선 위에서 정의된 공개키인지 확인
• 검증 시 ASN1 OID가 DID : 0x1dF5에 쓰인 문자열 secp256k1와 일치
$ openssl ec -pubin -in pub.pem -text -noout -conv_form uncompressed
read EC key
Public-Key: (256 bit)
pub:
04:cd:6b:d7:67:43:0c:7a:f5:92:1c:c3:02:cd:d2:
4a:3a:ad:ae:5e:bd:64:83:8e:4d:4b:5c:f4:c3:c6:
56:73:42:eb:9f:6a:98:9f:44:2c:5d:d4:77:69:35:
ca:f6:40:e6:3f:1d:9f:1f:7c:31:b7:d3:a4:86:bc:
07:9e:5f:87:b2
ASN1 OID: secp256k1
3. challenge_signed_firmware_files.zip 의 비밀번호를 hashcat을 이용해 cracking을 시도합니다.
• 사용된 dictionary는 여러 비밀번호 파일을 조합한 rockyou와 유사한 파일입니다.
$ hashcat -m 17225 -a 0 ./hash2.txt ../final_combo.txt --backend-ignore-cuda
hashcat (v6.2.5) starting
...
Dictionary cache hit:
* Filename..: ../final_combo.txt
* Passwords.: 29605980
* Bytes.....: 294047725
* Keyspace..: 29605980
$pkzip$4*1*1*0*0*24*c428*603a23180c25ce5d86ccbb95a7a5e5292381545dfcb146015c9b0acb1d7b70032bc8cd66*1*0*8*24*fc61*a8558f624cbb9eb41d07248cb6f1f04e9ff9dd59a1ac0dd0d364077876972f4e9a8d7c8d*1*0*8*24*1ec8*83f8ef4dce5e6fd80718b62388b3e165588b0ad28bff032be21d2d4dc36e4b6f5f15152e*2*0*4c*40*34d3532f*17894*2a*0*4c*34d3*3a2c342737bb82610c7aa3635a975d694d2ee223ed6be0797673024f41f88c79827102bda1700468df9b6b084402e13d26256b9d111881295bc375f7d32c5568c6dd29d836ee0d2cd4380103*$/pkzip$:password12345678
...
4. 해당 압축 파일을 해제하면 4개의 파일이 존재하는 것을 확인합니다.
ECUB.hex
ECUB.hex.sig
ECUC.hex
ECUC.hex.sig
5. .hex.sig 파일의 구조는 64바이트 RAW r||s인 것으로 추측되며 2개의 파일을 모두 .der로 변환을 수행합니다.
# make_der.py
# pip install cryptography
from math import ceil
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
with open("ECUB.hex.sig","rb") as f:
sig = f.read()
assert len(sig)==64, f"len={len(sig)}"
r = int.from_bytes(sig[:32], "big")
s = int.from_bytes(sig[32:], "big")
der = encode_dss_signature(r, s)
with open("sigB.der","wb") as f:
f.write(der)
print("OK:", len(der), "bytes")
6. .der 파일과 .hex 파일을 이용해 검증을 수행하여 맞는 서명 파일인지 확인합니다.
• OpenSSL로 검증(SHA-256 가정)
$ openssl dgst -sha256 -verify pub.pem -keyform PEM -signature sig.der challenge_signed_firmware_files/ECUB.hex
Verified OK
7. 2개의 .der 서명 파일 구조를 확인한 결과 r 값이 동일하기 때문에 d값을 연산할 수 있습니다.
• r 이 같은 경우 같은 nonce(k)를 사용했기 때문에 취약한 경우임
# der sign file format
30 LL ; SEQUENCE
02 l_r rr..; INTEGER r
02 l_s ss..; INTEGER s
$ openssl asn1parse -inform DER -in sigB.der
0:d=0 hl=2 l= 70 cons: SEQUENCE
2:d=1 hl=2 l= 33 prim: INTEGER :CB8F2F4901E5DC0610A562309A0BA238289EDD5A6A819A115CA3B2AC802E7589
37:d=1 hl=2 l= 33 prim: INTEGER :90B2130178E5AA4501E0DA0FA9FA02A82C94F9F137664E43562D040D68B21262
$ openssl asn1parse -inform DER -in sigC.der
0:d=0 hl=2 l= 70 cons: SEQUENCE
2:d=1 hl=2 l= 33 prim: INTEGER :CB8F2F4901E5DC0610A562309A0BA238289EDD5A6A819A115CA3B2AC802E7589
37:d=1 hl=2 l= 33 prim: INTEGER :AB711A35E232B03C5BAA9AD2D1F3F74D348381FF31203AB3CE66F5980DE2F945
8. 개인키(d)를 구하는 파이썬 코드를 작성하여 각 파일을 대입하면 FLAG를 얻을 수 있습니다.
# pip install cryptography
from hashlib import sha256
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
from cryptography.hazmat.primitives.serialization import load_pem_public_key
# 입력 파일 이름
sig1_path = "sigB.der" # 첫 번째 서명 DER
msg1_path = "ECUB.hex" # 첫 번째 메시지 바이너리
sig2_path = "sigC.der" # 두 번째 서명 DER
msg2_path = "ECUC.hex" # 두 번째 메시지 바이너리
# secp256k1 군 차수 n
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
# 1) r,s 추출
r1,s1 = decode_dss_signature(open(sig1_path,"rb").read())
r2,s2 = decode_dss_signature(open(sig2_path,"rb").read())
assert r1 == r2, "r가 다르면 k 재사용 아님"
r = r1
# 2) 해시 z 계산(SHA-256, big-endian 정수)
z1 = int.from_bytes(sha256(open(msg1_path,"rb").read()).digest(), "big")
z2 = int.from_bytes(sha256(open(msg2_path,"rb").read()).digest(), "big")
# 3) 모듈러 역원
def inv(x,m): return pow(x, -1, m)
# 4) k, d 복구
k = ((z1 - z2) * inv((s1 - s2) % n, n)) % n
d = ((s1 * k - z1) * inv(r, n)) % n
print("r =", hex(r))
print("s1=", hex(s1))
print("s2=", hex(s2))
print("k =", hex(k))
print("d =", hex(d))
print("d(dec) =", d)
• 코드 수행 결과는 아래와 같습니다.
FLAG : 58198666691408413489157977283298548714183388226985257069167369868995344613920
$ python .\cal_d.py
r = 0xcb8f2f4901e5dc0610a562309a0ba238289edd5a6a819a115ca3b2ac802e7589
s1= 0x90b2130178e5aa4501e0da0fa9fa02a82c94f9f137664e43562d040d68b21262
s2= 0xab711a35e232b03c5baa9ad2d1f3f74d348381ff31203ab3ce66f5980de2f945
k = 0x3a0c92075c0dbf3b8acbc5f96ce3f0ad2
d = 0x80ab472c8929b2190636f5a394aabf8d3c45ef7f4c6ccc9bb1257db73d4e4220
d(dec) = 58198666691408413489157977283298548714183388226985257069167369868995344613920