rkm0959's profile image

rkm0959

March 15, 2021 00:00

Inequality Solving with CVP

ctf , cryptography

서론

이 글은 제가 CTF 대회에서 자주 사용하는 toolkit인 github.com/rkm0959/Inequality_Solving_with_CVP의 설명서입니다. 이미 README에 많은 설명이 있지만, 설명서라기에는 부실한 점이 많아 이를 보강합니다.

제가 이 글을 작성하는 목적은

  • 이 repository의 존재를 더 많은 사람들에게 홍보하기 위해서
  • 이 repository의 기본적인 아이디어를 CTF를 하지 않는 사람들에게도 알리기 위해서
  • 이 repository가 더 많은 사람들에게 사용되기를 바라기 때문
  • 이 repository의 부족한 점을 보충하기 위해서

입니다. 이 점을 참고하시고 글을 읽어주시면 감사하겠습니다.

이 글에서 모든 나눗셈은 floor function을 거친 결과로 생각하시기 바랍니다.

본론

CVP란 무엇인가

우선 CVP가 무엇인지 알아야 이 도구를 이해할 수 있습니다. 이는 소프트웨어 멤버십 블로그에 있는 rbtree님의 글 “SVP and CVP”에서 이미 잘 설명되어 있지만, 다시 한 번 설명합니다.

선형독립인 $n$차원 벡터 $v_1, \cdots , v_d$가 있다고 합시다. 이때,

\[L = \left\{ \sum_{i=1}^d a_i v_i : a_i \in \mathbb{Z} \right\}\]

라고 합시다. 즉, 각 벡터들의 정수 계수 선형결합을 모은 집합을 생각합니다. 이를 Lattice라고 합니다.

Lattice와 관련된 중요한 문제 중 하나가 바로 CVP, Closest Vector Problem으로, 목표 벡터 $v$가 주어졌을 때 $v$와 가장 가까우면서 $L$에 속한 벡터를 찾는 문제입니다. NP-hard 문제 중 하나지만, 가장 가까운 벡터를 찾는 게 아니라 “꽤 가까운 벡터를 찾는 것”, 즉 문제를 근사적으로 해결하는 것은 Babai’s Nearest Plane Algorithm으로 할 수 있습니다. 자세한 결과는 여기서는 생략하도록 하겠습니다.

이제부터 Babai의 알고리즘의 강력한 힘을 믿으며 논의를 진행하겠습니다.

부등식에서 CVP로

$n$개의 변수와 $m$개의 부등식이 있다고 합시다. 각 변수의 이름을 $x_1, x_2, \cdots, x_n$이라 하고, 부등식의 형태는 모두

\[lb_j \le \sum_{i=1}^n a_{ij} x_i \le ub_j\]

형태라고 가정하겠습니다. 단, $lb_j, ub_j$ 및 $a_{ij}$ 들은 모두 정수이며, 원하는 해 $x_i$ 역시 정수입니다.

이제부터 $a_i \le b_i$가 모든 $i$에 대해서 성립할 경우 $(a_1, \cdots , a_n) \preceq (b_1, \cdots , b_n)$이라고 정의합시다.

그러면 우리가 해결하는 문제는

\[v_i = (a_{i1}, a_{i2}, \cdots, a_{im})\]

이라 했을 때

\[lb = (lb_1, \cdots , lb_m) \preceq \sum_{i=1}^n x_i v_i \preceq (ub_1, \cdots , ub_m) = ub\]

라고 쓸 수 있습니다. 이제 ${v_i}$들로 생성되는 Lattice

\[L = \left\{ \sum_{i=1}^n a_i v_i : a_i \in \mathbb{Z} \right\}\]

를 생각하면, 결국 우리가 푸는 문제는 $lb$와 $ub$ 사이에 들어오는 $L$의 원소를 찾는 문제가 됩니다.

이러한 문제를 CVP로 바꾸는 가장 직관적인 방법은

  • $lb$와 $ub$의 가운데에 있는 벡터 $v_{target} = (lb + ub) / 2$를 생각한 다음
  • 이 벡터를 목표 벡터로 하는 CVP를 해결

