라이터 하나로 리눅스 Root 권한 탈취하기: EMFI와 페이지 테이블 조작의 예술


솔직히 처음 이 글의 제목을 봤을 때는 흔한 어그로성 해킹 튜토리얼인 줄 알았다. 하지만 데이비드 뷰캐넌(David Buchanan)의 블로그 글을 끝까지 읽고 난 후, 나는 이 우아하면서도 무식한(?) 하드웨어 결함 주입(Fault Injection) 기법에 감탄을 금할 수 없었다.

우리는 보통 소프트웨어 아키텍처를 설계할 때 하드웨어가 ‘항상 정상적으로 동작한다’는 가정하에 시스템을 구축한다. 샌드박스, ASLR, 메모리 안전성을 보장하는 Rust 도입 등 수많은 소프트웨어적 방어 기제를 쌓아 올리지만, 밑단에 있는 물리적 하드웨어가 거짓말을 하기 시작하면 이 모든 것은 사상누각에 불과하다.

오늘은 고가의 장비 없이 단돈 몇 천 원짜리 압전식(Piezo-electric) 라이터 하나로 구형 노트북의 DDR3 메모리에 EMFI (Electro-Magnetic Fault Injection) 공격을 가해, 리눅스 시스템의 Root 권한을 탈취하는 과정을 엔지니어의 시각에서 딥다이브 해보자.

1. 하드웨어 셋업: 낭만과 광기 사이

일반적으로 하드웨어 결함 주입 공격은 타이밍과 위치를 마이크로초 단위로 제어해야 하므로, ChipWhisperer 같은 고가의 전문 장비나 레이저 장비가 필요하다. 하지만 이 실험의 타겟은 2011년산 삼성 S3520 노트북(Intel i3, 1GB DDR3 RAM)이었고, 공격 도구는 ‘바비큐용 점화 라이터’였다.

작업은 단순하다. 노트북의 SODIMM 메모리 모듈에서 데이터 핀(DQ) 하나를 찾아 전선을 납땜한다. 이 전선은 안테나 역할을 하며, 주변의 전자기 간섭(EM)을 흡수해 데이터 버스에 그대로 쏟아붓는다. 저자는 과도한 간섭으로 인한 시스템 크래시를 막기 위해 15옴 저항을 하나 달았을 뿐이다.

이 안테나 근처에서 라이터를 ‘딸깍’ 하고 켜면, 발생하는 전자기 펄스가 메모리 버스에 노이즈를 일으켜 읽기/쓰기 작업 중 특정 비트(Bit)를 뒤집어버린다(Bit-flip).

2. CPython 샌드박스 탈출: 캐시(Cache)와의 싸움

하드웨어 결함을 유발할 수 있다는 것을 확인했으니, 이제 이를 소프트웨어 익스플로잇으로 연결할 차례다. 첫 번째 타겟은 CPython 이었다.

핵심 전략은 불변 객체인 bytes 객체 내부에 가짜 bytearray 구조체를 심어두고, 포인터의 비트를 뒤집어 이 가짜 객체를 가리키게 만드는 것이다. 저자는 DQ7 핀에 안테나를 연결해 7번째 비트를 뒤집었는데, 이는 포인터 주소를 정확히 128 바이트 이동시키는 효과를 낳는다.

하지만 여기서 심각한 엔지니어링 허들이 등장한다. 바로 CPU 캐시 다.

우리가 조작해야 하는 것은 DRAM에 저장된 데이터가 아니라, CPU와 DRAM 사이의 ‘버스(Bus)‘를 지나는 데이터다. 즉, 데이터가 이미 L3 캐시에 올라와 있다면 메모리 버스를 타지 않으므로 공격이 불가능하다.

저자는 이 문제를 해결하기 위해 해당 CPU의 L3 캐시(3MiB)보다 훨씬 큰 배열을 만들어 객체 참조를 채워 넣었다. 그리고 루프를 돌며 순차적으로 접근해 CPU가 강제로 DRAM에서 데이터를 가져오도록(Cache eviction) 만들었다.

# "victim"은 미리 준비된 `bytes` 객체
spray = (victim,) * 0x100_0000 # 캐시를 밀어내기 위해 거대한 튜플 생성
for obj in spray:
    if obj is not victim:
        # 글리치가 성공하여 포인터가 변경된 경우
        print("Found corrupted ptr!")
        assert(type(obj) is bytearray)

이 코드는 단순해 보이지만, 하드웨어의 메모리 계층 구조를 완벽히 이해하고 이를 소프트웨어적으로 역이용한 훌륭한 패턴이다.

3. 리눅스 권한 상승(LPE): 페이지 테이블과 TLB 조작

