corCTF 2022 Writeup

Tue Aug 09 2022

I participated corCTF 2022 by myself. The results were 14th/978.

Here are my some write-ups for crypto challs.

crypto

rlfsr

14 solves

rlfsr.py
from secrets import randbits
from random import shuffle
from hashlib import sha256
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

class LFSR:
    def __init__(self, key, taps):
        self.key = key
        self.taps = taps
        self.state = list(map(int, list("{:0128b}".format(key))))
    
    def _clock(self):
        ob = self.state[0]
        self.state = self.state[1:] + [sum([self.state[t] for t in self.taps]) % 2]
        return ob

key = randbits(128)
l = LFSR(key, [1, 2, 7, 3, 12, 73])
out = []

for i in range(118):
    bits = [l._clock() for _ in range(128)]
    shuffle(bits)
    out += bits

print(hex(sum([bit*2**i for i, bit in enumerate(out)])))

flag = open("flag.txt", "rb").read()
iv = randbits(128).to_bytes(16, 'big')
aeskey = sha256(key.to_bytes(16, 'big')).digest()[:32]
print((iv + AES.new(aeskey, AES.MODE_CBC, iv=iv).encrypt(pad(flag, 16))).hex())

This LFSR is literally linear, but the output is shuffled by 128 each.

Since the tap is weird (the tap doesn't has 0-th), there is a possibility that the period of this LFSR is quite short. I first tried to leak this period, but it's not the case.

Then I noticed that the sum of 128 outputs can be calculated, though we didn't know how they were shuffled. Using this fact, we can get 118 bits of output. To leak initial state, we should get another 10 bits, but this can be found by brute force.

solve.sage
from hashlib import sha256
from binascii import unhexlify

from Crypto.Cipher import AES


out_int = 0x824edb4620dca76a4abda968ff8eb44a93c5e55fd3e63f4abec458a6141190f7eb94b0da6712aa1a9d087984402fdd3f57bbbc6cca1156967b4c2bdee51885176dd74c91098f71e2aa339af6f7d9ae081b5f794c4c49bc3085d01c34fdee7e7458d38610c5a2f9879a378b985afe2be32c206b6e33b2bc52ff2f9b1919fe824c56badd1f7122be46f2a2308bf89565a55729d4b83f19707a9db9122680b213fb127f58d030a1337ce76e5c7d31a369f57a68ee92dfe81d239104bb307ff25ced4624feb4f5f917335444ef30338566ce1451ec3b2f41f16ac3295a7decb0131033767a1def651eada3b5b2e69dbd486475d6f4552bf3bc1d1462ccd19f20c3da0139cc165a79968bb33858984f0fba5aa7e39ff6ec4fc466f1ca235b0a989542fed4526ed74bc33ff38562d5e842c1431941d345fcdb2eb1d1cae060b05f2084ddc3bff342b6f4fd13c680a331394a42bf3a8fb3d0ab57dcefa90a5bee60c503fb2cd596889716cbe3d880588c8fc58d4bee0f94daccc176fe1b33ba7aca29876a5005e903abbe6c9d02ce7855db027707ea81e93187624738732a8daf756a3d5d3d62eb105e07b4b35aa64e9c0c99725c5e3ac0ef6ce120d54ad68a61cc6841bc76aef197b0b71e5ca42fab3f047b4a64811611d0cb58e707e04ec10b56ead1adfccece3c7068ff15592a281e71697a756b955dd6147e0494f7a846663964063b3a485ad0ff15290e05c46c51af7ffb87eae257c411e66f7214e88c933770c9d4389e36c9a073d9a3137c2a3dc2dfac8e8ef298a75508ef66debc701b81850ccb774f7cce63dc723cc1b13746277345fa0df4146d183ea444e80d8e3825e29e1db1d59903c038f8cbcf8ed6360ad4806c7cab9f440e3d1d38d1c3577f5c6d6ce1e6dbbbeb89a67a04f004854ced5a17a36c3565ca3d3fdd06c4495cf36c333c1babc47c2eb681bc0c539ac0f1b2eb89a86524ffc4f7f5805872d3b30cba8738ca4db6588e2b72db30502861bcdc519033afa13dd1073db094bcf44fee9bd3488ebb446c3278b8b3f7b350e936a7e9b493c4b1b18655571114f1ab4a9c71871487a70b0648c7622753db39d2d53fb8f4bd4ff278a47428cce871338f149305d921623d7de34348ada43a0bc2137ec9b41d7f465cf3fe57eb6f805df8f85f6c6f7eaff26ad1b20495e41292fb33ea97c0b9b69920371231f08044c491518403f553c593894509060414abe47afcf3ad7617b69a9b423daf39a6d865e4ef5c46e8eaafe752fd79b345b49e77bf75b011e079886949d39db8d7fc5719e37480253dea7c00dd4debdd19c08870a90c3ef2f55109dcead09fc67dfc8fca50c577ec67168da5cdd6053edfbfc0f598137ef9ca1edf36009c6a9f879250ff6a323ee63e81306d834162b63f6fb223bb054f2e8c11611274919ee7d3a3721591ac6d7482142e0d54a4bd8fd07b0ac4a16c6b54792e27143489c2588a6b5cac79b24acd78db75cbe059e4c0ce4cb1d45dafba3c09fbf0aac2b8728d74f558ef538ca4ac9799fa558cc2fbb8beeccf1bf6997f3558a33c267742174b6b1e1431498d3e4884c73e126e904d09625247e1feddfcbe0c9a99258d4e282dc5606f1095f81f96924054e5020abd9dfac52973e3cb7005fb07e3fc67db805797eca46ee9cfcb37129879ec9bcda4a3932eac35988a75e635d8fcd1d2195451fb98edc3d5d0a086405e3484ff8dba16c738ff844583ab65fbc9ade0ccc4d0c310b46fe6fb39815ef3f1bd70f3997ee5290a8d0864133ecb668c13211f9fad682811cb123b76c9fb4cf93f32fbbe7a1300421a8d2710de1027c24a139c6978058d3667a9df2caeaad88da5aaa435331013c8aae10a64fe93d7c138a0f84016c6628fa9e7f62648be24474f2f73b5ee36238cbe0132d6d5a0db57650a8d194bcde065d3cdcf374f21df843675495c2baba1bd22a7f644882982a71804fa7aab4c55300471a4f4c10bd687682b90cea92214110098216348ce4115caa5ef1daa79f36511e4ad594394773eeded215b7a855a03985af9121bc83a0615d07355798f5f63e8f8e615ed5c99868136506e7facf153474a063cb5803fa6d83a713370e92ae2cc68d1304272ff1b407afec261138e8a2107767736cbb74aa28a16ef1b68345eed1e794bd4f0ecbac05dfb0b1307e27be71798f969c6177b981da53e2a172cb553a6f897cd1fdb67b35d6cf3410af88aa3a5040c42a06f3d2414605b9dbc882fefa7f2d76e51592f6cce654e00f9837801fee64a21218bd71c17a9c1cbcfe7e522d8da37886f789457047fe8bd7c8f41365762d8ef2a90e1de426faf18def1c4a2013f46cee4d955afbf9dddd4369ca5544a0c0cdba235359a9aba4a61fae81625c7a4ab7c287a905826af1d184c5eef22ee0940d75937f9ac740dbe7360a6c8c21d140b1dba5d7c5a4bc9449179398d67b83555a40ff1b4cad66dc522d75d6c9c6ae3071fd7c404011c054aa67c6122344f7c73cd9de35c37f2ccd5caa3717f1d5f571192c108dbf15557bb0bef72f830c9b6bd9295d5f1d7bd871d1889b1f849ecf5a8efc13233e38efded7a16772878a685e28944c42529abf0577d8027188658313fc80f65f626074c0d4214a826fad70bd8f39dae2ce8547e7bc29a20f064833267b524aa
tmp = out_int
out = []
for _ in range(128 * 118):
    out.append(tmp % 2)
    tmp //= 2

M = matrix(GF(2), 128, 128)
for i in range(127):
    M[i, i+1] = 1
tap = [1, 2, 7, 3, 12, 73]
for t in tap:
    M[127, t] = 1

t = vector(GF(2), [1] + [0] * 127)
# t_sum = t + t * M + t * M**2 + ... + t * M**127
# t_sum * initial_state equals to the sum of first 128 outputs.
t_sum = vector(GF(2), 128)
for i in range(128):
    t_sum += t * M ** i

# S[i] equals to the sum(out[128*i:128*(i+1)])
S = matrix(GF(2), 128, 128)
for i in range(118):
    S[i] = t_sum * M**(128*i)
target = vector(GF(2), 128)
for i in range(0, len(out), 128):
    target[i//128] = sum(out[i: i+128])


enc = unhexlify("38bd5d4c3f5fa5ef8c34cae70a03111d446361d73c4f315f309f6a7ce602b893fdb6d3ae15e2a12668e97c020bf13e54c6e00125e92f9859d8c2ac71a1534a9a234426f4ba3927c797b0924f04aa7f0f79578afc4c3e6054fb3601f88d511934a5424efa4ba2e6e776d91e398afc3c7f5335fefe78c0b58e98ae838f4ae73379")
iv = enc[:16]
enc_flag = enc[16:]

K = S.right_kernel()
base = S.solve_right(target)
basis = K.basis()
for n in range(2**10):
    v = vector(GF(2), 128)
    for i in range(10):
        if n % 2 == 1:
            v += basis[i]
        n //= 2
    ans = base + v
    key = int("".join(map(str, ans.change_ring(ZZ))), 2)
    aeskey = sha256(key.to_bytes(16, 'big')).digest()[:32]
    dec = AES.new(aeskey, AES.MODE_CBC, iv=iv).decrypt(enc_flag)
    if b"corctf" in dec:
        print(dec)

corctf{m4yb3_w3_sh0uld_ju5t_cut_hum4n5_0ut_0f_th1s_c0mpl3t3ly_1f_th3y_d3c1d3_t0_f4k3_shuffl3_0r_s0m3th1ng}

corrupted-curves, corrupted-curves+

20 solves, 15 solves

corruptedcurves.py
#!/usr/local/bin/python
from secrets import randbits
from Crypto.Util.number import getPrime

def square_root(a, p):
    if legendre_symbol(a, p) != 1:
        return 0
    elif a == 0:
        return 0
    elif p == 2:
        return 0
    elif p % 4 == 3:
        return pow(a, (p + 1) // 4, p)
    s = p - 1
    e = 0
    while s % 2 == 0:
        s //= 2
        e += 1
    n = 2
    while legendre_symbol(n, p) != -1:
        n += 1
    x = pow(a, (s + 1) // 2, p)
    b = pow(a, s, p)
    g = pow(n, s, p)
    r = e
    while True:
        t = b
        m = 0
        for m in range(r):
            if t == 1:
                break
            t = pow(t, 2, p)
        if m == 0:
            return x
        gs = pow(g, 2 ** (r - m - 1), p)
        g = (gs * gs) % p
        x = (x * gs) % p
        b = (b * g) % p
        r = m

def legendre_symbol(a, p):
    ls = pow(a, (p - 1) // 2, p)
    return -1 if ls == p - 1 else ls

class EllipticCurve:
    
    def __init__(self, p, a, b):
        self.a = a
        self.b = b
        self.p = p
        if not self.check_curve():
            raise Exception("Not an elliptic curve!")
        
    def check_curve(self):
        discrim = -16 * (4*pow(self.a, 3) + 27*pow(self.b, 2))
        if discrim % self.p:
            return 1
        return 0
    
    def lift_x(self, px):
        y2 = (pow(px, 3) + self.a*px + self.b) % self.p
        py = square_root(y2, self.p)
        if py == 0:
            raise Exception("No point on elliptic curve.")
        return py

with open("flag.txt", "rb") as f:
    flag = f.read()
    flag = int.from_bytes(flag, 'big')

print("Generating parameters...")
while True:
    p = getPrime(512)
    a, b = randbits(384), randbits(384)
    try:
        E = EllipticCurve(p, a, b)
        fy = E.lift_x(flag)
        print(f"p = {p}")
        print(f"flag y = {fy}")
        break
    except:
        continue
print(a, b)
checked = set()
count = 0
while count < 64:
    x = int(input("x = ")) % p
    if int(x) in checked or x < 2**384 or abs(x - p) < 2**384:
        print(">:(")
        continue
    e = randbits(64)
    print(f"e = {e}")
    try:
        E = EllipticCurve(p, a^e, b^e)
        py = E.lift_x(x)
        checked.add(int(x))
        print(f"y = {py}")
        count += 1
    except:
        print(":(")
print("bye!")

We can get random ee and one point on the elliptic curve y2=x3+(ae)x+(be)y^2 = x^3 + (a \oplus e) x + (b \oplus e). The xx coordinate of this point can be specified in corrupted-curves and generated randomly in corrupted-curves+.

Since ee is relatively smaller than a,ba, b, we can decompose ae,bea \oplus e, b \oplus e into MSB part and LSB part. MSB part is always fixed, while LSB part is different in each time because of ee. Since we know (x,y)(x, y) and have many linear relationship, we can leak a,ba, b by LLL.

The solver is almost the same in corrupted-curves and corrupted-curves+. I pasted one for corrupted-curves.

solve.sage
from pwn import remote


M = 6

io = remote("be.ax", 31345)
io.recvuntil(b"p = ")
p = int(io.recvline())
io.recvuntil(b"flag y = ")
flag_y = int(io.recvline())

points = []
for _ in range(100):
    x = random.randint(2**384, p - 2**384)
    io.sendlineafter(b"x = ", str(x).encode())
    io.recvuntil(b"e = ")
    e = int(io.recvline())
    ret = io.recvline().strip().decode()
    if "y = " in ret:
        y = int(ret[4:])
        points.append((x, y, e))
    if len(points) >= M:
        break

mat = matrix(ZZ, 3*M+3, 3*M+3)
mat[0, 0] = 1
mat[1, 1] = 1
mat[2, 2] = 1
for i in range(2*M):
    mat[3+i, 3+i] = 1
for i in range(M):
    x, y, e = points[i]
    j = 3+2*M+i
    mat[0, j] = x * 2**64
    mat[1, j] = 2**64
    mat[2, j] = (x**3 - y**2) % p
    mat[3+2*i, j] = x
    mat[3+2*i+1, j] = 1
    mat[3+2*M+i, j] = -p
weights = diagonal_matrix([1, 1, 2**320] + [2**256] * 2*M + [round(2**320 * sqrt(3*M+3))] * M)
mat *= weights
res = mat.LLL()
mat /= weights
res /= weights
C = mat.solve_left(res)

for data in res:
    if abs(data[2]) != 1:
        continue
    if data[2] == -1:
        data = -data
    for i in range(M):
        e = points[i][2]
        a = int(data[0] * 2**64 + data[3+i]) ^^ e
        b = int(data[1] * 2**64 + data[4+i]) ^^ e
        PR.<x> = PolynomialRing(GF(p))
        f = x**3 + a*x + b - flag_y**2
        roots = f.roots()
        # not necessarily found correct a, b. I don't know why...so I print all patterns
        print([long_to_bytes(int(root[0])) for root in roots])
    break

corctf{i_h0pe_you_3njoyed_p1ecing_t0geth3r_th4t_curv3_puzz1e_:)} corctf{cr4ftin6_f3as1ble_brut3s_unt1l_y0u_mak3_it!}

threetreasures

19 solves

source.py
from sage.all import *
from Crypto.Util.number import bytes_to_long, getPrime
from random import getrandbits
from secret import flag, p, x, y

def random_pad(n, length):
    return (n << (length - n.bit_length())) + getrandbits(length - n.bit_length())

flag = bytes_to_long(flag)
fbits = flag.bit_length()
piece_bits = fbits // 3
a, b, c = flag >> (2 * piece_bits), (flag >> piece_bits) % 2**piece_bits, flag % 2**piece_bits
print(a, b, c)
print(f'flag bits: {fbits}')
assert p.bit_length() == 512
q = getPrime(512)
n = p * q
ct = pow(random_pad(c, 512), 65537, n)
E = EllipticCurve(GF(p), [a, b])
G = E(x, y)
assert G * 3 == E(0, 1, 0)
print(f"n = {n}")
print(f"ct = {ct}")
print(f"G = {G}")

flag is separated into three parts, a,b,ca, b, c. We are given the point GG on the elliptic curve y2=x3+ax+by^2 = x^3 + ax + b on Fp\mathbb{F}_p such that 3G=O3G = O. We also have pad(c)65537modn\mathrm{pad}(c)^{65537} \mod n.

First consider about 3G=O3G = O. This is equivalent to 2G=G2G = -G, which means xx of 2G2G equals to xx of GG. From this relation, we have a2+6Gx2a+9Gx412GxGy2=0modpa^2 + 6G_x^2a + 9G_x^4 - 12G_xG_y^2 = 0 \mod p. Since aa is relatively small in this equation and pnp | n, we can use Coppersmith method to find aa. I tried it but it took much time...So I used information of the prefix of the flag. Using this, aa was found fast.

Now that aa is known, Gy2=Gx3+aGx+bmodpG_y^2 = G_x^3 + aG_x + b \mod p can be solved with regard to bb using Coppersmith method again. Also, using 2 equations mentioned above, we can find pp by GCD. Then nn can be factored. cc can be found by simple calculation like RSA.

solve.sage
from Crypto.Util.number import bytes_to_long, long_to_bytes


n = 97915144495462666300795364589570761584322186881492143950078938328867290046424857019504657598883431857075772605985768551863478086544857915637724181292135280539943713583281151707224031808445390796342632369109562433275679473233398168787639940620683354458292117457239552762694657810883738834935391913698852811737
ct = 20363336204536918055183609604474634074539942561101208682977506918349108499764147141944713060658857301108876346227077713201766486360148051069618774935469969057808945271860025712869868421279488925632657486125211168314387620225601572070169746014988350688548088791906161773656057212229972967244998930356157725393
Gx = 3115938227771961657567351113281194074601897467086373590156577019504528350118731801249444974253028485083440228959842232653488953448859690520619223338133881
Gy = 2665631524518629436093690344927156713668794128141943350227439039472817541262750706395352700109556004195261826476428128993836186741129487842876154876730189


PR.<a_lsb> = PolynomialRing(Zmod(n))
a = bytes_to_long(b"corctf{") * 2**(125-55) + a_lsb
f = a ** 2 + 6 * Gx**2 * a + 9 * Gx**4 - 12 * Gx * Gy**2
beta = 0.5
delta = 2.0
X = 2**70
epsilon = RR(beta**2/delta - log(X) / log(n))
a_lsb = f.small_roots(X=X, beta=0.5, epsilon=epsilon)[0]
a = bytes_to_long(b"corctf{") * 2**(125-55) + a_lsb

PR.<b> = PolynomialRing(Zmod(n))
f = Gx**3 + Gx * a + b - Gy**2
b = f.small_roots(beta=0.5)[0]

tmp1 = (Gx**3 + Gx * a + b - Gy**2) % n
tmp2 = a ** 2 + 6 * Gx**2 * a + 9 * Gx**4 - 12 * Gx * Gy**2
p = int(gcd(tmp1, tmp2))
assert is_prime(p)
assert n % p == 0
q = n // p
d = int(pow(0x10001, -1, (p - 1) * (q - 1)))
c = int(pow(ct, d, n))
c = c >> (c.bit_length() - 125)

long_to_bytes(int(2**250 * a + 2**125 * b + c))

corctf{you_have_conquered_the_order_of_three!!}

leapfrog

36 solves

leapfrog.py
from Crypto.Util.number import long_to_bytes, getPrime
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from hashlib import sha256
from secrets import randbelow
from random import sample

p = getPrime(256)
a = randbelow(p)
b = randbelow(p)
s = randbelow(p)

def f(s):
    return (a * s + b) % p

jumps = sample(range(3, 25), 12)
output = [s]
for jump in jumps:
    for _ in range(jump):
        s = f(s)
    output.append(s)

print(jumps)
print(output)

flag = open("flag.txt", "rb").read()
key = sha256(b"".join([long_to_bytes(x) for x in [a, b, p]])).digest()[:16]
iv = long_to_bytes(randbelow(2**128))

cipher = AES.new(key, AES.MODE_CBC, iv=iv)
print(iv.hex() + cipher.encrypt(pad(flag, 16)).hex())

This is the hardest (maybe) of a series of LCG-related challenges.

Let the current state be ss and the state after nn steps be ss'. We can write s=ans+(an1+an2++1)bmodps' = a^n s + (a^{n-1} + a^{n-2} + \cdots + 1) b \mod p. If we know three kinds of (s,s)(s, s') in fixed nn, say, s1=ans1+(an1++1)bmodp,s2=ans2+(an1++1)bmodp,s3=ans3+(an1++1)bmodps_1' = a^n s_1 + (a^{n-1} + \cdots + 1) b \mod p, s_2' = a^n s_2 + (a^{n-1} + \cdots + 1) b \mod p, s_3' = a^n s_3 + (a^{n-1} + \cdots + 1) b \mod p, we can have two equations, s1s3=an(s1s3)modp,s2s3=an(s2s3)modps_1' - s_3' = a^n (s_1 - s_3) \mod p, s_2' - s_3' = a^n (s_2 - s_3) \mod p. Using these two equations, we have (s1s3)(s2s3)(s1s3)(s2s3)=0modp(s_1' - s_3')(s_2 - s_3) - (s_1 - s_3)(s_2' - s_3') = 0 \mod p, which means that we find a multiple of pp.

Such (s,s)(s, s') pairs are easily found by brute force:

for n in range(40):
    for i in range(n - 1):
        for j in range(i, len(jumps)):
            if sum(jumps[i: j+1]) == n:
                print(n, i, j)
"""
16 5 6
16 7 8
16 11 11
...
30 4 5
30 6 9
30 8 10
"""

We can find pp by calculating GCD of a few multiples of pp.

p_mul1 = (output[5] - output[11]) * (output[9] - output[12]) - (output[7] - output[12]) * (output[7] - output[11])
p_mul2 = (output[4] - output[8]) * (output[10] - output[11]) - (output[6] - output[11]) * (output[6] - output[8])
p_mul = gcd(p_mul1, p_mul2)
# factor p_mul
p = 82854189798454303629690440183768928075006051188051668052925581650741089039941

After pp is found, we can enumerate candidates for aa by solving s1s3=an(s1s3)modps_1' - s_3' = a^n (s_1 - s_3) \mod p. Correct aa leads to correct bb and, therefore, key for AES.

PR.<a> = PolynomialRing(Zmod(p))
f = a**30 * (output[4] - output[8]) - (output[6] - output[11])
g = a**16 * (output[5] - output[11]) - (output[7] - output[12])
for a, _ in set(f.roots()) & set(g.roots()):
    PR.<b> = PolynomialRing(Zmod(p))
    f = a**3 * output[1] + (a**2 + a + 1) * b - output[2]
    b = f.roots()[0][0]

    key = sha256(b"".join([long_to_bytes(int(x)) for x in [a, b, p]])).digest()[:16]
    cipher = AES.new(key, AES.MODE_CBC, iv=iv)
    print(cipher.decrypt(enc_flag))

corctf{:msfrog:_is_pr0ud_0f_y0ur_l34pfr0gg1ng_4b1lit135}

generous

49 solves

generous.py
#!/usr/local/bin/python
from Crypto.Util.number import getPrime, inverse, bytes_to_long
from random import randrange

with open("flag.txt", "rb") as f:
	flag = f.read().strip()

def gen_keypair():
	p, q = getPrime(512), getPrime(512)
	n = (p**2) * q
	while True:
		g = randrange(2, n)
		if pow(g, p-1, p**2) != 1:
			break
	h = pow(g, n, n)
	return (n, g, h), (g, p, q)

def encrypt(pubkey, m):
	n, g, h = pubkey
	r = randrange(1, n)
	c = pow(g, m, n) * pow(h, r, n) % n
	return c

def decrypt(privkey, c):
	g, p, q = privkey
	a = (pow(c, p-1, p**2) - 1) // p
	b = (pow(g, p-1, p**2) - 1) // p
	m = a * inverse(b, p) % p
	return m

def oracle(privkey, c):
	m = decrypt(privkey, c)
	return m % 2

pub, priv = gen_keypair()
n, g, h = pub
print(f"Public Key:\n{n = }\n{g = }\n{h = }")
print(f"Encrypted Flag: {encrypt(pub, bytes_to_long(flag))}")
while True:
	inp = int(input("Enter ciphertext> "))
	print(f"Oracle result: {oracle(priv, inp)}")

This cryptosystem is Okamoto-Uchiyama cryptosystem. As mentioned in wiki, it has homomorphic property.

Let flag be mm. If we decrypt Enc(m)Enc(x)\mathrm{Enc}(m) \mathrm{Enc(-x)}, we get mxmodnm - x \mod n. When xmx \le m, the parity of mxm - x is (mmod2)(xmod2)(m \mod 2) \oplus (x \mod 2). But when x>mx > m, the parity of mxm - x is (mmod2)(xmod2)1(m \mod 2) \oplus (x \mod 2) \oplus 1 because nn is odd. Since we know mmod2m \mod 2 and xmod2x \mod 2, we can check whether mm is less than xx or not. Using this oracle we can find mm by bisection method.

solve.py
from Crypto.Util.number import long_to_bytes
from pwn import remote

io = remote("be.ax", 31244)
io.recvuntil(b"n = ")
n = int(io.recvline())
io.recvuntil(b"g = ")
g = int(io.recvline())
io.recvuntil(b"h = ")
h = int(io.recvline())
io.recvuntil(b"Encrypted Flag: ")
enc_flag = int(io.recvline().strip().decode())


def oracle(c):
    io.sendlineafter(b"ciphertext> ", str(c).encode())
    io.recvuntil(b"result: ")
    return int(io.recvline())


parity = oracle(enc_flag)
lb = 0
ub = 2**512
prev_c = None
while True:
    c = (lb + ub) // 2
    print(c)
    tmp_parity = oracle(pow(g, -c, n) * enc_flag % n)
    expected_parity = (parity + c) % 2
    if tmp_parity == expected_parity:
        lb = c
    elif tmp_parity != expected_parity:
        ub = c
    if c == prev_c:
        break
    prev_c = c
print(long_to_bytes(c))

corctf{see?1_bit_is_very_generous_of_me}