하는 것입니다. 성공했다면, 적당한 $v_{result} = \sum_{i=1}^n x_i v_i \in L$을 얻을 수 있습니다.

이제 $x_i$를 복구하는 것이 문제인데, 이에 대해서는 다양한 접근이 가능합니다.

  • 많은 경우에서 $lb_j \le x_i \le ub_j$ 형태의 부등식이 사용되고, 이때 $v_{result}$의 $j$번째 항이 $x_i$가 됩니다.
  • 많은 경우에서 $n \le m$이고 $v_i$들이 선형독립입니다. 이때는 $v_{result} = \sum_{i=1}^n x_i v_i$를 system of linear equations로 보고 직접 풀 수 있습니다. 이 toolkit은 이 접근을 선택했습니다.

가중치의 사용

위 논의에서 가장 큰 논리의 구멍은

\[lb \preceq \sum_{i=1}^n x_iv_i \preceq ub\]

라는 요구조건을

\[\sum_{i=1}^n x_i v_i \approx v_{target} = (lb+ub)/2\]

로 바꾼 것에 있습니다. 여기에는 사실 허점이 있는데, 아래의 극단적인 예시를 보면 더욱 명확해집니다.

$lb = (0, 0)$, $ub = (10^{300}, 2)$이 있다고 합시다. 우리의 새로운 요구조건은 $(10^{300}/2, 1)$에 가까운 벡터를 만드는 것입니다. 그렇다면, CVP 알고리즘은 두 번째 entry가 $0$ 이상 $2$ 이하여야 한다는 기존의 요구조건을 무시하고, 첫 번째 entry가 $10^{300}/2$에 최대한 가까워지도록 노력할 것입니다. CVP는 각 entry의 대소를 중요하게 보는 것이 아니라, 두 벡터의 차이의 크기만을 보기 때문에 이러한 문제가 발생하게 되는 것으로 해석할 수 있습니다.

다르게 생각하면, 이는 $lb, ub$ 벡터들의 entry의 스케일이 다르기 때문에 발생하는 문제라고도 볼 수 있습니다. $ub_i - lb_i$의 값이 전체적으로 비슷하다면 위와 같은 문제가 발생할 가능성이 줄어듭니다.

그러니 이 값이 비슷하도록 가중치를 부여합시다.

\[M = \max_{1 \le j \le m}\{ub_j - lb_j\}\]

라고 정의합시다. 이제 $j$번째 부등식에서 $lb_j \neq ub_j$라면, 그에 대한 가중치를 $w_j = M/(ub_j - lb_j)$로 둡시다. 즉,

\[lb_j \le \sum_{i=1}^n a_{ij} x_i \le ub_j \implies w_j \cdot lb_j \le \sum_{i=1}^n (a_{ij} w_j) \cdot x_i \le w_j \cdot ub_j\]

와 같이 식을 변형합시다. 만약 $lb_j = ub_j$라면 어떻게 할까요? 이 경우에는 $w_j$를 매우 큰 정수로 잡아줍시다.

특히, $lb_j = ub_j$인 경우에 큰 가중치를 잡으면 된다는 것은 직접적으로 설명할 수 있습니다. 큰 가중치 $w_j$를 걸어

\[w_j \cdot lb_j \le \sum_{i=1}^n (a_{ij} w_j) \cdot x_i \le w_j \cdot ub_j\]

를 얻었다고 합시다. 만약 $\sum_{i=1}^n (a_{ij}w_j) \cdot x_i$가 $w_j \cdot lb_j = w_j \cdot ub_j$와 달랐다면, 그 차이는

\[\left\lvert \sum_{i=1}^n (a_{ij} w_j) x_i - w_j (lb_j + ub_j) / 2 \right\rvert \ge w_j\]

