9. 압축/암호화 협상과 폴백 (전송 변환 파이프라인)
난이도 상해설 — 압축/암호화 협상과 폴백 (전송 변환 파이프라인)
난이도: 상
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
핸드셰이크에서 압축/암호화 알고리즘을 협상하고, 패킷 헤더 1바이트 flags 로 적용 여부를 표시하는 파이프라인이다. 골격(서버 우선순위 협상, 압축→암호화 순서)은 맞지만, 보안 다운그레이드(협상 실패 시 조용한 평문 폴백), flags 만 있고 "어떤 알고리즘인지"가 와이어에 없음(Comp/Crypt 가 세션 상태에만 존재) → 버전 공존 시 송수신 알고리즘 불일치, 역변환 순서와 인증 검증 누락, **수신 측 flags 무신뢰(공격자 강제 다운그레이드)**가 문제다. 핵심: "변환을 무엇으로 적용했는가"는 세션 합의 + 와이어 식별이 모두 필요하고, 보안 변환의 협상은 절대 조용히 약화되면 안 된다.
문제점
(B) 협상 실패 시 조용한 평문/무압축 폴백 — 보안(다운그레이드)
- 증상: 공통 알고리즘이 없으면
x.Crypt = None으로 남고 그냥 진행 → 암호화 없이 게임 전체가 평문으로 흐른다. 로그/거부도 없다. - 재현조건: MITM 이 핸드셰이크의
clientCrypt목록을 비우거나, 의도적으로 "약한 것만" 남기는 다운그레이드 공격. 또는 버전 차로 공통 알고리즘이 없을 때. - 근본원인: 암호화는 협상 대상이되 "암호화 없음"은 허용 정책이 되면 안 된다. 실패는 폴백이 아니라 연결 거부여야 한다(또는 명시적 안전한 최소선 보장).
(핵심) 와이어에 "알고리즘 식별자" 부재 — 호환성/정확성
- 증상: 헤더 flags 는 "압축됨/암호화됨" 비트만 있고 어떤 알고리즘인지는 없다.
복호화는
x.Crypt(세션 상태)에 의존. 그런데 세션의Comp/Crypt는 협상 결과를 양쪽이 각자 따로 저장한다. 신/구 버전이 협상 메시지 포맷을 다르게 해석하거나 우선순위가 어긋나면, 송신은 Zstd 로 압축했는데 수신은 Deflate 로 풀어 데이터가 깨진다. - 재현조건: 신버전 서버(Zstd 우선) ↔ 구버전 클라(협상 메시지에 Zstd 없음/오해석). flags 의 Compressed 비트만 보고 세션에 저장된 (잘못된) 알고리즘으로 해동.
- 근본원인: "적용된 변환의 정확한 정체"가 와이어에 실려있지 않고 세션 합의에만 의존. 합의가 어긋나는 순간 silent corruption. flags 의 표현력이 부족.
(D)+(C) 역변환 순서·인증 검증 — 보안/정확성
- 증상: Encode 는 압축→암호화 순, Decode 는 복호화→해동 순(대칭은 맞다). 하지만
- AES-CBC 같은 인증 없는 암호화(unauthenticated) 를 쓰면 변조 탐지 불가 (padding oracle 등). AES-GCM(AEAD)이 아닌 폴백이 위험.
- "압축 후 암호화"는 CRIME/BREACH 류 압축 사이드채널에 노출될 수 있다 (압축률로 평문 추론). 비밀이 섞인 채널이면 순서/정책 재고 필요.
- 근본원인: 변환 조합의 보안 속성(AEAD 여부, compress-then-encrypt 위험)을 정책으로 못박지 않음.
(D) 수신 측이 flags 를 무조건 신뢰 — 보안
- 증상:
Decode는 패킷 flags 만 보고 변환을 적용/생략. 협상에서 "암호화 필수"로 합의했어도, 공격자가 Encrypted 비트를 끈 평문 패킷을 주입하면 서버가 그대로 수용. - 근본원인: 협상된 정책과 수신 패킷 flags 의 일치를 강제하지 않음. "암호화 켜기로 했으면, 암호화 안 된 패킷은 거부"가 빠졌다.
(부수) 길이/널 검증 — 견고성
Decode가packet.Length >= 1검사 없이packet[0]접근. 빈 패킷에서 예외. 매 호출 배열 신규 할당(GC 압력).
수정안
(B) 폴백 금지 — 협상 실패는 거부
public bool Negotiate(SessionXform x, CompAlgo[] cc, CryptAlgo[] cy)
{
foreach (var c in ServerComp)
if (Array.IndexOf(cc, c) >= 0) { x.Comp = c; break; }
x.Crypt = CryptAlgo.None;
foreach (var c in ServerCrypt)
if (c != CryptAlgo.None && Array.IndexOf(cy, c) >= 0) { x.Crypt = c; break; }
// 암호화는 필수 — 공통 AEAD 가 없으면 연결 거부 (조용한 평문 금지)
if (x.Crypt == CryptAlgo.None) return false;
return true;
}
(핵심) 협상 결과를 양측이 "확정"하고, 필요한 경우 와이어에 알고리즘 ID
- 협상은 단순 "각자 추론"이 아니라 서버가 고른 결과를 클라에 명시 통보 → 클라 ack (3-way 가 아니면 최소 서버 결정 통보). 양쪽 세션 상태가 같음을 보장.
- flags 1바이트로 부족하면 헤더에 알고리즘 enum 바이트를 넣어 self-describing 하게:
[byte flags][byte compAlgo][byte cryptAlgo][body] // 또는 협상 시 고정하고 세션에 ID 봉인
세션에 고정하더라도, 협상 단계에서 algo id 까지 확정해 송수신이 같은 값을 쓰게 한다.
(D) 정책 강제 + 인증 암호화
public byte[] Decode(SessionXform x, byte[] packet, SessionPolicy pol)
{
if (packet.Length < 1) throw new ProtocolException("empty");
var flags = (XformFlags)packet[0];
// 협상 정책과 불일치하면 거부 — 다운그레이드 주입 방지
if (pol.RequireEncryption && (flags & XformFlags.Encrypted) == 0)
throw new ProtocolException("unencrypted packet rejected");
var body = packet.AsSpan(1);
if ((flags & XformFlags.Encrypted) != 0)
body = DecryptAead(x.Crypt, body); // AEAD: 복호화+무결성 검증 동시
if ((flags & XformFlags.Compressed) != 0)
body = Decompress(x.Comp, body, maxOut: pol.MaxDecompressed); // 압축폭탄 상한
return body.ToArray();
}
- AEAD(AES-GCM/ChaCha20-Poly1305) 강제, flags 도 AAD 에 포함해 비트 변조 탐지.
- 압축 해제 상한(decompression bomb 방어): 풀린 크기를 제한.
더 나은 설계
- TLS/DTLS/QUIC 같은 검증된 전송 보안을 우선 고려. 직접 만들 때만 아래.
- 암호화는 필수·AEAD·다운그레이드 불가를 정책 상수로 못박고, 협상 실패는 거부.
- 협상 결과를 명시적으로 확정·교환하고, 가능하면 패킷이 self-describing 하게 알고리즘을 식별(또는 키 ID/epoch). 키 롤오버 시 epoch 로 송수신 동기화.
- flags/헤더를 AAD 로 무결성에 포함 → 비트 플립 변조 탐지.
- 압축은 비밀 섞인 채널에선 주의(CRIME/BREACH). 게임 좌표 등 저민감 데이터에 한정하거나 per-record 분리.
트레이드오프
- 패킷에 알고리즘 ID 를 실으면 1~2바이트 오버헤드, 대신 버전 공존/키 롤오버에 견고. 세션 고정 + 협상 확정으로 갈음하면 오버헤드 0 이지만 협상 정확성에 더 의존.
- AEAD 강제는 약한 클라(구형 디바이스)를 배제할 수 있어, 최소 보안선을 정책으로 합의해야.
면접 포인트
- "협상 실패 시 평문 폴백이 왜 위험한가?" → 다운그레이드 공격. MITM 이 협상을 약화시켜 평문/약한 암호로 떨어뜨림. 암호화는 폴백 대상이 아니라 실패 시 연결 거부여야.
- "패킷에 압축됨/암호화됨 비트만 있고 알고리즘은 세션에 둔다. 무슨 문제?" → 송수신 세션 합의가 어긋나면(버전/협상 오류) flags 만으로는 같은 알고리즘 보장 못 함 → silent corruption. self-describing 또는 협상 확정 필요.
- "압축과 암호화 순서, AEAD 가 왜 중요한가?" → compress-then-encrypt 는 CRIME/BREACH 사이드채널 위험. 비인증 암호(CBC)는 변조/패딩 오라클 취약. AEAD 로 기밀성+무결성, 헤더는 AAD 로 묶어 검증.
내가 놓친 항목 (복습용)
- [ ] (B) 협상 실패 → 조용한 평문 폴백(다운그레이드)
- [ ] 와이어에 알고리즘 식별 부재 → 세션 합의 어긋나면 송수신 변환 불일치
- [ ] (D) flags 무신뢰 수용 → 다운그레이드 패킷 주입, 정책 미강제
- [ ] (D)(C) 비AEAD 암호/압축 사이드채널, 압축폭탄 상한 부재
- [ ] (D) 길이 검증/할당 비용
해설 — 압축/암호화 협상과 폴백 (전송 변환 파이프라인)
난이도: 상
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
핸드셰이크에서 압축/암호화 알고리즘을 협상하고, 패킷 헤더 1바이트 flags 로 적용 여부를
표시하는 파이프라인이다. 골격(서버 우선순위 협상, 압축→암호화 순서)은 맞지만,
보안 다운그레이드(협상 실패 시 조용한 평문 폴백), flags 만 있고 "어떤 알고리즘인지"가
와이어에 없음(Comp/Crypt 가 세션 상태에만 존재) → 버전 공존 시 송수신 알고리즘 불일치,
역변환 순서와 인증 검증 누락, **수신 측 flags 무신뢰(공격자 강제 다운그레이드)**가 문제다.
C++ 특유로 Decode 가 길이 검증 없이 packet[0]/packet+1을 읽어 빈 패킷에서 오버리드한다.
핵심: "변환을 무엇으로 적용했는가"는 세션 합의 + 와이어 식별이 모두 필요하고,
보안 변환의 협상은 절대 조용히 약화되면 안 된다.
문제점
(B) 협상 실패 시 조용한 평문/무압축 폴백 — 보안(다운그레이드)
- 증상: 공통 알고리즘이 없으면
x.Crypt = None으로 남고 그냥 진행 → 암호화 없이 게임 전체가 평문으로 흐른다. 로그/거부도 없다. - 재현조건: MITM 이 핸드셰이크의
clientCrypt목록을 비우거나, 의도적으로 "약한 것만" 남기는 다운그레이드 공격. 또는 버전 차로 공통 알고리즘이 없을 때. - 근본원인: 암호화는 협상 대상이되 "암호화 없음"은 허용 정책이 되면 안 된다. 실패는 폴백이 아니라 연결 거부여야 한다(또는 명시적 안전한 최소선 보장).
(핵심) 와이어에 "알고리즘 식별자" 부재 — 호환성/정확성
- 증상: 헤더 flags 는 "압축됨/암호화됨" 비트만 있고 어떤 알고리즘인지는 없다.
복호화는
x.Crypt(세션 상태)에 의존. 그런데 세션의Comp/Crypt는 협상 결과를 양쪽이 각자 따로 저장한다. 신/구 버전이 우선순위를 다르게 해석하면 송신은 Zstd 로 압축했는데 수신은 Deflate 로 풀어 데이터가 깨진다. - 재현조건: 신버전 서버(Zstd 우선) ↔ 구버전 클라(협상 메시지에 Zstd 없음/오해석).
- 근본원인: "적용된 변환의 정확한 정체"가 와이어에 실려있지 않고 세션 합의에만 의존. 합의가 어긋나는 순간 silent corruption. flags 의 표현력이 부족.
(D)+(C) 역변환 순서·인증 검증 — 보안/정확성
- 증상: Encode 는 압축→암호화 순, Decode 는 복호화→해동 순(대칭은 맞다). 하지만
- AES-CBC 같은 인증 없는 암호화(unauthenticated) 를 쓰면 변조 탐지 불가 (padding oracle 등). AES-GCM(AEAD)이 아닌 폴백이 위험.
- "압축 후 암호화"는 CRIME/BREACH 류 압축 사이드채널에 노출될 수 있다(압축률로 평문 추론).
- 근본원인: 변환 조합의 보안 속성(AEAD 여부, compress-then-encrypt 위험)을 정책으로 못박지 않음.
(D) 수신 측이 flags 를 무조건 신뢰 — 보안
- 증상:
Decode는 패킷 flags 만 보고 변환을 적용/생략. 협상에서 "암호화 필수"로 합의했어도, 공격자가 Encrypted 비트를 끈 평문 패킷을 주입하면 서버가 그대로 수용. - 근본원인: 협상된 정책과 수신 패킷 flags 의 일치를 강제하지 않음. "암호화 켜기로 했으면, 암호화 안 된 패킷은 거부"가 빠졌다.
(부수) 길이 무검증 + 매 호출 할당 — 견고성/메모리안전
Decode가len >= 1검사 없이packet[0]접근. 빈 패킷에서 오버리드(UB), 그리고len == 0이면std::vector(packet+1, packet+0)가 역순 이터레이터 → UB. 매 호출std::vector신규 할당(GC가 아니라 malloc/free 압력).
수정안
(B) 폴백 금지 — 협상 실패는 거부
bool Negotiate(SessionXform& x,
const std::vector<CompAlgo>& cc, const std::vector<CryptAlgo>& cy)
{
for (auto c : ServerComp)
if (std::find(cc.begin(), cc.end(), c) != cc.end()) { x.Comp = c; break; }
x.Crypt = CryptAlgo::None;
for (auto c : ServerCrypt)
if (c != CryptAlgo::None && std::find(cy.begin(), cy.end(), c) != cy.end()) { x.Crypt = c; break; }
// 암호화는 필수 — 공통 AEAD 가 없으면 연결 거부 (조용한 평문 금지)
return x.Crypt != CryptAlgo::None;
}
(핵심) 협상 결과를 양측이 "확정"하고, 필요하면 와이어에 알고리즘 ID
- 협상은 "각자 추론"이 아니라 서버가 고른 결과를 클라에 명시 통보 → 클라 ack. 양쪽 세션 상태가 같음을 보장.
- flags 1바이트로 부족하면 헤더에 알고리즘 enum 바이트를 넣어 self-describing 하게:
[uint8 flags][uint8 compAlgo][uint8 cryptAlgo][body] // 또는 협상 시 고정하고 세션에 ID 봉인
(D) 정책 강제 + 인증 암호화 + 길이 검증
std::vector<uint8_t> Decode(const SessionXform& x, const uint8_t* packet, size_t len,
const SessionPolicy& pol)
{
if (len < 1) throw std::runtime_error("empty packet"); // 길이 검증
uint8_t flags = packet[0];
// 협상 정책과 불일치하면 거부 — 다운그레이드 주입 방지
if (pol.RequireEncryption && !(flags & XF_Encrypted))
throw std::runtime_error("unencrypted packet rejected");
std::vector<uint8_t> body(packet + 1, packet + len);
if (flags & XF_Encrypted)
body = DecryptAead(x.Crypt, body); // AEAD: 복호화+무결성 검증 동시
if (flags & XF_Compressed)
body = Decompress(x.Comp, body, pol.MaxDecompressed); // 압축폭탄 상한
return body;
}
- AEAD(AES-GCM/ChaCha20-Poly1305) 강제, flags 도 AAD 에 포함해 비트 변조 탐지.
- 압축 해제 상한(decompression bomb 방어): 풀린 크기를 제한.
더 나은 설계
- TLS/DTLS/QUIC 같은 검증된 전송 보안을 우선 고려. 직접 만들 때만 아래.
- 암호화는 필수·AEAD·다운그레이드 불가를 정책 상수로 못박고, 협상 실패는 거부.
- 협상 결과를 명시적으로 확정·교환하고, 가능하면 패킷이 self-describing 하게 알고리즘을 식별(또는 키 ID/epoch). 키 롤오버 시 epoch 로 송수신 동기화.
- flags/헤더를 AAD 로 무결성에 포함 → 비트 플립 변조 탐지.
- 압축은 비밀 섞인 채널에선 주의(CRIME/BREACH). 저민감 데이터에 한정하거나 per-record 분리.
트레이드오프
- 패킷에 알고리즘 ID 를 실으면 1~2바이트 오버헤드, 대신 버전 공존/키 롤오버에 견고.
- AEAD 강제는 약한 클라(구형 디바이스)를 배제할 수 있어, 최소 보안선을 정책으로 합의해야.
면접 포인트
- "협상 실패 시 평문 폴백이 왜 위험한가?" → 다운그레이드 공격. MITM 이 협상을 약화시켜 평문/약한 암호로 떨어뜨림. 암호화는 폴백 대상이 아니라 실패 시 연결 거부여야.
- "패킷에 압축됨/암호화됨 비트만 있고 알고리즘은 세션에 둔다. 무슨 문제?" → 송수신 세션 합의가 어긋나면(버전/협상 오류) flags 만으로는 같은 알고리즘 보장 못 함 → silent corruption. self-describing 또는 협상 확정 필요.
- "압축과 암호화 순서, AEAD 가 왜 중요한가?" → compress-then-encrypt 는 CRIME/BREACH 사이드채널 위험. 비인증 암호(CBC)는 변조/패딩 오라클 취약. AEAD 로 기밀성+무결성, 헤더는 AAD 로 묶어 검증.
내가 놓친 항목 (복습용)
- [ ] (B) 협상 실패 → 조용한 평문 폴백(다운그레이드)
- [ ] 와이어에 알고리즘 식별 부재 → 세션 합의 어긋나면 송수신 변환 불일치
- [ ] (D) flags 무신뢰 수용 → 다운그레이드 패킷 주입, 정책 미강제
- [ ] (D)(C) 비AEAD 암호/압축 사이드채널, 압축폭탄 상한 부재
- [ ] (D) Decode 길이 무검증(빈 패킷 오버리드/역순 이터레이터 UB) + 매 호출 할당 비용