[보안/네트워크] Diffie-Hellman Key Exchange & RSA (Feat. Forward Secrecy + CTF Python 코드)

전윤혁·2024년 8월 8일

Network Security

목록 보기
5/6

이번 글에서는 비대칭키를 활용하는 Diffie-Hellman Key Exchange에 대해 주로 다루고, RSA와의 비교를 통해 Forward Secrecy라는 개념에 대해서도 알아보도록 하겠다.

해당 글은 아래의 암호화 내용 정리 글과 이어지는 글이다.

https://velog.io/@airoca/%EB%B3%B4%EC%95%88%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%95%94%ED%98%B8%ED%99%94Crypto-%EB%82%B4%EC%9A%A9-%EC%A0%95%EB%A6%AC-Feat.-%EB%8C%80%EC%B9%AD%ED%82%A4-%EB%B9%84%EB%8C%80%EC%B9%AD%ED%82%A4%EA%B3%B5%EA%B0%9C%ED%82%A4


✅ Diffie-Hellman Key Exchange

Diffie-Hellman Key Exchange는 비대칭키 암호화 방식을 사용하여 두 당사자가 공개적으로 정보를 교환하더라도 안전하게 공통의 비밀 키를 공유할 수 있는 방법이다.

각 당사자는 개인키와 공개키를 생성하고, 공개키를 서로 교환한 후, 서로의 공개 키와 자신의 개인 키를 사용해 동일한 공통 비밀 키를 생성하게 된다.

이전 글에서 설명했던 비대칭키 암호화 방식과는 사뭇 다르다. 공통 비밀 키를 이용한다고? 그 이유와 원리를 알아보자.

1. Diffie-Hellman Key Exchange 배경

대칭키 암호화는 계산적으로 빠르고 효율적이지만, Key Exchange Problem, Key Distribution Problem 등이 발생한다고 이전 글에서 설명했었다.

Diffie-Hellman Key Exchange는 대칭키 암호화의 장점을 사용하여 성능을 고려하면서도, 기존 대칭키 암호화의 문제를 해결한 방식이다.

그렇다면 어떻게? 그것이 바로 공개키 기반 시스템을 사용하여, 대칭키 교환을 구현하는 것이다! 이는 Hybrid Cryptosystem이라고 할 수 있는데, 대칭키 교환은 공개키 기반으로 구현하고, 데이터 암호화는 대칭키로 구현하는 것이다.

아래와 같이 좀 더 간단하게 요약할 수 있다.

  • 대칭키 기반 암호화 연결은 효율적이고, 비대칭키(공개키) 기반 암호화 연결은 느리다.

  • 하지만 대칭키 기반 방식에서는 키를 교환할 때 Secure Channel이 필요하고, Secure Channel을 연결하기 위해서는 또다시 키가 필요하다는 순환 문제(Key Exchange Problem)이 생긴다!

  • 그렇다면 대칭키를 교환하는 과정에서 공개키 방식을 사용하여, Insecure Channel에서 안전하게 대칭키를 결정한 후, 이후 데이터 교환은 해당 대칭키를 이용한 방식으로 효율적으로 하자!

이런 흐름이라고 할 수 있다.

그러면 또다시 의문이 생긴다. "공개키 방식을 사용하여 안전하게 대칭키를 결정" 한다는게 정확히 어떻게 한다는걸까?

다음으로 Diffie-Hellman Key Exchange의 원리를 알아보자.

2. Diffie-Hellman Key Exchange 원리

사실 이 방식은 지극히 수학적인 원리를 기반으로 한다. 따라서 이해가 가지 않더라도 "수학적으로 그렇구나~" 정도로 넘어가도 괜찮을 것 같다. 그래도 한번쯤은 이해하려고 시도해보자!

📌 원시근이란?

쉽게 설명하면, 원시근 gg는 어떤 소수 pp에 대해, gg를 거듭제곱해서 나머지를 구할 때 11부터 p1p-1까지 모든 숫자를 만들어낼 수 있는 값이다.

예시에서는 51,52,53...5^1, 5^2, 5^3 ...을 23을 나누로 나눈 나머지가 1부터 22까지 전부 존재한다는 의미이다. (소수 pp에 대해서는 항상 원시근이 존재한다.)

📌 모듈러 연산이란?

어떤 한 숫자를 다른 숫자로 나눈 나머지를 구하는 연산이다.

  1. Alice와 Bob은 공개적으로 사용할 소수 p=23p = 23과 원시근 g=5g = 5를 결정한다.

  2. Alice는 비밀 정수 SAlice=4S_{\text{Alice}} = 4를 선택한 후, Bob에게 A=gSAlicemodpA = g^{S_{\text{Alice}}} \mod p를 계산하여 보낸다.
    A=54mod23=4A = 5^4 \mod 23 = 4

  3. Bob은 비밀 정수 SBob=3S_{\text{Bob}} = 3을 선택한 후, Alice에게 B=gSBobmodpB = g^{S_{\text{Bob}}} \mod p를 보낸다.
    B=53mod23=10B = 5^3 \mod 23 = 10

  4. Alice는 SShared=BSAlicemodpS_{\text{Shared}} = B^{S_{\text{Alice}}} \mod p를 계산한다.
    SShared=104mod23=18S_{\text{Shared}} = 10^4 \mod 23 = 18

  5. Bob은 SShared=ASBobmodpS_{\text{Shared}} = A^{S_{\text{Bob}}} \mod p를 계산한다.
    SShared=43mod23=18S_{\text{Shared}} = 4^3 \mod 23 = 18

  6. 이제 Alice와 Bob은 1818이라는 비밀 값을 공유하게 된다.
    Alice와 Bob은 모두 동일한 SSharedS_{\text{Shared}} 값을 얻게 되는데, 이는 공통된 모듈러 연산에서 일관된 결과를 얻기 때문이다.

그림으로 과정을 다시 보면 위와 같다.

중요한 점은, 수학적 원리를 사용하여 암호화가 되지 않은 Insecure Channel에서 성공적으로 대칭키를 정했다는 점이다.

📌 Authenticate?

Diffie-Hellman Key Exchange가 비밀 키를 안전하게 교환하는 문제를 해결한다는 점은 알았다. 그렇다면 해당 방식이 인증(Authenticate)에 대한 문제도 해결해주는가?

그렇지 않다. Diffie-Hellman Key Exchange를 통해 Bob과의 통신을 암호화했더라도, 대화하고 있는 상대방이 정말 Bob인지는 알 수 없다. 왜? 공격자가 Bob으로 위장하고 Alice와 키 교환을 진행하더라도 Alice는 알 수 없기 때문이다.

이 부분에 대해서는 다음 글인 TLS 파트에서 다루겠지만, 먼저 언급하자면 Diffie-Hellman Key Exchange은 단독으로 사용되지 않고, 이전에 설명한 디지털 서명(Digital Signature) 등이 함께 사용된다.


✅ Forward Secrecy

RSA 공개키 시스템을 사용해도 안전한 키 교환을 구현할 수 있지만, 웹 브라우저와 웹 서버가 기본적으로 Diffie-Hellman을 선호하는 이유는 Forward Secrecy(전방향 비밀성)과 관련이 있다.

1. RSA Key Exchange

먼저 RSA 공개키 시스템을 사용한 키 교환 과정을 간단히 살펴보자.

  1. Alice와 Bob은 각각 RSA 공개키와 개인키 쌍을 생성한다.

  2. 서로의 공개키를 교환한다.

  3. Alice는 대칭키 알고리즘(AES 등)을 사용하여 무작위 대칭키를 생성하고, 이를 Bob의 공개키로 암호화하여 Bob에게 보낸다.

  4. Bob은 자신의 개인키로 대칭키를 복호화하여 대칭키를 얻는다.

  5. 양측은 동일한 대칭키를 가지고, 이를 사용하여 데이터를 암호화하고 복호화한다.

대충 보면 별 문제가 없어보인다. 그렇다면 Forward Secrecy가 무엇인지 알아보자.

2. Forward Secrecy란?

Forward Secrecy는 현재의 개인키가 유출되더라도 과거의 통신이 안전하게 유지되도록 하는 개념이다.

Forward Secrecy 충족 시, 만약 개인키가 유출되더라도, 해당 비밀 키를 통해 과거의 통신 내용까지 복호화 할 수는 없게 된다.

따라서 웹 브라우저와 웹 서버가 기본적으로 RSA보다 Diffie-Hellman 키 교환을 선호하는 이유는 RSA가 Forward Secrecy를 지원하지 않기 때문이다.

어떠한 이유에서건 서버의 개인키가 유출되었고, RSA Key Exchange를 사용했을 경우, 과거의 '기록된' 데이터까지 전부 해독되게 된다. 자세한 공격이 궁금하다면 아래의 Forward Secrecy CTF 파트를 참고하자.

3. Diffie-Hellman Ephemeral

그렇다면 Diffie-Hellman은 어떻게 Forward Secrecy를 충족하는 것일까?

Ephemeral이란 특정 시점의 암호화 통신이 종료되면 Session Key를 파기하고, 새로운 Session Key를 사용하는 개념이다.