가 됩니다. 즉, CVP 결과가 $j$번째 부등식을 만족시키지 못한다면 차이 벡터의 크기가 $w_j$ 이상이라는 결론을 얻습니다. CVP는 차이 벡터의 크기를 최소한으로 만들고자 하는데, $j$번째 부등식을 만족시키지 못한다면 매우 큰 크기의 벡터가 나오는 게 강제되므로, CVP는 $j$번째 부등식을 만족시킬 수 밖에 없습니다. 더욱 정확한 논의를 위해서는 CVP 문제와 determinant의 관계 등 더 많은 정보가 필요하지만, 직관적으로는 이미 충분히 납득할 수 있을 것이라고 생각합니다. 이렇게 가중치까지 걸어주면, 문제를 해결하기 위한 모든 준비가 끝납니다.

휴리스틱

CTF와 암호 체계를 깨는 context에서 중요한 또 다른 점은, 주어진 식을 만족하는 $x_i$들의 유일성입니다. 이를 증명하는 것은 어려운 일이지만, 휴리스틱을 통해서 해의 개수를 근사하는 것은 어렵지 않습니다. $n = m$과 $v_i$들의 선형독립성을 가정하겠습니다. CTF 문제풀이에서는 많은 경우에서 성립합니다.

\[lb \preceq \sum_{i=1}^n x_i v_i \preceq ub\]

에서 시작합시다. $lb$와 $ub$ 사이에 있는 벡터들이 이루는 영역의 부피는

\[\prod_{i=1}^n (ub_i - lb_i)\]

라고 쓸 수 있습니다. 또한, $\sum_{i=1}^n x_i v_i$ 형태의 선형결합에 대응되는 부피는, $B$를 $v_1, v_2, \cdots, v_n$을 column vector로 갖는 행렬이라 했을 때 $\lvert \det(B) \rvert$임은 이미 잘 알려져 있는 사실입니다.

그러니, 답이 되는 $(x_1, \cdots, x_n)$의 개수는 대강

\[\prod_{i=1}^n (ub_i - lb_i) \cdot \frac{1}{\lvert \det(B) \rvert}\]

와 같다고 볼 수 있겠습니다. 이 값이 지나치게 크다면 다른 접근을 시도하는 것이 현명합니다.

$n \neq m$인 경우에도 비슷한 분석을 할 수 있을 것으로 보이나, 대부분의 경우 $n = m$이므로 생략합니다.

구현

SageMath 구현체는 다음과 같습니다. rbtree님의 LLL repository를 참고하였습니다.

from sage.modules.free_module_integer import IntegerLattice

# Directly taken from rbtree's LLL repository
# From https://oddcoder.com/LOL-34c3/, https://hackmd.io/@hakatashi/B1OM7HFVI
def Babai_CVP(mat, target):
	M = IntegerLattice(mat, lll_reduce=True).reduced_basis
	G = M.gram_schmidt()[0]
	diff = target
	for i in reversed(range(G.nrows())):
		diff -=  M[i] * ((diff * G[i]) / (G[i] * G[i])).round()
	return target - diff


