文章

0xgame 2024-Crypto-Week 2

[Week 2] Diffie-Hellman

  • Diffie-Hellman题目是一个很好的hint,去了解了一下
  • 这是一种公钥交换的算法,在A,B两人处各自生成一对密钥(A,a)和(B,b)
  • 其中A和B是公钥用来互相间传输的,a和b是私钥,保存在本地
  • 然后两人用自己的私钥和对方的公钥就可以生成出S(共享公钥),两人计算出来的S是相同的
  • 然后查看题目附件,就是在靶机上生成(A,a),并发送A
  • 那本地为了能建立通信,同样也生成(B,b),并发送B。(此处生成过程可以套用附件的函数)
  • 此时靶机便会计算出S,继而计算出MD5,并以此用作AES加密的密钥
  • 最终输出密文
  • 那本地同样用b和A生成S,然后用自带库AES解密密文
  • 编写脚本
from string import ascii_letters, digits
from hashlib import sha256
from itertools import product
from Crypto.Cipher import AES
from hashlib import md5
from pwn import *

ip = '118.195.138.159' #要netcat的ip
port = 10000 #端口
io = remote(ip,port)

def proof():
	io.recvuntil(b'XXXX+')
	proof = io.recvuntil(b')')[:-1]
	io.recvuntil(b'== ')
	hash = io.recvuntil(b'\n')[:-1].decode()
	dict = ascii_letters + digits
	for word in product(dict, repeat=4):
		word = ''.join(word).encode()
		if sha256( (word+proof) ).hexdigest() == hash: break
	io.sendlineafter(b'XXXX: ',word)

def MD5(m):return md5( str(m).encode() ).digest()

proof()
q,g = io.recvline().decode()[15:-2].split(", ")
q,g = int(q),int(g)
Bob_PriKey = randint(1, q)
Bob_PubKey = pow(g, Bob_PriKey, q)
Alice_PubKey = int(io.recvline().decode()[15:-1])
print(f"Alice_PubKey={Alice_PubKey}")
io.recvuntil(b"[+] Give me the Bob_PubKey\n>")
io.sendline(str(Bob_PubKey).encode('utf-8'))
io.recvline()
c = io.recvline().decode()[17:-1]
Share_Key = pow(Alice_PubKey,Bob_PriKey,q)
Cipher = AES.new(MD5(Share_Key), AES.MODE_ECB)
m = Cipher.decrypt(bytes.fromhex(c))
print(m.strip(b"\x00"))
  • 0xGame{107c7960-d339-48b5-92b9-d59ad5644cf6}

[Week 2] Elgamal

  • 本题有关Elgamal数字签名

有关Elgamal数字签名

1. 系统初始化

选择一个大素数ppp和一个生成元ggg(通常是gggppp的原根)。然后选择一个私钥xxx,满足 1<x<p11<x<p−11<x<p1。公钥由以下元素构成:

  • ppp
  • ggg
  • y=gxmodpy = g^xmod\\ py=gxmodp

2. 签名生成

要签署消息mmm,执行以下步骤:

  1. 哈希消息:使用安全的哈希函数(如SHA-256)计算消息的哈希值H(m)H(m)H(m)
  2. 选择随机数:选择一个随机数kkk,满足1<k<p11<k<p−11<k<p1kkkvarphi(p)=p1\\varphi(p) = p-1varphi(p)=p1互质。
  3. 计算签名
    • 计算r=gkmodpr = g^k mod\\ pr=gkmodp
    • 计算s=k1cdot(H(m)+xcdotr)mod(p1)s = k^{-1} \\cdot (H(m) + x \\cdot r) mod (p-1)s=k1cdot(H(m)+xcdotr)mod(p1),其中k1k^{-1}k1kkkvarphi(p)\\varphi(p)varphi(p)的逆元。

签名为(r,s)(r,s)(r,s)

3. 签名验证

接受放在接收到消息mmm(r,s)(r,s)(r,s)后,可以通过以下步骤验证签名

  1. 验证rrr的有效性:检查0<r<p0<r<p0<r<p0<s<p10<s<p−10<s<p1是否成立。
  2. 计算哈希值:计算H(m)H(m)H(m)
  3. 计算验证值
    • 计算u_1=yrrsmodpu\_1=y^rr^smod\\ pu_1=yrrsmodp
    • 计算u_2=gmmodpu\_2=g^mmod\\ pu_2=gmmodp
  4. 验证签名:如果u_1=u_2modpu\_1=u\_2\\ mod\\ pu_1=u_2modp,则签名有效,否则无效。
  • 题干有明显提到在验签函数中参数校验出现问题
  • 检查发现,(r,s)(r,s)(r,s)是对qqq取模后的结果
  • 然而在验签中并没有校验rrrsss的大小,也就是说,我可以传入比qqq大的数
  • 这里就存在了伪造签名的可能性
  • 然而Elgamal并不是直接对明文加密,而是对其的sha256加密
  • 由于目前sha256的不可碰撞性,和无法预知性,并不能推测出伪造明文mm’m
  • 因而要依据mmmmm’m的关系进行推算得到rr’rss’s
  • 以下为推导过程

根据验签原理,已知gmmodp=yrrsmodp根据验签原理,已知g^mmod\\ p=y^rr^smod\\ p根据验签原理,已知gmmodp=yrrsmodp

要构造(m,r,s)满足gmmodp=yrrsmodp要构造(m’,r’,s’)满足g^{m’}mod\\ p=y^{r’}r’^{s’}mod\\ p要构造(m,r,s)满足gmmodp=yrrsmodp

存在关系m=kmmod(p1)存在关系m’=km\\ mod(p-1)存在关系m=kmmod(p1)

gmmodp=gkm+k(p1)modp=(gkmmodpcdotgk(p1)modp)modpg^{m’}mod\\ p=g^{km+k'(p-1)}mod\\ p=(g^{km}mod\\ p\\cdot g^{k'(p-1)}mod\\ p)mod\\ pgmmodp=gkm+k(p1)modp=(gkmmodpcdotgk(p1)modp)modp

根据费马小定理avarphi(p)modp=1gcd(p,1)=1根据费马小定理a^{\\varphi (p)}mod\\ p=1且gcd(p,1)=1根据费马小定理avarphi(p)modp=1gcd(p,1)=1

Rightarrowgkmmodp=ykrrksmodp\\Rightarrow g^{km}mod\\ p=y^{kr}r^{ks}mod\\ pRightarrowgkmmodp=ykrrksmodp

Rightarrowykr+k_1(p1)(r+k_2p)ksmodp=yrrsmodp\\Rightarrow y^{kr+k\_1(p-1)}(r+k\_2p)^{ks}mod\\ p=y^{r’}r’^{s’}mod\\ pRightarrowykr+k_1(p1)(r+k_2p)ksmodp=yrrsmodp

使kr+k_1(p1)=r+k_2p使kr+k\_1(p-1)=r+k\_2p使kr+k_1(p1)=r+k_2p

Rightarrowk_1=(k1)rmodp\\Rightarrow k\_1=(k-1)r\\ mod\\ pRightarrowk_1=(k1)rmodp

\\therefore \\left\\{\\begin{matrix}r’=kr+k\_1(p-1)\\\\s’=ks\\end{matrix}\\right.

  • 然后手写脚本,逆元计算k
from string import ascii_letters, digits
from hashlib import sha256
from itertools import product
from Crypto.Util.number import *
from pwn import *

ip = '118.195.138.159' #要netcat的ip
port = 10002 #端口
io = remote(ip,port)

def proof():
	io.recvuntil(b'XXXX+')
	proof = io.recvuntil(b')')[:-1]
	io.recvuntil(b'== ')
	hash = io.recvuntil(b'\n')[:-1].decode()
	dict = ascii_letters + digits
	for word in product(dict, repeat=4):
		word = ''.join(word).encode()
		if sha256( (word+proof) ).hexdigest() == hash: break
	io.sendlineafter(b'XXXX: ',word)

proof()
q,g,y = io.recvline().decode()[23:-2].split(", ")
q,g,y = map(int,[q,g,y])
phi = q-1
msg = bytes.fromhex(io.recvline().decode()[16:-1])
r,s = io.recvline().decode()[28:-2].split(", ")
r,s = map(int,[r,s])
io.recvuntil(b"Now, it's your turn to help me sign something\n[+] Give me your message:\n>")

msg_ = (b'Welcome_to_0xGame2024').hex()
io.sendline(msg_.encode('utf-8'))
msg_ = bytes.fromhex(msg_)
m = int(sha256(msg).hexdigest(),16)
m_ = int(sha256(msg_).hexdigest(),16)
k = m_*inverse(m,(q-1))
s_ = k*s
k1 = (k-1)*r//q+(k-1)*r%q
k2 = (k-1)*r%q
r_ = r+k1*q

io.recvuntil(b"[+] Give me your r:\n>")
io.sendline(str(r_).encode('utf-8'))
io.recvuntil(b"[+] Give me your s:\n>")
io.sendline(str(s_).encode('utf-8'))
io.interactive()
  • 0xGame{93e9adb8-8a6d-4517-9c61-13081c413e41}

[Week 2] RC4

  • 分析代码发现,在util文件中定义了由密钥KEY生成密钥流keystream
  • 以及异或加密
  • 用脚本连接靶机通过人机验证
from string import ascii_letters, digits
from hashlib import sha256
from itertools import product
from pwn import *

ip = '118.195.138.159' #要netcat的ip
port = 10001 #端口
io = remote(ip,port)

def proof():
	io.recvuntil(b'XXXX+')
	proof = io.recvuntil(b')')[:-1]
	io.recvuntil(b'== ')
	hash = io.recvuntil(b'\n')[:-1].decode()
	dict = ascii_letters + digits
	for word in product(dict, repeat=4):
		word = ''.join(word).encode()
		if sha256( (word+proof) ).hexdigest() == hash: break
	io.sendlineafter(b'XXXX: ',word)
	
proof()
io.interactive()
  • 输入明文获取对应的密文,以及flag的密文
  • 注意:因为密钥流是256长度随机生成,所以输入明文必须比flag长,才能计算出加密时使用过的密钥
  • 编写脚本计算flag
m = "6162636465666768696a6b6c6d6e6f707172737475767778797a6162636465666768696a6b6c6d6e6f707172737475767778797a"
c = "1c2227967f1f7e60b38d47862cd1bf575075ac29b4108d2a7604e4368cc63b5f8d89d93701d6a26257f97da0952b15a5211d94d9"
enc = "4d380393771c6230ebd114d8728ce70a1730ec6cec5fcb313853e164d992730a8f87d3390cdffe6e59bc6aaf"
KEY = None
def recover_key(plaintext, ciphertext):
    pt_bytes = bytes.fromhex(plaintext)
    ct_bytes = bytes.fromhex(ciphertext)
    keystream = bytes([b1 ^ b2 for b1, b2 in zip(pt_bytes, ct_bytes)])
    return keystream
enc = bytes.fromhex(enc)
keystream = recover_key(m,c)
flag = bytes([b1 ^ b2 for b1, b2 in zip(enc, keystream)])
print(flag)
  • 此处的输入我用了[a-zA-Z]
  • 最终得到0xGame{81682337-6731-91c7-d060-3efcdfe1ba5f}

[Week 2] RSA-IV

  • 都是常见的RSA攻击类型,BUUCTF全刷到过,查看原理以及之前的wp
  • 主要不想手动解,便花了点时间用pwntool写了个自动脚本
  • challenge1是低加密指数攻击
  • challenge2是dp泄露
  • challenge3是维纳攻击
  • challenge4是共模攻击
from string import ascii_letters, digits
from hashlib import sha256
from itertools import product
from pwn import *
from Crypto.Util.number import *
from RSAwienerHacker import *
import gmpy2

ip = '118.195.138.159' #要netcat的ip
port = 10003 #端口
io = remote(ip,port)

def proof():
	io.recvuntil(b'XXXX+')
	proof = io.recvuntil(b')')[:-1]
	io.recvuntil(b'== ')
	hash = io.recvuntil(b'\n')[:-1].decode()
	dict = ascii_letters + digits
	for word in product(dict, repeat=4):
		word = ''.join(word).encode()
		if sha256( (word+proof) ).hexdigest() == hash: break
	io.sendlineafter(b'XXXX: ',word)

def slove0():
	io.recvuntil(b"[+] input choice:\n>")
	io.sendline(b'0')
	n,e,c = io.recvline().decode()[1:-2].split(', ')
	n,e,c = int(n),int(e),int(c)
	# 低加密指数广播攻击
	io.recvuntil(b">")
	while True:
		if gmpy2.iroot(c,e)[1]:
			m = gmpy2.iroot(c,e)[0]
			print(f"1:{m}")
			break
		c += n
	io.sendline(str(m).encode('utf-8'))
	
def slove1():
	io.recvuntil(b"[+] input choice:\n>")
	io.sendline(b'1')
	n,e,c,dp = io.recvline().decode()[1:-2].split(', ')
	n,e,c,dp = int(n),int(e),int(c),int(dp)
	# dp泄露
	a = dp*e-1
	for x in range(2,e):
		if a%x == 0:
			p = a//x+1
			if n%p == 0:
				q = n//p
				break
	d = inverse(e,(p-1)*(q-1))
	m = pow(c,d,n)
	print(f"2:{m}")
	io.sendline(str(m).encode('utf-8'))

def slove2():
	io.recvuntil(b"[+] input choice:\n>")
	io.sendline(b'2')
	n,e,c = io.recvline().decode()[1:-2].split(', ')
	n,e,c = int(n),int(e),int(c)
	# 维纳攻击
	d = hack_RSA(e,n)
	m = pow(c,d,n)
	print(f"3:{m}")
	io.sendline(str(m).encode('utf-8'))

def slove3():
	io.recvuntil(b"[+] input choice:\n>")
	io.sendline(b'3')
	n,e,c,e_,c_ = io.recvline().decode()[1:-2].split(', ')
	n,e1,c1,e2,c2 = int(n),int(e),int(c),int(e_),int(c_)
	# 共模攻击
	e1_e2=e1-e2
	s1 = inverse(e1_e2,e2)
	s2 = (1-e1*s1)//e2
	m = pow(c1,s1,n)*pow(c2,s2,n)%n
	print(f"4:{m}")
	io.sendline(str(m).encode('utf-8'))
	
proof()
slove0()
slove1()
slove2()
slove3()
io.interactive()
  • 最后拿到0xGame{2b5e024a-3c62-4f4a-afe0-b81851d9efc8}

[Week 2] LFSR-baby

  • LFSR是指线性(Linear)反馈(Feedback)移位(Shift)寄存器(Register)
  • 即有一个可移动的寄存器,通过反馈计算出下一个移动的数值,而其计算方式是线性的
  • 分析源码得到存在一个mask固定窗口和state滑动窗口,最初始的state即为seed
  • 两个列表中的二进制对应按位与运算,将所有结果异或保存至output
  • output即为生成的随机数,存入getrandbits函数中的result末尾
  • 将output存入state末尾,使窗口向后滑动一位
  • 继续反复操作,不断生成随机数
  • 注意到题目给的随机数是生成128位后的结果
  • 因此可以判断,一组随机数生成后正好完全将原先128位的seed从state变量中顶出

  • 如图,假设这是最后一次生成随机数,计算时的mask是1128,state是0127,上下同列的按位与运算,再异或,存入state的末尾(128)最终红色框内的128位数就是最终输出的随机数
  • 也就是说,可以通过脚本算出state中0位置是什么,然后再反复使用这种方式计算出原来完整的128位seed
  • 下面编写脚本
from hashlib import md5

def MD5(m):return md5(str(m).encode()).hexdigest()

def init_state(seed):
    result = [int(i) for i in bin(seed)[2:]]
    PadLenth = 128 - len(result)
    result += [ 0 ] * PadLenth
    assert len(result) == 128
    return result

def init_random(seed):
    result = [int(i) for i in bin(seed)[2:]]
    PadLenth = 128 - len(result)
    result = [ 0 ] * PadLenth + result
    assert len(result) == 128
    return result

random1 = 103763907686833223776774671653901476306
copy = random1
random2 = 136523407741230013545146835206624093442
Mask_seed = 245818399386224174743537177607796459213
random1,random2 = map(init_random,[random1,random2])
mask = init_state(Mask_seed)

def calc(state):
    for i in range(128):
        output = 0
        for i in range(1,128):
            output += state[i-1]*mask[i]
        output += state[-1]
        output = output%2
        state = [output] + state[:-1]
    return state
result = int(''.join(str(x) for x in calc(random2)),2) == copy
print(f"The calculation is {result}")
print("0xGame{"+MD5(int(''.join(str(x) for x in calc(random1)),2))+"}")
  • 代码中要注意随机数的长度并没有128位要手动补零,同时是在开头补,而不是调用源码的方法
  • 另外可以用第二个随机数来检验算法是否正确
  • 运行得到0xGame{030ec00de18ceb4ddea5f6612d28bf39}

[Week 2] LFSR-easy

  • 这题是依据种子和随机数倒推掩码
  • mask用x_1 x_128x\_1~x\_128x_1 x_128表示
  • seed用s_1 s_128s\_1~s\_128s_1 s_128表示
  • random用x_129 x_256x\_129~x\_256x_129 x_256表示
  • 可以得到如下的计算式

\\sum\_{i=1}^{128} s\_{i+n}x\_i\\ mod\\ 2=s\_{129+n}m\\ ,\\ n\\in \[0,128\]

  • 因此可以写出在Zmod 2数域下的矩阵

\\begin{bmatrix} s\_{1} & \\dots & s\_{128}\\\\ \\dots & \\dots & \\dots\\\\ s\_{128} & \\dots & s\_{255}\\end{bmatrix}\\begin{bmatrix} s\_{129}\\\\ \\dots\\\\ s\_{256}\\end{bmatrix}

  • 用Sage编写脚本计算解
from hashlib import md5

def MD5(m):return md5(str(m).encode()).hexdigest()

def init_state(seed):
    result = [int(i) for i in bin(seed)[2:]]
    PadLenth = 128 - len(result)
    result += [ 0 ] * PadLenth
    assert len(result) == 128
    return result

def init_random(seed):
    result = [int(i) for i in bin(seed)[2:]]
    PadLenth = 128 - len(result)
    result = [ 0 ] * PadLenth + result
    assert len(result) == 128
    return result

random1 = 299913606793279087601607783679841106505
random2 = 192457791072277356149547266972735354901
seed = 165943427582675380464843619836793254673
random1,random2 = map(init_random,[random1,random2])
seed = init_state(seed)

def solve_GF2_linear_system(A, b):
    """
    使用 SageMath 在 GF(2) 上求解线性方程组 Ax = b
    :param A: 系数矩阵
    :param b: 结果向量
    :return: 解向量 x
    """
    F = GF(2)
    A_GF2 = Matrix(F, A)
    b_GF2 = vector(F, b)

    try:
        x = A_GF2.solve_right(b_GF2)
        return x
    except ValueError:
        return None

def solution(m):
    a,b = m[0],m[1]
    solution = solve_GF2_linear_system(a, b)
    if solution:
        print(f"解向量为: {solution}")
        return solution
    else:
        print("无解")
        return None

def change(seed,random):
    All = seed + random
    a = [[0]*128 for _ in range(128)]
    b = random
    for i in range(128):
        a[i] = All[i:i+128]
    return (a,b)

ans1 = solution(change(seed,random1))
ans2 = solution(change(random1,random2))
print("The calculation is ",ans1 == ans2)
if ans1 == ans2:
    print("0xGame{"+MD5(int("".join(str(i) for i in ans1),2))+"}")
  • 解出0xGame{d56821feacab64cdb87c754ad06823a2}

0 条评论