CONFidence 2020 CTF Finals - FibHash (Crypto 421)

In this challenge, we are given four linear recurrences (modulo a large prime $p$): $$ \begin{align} (a, b) &\mapsto (6 a - 9 b, a),\\ (a, b) &\mapsto (8 a - 15 b, a),\\ (a, b) &\mapsto (10 a - 21 b, a),\\ (a, b) &\mapsto (12 a - 35 b, a). \end{align} $$ Each recurrence is applied $n$ times to some known starting values, where $n$ is the flag's integer value. We are given the first element of each output. The goal is as usual to recover the flag, i.e. find $n$.

In [ ]:
# import gensafeprime
from Crypto.Util.number import bytes_to_long

flag = b"flag{local test flag}"
n = ZZ(bytes_to_long(flag))

# p = gensafeprime.generate(n.nbits() + 1)
# use challenge prime 
p = 3825410404746255934165193857151892506649946388303502923742559459860756754338954959067709726395434244171362767397769981378523965557840655385143008725493705117779476208930333229713993176785249759
assert n < p
print('p =', p)

K = Zmod(p)

def hash_with_params(g, h, a1, a0):
    def step(prev1, prev2):
        return vector([g * prev1 - h * prev2, prev1])
    def steps(n):
        Kx.<prev1, prev2> = K[]
        if n == 0: return vector([prev1, prev2])
        half = steps(n // 2)
        full = half(*half)
        if n % 2 == 0: return full
        else: return step(*full)
    return steps(n)[0](a1, a0)

starts = [
    (3141592653589793, 2384626433832795),
    (288419716939937, 5105820974944592),
    (3078164062862089, 9862803482534211),
    (7067982148086513, 2823066470938446),
]

# local test with sample flag
debug = 1
ys = [
    hash_with_params(6, 9,   3141592653589793, 2384626433832795),
    hash_with_params(8, 15,   288419716939937, 5105820974944592),
    hash_with_params(10, 21, 3078164062862089, 9862803482534211),
    hash_with_params(12, 35, 7067982148086513, 2823066470938446),
]

Let's study the matrices $M_i$ of the steps of each linear recurrence. Jordan normal form is particularly useful in this case, as it will reveal a diagonalization if it exists. The hope is to find a basis change matrix $A_i$ such that $J_i = A_i^{-1} \times M_i \times A_i$ is diagonal matrix. Diagonal matrices are interesting because their $n$-th power is the diagonal matrix with each diagonal element raise to $n$'th power.

In [2]:
Ms = []
Js = []
As = []
for g, h in [(6, 9), (8, 15), (10, 21), (12, 35)]:
    # decomposes over Z
    # otherwise, have to look in GF(p)
    M = matrix(ZZ, 2, 2, [
        g, -h,
        1, 0,
    ])
    J, A = M.jordan_form(subdivide=False, transformation=True)
    assert (~A) * M * A == J
    print(J, "\n")
    Ms.append(M.change_ring(K))
    Js.append(J.change_ring(K))
    As.append(A.change_ring(K))
[3 1]
[0 3] 

[5 0]
[0 3] 

[7 0]
[0 3] 

[7 0]
[0 5] 

Interesting! We can see that the 2nd, 3rd and 4th matrices compute powers of 3, 5 and 7. For example:

In [3]:
ys[1] == (As[1] * Js[1]**n * (~As[1]) * vector(starts[1]))[0] \
      == (As[1] * diagonal_matrix([K(5)**n, K(3)**n]) * (~As[1]) * vector(starts[1]))[0]
Out[3]:
True

We can also immediately notice that one side of the basis change simply modifies the starting points. However, we can not "remove" the other one, because we are given only one output of a pair. As a result, we obtain linear combinations of the diagonal powers:

In [4]:
a0p, a1p = (~As[1]) * vector(starts[1])
a, b = As[1][0]
ys[1] == a * a0p * pow(5, n, p) + b * a1p * pow(3, n, p)
Out[4]:
True

It is likely that they are linearly independent and we can obtain values of $3^n,5^n,7^n$ separately. Let's try:

In [5]:
coefs = []
for i in range(1, 4):
    a0p, a1p = (~As[i]) * vector(starts[i])
    a, b = As[i][0]
    coefs.append((a * a0p, b * a1p))
    
a, b = coefs[0]
c, d = coefs[1]
e, f = coefs[2]

# matrix mapping
# 3^n, 5^n, 7^n
# to outputs y1, y2, y3
m = matrix(GF(p), 3, 3, [
    b, a, 0,
    d, 0, c,
    0, f, e
])
m.rank()
Out[5]:
3

Great! We can obtain the powers now:

In [6]:
assert (~m)[0] * vector(ys[1:4]) == pow(3, n, p)
assert (~m)[1] * vector(ys[1:4]) == pow(5, n, p)
assert (~m)[2] * vector(ys[1:4]) == pow(7, n, p)
select3 = (~m)[0]

But what now? We have reduced the problem to standard discrete logarithm modulo $p$, which has 640 bits. This is rather infeasible for a CTF-level challenge.

But wait, we haven't used one of the outputs! Recall its Jordan form:

In [7]:
Js[0]
Out[7]:
[3 1]
[0 3]

How do it's powers look like? Let's compute symbolically:

In [8]:
_.<z> = QQ[]
print(matrix([[z, 1], [0, z]])**5)
print()
print(matrix([[z, 1], [0, z]])**10)
[  z^5 5*z^4]
[    0   z^5]

[  z^10 10*z^9]
[     0   z^10]

Interesting! It seems to have terms $3^n$ and $n3^{n-1}$ only. Let's check for our large $n$:

In [9]:
a0p, a1p = (~As[0]) * vector(starts[0])
a, b = As[0][0]
ys[0] == (a * a0p + b * a1p) * pow(3, n, p) + a * a1p * pow(3, n-1, p) * n 
Out[9]:
True

Cool! We also know $3^n$, so we can actually compute $n$ by simple arithmetics:

In [10]:
pow3n = select3 * vector(ys[1:4])
coef0 = a * a0p + b * a1p
coef1 = a * a1p
nsol = (ys[0] - coef0 * pow3n) / (pow3n / 3) / coef1
nsol == n
Out[10]:
True

Now let's solve the challenge data. Note that all the coefficients are the same, we only change the $y$ values:

In [11]:
ys = [
    1676692106337556305706016770958074706883705493218787835803500458160553522426125469104058304906236524961048084572483189178908787893713024829966510890627053066279418122862473691624486823188135802,
    2857964008499737244123466571337419134404523101896831014364752478262678705106912578400545946910842379193592617481759402862420715714781790030237246290673584587132334065412406222540095225886964680,
    377100011534086235658104379801079570118458049751793753941421646112566213186676075464534398000332714495426504495371864334278835331209624236632121660999291442470627222536838461859856916754666911,
    1452032184499983915936783392161922013770235576423037573850700970726680707755112154943113287273883628447114298114469426584835959464327248935513505491784021332818735403611670877092428106276909863,
]
b, c, d = -coef0 * select3
pow3n = select3 * vector(ys[1:4])
e, f, g = (K(coef1)/3) * select3
nsol = (ys[0] - coef0 * pow3n) / (pow3n / 3) / coef1
#nsol = (ys[0] + b * ys[1] + c * ys[2] + d * ys[3]) / (e * ys[1] + f * ys[2] + g * ys[3])
assert nsol * (e * ys[1] + f * ys[2] + g * ys[3]) ==  (ys[0] + b * ys[1] + c * ys[2] + d * ys[3])
In [12]:
int(nsol).to_bytes(1000, "big").strip(b"\x00").decode()
Out[12]:

Bonus

Note that the solution is a degree-1 rational function of the outputs. Knowing (or guessing) this is sufficient for solving it in a black-box style! We can simply interpolate the polynomial $n\cdot g(y) = f(y)$, such that $n = f(y) / g(y)$:

In [13]:
rows = []
for i in range(10):
    n = 100 + i
    ys = [
        hash_with_params(6, 9,   3141592653589793, 2384626433832795),
        hash_with_params(8, 15,   288419716939937, 5105820974944592),
        hash_with_params(10, 21, 3078164062862089, 9862803482534211),
        hash_with_params(12, 35, 7067982148086513, 2823066470938446),
    ]
    # for generality,
    # allow constants in numerator/denominator
    # but we don't need them in this problem
    row = []
    row += [1] + ys  # [1] for constants in numerator
    row += [n] + [n*y for y in ys] # [n] for constants in denominator
    rows.append(row)
m = matrix(K, rows)
print(m.nrows(), m.ncols(), m.rank())
10 10 9
In [14]:
comb = m.right_kernel().matrix()[0]
top = -comb[:5]
bottom = comb[5:]
In [15]:
ys = vector(K, [1] + [
    1676692106337556305706016770958074706883705493218787835803500458160553522426125469104058304906236524961048084572483189178908787893713024829966510890627053066279418122862473691624486823188135802,
    2857964008499737244123466571337419134404523101896831014364752478262678705106912578400545946910842379193592617481759402862420715714781790030237246290673584587132334065412406222540095225886964680,
    377100011534086235658104379801079570118458049751793753941421646112566213186676075464534398000332714495426504495371864334278835331209624236632121660999291442470627222536838461859856916754666911,
    1452032184499983915936783392161922013770235576423037573850700970726680707755112154943113287273883628447114298114469426584835959464327248935513505491784021332818735403611670877092428106276909863,
])
nsol = (top * ys) / (bottom * ys)
In [16]:
int(nsol).to_bytes(1000, "big").strip(b"\x00")#.decode()
Out[16]: