사전 도메인 분리
개념

도메인 분리

gabury1고친 사람 github-actions[bot]

도메인 분리는 쓰임이 다른 두 영역이 서로를 건드리거나 서로인 척하지 못하게 갈라 둡니다. 운영체제에서는 시스템을 지키는 코드의 메모리를 일반 프로그램이 못 만지게 막는 일을 말합니다. 암호에서는 해시 함수 하나를 여러 용도에 쓸 때 용도마다 다른 꼬리표를 붙이는 일을 말합니다. 두 뜻 모두 한쪽에서 생긴 실수나 공격이 다른 쪽으로 번지지 않게 하려는 장치입니다.

쉽고 빠른 이해

도메인 분리는 쓰임이 다른 것끼리 섞이지 않게 경계를 긋는 일입니다. 웹 브라우저가 아무리 이상하게 굴어도 운영체제의 메모리는 못 고칩니다. 로그인 토큰과 비밀번호 재설정 토큰을 같은 키로 서명해도 하나를 다른 하나로 바꿔 쓸 수 없게 만듭니다.

경계가 없으면 약한 쪽 하나가 뚫릴 때 강한 쪽까지 같이 무너집니다. 프로그램 하나의 버그가 운영체제를 덮어쓰고, 재설정 링크 하나가 로그인 열쇠가 됩니다.

어떻게 도나:

  1. 운영체제는 프로세서의 두 실행 모드와 프로그램마다 따로 주는 메모리로 경계를 긋습니다
  2. 경계를 넘는 길은 운영체제에 일을 청하는 정해진 문 하나로 줄입니다
  3. 암호에서는 해시하거나 서명할 데이터 앞에 용도 이름을 붙입니다. 같은 데이터라도 용도가 다르면 다른 값이 나옵니다

운영체제의 경계는 늘 켜져 있습니다. 개발자가 직접 가를 일은 믿지 못할 코드를 돌릴 때 생깁니다. 암호의 꼬리표는 해시 함수나 키 하나를 두 가지 이상 용도에 쓸 때 붙입니다.

대가도 있습니다. 운영체제의 경계를 넘을 때마다 시간이 듭니다. 꼬리표는 한 번 정하면 못 바꿉니다. 바꾸면 그동안 만든 해시와 서명이 전부 안 맞습니다.

상세

도메인 분리(domain separation)는 쓰임이나 주인이 다른 영역 사이에 경계를 긋는 설계 원칙입니다. 한 영역의 것이 다른 영역에 닿거나 다른 영역의 것으로 통하지 못하게 합니다. 여기서 도메인은 한 쓰임이 차지하는 범위를 뜻합니다. 인터넷 주소의 도메인과는 상관이 없습니다.

이 절은 같은 이름이 쓰이는 두 분야를 차례로 다룹니다. 먼저 운영체제가 커널과 일반 프로그램을 어떻게 가르는지 봅니다. 그다음 암호에서 해시 입력에 꼬리표를 붙이는 방법을 머클 트리와 서명 토큰으로 보입니다. 끝으로 언제 가르고 무엇을 치르는지 정리합니다.

두 분야에서 가르는 것

같은 이름이지만 두 분야는 가르는 대상이 다릅니다. 아래 표가 둘을 나란히 놓습니다.

운영체제와 시스템 보안 암호
도메인 코드 하나가 손댈 수 있는 메모리와 자원의 범위 해시 함수나 키 하나가 쓰이는 용도 하나
가르는 법 프로세서 실행 모드와 프로그램마다 다른 메모리 입력 앞에 용도마다 다른 꼬리표
막는 일 일반 프로그램이 커널을 고쳐 쓰는 일 한 용도의 값을 다른 용도의 값으로 내미는 일

두 뜻이 공유하는 생각은 하나입니다. 경계 이쪽의 것은 경계 저쪽에서 힘을 못 쓰게 한다는 것입니다.

보호 도메인

앞 표에서 운영체제 쪽 도메인이라고 한 것을 보호 도메인이라고 부릅니다. 코드 하나가 읽고 쓸 수 있는 메모리, 열 수 있는 파일, 만질 수 있는 장치를 모은 범위입니다. 운영체제에서는 대개 실행 중인 프로그램 하나, 곧 프로세스 하나가 보호 도메인 하나를 가집니다.

보호 도메인이 왜 필요한지는 그것이 없는 장면을 보면 압니다. 모든 프로그램이 같은 메모리를 함께 쓰면 버그 하나가 남의 데이터를 덮어씁니다. 악성 프로그램은 다른 프로그램이 들고 있는 비밀번호를 그냥 읽어 갑니다.

