二次剩余问题

二次剩余问题之前就遇到过,但未深入学习过,也未做过记录,今天来补充。

什么是二次剩余

指一个整数n,如果存在一个整数x,使得$x^2 \equiv n \pmod p$,那么n就是p的一个二次剩余。其中,p是一个正整数,x是一个整数。如果不存在这样的x,那么n就是p的一个二次非剩余。

判断方法

Euler判别法

当且仅当$a^{\frac{p-1}{2}} \equiv 1 \pmod p$,a是p的二次剩余,即方程有解

当且仅当$a^{\frac{p-1}{2}} \equiv -1 \pmod p$,a是p的二次非剩余,即方程无解

勒让德符号(Legendre)

引入勒让德符号:

$(\frac{a}{p}) = \left{ \begin{matrix} 1,a在模p意义下是二次剩余\-1,a在模p意义下是非二次剩余\0,a\equiv0 \pmod p \end{matrix} \right.$

有结论:

$(\frac{a}{p}) \equiv a^{\frac{p-1}{2}} \pmod p$

如何解二次同余方程

前提,p为奇素数(不能被2整除的素数,是素数中的特例)。

定理一

对于上述同余方程,共有$\frac{p-1}{2}+1$个n的值能使方程有解,$+1$为n=0的情况。

定理二

设a满足$\omega = a^2-n$是模p的非二次剩余,那么:

$x\equiv (a+\sqrt \omega)^{\frac{p+1}{2}}$是二次同余方程$x^2 \equiv n \pmod p$的解。

相关算法

特殊情况

对于同余方程$x^2 \equiv n \pmod p$,且有$p \mod 4 \equiv 3$

那么$n^{\frac{p+1}{4}} \mod p$为一个解

Atkin算法

满足$p \mod 8 \equiv 5$,令$b \equiv (2n)^{\frac{p-5}{8}}\pmod p$和$i \equiv 2nb^2 \pmod p$

此时$i^2 \equiv -1 \pmod p$且$nb(i-1) \pmod p$为一个解。

Tonelli–Shanks 算法

image-20230805161744054

image-20230805161759991

代码实现:

def Legendre(n,p): #这里用勒让德符号来表示判断二次(非)剩余的过程
    return pow(n,(p - 1) // 2,p)
def Tonelli_Shanks(n,p):
    assert Legendre(n,p) == 1
    if p % 4 == 3:
        return pow(n,(p + 1) // 4,p)
    q = p - 1
    s = 0
    while q % 2 == 0:
        q = q // 2
        s += 1
    for z in range(2,p):
        if Legendre(z,p) == p - 1:
            c = pow(z,q,p)
            break
    r = pow(n,(q + 1) // 2,p)
    t = pow(n,q,p)
    m = s
    if t % p == 1:
        return r
    else:
        i = 0
        while t % p != 1: #外层循环的判断条件
            temp = pow(t,2**(i+1),p) #这里写作i+1是为了确保之后内层循环用到i值是与这里的i+1的值是相等的
            i += 1
            if temp % p == 1: # 内层循环的判断条件
                b = pow(c,2**(m - i - 1),p)
                r = r * b % p
                c = b * b % p
                t = t * c % p
                m = i
                i = 0 # 注意每次内层循环结束后i值要更新为0
        return r

sympy库中的nthroot_mod()函数也可用与求解同余式$n^{index} \equiv x \pmod p$

from sympy.ntheory.residue_ntheory import nthroot_mod

x=nthroot_mod(n,index,p)

我只是知识的搬运工,相关证明可参考:

二次剩余 - OI Wiki (oi-wiki.org)

Tonelli-Shanks算法_python_M3ng@L的博客-CSDN博客

相关例题

[V&N2020 公开赛]easy_RSA

from random import randint
from gmpy2 import *
from Crypto.Util.number import *

def getprime(bits):
    while 1:
        n = 1
        while n.bit_length() < bits:
            n *= next_prime(randint(1,1000))
        if isPrime(n - 1):
            return n - 1

m = bytes_to_long(b'flag{************************************}')

p = getprime(505)
q = getPrime(512)
r = getPrime(512)
assert m < q

n = p * q * r
e = 0x10001
d = invert(q ** 2, p ** 2)
c = pow(m, 2, r)
cipher = pow(c, e, n)

print(n)
print(d)
print(cipher)
'''
7941371739956577280160664419383740967516918938781306610817149744988379280561359039016508679365806108722198157199058807892703837558280678711420411242914059658055366348123106473335186505617418956630780649894945233345985279471106888635177256011468979083320605103256178446993230320443790240285158260236926519042413378204298514714890725325831769281505530787739922007367026883959544239568886349070557272869042275528961483412544495589811933856131557221673534170105409
7515987842794170949444517202158067021118454558360145030399453487603693522695746732547224100845570119375977629070702308991221388721952258969752305904378724402002545947182529859604584400048983091861594720299791743887521228492714135449584003054386457751933095902983841246048952155097668245322664318518861440
1618155233923718966393124032999431934705026408748451436388483012584983753140040289666712916510617403356206112730613485227084128314043665913357106301736817062412927135716281544348612150328867226515184078966397180771624148797528036548243343316501503364783092550480439749404301122277056732857399413805293899249313045684662146333448668209567898831091274930053147799756622844119463942087160062353526056879436998061803187343431081504474584816590199768034450005448200
'''

已知关系

  • $d\cdot q^2 \equiv 1 \pmod {p^2}$
  • $m^2 \equiv c \pmod r$
  • $cipher \equiv c^e \pmod n$
  • $n = p\cdot q \cdot r$

其中$n、d、cipher$已知,且$m < q$

通过getprime函数可知,生成的素数为光滑数。直接分解n得到p、q、r。然后常规RSA解出c。最后便是二次剩余问题了,用下算法即可:

n = ...
d = ...
cipher = ...
e = 0x10001

p = 102634610559478918970860957918259981057327949366949344137104804864768237961662136189827166317524151288799657758536256924609797810164397005081733039415393
q = 7534810196420932552168708937019691994681052660068275906973480617604535381306041583841106383688654426129050931519275383386503174076258645141589911492908993
r = 10269028767754306217563721664976261924407940883784193817786660413744866184645984238866463711873380072803747092361041245422348883639933712733051005791543841

phi = (p-1)*(q-1)*(r-1)
d = inverse(e,phi)
c = pow(cipher,d,n)

def Legendre(n,p): #这里用勒让德符号来表示判断二次(非)剩余的过程
    return pow(n,(p - 1) // 2,p)
def Tonelli_Shanks(n,p):
    assert Legendre(n,p) == 1
    if p % 4 == 3:
        return pow(n,(p + 1) // 4,p)
    q = p - 1
    s = 0
    while q % 2 == 0:
        q = q // 2
        s += 1
    for z in range(2,p):
        if Legendre(z,p) == p - 1:
            c = pow(z,q,p)
            break
    r = pow(n,(q + 1) // 2,p)
    t = pow(n,q,p)
    m = s
    if t % p == 1:
        return r
    else:
        i = 0
        while t % p != 1: #外层循环的判断条件
            temp = pow(t,2**(i+1),p) #这里写作i+1是为了确保之后内层循环用到i值是与这里的i+1的值是相等的
            i += 1
            if temp % p == 1: # 内层循环的判断条件
                b = pow(c,2**(m - i - 1),p)
                r = r * b % p
                c = b * b % p
                t = t * c % p
                m = i
                i = 0 # 注意每次内层循环结束后i值要更新为0
        return r

m = Tonelli_Shanks(c,r)
print(long_to_bytes(m))

D^3CTF d3qcg

from Crypto.Util.number import *
import random
from random import randint
from gmpy2 import *
from secret import flag
import hashlib
assert b'd3ctf' in flag
Bits = 512
UnKnownBits = 146

class QCG():
    def __init__(self,bit_length):
        p = getPrime(bit_length)
        a = randint(0,p)
        c = randint(0,p)
        self._key = {'a':a,'c':c,'p':p}
        self.secret = randint(0,p)
        self.high = []
    def Qnext(self,num):
        return ((self._key['a'])*num**2+self._key['c'])%self._key['p']
    
    def hint(self):
        num = self.secret
        for i in range(2):
            num = self.Qnext (num)
            self.high.append(num>>UnKnownBits)
    def get_key(self):
        return self._key
    def get_hint(self):
        return self.high

Q1 = QCG(Bits)
print(Q1.get_key())
#{'a': 3591518680290719943596137190796366296374484536382380061852237064647969442581391967815457547858969187198898670115651116598727939742165753798804458359397101, 'c': 6996824752943994631802515921125382520044917095172009220000813718617441355767447428067985103926211738826304567400243131010272198095205381950589038817395833, 'p': 7386537185240346459857715381835501419533088465984777861268951891482072249822526223542514664598394978163933836402581547418821954407062640385756448408431347}
Q1.hint()
print(Q1.get_hint())
#[67523583999102391286646648674827012089888650576715333147417362919706349137337570430286202361838682309142789833, 70007105679729967877791601360700732661124470473944792680253826569739619391572400148455527621676313801799318422]
enc = bytes_to_long(hashlib.sha512(b'%d'%(secret)).digest())^bytes_to_long(flag)
print(enc)
# 6176615302812247165125832378994890837952704874849571780971393318502417187945089718911116370840334873574762045429920150244413817389304969294624001945527125

想求flag就必须先求secret,对于secret,有下面的关系:

$num_1 = a \cdot secret^2 + c \pmod p$

$num_2 = a \cdot num_1^2 + c \pmod p$

$high[0] = num_1 >> 146,high[1] = num_2 >> 146$

$a、c、p、high[0]、high[1]$已知。

显然得想办法恢复出完整的$num_1$来。

对于$num_i$,高位记作$h_i$,低位记作$l_i$,那么$num_2 = a \cdot num_1^2 + c \pmod p$可写成:

$h_2+l_2 = a \cdot (h_1+l_1)^2 + c \pmod p$

$a \cdot (h_1+l_1)^2 - (h_2+l_2) + c \equiv 0 \pmod p$

高位$h_i$我们是知道的,大概365位。低位$l_i$不知道,有146位。

显然需要构造二元copper解。

a = ...
c = ...
p = ...
hint = [..., ...]
enc = ...

def small_roots(f, bounds, m=1, d=None):
    if not d:
        d = f.degree()

    R = f.base_ring()
    N = R.cardinality()

    f /= f.coefficients().pop(0)
    f = f.change_ring(ZZ)

    G = Sequence([], f.parent())
    for i in range(m + 1):
        base = N ^ (m - i) * f ^ i
        for shifts in itertools.product(range(d), repeat=f.nvariables()):
            g = base * prod(map(power, f.variables(), shifts))
            G.append(g)

    B, monomials = G.coefficient_matrix()
    monomials = vector(monomials)

    factors = [monomial(*bounds) for monomial in monomials]
    for i, factor in enumerate(factors):
        B.rescale_col(i, factor)

    B = B.dense_matrix().LLL()

    B = B.change_ring(QQ)
    for i, factor in enumerate(factors):
        B.rescale_col(i, 1 / factor)

    H = Sequence([], f.parent().change_ring(QQ))
    for h in filter(None, B * monomials):
        H.append(h)
        I = H.ideal()
        if I.dimension() == -1:
            H.pop()
        elif I.dimension() == 0:
            roots = []
            for root in I.variety(ring=ZZ):
                root = tuple(R(root[var]) for var in f.variables())
                roots.append(root)
            return roots

    return []

h1 = hint[0] << 146
h2 = hint[1] << 146
P.<l1,l2> = PolynomialRing(Zmod(p))
f = a*(h1 + l1)^2 + c - (h2 + l2)
l1,l2 = small_roots(f,[2^146,2^146],m=4,d=4)[0]
num1 = h1 + l1
print(num1)

m和d的取值有待考究。

defund/coppersmith: Coppersmith’s method for multivariate polynomials (github.com)

求出$num_1$后,看第一个式子:

$num_1 = a \cdot secret^2 + c \pmod p$

$secret^2 = a^{-1}\cdot (num_1 - c) \pmod p$

满足二次同余式$x^2 \equiv n \pmod p$

可利用Tonelli–Shanks算法求解secret,然后异或enc便得到了flag:

def Legendre(n,p): #这里用勒让德符号来表示判断二次(非)剩余的过程
    return pow(n,(p - 1) // 2,p)
def Tonelli_Shanks(n,p):
    assert Legendre(n,p) == 1
    if p % 4 == 3:
        return pow(n,(p + 1) // 4,p)
    q = p - 1
    s = 0
    while q % 2 == 0:
        q = q // 2
        s += 1
    for z in range(2,p):
        if Legendre(z,p) == p - 1:
            c = pow(z,q,p)
            break
    r = pow(n,(q + 1) // 2,p)
    t = pow(n,q,p)
    m = s
    if t % p == 1:
        return r
    else:
        i = 0
        while t % p != 1: #外层循环的判断条件
            temp = pow(t,2**(i+1),p) #这里写作i+1是为了确保之后内层循环用到i值是与这里的i+1的值是相等的
            i += 1
            if temp % p == 1: # 内层循环的判断条件
                b = pow(c,2**(m - i - 1),p)
                r = r * b % p
                c = b * b % p
                t = t * c % p
                m = i
                i = 0 # 注意每次内层循环结束后i值要更新为0
        return r

num1 = 6023304966622247460261427847144394818572943247946434323275721792843243938440324294324349326166203828252327046668948034768905493329350113405677812338671880
n = inverse(a,p)*(num1 - c)
secret = Tonelli_Shanks(n,p)

import hashlib
flag = enc ^ bytes_to_long(hashlib.sha512(b'%d'%(secret)).digest())
print(long_to_bytes(flag))
#b'Here_is_ur_flag!:)d3ctf{th3_c0oppbpbpbp3rsM1th_i5_s0_1ntr35ting}'

这道题里考的求解二元多项式也很重要。

这两道题只是简单用来下TS算法,其它类型的待补充。

参考:

AntCTF x D^3CTF_Crypto_部分复现_M3ng@L的博客-CSDN博客


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1666739907@qq.com
github