Documentation & Blog

PAC 보호기법 조사

KWAKBUMJUN 2026. 4. 5. 23:23

1. 들어가며: 왜 포인터를 보호해야 하는가

메모리 취약점을 이용한 공격의 핵심은 거의 언제나 포인터 변조다. 스택 버퍼 오버플로우로 리턴 주소를 덮어쓰든(ROP), 함수 포인터를 변조하든(JOP), vtable을 조작하든 — 결국 공격자가 하는 일은 "프로그램이 의도하지 않은 주소로 제어 흐름을 바꾸는 것"이다.

소프트웨어 수준의 방어(ASLR, Stack Canary, CFI 등)는 효과적이지만 한계가 있다. ASLR은 정보 유출로 우회되고, Stack Canary는 비순차적 쓰기로 건너뛸 수 있다. 이 한계를 극복하기 위해 ARM은 하드웨어 수준에서 포인터 자체에 암호학적 서명을 삽입하는 기법을 도입했다.

이것이 PAC(Pointer Authentication Code)이다.


2. PAC의 핵심 개념

2.1 한 문장 요약

포인터의 미사용 상위 비트에 암호학적 해시(PAC)를 삽입하고, 포인터 사용 시 해시를 검증하여 변조 여부를 판별한다.

2.2 PAC가 들어가는 자리: 64비트 포인터의 빈 공간

현대 ARM64 시스템에서 64비트 포인터의 모든 비트가 실제 주소 지정에 사용되지는 않는다. 예를 들어 48비트 가상 주소 체계를 사용하는 Linux 시스템에서는:

63                              48 47                                0
┌────────  ─────────────────────────┬─────────────────────────────────────┐
│        미사용 상위 비트            │          가상 주소 (48비트)          │
│    (기존: 부호 확장으로 채움)       │                                     │
│                                  │                                     │
│   ← 여기에 PAC를 삽입 →          │                                     │
└─────────────────  ────────────────┴─────────────────────────────────────┘

PAC 비트 폭 = 55 - 가상 주소 크기

가상 주소 크기 PAC 비트 폭 가능한 PAC 값 수
48비트 7비트 128
42비트 13비트 8,192
39비트 16비트 65,536

PAC 비트 폭이 클수록 보안 강도가 높아진다. 7비트 PAC는 128개의 가능한 값 중 하나를 무작위로 맞춰야 하므로, 단순 브루트포스의 성공 확률은 약 1/128이다. 16비트라면 1/65,536이 된다.

2.3 PAC 계산의 세 가지 입력

PAC는 다음 세 가지 값을 입력으로 받아 계산된다:

┌──────────────┐
│  포인터 값     │  (서명할 대상 — 예: 리턴 주소)
│  (64비트)      │
└──────┬───────┘
       │
       ▼
┌──────────────┐     ┌───────────   ──┐
│   컨텍스트    │     │  비밀 키      │
│  (64비트)     │     │ (128비트)     │
│              │     │              │
│ 예: 현재 SP  │     │ 프로세스별    │
│    또는 0    │     │ 랜덤 할당    │
└──────┬───────┘     └──────┬───────┘
       │                    │
       └────────┬───────────┘
                │
                ▼
        ┌──────────────┐
        │   QARMA-64   │  (또는 구현체 선택 암호 알고리즘)
        │   암호 연산   │
        └──────┬───────┘
               │
               ▼
        ┌────────────   ─┐
        │   PAC 값      │  → 포인터 상위 비트에 삽입
        │ (7~16비트)    │
        └──────────────┘
입력 설명 역할
포인터 값 서명할 64비트 포인터 (예: 함수 리턴 주소) 서명 대상
컨텍스트(Modifier) 64비트 보조 값 (예: 현재 스택 포인터, 0 등) 동일 포인터라도 컨텍스트가 다르면 다른 PAC 생성 → 재사용 공격 방지
비밀 키 128비트 비밀 키 (시스템 레지스터에 저장) 키를 모르면 PAC 위조 불가

컨텍스트의 중요성: 같은 함수의 리턴 주소라도, 호출 시점의 스택 포인터(SP)가 다르면 PAC 값이 달라진다. 이는 공격자가 한 곳에서 유출한 서명된 포인터를 다른 곳에서 재사용하는 것을 방지한다.


3. PAC의 5개 키 체계

ARMv8.3은 5개의 독립적인 128비트 비밀 키를 제공한다:

