
CH55x 마이크로프로세서용 펌웨어 추출기
CH55x 펌웨어 추출기는 CH55x 집적 회로에서 펌웨어를 읽어내는 데 사용됩니다. 이 장치들은 부트로더를 통해 펌웨어를 직접 읽어내는 기능을 기본적으로 지원하지 않습니다. 그러나 제공된 데이터와 8바이트씩 펌웨어 내용을 검증하는 기능이 있습니다. 이 기능은 전형적인 타이밍 공격에 취약하며, 이를 이용하여 이 장치들에서 펌웨어를 추출합니다. 부트로더는 USB 또는 UART를 통해 접근할 수 있습니다. 이 펌웨어 추출기는 USB의 높은 지연 시간 때문에 공격이 어려우므로 UART를 통해서만 작동합니다. 테스트된 칩은 CH552와 CH554이며, 부트로더 버전은 2.4와 2.5입니다. 펌웨어 추출기의 하드웨어 솔루션은 STM32 Blue Pill을 기반으로 합니다. 그 이유는 쉽게 구할 수 있고 저렴하며 필요한 성능을 갖추고 있기 때문입니다.
부트로더는 이전에 장치에서 읽혀졌으며, 그 통신 프로토콜이 리버스 엔지니어링되었습니다. 아래는 부트로더에서 사용되는 대략적으로 올바른 검증 명령어와 검증 함수입니다. 몇 가지 보호 장치가 마련되어 있어, 길이가 8의 배수여야 하고, 주소가 8바이트 경계에 정렬되어야 하며, 주소가 0x3800 미만이어야 하고, 이전 검증 오류가 없어야 합니다. 마지막 보호 장치는 검증에 실패할 때마다 CH55x를 재시작해야 함을 의미합니다.
검증 함수는 검증이 실패하는 즉시 반환되는 것을 볼 수 있습니다. 즉, 올바른 바이트가 많을수록 검증 함수가 더 오래 걸립니다. 이것은 전형적인 타이밍 공격의 예시이며, 이를 이용할 수 있습니다.
unsigned char verifycmd[] = {
// 0x57, 0xab, // UART magic not included to verify function
0xa6, // Verify command
5 + len, // Constant 5 plus length of data to verify
0, // Unused
addr_low, // Low byte of address
addr_high, // High byte of address
0, 0, 0, // Unused
0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, // Data to verify against
checksum
}
unsigned char verify(unsignec char *cmdbuffer)
{
static char prev_verify_error;
unsigned char len = cmdbuffer[1]-5
unsigned short addr = cmdbuffer[3] + cmdbuffer[4] << 8;
if (len & 0x07 || addr & 0x07 || addr > 0x3800 || prev_verify_error) {
return 0xfe;
}
for (int i=0; i < len; i++) {
// Key can be set through bootloader, and CBYTE[] means code memory
if(key[i & 0x07] ^ cmdbuffer[8+i] ^ CBYTE[addr+i]) {
prev_verify_error = 1;
return 0xf5;
}
}
return 0;
}
시행착오를 통해 각 올바른 바이트가 검증 함수의 실행 시간을 약 4.2 µs 연장한다는 것을 발견했습니다. 부트로더가 UART 통신에 사용하는 baud rate는 57600입니다 (다른 곳에서 읽은 내용과 관계없이). 이는 1비트 전송에 약 17.4 µs가 소요된다는 의미입니다. 이 두 시간 간의 관계는 중요합니다. 응답이 약 17 µs의 지터로 전송되기 때문입니다. 그런 다음 검증 함수의 응답 시간이 4.2 µs 차이가 나지만 UART 지터 (클록 타이밍)로 인해 최대 17 µs까지 차이가 날 수 있는 응답을 구별하려고 합니다. 이는 어려운 작업처럼 보이지만 통계적 방법을 통해 가능합니다.
바이트가 올바른지 여부는 여러 번 검증을 시도하고 결과를 기록함으로써 결정할 수 있습니다. 예를 들어 첫 번째 바이트가 틀린 경우 응답을 받는 가장 짧은 시간이 30 µs라고 가정해 봅시다. 그러면 UART 지터로 인해 잘못된 첫 번째 바이트의 최대 응답 시간은 30 µs + 17.4 µs = 47.4 µs가 될 것으로 예상할 수 있습니다. 동시에 올바른 첫 번째 바이트와 잘못된 두 번째 바이트는 이 '범위'를 34.2 µs에서 51.6 µs로 밀어 올립니다. 약간의 마진을 추가하면 응답 시간이 약 33 µs보다 짧은 경우 첫 번째 문자가 잘못된 것이라고 결론 내릴 수 있습니다. 유사하게 응답 시간이 48 µs보다 높은 경우 첫 번째 문자가 올바른 것이라고 말할 수 있습니다. 이것이 펌웨어 추출에 사용되는 타이밍 공격의 기초입니다.
이것은 완전 자동화된 펌웨어 추출 도구가 아닙니다. 소스 코드 수정 및 재컴파일이 필요합니다 (VS Code 및 PlatformIO 사용). 주된 이유는 각 바이트의 정확한 타이밍 특성이 설정마다 다르며 조정이 필요하기 때문입니다. 조정 과정은 자동화될 수 있지만, 이것이 이 프로젝트의 목표는 아니었습니다. 주요 조정은 prober_limits 변수를 통해 이루어집니다. 예를 들어, prober_limits[0]은 검증할 8바이트 중 바이트 0에 대한 한계를 포함합니다. 응답 시간이 .invalid_under_time 미만이면 바이트가 잘못된 것임을 알 수 있습니다. .valid_over_time을 초과하면 바이트가 올바른 것임을 알 수 있습니다. 또한 .min_delta가 있어 바이트가 올바른지 확실히 알기 전에 다음 바이트로 진행할 수 있습니다.
struct ProberByteLimits prober_limits[8] = {
{
// Byte 0
.invalid_under_time = 33,
.valid_over_time = 50,
.min_delta = 30
}, // ...
사용할 적절한 값을 찾으려면 .invalid_under_time을 0으로, .valid_over_time을 예를 들어 100으로 설정하고, .min_delta는 30으로 유지하는 것이 좋습니다. 이는 프로버가 첫 번째 올바른 바이트를 찾지 못하지만, 진행 상황이 호스트 PC UART로 출력된다는 의미입니다. 다음과 유사한 내용이 표시됩니다:
[0x0000]=0x01? min=31 max=47 tries=63
min=31 max=48 tries=127
min=31 max=48 tries=191
min=30 max=48 tries=255
min=30 max=48 tries=319
테스트된 첫 번째 문자가 잘못된 경우, 이러한 최소/최대 값을 1 또는 2의 오프셋으로 사용하여 적절한 한계를 결정할 수 있습니다. 위의 예에서 .invalid_under_time을 33으로, .valid_over_time을 50으로 설정할 수 있습니다. 그러면 프로버는 유효한 값을 찾을 때까지 다른 값들을 시도하며, 아래와 같이 보일 것입니다:
...
[0x0000]=0x7d? min=31 max=39 tries= 7
[0x0000]=0x02? min=40 max=56 tries= 7
[0x0001]=0x01? min=35 max=52 tries=34
주소 0x0000에서 마지막 시도의 최대 응답 시간이 56 µs였으며, 이는 올바른 바이트임을 의미합니다. 그러면 프로버는 다음 바이트로 진행하여 과정을 계속합니다. 모든 8바이트 한계는 별도로 조정되어야 하지만, 한 번 조정되면 전체 메모리에 대해 작동합니다. 8개의 올바른 바이트가 발견되면 ihex 형식으로 출력됩니다:
:0800000002002932ffffffff9f
UART 출력을 텍스트 파일로 기록하면 ^:를 grep하여 ihex 형식의 전체 메모리 내용을 추출할 수 있습니다.
펌웨어 추출을 위한 예시 회로는 아래에 나와 있습니다. 두 개의 트랜지스터를 사용하여 Blue Pill에서 CH55x로의 전원을 차단할 수 있습니다. 이는 소프트웨어 리셋만 사용하는 것보다 권장됩니다. 소프트웨어 리셋은 CH55x가 부트로더 모드일 때만 작동하며 (부트로더에는 제한 시간이 있어 애플리케이션 코드가 시작됨), UART에 연결된 저항은 CH55x의 내부 풀업이 전원이 꺼져 있을 때도 UART를 통해 전원을 공급할 수 있다는 의심 때문에 포함되었습니다. V33과 P3.6 사이의 10k 저항은 CH55x를 부트로더 모드로 전환하는 데 필요합니다. Blue Pill에서 PA11은 UART RX 핀에 연결되어 타이머 1을 사용하여 응답 시간을 측정할 수 있습니다.

이 도구를 조정하고 사용함으로써 CH55X 장치의 펌웨어를 추출할 수 있습니다. 추출 과정은 빠르지 않지만, 14KB를 하루나 이틀 안에 추출합니다. 조정 정도와 사용된 주파수 테이블이 실제 펌웨어 어셈블러와 얼마나 잘 일치하는지에 따라 다릅니다. 안타깝게도 소스 코드는 다소 엉망입니다. CH554 장치의 펌웨어가 필요했기 때문에 이 도구를 만들었고, 이제 그것을 얻었으므로 도구 자체를 개선할 실질적인 이유가 없습니다. 그럼에도 불구하고 누군가 CH55x 장치에서 펌웨어를 추출해야 할 경우 유용할 것입니다.