1393 words
7 minutes
KalmarCTF 2026 Writeup - RBG+++
2026-03-30

This is a writeup for a difficult crypto challenge from KalmarCTF 2026 called RBG+++. Basically, it is an advanced version of lance-hard? from last year’s KalmarCTF, so make sure you have read Neobeo’s writeup for lance-hard? before starting this one.

Description#

RBG sometimes stands for Random Bit Generator. I didn’t really like a challenge that was required to guess Dual_EC_DRBG.

This is a revenge challenge of RBG from Daily Alpacahack.

chall.py
from Crypto.Util.number import *
PBITS, NDAT = 137, 137
with open("flag.txt", "rb") as f:
m = int.from_bytes(f.read())
N = getPrime(PBITS) * getPrime(PBITS)
e = getRandomRange(731, N)
print(f"{N = }")
lcg = lambda s: (s * 3 + 1337) % N
for i in range(NDAT):
print(pow(m, e:=lcg(e), N) + pow(m, e:=lcg(e), N))
# Internal audit
e = getRandomRange(731, N)
print(f"[DEBUG] {e = }")

Analysis#

The challenge first gives us a modulus NN which is a product of two 137-bit primes. After that, it generates 136 pairs of e,re, r, where r=me+me(modN)r=m^e + m^{e'} \pmod N and e=3e+1337(modN)e' = 3e + 1337 \pmod N.

It is obvious that the modulus NN is not large enough, and we can easily factor it to get the two primes. The main problem is that we can’t directly find the roots of r=me+me(modN)r=m^e + m^{e'} \pmod N even if we know the factorization of NN because ee is too large.

Let’s can first try to simplify the equation. Since ee' is almost the triple of ee, we can set x=mex=m^e. If e<N3e<\frac{N}{3}, then we get

x+x3m1337r(modN)x+x^3m^{1337} \equiv r \pmod N

Although m1337m^{1337} is still unknown, it is a constant across all pairs. If we can find another relation between these equations, we can cancel out some variables and get a “low” degree univariate equation.

But we don’t want z=m1337z=m^{1337} to be the coefficient of x3x^3 because it is annoying to handle a non-monic polynomial. So Let’s first do some arrangements. Let N<3e+1337<2NN<3*e+1337<2*N, we have

me+m3e+1337Nr(modN)m^e+m^{3e+1337-N} \equiv r \pmod N

which is equivalent to

me+(1337N)/2+m3(e+(1337N)/2)rm(1337N)/2(modN)m^{e+(1337-N)/2}+m^{3(e+(1337-N)/2)} \equiv r \cdot m^{(1337-N)/2} \pmod N

Now we can set x=me+(1337N)/2x=m^{e+(1337-N)/2} and z=m(1337N)/2z=m^{(1337-N)/2}, then it becomes x3+xrz(modN)x^3+x\equiv r\cdot z \pmod N. We can solve the equations seperately on modp\mod p and modq\mod q to reduce the degree complexity.

Similar to lance-hard?, the first step is to find small coefficients relations between mem^e‘s.

Finding small relations#

Since mp11(modp)m^{p-1} \equiv 1 \pmod p, we can use LLL to find i=1kciei0(modp1)\sum_{i=1}^k c_ie_i\equiv 0 \pmod{p-1} with small cic_i‘s. Thus we get

i=1k(mei)ci1(modp)\prod_{i=1}^k (m^{e_i})^{c_i} \equiv 1 \pmod p

In addition, we can also add zz to the LLL because z=m(1337N)/2z=m^{(1337-N)/2} is also a power of mm. Then the equation becomes

i=1k(mei)cizc1(modp)\prod_{i=1}^k (m^{e_i})^{c_i} \cdot z^c \equiv 1 \pmod p

We can heuristically guess the degree of the final polynomial is proportional to 3kci3^k \sum |c_i|. After some experiments, the best parameters are k=8k=8 and there’s exactly 4 positive cic_i‘s and 4 negative cic_i‘s, which balances the degree on both sides.

Reduce to univariate polynomial#

Now we have 8 equations of x3+xrz(modp)x^3+x\equiv r\cdot z \pmod p and one equation of the products of xi=meix_i=m^{e_i} and zz.

A straightforward way is to use resultant to eliminate xix_i and get a univariate polynomial in zz. However, the computation of resultant is very expensive, and the degree of final polynomial will explode very fast. By our previous heuristic guess, the degree will be about 2282^{28}, which is impossible to compute. So we need to be more careful about how to eliminate variables.

Remember that we have 4 positive cic_i‘s and 4 negative cic_i‘s, we can rewrite the product relation as

ti=14(mei)cizc(modp)ti=58(mei)ci(modp)\begin{align*} t&\equiv\prod_{i=1}^4 (m^{e_i})^{c_i} \cdot z^c \pmod p\\ t&\equiv\prod_{i=5}^8 (m^{e_i})^{-c_i} \pmod p \end{align*}

For each part, we first reduce it into a bivariate polynomial of tt and zz, then use a resultant to eliminate tt and get the final result.

Algebraic Number Tricks#

As we already know that xi3+xiriz(modp)x_i^3+x_i\equiv r_i\cdot z \pmod p, we can think of it as an “algebraic number over Fp[z]\mathbb{F}_p[z]1

A famous result about algebraic numbers is that the sum, product of algebraic numbers is still an algebraic number. Similarly, we can do the same thing in our case, which means tt is also an algebraic number over Fp[z]\mathbb{F}_p[z].

Prop. For a degree dd algebraic number f(α)=0f(\alpha)=0 over Fp[z]\mathbb{F}_p[z], t=αkt=\alpha^k is also a degree dd algebraic number.

Let MM be the companion matrix of ff, then α\alpha is the eigenvalue of MM. Thus t=αkt=\alpha^k is the eigenvalue of MkM^k, whose characteristic polynomial is degree dd and has tt as a root.

Prop. For two algebraic numbers f(α)=0f(\alpha)=0 and g(β)=0g(\beta)=0 over Fp[z]\mathbb{F}_p[z], t=αβt=\alpha\cdot\beta is also an algebraic number with degree degfdegg\deg f \cdot \deg g.

Let the roots of ff be α1,α2,...,αm\alpha_1,\alpha_2,...,\alpha_m and the roots of gg be β1,β2,...,βn\beta_1,\beta_2,...,\beta_n.2 Then the polynomial ij(xαiβj)\prod_{ij}(x-\alpha_i\beta_j) has tt as a root and the degree is mnmn.

Note that the coefficients of ij(xαiβj)\prod_{ij}(x-\alpha_i\beta_j) are still in the field Fp[z]\mathbb{F}_p[z] because they are symmetric polynomials of αi\alpha_i‘s and βj\beta_j‘s, thus it can be computed by the elementary symmetric polynomials, which is exactly the coefficients of ff and gg.

Now I’ll introduce an efficient way to compute the coefficients of ij(xαiβj)\prod_{ij}(x-\alpha_i\beta_j).

Def. The power sums are defined as pk=i=1mαikp_k=\sum_{i=1}^m \alpha_i^k and qk=j=1nβjkq_k=\sum_{j=1}^n \beta_j^k.

Clearly,

ij(αiβj)k=i=1mαikj=1nβjk=pkqk\sum_{ij}(\alpha_i\beta_j)^k=\sum_{i=1}^m \alpha_i^k \cdot \sum_{j=1}^n \beta_j^k=p_k\cdot q_k

On the other hand, we can quickly convert elementary symmetric polynomials from and to power sums using Newton’s identities. Therefore, we can compute the coefficients of ij(xαiβj)\prod_{ij}(x-\alpha_i\beta_j) in O(mn)O(mn) time. Since we only consider m,n9m,n\leq 9, this is much more efficient than any other fancy methods.

Exact degree prediction#

Now we have a way to compute the polynomial of tt without computing any resultant. The final polynomial of tt and zz is a degree 81 polynomial for tt with thousands degree for zz, which is still impossible to compute resultant.

Again, we can use the trick in lance-hard? to map Fp[z]\mathbb{F}_p[z] to Fp\mathbb{F}_p by substituting zz with some random value, and interpolate later.

Notice that all of these “algebraic numbers tricks” will not break the coefficients of polynomial, i.e. the coefficients are still polynomials of zz. Thus, the only thing we need to do is to approximate the degree of the final polynomial.

Since the analysis is very hard, I just did some experiments to find the pattern of degree growth. The result shows that the degree of the final polynomial is about 3kc+3k1max{i=14ci,i=58ci}3^kc+3^{k-1}\max\{\sum_{i=1}^4 c_i, \sum_{i=5}^8 -c_i\}. Now we can use it to choose the best LLL candidates and predict the degree of final polynomial, which is about 2252^{25}.

Solve the polynomial#

Fast Lagrange interpolation#

Since we can free choose the evaluation points, we can use consecutive xs for interpolation. We can easily compute it in O(n)O(n) time.3

Finding roots of univariate polynomial#

In general, the best method to find roots is first compute gcd(f,zpz)gcd(f, z^p-z) and then factor the result. However, the degree of our polynomial is about 2252^{25}, which is too large for NTL to run FFT, so it failed.

Thus we have to compute two final polynomials and run gcd on them. Thanks to NTL and half-gcd and FFT(?), this time it doesn’t raise error and finished in about 20 minutes.

After getting the gcd, I find that it contains a large factor of ziz^i, so it could be faster if we remove the factors of ziz^i before computing gcd and interpolation.

The rest is simple as we have got m(1337N)/2m^{(1337-N)/2}.

Conclusion#

My final solution is written in sage mixed with pyx4. The overall running time is

  • LLL: single thread, ~1 hour. This is not optimized at all.
  • Compute many zz‘s: multi-thread, ~2 hours per polynomial. We need 4 polynomials in total.
  • Interpolation: single thread, ~50 minutes per polynomial.
  • GCD: single thread, ~20 minutes.

So the total running time is about 10 hours, which is quite long but still acceptable for a hard crypto challenge. It is still possible to optimize the code further, but I will leave it for future work.

Footnotes#

  1. Please be aware that the real definition of algebraic number is the root of a non-zero polynomial with coefficients over integers. But here we use it in finite field and the coefficients are polynomials of zz.

  2. No need to worry if the roots exist in the field, these are just symbols over some unknown extension field.

  3. Thanks Neobeo for the implementation in lance-hard?, and check grhkm’s blog for more details about the algorithm.

  4. I was surprised that sage supports load("file.pyx"), which will automatically compile the pyx file and load it.