키 이름 용도 관련 명령어 사용 예시
APIAKey 명령어 포인터 서명 A PACIA, AUTIA 리턴 주소 보호 (주로 사용)
APIBKey 명령어 포인터 서명 B PACIB, AUTIB 대체 키 (교차 서명 등)
APDAKey 데이터 포인터 서명 A PACDA, AUTDA C++ vtable, 함수 포인터 보호
APDBKey 데이터 포인터 서명 B PACDB, AUTDB 대체 데이터 키
APGAKey 범용 인증 PACGA 임의 데이터 블록의 MAC 계산

왜 2개씩? 키를 분리하면 서로 다른 보안 도메인 간의 격리가 가능하다. 예를 들어 커널과 유저 공간이 다른 키를 사용하면, 유저 공간에서 유출된 서명이 커널 포인터 위조에 사용될 수 없다.

Apple의 확장: Apple Silicon(M1/M2)은 EXTRAKEY_EL1이라는 128비트 레지스터를 추가로 도입했다. 이 키는 ARM 표준 키와 XOR되어 실제 PAC 연산에 사용된다. 이를 통해 유저/커널 모드 간 PAC 키를 추가로 분리한다.


4. PAC 명령어 상세

4.1 명령어 명명 규칙

PAC  +  I/D  +  A/B  +  수식어
 │       │       │       │
 │       │       │       └── SP: 스택 포인터를 컨텍스트로 사용
 │       │       │           Z:  컨텍스트 = 0
 │       │       │           (없음): 범용 레지스터를 컨텍스트로 사용
 │       │       │
 │       │       └── A: A키 사용, B: B키 사용
 │       │
 │       └── I: 명령어(Instruction) 포인터
 │           D: 데이터(Data) 포인터
 │
 └── PAC: 서명 삽입
     AUT: 서명 검증
     XPAC: 서명 제거 (검증 없이)

4.2 핵심 명령어 일람

서명 삽입 (PAC*)

명령어 동작 입력 컨텍스트
PACIA Xd, Xn A키로 Xd에 PAC 삽입 Xd = 포인터 Xn = 컨텍스트
PACIASP A키로 LR(x30)에 PAC 삽입 LR = 리턴 주소 SP = 컨텍스트
PACIAZ A키로 LR에 PAC 삽입 LR 0
PACIZA Xd A키로 Xd에 PAC 삽입 Xd 0
PACDA Xd, Xn A키로 데이터 포인터 서명 Xd = 포인터 Xn = 컨텍스트
PACIB, PACDB B키 버전 (동일 구조)
PACGA Xd, Xn, Xm 범용 인증 코드 생성 Xn, Xm 입력 결과를 Xd에 저장

서명 검증 (AUT*)

명령어 동작 성공 시 실패 시
AUTIA Xd, Xn A키로 Xd의 PAC 검증 PAC 제거, 원본 포인터 복원 포인터 변조 (역참조 시 폴트)
AUTIASP A키로 LR 검증 (SP 컨텍스트) LR 복원 폴트 유발
AUTDA Xd, Xn A키로 데이터 포인터 검증 원본 포인터 복원 포인터 변조

서명 제거 (XPAC*)

명령어 동작 용도
XPACI Xd PAC를 제거하고 원본 포인터 복원 디버깅, 포인터 비교 시
XPACD Xd 데이터 포인터에서 PAC 제거

복합 명령어

명령어 동작 설명
RETAA AUTIASP + RET 리턴 주소 검증 후 리턴 (원자적)
RETAB AUTIBSP + RET B키 버전
BRAA Xn, Xm AUTIA + BR 검증 후 간접 분기
BLRAA Xn, Xm AUTIA + BLR 검증 후 간접 호출

4.3 인증 실패 처리

PAC 검증(AUT*)이 실패하면, 포인터가 의도적으로 변조(mangled)된다. 구체적으로:

인증 성공:
  포인터 상위 비트에서 PAC 제거 → 원본 유효 주소 복원 → 정상 사용

인증 실패:
  포인터 상위 비트를 고의적으로 오류 값으로 설정
  → 이 포인터를 역참조하면 가상 주소 범위 밖의 주소에 접근 시도
  → MMU가 페이지 폴트(Segmentation Fault) 발생
  → 프로그램 크래시

