패딩
고친 사람 github-actions[bot]
패딩은 데이터에 뜻 없는 값을 채워 넣어 정해진 크기에 맞춥니다. 채워 넣은 값 자체는 아무것도 가리키지 않습니다. 크기를 맞추는 일만 합니다. 메모리 배치·암호·통신이 저마다 다른 까닭으로 이 채움을 씁니다.
쉽고 빠른 이해
패딩은 정해진 크기에 모자라는 데이터를 아무 값으로 채워 크기를 맞추는 일입니다. 5바이트짜리 데이터를 8바이트 단위로만 다루는 곳에 넘길 때 3바이트를 덧붙이는 것이 패딩입니다.
이게 없으면 반 덩어리가 남습니다. 고정된 크기로만 일하는 하드웨어와 암호 알고리즘은 반 덩어리를 어떻게 다룰지 모릅니다. 반대로 길이를 따로 적어 두고 그만큼만 읽는 곳이라면 채울 일이 없습니다.
어떻게 도는가:
- 보내는 쪽이 모자란 만큼 채움 값을 덧붙입니다
- 어디까지가 채움인지 알아볼 수 있게 규칙을 정해 둡니다
- 받는 쪽이 그 규칙대로 채움을 걷어 내고 원래 데이터만 씁니다
상세
택배 상자에 물건을 담고 남은 곳에 완충재를 채웁니다. 완충재는 받는 사람이 주문한 물건이 아닙니다. 상자를 정해진 모양대로 채워 두는 몫만 합니다. 받는 사람은 완충재를 걷어 내고 물건만 꺼냅니다.
컴퓨터 안에도 데이터를 정해진 크기 단위로만 다루는 곳이 많습니다. 데이터가 그 단위에 못 미치면 남는 곳을 아무 값으로 채웁니다. 이 채움이 패딩입니다. 채움에 쓰는 바이트는 채움 바이트라고 부릅니다.
패딩이 붙는 까닭은 셋입니다. 크기를 정해진 단위에 맞추는 것, 값을 서로 떼어 놓는 것, 데이터의 진짜 길이를 감추는 것입니다. 아래 소절이 그 셋을 차례로 봅니다. 그리고 패딩을 걷어낼 때 생기는 문제와, 이름만 같은 화면 여백을 마지막에 봅니다.
크기를 맞추라고 요구하는 하드웨어와 알고리즘
CPU(Central Processing Unit, 중앙처리장치)는 메모리를 한 바이트씩 읽지 않습니다. 한 번에 네 바이트나 여덟 바이트씩 뭉쳐서 읽습니다. 이 뭉치 하나를 워드라고 부릅니다.
값 하나가 워드 경계를 걸쳐 놓이면 CPU 가 두 번 읽어서 이어 붙여야 합니다. 이 두 번을 없애려고 값의 시작 주소를 자기 크기의 배수에 맞추라는 규칙을 둡니다. 이 규칙이 정렬입니다.
암호도 사정이 비슷합니다. 블록 암호는 정해진 크기의 덩어리 단위로만 계산합니다. 이 덩어리를 블록이라고도 부릅니다. 반 덩어리는 아예 입력으로 받지 못합니다.
두 경우 모두 모자란 만큼을 채워야 일이 시작됩니다. 그 채움이 패딩입니다.
거꾸로 단위가 정해져 있지 않으면 패딩도 필요 없습니다. 길이를 앞에 적어 두고 그만큼만 읽는 길이 접두사 방식이나, 바이트 단위로 이어서 계산하는 스트림 암호는 남는 곳이 아예 안 생깁니다.
메모리에서 필드 사이에 끼는 패딩
구조체는 필드 여럿을 메모리에 나란히 놓습니다. 앞 필드가 끝난 주소가 다음 필드의 정렬 배수에 안 맞으면 컴파일러가 그 사이에 쓰지 않는 바이트를 끼웁니다.
규칙이 하나 더 있습니다. 구조체 전체 크기도 가장 큰 필드의 배수여야 해서 마지막 필드 뒤에도 남는 바이트가 붙습니다. 구조체를 배열로 늘어놓았을 때 둘째 칸부터 정렬이 깨지지 않게 하려는 것입니다.
block-beta columns 8 W1["워드 · 주소 0~3"]:4 W2["워드 · 주소 4~7"]:4 A["char a"]:1 P["패딩 3바이트"]:3 B["int b"]:4 A2["char a"]:1 B2["int b · 워드 둘에 걸친다"]:4 E2["빈 곳"]:3
맨 윗줄이 워드 눈금입니다. 가운뎃줄이 정렬을 맞춘 배치입니다. char 는 한 바이트라 주소 0 에 놓입니다. 뒤따르는 int 는 4 의 배수 주소에 놓여야 하므로 주소 1~3 이 비고 int 가 4 에서 시작합니다. 이 빈 세 바이트가 패딩입니다.
아랫줄은 패딩 없이 붙여 놓았을 때입니다. int 가 주소 1~4 에 놓여 워드 둘을 물고 있어서 CPU 는 값 하나를 읽으려고 워드를 두 번 읽어야 합니다.
구조체가 차지하는 크기는 필드 크기를 더한 값보다 큽니다. 게다가 필드를 어떤 차례로 적느냐에 따라 크기가 달라집니다.
struct A { char a; int b; char c; }; // 12
struct B { int b; char a; char c; }; // 8
int 가 네 바이트이고 4 의 배수 주소에 놓이는 기계에서 잰 크기입니다. 단위는 바이트입니다. A 는 char 뒤에 패딩이 한 번, 끝에 한 번 끼어 12 바이트가 됩니다. B 는 큰 필드를 앞에 모아 8 바이트로 줄었습니다. 담은 값은 둘이 똑같습니다.
같은 눈금에 늘어놓으면 두 크기의 차이가 이렇게 보입니다.
block-beta columns 12 A1["char a"]:1 A2["패딩 3"]:3 A3["int b"]:4 A4["char c"]:1 A5["패딩 3"]:3 B1["int b"]:4 B2["char a"]:1 B3["char c"]:1 B4["패딩 2"]:2 B5["B 는 여기서 끝난다"]:4
패딩을 일부러 넣기도 합니다. 캐시 라인은 CPU 가 메모리를 캐시에 올릴 때 쓰는 고정 크기 덩어리입니다. 캐시는 바이트 하나씩이 아니라 이 덩어리째로 올리고 내립니다.
여러 코어가 저마다 다른 변수를 고칩니다. 그 변수들이 한 캐시 라인에 같이 앉아 있으면 라인이 코어 사이를 오갑니다. 서로 남의 변수는 건드리지 않습니다. 그래도 라인을 주고받는 동안 서로 기다리게 되어 둘 다 느려집니다.
이것을 거짓 공유라고 합니다. 변수 사이에 패딩을 넣어 서로 다른 라인으로 밀어냅니다.
block-beta columns 8 L1["캐시 라인 하나"]:8 X1["변수 x"]:1 Y1["변수 y"]:1 R1["같은 라인에 함께 앉았다"]:6 L2["캐시 라인 1"]:4 L3["캐시 라인 2"]:4 X2["변수 x"]:1 PD["패딩"]:3 Y2["변수 y"]:1 R2["따로 앉았다"]:3
위 두 줄이 밀어내기 전이고 아래 두 줄이 패딩을 넣은 뒤입니다.
두 패딩은 방향이 반대입니다. 앞은 정렬을 맞추다 어쩔 수 없이 생기고, 뒤는 값을 떼어 놓으려고 일부러 넣습니다.
암호가 덩어리를 채우려고 붙이는 패딩
블록 암호는 원문을 정해진 크기의 덩어리로 잘라 하나씩 암호화합니다. 마지막 덩어리는 대개 꽉 차지 않습니다. 모자란 만큼을 채워야 암호화할 수 있습니다.
받는 쪽은 복호한 다음 어디까지가 원래 데이터인지 알아내야 합니다. 채움 값이 자기 길이를 말하게 하는 규칙을 쓰면 됩니다. 세 바이트를 채워야 하면 값이 3 인 바이트를 세 개 붙입니다.
block-beta columns 8 B1["덩어리 1 · 데이터로 꽉 찼다"]:8 D1["덩어리 2 · 데이터 5바이트"]:5 P1["채움 3바이트 · 값은 전부 3"]:3 B2["덩어리 1 · 데이터로 꽉 찼다"]:8 D2["덩어리 2 · 데이터 8바이트로 꽉 찼다"]:8 P2["덩어리 3 · 채움 8바이트 · 값은 전부 8"]:8
위 두 줄은 마지막 덩어리가 모자랄 때입니다. 아래 세 줄은 데이터가 덩어리에 딱 맞아떨어질 때입니다.
받는 쪽은 맨 끝 바이트를 봅니다. 그 값이 3 이면 뒤 세 바이트가 채움입니다.
데이터가 덩어리에 딱 맞아떨어질 때도 덩어리 하나를 통째로 채워 붙입니다. 안 붙이면 마지막 바이트가 데이터인지 채움인지 가릴 길이 없기 때문입니다.
해시 함수도 메시지를 이 덩어리 단위로 먹습니다. 메시지 끝에 채움을 붙이고 원래 길이까지 적어 붙여 전체를 덩어리 크기의 배수로 만듭니다.
길이를 감추려고 넣는 패딩
암호화는 내용을 가리지만 길이는 못 가립니다. 암호문의 길이가 원문의 길이를 거의 그대로 드러냅니다.
길이만 모아도 알아낼 수 있는 것이 있습니다. 오가는 메시지의 길이와 시각을 모아 무엇을 했는지 짚어 내는 기법을 트래픽 분석이라고 합니다. 예 또는 아니오 한 마디를 담은 응답은 길이만으로 어느 쪽인지 갈릴 수 있습니다.
이를 막으려고 TLS(Transport Layer Security, 전송 계층 보안)는 암호화하는 본문 뒤에 패딩을 덧붙일 수 있게 열어 두었습니다. 본문을 늘려 진짜 길이를 흐리는 것입니다.
block-beta columns 12 A1["응답 A · 본문 3바이트"]:3 A2["여기서 끝난다"]:9 B1["응답 B · 본문 9바이트"]:9 B2["여기서 끝난다"]:3 C1["응답 A · 본문 3바이트"]:3 C2["패딩 9바이트"]:9 D1["응답 B · 본문 9바이트"]:9 D2["패딩 3바이트"]:3
위 두 줄은 패딩 없이 보낸 것이라 길이가 다릅니다. 아래 두 줄은 패딩을 붙여 총 길이를 같게 맞춘 것입니다.
받는 쪽은 복호한 다음 뒤에서부터 채움을 걷습니다. 얼마나 채웠는지는 보내는 쪽만 알기 때문입니다. 그만큼 안 보내도 될 바이트를 더 보내는 것이 대가입니다.
패딩을 걷어낼 때 새는 정보
앞 절들처럼 암호문에 붙인 채움은 받는 쪽이 걷어내야 합니다. 그 걷어내기가 공짜가 아닙니다. 받은 데이터의 채움이 규칙에 안 맞으면 어딘가 잘못된 것입니다. 이때 패딩이 틀렸다고 상대에게 알려 주면 그 대답 자체가 정보가 됩니다.
공격자는 암호문을 조금씩 바꿔 보냅니다. 패딩이 맞았는지 틀렸는지만 모아도 원문을 한 바이트씩 알아낼 수 있습니다. 이것을 패딩 오라클 공격이라고 합니다.
sequenceDiagram
participant 공격자
participant 서버
loop 한 바이트를 확정할 때까지
공격자->>서버: 끝 바이트를 바꾼 암호문
서버-->>공격자: 패딩 오류 또는 정상
end
Note over 공격자: 정상이 나온 값 하나가 원문 한 바이트를 확정한다
요즘 암호는 패딩 검사 결과를 밖으로 내보내지 않습니다. 암호화와 위조 검사를 한 연산으로 묶는 인증 암호를 쓰기도 합니다. 위조 검사를 먼저 통과한 것만 복호하므로, 패딩이 맞는지 틀린지 물어볼 기회 자체가 없어집니다.
화면 배치에서 쓰는 같은 이름
웹 화면을 꾸밀 때도 패딩이라는 말을 씁니다. 여기서는 내용과 테두리 사이의 안쪽 여백을 가리킵니다. CSS(Cascading Style Sheets, 종속형 시트)의 padding 속성이 그 값입니다.
크기를 정해진 단위에 맞추려는 것이 아닙니다. 글을 읽기 편하게 벌려 놓으려는 것이라 하는 일이 다릅니다. 빈 곳을 채운다는 뿌리만 같습니다.
관련 항목
패딩이 크기를 맞춰 주는 데이터 단위
바이트 · 워드 · 블록 · 캐시 라인 · 레코드 · 페이지 · 비트
메모리에서 값의 주소를 정하는 규칙
정렬 · 오프셋 · 구조체 · 필드 · 메모리 레이아웃 · 엔디언 · 워드 크기
마지막 덩어리를 채워야 도는 암호 알고리즘
블록 암호 · AES · CBC · 해시 함수 · 머클-담고르 구조 · 인증 암호 · AEAD
패딩을 잘못 다뤄 터지는 공격
패딩 오라클 공격 · 사이드 채널 공격 · 타이밍 공격 · 트래픽 분석 · 상수 시간 비교
패딩을 실제로 실어 나르는 프로토콜과 포맷
TLS · IPsec · Base64 · 직렬화 · 고정 길이 레코드 · 비트 필드
패딩이 더 쓰게 만드는 자원
메모리 · 대역폭 · 처리량 · 메모리 단편화 · 압축
패딩과 반대로 값을 떼어 놓는 기법
다른 이름: padding · 채움 · 채움 바이트