따라서 Diffie-Hellman Ephemeral은 매 세션마다 새로운 키 쌍(공개키와 개인키)을 생성하는 방식이다. 각 세션마다 새로운 키를 사용하므로, 하나의 세션이 공격받거나 개인키가 유출되더라도, 다른 세션의 보안에는 영향을 미치지 않는다.

RSA에서 Ephemeral 방식을 구현하기 어려운 이유는 정적 키(서버의 고정된 공개키와 개인키)를 사용하기 때문이다. 반면 Diffie-Hellman은 간단하게 새로운 키 쌍을 생성할 수 있기 때문에 Ephemeral 방식을 구현할 수 있다.

📌 Ephemeral 추가 설명

키 교환 과정을 다시 살펴보면, Diffie-Hellman은 대칭키를 만들기 위해 파라미터를 사용하는 것을 알 수 있다. 따라서 파라미터만 바꾸면 대칭키를 쉽게 바꿀 수 있고, Ephemeral 구현이 쉽게 가능하다.

RSA의 경우 Diffie-Hellman에 비해 키 교환 과정이 길고, 위에서 설명한 것처럼 고정된 개인키와 공개키를 이용하는 방식이다. 개인키를 바꾸기 위해서는 공개키와 개인키 쌍을 생성하는 단계부터 반복해야 하기 때문에, Ephemeral 구현이 어렵다.

4. Forward Secrecy CTF

실제로 Forward Secrecy 실습을 시도할 계획이라면 읽어보길 바란다!

RSA Key Exchange의 구체적인 흐름은 위의 그림과 같다. 또한, 각 변수의 구체적인 의미는 아래와 같다.

아래는 실제로 Forward Secrecy CTF를 진행한 파이썬 코드 예시이다.

import codecs
# doc https://scapy.readthedocs.io/en/latest/api/scapy.layers.tls.cert.html
# source code https://github.com/secdev/scapy/blob/master/scapy/layers/tls/cert.p
from scapy.layers.tls.crypto.prf import PRF
from scapy.layers.tls.cert import PrivKeyRSA
import binascii

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad


# Useful Parameters for scapy API
TLS_12 = 0x303
SHA256 = "SHA256"

# Useful functions for converting between hex string and binary data in Python3
def hex_to_data(hex_str):
    return bytearray.fromhex(hex_str)

def data_to_hex(data):
    return bytearray(data).hex()


server_pubkey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv0TmsnW3jQdQHHc7doWL6DkxZLNBk7/OGZefY4PYtZWpz8oJc9uWKjFjZfdW2qbEo0zawWzf6wo2oY/Tu8WjZ0NBKmJdtOoMNtazUzkezAf8RwmTZW7l9TjsFI3xTRJQWTw+QxDByoQgLBtQyrrn/CTukJGpYfUEQTwrsP3OnnEJ9+r8aPKvs46JMOG73AmLDf86C+Ivv7buV12DXloXPmewYEYMk/pdsVvmTCLq6ac67Bzy5vH3lbiZzhwEPy6Qiix6PHSeSMmBM7Ra7IdfLPtSoShxpcozaKwMcSF4Jc/7pnhhzrnm0FDMHugU8Bc3jDQahQnmsfq0imGB03VEEwIDAQAB"

client_random = "50ba2c6dd9a809d3560429e5d4f36584b0120909ba76e642f012ed847d1711fe"

server_random = "19a75738be10c27b2206d0acd7678336ee0c1a36f13e47109df94e9a7823ecc2"


"""
1. Decrypt the premaster secret
"""

premaster_secret = ''
enc_premaster_secret = "92c4684b5c1bb97aa3cd3bf8caf33cc659b1e3294d8f98618eb4f961792985ec75d18088f760db4096be2b894f5778a73e0f40b118120bd306340a158be3a770fc173977fceb7b1f1fad35f6cfbbe2efa4dcc7b4b9f798879b6ff22e190e3f75e194333e00472a7c6370425c4ef1702ed3a9166a2c27a1fe2587dc13794192cd0677b49e600e77ea153dce079ea34756bd813de352f3aeae9a09b9369cc16a79c8cd51d48bf484b08a6fc3f245812236ea10285ce347e41a93f0a398ec6f8b8b2edcd55d10fe35bb88ebbabb556d6d42544886f462bce76c1515b6ad0ed1f547cf4a1a9ba423853ffa99d174dfba8071d6808155ab4d9ac6866a472df7a77106"
enc_premaster_secret = binascii.unhexlify(enc_premaster_secret)
server_priv_key = PrivKeyRSA('key.pem')

# Decrypt pre-master secret 
premaster_secret = server_priv_key.decrypt(enc_premaster_secret)