def solve(mat, lb, ub, weight = None):
	num_var  = mat.nrows()
	num_ineq = mat.ncols()

	max_element = 0 
	for i in range(num_var):
		for j in range(num_ineq):
			max_element = max(max_element, abs(mat[i, j]))

	if weight == None:
		weight = num_ineq * max_element

	# sanity checker
	if len(lb) != num_ineq:
		print("Fail: len(lb) != num_ineq")
		return

	if len(ub) != num_ineq:
		print("Fail: len(ub) != num_ineq")
		return

	for i in range(num_ineq):
		if lb[i] > ub[i]:
			print("Fail: lb[i] > ub[i] at index", i)
			return

	# heuristic for number of solutions
	DET = 0

	if num_var == num_ineq:
		DET = abs(mat.det())
		num_sol = 1
		for i in range(num_ineq):
			num_sol *= (ub[i] - lb[i])
		if DET == 0:
			print("Zero Determinant")
		else:
			num_sol //= DET
			# + 1 added in for the sake of not making it zero...
			print("Expected Number of Solutions : ", num_sol + 1)

	# scaling process begins
	max_diff = max([ub[i] - lb[i] for i in range(num_ineq)])
	applied_weights = []

	for i in range(num_ineq):
		ineq_weight = weight if lb[i] == ub[i] else max_diff // (ub[i] - lb[i])
		applied_weights.append(ineq_weight)
		for j in range(num_var):
			mat[j, i] *= ineq_weight
		lb[i] *= ineq_weight
		ub[i] *= ineq_weight

	# Solve CVP
	target = vector([(lb[i] + ub[i]) // 2 for i in range(num_ineq)])
	result = Babai_CVP(mat, target)

	for i in range(num_ineq):
		if (lb[i] <= result[i] <= ub[i]) == False:
			print("Fail : inequality does not hold after solving")
			break
    
	# recover x
	fin = None

	if DET != 0:
		mat = mat.transpose()
		fin = mat.solve_right(result)
	
	# recover your result
	return result, applied_weights, fin

위 코드는 행렬 mat, 벡터 lb, ub와 자연수 weight를 받습니다.

  • mat은 부등식의 $a_{ij}$에 해당하는 $n \times m$ 행렬입니다.
  • lb, ub는 말 그대로 $lb, ub$에 해당하는 벡터입니다.
  • weight는 필요한 경우에만 넘기면 되는 인자로, $lb_i = ub_i$일 때 줄 가중치의 값입니다.

sanity check를 통과하지 못한 경우, 그 이유를 말해줍니다.

  • 전달받은 데이터의 차원이 맞지 않는다던가
  • $lb \preceq ub$ 조차 성립하지 않는다던가

그 후, $n = m$이면 휴리스틱을 통해 예상되는 해의 개수를 출력합니다.

  • 만약 determinant가 0이라면, 이를 알려줍니다.
  • 아니라면, 휴리스틱의 공식을 통해서 예상되는 해의 개수를 알려줍니다.
  • 이 예상되는 해의 개수가 지나치게 큰 경우, 다른 접근을 시도하는 게 현명합니다.

최종 출력 결과는 세 벡터 result, applied_weights, fin입니다.

  • result는 CVP의 결과 벡터 $v_{result}$입니다.
  • applied_weights는 말 그대로 적용된 가중치입니다.
  • 특히, result는 가중치가 적용된 벡터이므로, 이에 주의하여 답을 복구해야 합니다.
  • fin은 $x_i$들의 값을 갖고 있는 벡터로, $x_i$들을 복구하기 위해서 선형방정식을 푼 결과입니다.

답이 잘 나오지 않는다면, 가중치를 직접 바꾸어가며 다시 시도하는 것도 방법입니다.

예시 문제 - SECCON CTF 2020 sharsable

from Crypto.Util.number import getPrime, GCD
from flag import FLAG
import random
 
def egcd(a, b):
    r0, r1 = a, b
    s0, s1 = 1, 0
    t0, t1 = 0, 1
    while r1 > 0:
        q = r0 // r1
        r0, r1 = r1, r0 % r1
        s0, s1 = s1, s0 - q * s1
        t0, t1 = t1, t0 - q * t1
    return s0, t0
 
def generateKey():
    p = getPrime(512)
    q = getPrime(512)
    n = p * q
    phi = (p-1)*(q-1)
 
    while True:
        d1 = getPrime(int(n.bit_length()*0.16))
        e1 = random.randint(1, phi)
        ed1 = e1 * d1 % phi
 
        d2 = getPrime(int(n.bit_length()*0.16))
        e2, k = egcd(d2, phi)
        e2 = e2 * (phi + 1 - ed1) % phi
        ed2 = e2 * d2 % phi
 
        if GCD(e1, e2) > 10:
            break
 
    assert((ed1 + ed2) % phi == 1)
 
    return (n, (e1, d1), (e2, d2))
 
n, A, B = generateKey()
M = int.from_bytes(FLAG, 'big')
C1 = pow(M, A[0], n)
C2 = pow(M, B[0], n)
assert(pow(C1, A[1], n) * pow(C2, B[1], n) % n == M)
 
import json
print(json.dumps({
    "n": n,
    "A": (A[0], C1),
    "B": (B[0], C2),
    #"d": (A[1], B[1]), # for debug
    }))

문제를 해결한 당시의 writeup은 https://rkm0959.tistory.com/165 를 참고합시다.

문제에서 주어진 조건은

  • $d_1, d_2$는 대략 $n^{0.16}$ 이하로 작은 편
  • $e_1d_1 + e_2d_2 \equiv 1 \pmod{\phi(n)}$
  • $gcd(e_1, e_2) > 10$이므로 common modulus attack 사용 불가능

문제에서 우리의 목표는 $d_1, d_2$를 복원하는 것입니다. 풀이를 위해서는 먼저

\[e_1d_1 + e_2d_2 = k \phi(n) + 1 = k(n-p-q+1)+1 \equiv -k(p+q-1)+1 \pmod{n}\]

이 성립함을 확인합시다. 여기서 $k$는 정수이고, $e_1, e_2 < n$이므로 $d_1, d_2 < n^{0.16}$이므로 $k < 2 \cdot n^{0.16}$입니다.

또한, $p + q < 3\sqrt{n/2}$인 점을 이용하면, 대략

\[-3 \sqrt{2} n^{0.66} \le (e_1 d_1 + e_2d_2 \pmod{n}) \le 0\]

을 얻습니다. 이는 새로운 변수 $d_3$을 하나 잡으면,

\[-3 \sqrt{2}n^{0.66} \le e_1d_1 + e_2d_2 + nd_3 \le 0\]

으로 쓸 수 있습니다. 이 부등식과 이미 아는 부등식

\[0 \le d_1, d_2 < n^{0.16}\]

을 합치면, 변수 3개, 부등식 3개를 얻습니다. 이제 위 도구를 사용하여 $d_1, d_2$를 복원하면 됩니다.

# Example Challenge 2 in https://github.com/rkm0959/Inequality_Solving_with_CVP

# find actual data in the repository :)
n  = 
e_1 = 
C_1 = 
e_2 = 
C_2 = 

# build matrix
M = matrix(ZZ, 3, 3)

# encode d_1
M[0, 0] = 1 

# encode d_2
M[1, 1] = 1 

# encode e_1d_1 + e_2d_2 + nd_3
M[0, 2] = e_1
M[1, 2] = e_2
M[2, 2] = n

# build lb/ub
lb = [0, 0, -int(n ** 0.66 * 3 * sqrt(2))]
ub = [int(n ** 0.16), int(n ** 0.16), 0]

# solve system
res, weights, fin = solve(M, lb, ub)

d_1 = int(fin[0])
d_2 = int(fin[1])

val = (pow(C_1, d_1, n) * pow(C_2, d_2, n)) % n
print(long_to_bytes(val))

특수 케이스

지금까지 우리는 linear inequality의 system을 CVP로 해결했습니다. 하지만 매우 특수한 경우에서는 더욱 효율적이고 강력한 알고리즘이 존재합니다. 이제부터 우리가 다룰 문제는

\[L \le Ax + My \le R, \quad S \le x \le E\]

형태의 문제입니다. 이는 사실 조금만 생각해보면

\[L \le Ax \pmod{M} \le R, \quad S \le x \le E\]

과 동일한 문제임을 알 수 있습니다. 여기서 주의해야 할 점이 있습니다.

\[L \le Ax \pmod{M} \le R\]

이라는 부등식의 의미가 생각하는 것과 다를 수 있습니다.

$0$부터 시작해서 $M-1$까지 갔다가 다시 $0$으로 돌아오는 시계 같은 원을 하나 생각을 합시다. 여기서 위 부등식의 의미는, $Ax \pmod{M}$이 $L$에서 시작해서 시계방향으로 $R$에서 끝나는 “원호” 위에 존재한다는 것입니다.

예를 들어, 충분히 큰 $M$에 대해서

\[M-2 \le Ax \pmod{M} \le 1\]

의 의미는 $Ax \pmod{M} = M-2 ,M-1, 0, 1$이라는 뜻입니다.

이 문제의 특별한 점은,

\[L \le Ax \pmod{M} \le R\]

을 만족하는 최소의 음이 아닌 정수 $x$를 찾는 것이 빠르게 된다는 점입니다.

그 원리를 여기에 설명하기에는 너무 길어지니, 제 PS 정수론 가이드 https://rkm0959.tistory.com/188 와 Codeforces Good Bye 2014의 G번, NWRRC 2019의 G번을 참고하시기 바랍니다.

이제 기존의 문제도 해결할 수 있습니다. 먼저

\[L \le Ax \pmod{M} \le R, \quad S \le x \le E\]

가 있으면, $L’ \equiv L - AS \pmod{M}$, $R’ \equiv R - AS \pmod{M}$을 생각하고

\[L' \le Ax' \pmod{M} \le R', \quad 0 \le x' \le E-S\]

으로 식을 바꿀 수 있습니다. 이제

\[L' \le At \pmod{M} \le R'\]

을 만족하는 최소의 음이 아닌 정수 $t$을 찾아줍시다. 이제 풀어야 하는 문제는

\[L' \le Ax' \pmod{M} \le R', \quad t+1 \le x' \le E-S\]

이므로, 다시 $L’’ = L’ - (t+1)S \pmod{M}$, $R’’ = R’ - (t+1)S \pmod{M}$이라 하고

\[L'' \le Ax'' \pmod{M} \le R'', \quad 0 \le x'' \le E-S-(t+1)\]

을 만족하는 최소의 음이 아닌 정수를 찾고, 이를 $x$에 대한 허용 구간이 공집합이 될 때까지 반복하면 됩니다.

확률적으로 생각하면, 해의 개수는 대략

\[(E-S+1)(R-L+1) / M\]

개 정도 있을 것이라고 생각할 수 있습니다. 이 값이 크면 다른 접근을 사용하는 것이 좋겠습니다.

구현

python3에서 구현을 했습니다.

from Crypto.Util.number import GCD

def ceil(n, m): # returns ceil(n/m)
	return (n + m - 1) // m

def is_inside(L, R, M, val): # is L <= val <= R in mod M context?
	if L <= R:
		return L <= val <= R
	else:
		R += M
		if L <= val <= R:
			return True
		if L <= val + M <= R:
			return True 
		return False

## some notes : it's good idea to check for gcd(A, M) = 1
def optf(A, M, L, R): # minimum nonnegative x s.t. L <= Ax mod M <= R
	if L == 0:
		return 0
	if 2 * A > M:
		L, R = R, L
		A, L, R = M - A, M - L, M - R
	cc_1 = ceil(L, A)
	if A * cc_1 <= R:
		return cc_1
	cc_2 = optf(A - M % A, A, L % A, R % A)
	return ceil(L + M * cc_2, A)

# check if L <= Ax (mod M) <= R has a solution
def sol_ex(A, M, L, R):
	if L == 0 or L > R:
		return True
	g = GCD(A, M)
	if (L - 1) // g == R // g:
		return False
	return True

## find all solutions for L <= Ax mod M <= R, S <= x <= E:
def solve(A, M, L, R, S, E):
	# this is for estimate only : if very large, might be a bad idea to run this
	print("Expected Number of Solutions : ", ((E - S + 1) * (R - L + 1)) // M + 1)
	if sol_ex(A, M, L, R) == False:
		return []
	cur = S - 1
	ans = []
	num_sol = 0
	while cur <= E:
		NL = (L - A * (cur + 1)) % M
		NR = (R - A * (cur + 1)) % M
		if NL > NR:
			cur += 1
		else:
			val = optf(A, M, NL, NR)
			cur += 1 + val
		if cur <= E:
			ans.append(cur)
			# remove assert for performance if needed
			assert is_inside(L, R, M, (A * cur) % M)
			num_sol += 1
	print("Actual Number of Solutions : ", num_sol)
	return ans

solve 함수를 호출하여 원하는 $x$의 list를 얻을 수 있습니다.

만약 예상되는 해의 개수가 지나치게 많다면, 다른 접근을 생각하는 것이 현명합니다.

예시 문제 - PBCTF 2020 Special Gift

from Crypto.Util.number import getStrongPrime, inverse, bytes_to_long, GCD as gcd
from Crypto.Random.random import randint
from flag import flag
 
p = getStrongPrime(512)
q = getStrongPrime(512)
N = p * q
phi = (p - 1) * (q - 1)
 
# Hehe, boi
while True:
    d = randint(int(N ** 0.399), int(N ** 0.4))
    if gcd(d, phi) == 1:
        break
 
e = inverse(d, phi)
 
# Here's a special gift. Big.
gift = d >> 120
 
enc = pow(bytes_to_long(flag), e, N)
 
print("N =", N)
print("e =", e)
print("gift =", gift)
print("enc =", enc)

첫 예시 문제와 동일한 식

\[ed = k \phi(n) + 1 = k(n-p-q+1)+1 \equiv -k(p+q-1)+1 \pmod{n}\]

를 생각합시다. 이번에는 $d < n^{0.4}$이므로 $k < n^{0.4}$이고, $p+q < 3 \sqrt{n/2}$를 이용하면 대략

\[-3 \cdot n^{0.9} \le ed \pmod{n} \le 0\]

를 얻습니다. 좌변의 상수 $3$은 매우 넉넉하게 잡은 것입니다.

그런데 $gift = \lfloor d / 2^{120} \rfloor$를 알고 있으니, $d = gift \cdot 2^{120} + c$라고 쓸 수 있습니다. 이제

\[(-3 \cdot n^{0.9} - e \cdot gift \cdot 2^{120}) \pmod{n} \le ec \pmod{n} \le (-e \cdot gift \cdot 2^{120}) \pmod{n}\]

를 얻고, 동시에 $0 \le c < 2^{120}$이니, 위 알고리즘을 적용할 환경이 준비가 됩니다.

휴리스틱을 적용하면 대략 $2^{120} \cdot 3 \cdot n^{-0.1} \approx 6 \cdot 10^5$개의 해가 나올 것을 예상할 수 있습니다.

이를 전부 구하고, 각 $c$에 대하여 decryption을 시도해보면 flag를 얻을 수 있습니다.

# find actual data in the repository! 
N = 
e = 
gift = 
enc = 
 
R = (-e * (gift << 120)) % N
L = (-e * (gift << 120) - 3 * (int)(N ** 0.9)) % N

val = solve(e, N, L, R, 0, 1 << 120)
num_sol = len(val)

for i in tqdm(range(num_sol)):
	d = (gift << 120) + val[i]
	s = long_to_bytes(pow(enc, d, N))
	if s[:5] == b"pbctf":
		print(s)

결론 및 후기

지금까지 제 repository “Inequality Solving with CVP”의 기본 원리를 알아보았고, 실제 CTF에서 출제된 문제들을 풀어보면서 사용법도 알아보았습니다. Lattice를 활용하는 많은 문제가 이 도구를 활용하여 해결될 수 있으니, 많이 사용해주시면 정말 기쁠 것 같습니다. RSA 관련 문제를 예시로 들었으나, Hidden Number Problem, Linear Congruential Generator 등 문제를 선형적인 방정식으로 나타낼 수 있다면 이 방법을 시도할 수 있습니다. repository에 예시 문제가 위에서 소개한 것을 포함해서 총 8문제가 있으니, 참고해주시기 바랍니다.

Lattice를 사용하는 대표적인 암호를 깨는 알고리즘에는

  • Coppersmith’s Algorithm을 사용한 polynomial root 계산
  • 이 글에서 다루는, Linear System of Inequality를 만들고 해결

이 있습니다. (정보를 더 원하신다면, https://eprint.iacr.org/2020/1506.pdf 를 참고해주세요)

첫 알고리즘의 경우에는 dicegang의 defund라는 분이 만든 repository인 “defund/coppersmith” 가 있습니다. 제 repository는 두 번째 알고리즘에 집중하는 것으로 볼 수 있는데, 제 구현체도 defund의 구현체처럼 많은 사람들이 사용해주었으면 좋겠습니다. 긴 글 읽어주셔서 감사합니다. 질문은 rkm0959.tistory.com에서 받습니다.