CPU에 백도어를 심는 것이 가능한지, 가능하다면 무엇을 할 수 있고 어떻게 트리거되며 어떤 경로로 삽입될 수 있는지에 대한 추측과 논의.
소프트웨어는 어떤 것이든 백도어로 훼손될 수 있다는 점이 일반적으로 받아들여져 있다. 대표적인 예로는 Sony/BMG 인스톨러가 있는데, 이는 사용자가 CD를 복사하지 못하게 하려고 Sony가 백도어를 내장해 둔 것이었고, 동시에 악의적인 제3자가 해당 소프트웨어가 설치된 어떤 머신이든 장악할 수 있게도 했다. Samsung Galaxy는 모뎀이 기기의 파일시스템에 접근할 수 있게 해 주는 백도어가 있었고, 이는 또한 가짜 기지국을 운영하는 누구나 기기 내 파일에 접근할 수 있게 한다. Lotus Notes는 암호화를 무력화할 수 있게 하는 백도어가 있었고, Lenovo 노트북은 광고를 띄우기 위해 모든 웹 트래픽을 프록시로 밀어 넣었는데(신뢰된 루트 인증서를 통해 HTTPS까지 포함), 이로 인해 올바른 키(모든 노트북에 배포되어 있었다)를 가진 누구나 HTTPS 트래픽을 가로챌 수 있었다.
FPGAs와 네트워킹 장비에서 백도어가 목격된 적이 있음에도, 누군가 CPU 백도어 가능성을 꺼내면 여전히 사람들이 그것은 불가능하다고 주장하는 경우가 흔하다. 나는 CPU 백도어가 존재한다고 주장하지는 않겠지만, 적절한 접근 권한만 있다면 구현 자체는 쉽다고는 주장하겠다.
백도어를 만들고 싶다고 해 보자. 어떻게 할까? 여기에는 세 가지 부분이 있다. 백도어가 심어진 CPU가 무엇을 할 수 있는가, 백도어는 어떻게 접근(트리거)되는가, 그리고 백도어를 설치하려면 어떤 종류의 침해가 필요한가?
첫 번째 항목부터 시작하자. 백도어는 무엇을 하는가? 가능성은 많다. 가장 단순한 것은 권한 상승을 허용하는 것이다. CPU가 ring3에서 ring0 또는 SMM으로 전환하도록 만들어, 실행 중인 프로세스에 커널 수준 권한을 부여한다. CPU가 직접 수행하므로, 하드웨어 및 소프트웨어 가상화를 모두 관통할 수 있다. 더 미묘하거나 더 침습적인 일도 많이 할 수 있지만, 권한 상승은 충분히 단순하면서도 충분히 강력하기 때문에 다른 선택지는 여기서 논의하지 않겠다.
이제 백도어가 무엇을 하길 원하는지 알았으니, 어떻게 트리거되게 해야 할까? 이상적으로는, 우연히는 물론 백도어를 찾으려고 무차별 대입을 하더라도 아무도 마주치지 않을 만한 것이어야 한다. 그런 제한이 있더라도 가능한 트리거 상태 공간은 거대하다.
특정 명령어 하나를 보자. fyl2x1. 정상 동작에서는 부동소수점 레지스터 두 개를 입력으로 받으므로, 트리거를 숨길 수 있는 비트가 2*80=160개나 된다. 특정 값 쌍에 대해 백도어가 트리거되게 하면, 무작위 발견에 대해서는 아마 안전하다. 누군가 실수로 백도어를 우연히 밟을까 걱정되거나, 의심되는 백도어를 무차별 대입으로 찾을까 걱정된다면, 두 개의 일반 입력 레지스터 외에도 더 많은 것들을 검사하면 된다(어차피 CPU를 제어하고 있으니까).
이 트리거는 깔끔하고 단순하지만, 단점은 트리거를 맞추려면 네이티브 코드를 실행해야 할 가능성이 높다는 점이다. chrome이나 Firefox가 fyl2x 명령어를 내보내게 만들 가능성은 낮기 때문이다. 이를 우회하려고 JavaScript 엔진이 쉽게 내보낼 수 있는 명령어(예: fadd)를 트리거로 삼을 수도 있다. 문제는 add 명령어를 패치하고 거기에 검사 코드를 추가하면 눈에 띄게 느려진다는 점이다(다만 하드웨어를 편집할 수 있다면 오버헤드 없이 할 수 있어야 한다). rep string 명령어를 패치하고, 적절한 “키”를 설정하기 위한 어떤 작업을 한 뒤 블록 복사를 수행하게 하는 방식으로, 혹은 idiv로, JavaScript를 통해서도 트리거 가능하면서 탐지하기 어려운 무언가를 만들 수 있을지도 모른다. 또는 설계 사본을 확보할 수 있었다면, 디버그 로직 트리거2나 성능 카운터를 이용해, 임의의 JavaScript가 실행될 때 백도어가 발동되게 만드는 방법을 아마 찾아낼 수 있을 것이다.
좋다. 이제 백도어가 생겼다. 그럼 백도어를 어떻게 삽입할까? 소프트웨어라면 소스를 수정하거나 바이너리를 수정할 것이다. 하드웨어에서도 소스에 접근할 수 있다면 소프트웨어만큼 쉽게 편집할 수 있다. 소스를 다시 컴파일하고 물리 칩을 만들어내는 하드웨어 쪽의 대응 과정에는 엄청나게 높은 고정 비용이 든다. 따라서 변경 사항을 소스에 넣으려면, 설계3를 침해해 제조에 넘기기 전에 편집을 삽입하거나, 제조 공정을 침해해 마지막 순간에 편집을 슬쩍 끼워 넣는4 쪽을 원할 것이다.
그게 너무 어렵게 들린다면, 패치 메커니즘을 침해하는 방법을 시도해 볼 수 있다. 대부분의 현대 CPU에는 사후 버그 수정을 가능하게 하는 내장 패치 메커니즘이 있다. 여러분이 쓰고 있는 CPU도 아마 패치된 적이 있을 것이다. 어쩌면 출시 첫날부터 그랬을 수도 있고, 펌웨어 업데이트의 일부로 적용되었을 수도 있다. CPU의 패치 메커니즘 세부 사항은 철저히 비밀로 보호된다. CPU 안에 공개키가 각인되어 있고, 올바른 개인키로 서명된 패치만 받아들이는 방식일 가능성이 높다.
이게 실제로 일어나고 있을까? 전혀 모르겠다. 일어날 수 있을까? 물론이다. 가능성은 얼마나 될까? 글쎄, 핵심 난제는 기술적이라기보다 비기술적이므로, 그건 나에게 물을 일이 아니다. 내가 추측해야 한다면, 다른 장비를 전복하는 쉬움만 보더라도, 아니라는 쪽에 걸겠다.
나는 누군가가 백도어를 트리거하기 위해 사용한 소프트웨어에 접근할 수 있더라도 탐지하기 어려운 백도어를 만드는 방법은 논의하지 않았다. 그건 더 어렵지만, 칩에 내장 TPM이 달리기 시작하면 가능해질 것이다.
이 글이 마음에 들었다면, 아마 CPU 버그에 관한 이 글도 즐길 것이고, 지난 35년 동안의 새로운 CPU 기능에 관한 이 글도 흥미로울 수 있다.
훨씬 더 많은 논의가 있는 이 트위터 스레드를 보라. 그중 일부는 아래에 요약되어 있다.
댓글이 너무 많아서 개별적으로 출처 표기는 하지 않겠지만, @hackerfantastic, Arrigo Triulzi, David Kanter, @solardiz, @4Dgifts, Alfredo Ortega, Marsh Ray, Russ Cox의 코멘트를 요약하면 다음과 같다. 물론 오류는 전적으로 내 책임이다.
AMD의 K7과 K8은 마이크로코드 패치 메커니즘이 침해된 적이 있으며, 이 글에서 언급한 종류의 공격을 가능하게 했다. 알고 보니 AMD는 업데이트를 암호화하지도 않았고 체크섬으로 검증하지도 않았는데, 이 때문에 원하는 동작을 하는 업데이트가 나올 때까지 업데이트를 쉽게 수정할 수 있었다.
Alfredo Ortega가 데모 목적으로 만든 백도어의 예시로 이 자료가 있다.
하드웨어 배경이 없는 사람들을 위해서, VHDL로 CPU를 구현하는 방법에 대한 이 발표는 괜찮고, 백도어를 구현하는 섹션도 포함한다.
나쁜 난수 결과를 제공하는 방식으로 RDRAND에 백도어를 심는 것이 가능할까? 그렇다. 이 글의 첫 초안에서는 그 점을 언급했지만, 사람들이 RDRAND를 신뢰하지 않고 다른 엔트로피 소스와 결과를 섞는다는 인상을 받았기 때문에 삭제했다. 그게 백도어를 무용하게 만들지는 않지만, 가치를 크게 떨어뜨리기는 한다.
AES-NI 키를 저장해 덤프하는 것이 가능할까? 아무도 눈치채지 못하게 칩에 플래시 메모리를 몰래 얹는 것은 아마 비현실적이겠지만, 현대 칩에는 데이터를 저장하고 덤프할 수 있는 로직 애널라이저 설비가 있다. 하지만 그것들에 대한 접근은 어떤 비밀 메커니즘을 통해 이뤄지고, 그 동작을 리버스 엔지니어링할 수 있게 해 줄 바이너리에 접근하는 것조차 어떻게 가능한지 불명확하다. 이는 K8 리버스 엔지니어링과는 크게 대조적이다. K8은 마이크로코드 패치가 펌웨어 업데이트에 포함되기 때문에 가능했다.
트리거를 위해 명령어 프리픽스를 검사하는 것도 가능하다. x86에서는 중복되거나(심지어 서로 모순되는) 명령어 프리픽스를 명령어에 붙일 수 있다. 어떤 프리픽스가 사용되는지는 잘 정의되어 있으므로, 문제를 일으키지 않고 원하는 만큼 프리픽스를 추가할 수 있다(프리픽스 길이 제한까지). 이 방식의 문제는, 마이크로코드 패치로 성능을 희생하지 않고 구현하기가 아마 어렵다는 점, 프리픽스 개수가 제한되어 있고 길이 제한도 있어 여러 명령어에 걸쳐 상태를 추적하지 않으면 유효 키 크기가 상대적으로 작다는 점, 그리고 네이티브 코드로만 트리거를 생성할 수 있다는 점이다.
알려진 한에서는, 이 모든 것은 추측에 불과하며, 실제 환경에서 사용되는 진짜 CPU 백도어를 본 사람은 아무도 없다.
광범위한 코멘트를 준 Leah Hanson, 제안/정정을 준 Aleksey Shipilev과 Joe Wilder, 그리고 위에 링크한 트위터 토론의 많은 참여자들에게 감사한다. 또한 일부 RSS 리더의 버그가 문제를 일으키고 있다는 것을 알아채고 우회 방법을 제공해 준 Markus Siemens에게도 감사한다. 이 글에만 특화된 것은 아니지만, 우연히 여기서 이 이슈가 나왔다.
마이크로코딩된 명령어와 하드웨어로 구현된 명령어의 구분 자체도 어느 정도는 임의적이다. CPU는 자신이 구현하는 명령어 집합을 갖고 있는데, 이를 공개 API로 생각할 수 있다. 내부적으로는 다른 명령어 집합을 실행할 수 있는데, 이를 비공개 API로 생각할 수 있다.
현대 Intel 칩에서 4개(또는 그 이하)의 uop(비공개 API 호출)으로 변환되는 명령어는 디코더에 의해 직접 uop으로 번역된다. 더 많은 uop을 만들어내는 명령어(5개에서 수백, 또는 수천 개까지도)는 CPU의 작은 ROM 또는 RAM에서 uop을 읽어오는 마이크로코드 엔진을 통해 디코딩된다. 왜 5가 아니라 4인가? 그것은 어떤 근본적 진리의 결과가 아니라 트레이드오프의 결과다. 이에 대한 용어는 표준화되어 있지 않지만, 내가 아는 사람들은 어떤 명령어의 디코드가 마이크로코드 엔진에서 처리되면 그 명령어를 “마이크로코딩됐다”고 하고, 표준 디코더에서 처리되면 “하드웨어로 구현됐다”고 말할 것이다. 마이크로코드 엔진은 일종의 독자적인 CPU 같은데, 아키텍처적으로는 보이지 않는 임시 레지스터에 대한 읽기/쓰기, 몇 개 레지스터의 스크래치 공간만으로는 부족한 명령어를 위한 내부 RAM에 대한 읽기/쓰기, 마이크로코드 엔진이 어떤 마이크로코드를 가져와 디코딩할지 바꾸는 조건부 마이크로코드 분기 등을 처리할 수 있어야 하기 때문이다.
구현 세부는 다양하고(대체로 비밀이다). 하지만 어떤 구현이든, 마이크로코드 엔진은 CPU가 시작될 때 RAM에 마이크로코드를 로드하고, 이후 마이크로코딩된 명령어를 그 RAM에서 가져와 디코딩하는 무언가로 생각할 수 있다. 마이크로코드 패치를 통해 부팅 시 로드되는 내용을 바꾸면 실행되는 마이크로코드를 쉽게 변경할 수 있다.
디버깅 중 더 빠른 반복을 위해, Intel이 마이크로코딩되지 않은 명령어도 마이크로코드 RAM에서 실행되도록 강제해 마이크로코드 패치로 패치할 수 있게 하는 메커니즘을 가지고 있을 가능성은 ‘그럴듯함’과 ‘높은 확률’ 사이 어딘가에 있다고 본다. 하지만 설령 그렇지 않더라도, 마이크로코드 패치 메커니즘을 침해하고 단 하나의 마이크로코딩된 명령어를 수정하는 것만으로도 백도어를 설치하기에는 충분해야 한다.
[return] 2. 대체로 공개 문서화되어 있지는 않지만, Intel이 두어 세대 전 칩에 어떤 종류의 디버그 트리거를 내장하고 있었는지에 대한 개괄은 Intel Technology Journal, Volume 4, Issue 3 128페이지부터 볼 수 있다. [return] 3. 지난 몇 년간 대기업들이 침해되었는지, 그리고 그런 일이 가능하기는 한지에 대한 논쟁이 있었다. 냉전 시기에는 오늘날 어떤 기업도 갖지 못한 수준의 대응 수단(외국 국적자 채용 금지, "강화된 심문 기법" 등)에 접근할 수 있었음에도 불구하고, 여러 정부 기관들이 장기간에 걸쳐 다양한 수준에서 침해되었다. 기업이 침해되고 있는지 우리가 알게 될지는 확신할 수 없지만, 냉전 시절 정부 기관을 침해하는 것보다 오늘날 기업을 침해하는 것이 더 쉬웠을 것은 분명하며, 그 당시에도 그것은 충분히 가능했다. 마이크로코드 패치 키를 얻을 정도로 회사를 침해하는 것은 냉전 시절에 이뤄졌던 일들에 비하면 사소하다. [return] 4. 이것도 사소한 세부에 관한 또 하나의 엄청 긴 각주다! 특히 제조 공정에 관한 내용이다. 건너뛰고 싶을 수도 있다! 그래도 읽겠다면, 경고하지 않았다고는 말하지 말라.
제조가 완전히 끝나기 전에 칩을 편집하는 것은, 설계상 비교적 쉽다. 왜 그런지 설명하려면, 칩이 어떻게 만들어지는지 봐야 한다.

