40. 세션 암호화 스트림의 nonce/IV 재사용 (C#)
난이도 최상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// [시나리오] 게임 세션 패킷 암호화 (서버-클라)
//
// 로그인 후 클라·서버는 세션 키를 공유하고, 이후 모든 게임 패킷을
// 스트림 암호(AES-CTR 류: keystream = E(key, nonce) 를 평문과 XOR)로
// 암호화해 주고받는다. 각 패킷마다 nonce 를 하나씩 증가시켜 사용한다.
//
// 재접속(reconnect)이나 세션 재협상 시 SessionCrypto 를 다시 초기화한다.
//
// [요구사항]
// - 같은 (key, nonce) 조합으로 서로 다른 평문을 두 번 암호화해서는 안 된다.
// - 송신 스트림과 수신 스트림, 재접속 전/후가 이 불변식을 깨지 않아야 한다.
//
// [과제]
// 아래 SessionCrypto 를 리뷰하라. (A)(B) 지점의 동작이 위 불변식에
// 어떤 영향을 주는지, 공격자가 무엇을 할 수 있는지 재현과 함께 설명하고
// 수정안을 제시하라. (Encrypt/Decrypt 의 블록 함수 자체는 올바르다고 가정)
using System;
public class SessionCrypto
{
private byte[] _key;
private ulong _nonce; // 패킷마다 증가
public void InitSession(byte[] sessionKey)
{
_key = sessionKey;
// (A)
_nonce = 0;
}
// 재접속/재협상 시 호출 (키는 그대로 재사용, 카운터만 리셋)
public void ResetForReconnect()
{
// (A)
_nonce = 0;
}
public byte[] EncryptOutbound(ReadOnlySpan<byte> plain)
{
// (B)
var keystream = BlockCipher.Ctr(_key, _nonce, plain.Length);
_nonce++;
return Xor(plain, keystream);
}
public byte[] DecryptInbound(ReadOnlySpan<byte> cipher)
{
var keystream = BlockCipher.Ctr(_key, _nonce, cipher.Length);
_nonce++;
return Xor(cipher, keystream);
}
private static byte[] Xor(ReadOnlySpan<byte> a, byte[] k)
{
var o = new byte[a.Length];
for (int i = 0; i < a.Length; i++) o[i] = (byte)(a[i] ^ k[i]);
return o;
}
} 결함 코드 · C++
// [시나리오] 게임 세션 패킷 암호화 (서버-클라)
//
// 세션 키를 공유한 뒤 모든 패킷을 스트림 암호(CTR: keystream = E(key, nonce)
// 를 평문과 XOR)로 주고받는다. 패킷마다 nonce 를 증가시킨다.
// 재접속/재협상 시 SessionCrypto 를 재초기화한다.
//
// [요구사항]
// - 같은 (key, nonce) 로 서로 다른 평문을 두 번 암호화하면 안 된다.
// - 송신/수신 스트림과 재접속 전/후가 이 불변식을 깨지 않아야 한다.
//
// [과제]
// (A)(B) 지점의 동작이 위 불변식에 어떤 영향을 주는지, 공격자가 무엇을
// 할 수 있는지 재현과 함께 설명하고 수정안을 제시하라.
// (BlockCipher::Ctr 자체는 올바르다고 가정)
#include <cstdint>
#include <vector>
#include <cstddef>
struct BlockCipher {
// key, nonce 로부터 length 바이트 keystream 생성 (구현은 올바르다고 가정)
static std::vector<uint8_t> Ctr(const std::vector<uint8_t>& key,
uint64_t nonce, size_t length);
};
class SessionCrypto {
std::vector<uint8_t> key_;
uint64_t nonce_ = 0; // 패킷마다 증가
static std::vector<uint8_t> Xor(const uint8_t* a, const std::vector<uint8_t>& k) {
std::vector<uint8_t> o(k.size());
for (size_t i = 0; i < k.size(); ++i) o[i] = a[i] ^ k[i];
return o;
}
public:
void InitSession(std::vector<uint8_t> sessionKey) {
key_ = std::move(sessionKey);
// (A)
nonce_ = 0;
}
// 재접속/재협상 시 호출 (키는 그대로, 카운터만 리셋)
void ResetForReconnect() {
// (A)
nonce_ = 0;
}
std::vector<uint8_t> EncryptOutbound(const uint8_t* plain, size_t len) {
// (B)
auto ks = BlockCipher::Ctr(key_, nonce_, len);
++nonce_;
return Xor(plain, ks);
}
std::vector<uint8_t> DecryptInbound(const uint8_t* cipher, size_t len) {
auto ks = BlockCipher::Ctr(key_, nonce_, len);
++nonce_;
return Xor(cipher, ks);
}
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.