print('Decrypted Premaster Secret: %s' % data_to_hex(premaster_secret))


"""
2. Caclulate Master Secret
"""


master_secret = ''

client_random = binascii.unhexlify(client_random)
server_random = binascii.unhexlify(server_random)

prf = PRF(SHA256,TLS_12)
master_secret = prf.compute_master_secret(premaster_secret, client_random, server_random, extms=False, handshake_hash=None)

print('Master Secret: %s' % data_to_hex(master_secret))


"""
3. Calculate the Following: 

    1) Client Write Key,
    2) Client Write IV,
    3) Client Write Mac Key,
    4) Server Write Key,
    5) Server Write IV,
    6) Server Write Mac Key
 
"""


key_block = prf.derive_key_block(master_secret, server_random, client_random, 128)

print('Key Block: %s ' % data_to_hex(key_block))

client_write_key = ''
client_write_IV = ''
client_write_MAC_key = ''

server_write_key = ''
server_write_IV = ''
server_write_MAC_key = ''

client_write_IV = data_to_hex(key_block[112:128])
server_write_IV = data_to_hex(key_block[96:112])
client_write_key = data_to_hex(key_block[80:96])
server_write_key = data_to_hex(key_block[64:80])
client_write_MAC_key = data_to_hex(key_block[32:64])
server_write_MAC_key = data_to_hex(key_block[0:32])

print('Client Write Key: %s' % client_write_key) 
print('Client Write IV: %s' % client_write_IV) 
print('client Write MAC Key: %s' % client_write_MAC_key)

print('Server Write Key: %s' % server_write_key) 
print('Server Write IV: %s' % server_write_IV) 
print('Server Write MAC Key: %s' % server_write_MAC_key) 


"""
4. Decrypt the Message, Get the Flag.
"""
flag = ''


client_write_IV = hex_to_data(client_write_IV)
server_write_IV = hex_to_data(server_write_IV)
client_write_key = hex_to_data(client_write_key)
server_write_key = hex_to_data(server_write_key)
client_write_MAC_key = hex_to_data(client_write_MAC_key)
server_write_MAC_key = hex_to_data(server_write_MAC_key)

enc = bytes.fromhex("e8c9f048fb62f3b24fbc3797754cc4387fc5ac38d0dcd911d7524528bf85abe0398a448023364ffd506dd1a01d32ead7b3b13037f17b1731428f509df91243a9489edc6badb9555ef7d9a584bfc4e5ba94da7470780bbb930997b552e1d45055ebed2c8fe973ab4e19c8bb5a5f8ae24a")
cipher = AES.new(client_write_key,AES.MODE_CBC, client_write_IV)
dec = cipher.decrypt(enc)
plainText = unpad(dec, AES.block_size).split(b" ")

byte_flag = plainText[3][1:46]
flag = str(byte_flag, 'utf-8')

print('Flag: %s' % flag)

공격을 요약하면 아래와 같다.

  • 서버의 개인키를 탈취
    공격자가 서버의 개인키를 탈취한 시나리오에 대한 공격이다.
    server_priv_key = PrivKeyRSA('key.pem')
    코드의 나머지 값들은 해당 개인키를 시작으로 전부 구하게 된 것이다.
    (공개키의 경우 비밀이 아니기 때문에 패킷 캡처만으로 취득할 수 있는 상황)

  • 암호화된 데이터 복호화
    탈취한 개인키를 사용하여 암호화된 프리마스터 개인키를 복호화할 수 있다. 프리마스터 개인키는 세션의 대칭키를 생성하는 데 사용된다.

  • 세션 키 재구성
    복호화된 프리마스터 개인키와 서버 및 클라이언트 랜덤 데이터를 사용하여 세션의 마스터 개인키를 계산할 수 있다.

  • 키 블록 생성
    마스터 개인키를 바탕으로 키 블록을 생성하고, 이를 통해 클라이언트와 서버의 암호화 및 MAC 키를 재구성한다.

  • 암호화된 메시지 복호화
    재구성된 키를 사용하여 암호화된 메시지를 복호화하고, 원문 데이터를 복원할 수 있다.

결론적으로, 서버의 개인키가 탈취된 경우, 공격자는 세션 동안의 모든 암호화된 통신 내용을 복호화할 수 있게 된다. 따라서 개인키가 탈취되었을 때 Forward Secrecy 여부가 중요하게 작용한다는 것이다.


마치며

이전 글과 이번 글에서 다룬 내용의 종착지는 결국 TLS(Transport Layer Security)이다! 다음 글에서는 TLS와 TLS Handshake에 대해서 얘기해보겠다.

profile
전공/개발 지식 정리

0개의 댓글