실습 목표#
- 실제 ECU처럼 DoIP 헤더를 검증하고 잘못된 프레임을 표준 방식으로 거부한다.
- 헤더 오류의 DoIP NACK와 진단 내용 오류의 UDS 부정 응답을 구분한다.
- 쓰기 서비스를 추가하고, 중요한 값인 VIN은 쓰지 못하도록 보호한다.
거부 응답 개념 미리보기#
두 종류의 거부#
거부 응답은 발생 계층에 따라 두 가지로 나뉜다. 헤더가 손상된 경우는 전송 계층에서 DoIP NACK로 거부하고, 진단 내용이 잘못된 경우는 진단 계층에서 UDS 부정 응답으로 거부한다.
| 구분 | DoIP NACK | 허용/UDS 부정 응답 7F |
|---|---|---|
| Payload Type | 0x0000 | 0x8001 · 진단 메시지 |
| 언제 | 헤더나 길이가 잘못됨 | 헤더는 정상, 진단 내용이 문제 |
| 예시 | Inverse 손상, 길이 불일치 | 미지원 서비스, 보호된 DID 쓰기 |
| 붙는 코드 | NACK 코드 0x00 ~ 0x04 | NRC 0x11 · 0x31 · 0x33 등 |
| 거부한 층 | 전송 계층 | 진단 계층 |
DoIP Generic Header NACK 코드#
NACK 응답에는 거부 이유가 한 바이트로 표시된다. 이번 실습에서는 0x00과 0x04 두 가지를 확인할 수 있다.
| 코드 | 의미 | 이번 실습에서 확인할 수 있는 것들 |
|---|---|---|
0x00 |
Incorrect pattern format · 형식 오류 | Inverse를 손상시킨 bad 모드 |
0x01 |
Unknown payload type · 지원하지 않는 타입 | - |
0x02 |
Message too large · 최대 크기 초과 | - |
0x03 |
Out of memory · 메모리 부족 | - |
0x04 |
Invalid payload length · 길이 불일치 | badlen 모드와 short 모드 |
실습 1. 정상 동작 확인#
먼저 수정 전 원본 서버를 실행하여 정상 동작을 먼저 확인한다. 이후 구현 결과와 비교하기 위한 기준으로 활용한다.
기본 모드는 f190, VIN을 읽는 22 F1 90 요청을 보낸다.
make
./ecu_server
make
./tester_client
실습 2. 헤더 형식 손상#
Version 0x02와 Inverse 0xFD를 XOR 연산하면 0xFF가 나온다.
bad 모드(헤더를 의도적으로 잘못 만들어 보내는 모드)를 통해 NACK이 올바르게 응답하는지 확인할 수 있다. bad 모드는 앞서 연산한 Inverse 바이트를 0x00으로 바꿔 보낸다.
./tester_client bad
[bad] corrupted Inverse byte
-> DoIP NACK code=0x00
DoIP NACK 프레임 분석#
다음 DoIP NACK 프레임을 하단 표를 참고하여 분석할 수 있다.
02 FD 00 00 00 00 00 01 00
| 오프셋 | 바이트 | 필드 | 의미 |
|---|---|---|---|
| 0 ~ 1 | 02 FD |
Version / Inverse | 응답은 정상적인 헤더로 보냄 |
| 2 ~ 3 | 00 00 |
Payload Type | Generic Header NACK |
| 4 ~ 7 | 00 00 00 01 |
Payload Length | 1 byte |
| 8 | 00 |
NACK Code | 형식 오류 |
과제1. Payload Length 검증#
헤더에 적힌 길이와 실제로 받은 길이가 다르다면 프레임이 잘렸거나 조작된 것이다.
uint32_t plen = (buf[4]<<24)|(buf[5]<<16)|(buf[6]<<8)|buf[7];
uint32_t actual = (uint32_t)(n - 8);
// 헤더 Length와 실제 길이가 다르면 NACK로 거부하고 break
/* [[BLANK1]] */
라즈베리파이의 'ecu_server_blank.cpp'의 [BLANK1]을 채워 NACK로 거부하고 break하는 코드를 채워야 한다. 해당 코드는 다음과 같다.
if (plen != actual) {
send_doip_nack(conn, NACK_INVALID_LENGTH);
break;
}
[BLANK1]이 제대로 동작하는지 확인하기 위해 아래 스크립트를 실행하여 의도적으로 깨진 프레임을 보내보자.
./tester_client badlen
./tester_client short
}
과제2. 쓰기 서비스 0x2E 추가#
요청 프레임은 2E F1 90 뒤에 새 VIN이 이어지며, 15번째 바이트부터 끝까지가 새 VIN 값에 해당한다. [BLANK2]를 채워 해당 값을 저장한 후 긍정 응답으로 RSID와 DID(6E F1 90)만 반환해보자.
uint16_t did = (buf[13] << 8) | buf[14];
// DID가 F190이면 새 VIN을 저장하고 긍정 응답, 아니면 부정 응답 0x31
/* [[BLANK2]] */
예시답안#
[BLANK2]에 들어갈 코드는 다음과 같다.
if (did == 0xF190) {
vin.assign((char*)&buf[15], n - 15);
std::vector<uint8_t> uds = {rsid, 0xF1, 0x90};
send_positive(conn, uds);
} else {
send_negative(conn, sid, 0x31);
}
VIN을 읽고 → 새 값으로 쓰고 → 다시 읽어서 제대로 바뀌었는지 확인해 보자.
./tester_client f190
./tester_client write HELLOVIN123456789
./tester_client f190
실습 3. VIN 쓰기 보호#
VIN은 차량의 신원이기 때문에, 실제 ECU는 보안 인증을 거친 경우에만 쓰기를 허용한다. [BLANK3]를 채워보자.
uint16_t did = (buf[13] << 8) | buf[14];
/* [[BLANK3]] */
예시답안#
[BLANK3]에 들어갈 코드는 다음과 같다.
if (did == 0xF190) {
send_negative(conn, sid, 0x33);
continue;
}
다음 스크립트를 실행하여 VIN 보호가 제대로 구현됐는지 확인해 보자. 쓰기 시도가 거부되고, VIN이 바뀌지 않아야 한다.
./tester_client f190
./tester_client write HACKED9999999999
./tester_client f190