사이드 채널
고친 사람 github-actions[bot]
사이드 채널은 프로그램이 내놓는 답이 아니라 일하는 모습을 통해 비밀이 새어 나가는 길입니다. 일하는 데 걸린 시간이나 내보낸 데이터의 길이 같은 흔적이 그 길이 됩니다. 암호가 튼튼하고 계산이 옳아도 공격자는 이 흔적만 모아 비밀을 좁혀 갑니다.
쉽고 빠른 이해
사이드 채널은 답 말고 다른 흔적으로 비밀이 새는 길입니다. 서버가 받은 값을 비밀 값과 앞 글자부터 견주다 틀린 글자에서 바로 멈추면, 거절이 늦게 올수록 앞 글자가 많이 맞았다는 뜻이 됩니다.
이 이름이 따로 있는 까닭은 흔히 지키는 통로 밖으로 새기 때문입니다. 응답 내용을 암호로 가리고 권한을 검사해도 걸린 시간과 데이터 길이는 밖에 보입니다.
어떻게 새나:
- 비밀 값에 따라 프로그램이 하는 일의 양이 달라집니다
- 그 차이가 걸린 시간이나 데이터 길이 같은 흔적에 묻어납니다
- 공격자가 입력을 바꿔 가며 여러 번 잽니다. 모은 흔적으로 비밀을 한 조각씩 좁힙니다
막는 쪽이 치르는 값도 있습니다. 비밀과 상관없이 늘 같은 양의 일을 해야 해서 느려집니다.
그래서 모든 코드에서 챙기지는 않습니다. 공격자가 몰라야 할 값을 다루는 코드에서 챙깁니다. 그 흔적을 공격자가 여러 번 재 볼 수 있으면 더 챙겨야 합니다. 공개된 값은 흔적이 새도 괜찮습니다.
상세
이 절은 사이드 채널이 무엇이고 어떤 흔적으로 새는지를 먼저 봅니다. 그다음 새는 조건, 공격자가 작은 차이를 읽는 법, 압축과 공유 하드웨어라는 두 사례, 막는 원칙을 차례로 봅니다.
예로는 틀린 글자에서 멈추는 비교 함수 하나를 끝까지 들고 갑니다. 백엔드 코드에서 가장 쉽게 만드는 사이드 채널이기 때문입니다.
봉투의 두께
합격 통지를 우편으로 받던 시절 이야기가 있습니다. 합격자에게는 안내서가 잔뜩 든 두꺼운 봉투가 갑니다. 불합격자에게는 종이 한 장 든 얇은 봉투가 갑니다. 봉투를 뜯지 않아도 우편함에서 꺼내는 순간 결과를 압니다.
사이드 채널에서 봉투 두께 노릇을 하는 것이 프로그램이 일하며 남기는 흔적입니다. 내용은 봉인돼 있어도 흔적이 내용을 알려 줍니다.
정해진 통로와 그 밖의 흔적
프로그램이 정보를 내보내는 통로는 보통 정해져 있습니다. 반환값, 응답 본문, 로그가 그렇습니다. 설계자는 이 통로에 무엇을 실을지 정합니다. 비밀은 이 통로에 실리지 않게 지킵니다.
프로그램은 일하는 동안 설계자가 통로로 여기지 않은 흔적도 남깁니다. 걸린 시간, 쓴 전력, 내보낸 데이터의 길이가 그렇습니다. 이 흔적이 비밀 값에 따라 달라지면 흔적이 곧 비밀을 나르는 또 하나의 길이 됩니다. 이 뜻하지 않은 길을 사이드 채널이라고 부릅니다.
정해진 통로를 지키는 보안 장치는 사이드 채널을 못 봅니다. 암호화는 내용을 못 읽게 바꾸는 일입니다. 접근 제어는 누가 무엇을 읽을지 정하는 일입니다. 둘 다 통로에 실린 내용을 지킬 뿐 일하는 데 걸린 시간은 건드리지 않습니다.
아래 그림은 한 프로그램에서 나가는 두 갈래와 그중 지키는 쪽을 보입니다.
flowchart TD
P["프로그램"] --> T
P --> H
subgraph T["정해진 통로 · 암호화와 접근 제어가 지킨다"]
T1["반환값 · 응답 본문 · 로그"]
end
subgraph H["그 밖의 흔적 · 지키는 것이 없다"]
H1["걸린 시간 · 전력 · 데이터 길이"]
end
H --> A["공격자가 잰다"]
사이드 채널을 노려 비밀을 알아내는 일을 사이드 채널 공격이라고 부릅니다. 걸린 시간을 읽는 타이밍 공격이 그 한 갈래입니다.
은닉 채널은 이름이 비슷하지만 다른 것입니다. 은닉 채널은 안에 있는 프로그램이 일부러 정보를 몰래 내보내려고 흔적을 만듭니다. 사이드 채널은 아무도 내보낼 뜻이 없어도 샙니다.
새는 흔적
어떤 흔적을 잴 수 있는지는 공격자가 어디에 있느냐에 따라 갈립니다. 네트워크 너머에 있으면 응답 시간과 데이터 길이만 봅니다. 같은 기계 안에 있거나 기계를 손에 쥐고 있으면 더 많은 것을 잽니다.
같은 기계 안에서 새는 흔적의 대표는 CPU 캐시입니다. CPU 캐시는 CPU(Central Processing Unit, 중앙 처리 장치) 곁에 둔 작은 메모리입니다. 최근에 읽은 메모리 칸은 여기 남아서 다음번에 주 메모리보다 빨리 읽힙니다.
아래 표는 흔적 다섯 가지를 공격자가 기계에 가까워야 하는 순서로 늘어놓은 것입니다.
| 흔적 | 비밀에 따라 달라지는 것 | 공격자가 있어야 하는 곳 |
|---|---|---|
| 걸린 시간 | 비교가 몇 글자에서 멈추나 · 어느 분기로 가나 | 네트워크 너머도 된다 |
| 데이터 길이 | 압축된 뒤 크기 · 응답 본문 크기 | 네트워크 너머도 된다 |
| CPU 캐시 상태 | 최근에 어느 메모리 칸을 읽었나 | 같은 기계 |
| 전력 소비 | 연산마다 드는 전력 | 기계 곁 |
| 전자기파 · 소리 | 회로와 부품이 내는 신호 | 기계 곁 |
백엔드 개발자가 주로 챙길 것은 위의 셋입니다. 서버는 대개 네트워크 너머의 공격자를 상대합니다. 클라우드에서는 남의 프로그램과 한 물리 서버를 나눠 쓰기도 합니다. 아래 둘은 스마트카드처럼 공격자가 기계를 손에 넣을 수 있는 물건에서 주로 문제가 됩니다.
새는 조건
사이드 채널이 생기려면 두 가지가 겹쳐야 합니다. 하나라도 빠지면 흔적이 있어도 비밀은 안 샙니다.
flowchart TD
A["비밀 값에 따라 하는 일이 달라지나"] -->|아니다| N["흔적이 비밀을 안 나른다"]
A -->|그렇다| B["공격자가 그 차이를 잴 수 있나"]
B -->|아니다| N
B -->|그렇다| C["사이드 채널이 생긴다"]
첫째, 비밀 값에 따라 프로그램이 하는 일이 달라져야 합니다. 분기가 갈리거나, 읽는 메모리 칸이 바뀌거나, 내보내는 데이터 길이가 변하는 경우입니다. 공개된 값에 따라서만 달라진다면 흔적이 새도 잃을 것이 없습니다.
둘째, 공격자가 그 차이를 잴 수 있어야 합니다. 입력을 바꿔 가며 되풀이해 잴 수 있으면 훨씬 위험해집니다. 한 번 잰 값은 잡음에 묻혀도 여러 번 잰 값은 모이면서 차이를 드러내기 때문입니다.
틀린 글자에서 멈추는 비교
두 조건이 한 함수에서 어떻게 겹치는지 봅니다. 서버가 요청에 실려 온 인증값을 자기가 가진 비밀 값과 견주는 장면입니다. 인증값은 요청을 보낸 쪽이 비밀을 안다는 것을 보이려고 붙이는 짧은 값입니다.
흔히 쓰는 비교 함수 eq 는 앞 글자부터 견주다가 다른 글자를 만나면 바로 거절합니다. 비밀 값이 7f3a 일 때 이 함수가 몇 글자를 보고 멈추는지 세어 보면 이렇습니다.
eq("0000", "7f3a") // 1글자 보고 거절
eq("7000", "7f3a") // 2글자 보고 거절
eq("7f00", "7f3a") // 3글자 보고 거절
eq("7f3a", "7f3a") // 4글자 보고 통과
앞의 세 입력은 똑같이 거절됩니다. 그런데 거절까지 걸린 시간은 셋이 다 다릅니다. 앞 글자가 많이 맞을수록 함수가 늦게 끝나기 때문입니다.
이 함수는 두 조건을 다 채웁니다. 멈추는 시점이 비밀 값에 달려 있습니다. 공격자는 응답 시간을 재면서 입력을 얼마든지 바꿔 보낼 수 있습니다. 답은 옳게 내지만 걸린 시간이 비밀을 한 글자씩 흘립니다.
작은 차이를 읽어 내는 법
글자 하나를 더 견주는 시간은 아주 짧습니다. 응답이 네트워크를 건너오는 시간은 그보다 훨씬 크게 흔들립니다. 이 흔들림을 지터라고 부릅니다.
그래서 공격자는 같은 입력을 여러 번 보냅니다. 흔들림은 매번 방향이 제멋대로라서 여러 번 잰 값을 모으면 서로 상쇄됩니다. 비교가 한 글자 더 가서 생긴 차이는 매번 같은 쪽으로 쌓여 남습니다.
공격자는 비밀을 한꺼번에 맞히지 않고 한 조각씩 맞힙니다. 첫 글자 후보마다 시간을 재서 가장 늦게 거절된 후보를 고릅니다. 그다음 글자로 넘어가 같은 일을 되풀이합니다.
flowchart TD
S["다음 글자 자리"] --> M["후보 글자마다 같은 요청을 여러 번 보내 시간을 모은다"]
M --> P["가장 늦게 거절된 후보를 그 자리 글자로 정한다"]
P --> D{"끝까지 맞혔나"}
D -->|아니다| S
D -->|그렇다| E["비밀 전체를 안다"]
한 조각씩 맞히면 따져 볼 경우가 크게 줄어듭니다. 앞의 7f3a 는 16진 네 글자라서 한 자리에 올 수 있는 글자가 열여섯 가지입니다. 네 글자를 한 번에 맞히려면 16×16×16×16=65536가지를 따져야 합니다. 한 자리씩 맞히면 16×4=64가지만 재면 됩니다.
이렇게 한 조각씩 좁혀 가는 방식이 사이드 채널 공격의 흔한 모양입니다. 흔적 하나가 알려 주는 것은 적습니다. 대신 흔적이 비밀의 한 조각과 묶여 있으면 조각을 차례로 풀어 전체에 닿습니다.
압축된 길이로 새는 비밀
데이터 길이도 사이드 채널이 됩니다. 이 소절은 압축과 암호화를 함께 쓸 때 길이가 어떻게 비밀을 흘리는지 봅니다.
압축은 되풀이되는 글자열을 줄이는 일입니다. 같은 글자열이 두 번 나오면 두 번째는 앞의 것을 가리키는 짧은 표시로 바뀝니다. 그래서 되풀이가 많은 데이터일수록 더 짧아집니다.
암호화는 내용을 가리지만 길이는 대체로 원문을 따라갑니다. 짧은 원문은 짧은 암호문이 됩니다. 긴 원문은 긴 암호문이 됩니다. 내용은 못 읽어도 길이는 밖에 보입니다.
문제는 비밀과 공격자가 넣은 값이 한 덩이로 압축될 때 생깁니다. 흔한 장면은 이렇습니다. 응답 페이지에 로그인 토큰이 들어 있습니다. 사용자가 보낸 검색어도 같은 페이지에 되찍혀 함께 압축됩니다.
공격자는 검색어를 마음대로 넣을 수 있습니다. 맞히려는 것은 토큰입니다. 토큰이 token=7f3a 라면 공격자는 검색어로 token=0, token=7 처럼 끝 글자를 바꿔 넣습니다. 추측이 토큰의 앞부분과 겹치면 되풀이가 길어져 압축본이 짧아집니다.
flowchart TD
A["토큰 token=7f3a 와 추측을 한 덩이로 압축한다"] --> B{"추측이 토큰 앞부분과 겹치나"}
B -->|"겹친다 · 추측 token=7"| C["겹친 부분이 앞을 가리키는 짧은 표시로 바뀐다"]
C --> D["압축본이 짧다"]
B -->|"끝 글자가 어긋난다 · 추측 token=0"| E["어긋난 글자는 그대로 남는다"]
E --> F["압축본이 길다"]
공격자는 추측을 한 글자씩 바꿔 가며 길이가 줄어드는 쪽을 따라갑니다. 아래 그림은 공격자와 서버가 한 글자를 맞히려고 주고받는 한 바퀴입니다.
sequenceDiagram
participant 공격자
participant 서버
공격자->>서버: 추측한 글자를 넣은 요청
Note over 서버: 추측과 비밀을 한 덩이로 압축하고 암호화한다
서버-->>공격자: 암호화된 응답
Note over 공격자: 길이가 줄었으면 추측이 비밀과 겹쳤다
Note over 공격자: 다음 글자로 넘어가 되풀이한다
공격자는 응답 내용을 한 글자도 못 읽습니다. 길이 하나만 보고 비밀을 맞혀 갑니다.
한 기계를 나눠 쓸 때
같은 기계 안에서는 잴 수 있는 흔적이 늘어납니다. 여러 프로그램이 한 CPU 와 한 CPU 캐시를 나눠 쓰기 때문입니다.
격리는 프로그램끼리 서로의 메모리를 못 읽게 떼어 놓는 일입니다. 운영체제가 프로세스마다 메모리를 따로 주는 것이 가장 흔한 격리입니다. 클라우드에서 한 물리 서버를 여러 고객이 나눠 쓸 때도 고객끼리 격리됩니다.
샌드박스는 믿지 못할 코드를 가둬 두고 허락한 일만 하게 하는 실행 환경입니다. 남이 보낸 코드를 내 기계에서 돌려야 할 때 씁니다.
격리와 샌드박스는 남의 메모리를 직접 읽지 못하게 막습니다. 그러나 CPU 캐시는 여전히 함께 씁니다. 캐시는 작아서 누가 새 칸을 읽으면 그 칸이 올라옵니다. 대신 다른 칸이 밀려납니다.
그래서 남이 무엇을 읽었는지가 내 읽기 속도에 묻어납니다. 남이 방금 읽은 칸을 나도 읽으면 캐시에 있어서 빨리 읽힙니다. 남의 읽기에 밀려난 내 칸은 주 메모리에서 다시 가져와야 해서 늦게 읽힙니다.
비밀을 다루는 쪽을 피해 프로그램, 흔적을 재는 쪽을 옆 프로그램이라고 부르겠습니다. 피해 프로그램이 비밀에 따라 어느 칸을 읽는다면 옆 프로그램은 제 읽기 속도만 재서 그 칸을 짐작합니다. 아래 그림은 두 프로그램이 함께 읽는 칸 하나를 두고 벌어지는 일입니다. 두 프로그램이 함께 쓰는 라이브러리 코드가 그런 칸입니다.
sequenceDiagram
participant V as 피해 프로그램
participant C as CPU 캐시
participant O as 옆 프로그램
Note over V,O: 격리돼도 캐시는 함께 쓴다
V->>C: 비밀에 따라 고른 칸을 읽는다
Note over C: 그 칸이 캐시에 남는다
O->>C: 같은 칸을 읽고 걸린 시간을 잰다
C-->>O: 빨리 읽힌다
Note over O: 빨랐으면 피해 프로그램이 그 칸을 읽었다
그래서 믿지 못할 코드를 돌리는 환경은 정밀한 시계를 일부러 흐리게 주기도 합니다. 시간을 잘게 못 재면 캐시의 시간차를 가려내기 어려워집니다. WebAssembly처럼 남이 보낸 코드를 샌드박스에서 돌리는 환경이 이 문제를 안고 있습니다.
막는 원칙
막는 원칙은 하나입니다. 흔적이 비밀 값에 따라 달라지지 않게 만듭니다. 새는 조건 둘 중 첫째를 끊는 것입니다.
틀린 글자에서 멈추는 비교라면 다른 글자를 만나도 멈추지 않고 끝까지 견준 뒤 한 번에 판정합니다. 걸리는 시간이 입력과 상관없이 일정해서 이런 비교를 상수 시간 비교라고 부릅니다. 앞의 입력을 끝까지 견주는 함수 eqAll 에 넣으면 멈추는 시점이 모두 같아집니다.
eqAll("0000", "7f3a") // 4글자 보고 거절
eqAll("7f00", "7f3a") // 4글자 보고 거절
eqAll("7f3a", "7f3a") // 4글자 보고 통과
세 입력 모두 네 글자를 다 보고 판정합니다. 이제 걸린 시간은 몇 글자가 맞았는지를 알려 주지 않습니다.
이런 코드는 손으로 짜기 까다롭습니다. 컴파일러가 코드를 빠르게 다듬다가 결과가 이미 정해졌다고 보고 반복을 일찍 끝내 버릴 수 있기 때문입니다. 그래서 비밀을 견주는 코드는 암호 라이브러리가 따로 손본 함수를 씁니다.
흔적마다 끊는 방법이 다릅니다. 데이터 길이를 가릴 때 쓰는 패딩은 데이터 뒤에 뜻 없는 바이트를 덧붙여 길이를 맞추는 일입니다. 아래 표는 앞에서 본 흔적 셋을 막는 방법과 그 대가를 모은 것입니다.
| 흔적 | 막는 방법 | 대가 |
|---|---|---|
| 걸린 시간 | 비밀에 따라 분기하지 않고 끝까지 같은 일을 한다 | 늘 가장 오래 걸리는 경우만큼 일한다 |
| 데이터 길이 | 비밀과 남이 넣은 값을 한 덩이로 압축하지 않는다 · 패딩으로 길이를 맞춘다 | 압축으로 줄이던 크기를 잃는다 · 보낼 데이터가 늘어난다 |
| CPU 캐시 상태 | 비밀 값으로 메모리 칸을 고르지 않는다 · 믿지 못할 코드와 기계를 나눠 쓰지 않는다 | 코드가 느려진다 · 기계를 더 써야 한다 |
응답마다 무작위로 조금씩 기다리게 하는 방법은 잘 안 통합니다. 무작위 지연은 네트워크 흔들림과 성질이 같아서 여러 번 잰 값을 모으면 묻힙니다. 공격자가 보낼 횟수만 늘어나고 차이는 남습니다.
사이드 채널을 챙길 코드
모든 코드가 사이드 채널을 걱정할 필요는 없습니다. 가르는 물음은 앞의 두 조건과 같습니다. 하나는 공격자가 몰라야 하는 비밀을 다루는지입니다. 다른 하나는 공격자가 입력을 바꿔 가며 흔적을 되풀이해 잴 수 있는지입니다.
백엔드에서 두 물음에 모두 「예」가 나오기 쉬운 경우를 추리면 아래와 같습니다.
| 상황 | 샐 수 있는 것 | 흔적 |
|---|---|---|
| 요청의 인증값이나 토큰을 저장된 값과 견준다 | 인증값 자체 | 걸린 시간 |
| 없는 [[사용자 계정 | 계정]]은 비밀번호 확인을 건너뛰고 거절한다 | 그 계정이 있는지 |
| 사용자 입력과 비밀을 한 응답에 담아 압축한다 | 응답 속 비밀 | 데이터 길이 |
| 남의 프로그램과 한 기계에서 암호 연산을 돌린다 | 암호 키 | CPU 캐시 상태 |
둘째 줄은 비밀번호 확인이 일부러 느리게 만든 계산이라서 생깁니다. 있는 계정만 그 계산을 거치니 두 경우의 응답 시간이 벌어집니다. 아래 그림이 고치기 전의 두 갈래입니다.
flowchart TD
R["로그인 요청"] --> Q{"계정이 있나"}
Q -->|없다| X["계산을 건너뛰고 바로 거절한다"]
Q -->|있다| H["비밀번호를 확인한다 · 일부러 오래 걸리게 만든 계산"]
H --> J["맞나 판정한다"]
없는 계정에도 같은 계산을 한 번 돌리고 거절하면 두 갈래의 길이가 같아집니다. 그러면 시간차가 사라집니다.
정렬 기준이나 페이지 번호처럼 공개된 값을 견주는 코드는 끝까지 견줄 필요가 없습니다.
관련 항목
사이드 채널을 노리는 공격
사이드 채널 공격 · 타이밍 공격 · 캐시 타이밍 공격 · 전력 분석 공격 · 전자기파 분석 공격 · 음향 암호 분석 · 트래픽 분석
사이드 채널로 비밀을 빼낸 알려진 공격
스펙터 · 멜트다운 · CRIME · BREACH · Flush+Reload · Prime+Probe
사이드 채널을 막는 기법
상수 시간 비교 · 상수 시간 프로그래밍 · 블라인딩 · 패딩 · 속도 제한
흔적을 남기는 하드웨어 동작
CPU 캐시 · 캐시 미스 · 분기 예측 · 투기적 실행
사이드 채널이 우회하는 보안 장치
암호화 · 접근 제어 · 격리 · 샌드박스 · 가상화 · 신뢰 경계
사이드 채널로 흔적이 새기 쉬운 처리
압축 · 암호문 · 메시지 인증 코드 · 비밀번호 해싱 · 계정 열거 · WebAssembly
사이드 채널과 헷갈리는 이웃
은닉 채널 · 패딩 오라클 공격 · 오라클 공격 · 정보 유출
사이드 채널이 속하는 상위 분류
다른 이름: side channel · 부채널 · 사이드채널