그래서 운영체제는 프로그램마다 따로 보호 도메인을 줍니다. 그리고 손댈 수 있는 것이 가장 많은 보호 도메인 하나를 자기 몫으로 둡니다. 운영체제의 중심 코드인 커널이 도는 영역입니다.

커널과 일반 프로그램을 가르는 두 장치

운영체제는 하드웨어 장치 둘로 경계를 긋습니다. 하나는 CPU(Central Processing Unit, 중앙 처리 장치)의 실행 모드입니다. 다른 하나는 프로그램마다 따로 주는 주소 지도입니다.

CPU 는 적어도 두 가지 모드로 돕니다. 커널 모드에서는 모든 명령을 쓸 수 있습니다. 사용자 모드에서는 장치를 직접 다루거나 메모리 설정을 바꾸는 명령이 막힙니다. 일반 프로그램은 사용자 모드로 돕니다. 커널만 커널 모드로 돕니다.

메모리 쪽은 가상 메모리가 맡습니다. 가상 메모리는 프로그램마다 자기만의 주소 지도를 주고, 그 지도의 주소를 실제 메모리 위치로 바꿔 줍니다. 바꾸는 일은 CPU 안의 MMU(Memory Management Unit, 메모리 관리 장치)가 합니다.

프로그램 A 의 주소 지도에는 프로그램 B 의 메모리가 아예 없습니다. 그래서 A 는 B 를 가리킬 방법조차 없습니다.

커널의 메모리는 모든 주소 지도에 들어 있지만 커널 모드에서만 접근되게 표시됩니다. 사용자 모드에서 그 주소를 건드리면 CPU 가 막고 커널에 알립니다.

flowchart TD
    subgraph U["사용자 모드 · 프로그램마다 따로"]
        A["프로세스 A · 자기 메모리"]
        B["프로세스 B · 자기 메모리"]
    end
    subgraph K["커널 모드"]
        KR["커널 · 커널 메모리 · 장치"]
    end
    A -->|시스템 콜| KR
    B -->|시스템 콜| KR

그림에서 두 프로세스 사이에는 선이 없습니다. 서로의 메모리를 못 보기 때문입니다. 위에서 아래로 내려가는 길은 시스템 콜 하나뿐입니다. 시스템 콜은 프로그램이 커널에 일을 청하는 정해진 문입니다. 파일을 열거나 네트워크로 보내려면 이 문을 지나야 합니다. 커널이 청을 확인한 뒤 대신 해 줍니다.

은행 창구에 견주면 쉽습니다. 손님은 창구 너머 금고에 직접 손을 넣지 못합니다. 창구에서 청하면 직원이 확인한 뒤 대신 꺼내 줍니다. 창구는 시스템 콜, 직원은 커널, 금고는 커널 메모리에 해당합니다.

보안 코드 자신을 지키는 일

시스템 보안에서 도메인 분리가 특히 중요한 까닭은 접근을 검사하는 코드가 검사받는 프로그램들과 같은 기계에서 돌기 때문입니다. 경계를 긋지 않으면 검사받는 프로그램이 검사하는 코드의 메모리를 그대로 만질 수 있습니다.

참조 모니터는 「누가 무엇에 접근해도 되나」를 판정하는 장치입니다. 파일을 열 때 권한을 확인하는 커널 코드가 그 예입니다. 참조 모니터가 믿을 만하려면 세 가지가 서야 합니다. 첫째, 모든 접근이 빠짐없이 이것을 거쳐야 합니다. 둘째, 누구도 이것을 고칠 수 없어야 합니다. 셋째, 검증할 수 있을 만큼 작아야 합니다.

도메인 분리는 이 중 둘째를 맡습니다. 검사하는 코드가 자기만의 보호 도메인에서 돌면, 검사받는 프로그램은 그 코드를 고쳐 쓸 수 없습니다. 검사 코드가 일반 프로그램과 같은 메모리에 있다면 공격자는 검사를 통과하려 애쓸 필요가 없습니다. 검사 코드를 지우면 됩니다.

보안을 지키는 데 쓰이는 하드웨어와 소프트웨어를 통틀어 신뢰 컴퓨팅 기반(TCB, Trusted Computing Base)이라고 부릅니다. 도메인 분리는 이 신뢰 컴퓨팅 기반을 그 밖의 코드와 갈라 놓는 원칙이기도 합니다.