칩의 단면을 보면, 실리콘 게이트가 맨 아래에 있고, nand 게이트 같은 논리 프리미티브를 형성하며, 그 위에 M1부터 M8까지로 표시된 일련의 금속 층이 있어 서로 다른 게이트를 연결하는 배선을 이룬다는 것을 알 수 있다. 제조 공정을 만화적으로 모델링하면, 칩은 아래에서 위로 한 번에 한 층씩 만들어지며, 각 층은 어떤 물질을 증착한 뒤 마스크를 이용해 일부를 에칭해 제거하는 방식으로 형성된다고 할 수 있다. 이 과정은 리소그래피 인쇄와 유사하다. 하지만 만화가 아닌 실제 버전은 매우 복잡하다. Todd Fernendez는 “M1” 아래의 층들을 만들기 위해 약 500단계가 필요하다고 추정한다. 또한 필요한 정밀도 수준이 매우 높아서, 에칭에 사용되는 빛이 장비를 충분히 마모시켜 결국 장비가 닳아 버린다. 보통은 렌즈가 빛이 통과하는 것만으로 닳는다고 생각하지 않겠지만, 트랜지스터를 만드는 데 필요한 수백 단계 각각에서 요구되는 정밀도 수준에서는 심각한 문제다. 그게 놀랍게 들린다면, 당신만 그런 것이 아니다. 90년대의 ITRS 로드맵은 2016년이면 9nm 공정(작을수록 좋다)에서 거의 30GHz(높을수록 좋다)에 도달하고, 칩이 거의 300와트를 소비할 것이라고 예측했다. 하지만 실제로는 5 GHz면 꽤 빠르다고 여겨지고, Intel이 아닌 누구든 2016년 초까지 14nm 공정에서 높은 수율의 양산에 성공하면 운이 좋은 편일 것이다. 칩을 만드는 일은 누구나 예상했던 것보다 어렵다.
현대 칩은 층이 충분히 많아서, 처음부터 끝까지 하나를 만드는 데 약 3개월이 걸린다. 그래서 버그는 매우 나쁜 소식이다. 바닥 쪽 층 중 하나를 바꿔야 하는 버그 수정은 제조에 3개월이 걸리기 때문이다. 버그 수정의 턴어라운드 시간을 줄이기 위해, 보통 실리콘 곳곳에 사용되지 않는 로직 게이트를 흩뿌려 둔다. 그러면 위쪽에 가까운 몇 개 층만 편집해도 작은 버그 수정을 할 수 있다. 칩은 생산 라인 공정으로 만들어지기 때문에, 어느 시점이든 부분적으로 완성된 칩 배치들이 존재한다. 상단 금속 층 중 하나만 편집하면 된다면, 부분적으로 완성된 칩에 편집을 적용할 수 있어, 턴어라운드 시간을 수개월에서 수주로 줄일 수 있다.
칩은 쉽게 편집할 수 있도록 설계되기 때문에, 칩이 제조되기 전에 설계에 접근할 수 있는 사람(예: 제조사)은 비교적 작은 편집으로도 큰 변화를 만들 수 있다. 내가 보기에는 이런 말을 주요 CPU 회사의 누군가에게 하면, 특성화(characterization) 과정이나 속도 경로를 찾는 과정 같은 데서 걸리기 때문에 들키지 않고는 불가능하다고 말할 것 같다. 그러길 바라지만, 실제 하드웨어 장치가 백도어를 포함한 채로 출하된 적도 있고, 아무도 눈치채지 못했거나, 혹은 공모했을 수도 있다.