ANZEN 연구: 우리가 어떻게 USB 취약점을 통해 Keystone을 해킹했는가

주요 결과
• USB SDK가 호스트가 제어하는 길이에 대한 검증이 부족하여 고정 버퍼에서 범위를 벗어난 읽기/쓰기(OOB Read/Write)가 발생함
• 공격자는 USB class 콜백 테이블을 하이재킹하여 범위를 벗어난 쓰기를 MCU 임의 코드 실행(Arbitrary Code Execution)으로 전환할 수 있음
• 잘못된 MPU 구성으로 인해 SRAM 내의 shellcode를 직접 실행 가능함
• Passcode, OTP 및 보안 칩 내부의 암호화 자료를 확보한 후, AES Key를 재구성하여 BIP39 니모닉을 추출할 수 있음
• 서드파티 SDK 역시 하드웨어 지갑의 핵심 보안 경계에 해당하므로 반드시 포괄적인 감사가 이루어져야 함
보안 업데이트(2026년 7월): 본 문서에서 다루는 문제는 OneKey Anzen이 2026년 3월 Keystone과 공동 보안 감사를 진행할 때 발견한 것으로, MCU 공급업체의 USB SDK가 통합된 Keystone 3 Pro 펌웨어에 영향을 미칩니다. Keystone은 2026년 4월 1일 펌웨어 v2.4.0을 배포하여 해당 문제를 완전히 해결했습니다. 공동 공지 발표 시점까지 양측은 해당 취약점이 Keystone 사용자를 공격하는 데 사용되었다는 증거를 발견하지 못했습니다. 성공적인 악용을 위해서는 공격자가 기기를 물리적으로 소유하고, 올바른 PIN을 알고 있으며, 기기 잠금을 해제하고, USB 연결에 대한 사용자의 승인을 얻은 뒤 기기를 공격자가 제어하는 컴퓨터에 연결해야 합니다. v2.4.0 미만의 펌웨어를 실행 중인 사용자는 즉시 최신 버전으로 업그레이드해야 합니다. 에어갭 QR 서명을 사용하면 이 특정 USB 공격 경로를 피할 수 있으나, 업그레이드를 완료할 것을 권장합니다. 자세한 내용은 Keystone 및 OneKey Anzen 공동 보안 업데이트를 확인하십시오.
8월 초, 라스베이거스에서 2026년 Black Hat USA가 개최되었습니다. 비자 문제로 인해 저희의 발표 세션은 on-demand 형식으로 전환되었습니다. 이 발표 주제는 저희가 Keystone 지갑을 감사하던 중 칩 공급업체의 USB SDK에서 심각한 문제를 발견하고, 최종적으로 니모닉을 추출해 낸 사례에서 출발했습니다.
사실 USB 프로토콜 스택에 문제가 발생한 것은 이번이 처음이 아닙니다. 지난 10여 년 동안 수많은 장치에서 발생한 심각한 공격들이 USB 프로토콜 스택과 밀접하게 연관되어 있었습니다. 2014년의 USB 드라이버 취약점과 원격 리디렉션부터 Fusée Gelée, checkm8, Kamakiri, 그리고 2026년에 공개된 usbliter8에 이르기까지 다양합니다.
USB 프로토콜 취약점의 빈번한 발생
2014: USB 취약점은 정말 물리적 연결이 필수일까?
2014년 NCC Group은 Black Hat Asia에서 《USB Attacks Need Physical Access Right? Not Any More...》라는 주제로 발표했습니다.
당시 사람들은 USB 취약점을 주로 '로컬 물리적 공격'으로 분류하곤 했습니다. 즉, 공격자가 대상 기기에 악성 장치를 직접 연결하여 기형적인 descriptor나 class-specific data를 통해 드라이버 취약점을 유발해야 한다고 생각했습니다.
그러나 해당 발표에서 연구원들은 RemoteFX USB Redirection을 활용하여 저수준 USB 장치를 원격 Windows Server로 리디렉션했습니다. 악성 USB 장치는 물리적으로 공격자의 클라이언트에 연결되어 있지만, 실제로 악성 USB 데이터를 파싱하는 드라이버는 원격 서버에서 실행되었던 것입니다.
2018, Fusée Gelée: 요청 길이를 객체 길이로 오인
2018년에 공개된 Fusée Gelée는 USB BootROM 취약점 중 가장 클래식한 사례 중 하나입니다. 취약점은 NVIDIA Tegra의 Recovery Mode, 즉 RCM에 존재합니다. 장치는 극초기 BootROM 환경에서 USB 스택을 시작하고 호스트가 복구 이미지를 업로드하기를 기다립니다. 그런데 취약점은 겉보기에 평범해 보이는 GET_STATUS 제어 요청에서 발생했습니다.
USB 시맨틱에 따르면 status 객체는 2바이트에 불과합니다. 호스트는 wLength를 통해 자신이 수신하고자 하는 최대 데이터 크기를 알리며, 장치는 min(호스트 요청 길이, 실제 사용 가능한 데이터 길이)를 반환해야 합니다.
그러나 endpoint recipient를 처리할 때 BootROM은 다음과 유사한 잘못된 로직을 실행했습니다:
status = get_usb_endpoint_status(index);
/* 오류: 호스트가 제공한 요청 길이 사용 */
size_to_tx = setup_packet.length;
memcpy(dma_buffer, &status, size_to_tx);
즉, 2바이트 객체의 실제 길이는 사용되지 않고, 공격자가 제어하는 wLength가 그대로 memcpy의 길이가 되어 이론적으로 최대 65,535바이트에 달할 수 있었습니다.
그뿐만 아니라 RCM은 서명을 검증하기 전에 공격자가 대용량 payload를 메모리에 배치하도록 이미 허용하고 있었습니다. 그리고 해당 payload 영역은 공교롭게도 BootROM의 활성 스택 근처에 위치했습니다. 공격자는 DMA buffer를 선택하고 메모리 레이아웃을 구성함으로써, 범위 밖 복사(out-of-bounds copy)를 통해 공격자가 제어하는 데이터를 실행 스택에 덮어쓸 수 있었습니다.
당시의 BootROM 환경에는 일반적인 stack canary, ASLR 및 유효한 메모리 실행 보호 메커니즘이 없었습니다. 그 결과, 보안 잠금이나 권한 강하가 일어나기 전에 공격자는 이미 BootROM 컨텍스트에서 임의 코드 실행 권한을 획득하게 되었습니다.
이는 본문 뒤에서 다룰 하드웨어 지갑 취약점과 매우 직접적인 유사성을 가집니다.
2019, checkm8: 공격자가 제어하는 것은 데이터뿐만이 아니라 '미완료 상태'이기도 하다
2019년에 공개된 checkm8은 Apple SecureROM 취약점 중 가장 파급력이 컸던 사례 중 하나입니다. 여러 세대의 iOS 기기에 영향을 미쳤으며, 공격자가 DFU 모드에서 BootROM 컨텍스트의 코드 실행 권한을 얻어 SecureROM을 덤프하고, 펌웨어 keybag을 복호화하거나, 장치를 JTAG을 허용하는 demotion 상태로 전환할 수 있게 했습니다. 취약점이 칩 제조 시 고정된 BootROM에 존재하기 때문에 이미 출고된 장치는 iOS 업데이트를 통해 이 취약점을 해결할 수 없습니다.
Fusée Gelée와 달리 checkm8은 단순한 길이 검증 오류가 아닙니다. 그 핵심은 Apple DFU USB 상태 머신에서 발생한 use-after-free 취약점입니다. USB 데이터 단계가 정상적으로 종료되지 않아 데이터 단계의 상태를 저장하는 전역 변수가 정리되지 않았지만, 하위 DFU buffer는 USB 스택이 종료될 때 해제되었습니다.
정상적인 상황에서는 BootROM이 DATA 단계를 동반한 DFU_DNLOAD 요청을 수신하면 수신 buffer와 wLength를 저장합니다. 데이터 수신이 완료되면 완료 콜백이 해당 상태들을 정리하고, DFU를 종료할 때 해당하는 I/O buffer를 해제합니다.
DFU_DNLOAD(setup)
{
ep0_buffer = dfu_io_buffer;
ep0_remaining = setup.wLength;
}
DATA_complete()
{
ep0_buffer = NULL;
ep0_remaining = 0;
}
DFU_exit()
{
free(dfu_io_buffer);
}
checkm8이 이용한 것은 바로 이 단계들 사이의 틈새입니다. 공격자는 먼저 DATA 단계가 포함된 요청을 보내 BootROM이 buffer 포인터와 남은 길이를 기록하게 만든 뒤, 의도적으로 데이터를 끝까지 보내지 않습니다. 전송을 취소하거나 타임아웃을 유발함으로써 DATA 완료 콜백이 실행되지 않도록 하고, 이어서 DFU 상태 전환 또는 USB reset을 트리거하여 USB 스택이 종료되면서 기존 buffer를 해제하도록 만듭니다.
이때 메모리는 이미 해제되었지만 수신 상태에는 여전히 이전 포인터가 남아 있게 됩니다:
free(dfu_io_buffer);
ep0_buffer = old_dfu_io_buffer; // 댕글링 포인터
그다음 heap을 정리(heap feng shui)해야 합니다. 공개된 익스플로잇은 다수의 USB request 객체를 누수시켜 이후의 할당 위치를 변경함으로써, 새로운 DFU buffer가 다른 곳에 할당되게 하는 동시에 이전 주소가 usb_device_io_request와 같은 객체로 다시 채워지도록 유도합니다.
이후 다시 USB 데이터를 전송하면 BootROM은 여전히 남아 있는 ep0_buffer를 따라 데이터를 쓰게 되며, 실제로는 새로운 객체를 덮어쓰게 됩니다. 익스플로잇은 그중 callback, next 등의 필드를 수정하여 콜백 주소를 미리 준비해 둔 payload로 향하게 만듭니다. 다음 USB 요청이 완료될 때 SecureROM은 공격자의 코드로 분기합니다.
2020, Kamakiri: USB 요청이 어떻게 간접 점프로 변환되는가
Kamakiri는 여러 MediaTek SoC의 BootROM 다운로드 모드를 겨냥한 취약점입니다. 이 취약점은 인가되지 않은 Download Agent를 제한하는 기존 인증 경로를 우회하여 공격자가 BootROM 컨텍스트에서 서명되지 않은 1단계 payload를 실행할 수 있도록 합니다. 이 실행 능력을 확보하면 후속 payload를 통해 플래시 메모리를 조작하거나 메모리를 덤프하고, 부팅 상태를 수정하거나, 정상적으로 부팅되지 않던 장치를 복구할 수 있습니다.
Kamakiri의 익스플로잇 체인은 매우 짧습니다. MediaTek BootROM 자체가 공격자에게 필요한 첫 번째 기능인 다운로드 프로토콜을 통해 알려진 SRAM 영역에 데이터를 업로드하는 기능을 이미 제공하고 있기 때문입니다. 공격자는 1단계 payload를 주입한 뒤, 특수한 USB control request를 통해 취약점을 트리거합니다.
취약점은 정확히 USB control request의 디스패치 처리 로직에 위치합니다. 독립적인 리버스 엔지니어링 분석에 따르면 해당 로직은 대략 다음과 같은 형태를 띱니다:
handler = handler_array[value * 13];
handler();
핵심 문제는 USB 요청에서 전달된 선택 값이 충분한 범위 검사 없이 함수 포인터 테이블의 인덱스로 사용된다는 점입니다. 공격자가 적절한 제어 값을 찾아 테이블 항목을 읽은 결과가 앞서 업로드한 SRAM payload 주소와 일치하게 만들기만 하면, 하나의 USB 요청을 payload에 대한 간접 호출로 전환할 수 있습니다. SoC마다 BootROM 레이아웃이 완전히 동일하지는 않으므로 구체적인 인덱스 값과 payload 주소는 환경에 맞게 조정하거나 열거(enumeration)해야 할 수 있습니다.
공개된 PoC는 stage 1을 업로드한 후 다음과 같은 제어 전송(control transfer)을 보냅니다:
ctrl_transfer(
0xA1, # class request, device-to-host, interface recipient
0,
0,
10, # wIndex
0
)
2026, usbliter8: USB 컨트롤러가 DMA 쓰기 포인터를 buffer 앞으로 이동시키다
2026년 6월에 공개된 usbliter8은 소프트웨어 패치가 불가능한 BootROM 익스플로잇의 범위를 Apple A12, A13 및 S4, S5 SoC로 다시 한번 확장했습니다.
익스플로잇에 성공하면 공격자는 SecureROM의 EL1 컨텍스트에서 코드를 실행하여 DFU 동작을 수정하거나, 칩의 production mode를 일시적으로 demote하거나, 서명 검사를 우회하여 서명되지 않은 순정 iBoot를 부팅할 수 있습니다. 이는 애플리케이션 프로세서의 부팅 신뢰 체인(Chain of Trust)이 무너졌으며, 후속 iOS 업데이트로도 이를 해결할 수 없음을 의미합니다.
usbliter8은 앞선 사례들과 사뭇 다릅니다. 이는 주로 Synopsys DWC2 USB 컨트롤러 자체가 SETUP transaction을 처리할 때의 DMA 포인터 시맨틱을 이용합니다.
USB 프로토콜에 따르면 SETUP transaction의 데이터 부분은 8바이트로 고정되어 있습니다. DWC2 컨트롤러는 3개의 SETUP 패킷을 연속으로 수신할 수 있으며, DMA를 통해 이를 차례대로 메모리에 씁니다. 정상적인 SETUP 패킷 3개는 총 24바이트를 차지합니다.
네 번째 SETUP transaction이 도착하면 컨트롤러는 DOEPDMA 쓰기 포인터에서 고정적으로 24바이트를 차감하여 주소를 이 SETUP buffer 세트의 시작점으로 되돌립니다. 정상적인 8바이트 SETUP 패킷의 경우 이 로직은 완벽하게 맞아떨어집니다.
그러나 컨트롤러는 8바이트보다 짧은 비정상적인 SETUP 데이터도 수용하며, 실제 기록된 길이에 맞춰 DMA 포인터를 전진시킵니다. 내부 쓰기 단위는 4바이트입니다. 공격자가 4바이트 크기의 숏 패킷을 반복 전송할 경우, 앞선 세 패킷은 주소를 총 12바이트만 전진시키지만, 네 번째 패킷이 올 때 컨트롤러는 여전히 고정적으로 24바이트를 되돌립니다. 따라서 다음 차례의 SETUP 데이터는 원래 buffer에 쓰이지 않고 buffer 시작점보다 12바이트 앞선 위치에 쓰이게 됩니다. 이 과정을 계속 반복하면 DMA 쓰기 위치가 예측 가능한 간격으로 하위 주소를 향해 이동하며, 결국 안정적인 buffer underflow가 형성됩니다.
일반적으로 USB 컨트롤러의 범위를 벗어난 DMA가 임의의 시스템 메모리를 손상시킬 수 있는 것은 아닙니다. 최신 SoC는 보통 IOMMU를 통해 USB 컨트롤러가 접근할 수 있는 주소 범위를 제한합니다. 그러나 A12 및 A13의 SecureROM 환경에서는 USB DART가 bypass 상태로 동작하므로 컨트롤러의 DMA에 유효한 주소 격리가 결여되어 있었습니다. 이로 인해 underflow는 특정 좁은 SETUP buffer에 국한되지 않고 다른 SRAM 객체까지 계속해서 덮어쓸 수 있게 되었습니다.
A12에서는 USB DMA buffer가 USB task의 스택과 인접해 있어, 저장된 link register를 덮어쓰고 태스크 전환 시 프로그램 카운터(PC) 제어권을 획득할 수 있습니다. A13에는 Pointer Authentication이 도입되어 반환 주소를 직접 덮어쓰는 것이 어려웠기 때문에, 공개된 익스플로잇은 더 정교한 메모리 손상 체인을 채택하여 최종적으로 BSS의 USB interrupt handler 포인터를 변조하고 다음 USB 인터럽트를 통해 공격자 코드로 진입하도록 했습니다. 두 경로 모두 최종적으로 SecureROM EL1 실행 권한을 획득합니다.
USB 경계 외 읽기/쓰기에서 임의 실행까지
이번 Black Hat USA에서 공유한 취약점의 근본 원인 역시 USB SDK에 있었습니다.
CDC의 line-coding 요청에는 아주 작은 고정 구조체만 필요하지만, 통합된 USB SDK는 이를 위해 고정된 8바이트 크기의 CmdBuff를 할당한 뒤 호스트가 제공한 req->wLength를 하위 전송 또는 수신 함수에 그대로 전달했습니다. 그러나 코드에는 길이 검사가 없었으며, 특정 요청에 따라 프로토콜이 규정한 고정 길이와 일치하는지 요구하지도 않았습니다. 그 결과 범위를 벗어난 읽기와 쓰기가 발생했습니다.
USB SDK 경계 외 읽기/쓰기 취약점 코드
물론 범위를 벗어난 읽기/쓰기를 확보한 것은 첫 단계에 불과하며, 메모리 손상 프리미티브를 얻은 상태일 뿐 최종 목표는 여전히 임의 실행 능력을 확보하는 것이었습니다.
취약점이 USB CDC 경로에서 발생했기 때문에, 저는 먼저 현재 요청의 디스패치 흐름을 따라 USB 프로토콜 스택을 점검했습니다. g_usbDev를 분석하던 중 그 안의 dev.class_cb에 주목하게 되었습니다. 이는 일반적인 데이터 포인터가 아니라 USB class driver의 런타임 콜백 테이블입니다. USB core는 요청을 처리할 때 이 테이블을 통해 Setup, DataIn, DataOut 등의 함수를 호출합니다. class_cb를 변조할 수 있다면 이후 정상적인 USB 요청 하나가 우리가 제어하는 함수 포인터를 거치게 되므로, 하이재킹하기에 매우 적합한 대상이었습니다.
전체 익스플로잇은 두 번의 USB 요청으로 나눌 수 있습니다.
첫 번째 요청은 SET_LINE_CODING을 사용하여 범위를 벗어난 쓰기를 트리거합니다. 이 쓰기 과정에서 세 가지 작업을 동시에 완료해야 합니다: shellcode를 CmdBuff에 배치하고, 인접한 SRAM에 위조된 콜백 테이블을 구성하며, 마지막으로 class_cb를 이 위조된 테이블의 주소로 변경하는 것입니다.
그 후 일반적인 GET_LINE_CODING 요청을 다시 전송합니다. USB core는 평소처럼 이 요청을 처리하고 class_cb->Setup(...)을 통해 class driver의 Setup 콜백을 호출합니다. 차이점은 이때 class_cb가 더 이상 원래 콜백 테이블을 가리키지 않고 우리가 위조한 테이블을 가리킨다는 것입니다. 위조된 Setup은 다시 CmdBuff 내부의 shellcode를 가리킵니다. 따라서 두 번째 USB 요청은 PC를 우리가 SRAM에 작성한 코드로 곧바로 유도하게 됩니다.
shellcode가 SRAM에 위치하기 때문에 본래는 데이터 영역의 실행 불가 문제를 고려해야 했습니다. 그러나 펌웨어가 MPU를 구성할 때 잘못된 크기 포맷을 사용하여 최종 보호 범위가 0으로 설정되었고, 원래 BSS의 실행 권한을 제한하려던 MPU region이 실제로는 작동하지 않았습니다. 따라서 CmdBuff에 위치한 이 코드는 직접 실행될 수 있었습니다.
USB 콜백 테이블 하이재킹을 통한 shellcode 실행
이론적 타당성을 확인한 후 실제 작업에 착수했습니다. 먼저 실제 범위를 벗어난 읽기/쓰기 범위를 테스트했습니다. 64바이트 크기의 제어 전송을 구성한 결과, 범위 밖 읽기를 통해 약 24 KB의 SRAM을 지속적으로 유출할 수 있었지만, 4,352바이트 위치까지 범위를 벗어나 쓸 경우 안정적으로 크래시가 발생했습니다.
이후 확인해 보니 해당 위치에서 범위 밖 데이터가 CmdBuff + 0x1130 위치에서 여전히 사용 중이던 포인터를 덮어썼고, 그로 인해 EP0가 후속 데이터를 계속 수신하는 능력을 상실했던 것입니다.
범위를 벗어난 쓰기로 인해 장치 크래시가 발생하는 위치
이 문제의 해결 방법 역시 비교적 간단했습니다. 이미 범위를 벗어난 읽기 능력을 확보했으므로 실제 상태를 직접 읽어 들인 뒤, 덤프한 상태 데이터를 shellcode에 로드하면 되었습니다. 이로써 완벽한 임의 코드 실행 로직 세트가 구성되었습니다.
이제 class_cb를 덮어쓸 수 있게 되었지만, 이번 하이재킹으로 인해 USB 통신 채널까지 망가지는 것은 원치 않았습니다. 긴 쓰기(long write)를 통해 먼저 shellcode와 위조 콜백 테이블을 SRAM에 배치하고 일시적으로 class_cb가 위조 테이블을 가리키도록 만듭니다. 이후 GET_LINE_CODING을 전송하면 USB core는 위조된 Setup을 거쳐 shellcode로 점프합니다. 작업이 완료되면 shellcode는 결과를 CmdBuff 뒤쪽에 기록하고 원래의 class_cb를 복원한 뒤, 원래의 Setup을 호출하여 이번 EP0 전송이 정상적으로 종료되도록 합니다. 이렇게 하면 매 하이재킹이 짧은 우회로(detour)에 불과하여 USB가 계속 사용 가능한 상태로 유지되고, 다음 작업을 지속적으로 주입할 수 있습니다.
펌웨어는 원래 MPU를 통해 BSS를 실행 불가로 설정하려고 시도했으나, 설정 코드에서 잘못된 크기 포맷을 사용하는 바람에 최종 계산된 보호 범위가 0이 되었습니다.
MCU 임의 실행에서 니모닉 추출까지
MCU 임의 실행 권한을 얻었다고 해서 곧바로 니모닉을 추출할 수 있는 것은 아닙니다. 하드웨어 지갑의 구체적인 구조는 다음과 같습니다. 요약하자면 니모닉의 암호화 정보는 DS28S60 SE에 저장되어 있으며, 이 정보를 복호화하기 위한 AES KEY는 OTP와 ATECC608, DS28S60이라는 두 개의 SE가 각각 관여하여 생성됩니다.
하드웨어 지갑 보안 아키텍처
이 AES KEY의 생성 과정은 다음과 같습니다:
AES Key 생성 흐름
SE와의 상호작용 및 AES 생성에 관여하는 자격 증명은 주로 지갑의 Passcode(잠금 해제 비밀번호)와 OTP를 통해 생성되며, 그 과정은 다음과 같습니다:
Passcode 및 OTP 자격 증명 생성 흐름
Passcode는 비밀번호 해시를 획득한 후 브루트포스(무차별 대입 공격)를 통해 알아낼 수 있으므로, 최종적으로 OTP의 salt만 확보하면 AES key 생성을 완료할 수 있습니다. OTP 추출의 경우, shellcode가 일반 펌웨어와 동일한 특권 레벨에서 실행되므로 USB 취약점을 통해 MPU 레지스터에 값을 써서 MPU를 비활성화한 뒤 OTP를 읽어올 수 있습니다.
최종 익스플로잇 방법은 다음과 같으며, AES KEY를 획득한 후 복호화하여 BIP39 entropy를 얻고 니모닉을 재구성할 수 있습니다.
USB 취약점부터 니모닉 추출까지의 전체 공격 체인
요약
이번 연구 과정에서는 취약점 분석 및 익스플로잇 개발 과정에 AI가 대거 활용되었습니다. AI의 등장은 보안 연구원의 공격 분석 효율을 대폭 향상시켰습니다. 또한 AI는 이러한 취약점 연구 비용을 변화시키고 있습니다. 과거에는 취약점을 발견하고 안정적인 익스플로잇 단계까지 도달하는 데 수많은 수작업 시간이 소요되었으나, 오늘날에는 코드 이해, 구조 매칭, 스크립트 생성 및 실패 원인 분석 등이 눈에 띄게 빨라졌습니다.
그리고 USB SDK든 기타 서드파티 구성 요소든 제품 보안 검토에서 간과되기 쉬운 영역이며, 종종 이러한 취약한 단 한 곳의 결함으로 인해 제품 전체의 보안이 무너지게 됩니다.






