[Week 3] ECC-DH
ECDH 算法的工作流程:
- 椭圆曲线选择:
- 首先,双方需要选定一条公共的椭圆曲线。椭圆曲线可以用方程 $y^2 = x^3 + ax + b \mod p$表示,其中 $a$、$b$ 是曲线参数,$p$ 是素数,用于定义有限域上的曲线。
- 在这条曲线上,双方还需要选定一个公共点 $G$,称为基点。基点是椭圆曲线上的一个已知点,通信双方将用它来生成密钥。
- 密钥生成:
- Alice 随机生成一个私钥 $a$,这个私钥是一个整数。
- 她计算对应的公钥 $A = a \cdot G$,这里的点乘是椭圆曲线点的标量乘法(多次加法运算)。
- Bob 也生成一个随机的私钥 $b$,并计算出对应的公钥 $B = b \cdot G$。
- Alice 随机生成一个私钥 $a$,这个私钥是一个整数。
- 公钥交换:
- Alice 将她的公钥 $A$ 发送给 Bob,Bob 将他的公钥 $B$ 发送给 Alice。
- 共享密钥计算:
- Alice 使用她的私钥 $a$ 和 Bob 的公钥 $B$ 计算共享密钥:
$S = a \cdot B = a \cdot (b \cdot G) = (a \cdot b) \cdot G$ - Bob 使用他的私钥 $b$ 和 Alice 的公钥 $A$ 计算共享密钥:
$S = b \cdot A = b \cdot (a \cdot G) = (b \cdot a) \cdot G$ - 由于点乘是交换的,Alice 和 Bob 最终计算得到相同的共享密钥 $S$。
- Alice 使用她的私钥 $a$ 和 Bob 的公钥 $B$ 计算共享密钥:
- 很好理解啊,不涉及比较难的数学知识,只要搞懂原理就行,也用会算
- 和[Week 2] Diffie-Hellman逻辑上几乎一样,就是本地生成私钥和公钥,然后与靶机交互确定共享公钥
- 最终用AES对称加密来传输数据
- 上代码
from hashlib import md5, sha256
from itertools import product
from string import ascii_letters, digits
from Crypto.Cipher import AES
from pwn import *
from Util import *
addr = "nc 118.195.138.159 10004".split(" ")
io = remote(addr[1],int(addr[2]))
def MD5(m):return md5( str(m).encode() ).digest()
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()
a = 10809567548006703521
b = 9981694937346749887
p = 25321837821840919771
E = Curve(a, b, p)
g_x,g_y = map(int,io.recvline().decode()[15:-2].split(","))
a_x,a_y = map(int,io.recvline().decode()[20:-2].split(","))
G = Point(g_x,g_y,E)
A = Point(a_x,a_y,E)
b = randint(1, p)
B = b * G
io.sendlineafter(b"[+] Give me the Bob_PubKey.x\n>",str(B.x).encode('utf-8'))
Share_Key = b * A
Cipher = AES.new(MD5(Share_Key.x), AES.MODE_ECB)
io.recvline()
c = bytes.fromhex(io.recvline().decode()[21:-1])
m = Cipher.decrypt(c)
print(m)
io.interactive()
- 0xGame{71234da9-baf8-406e-9cc7-d08ceedea945}
[Week 3] ECC-baby
- ECC加密用来交换对称密钥
- 可以发现素数p不是太大,于是尝试暴力计算key或k
- 实测k和key都能爆出来
- Sage脚本:Baby-step Giant-step 算法求解 ECDLP
# Sage 环境
def baby_step_giant_step(E, G, P, n):
"""
Baby-step Giant-step 算法求解椭圆曲线离散对数问题
E: 椭圆曲线
G: 基点
P: 已知点 P = k * G
n: 椭圆曲线的阶
"""
m = ceil(sqrt(n))
# Step 1: Baby-step, 计算表 {i: i*G} for i = 0, 1, ..., m-1
baby_steps = {}
for i in range(m):
baby_steps[i * G] = i
# Step 2: Giant-step, 计算 j * (-m * G)
inv_mG = -m * G
current = P
for j in range(m):
if current in baby_steps:
return j * m + baby_steps[current]
current += inv_mG
# 如果没有找到解,返回None
return None
# 椭圆曲线参数
p = 4559252311 # 椭圆曲线的素数域
a = 1750153947 # 椭圆曲线参数 a
b = 3464736227 # 椭圆曲线参数 b
E = EllipticCurve(GF(p), [a, b]) # 定义椭圆曲线
# 基点 G 和已知点 P = k * G
G = E(2909007728, 1842489211)
P = E(1923527223,2181389961)
# 椭圆曲线的阶
n = E.order()
# 使用 Baby-step Giant-step 求解 k
key = baby_step_giant_step(E, G, P, n)
if k is not None:
print(f"Found key: {key}")
else:
print("No solution found.")
- 得到
key = 1670419487
- 然后用内置的点乘计算$P = G’ \cdot key$
- 再进行逆运算$M = C + ( – P’)$
- 又可以根据定义可以知道$−Q=(x_Q,−y_Qmod\ p)$
- 所以搓脚本
from hashlib import md5
from Crypto.Cipher import AES
from Util import *
def MD5(m):return md5(str(m).encode()).digest()
p = 4559252311
a = 1750153947
b = 3464736227
curve = Curve(a, b, p)
G = Point(2909007728,1842489211,curve)
P = Point(1923527223,2181389961,curve)
G_= Point(1349689070,1217312018,curve)
C = Point( 662346568,2640798701,curve)
enc= bytes.fromhex("29bb47e013bd91760b9750f90630d8ef82130596d56121dc101c631dd5d88201a41eb3baa5aa958a6cd082298fc18418")
key = 1670419487
P_ = G_*key
M = C + Point(P_.x,(-P_.y)%p,curve)
Cipher = AES.new(MD5(M.x), AES.MODE_ECB)
m = Cipher.decrypt(enc)
print(m)
- 0xGame{0b0e28c2-b36d-d745-c0be-fcf0986f316a}
[Week 3] EzLogin-I
- 分析源码,要使输入的cookie能被正确解析为{“username”: “admin”, “time”: ……}才能得到flag
- 分析发现对输入转化为json然后进行了AES.CBC加密,再以Base64输出
- 可以利用CBC加密特性,字节翻转攻击,定向改变某个解析后的明文
- 下面引用CBC加密和解密的图示
- 下面是反转攻击示例
- 由于C=B⊕A,可以特定改变A,使C变成想要的指定字符C’
- 推导得A’=A⊕C⊕C’
- 但要注意的是,虽然C被指定改变,但A的改变会影响到整段第一明文的变化
- 而json是格式固定的,也就意味着只能修改通过修改IV来修改第一明文,而无法改变第二明文
- 构造payload
from base64 import b64decode, b64encode
from pwn import *
addr = "nc 118.195.138.159 10005".split(" ")
io = remote(addr[1],int(addr[2]))
io.sendlineafter('''+--------------+
| [R] Regist |
| [L] Login |
| [F] Getflag |
+--------------+
[+] Tell me your choice:
>''',b"R")
io.sendlineafter("[+] username:\n>",b"zdmin")
base64en = io.recvline().decode()[13:]
base64de = bytearray(b64decode(base64en))
base64de[14] = base64de[14] ^ ord("z") ^ ord("a")
fake_cookie = b64encode(bytes(base64de))
io.sendlineafter("[+] Tell me your choice:\n>",b"L")
io.sendlineafter("[+] cookie:\n>",fake_cookie)
io.interactive()
- 0xGame{ad34acff-a813-4bc3-a44a-c270edf244b7}
[Week 3] EzLogin-II
- 在CBC加密的基础上,由于是分块处理,所以要对不能成块的部分进行填充
- 这里填充通常采用的是PKCS#7的标准进行填充,即填充字符个数与填充值相同,并且必须填充
- 如1234→1234\0x04\0x04\0x04\0x04
- 或12345678→12345678\0x08\0x08\0x08\0x08\0x08\0x08\0x08\0x08
- (上述是对于8个字节分块的情况举例)
- 下面讲述Padding Oracle Attack(或者CTF Wiki会比我更详细)
- 参考上一题的解密方式能发现
- 由于Block i在经过key解密后要和Block i-1异或得到Plaintxt
- 也就是说可以改变Block i-1的值来改变Plaintxt的结果,这都是上一题已知的
- 这题由于对明文脱padding的方法unpad中有Unpad error的报错
- 可以通过构造Block i-1的最后一个字节使得Plaintxt中的最后一个字节为\0x01
- 可以发现这样是不会报错的,而其他不正确的填充则会报错
- 所以可以通过靶机的回显来判断构造是否正确,从而得到正确明文(已知假明文,假IV可以推出中间值,与真IV异或后得到真明文)
- 然后再对倒数第二个字节同样方式爆破
- 注意:这里要保证爆破字节以外的字节满足假明文为\0x02
- 明白原理后就可以手搓脚本了
from base64 import b64decode, b64encode
from pwn import *
addr = "nc 118.195.138.159 10005".split(" ")
io = remote(addr[1], int(addr[2]))
io.sendlineafter('''+--------------+
| [R] Regist |
| [L] Login |
| [F] Getflag |
+--------------+
[+] Tell me your choice:
>''', b"F")
base = io.recvline().decode()[20:]
enc = b64decode(base)
io.sendlineafter("[+] Tell me your choice:\n>", b"L")
alpha = [4]
for i in "0123456789abcdef-}{xGame":
alpha += [ord(i)]
def padding_oracle_attack(ciphertext):
# 分块
blocks = [ciphertext[i:i + 16] for i in range(0, len(ciphertext), 16)]
decrypt = []
for block_index in range(1,len(blocks))[::-1]:
# 构造IV和密文块
current_block = blocks[block_index]
iv = blocks[block_index-1]
fake_iv = bytearray(iv)
# 遍历IV块
for attack_index in range(1,17):
# 伪造IV块中先前值
for change_index in range(1,attack_index):
fake_iv[-change_index] = fake_iv[-change_index] ^ (attack_index-1) ^ attack_index
# 遍历字节值
for bytes_value in range(1,256):
fake_iv[-attack_index] = bytes_value
print(f"尝试: bytes_value: {bytes_value:3}, fake_iv: {fake_iv.hex()}")
print(f"已得: {decrypt}\n")
if sendtest(fake_iv + current_block):
if (attack_index ^ bytes_value ^ iv[-attack_index]) in alpha:
decrypt += [attack_index ^ bytes_value ^ iv[-attack_index]]
break
return "".join([chr(i) for i in decrypt])
def sendtest(modified_ciphertext):
# 测试靶机响应
encoded_ciphertext = b64encode(modified_ciphertext).decode()
io.sendlineafter("[+] cookie:\n>", encoded_ciphertext)
resp = io.recvline().decode()
print("响应:", resp[:-1])
return resp != "[!] Unkown Wrong\n"
# 执行 padding oracle 攻击
decrypted = padding_oracle_attack(enc)[::-1]
print("解密结果: ", decrypted)
io.close()
注意几个点
- 原码在经过unpad之后有decode的处理,所以说不能转化为字符的字节同样会引发Unkown Wrong的报错,所以构造IV的时候不要用随机值或者全0填充
- 由于直接认定flag是uuid格式并且是由\0x04填充的,所以可以写入alpha,来避免其他同样可能满足不报错的可能性。(正常来说对于末尾字节会有多种可能,若当前可能无法继续往下爆破的时候,需要代码处理好回退操作。)
- 靶机测试响应中,有可能会成功json load从而触发TypeError Wrong报错,因此直接判断是否为Unkown Wrong即可
- 0xGame{6e02937e-634d-4f6f-8ef6-e5f387006cde}
大佬的解法
速度很快,用了明文攻击,来自三顺七
from pwn import *
from base64 import b64encode, b64decode
from time import time
#context(log_level = 'debug')
s = time()
xor = lambda a, b: bytes([x^y for x, y in zip(a, b)])
io = remote('118.195.138.159', 10005)
io.sendlineafter(b'>', b'R')
io.sendlineafter(b'>', b'Admin')
io.recvuntil(b'[+] cookie : ')
cookie = io.recvline()[:-1]
prefix = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\x00'
cookie = xor(b64decode(cookie), prefix) + b64decode(cookie)[16:]
io.sendlineafter(b'>', b'L')
io.sendlineafter(b'>', b64encode(cookie))
print(io.recvline().decode())
def Send(payload):
io.sendlineafter(b'>', b64encode(payload))
result = io.recvline()
#print(result)
if result == b'[!] Unkown Wrong\n':
return 0
elif result == b'[!] JSON Wrong\n':
return 1
elif result == b'[!] TypeError Wrong\n':
return 1
def PaddingOracle(before,after):
d_iv = [0 for _ in range(16)]
dict = list(b'0123456789abcdef{}xGame-') + [i for i in range(2, 16)]
print(f'dict = {dict}')
for i in range(1, 17):
flag = 1
Pad = [0 for _ in range(17-i)]
if i == 1:
Index = []
else:
Index = [i for _ in range(i-1)]
Prefix = Pad + Index
for j in dict:
d_iv[-i] = j ^ i
IV = bytes(xor(xor(before,d_iv), Prefix))
payload = IV + after
result = Send(payload)
if result == 1:
d_iv[-i] = j
flag = 0
print(i, d_iv)
break
if flag == 1:
for j in dict[::-1]:
d_iv[-(i-1)] = j ^ i
IV = bytes(xor(xor(before,d_iv), Prefix))
payload = IV + after
result = Send(payload)
if result == 1:
d_iv[-(i-1)] = j
print(i, d_iv)
break
for j in dict:
d_iv[-i] = j ^ i
IV = bytes(xor(xor(before,d_iv), Prefix))
payload = IV + after
result = Send(payload)
if result == 1:
d_iv[-i] = j
flag = 0
print(i, d_iv)
break
return bytes(d_iv)
def Oracle():
io.sendlineafter(b'>', b'F')
io.recvuntil(b'[+] Here is flag2 : ')
enc = io.recvline()
print(enc)
msg = b64decode(enc)
io.sendlineafter(b'>', b'L')
result = b''
BlockLength = len(msg)//16
print(f'[+] BlockLength = {BlockLength}')
Block = [msg[16 * i: 16 * (i+1)] for i in range(BlockLength)]
for i in range(0,BlockLength - 1):
print(f'[x] Procing BlockIndex : {i}')
result += PaddingOracle(Block[i], Block[i+1])
return result
flag2 = Oracle()
print(flag2)
io.close()
e = time()
print(f'[+] Cost : {e-s}')
[Week 3] LLL-I
- 原理比较难搞(我不会
- 题干说LLL算法出来就行,所以写如下sage
from sage.all import *
B = Matrix(ZZ, [[1849784703482951012865152264025674575, 2664848085955925754350117767673627932, 2099783527396520151610274180590854166, 1020558595577301617108111920545804527],
[1207449566811121614020334020195802372, 1954621976999112878661150903673543232, 1326050406731534201574943690688237338, 1361813208094227445768111591959011963],
[888810907577479776819993141014777624 , 1216302736807928240875874427765340645, 1027359437421599069599327712873719567, 238961447144792739830554790892164336 ],
[60622164517940943037274386912282 , 82958508138755168576836012717468 , 70072118066826856564329627650828 , 16296740862142507745322242235326 ]])
print(B.LLL())
- 由于flag混入在第一行,得到矩阵后取第一行,无视正负就是flag
- (由于LLL算出来的是最短基向量,而正负不会影响最短的特性)
- 脚本
from Crypto.Util.number import *
c = [ -58596440058654765094286903, -69377248846131264731819316, -60910008503494441471652194, -58497746791226042414948989]
print("".join([(long_to_bytes(abs(i))).decode() for i in c]))
- 0xGame{04679c42-2bc1-42b2-b836-1b0ca542f36b}
后续补充的原理
- $CM=S$由于$||M||=1$可以认为$C$与$S$是等价的
- 而又由于$C$是随机生成的其施密特正交化程度几乎可以认为是最高–随机矩阵正交性的证明
- 因此对$S$进行LLL算法求其等价正交格基就等于求$C$
[Week 3] LLL-II
参考HNP讲解
- 基于LCG的生成规律$X_{n+1}=aX_n+b\ mod\ m$
- 已知如下式子
$$\left\{\begin{align} Cs[0]&=a*seed\ mod\ m\\ Cs[1]&=a*Cs[0]+b_1\ mod\ m\\ Cs[i]&=a*Cs[i-1]+b_i\ mod\ m\\ b_i&=Cs[i]-a*Cs[i-1]+k_i*m\\ \end{align}\right.$$
- 然后构造格
$$\begin{array}\ (k_1&k_2&k_3&k_4&-a&1) \left[\matrix{ m&0&0&0&0&0\\ 0&m&0&0&0&0\\ 0&0&m&0&0&0\\ 0&0&0&m&0&0\\ Cs[0]&Cs[1]&Cs[2]&Cs[3]&K/n&0\\ Cs[1]&Cs[2]&Cs[3]&Cs[4]&0&K } \right] = (b_1&b_2&b_3&b_4&K\cdot a/m&K) \end{array}$$
- 其中$K$是$b_i$的估计值令$K=2^{128}$
- 然后就可以根据构造的格,来用sage求解
from Crypto.Util.number import *
cs = [
11804527453299586684489593808016317337345238230165321056832279785591503368758306671170625597063579251464905729051049524014502008954170088604924368057540940, 4930922884306486570759661288602557428608315558804950537470100263019228888817481617065454705843164809506859574053884206133344549895853064735361336486560981, 5380263856446165449531647111260010594620416730932539097782399557603420658350407080366132490174060420530708293564252852668431923560882648691392446521188465, 10746696290782998433216934286282230556131938525513632178308443345441147075710552571129957873399395862207656161609046567289600084193860244770966610161184627, 2195032957511830992558961021566904850278796737316238566513837995297394215638259916944087623923636789312134734949452839561765171446217520081402769962517110]
m = 12813864523019740432913161815051292412705285817864701047922722497269479288096574264414061282833203433542813637861620032851255308640850882149603687035724753
M = matrix(QQ,6,6)
for i in range(4):
M[i,i] = m
M[-2,i] = cs[i]
M[-1,i] = cs[i+1]
k=2^254
M[-2,-2]=k/m
M[-1,-1]=k
L=M.LLL()
print(L[0])
print(L[1])
print(L[2])
print(L[3])
print(L[4])
print(L[5])
res=L[1][-2].numerator()/k
a=abs(res)
print(a)
# 不知道为什么下面代码Sage中运行不了,但单独拎出来是可以运行的
from hashlib import md5
def MD5(m):return md5(str(m).encode()).hexdigest()
seed=cs[0]*inverse(a,m)%m
flag = '0xGame{' + MD5(seed) + '}'
print(flag)
- 0xGame{2db84757dd4197f9b9441be25f35bfd5}
[Week 3] LLL-III
- 没搞懂LLL但找到了板子
- 几乎一样,改改数据和移位
from Crypto.Cipher import AES
from Crypto.Util.number import *
from hashlib import md5
def MD5(m):return md5(str(m).encode()).hexdigest()
m = 181261975027495237253637490821967974838107429001673555664278471721008386281743
a = 80470362380817459255864867107210711412685230469402969278321951982944620399953
b = 108319759370236783814626433000766721111334570586873607708322790512240104190351
c = [2466192191260213775762623965067957944241015, 1889892785439654571742121335995798632991977, 1996504406563642240453971359031130059982231, 1368301121255830077201589128570528735229741, 3999315855035985269059282518365581428161659, 3490328920889554119780944952082309497051942, 2702734706305439681672702336041879391921064, 2326204581109089646336478471073693577206507, 3428994964289708222751294105726231092393919, 1323508022833004639996954642684521266184999, 2208533770063829989401955757064784165178629, 1477750588164311737782430929424416735436445, 973459098712495505430270020597437829126313, 1849038140302190287389664531813595944725351, 1172797063262026799163573955315738964605214, 1754102136634863587048191504998276360927339, 113488301052880487370840486361933702579704, 2862768938858887304461616362462448055940670, 3625957906056311712594439963134739423933712, 3922085695888226389856345959634471608310638]
h = [0] + c
length = len(h)
for i in range(length):
h[i] <<= 115
A = [1]
B = [0]
for i in range(1, len(h)-1):
A.append(a*A[i-1] % m)
B.append((a*B[i-1]+a*h[i]+b-h[i+1]) % m)
A = A[1:]
B = B[1:]
Ge = Matrix(ZZ,length,length)
for i in range(len(A)):
Ge[i,i] = m
Ge[-2,i] = A[i]
Ge[-1,i] = B[i]
K = 2**115
Ge[-2,-2] = 1
Ge[-1,-1] = K
for line in Ge.LLL():
if abs(line[-1]) == K:
L1 = line[-2]
seed1 = h[1] + L1
seed = (seed1 - b) * inverse(a,m) % m
print(f"seed = {seed}")
print('0xGame{' + MD5(seed) + '}')
- 0xGame{459049e068d93f6d70f1ea0da705264a}