파이썬 레벨의 조작은 학술적으론 흥미롭지만, 진짜 목표는 리눅스 OS의 Root 권한 탈취다. 여기서부터는 운영체제의 핵심인 가상 메모리(Virtual Memory)페이지 테이블(Page Table), 그리고 TLB (Translation Lookaside Buffer)가 등장한다.

개인적으로 이 공격의 백미는 Rowhammer 공격의 철학을 EMFI에 맞게 변형한 부분이라고 생각한다.

  • 스프레이(Spray) 기법: 저자는 memfdmmap (MAP_FIXED 옵션 사용)을 이용해 물리 메모리의 50%를 레벨 0 페이지 테이블로 꽉 채웠다. 각 매핑은 32MiB 크기로, 가상 메모리는 많이 차지하지만 물리 메모리 비용은 페이지 테이블 자체뿐이므로 효율적으로 메모리를 채울 수 있다.
  • TLB 우회 및 글리치 유도: TLB 캐싱을 우회하기 위해 수많은 R/W 매핑에 루프를 돌며 접근한다. 이때 라이터를 튕겨 메모리 버스에 글리치를 발생시키면, 페이지 테이블 항목(PTE)을 읽어오는 과정에서 비트 29가 뒤집힌다.
  • 결과: 원래 읽고 쓰려던 페이지가 아니라, 우리가 미리 물리 메모리에 깔아둔 ‘다른 레벨 0 페이지 테이블’을 가리키게 된다. 이제 공격자는 페이지 테이블 자체에 대한 R/W 권한을 얻게 된 것이다.

페이지 테이블을 마음대로 수정할 수 있게 되면 게임은 끝난다. 저자는 PTE를 수정해 물리 메모리 전체를 스캔하고, /usr/bin/su (Setuid root) 실행 파일의 첫 번째 페이지가 캐시된 물리적 위치를 찾아냈다. 그리고 그곳을 Root 쉘을 띄우는 자신의 커스텀 ELF 바이너리로 덮어썼다.

이후 사용자가 su 명령어를 실행하면, 리눅스는 디스크에서 원본을 읽어오는 대신 이미 오염된 페이지 캐시의 코드를 실행하게 된다. 정말 우아한 Page Cache Poisoning 이다.

4. 커뮤니티 반응과 향후 전망 (Hacker News)

Hacker News의 반응도 뜨거웠다. 흥미로운 기술적 업데이트와 논의들을 요약해 본다.

  • 최신 메모리에서도 통할까? 저자는 HN 댓글을 통해 ARM 아키텍처는 물론, LPDDR4 및 LPDDR5에서도 동일한 공격이 가능하다고 확인했다.
  • 방어 기제: 닌텐도 스위치 1세대 커널은 이 방식으로 뚫을 수 있었지만, 스위치 2세대는 메모리 암호화 가 적용되어 있어 비트 하나만 뒤집혀도 캐시 라인 전체가 손상되므로 이 공격이 통하지 않는다고 한다. 하드웨어 기반 메모리 암호화(SME/MKTME)가 왜 중요한지 보여주는 완벽한 사례다.
  • 더 정교한 트리거: 라이터 대신 고속 Mux IC를 사용하면 훨씬 더 정교하고 우아하게 전기적 결함을 주입할 수 있다는 의견도 있었다.

5. 총평 (Verdict)

이 공격 방식이 당장 엔터프라이즈 프로덕션 서버를 위협할까? 아니다. 물리적인 접근이 필요하고, 서버 섀시를 열어 RAM에 납땜을 해야 하니까. (물론 데이터센터에 라이터를 들고 잠입하는 닌자가 있다면 예외겠지만 말이다.)

하지만 DRM, 콘솔 게임기, 모바일 디바이스, 안티치트(Anti-cheat) 솔루션의 세계에서는 이야기가 다르다. 사용자가 기기에 대한 물리적 통제권을 쥐고 있는 환경에서, TPM이나 Secure Boot 같은 논리적 방어막이 EMFI 기반의 물리적 결함 주입 앞에 얼마나 무력해질 수 있는지 명확히 보여주었다.

소프트웨어 엔지니어로서 우리는 종종 하드웨어의 무결성을 맹신한다. 하지만 페이지 테이블, TLB, 캐시 라인 같은 로우레벨 구조들이 물리적인 전자기 펄스 한 방에 어떻게 무너지는지 보는 것은 꽤나 충격적이면서도 즐거운 경험이었다. 하드웨어와 소프트웨어의 경계에서 발생하는 취약점은 앞으로 더욱 중요한 보안 화두가 될 것이다.

References: