HMAC(Hash-based Message Authentication Code)은 패킷의 위변조 여부와 송신자의 인증을 검증하기 위한 메시지 인증 방식이다.
게임 서버와 클라이언트가 통신할 때 패킷 자체를 암호화하지 않더라도, HMAC을 사용하면 공격자가 패킷 내용을 임의로 수정했는지 검증할 수 있다.
HMAC이 필요한 이유
네트워크 패킷은 전송 과정에서 공격자에게 노출되거나 변조될 수 있다.
예를 들어 게임에서 다음과 같은 패킷을 보낸다고 하자.
USER_ID=1001
ITEM_ID=500
AMOUNT=100
공격자가 패킷을 가로채서
USER_ID=1001
ITEM_ID=500
AMOUNT=999999
로 변경하면 서버가 이를 그대로 처리할 위험이 있다.
단순히 패킷 구조나 데이터 타입을 검증하는 것만으로는 패킷을 누가 만들었는지를 검증할 수 없다.
HMAC은 이 문제를 해결한다.
HMAC의 기본 구조
HMAC은 비밀 키와 메시지를 이용해서 인증값을 생성한다.
HMAC = HMAC(HashAlgorithm, SecretKey, Message)
예를 들어 SHA-256을 사용하면 다음과 같이 구성할 수 있다.
HMAC-SHA256(SecretKey, PacketData)
클라이언트와 서버가 동일한 비밀 키를 가지고 있다고 가정한다.
Client -> Packet + HMAC
Server -> HMAC 검증 -> Packet 처리
서버는 전달받은 PacketData와 자신이 가지고 있는 SecretKey로 HMAC을 다시 계산한다.
Received HMAC == Calculated HMAC
이면 패킷이 검증된 것으로 판단한다.
패킷 구조
일반적으로 다음과 같은 형태로 구성할 수 있다.
[Header][Payload][HMAC]
예를 들어:
Header
Command = Move
Payload
UserID = 1001
Position = (10, 20, 30)
HMAC
9F8A...
서버는 HMAC을 계산할 때 일반적으로 인증해야 하는 데이터 전체를 대상으로 한다.
Message = Header + Payload
HMAC = HMAC-SHA256(SecretKey, Message)
패킷 검증 과정
서버가 패킷을 수신하면 다음과 같은 순서로 처리할 수 있다.
Receive Packet -> Parse Header -> Extract Payload/HMAC -> Recalculate HMAC -> Compare -> Process
예를 들어 공격자가 Payload를 변경하면:
Original Payload -> HMAC 생성
Modified Payload -> 기존 HMAC과 불일치
서버에서 검증에 실패한다.
Received HMAC != Calculated HMAC
따라서 해당 패킷을 폐기할 수 있다.
HMAC은 암호화가 아니다
가장 중요한 부분이다.
HMAC은 데이터를 숨기는 기술이 아니다.
Encryption -> Confidentiality
HMAC -> Integrity + Authentication
HMAC을 사용해도 공격자는 패킷의 내용을 볼 수 있다.
예를 들어:
USER_ID=1001
AMOUNT=100
HMAC=...
이라는 패킷을 전송한다고 해도 암호화하지 않았다면 내용 자체는 노출될 수 있다.
HMAC이 보장하는 것은 다음에 가깝다.
"이 데이터가 비밀 키를 가진 주체에 의해 생성되었는가?"
"전송 과정에서 데이터가 변경되지 않았는가?"
내용을 숨겨야 한다면 TLS 또는 별도의 암호화 계층이 필요하다.
Hash와 HMAC의 차이
단순 Hash와 HMAC을 혼동하면 안 된다.
일반적인 Hash는 다음과 같다.
Hash(Message)
HMAC은 다음과 같다.
HMAC(SecretKey, Message)
공격자가 다음과 같이 패킷을 변경했다고 하자.
Message -> ModifiedMessage
단순 Hash라면 공격자는 새로운 Hash도 계산할 수 있다.
Hash(ModifiedMessage)
따라서 Hash만으로는 공격자를 막을 수 없다.
반면 HMAC은 SecretKey가 필요하다.
HMAC(SecretKey, ModifiedMessage)
공격자가 SecretKey를 모르면 올바른 인증값을 생성하기 어렵다.
HMAC과 디지털 서명의 차이
HMAC과 디지털 서명은 모두 무결성과 인증에 사용할 수 있지만 구조가 다르다.
| 구분 | HMAC | 디지털 서명 |
|---|---|---|
| 방식 | 대칭 키 | 비대칭 키 |
| 키 | 동일한 Secret Key | Private/Public Key |
| 생성 | Secret Key | Private Key |
| 검증 | Secret Key | Public Key |
| 성능 | 빠름 | 상대적으로 느림 |
| 주요 용도 | 서버-클라이언트 인증 | 공개 검증이 필요한 인증 |
게임 서버처럼 서버와 클라이언트 사이의 통신 인증이 목적이라면 HMAC이 비교적 단순하고 빠르다.
다만 클라이언트에 SecretKey를 넣는 순간 중요한 문제가 발생한다.
클라이언트에 SecretKey를 넣으면 안전한가
PC 게임이나 Unity 게임에서는 특히 중요하다.
클라이언트는 공격자의 환경에서 실행될 수 있다.
따라서 다음과 같은 구조는 근본적으로 취약하다.
Unity Client
SecretKey = "ABC123..."
공격자는 클라이언트 바이너리를 분석하거나 메모리를 확인해서 SecretKey를 추출할 수 있다.
SecretKey가 노출되면 공격자는 정상 클라이언트와 동일한 HMAC을 생성할 수 있다.
Attacker
-> Extract SecretKey
-> Modify Packet
-> Generate Valid HMAC
-> Server
따라서 HMAC을 사용했다고 해서 클라이언트 위변조 문제가 해결되는 것은 아니다.
게임 서버에서의 올바른 관점
게임에서는 클라이언트 자체를 신뢰하면 안 된다.
HMAC은 보안의 전부가 아니라 방어 계층 중 하나로 보는 것이 적절하다.
예를 들어:
Client -> Authentication -> Session Key -> HMAC Packet -> Server Validation -> Server-side Rule Validation
서버에서는 HMAC뿐만 아니라 실제 게임 규칙도 검증해야 한다.
예를 들어 이동 패킷이라면:
HMAC 검증
-> Session 검증
-> User 검증
-> Sequence 검증
-> 이동 거리 검증
-> 이동 속도 검증
-> Server State 검증
-> 적용
HMAC이 정상이라고 해서 그 행동이 정상적인 행동이라는 의미는 아니다.
Replay Attack 문제
HMAC을 단순하게 사용하면 Replay Attack이 발생할 수 있다.
정상적인 패킷이 다음과 같다고 하자.
BuyItem
ItemID = 100
Amount = 1
HMAC = ABCDEF...
공격자가 이 패킷을 저장했다가 나중에 그대로 다시 전송할 수 있다.
Packet -> Server
Packet -> Server
Packet -> Server
Packet -> Server
패킷 내용이 변경되지 않았기 때문에 HMAC 검증은 계속 통과한다.
HMAC은 패킷이 변조되지 않았다는 사실을 검증할 뿐, 처음 전송된 패킷인지까지 보장하지 않는다.
Replay Attack 방어
이를 방어하기 위해 패킷에 추가적인 상태값을 넣을 수 있다.
대표적으로:
- Sequence Number
- Nonce
- Timestamp
- Request ID
등을 사용할 수 있다.
예를 들어:
[Sequence][Command][Payload][HMAC]
HMAC 대상도 Sequence까지 포함한다.
HMAC = HMAC(SecretKey, Sequence + Command + Payload)
서버는 마지막 Sequence를 관리한다.
Received Sequence <= Last Sequence
-> Reject
정상적인 경우:
100 -> 101 -> 102 -> 103
공격자가 이전 패킷을 다시 보내면:
103 -> 101
서버가 거부한다.
Timestamp 방식
Timestamp를 사용하는 방법도 있다.
Timestamp + Payload + HMAC
서버에서 허용 가능한 시간 범위를 검사한다.
abs(ServerTime - PacketTime) <= AllowedWindow
예를 들어 5초의 허용 범위를 설정한다면 오래된 패킷을 거부할 수 있다.
다만 Timestamp만으로는 동일한 패킷을 짧은 시간 동안 반복하는 공격을 완전히 막을 수 없다.
실무에서는 Nonce, Sequence Number, Request ID 등의 상태값을 함께 사용하는 것이 더 적절하다.
HMAC 검증 시 주의점
HMAC 비교는 가능하면 constant-time comparison을 사용하는 것이 좋다.
단순 문자열 비교는 구현에 따라 비교 시간이 입력값에 영향을 받을 수 있다.
따라서 보안 라이브러리가 제공하는 안전한 비교 함수를 사용하는 것이 좋다.
또한 HMAC 알고리즘은 직접 구현하지 않는 것이 원칙이다.
Bad
-> 직접 SHA-256/HMAC 구현
Good
-> 검증된 암호화 라이브러리 사용
일반적인 선택지는 다음과 같다.
HMAC-SHA-256
HMAC-SHA-512
MD5나 SHA-1 기반의 오래된 구조는 신규 시스템에서 사용할 이유가 거의 없다.
HMAC 키 관리
HMAC에서 가장 중요한 자산은 결국 SecretKey다.
Algorithm Security
+
SecretKey Security
+
Protocol Design
특히 서버 측 SecretKey는 외부에 노출되어서는 안 된다.
서버 환경에서는 환경 변수나 Secret Manager 등의 방법으로 관리하고 소스 코드에 하드코딩하지 않는 것이 좋다.
Bad
const SECRET_KEY = "my-secret-key";
보다는:
Environment
HMAC_SECRET_KEY
Server
-> Load Secret
-> Initialize HMAC
형태가 적절하다.
실시간 게임 서버에서의 적용
실시간 게임에서는 모든 패킷에 무거운 보안 처리를 적용하는 것이 반드시 좋은 것은 아니다.
예를 들어 초당 수십~수백 개가 발생할 수 있는 이동/회전 패킷과 결제/아이템 획득 같은 중요 패킷은 보안 요구사항이 다르다.
Movement
-> Server Authority
-> Rate Limit
-> State Validation
Inventory
-> HMAC
-> Sequence
-> Server Validation
Purchase
-> Authentication
-> HMAC
-> Replay Protection
-> Server-side Transaction Validation
특히 중요한 것은 클라이언트가 보낸 값을 그대로 게임 상태로 반영하지 않는 것이다.
HMAC은 데이터의 진위를 보조하지만 최종적인 게임 상태의 권한은 서버가 가져야 한다.
전체적인 보안 구조
실제 게임 네트워크에서는 하나의 기술만으로 해결하기보다 계층적으로 구성하는 것이 적절하다.
Transport Security -> Authentication -> Session Management -> HMAC -> Replay Protection -> Rate Limit -> Server-side Validation
각 계층의 책임을 분리해야 한다.
TLS
-> 통신 구간 보호
Authentication
-> 사용자 인증
Session
-> 연결 및 사용자 상태 관리
HMAC
-> 패킷 무결성 및 메시지 인증
Sequence / Nonce
-> Replay Attack 방어
Rate Limit
-> 과도한 요청 제한
Server Validation
-> 비정상 게임 로직 차단
핵심 정리
HMAC은 패킷을 암호화하는 기술이 아니라 패킷의 무결성과 인증을 검증하는 기술이다.
핵심 구조는 다음과 같다.
SecretKey + Message -> HMAC
서버에서는:
Receive Packet -> Recalculate HMAC -> Compare -> Validate -> Process
다만 게임 클라이언트에 SecretKey가 존재한다면 키 탈취 가능성이 있기 때문에 HMAC만으로 클라이언트를 신뢰할 수는 없다.
실제 게임 서버에서는 다음과 같이 접근하는 것이 핵심이다.
HMAC
+
Replay Protection
+
Rate Limit
+
Server Authority
+
Server-side Validation
HMAC은 패킷을 믿게 만드는 기술이 아니라, 패킷이 변조되지 않았음을 검증하는 기술이다. 서버가 최종적으로 믿어야 하는 것은 클라이언트의 주장 자체가 아니라 서버가 검증한 상태다.