이산 로그 문제(Discrete Logarithm Problem)
자연수 , 정수 에 대해 을 만족하는 정수 를 구하는 문제이다
Diffie-Hellman 키 교환
1976년 Whitfield Diffie와 Martin Hellman은 공개된 채널로 키를 교환할 수 있는 Diffie-Hellman 키 교환 절차를 만들어냈다. 예시를 들어 설명해보겠다. 키 교환에서 통신을 진행하는 가상의 두 인물을 Alice와 Bob이라고 한다. 둘 사이에서는 다음의 절차에 따라 교환이 이루어진다.
1. 키를 교환하고자 하는 Alice는 소수 와 을 만족하는 적당한 수 를 정해 Bob에게 전송합니다. 는 보통 이상의 큰 소수로 설정합니다.
2. 다시 Alice는 을 만족하는 적당한 수 를 정하여 를 Bob에게 전송합니다.
3. Alice로 부터 와 를 받은 Bob은 을 만족하는 적당한 수 를 정해 를 Alice에게 전송합니다.
4. Alice는 Bob이 보낸 를 제곱하여 를 계산하고, Bob은 Alice가 보낸 를 제곱하여 를 계산합니다. Alice와 Bob은 계산한 를 통신의 키로 사용하게 됩니다.
Diffie-Hellman에 대한 중간자 공격
네트워크로 통신하는 두 주체는 서로의 신원을 확인하기 어렵다. 중간자 공격(Man-In-The-Middle attack, MITM)은 네트워크의 이런 특성을 이용한 공격이다. 중간자 공격은 능동적 공격으로 통신을 도청하면서 중간에 데이터를 위변조할 수 있다.
먼저, Alice와 Bob이 통신할 때, 공격자는 Alice에게 자신을 Bob이라고, 그리고 Bob에게는 자신을 Alice라고 속인다. Alice와 Bob은 통신하는 상대방이 공격자인지, 아니면 신뢰하는 상대방인지 알 수 없다.
challenge.py
#!/usr/bin/python3
from Crypto.Util.number import getPrime
from Crypto.Util.Padding import pad, unpad
from Crypto.Cipher import AES
import hashlib
import random
class Person(object):
def __init__(self, p):
self.p = p
self.g = 2
self.x = random.randint(2, self.p - 1)
def calc_key(self):
self.k = pow(self.g, self.x, self.p)
return self.k
def set_shared_key(self, k):
self.sk = pow(k, self.x, self.p)
aes_key = hashlib.md5(str(self.sk).encode()).digest()
self.cipher = AES.new(aes_key, AES.MODE_ECB)
def encrypt(self, pt):
return self.cipher.encrypt(pad(pt, 16)).hex()
def decrypt(self, ct):
return unpad(self.cipher.decrypt(bytes.fromhex(ct)), 16)
flag = open("flag", "r").read().encode()
prime = getPrime(1024)
print(f"Prime: {hex(prime)}")
alice = Person(prime)
bob = Person(prime)
alice_k = alice.calc_key()
print(f"Alice sends her key to Bob. Key: {hex(alice_k)}")
print("Let's inturrupt !")
alice_k = int(input(">> "))
if alice_k == alice.g:
exit("Malicious key !!")
bob.set_shared_key(alice_k)
bob_k = bob.calc_key()
print(f"Bob sends his key to Alice. Key: {hex(bob_k)}")
print("Let's inturrupt !")
bob_k = int(input(">> "))
if bob_k == bob.g:
exit("Malicious key !!")
alice.set_shared_key(bob_k)
print("They are sharing the part of flag")
print(f"Alice: {alice.encrypt(flag[:len(flag) // 2])}")
print(f"Bob: {bob.encrypt(flag[len(flag) // 2:])}")
해당 문제를 해결하는 방법은 악의적인 값을 송신하는 방법도 있지만 저는 특수한 값을 송신하는 걸로 했습니다. 0은 얼마를 거듭제곱하여도 0의 결과를 가지고, 1도 동일한 성질을 가지고 있습니다. 따라서 0 또는 1을 송신하게 되면 Alice, Bob의 키가 0 또는 1로 설정되는 걸 이용했습니다.
from pwn import *
from Crypto.Util.Padding import pad, unpad
from Crypto.Cipher import AES
import hashlib
def decrypt(shared, ciphertext):
aes_key = hashlib.md5(str(shared).encode()).digest()
cipher = AES.new(aes_key, AES.MODE_ECB)
return unpad(cipher.decrypt(ciphertext), 16)
io = remote("host3.dreamhack.games", 9957)
io.sendlineafter(">> ", b"0")
io.sendlineafter(">> ", b"0")
io.recvuntil(b"Alice: ")
flag1 = decrypt(0, bytes.fromhex(io.recvline().decode()))
io.recvuntil(b"Bob: ")
flag2 = decrypt(0, bytes.fromhex(io.recvline().decode()))
io.close()
print((flag1 + flag2).decode())
chall.py
from Crypto.Util.number import isPrime
import secrets
flag = open("flag.txt", "r").read()
pbits = 512
p = int(input("Prime: "))
g = int(input("g: "))
assert isPrime(p) and p.bit_length() == pbits
for rnd in range(100):
print(f"--- Round {rnd + 1} ---")
secret = secrets.randbits(pbits)
print(f"{pow(g, secret, p) = }")
assert secret == int(input("secret: "))
print("Wow, you are the master of DLP!")
print(flag)
문제를 해결하기 위해서는
- 를 소인수분해 했을 때, 작은 소인수만으로 구성되어야 한다. 즉, 작은 수 에 대해서 -smooth 해야 한다.
- 100회 연속 이산 로그 문제를 해결할 수 있어야 한다.
이 조건을 만족하는 512비트 소수 p를 찾아야한다.
익스플로잇 설계
가 Smooth한 수일 때 와 로부터 를 빠른 시간 내에 구할 수 있음에 앞서, 의 Multiplicative order이라는 개념을 정리해야 할 필요가 있다.
페르마의 소정리에 의하면, 와 서로소인 어떤 정수 에 대해서도 를 만족한다. 따라서 모든 이상 이하의 정수 에 대하여 회 곱하면 곱셈의 항등원인 의 결과가 나온다. 정수 의 Multiplicative order은 항등원 이 되기까지 곱해야 하는 최소 횟수로 정의페르마의 소정리에 의하면 에 속하는 모든 정수 에 대하여 의 Multiplicative order은 로 추정되지만, 실제로는 그렇지 않다.
from sage.all import *
from pwn import *
from Crypto.Util.number import isPrime
from tqdm import trange
# io = process(["python3", "chall.py"])
io = remote("host3.dreamhack.games", "15789")
pbits = 512
base = 1
for small_p in Primes():
base *= small_p
if int(base).bit_length() >= pbits - 20: # SageMath 정수를 Python 정수로 변환
break
k = 2**pbits // base
p = k * base + 1
while not isPrime(int(p)): # SageMath 정수를 Python 정수로 변환
p -= base
Fp = GF(p)
g = Fp.multiplicative_generator()
io.sendlineafter(b": ", str(p).encode())
io.sendlineafter(b": ", str(g).encode())
for _ in trange(100):
io.recvuntil(b"= ")
res = int(io.recvline())
secret = Fp(res).log(g)
io.sendlineafter(b": ", str(secret).encode())
io.interactive()