한눈에 보기
- eFuse는 칩 다이 안에 있는 한 번만 쓸 수 있는 비트
- 보드 위 부품이 아니라 SoC 실리콘 안의 작은 회로 블록(eFuse 매크로)
- 큰 전류로 가는 연결부를 끊어 0을 1로 바꾸고, 되돌릴 수 없음
- 용량은 수백 바이트 수준. ESP32는 모두 1024비트(128바이트), ESP32-C3는 4096비트 중 사용자 영역이 최대 1792비트(224바이트)라 256비트(32바이트) 키를 여러 개 담을 수 있음
- IoT 기기가 펌웨어를 암호화하면 키를 둘 곳이 필요함
- 펌웨어는 보드 위 외부 플래시에 있어 누구나 읽을 수 있음
- 키는 칩 안에, 칩마다 다르게, 바꿀 수 없게, 소프트웨어가 못 읽게 둬야 함. eFuse가 이 조건을 맞춤
- eFuse가 펌웨어 보호에서 맡는 일은 다섯 가지
- 암호화 키 저장, 서명 검증용 공개키 해시 저장, 장치 고유 키, 디버그 잠금, 롤백 방지 카운터
- ESP32는 eFuse에 플래시 암호화 키를 직접 구움
- 하드웨어가 플래시를 읽을 때마다 투명하게 복호화
- 시큐어 부트용 공개키 해시도 eFuse에 두어 신뢰의 출발점으로 씀
- eFuse도 깨질 수 있음
- 부팅 직후 eFuse를 읽는 순간에 결함을 주입하거나, 칩을 열어 셀을 직접 판독
- 그래서 모든 기기에 같은 키를 굽지 말고 기기마다 다른 키를 써야 함
왜 칩 안에 한 번만 쓰는 비트가 필요한가
공유기나 스마트 플러그 같은 IoT 기기를 뜯어 보면, 보드 가운데에 SoC(System on Chip, 프로세서와 주변 장치를 칩 하나에 담은 것)가 있고 그 옆에 다리 8개짜리 작은 칩이 붙어 있는 경우가 많다. 이 작은 칩이 SPI(Serial Peripheral Interface) 플래시이고, 펌웨어는 여기에 들어 있다.
덮개를 벗긴 ESP-WROOM-32 모듈. 가운데가 ESP32(SoC), 오른쪽 8핀 칩이 SPI 플래시다. 사진: Brian Krent, Wikimedia Commons, CC BY-SA 4.0
문제는 이 플래시를 누구나 읽을 수 있다는 점이다. 집게처럼 생긴 테스트 클립을 칩 다리에 물리고 값싼 플래시 리더에 연결하면, 몇 분 만에 펌웨어 전체가 PC로 들어온다. 그 안에는 하드코딩된 비밀번호, 클라우드 API 키, 인증서 개인키가 그대로 들어 있고, 취약점을 찾을 실행 파일도 함께 손에 들어온다.
그럼 펌웨어를 암호화해 두면 되지 않을까? 맞는 말이다. 그런데 암호화하는 순간 새 질문이 생긴다. 복호화할 키는 어디에 둘 것인가. 키를 같은 플래시에 두면 자물쇠 바로 옆에 열쇠를 걸어 두는 셈이다. 키를 둘 자리는 적어도 다섯 가지 조건을 만족해야 한다.
| 조건 | 이유 |
|---|---|
| 칩 안에 있다 | 칩 밖(보드 위)에 있으면 플래시처럼 읽힌다 |
| 비휘발성이어야 한다 | 부팅할 때마다 키가 필요하다 |
| 칩마다 다른 값을 넣을 수 있다 | 칩 하나에서 키가 새도 나머지 기기는 안전해야 한다 |
| 출고 뒤 바꿀 수 없다 | 공격자가 자기 키로 바꿔 넣으면 안 된다 |
| 소프트웨어가 못 읽게 할 수 있다 | 펌웨어에 버그가 있어도 키는 새지 않아야 한다 |
이 조건을 모두 만족하는 저장소가 OTP(One-Time Programmable) 메모리다. 이름 그대로 한 번만 쓸 수 있는 메모리로, 일회용 비밀번호(One-Time Password)와는 다른 말이다. 볼펜으로 쓴 글씨처럼 한 번 쓰면 지울 수 없고, 칩을 다 만든 뒤에 칩마다 다른 값을 쓸 수 있다. eFuse는 이런 OTP를 만드는 가장 흔한 방법이다.
OTP 메모리의 종류
칩 안에 한 번만 쓰는 비트를 만드는 방법은 여러 가지다. 이름이 뒤섞여 쓰이는 일이 많아서, 본론에 들어가기 전에 한 번 정리해 두자.
| 종류 | 쓰는 방법 | 쓰는 시점 | 특징 |
|---|---|---|---|
| 마스크 ROM | 제조 마스크에 배선을 새김 | 설계 때 | 모든 칩이 같은 값. 부트 ROM 코드에 쓴다 |
| 레이저 퓨즈 | 레이저로 금속 배선을 태워 끊음 | 웨이퍼 시험 때 | 장비가 따로 필요하고 패키징 뒤에는 못 쓴다. 위에서 보면 끊긴 자리가 보인다 |
| eFuse(전기 퓨즈) | 큰 전류로 가는 연결부를 끊음 | 패키징 뒤, 현장에서도 | 칩 안 회로로 쓴다. 이 글의 주제 |
| 안티퓨즈 | 높은 전압으로 얇은 산화막을 뚫어 이음 | 패키징 뒤, 현장에서도 | eFuse와 반대 방향. 셀이 작고 겉으로 잘 안 보인다 |
현장에서는 칩 안에서 전기로 쓰는 OTP를 통틀어 그냥 “eFuse”라고 부르는 경우가 많다. 데이터시트에 “eFuse”라고 적혀 있어도 실제 셀은 안티퓨즈일 수 있다. 이 글도 둘을 굳이 구분할 필요가 없을 때는 eFuse라고 부른다.
전기 퓨즈: 일렉트로마이그레이션으로 끊는다
eFuse의 출발점은 보안이 아니라 수리였다. IBM이 2004년 발표하며 처음 상용화했는데, 목적은 칩 안 캐시 메모리의 불량 셀을 살리는 것이었다. 시험에서 불량 셀이 나오면 퓨즈를 끊어 예비 행으로 바꿔 연결한다. 그전까지는 레이저로 끊어야 했지만, eFuse는 패키징까지 끝난 칩에서도 칩 안 회로만으로 끊을 수 있었다.
그렇다면 칩 안의 퓨즈는 어떻게 끊을까? 원리는 일렉트로마이그레이션(electromigration)이다. 도체에 전류가 아주 세게 흐르면 전자가 금속 원자를 떠밀어 옮긴다. 세찬 물살이 강바닥의 모래를 쓸어 가는 것과 비슷하다. 보통 칩 설계에서는 배선을 망가뜨리는 고장 원인이라 애써 피하는 현상인데, eFuse는 이것을 일부러 일으킨다.
전형적인 eFuse 셀은 트랜지스터 게이트를 만드는 폴리실리콘 층에 있다. 양쪽에 넓은 단자(캐소드, 애노드)가 있고 가운데를 좁은 연결부가 잇는, 모래시계의 잘록한 허리 같은 모양이다. 폴리실리콘 위에는 저항을 낮추려고 금속과 실리콘의 화합물인 실리사이드 층이 얇게 덮여 있다. 비트 하나를 쓰는 과정을 따라가 보자.
- 쓰기 전: 전류가 실리사이드로 흘러 저항이 수백 Ω 정도다.
- 쓸 때: 프로그래밍 트랜지스터를 켜서 연결부에 10mA 안팎의 전류를 짧게 흘린다. 좁은 연결부에서 전류 밀도가 높아져 실리사이드가 애노드 쪽으로 밀려난다.
- 쓴 뒤: 실리사이드가 사라진 구간은 저항이 높은 폴리실리콘만 남아, 저항이 수 kΩ 이상으로 뛴다(US 7902903).
“퓨즈를 끊는다”고 하지만 펑 하고 터뜨리는 것은 아니다. 연결부가 물리적으로 터지면 파편이 주변 회로를 망가뜨리기 때문에, 저항이 확실히 올라갈 만큼만 조절해서 전류를 흘린다. 읽을 때는 감지 증폭기(sense amplifier)가 이 저항을 기준 저항과 비교해 0과 1로 판정한다. 끊긴 정도가 셀마다 조금씩 다르고 온도에 따라 저항도 변하므로, 기준값은 두 상태 사이에 넉넉하게 잡아야 한다.
공정이 미세해지면서 재료도 바뀌었다. 45~28nm 무렵부터 쓰인 고유전율 금속 게이트(High-K Metal Gate, HKMG) 공정에서는 게이트가 폴리실리콘이 아니라 금속이라, 금속 배선을 같은 원리로 끊는 금속 eFuse나 안티퓨즈를 많이 쓴다.
안티퓨즈: 산화막을 뚫어 잇는다
안티퓨즈는 이름처럼 퓨즈와 정반대로 동작한다. 처음에는 끊겨 있다가, 쓰면 이어진다.
셀 구조는 트랜지스터와 거의 같다. 게이트와 기판 사이에 수 nm 두께의 얇은 산화막이 있어 평소에는 전류가 흐르지 않는다. 여기에 평소보다 훨씬 높은 전압을 걸면 산화막이 깨지면서 전류가 지나갈 길이 생긴다. 얇은 벽에 바늘구멍을 뚫는 셈이고, 한 번 뚫린 구멍은 다시 막을 수 없다.
안티퓨즈는 셀 하나가 트랜지스터 한두 개 크기라 같은 면적에 비트를 더 많이 담을 수 있다. 게다가 뚫린 자리가 산화막 속 아주 작은 점이라, 현미경으로 위에서 내려다봐도 쓴 셀과 안 쓴 셀이 잘 구분되지 않는다. 물리적 판독에 eFuse보다 강하다고 보는 이유다.
eFuse는 칩의 어디에 있나
이름 때문에 보드 위 어딘가에 퓨즈 부품이 붙어 있을 것 같지만, eFuse는 SoC 다이(패키지 안에 든 실리콘 조각) 안에 다른 회로와 함께 만들어져 있다. 보드를 아무리 들여다봐도 보이지 않는 게 당연하다. 위치를 보려면 칩 패키지를 열고 다이를 현미경으로 봐야 한다.
[NOTE]
보드 위에 “eFuse”라고 적힌 부품은 전혀 다른 물건이다. 과전류가 흐르면 전원을 끊었다가 다시 이어 주는 전원 보호 칩으로, 이 글의 OTP와는 관계가 없다.
eFuse 매크로
칩 설계에서 eFuse는 보통 eFuse 매크로라는 사각형 블록 하나로 들어간다. 매크로는 파운드리나 IP 업체(Synopsys, eMemory 등)가 공정에 맞춰 미리 만들어 둔 회로 블록이다. 칩 설계자는 레고 블록을 고르듯 크기와 비트 수를 골라 다이 위 빈자리에 배치한다. 매크로 안에는 네 가지가 들어 있다.
- 셀 배열: 퓨즈 셀이 행과 열로 놓인 부분. 용량은 칩에 따라 수백 비트에서 수십 킬로비트까지다. 예를 들어 ESP32는 1024비트, ESP32-C3는 4096비트다.
- 프로그래밍 회로: 셀마다 붙은 큰 트랜지스터와 행·열 선택 회로. 한 번에 한 비트씩 큰 전류를 흘린다. 큰 전류가 지나가야 해서 배선이 굵고, 그만큼 면적을 차지한다.
- 감지 증폭기: 셀의 저항을 읽어 0과 1로 판정한다.
- 전원: 쓸 때는 읽을 때보다 높은 전압이 필요하다. 칩에 따라 프로그래밍 전용 전원 핀을 따로 두고, 공장에서 쓸 때만 전압을 넣는다. 전원 핀을 연결하지 않은 보드에서는 eFuse를 쓸 수 없으므로 그 자체가 보호 장치가 된다.
전체 다이에 비하면 면적이 아주 작아서 다이 사진에서 찾기가 쉽지 않다. 같은 무늬가 반복되는 작은 사각형 배열과, 그 옆을 지나는 굵은 배선이 단서다.
읽기: 리셋 때 한 번, 섀도 레지스터로
CPU는 키나 설정이 필요할 때마다 eFuse 셀을 직접 읽을까? 그렇지 않다. 감지 증폭기로 저항을 판정하는 일은 느리고, 읽는 회로를 칩 곳곳에 퍼뜨릴 수도 없다.
대신 출근해서 그날 일정을 한 번 확인하고 책상에 메모로 붙여 두는 것과 비슷한 방법을 쓴다. 칩에 전원이 들어오고 리셋이 풀리는 순간, eFuse 컨트롤러가 매크로 전체를 한 번 읽어 섀도 레지스터(shadow register)라는 일반 플립플롭에 담아 둔다. 이후로 칩의 다른 회로와 소프트웨어는 이 메모만 본다. 부트 ROM이 보안 부팅이 켜져 있는지 확인할 때도, 암호 엔진이 키를 가져갈 때도 섀도 레지스터에서 가져간다.
편리한 구조지만 약점도 여기서 생긴다. eFuse의 보안 기능이 모두 리셋 직후 한 번 읽는 순간에 달려 있기 때문이다. 이 순간을 방해해 메모를 잘못 적게 만들면, 칩은 전원이 꺼질 때까지 그 잘못된 메모를 믿는다. 결함 주입 공격이 노리는 지점이 바로 여기다.
쓰기 규칙: 0에서 1로만, 그리고 보호 비트
eFuse 비트는 0(끊기 전)에서 1(끊은 뒤)로만 바뀐다. 끊은 퓨즈를 다시 이을 수는 없으니 당연하다. 그럼 숫자는 어떻게 셀까?
보통의 2진수로 3(011)을 쓴 뒤 4(100)로 올리려면 1이던 비트를 0으로 되돌려야 한다. eFuse에서는 불가능한 일이다. 그래서 eFuse 카운터는 손가락을 하나씩 펴 가며 세듯, 켜진 비트 수로 값을 센다.
값 0 0000000
값 1 0000001
값 2 0000011
값 3 0000111값을 하나 올릴 때마다 비트를 하나 더 끊기만 하면 되니 0을 1로 바꾸는 일만으로 셀 수 있다. 대신 비트 수만큼만 셀 수 있다.
같은 성질 때문에 이미 쓴 값도 안심할 수 없다. 키를 구운 뒤 누군가 남은 0 비트를 더 끊으면 다른 키가 되어 버린다. 이렇게 값이 바뀌지 않도록 eFuse 영역에는 데이터 비트와 함께 보호 비트를 둔다. 보호 비트도 eFuse라서 한 번 켜면 끌 수 없다.
- 쓰기 금지: 해당 블록의 남은 0 비트를 더는 끊을 수 없게 한다. 공격자가 키나 설정에 비트를 더 끊어 값을 바꾸는 것을 막는다.
- 읽기 금지: 소프트웨어가 해당 블록을 읽으면 0이나 의미 없는 값을 돌려준다. 값 자체는 섀도 레지스터에 있지만, 그 값은 내부 버스로 나가지 않고 전용 배선을 통해 암호 엔진에만 간다.
키를 eFuse에 두는 가장 큰 이유가 이 읽기 금지다. 금고 안에서만 쓸 수 있는 열쇠라고 생각하면 된다. 펌웨어에 임의 코드 실행 취약점이 있어 공격자가 CPU를 장악해도, CPU는 열쇠를 꺼내 볼 수 없고 금고(암호 엔진)에 복호화를 맡길 수 있을 뿐이다.
eFuse 비트가 잘못 끊기거나 잘못 읽힐 때를 대비해, 블록마다 오류 검사 부호를 함께 쓰는 칩도 있다. ESP32는 3/4 부호화를, ESP32-C3 같은 후속 칩은 리드-솔로몬 부호를 쓴다.
펌웨어 보호에서 eFuse가 하는 일
“eFuse로 펌웨어를 암호화한다”는 말을 자주 듣지만, 그 안에는 여러 가지 일이 섞여 있다. 하나씩 풀어 보면 다섯 가지다.
| 역할 | eFuse에 넣는 것 | 막는 공격 |
|---|---|---|
| 암호화 키 저장 | 펌웨어 암호화 키(AES) | 플래시 덤프로 펌웨어 읽기 |
| 서명 검증 기점 | 공개키의 해시 | 변조한 펌웨어 실행 |
| 장치 고유 키 | 칩마다 다른 마스터 키 | 한 기기에서 뽑은 키로 다른 기기 공격 |
| 기능 잠금 | 디버그·다운로드 모드 차단 비트 | JTAG나 시리얼로 칩 장악 |
| 롤백 방지 | 펌웨어 보안 버전 카운터 | 취약한 옛 펌웨어로 되돌리기 |
암호화 키 저장
가장 직접적인 방법은 펌웨어를 암호화한 AES 키를 eFuse 블록에 그대로 굽고 읽기 금지를 켜는 것이다. 칩은 외부 플래시의 내용을 쓸 때 이 키로 복호화한다. 부팅할 때 한 번에 복호화해 RAM에 올리는 칩도 있고, ESP32처럼 플래시에서 캐시로 가져올 때마다 하드웨어가 그 자리에서 복호화하는 칩도 있다.
여기서 하나 짚고 넘어가자. 이 방식이 지키는 것은 기밀성뿐이다. 대부분의 플래시 암호화는 위치마다 다른 값을 섞어 AES로 암호화할 뿐 인증 태그가 없다. 공격자가 암호문 일부를 바꿔도 칩은 알아채지 못하고 바뀐 대로 복호화한다. 결과가 대개 쓰레기 값이라 정교한 조작은 어렵지만, 변조를 막아 주지는 않는다. 변조는 서명 검증으로 막아야 한다.
공개키 해시 저장
시큐어 부트(secure boot)는 제조사가 펌웨어에 서명하고, 칩이 실행 전에 그 서명을 검증하는 방식이다. 검증하려면 제조사의 공개키가 필요하다. 그런데 공개키를 플래시에 두면 어떻게 될까? 공격자가 그 공개키를 자기 것으로 바꾸고, 자기 키로 서명한 펌웨어를 넣으면 그만이다.
공개키를 칩 안에 고정하면 이 문제가 사라진다. 다만 RSA-3072 공개키는 384바이트라 eFuse에 담기에는 너무 크다. 보통은 공개키의 SHA-256 해시(32바이트)만 eFuse에 굽는다. 공개키 자체는 펌웨어 이미지에 붙여 두고, 부트 ROM이 그 공개키의 해시가 eFuse 값과 같은지 먼저 확인한 다음 서명을 검증한다. 신분증 사진과 얼굴을 맞춰 본 뒤에 서류를 믿는 것과 같은 순서다.
이 해시는 공개키에서 나온 값이라 비밀이 아니다. 누가 읽어도 상관없으니 읽기 금지는 걸지 않는다. 대신 쓰기 금지는 반드시 건다. 해시를 바꿀 수 없어야 모든 검증이 시작되는 신뢰의 출발점(root of trust)이 될 수 있다.
장치 고유 키와 키 파생
모든 기기에 같은 펌웨어 키를 굽는 게 편하긴 하다. 하지만 한 대에서 키가 새면 같은 모델 전체의 펌웨어가 한꺼번에 풀린다. 아파트 모든 집의 현관문을 열쇠 하나로 여는 것과 같다. 그래서 칩 제조사가 칩마다 다른 무작위 마스터 키를 공장에서 굽고, 이 키를 직접 쓰지 않고 다른 키를 감싸는 데에만 쓰는 칩이 많다.
- 마스터 키는 칩 안 암호 엔진만 쓴다. 기기 제조사도 값을 모른다.
- 펌웨어 키나 기기별 비밀은 마스터 키로 암호화한 덩어리(블롭)로 플래시에 둔다.
- 블롭은 그 칩에서만 풀린다. 다른 기기로 복사하면 쓸모가 없다.
ESP32는 다른 방식으로 같은 효과를 낸다. 기본 설정에서는 첫 부팅 때 칩이 스스로 키를 만들어 굽기 때문에 기기마다 키가 다르다. 칩마다 다른 키를 eFuse에 굽는 대신, 실리콘 제조 편차에서 매번 같은 값을 뽑아내는 PUF(Physically Unclonable Function, 물리적 복제 방지 함수)를 쓰는 칩도 있다. 같은 공정에서 찍어도 칩마다 미세하게 다른 차이를 지문처럼 키로 쓰는 방식이다.
디버그·다운로드 경로 잠금
키를 아무리 잘 숨겨도 뒷문이 열려 있으면 소용없다. JTAG 디버거로 CPU를 멈추고 이미 복호화된 메모리를 읽으면, 키를 몰라도 펌웨어가 보인다. 이런 뒷문을 닫는 잠금 비트도 eFuse에 있다.
- JTAG 디버그 비활성화
- 부트 ROM의 시리얼 다운로드 모드 비활성화 또는 기능 제한
- 다운로드 모드에서 플래시 암호화·복호화 엔진 사용 금지
이런 비트는 보통 공장 출하 단계에서 켠다. 개발하는 동안에는 디버깅이 필요해 꺼 두었다가, 양산 라인에서 켜는 식이다.
롤백 방지 카운터
서명 검증까지 했으면 이제 안전할까? 아직 구멍이 하나 남는다. 제조사가 예전에 정상으로 서명했지만 취약점이 있는 옛 펌웨어를 공격자가 다시 넣는 경우다. 서명은 진짜이니 칩은 그대로 받아들인다.
롤백 방지 카운터가 이 구멍을 막는다. 펌웨어에 보안 버전 번호를 넣고, eFuse에는 “지금까지 설치된 가장 높은 보안 버전”을 켜진 비트 수로 기록한다. 부트 ROM이나 부트로더는 펌웨어의 보안 버전이 eFuse 카운터보다 낮으면 실행을 거부한다. 업데이트가 끝나면 비트를 하나 더 끊는다. 끊은 비트는 되돌릴 수 없으니, 한 번 올라간 버전 아래로는 영영 내려갈 수 없다.
ESP32 플래시 암호화
지금까지 본 원리가 실제 칩에서는 어떻게 쓰일까? Espressif의 ESP32로 따라가 보자. ESP32는 와이파이 IoT 기기에 널리 쓰이는 SoC이고, eFuse 구조와 플래시 암호화 방식이 문서로 자세히 공개돼 있다. 아래 내용은 원래 ESP32(ESP32-D0WD 계열) 기준이다.
eFuse 구성
ESP32의 eFuse는 모두 1024비트(128바이트)이고, 256비트 블록 4개로 나뉜다(ESP-IDF eFuse Manager). 블록마다 쓰임은 이렇다.
| 블록 | 용도 |
|---|---|
| BLOCK0 | 시스템 설정. MAC 주소, 보호 비트, 보안 설정 비트(FLASH_CRYPT_CNT, JTAG_DISABLE 등) |
| BLOCK1 | 플래시 암호화 키 |
| BLOCK2 | 시큐어 부트 키. Secure Boot V2에서는 공개키의 SHA-256 해시 |
| BLOCK3 | 사용자 정의 데이터. 커스텀 MAC 주소, ADC 보정값, 롤백 방지 카운터도 여기에 있다 |
BLOCK0을 뺀 BLOCK1~3, 즉 키와 사용자 데이터에 쓸 수 있는 공간은 블록마다 32바이트씩 모두 96바이트다. 여기에 3/4 부호화를 쓰면 블록마다 실제로 담기는 값이 192비트(24바이트)로 줄어 모두 72바이트가 되고, 나머지는 오류 검사용으로 쓴다.
128바이트에 펌웨어 보호에 필요한 다섯 가지 값을 다 담을 수 있을까? 값마다 필요한 만큼만 비트를 쪼개 쓰기 때문에 가능하다(ESP-IDF eFuse 정의 파일).
| 역할 | 자리 | 크기 |
|---|---|---|
| 플래시 암호화 키 | BLOCK1 전체 | 256비트(32바이트) |
| 시큐어 부트 V2 공개키 해시 | BLOCK2 전체 | 256비트(32바이트, SHA-256) |
| 디버그·다운로드 잠금 | BLOCK0 | 기능마다 1비트(JTAG_DISABLE, DISABLE_DL_ENCRYPT, DISABLE_DL_DECRYPT, DISABLE_DL_CACHE, UART_DOWNLOAD_DIS 등) |
| 롤백 방지 카운터 | BLOCK3의 128~159번 비트(SECURE_VERSION) | 32비트 |
| 장치 고유 키 | 따로 없음 | 0 |
공개키는 RSA-3072 기준 384바이트로 eFuse 전체(128바이트)보다 커서, 32바이트짜리 해시만 굽고 공개키는 펌웨어 이미지에 붙인다. 잠금은 켜졌는지만 표시하면 되니 기능마다 1비트면 충분하다.
장치 고유 키는 따로 두지 않는다. 원래 ESP32에는 다른 키를 감싸는 별도의 마스터 키가 없고, 대신 첫 부팅 때 칩이 스스로 만든 플래시 암호화 키가 기기마다 달라서 그 역할을 겸한다.
롤백 카운터는 비트 하나에 한 번이므로 보안 버전을 최대 32번까지만 올릴 수 있다. BLOCK3는 커스텀 MAC 주소와 ADC 보정값도 함께 쓰기 때문에 사용자가 자유롭게 쓸 공간은 얼마 남지 않는다.
보안 설정에서 가장 중요한 값은 7비트짜리 FLASH_CRYPT_CNT다. 앞의 카운터처럼 켜진 비트 수로 세는데, 그 수가 홀수면 플래시 암호화가 켜진 상태, 짝수면 꺼진 상태다. 0에서 1로만 바뀌는 eFuse로 켜고 끄기를 반복하려고 홀짝을 쓰는 것이다. 비트를 하나씩 끊어 가면 상태가 이렇게 바뀐다.
0000000 켜진 비트 0개(짝수) 꺼짐 출고 상태
0000001 1개(홀수) 켜짐 첫 부팅에서 암호화한 뒤
0000011 2개(짝수) 꺼짐 개발 모드에서 암호화를 끔
0000111 3개(홀수) 켜짐 다시 켬비트가 7개뿐이라 상태를 바꿀 수 있는 횟수도 정해져 있다.
첫 부팅에서 일어나는 일
플래시 암호화를 켠 펌웨어를 평문으로 처음 써 넣고 부팅하면, 부트로더가 이렇게 움직인다(ESP-IDF Flash Encryption).
FLASH_CRYPT_CNT가 0이므로 아직 암호화되지 않은 상태로 판단한다.- 하드웨어 난수 발생기로 256비트 키를 만들어 BLOCK1에 굽는다. 이미 미리 구워 둔 키가 있으면 그것을 쓴다.
- BLOCK1에 읽기 금지와 쓰기 금지를 켠다. 이 순간부터 소프트웨어는 키를 읽을 수 없다.
- 플래시의 부트로더, 파티션 테이블, 암호화 대상 파티션을 그 자리에서 암호화한다.
FLASH_CRYPT_CNT의 첫 비트를 끊어 “암호화됨”으로 표시하고 다시 부팅한다.
이후로는 플래시를 덤프해도 암호문만 나온다. 눈여겨볼 점은 키를 누가 만드느냐다. 이 키는 공장에서 정해 주는 것이 아니라, 칩이 처음 켜질 때 스스로 깎아 만든 열쇠다. 기본 설정에서는 기기마다 다르고, 제조사를 포함해 아무도 모르는 키가 된다.
여기서 한 가지 의문이 든다. 키를 칩만 알고 있다면, 처음 플래시에 쓰는 펌웨어는 누가 암호화할까? 아무도 하지 않는다. 처음에는 평문 그대로 쓰고, 위 과정처럼 첫 부팅 때 칩이 그 자리에서 암호화한다(ESP-IDF Flash Encryption). 그래서 펌웨어를 굽는 PC도 키를 알 필요가 없다. 대신 첫 부팅이 끝나기 전까지는 플래시에 평문이 들어 있고, 이 과정 중에 전원이 끊기면 플래시 내용이 망가져 다시 써야 한다.
업데이트도 마찬가지다. OTA로 받은 새 펌웨어를 esp_partition_write()로 쓰면, 칩이 자기 키로 암호화하면서 플래시에 쓴다. 업데이트 서버도 키를 몰라도 되는 이유다. 릴리스 모드에서는 시리얼로 평문 펌웨어를 써 넣을 수 없어서 OTA가 유일한 업데이트 경로가 된다.
플래시의 코드를 어떻게 복호화하며 실행하나
ESP32는 외부 플래시의 코드를 RAM에 모두 복사하지 않고, 캐시를 통해 플래시에 있는 그대로 실행한다(XIP, eXecute In Place). CPU가 플래시 주소를 읽으면 캐시가 플래시에서 데이터를 가져오는데, 그 길목에 있는 플래시 암호화 엔진이 BLOCK1의 키로 즉시 복호화한다. 통역사를 사이에 두고 대화하는 것처럼, CPU와 소프트웨어는 평문 플래시를 읽을 때와 아무 차이를 느끼지 못한다. 복호화는 캐시가 플래시에서 데이터를 새로 가져올 때만 일어나고, 이미 캐시에 올라온 내용은 평문 그대로 다시 쓴다.
알고리즘은 AES-256인데, 키를 그대로 쓰지 않고 32바이트 블록마다 그 블록의 플래시 주소로 키를 비틀어(tweak) 쓴다. 덕분에 같은 평문이라도 플래시의 다른 위치에 있으면 다른 암호문이 되어, 같은 내용이 반복되는 펌웨어 영역에서도 패턴이 드러나지 않는다.
이 방식에는 무결성 검사가 없다. 그래서 Espressif도 플래시 암호화를 시큐어 부트와 함께 쓰라고 권한다.
개발 모드와 릴리스 모드
ESP-IDF는 플래시 암호화를 두 가지 모드로 켤 수 있다. 개발 중에는 펌웨어를 수없이 다시 써 넣어야 하니 문을 조금 열어 두고, 출하할 때는 완전히 잠그는 식이다.
| 개발 모드 | 릴리스 모드 | |
|---|---|---|
FLASH_CRYPT_CNT | 쓰기 금지 안 함 | 쓰기 금지 |
| 시리얼로 다시 쓰기 | 평문 펌웨어를 몇 번 더 써 넣을 수 있음 | 불가. OTA 업데이트만 가능 |
| 다운로드 모드의 암호 엔진 | 쓸 수 있음 | DISABLE_DL_ENCRYPT, DISABLE_DL_DECRYPT, DISABLE_DL_CACHE를 끊어 막음 |
| JTAG | 켜져 있을 수 있음 | 끔 |
개발 모드에서는 FLASH_CRYPT_CNT에 남은 비트를 써서 암호화를 껐다 다시 켤 수 있다. 시리얼로 평문 펌웨어를 써 넣으면 부트로더가 다시 암호화하는 식이다. 출하 기기가 개발 모드로 남아 있으면 공격자도 같은 경로를 쓸 수 있으니, 기기를 분석할 때 가장 먼저 확인하는 항목이다.
[WARN]
eFuse를 굽는 명령은 되돌릴 수 없다. 키를 잘못 굽거나 보호 비트를 먼저 켜면 칩을 다시 쓸 수 없게 된다. 실습은 버려도 되는 개발 보드로 한다.
eFuse 상태 확인과 키 미리 굽기
실제 칩에서 eFuse 상태를 확인해 보자. espefuse.py는 esptool에 포함된 eFuse 도구로, 칩이 시리얼 다운로드 모드에 들어가 있으면 eFuse 요약을 보여 준다. 릴리스 모드로 암호화한 ESP32에서 보면 이렇다. 출력 형식은 esptool 버전마다 조금 다르다.
$ espefuse.py --port /dev/ttyUSB0 summary
...
Security fuses:
FLASH_CRYPT_CNT (BLOCK0) Flash encryption mode counter = 127 R/- (0b1111111)
...
BLOCK1 (BLOCK1) Flash encryption key
= ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? -/-
R/W,R/-,-/-는 읽기·쓰기 가능 여부다.R/-는 읽을 수 있지만 쓰기 금지,-/-는 둘 다 금지다.- BLOCK1이
??로만 나오는 건 읽기 금지 때문이다. 도구가 값을 숨기는 게 아니라 하드웨어가 값을 돌려주지 않는다. FLASH_CRYPT_CNT가 7비트 모두 켜져 쓰기 금지된 상태라면 다시 끌 방법이 없다.
양산에서는 키를 칩이 스스로 만들게 두지 않고 미리 만들어 굽기도 한다. 공장에서 같은 키로 암호화한 이미지를 한 번에 써 넣기 위해서다.
$ espsecure.py generate_flash_encryption_key key.bin
$ espefuse.py --port /dev/ttyUSB0 burn_key flash_encryption key.bin
이렇게 하면 키 파일이 호스트 PC에 남는다. 이 파일을 어떻게 관리하느냐가 곧 기기 보안이고, 모든 기기에 같은 key.bin을 쓰면 기기마다 다른 키의 장점이 사라진다.
ESP32-C3·S3 이후의 변화
ESP32-S2, S3, C3 등 후속 칩은 eFuse 구조와 암호화 방식이 바뀌었다(ESP32-C3 Flash Encryption).
- 용량: eFuse 메모리는 모두 4096비트이고, 그중 사용자가 쓸 수 있는 영역은 최대 1792비트(224바이트)다(ESP32-C3 데이터시트). 256비트 블록 가운데 사용자 데이터 블록(
BLOCK3) 1개와 키 블록 6개를 합한 크기이고, 나머지는 시스템 설정과 오류 정정용 비트다. - 용도를 정하는 키 블록: 키 블록(
BLOCK_KEY0~BLOCK_KEY5)이 여러 개 있고, 블록마다KEY_PURPOSEeFuse로 용도를 정한다. 플래시 암호화 키, 시큐어 부트 공개키 해시, HMAC 키 등이다. 용도 필드도 함께 쓰기 금지되므로 나중에 용도를 바꿀 수 없다. - XTS-AES: 플래시 암호화가 디스크 암호화 표준인 XTS-AES로 바뀌었다. ESP32-C3는
XTS_AES_128_KEY용도의 256비트 키(128비트 키 두 개)를 쓴다. - 카운터:
FLASH_CRYPT_CNT대신 3비트짜리SPI_BOOT_CRYPT_CNT를 쓴다. 1개나 3개가 켜지면 암호화가 켜진 상태다.
$ espefuse.py --port /dev/ttyUSB0 burn_key BLOCK_KEY0 key.bin XTS_AES_128_KEY
eFuse의 한계와 공격
eFuse는 소프트웨어 공격에는 강하다. 펌웨어를 완전히 장악해도 읽기 금지된 키는 읽을 수 없다. 그럼 eFuse에 넣은 키는 절대 안전할까? 공격자가 기기를 손에 쥐고 있다면 이야기가 달라진다. 하드웨어를 직접 공격할 수 있기 때문이다.
읽는 순간을 노리는 결함 주입
eFuse 값은 리셋 직후 한 번 섀도 레지스터로 읽힌다. 바로 이 순간 전원 전압을 아주 짧게 떨어뜨리면(전압 글리칭) 감지 회로가 비트를 잘못 읽는다. 출근길 메모로 치면, 메모를 적는 순간 손을 툭 쳐서 엉뚱하게 적게 만드는 셈이다.
2019년 LimitedResults가 ESP32에서 이 방법으로 플래시 암호화 키와 시큐어 부트 키를 읽어 냈다(CVE-2019-17391, 원문, Espressif 권고문). 노린 것은 키가 아니라 읽기 금지 비트였다. 부팅 직후 eFuse 컨트롤러가 값을 읽어 들이는 순간에 전원 전압을 흔들자, BLOCK1·BLOCK2를 막고 있던 읽기 금지 비트가 꺼진 값으로 잘못 읽혔다. 메모로 치면 “이 서랍은 잠금”이라는 줄을 “잠금 없음”으로 적게 만든 셈이다. 그 상태에서 다운로드 모드로 eFuse를 덤프하자 두 키가 나왔다. 한 번의 덤프에는 오류 바이트가 섞여 있어서, 30~50번 덤프한 값을 통계로 맞춰 키를 복원했다. 저자는 결함이 eFuse 읽기 하드웨어에 있어 칩 리비전 없이는 고칠 수 없다고 봤다.
Espressif의 대응은 두 가지였다.
- 결함 주입에 대비한 검사를 ROM에 넣은 새 리비전(ESP32-D0WD-V3) 사용을 권했다.
- 시큐어 부트와 플래시 암호화에 기기마다 다른 키를 쓰라고 권했다. 이 공격은 칩 하나씩 따로 해야 하므로, 키가 기기마다 다르면 한 대를 뚫어도 그 한 대만 뚫린다.
Secure Boot V2가 eFuse에 비밀 키 대신 공개키 해시를 두는 이유도 여기에 있다. 읽혀도 상관없는 값만 두면 eFuse가 판독돼도 잃을 것이 없다.
칩을 열어 직접 읽기
더 근본적인 방법도 있다. 칩 패키지를 열고 층을 한 겹씩 벗겨 셀을 직접 들여다보는 것이다. 폴리실리콘 eFuse는 끊긴 자리가 물리적으로 남으므로, 전자 현미경으로 위치를 찾아 보면 쓴 셀과 안 쓴 셀이 구별된다. 시간과 장비가 들지만 원리는 단순하다.
이 공격도 칩마다 따로 해야 하므로 결론은 결함 주입과 같다. 위험한 것은 같은 비밀을 여러 기기에 공유하는 제품이고, 기기마다 다른 비밀을 쓰면 한 대를 뚫어도 그 한 대만 뚫린다.
키가 엔진을 지나갈 때: 부채널
eFuse가 키를 숨겨도, 키는 결국 암호 엔진에서 쓰인다. 엔진이 AES를 계산하는 동안 새어 나오는 전력 소비나 전자기파를 수천~수만 번 측정해 통계로 키를 추정하는 공격을 부채널 공격(side-channel attack)이라고 한다. 금고를 열지 않고 다이얼 소리로 번호를 알아내는 것과 비슷하다. 이 공격은 eFuse와 관계없이 통한다. 플래시 암호화 엔진은 부팅할 때마다 같은 키로 많은 블록을 복호화하므로 측정할 기회도 많다. 이 방어는 eFuse가 아니라 암호 엔진 설계의 몫이다.
실제로 2023년 Ledger 보안팀은 결함 주입 대책을 넣은 ESP32-V3에서, 전원을 켤 때 일어나는 펌웨어 복호화의 전력 소비를 30만 번 측정해 eFuse에 든 256비트 플래시 암호화 키 전체를 복원했다(IACR ePrint 2023/090). 측정하려면 칩 전원선에 측정용 저항을 넣는 식으로 보드를 개조하고, 오실로스코프로 부팅을 수십만 번 반복해 기록해야 한다. 손이 많이 가지만 칩 하나를 손에 쥔 공격자에게는 현실적인 공격이다.
설계·운영 실수
현실에서는 이렇게 정교한 하드웨어 공격까지 갈 필요도 없는 경우가 더 많다. 펌웨어가 풀리는 원인은 대개 아래 같은 실수다.
- 모든 기기에 같은 키: 공장에서 하나의 키 파일로 전 기기를 구우면, 위의 공격 중 하나가 한 대에서만 성공해도 전 모델의 펌웨어가 풀린다.
- 개발 모드로 출하: ESP32 플래시 암호화를 개발 모드로 둔 채 나간 기기는 eFuse가 있어도 보호가 거의 없다.
- 디버그를 안 막음: JTAG가 살아 있으면 키를 읽지 않고도 복호화된 메모리를 읽는다.
- 암호화만 하고 서명은 안 함: 펌웨어 내용은 숨겨져도 변조를 막지 못한다.
- 키 파일 관리: 미리 굽는 키가 빌드 서버나 개발자 PC, 저장소에 남는다.
기기를 분석할 때도 이 순서로 확인한다. 어떤 SoC인지, 플래시 덤프가 암호화돼 있는지(엔트로피가 고르게 높은지), 시리얼 다운로드 모드나 JTAG가 열려 있는지, 열려 있다면 eFuse 요약에서 보호 비트가 어떻게 돼 있는지다.
정리
eFuse는 칩 다이 안에 있는 한 번만 쓸 수 있는 작은 퓨즈 배열이다. 큰 전류로 연결부를 끊어 0을 1로 바꾸고, 리셋 때마다 컨트롤러가 읽어 섀도 레지스터에 담는다. 읽기 금지 비트를 켜면 그 값은 소프트웨어를 거치지 않고 암호 엔진으로만 간다.
펌웨어 보호에서 eFuse가 하는 일은 결국 “칩 밖에 둘 수 없는 작은 값”을 칩 안에 고정하는 것이다. 펌웨어 암호화 키, 서명 검증의 출발점이 되는 공개키 해시, 칩마다 다른 마스터 키, 디버그 잠금, 롤백 방지 카운터가 그 값이다.
eFuse도 결함 주입이나 칩 개봉으로 읽힐 수 있다. 그러니 eFuse가 무엇을 지키는지만큼 무엇을 담느냐가 중요하다. 공개해도 되는 값(공개키 해시)은 그대로 두고, 비밀은 기기마다 다르게 만들고, 암호화에는 서명 검증과 디버그 잠금을 함께 쓴다.
참고
- IBM, “IBM Introduces Chip Morphing Technology”, 2004
- US 7902903: Programmable efuse and sense circuit
- Brian Krent, “Espressif ESP-WROOM-32 Wi-Fi & Bluetooth Module”, Wikimedia Commons, CC BY-SA 4.0
- Espressif, ESP-IDF eFuse Manager (ESP32)
- Espressif, ESP-IDF eFuse 정의 파일: esp_efuse_table.csv (ESP32)
- Espressif, ESP-IDF Flash Encryption (ESP32)
- Espressif, ESP32-C3 Datasheet
- Espressif, ESP-IDF Flash Encryption (ESP32-C3)
- Espressif, esptool: espefuse burn-key
- LimitedResults, “Pwn the ESP32 Forever: Flash Encryption and Sec. Boot Keys Extraction”, 2019
- K. Abdellatif, O. Hériveaux, A. Thillard, “Unlimited Results: Breaking Firmware Encryption of ESP32-V3”, IACR ePrint 2023/090
- Espressif, “Security Advisory Concerning Fault Injection and eFuse Protections (CVE-2019-17391)”, 2019