같은 생각은 커널 말고 다른 경계에도 쓰입니다. 가상 머신은 하드웨어를 흉내 내어 운영체제 하나를 통째로 돌리는 환경입니다. 하이퍼바이저는 한 기계에서 가상 머신 여럿을 돌리면서 서로 갈라 놓는 소프트웨어입니다. 이렇게 가상 머신끼리 가르는 것도 도메인 분리입니다.

믿지 못할 코드를 샌드박스 안에서 돌리는 것도 마찬가지입니다. 샌드박스는 프로그램이 손댈 수 있는 파일과 자원을 좁게 묶어 둔 실행 환경입니다.

암호에서 말하는 도메인

이제 암호 쪽입니다. 여기서 도메인은 해시 함수나 키 하나가 쓰이는 용도 하나를 가리킵니다.

해시 함수는 어떤 길이의 입력이든 받아 정해진 길이의 값으로 줄이는 함수입니다. 같은 입력에는 언제나 같은 값을 냅니다. 입력이 조금만 달라도 값은 전혀 달라집니다.

문제는 해시 함수가 입력이 어디에 쓰일지 모른다는 데 있습니다. 바이트가 같으면 용도가 달라도 같은 값이 나옵니다. 그래서 한 용도로 만든 해시 값을 다른 용도의 해시 값이라고 내밀어도 받는 쪽은 구별하지 못합니다.

도메인 분리는 입력 앞에 용도마다 다른 꼬리표를 붙여 이 문제를 막습니다. 데이터가 같아도 꼬리표가 다르면 해시 값이 달라집니다. 그러면 한 용도의 값은 다른 용도에서 쓸모가 없어집니다.

꼬리표가 빠진 머클 트리

머클 트리는 데이터 여러 개를 해시 값 하나로 묶는 트리입니다. 맨 아래 잎마다 데이터 하나의 해시를 둡니다. 그 위로는 이웃한 두 해시를 이어 붙여 다시 해시하기를 되풀이합니다. 맨 위에 남는 하나를 뿌리라고 부릅니다.

뿌리 하나만 믿으면 아래 데이터 전부가 맞는지 확인할 수 있습니다. 그런데 잎과 내부 노드를 같은 방식으로 해시하면 구멍이 생깁니다.

내부 노드의 입력은 두 자식 해시를 이어 붙인 바이트열입니다. 공격자는 이 바이트열 자체를 잎 데이터 하나라고 내밉니다. 그러면 그 가짜 잎의 해시는 원래 내부 노드의 해시와 같아집니다. 잎 목록은 달라졌는데 뿌리는 그대로입니다.

아래 그림과 식에서는 해시 함수를 H 로, x 의 해시 값을 H(x) 로 적습니다. ‖ 는 두 바이트열을 이어 붙인다는 표시입니다. a·b·c·d 는 잎 데이터 넷 각각의 해시 값입니다.

flowchart TD
    subgraph T1["원래 트리 · 잎 넷"]
        R1["뿌리"] --> N1["노드 = H(a ‖ b)"]
        R1 --> N2["노드 = H(c ‖ d)"]
        N1 --> L1["잎 해시 a"]
        N1 --> L2["잎 해시 b"]
        N2 --> L3["잎 해시 c"]
        N2 --> L4["잎 해시 d"]
    end
    subgraph T2["가짜 트리 · 잎 셋"]
        R2["같은 뿌리"] --> F["가짜 잎 데이터 = a ‖ b"]
        R2 --> M2["노드 = H(c ‖ d)"]
        M2 --> P3["잎 해시 c"]
        M2 --> P4["잎 해시 d"]
    end

아래 트리의 가짜 잎은 위 트리의 a ‖ b 를 그대로 데이터로 쓴 것입니다. 이 데이터를 해시하면 H(a ‖ b) 가 되어 원래 노드와 같은 값이 나옵니다. 잎은 넷에서 셋이 됐지만 두 트리의 뿌리는 같습니다.

이 공격은 해시 함수가 튼튼해도 통합니다. 해시 함수가 깨진 것이 아니라 잎과 노드를 구별하지 못한 것이기 때문입니다.

제2 원상 저항성은 주어진 입력과 같은 해시를 내는 다른 입력을 찾기 어렵다는 성질입니다. 이 공격은 같은 뿌리를 내는 다른 잎 목록을 찾아냈습니다. 트리 전체를 해시 함수 하나로 보면 이 성질이 깨진 셈입니다.

막는 법은 잎과 노드에 서로 다른 꼬리표를 붙이는 것입니다. 흔히 잎 앞에는 바이트 0x00 을, 내부 노드 앞에는 0x01 을 붙입니다.

H(0x00 ‖ 데이터)           // 잎 해시
H(0x01 ‖ 왼쪽 ‖ 오른쪽)    // 노드 해시

이제 공격자가 a ‖ b 를 잎으로 내밀면 H(0x00 ‖ a ‖ b) 가 계산됩니다. 원래 노드는 H(0x01 ‖ a ‖ b) 였으니 두 값이 다릅니다. 뿌리도 달라져서 가짜 트리가 드러납니다.

서명 토큰이 둔갑하는 경우

백엔드에서는 서명 토큰에서 같은 문제를 만납니다. HMAC(Hash-based Message Authentication Code, 해시 기반 메시지 인증 코드)은 비밀 키와 해시 함수로 메시지에 서명값을 붙이는 방법입니다. 키를 모르는 사람은 서명값을 못 만들므로 토큰을 위조할 수 없습니다.

서비스 하나가 키 k 하나로 두 가지 토큰을 만든다고 합시다. 하나는 로그인 토큰입니다. 다른 하나는 비밀번호 재설정 링크에 넣는 토큰입니다. 둘 다 사용자 번호만 담아 서명하면 서명값이 같아집니다.

로그인  HMAC(k, "user=42")  // s1
재설정  HMAC(k, "user=42")  // s1

메일로 받은 재설정 토큰을 로그인 토큰 자리에 넣어도 서버는 통과시킵니다. 재설정 메일을 엿본 사람이 그 계정으로 로그인할 수 있게 됩니다.

서명할 문자열 앞에 용도를 붙이면 두 값이 갈립니다.

로그인  HMAC(k, "login|user=42")  // s1
재설정  HMAC(k, "reset|user=42")  // s2

용도마다 키를 따로 쓰는 방법도 있습니다. 주 키 하나에서 용도 이름을 넣어 하위 키를 뽑아내는 키 유도 함수를 쓰면 키를 여러 벌 보관하지 않아도 됩니다. 이 역시 용도 이름을 꼬리표로 쓰는 도메인 분리입니다.

가르는 때와 치르는 대가

운영체제의 도메인 분리는 개발자가 고를 일이 드뭅니다. 운영체제가 언제나 켜 둡니다. 고를 일이 생기는 때는 믿지 못할 코드를 돌릴 때입니다. 사용자가 올린 코드나 외부 플러그인은 별도 프로세스나 샌드박스에 띄워 자기 서비스와 다른 보호 도메인에 둡니다.

대가는 경계를 넘을 때마다 드는 시간입니다. 시스템 콜 하나마다 CPU 가 모드를 바꾸고 커널이 청을 확인합니다. 작은 읽기와 쓰기를 아주 여러 번 하면 이 시간이 쌓입니다. 그래서 데이터를 모아서 한 번에 넘기는 방식이 흔합니다.

암호의 도메인 분리는 해시 함수나 키 하나를 두 가지 이상 용도에 쓸 때마다 필요합니다. 용도가 하나뿐이면 꼬리표가 할 일이 없습니다. 다만 나중에 용도가 늘어날 수 있으므로 처음부터 붙여 두는 쪽이 안전합니다.

꼬리표 자체의 계산 비용은 몇 바이트를 더 해시하는 것뿐입니다. 진짜 비용은 나중에 바꾸기 어렵다는 데 있습니다. 꼬리표를 바꾸면 이미 만든 해시 값과 서명이 전부 안 맞습니다. 그래서 꼬리표는 설계할 때 정하고 그대로 지킵니다.

관련 항목

도메인 분리와 함께 쓰이는 보안 설계 원칙

최소 권한 · 심층 방어 · 격리 · 참조 모니터 · 완전한 중재 · 신뢰 컴퓨팅 기반 · 공격 표면 · 보안 엔지니어링

운영체제가 보호 도메인을 가르는 장치

보호 도메인 · 커널 · 사용자 모드와 커널 모드 · 보호 링 · 가상 메모리 · MMU · 페이지 테이블 · 주소 공간 · 시스템 콜 · 트랩 · 프로세스

같은 원칙으로 경계를 긋는 실행 환경

가상 머신 · 하이퍼바이저 · 샌드박스 · 컨테이너 런타임 · 네임스페이스 · 신뢰 실행 환경

암호에서 꼬리표로 용도를 가르는 기법

해시 함수 · 머클 트리 · 메시지 인증 코드 · HMAC · 키 유도 함수 · HKDF · 전자 서명 · 인증서 투명성

꼬리표가 빠지면 깨지는 성질과 공격

제2 원상 저항성 · 충돌 저항성 · 원상 저항성 · 제2 원상 공격 · 교차 프로토콜 공격 · 토큰

다른 이름: domain separation