핵심: PAC은 "공격을 막는 것"이 아니라, "변조된 포인터 사용 시 프로그램이 크래시하도록 보장"하는 것이다. 공격자가 포인터를 변조하면 해당 포인터로의 제어 흐름 전환이 폴트를 일으켜, 익스플로잇이 실패한다.


5. QARMA-64: PAC의 암호 엔진

5.1 왜 전용 암호 알고리즘이 필요한가

PAC 연산은 모든 함수 호출과 리턴에서 수행된다. 즉 프로그램 실행 중 매우 빈번하게 호출되 로, 암호 알고리즘이 다음 조건을 만족해야 한다:

  1. 극도로 낮은 지연 시간 (수 사이클 내 완료)
  2. 작은 하드웨어 면적 (모든 코어에 내장)
  3. 충분한 암호학적 강도 (PAC 비트 폭 내에서의 충돌 저항성)

AES나 SHA-256은 너무 무겁다. 그래서 Qualcomm이 ARM을 위해 QARMA-64를 설계했다.

5.2 QARMA-64 특성

항목
설계자 Qualcomm (Roberto Avanzi)
블록 크기 64비트
키 크기 128비트
구조 Tweakable block cipher (조정 가능 블록 암호)
라운드 수 5~7라운드 (QARMA-5, QARMA-7)
특징 역변환이 거의 동일한 비용 → 서명과 검증 모두 빠름

"Tweakable"의 의미: 입력 블록(포인터)과 키 외에 트윅(tweak = 컨텍스트 값)을 추가 입력으로 받는다. 같은 포인터+같은 키여도 트윅(SP 등)이 다르면 른 출력을 생성한다. 이것이 PAC의 "컨텍스트" 역할을 하드웨어적으로 구현하는 방식이다.

참고: ARM은 QARMA-64를 권장하지만 구현체에 강제하지는 않는다. 칩 제조사는 다른 알고리즘을 사용할 수 있다. Apple Silicon은 자체 알고리즘을 사용하는 것으로 알려져 있다.


6. PAC의 실전: 함수 호출 보호

6.1 ROP 공격과 PAC 방어

ROP(Return-Oriented Programming) 공격의 핵심:

  1. 버퍼 오버플로우로 스택의 리턴 주소를 덮어쓴다
  2. 리턴 주소를 프로그램 내 기존 코드 조각(가젯)의 주소로 교체한다
  3. 함수가 리턴할 때 가젯으로 제어 흐름이 전환된다

PAC가 이를 막는 원리:

; ===== 함수 프롤로그 =====
my_function:
    paciasp                        ; ① LR(리턴 주소)에 PAC 삽입
                                   ;    키 = APIAKey
                                   ;    컨텍스트 = 현재 SP 값
    stp     x29, x30, [sp, #-16]!  ; ② PAC가 삽입된 LR을 스택에 저장
    mov     x29, sp

    ; ... 함수 본문 ...

; ===== 함수 에필로그 =====
    ldp     x29, x30, [sp], #16    ; ③ 스택에서 LR 복원
    autiasp                        ; ④ LR의 PAC 검증
                                   ;    컨텍스트 = 현재 SP (프롤로그와 동일해야 함)
                                   ;    검증 실패 시 → LR 변조 → 폴트
    ret                            ; ⑤ 검증 통과한 LR로 리턴

또는 복합 명령어를 사용하면:

; 에필로그 (간소화)
    ldp     x29, x30, [sp], #16
    retaa                          ; AUTIASP + RET을 원자적으로 수행

공격자의 딜레마:

  • 리턴 주소를 덮어쓰면 → PAC가 불일치 → autiasp에서 폴트
  • 유효한 PAC를 삽입하려면 → 128비트 비밀 키를 알아야 함 → 현실적으로 불가

6.2 JOP 공격과 PAC 방어

JOP(Jump-Oriented Programming)은 리턴 대신 간접 분기(BR Xn)를 악용한다. PAC는 간접 분기 전에도 포인터를 검증하는 명령어를 제공한다:

; 함수 포인터 호출 보호
    ldr     x8, [x0]              ; 함수 포인터 로드
    blraa   x8, x9                ; x8의 PAC를 x9 컨텍스트로 검증 후 호출
                                  ; 검증 실패 시 → 폴트

6.3 데이터 포인터 보호

C++ vtable이나 연결 리스트의 next 포인터 등 데이터 포인터도 보호할 수 있다:

; 데이터 포인터 서명
    pacda   x0, x1                ; x0(데이터 포인터)에 PAC 삽입
                                  ; 키 = APDAKey, 컨텍스트 = x1

; 데이터 포인터 검증
    autda   x0, x1                ; PAC 검증 후 원본 포인터 복원
    ldr     x2, [x0]              ; 검증된 포인터로 데이터 접근

7. 실제 구현: 컴파일러와 OS의 역할

7.1 컴파일러 지원

# GCC: PAC 활성화 컴파일
gcc -march=armv8.3-a -mbranch-protection=standard -o program program.c

# Clang/LLVM: PAC 활성화
clang -target aarch64-linux-gnu -march=armv8.3-a \
      -msign-return-address=all -o program program.c

-mbranch-protection=standard는 다음을 활성화한다:

  • 리턴 주소 서명: 모든 non-leaf 함수에 paciasp/autiasp 삽입
  • BTI(Branch Target Identification): 간접 분기 대상 검증

7.2 Linux 커널의 PAC 관리

커널 부팅
  │
  ├── CONFIG_ARM64_PTR_AUTH 활성화 확인
  │
  ├── 각 프로세스에 exec() 시 5개의 랜덤 키 할당
  │     APIAKey, APIBKey, APDAKey, APDBKey, APGAKey
  │
  ├── 동일 프로세스 내 모든 스레드는 키를 공유
  │
  ├── fork() 시 부모의 키가 자식에게 상속
  │
  └── exec() 시 새로운 랜덤 키 할당

커널 자체 보호: CONFIG_ARM64_PTR_AUTH_KERNEL이 활성화되면 커널 코드의 함수 리턴 주소도 PAC로 보호된다. 이때 HINT 공간 명령어를 사용하여 PAC 미지원 CPU에서도 NOP로 처리되어 호환성을 유지한다.

7.3 Apple XNU (iOS/macOS)

Apple의 PAC 구현은 ARM 표준보다 훨씬 광범위하다:

보호 대상 표준 ARM PAC Apple PAC
리턴 주소
함수 포인터 선택적 기본 적용
Objective-C isa 포인터
C++ vtable 선택적
블록 invoke 포인터
시스템 콜 핸들러

Apple은 9가지 수식어(modifier) 유형을 정의하여 포인터 종류별로 다른 컨텍스트를 사용한다. 이는 한 종류의 서명된 포인터를 다른 종류로 재사용하는 타입 혼동 공격을 방지한다.


8. PAC 우회 공격 연구

PAC는 강력하지만 완벽하지 않다. 다음은 알려진 주요 공격 연구이다.

8.1 PACMAN (MIT, 2022)

논문: "PACMAN: Attacking ARM Pointer Authentication with Speculative Execution" (ISCA 2022)

핵심 아이디어: 투기적 실행(Speculative Execution)을 이용하여 PAC 값을 브루트포스 추측한다.

[PACMAN 공격 원리]

일반적인 PAC 브루트포스:
  추측값이 틀림 → 프로그램 크래시 → 탐지됨 (불가능)

PACMAN의 우회:
  ① 추측값으로 포인터를 투기적으로(speculatively) 사용
  ② CPU가 투기적 실행 경로에서 해당 포인터로 메모리 접근
  ③ 추측이 맞으면 → 캐시에 흔적이 남음 (TLB/캐시 상태 변화)
     추측이 틀리면 → 투기적 실행이 롤백되어 흔적 없음
  ④ 캐시 사이드 채널로 추측 성공 여부를 판별
  ⑤ 크래시 없이 PAC 값 확인 가능 → "PAC Oracle"

영향:

  • 7비트 PAC: 최대 128번 시도로 정확한 PAC 추측 가능
  • Apple M1, ARM Cortex-A78, Neoverse V1/N2 등 광범위한 칩 영향
  • 하드웨어 결함이므로 소프트웨어 패치 불가

제한 사항:

  • PACMAN 자체만으로는 시스템을 침해할 수 없음
  • 별도의 메모리 취약점(버퍼 오버플로우 등)과 결합해야 함
  • Spectre와 유사하게 "익스플로잇 기법"이지 "독립적 취약점"이 아님

8.2 PAC 위조 (Signing Oracle) 공격

RET2 Systems의 연구에서 시연된 시나리오:

[Signing Oracle 공격 흐름]

① 프로그램 내에 "임의 포인터를 서명하는" 기능이 노출됨
   (예: 사용자 입력을 PACIA로 서명하는 함수)

② 공격자가 원하는 목표 주소(예: system() 함수)를 입력

③ 프로그램이 해당 주소에 유효한 PAC를 삽입하여 반환

④ 공격자는 이 서명된 포인터를 버퍼 오버플로우로 리턴 주소에 배치

⑤ 함수 리턴 시 PAC 검증 통과 → 공격 성공

교훈: PAC의 보안은 "서명 기능이 공격자에게 노출되지 않아야 한다"는 전제에 의존한다. 소프트웨어 설계에서 PACIA 등의 명령어 사용을 신중하게 제한해야 한다.

8.3 기타 연구

연구 연도 공격 방식 대상
"Demystifying Pointer Authentication on Apple M1" 2023 (USENIX Security) Apple의 PAC 구현 역공학 + 키 관리 분석 Apple M1
"PAC it up" 2019 (USENIX Security) PAC의 보안 보장 범위 형식적 분석 ARM PAC 일반
TikTag 2024 (Black Hat) MTE(Memory Tagging Extension) 우회 — PAC와 관련 기법 ARM MTE

9. PAC의 한계와 보완 기술

9.1 PAC 단독의 한계

한계 설명
낮은 PAC 비트 폭 가상 주소 48비트 시 PAC은 7비트 = 128가지. 브루트포스에 취약
투기적 실행 사이드채널 PACMAN 공격으로 크래시 없이 PAC 추측 가능
Signing Oracle 소프트웨어 버그로 서명 기능이 노출되면 무력화
데이터 전용 공격 제어 흐름이 아닌 데이터만 변조하는 DOP(Data-Oriented Programming)에는 무력
같은 컨텍스트 내 재사용 동일 SP에서 서명된 포인터는 해당 SP 값이 같은 시점에 재사용 가능

9.2 PAC + MTE (Memory Tagging Extension)

ARMv8.5에서 도입된 MTE는 PAC와 상호보완적이다:

PAC: 포인터의 "진위 여부"를 검증 (변조 탐지)
MTE: 포인터와 메모리의 "소유권"을 검증 (공간적 안전성)

┌─────────────────────────────────────────┐
│              PAC + MTE 병합 방어          │
│                                         │
│  [PAC]  리턴 주소 변조? → 크래시         │
│  [MTE]  범위 밖 메모리 접근? → 크래시     │
│                                         │
│  두 기법 모두 우회해야 익스플로잇 성공     │
└──────────────────────────────────────   ──┘

9.3 PAC + BTI (Branch Target Identification)

ARMv8.5의 BTI는 간접 분기의 착지점(landing pad)을 제한한다:

방어 기법 보호 대상 위협 모델
PAC 리턴 주소, 함수 포인터 ROP, JOP
BTI 간접 분기 착지점 JOP (가젯 체이닝 차단)
PAC + BTI 리턴 + 분기 모두 ROP + JOP 통합 방어

10. 정리: PAC가 바꾼 것과 바꾸지 못한 것

PAC가 바꾼 것

  1. 하드웨어 수준 CFI(Control Flow Integrity): 소프트웨어 CFI의 성능 오버헤드 없이 제어 흐름 보호
  2. ROP/JOP 공격 비용 극대화: PAC 없이는 스택 오버플로우 하나로 가능하던 공격이, PAC 하에서는 사이드 채널 + 메모리 취약점 + 정밀한 타이밍이 동시에 필요
  3. 산업 표준화: Apple, Google(Android), Linux 커널 모두 PAC를 채택하여 ARM 생태계 전반의 보안 수준 상승

PAC가 바꾸지 못한 것

  1. 메모리 안전성 자체: PAC는 포인터 변조를 탐지하지만, 버퍼 오버플로우나 Use-After-Free 자체를 방지하지는 않음
  2. 데이터 전용 공격: 제어 흐름을 변경하지 않고 데이터만 조작하는 공격(DOP)에는 무력
  3. 사이드 채널 내성: PACMAN이 증명했듯이, 투기적 실행이라는 현대 CPU의 근본 설계와 충돌

한 문장 요약

PAC는 "메모리 취약점이 존재해도 제어 흐름 탈취를 어렵게 만드는" 하드웨어 보호 기법이다. 완벽하지 않지만, 공격의 비용을 극적으로 높인다.


참고 자료