CryptoCUP2026 wp
又是被队友带飞的一天,也太强
2026熵密杯解题思路
赛题整理:shangmibei.zip
本身密码学的不是很久,主要还是完全靠队友完全带飞,这里就放一下相关wp,以后可能不太会继续搞密码了,还是专心web,本次成绩还算不错,最终第三名应该是一等奖
初始谜题1
验签部分代码逻辑如下
P2 = (r + s) * P = r*P + P1 * sk = r * sk * G + s * sk * G
P1 = s * G
最终x = P1 + P2 //x坐标
展开P1 + P2 = s * G + r * sk * G + s * sk * G
最终判定 r + s == 0的时候消去sk未知数
x = (s*G).x
进一步构造e = r - x
代码如下
r = n - 2
s = 2
# 2*G
x = r = n - 2 - 39264624226210491828350299246801547995169179683885222662092227069188840275282
hash = hex(r % n)
s = int.to_bytes(2, 32).hex()
r = int.to_bytes(nn - 2, 32).hex()
data = r + s
print(data)
初始谜题2
题目为CTR算法,每次使用序号来做IV,序号发现为递增关系
所以根据CTR模式得到第二组数据的密钥流[:16]和第一组数据的[16:32]一致,依次推导出第三组数据密钥流
数据排列未知,可以两组都试一下,然后使用比较合理的
from pwn import xor
c1 = bytes.fromhex("7d2a3dc99109ff9934c1237cc136786612ab51d25cb045550fac8c377c98357de1b0d616039e6f8218506a6dd3d9605df19253c726eceb3a410f72ba3af20e")
c2 = bytes.fromhex("1cae58cb5cb9475504a598357893657deeb3d6170f9e68830551226887cd6754fdc24cd32bebb8395f0a27bd67e9542ddf1223c08abd32e8da7f28d0a8a6fb")
# "63"6209310025002437845
m1 = b"6205310113926086879 382621 019f1942-4000-7d5e-a85d-997c332b3e6c"
m2 = b"6205310698825261672 807106 019f1942-4000-7748-992c-c342414cb0fe"
k2 = xor(m2,c2)
k1 = xor(m1,c1)
m3 = bytes.fromhex("eeb6d40e049f6f820752396884c03252fcc24bd22eeabe3a450a3cb96efd0b25d4453c8e8db930ecc67c2e87aceda6c2f166a0930963b3c5e5630ff45a9612")
print(xor(m3,k2[16:]))
print(xor(m3,k1[32:]))
初始谜题3
根据题目的数据很显然,发现每次加密的数据u都一样,可以判定r1,r2重用,再加密两组一样的数据发现v一样,得到e重用,很显然m1 - m2 = v1 - v2
# rng = random.Random(1)
# h = sample_random_poly(PARAM_N, rng)
# x = sample_fixed_weight(PARAM_N, PARAM_W, rng)
# y = sample_fixed_weight(PARAM_N, PARAM_W, rng)
# s = poly_add(x, poly_mul_mod(h, y))
# e = sample_fixed_weight(PARAM_N, PARAM_W, rng)
# r1 = sample_fixed_weight(PARAM_N, PARAM_W, rng)
# r2 = sample_fixed_weight(PARAM_N, PARAM_W, rng)
# u = poly_add(r1, poly_mul_mod(h, r2))
# # u = r1 + h*r2
# # v = m + s*r2 + e
# v1 = poly_add(poly_add(m1, poly_mul_mod(s, r2)), e)
# v2 = poly_add(poly_add(m2, poly_mul_mod(s, r2)), e)
# print(poly_add(poly_add(v1, v2),m1) == m2)
根据如下的测试代码得到消息(v1,v2,v3)从页面取出
m1 = bytes.fromhex("11111111111111111111111111111111")
v1 = ""
m2 = bytes.fromhex("c69d784d3b68532d24a38be81d373511")
v2 = ""
m1 = concat_encode(m1)
m2 = concat_encode(m2)
v3 = ""
v1 = poly_from_bytes(bytes.fromhex(v1))
v2 = poly_from_bytes(bytes.fromhex(v2))
v3 = poly_from_bytes(bytes.fromhex(v3))
m3 = poly_add(poly_add(v1, v3),m1)
print(concat_decode(m3))
print(hashlib.sha512(concat_decode(m3)).digest()[:32].hex())
flag1
根据go代码,3个登录方式只能通过Bob登录:(口令登录无法知道盐值,证书登录验证了证书链无法进一步伪造公私钥)
很显然Bob登录逻辑为,后台储存会话密钥密文(内部索引为1);登录时需要提交挑战值的ECB结果,我们可以调用SDF接口,先生成会话密钥并使用索引1导出,再替换后台会话密钥密文,再调用SDF接口加密(需要手动填充)
# 转换挑战值数据并填充
hex = "c69d784d3b68532d24a38be81d373d92c5efc1bd65b22e092e786c5329e19e60"
for i in range(0,len(hex),2):
print("\\x" + hex[i:i+2],end='')
print('\\x10'*16)
SDF代码如下
int main(int argc, char *argv[]) {
int ret = 0;
ECCCipher *pucKey = NULL;
void **hImportSessionKey = NULL;
unsigned char hexc[1024] = "\xc6\x9d\x78\x4d\x3b\x68\x53\x2d\x24\xa3\x8b\xe8\x1d\x37\x3d\x92\xc5\xef\xc1\xbd\x65\xb2\x2e\x09\x2e\x78\x6c\x53\x29\xe1\x9e\x60\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10";
unsigned char cc[1024] = {0};
const char chall[] = "08db49afde9c9e3645a8d6d988e3f87c3aadc64e4e31a441b9da4bbf5278c8d9";
// hex_string_to_char_array(chall, hexc);
int nKeyLen = 16;
int uiKeyBufferLen = sizeof(ECCCipher) + nKeyLen;
pucKey = (ECCCipher*)calloc(uiKeyBufferLen, 2);
if(pucKey == NULL) {
printf("calloc error\n");
return 0;
}
ret = init();
if (ret) {
printf("init error, ret=%08x\n", ret);
return ret;
}
// 调用密码机进行具体的密码运算
ret = SDF_GenerateKeyWithIPK_ECC(sessionHandle, 1, 16 * 8, pucKey, &keyHandle);
if (ret) {
printf("SDF_GenerateKeyWithIPK_ECC error, ret=%08x\n", ret);
return ret;
}
for (unsigned int i = 0; i < 32; i++) {
printf("%02X", pucKey->x[i + 32]);
}
for (unsigned int i = 0; i < 32; i++) {
printf("%02X", pucKey->y[i + 32]);
}
for (unsigned int i = 0; i < 32; i++) {
printf("%02X", pucKey->M[i]);
}
for (unsigned int i = 0; i < 16; i++) {
printf("%02X", pucKey->C[i]);
}
printf("\n");
unsigned int *ciphertext_len = 48;
printf("\n");
ret = SDF_Encrypt(sessionHandle, keyHandle, SGD_SM4_ECB, NULL, hexc, 48, &cc, &ciphertext_len);
if (ret) {
printf("SDF_Encrypt error, ret=%08x\n", ret);
return ret;
}
for (unsigned int i = 0; i < 48; i++) {
printf("%02X", cc[i]);
}
printf("\n");
ret = destroy();
if (ret) {
printf("destroy error, ret=%08x\n", ret);
}
return 0;
}
flag2
题目很显然需要手动生成一组CA和用户证书
这部分使用工具做起来很简单我们的生成结果如下
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDFcqNVnPqxBidp
voCo4hYmgR8NR2FHw3p6hZ0fXayIQ3TAD7zlbsvoCP/kQ4hsyqzMEHDTzGDRUtLK
mSHdnRWeL6TNnmdu7VRy9Lg2T6gr7Uik1VM8fPxMSgRYkVUy01QIobhNrtDVYDu6
Wi2QH26e1UrYgNhFuWRo7as1iiQLuPHSQTNJ0CUYn938UAoEVndQo95YGsEl3tV+
1CIu1M4RNE599y5pHAffLhJwPvZ547hrAJBLetRr6bfML8lY1kWaSAMK/9CGgFuR
1RB10zgfLb3Wp/D/lrP+An1+EXvsd/KqIf9MAiLueOMa2l6vBTfl3NF8h4D+XN3a
Nv26S0uXAgMBAAECggEAWtNwJRrQCwycprz2LDnMHKw6333dVwip16CM9Xe+vPPi
ydKzfwrni3ZQV+1k3T6iHEDX1eXnB4NE6eNlO69d2WVrNxbN164esb622mzsHEDL
sN9cqa+I04Mc2q0H9xhVSJ8zy7BF6lGZSBDhcrGyziephUxHsFMmyEnD/x3rlOQP
BH7MxEitxoiFBfhUy/Mp1HyrvQEHfEgKzRFAfVmwowNlkTt+ztcJu1vVkfrcxAUQ
DHUn/q8mOe0m/oCm6F327gvj5Fm7ni0HOGUnyTcdBHPLQeO/LE5RANqYizoXmftA
opv6JXxvUDpTtRTsw6tFmkJDPU9Me4KgL5qRk8H9uQKBgQD/BR3Erx03m1Ve18yz
IqHfNy1B/H0/tNOS82uovLKiRBduSOIKz8EJ8u8Ai/ylUxeuazZvY6OFF2Pd6BXd
86MTWSGaxeCdYPA2Zhlo5UxJr9Cxu33ZnD7DmWyk0bOyW7KKvUVoQd3vR1m2i9d/
QtLU8zQ1zyFVMDZuNL0WgzE4ywKBgQDGNOIjD6ssm0otf8DsYEZhxbsNWfygMiqy
Eu1UyC24TsxRgdAxxNWZWhuEAUez8f0LUciuXLPze6hydM2gWpHGTJKrN3qWhrHE
4Swo7KERIsllSfQrGvs7dVNwDoilhHwd/S11cUKNimOKnjS+9lTVyz/q/DTLIxm1
qKDyVAC65QKBgQDKhvaoL+YFNlBrPPitPxDM49YGdI3HnUT1jGnwnMNFCMyO4kTa
DzogO5VBmurvJPlGpJCbj8baOMQwBFPxa78T65I7h3Clq8m5egYt/wRB9OXJcLw2
qNZTorHch0mtmRNHCZ9od/7YO5Ls3UwsHhav1eGd5jxzmhFwHmQYz+G0VwKBgEDJ
nHl1b7Be+joxHjClr5vZ9DCDD46Vl1sp0KX74OXz69lgo1sBSgicaY4WlQd+vAXt
L7m+15to+kCfaW9W63t0ICUTN2DomkcOi069OlDNnCMdr1A/u4tN6H4iYgwoaCGt
ViSjZuu5Jpxy5i7dehGAJF59HgygZpD4k9NiHfL5AoGBAPmvgVEmjd1SUl+OuCEB
8t8i/yxxiIC3/XE/YD/t2vsKfquJZ4OzTifvg5oZZtbJHMbpEbSFpymDYPp3W1GF
5ekLR68/Kux8YveW1LxSZ/cX6FH8go3upaqIwcKhhhbjRU+iUdZv5/8URvZsPZRX
moafEpOB1voqtNW60yFhbofR
-----END PRIVATE KEY-----
CA
-----BEGIN CERTIFICATE-----
MIIDWDCCAkCgAwIBAgIUdAB4HCNw+oa0Frp+fZfs+g+RLfMwDQYJKoZIhvcNAQEL
BQAwMjELMAkGA1UEBhMCQ04xDDAKBgNVBAoMA1NNQjEVMBMGA1UEAwwMVGVzdCBS
b290IENBMB4XDTI2MDcyNTA1NDQyN1oXDTM2MDcyMjA1NDQyN1owMjELMAkGA1UE
BhMCQ04xDDAKBgNVBAoMA1NNQjEVMBMGA1UEAwwMVGVzdCBSb290IENBMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7c5/gPsgXyKngLjbiKOOZzVhatC7
TzzrZjcjfUS0rWgbrQKx6mjvR7647BMvj12IPoL6LDaLNUDy6W/2svqTqqxBU8SP
CAvJwpUUAtmalj4acgJ4gZB7Jfo2VOdWAHUzz4t9BpmFaQ5dkAncnAQtCO5qLyly
8hyZJryZf60jgXohpOSPeQP+rTREOqgTE5n6JXCRb2getXO+VtqJdU2mcBRpoTX1
U76IsqgCaiPelv7kYldF3bgdoB9SNs2P7GefVI15ddnZHu806OhtRa5tYvCyILRk
RyzZCXQJ+3wYg3T6b9eTVQwxSkOp0XZD6rm9O+QPtwbeEg4GADX2rIDG0wIDAQAB
o2YwZDASBgNVHRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4E
FgQUtotcM/sE2953QROb0/YnkOHHtKAwHwYDVR0jBBgwFoAUtotcM/sE2953QROb
0/YnkOHHtKAwDQYJKoZIhvcNAQELBQADggEBAOFKr1RAbBvIDWFA/U0w0lO/7XPa
5p5kZm/DVRB+W4OdfOK89LgSPKtqHv5YaZXGOBgPWCKd9LrM8Ky6I1PbqW5NWAr2
tYutlVBoBh+Jfrf3wINXpdvVN9jgPla4Mv4Gtso1G0QavshErV3AUahZLuIeXT0U
bY3oGoFDfW5JdSScIo+9FGCrJM1L5+VTMxeLaBg3b6RsBVsKbrGkze71v5iFpvzp
o45diUp5HlD5YuQjKpQs+EIz+zTfYnIxO/KQ94QwfJGIa9+UUqaQcaBgPsVCHxNr
qIAj8ttqPez/IujGJLz2+7vNYPaQAuQvcCMvqJ/mKbXaKEsdxNPQvxGobg0=
-----END CERTIFICATE-----
用户
-----BEGIN CERTIFICATE-----
MIIDUDCCAjigAwIBAgIUGEat6WKZ2Bw0O1w5xfJZ56KuSlswDQYJKoZIhvcNAQEL
BQAwMjELMAkGA1UEBhMCQ04xDDAKBgNVBAoMA1NNQjEVMBMGA1UEAwwMVGVzdCBS
b290IENBMB4XDTI2MDcyNTA1NDYxNloXDTI4MTAyNzA1NDYxNlowMDELMAkGA1UE
BhMCQ04xDDAKBgNVBAoMA1NNQjETMBEGA1UEAwwKTG9nIENsaWVudDCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMVyo1Wc+rEGJ2m+gKjiFiaBHw1HYUfD
enqFnR9drIhDdMAPvOVuy+gI/+RDiGzKrMwQcNPMYNFS0sqZId2dFZ4vpM2eZ27t
VHL0uDZPqCvtSKTVUzx8/ExKBFiRVTLTVAihuE2u0NVgO7paLZAfbp7VStiA2EW5
ZGjtqzWKJAu48dJBM0nQJRif3fxQCgRWd1Cj3lgawSXe1X7UIi7UzhE0Tn33Lmkc
B98uEnA+9nnjuGsAkEt61Gvpt8wvyVjWRZpIAwr/0IaAW5HVEHXTOB8tvdan8P+W
s/4CfX4Re+x38qoh/0wCIu544xraXq8FN+Xc0XyHgP5c3do2/bpLS5cCAwEAAaNg
MF4wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0OBBYEFAMU4ocs
/R7Pe5FrmB7eqm9qLG2cMB8GA1UdIwQYMBaAFLaLXDP7BNved0ETm9P2J5Dhx7Sg
MA0GCSqGSIb3DQEBCwUAA4IBAQDM2HinDw1zUcoVjArKlu0YM/vyb9I4rMLftSoj
XD/2viBeu0KqD6jVcdCx3zWDyRmQUuuZ2x+WWh6e62xgVbZvcTR2cbt69Yqm4GEJ
bcTJqVNsOlQI5imNTvGH5DAnMsXXJHB4oBOVeZwtU53mXSjHuPp6ydADysQjA/cP
p0JjddczgZP02kOkat4xU9+7D4BVXP0Izh00Cq+MHW2kCaWHovBOYral9G4kmv74
hun9sAI0ZRFIGh4yil9iI43Rbvy7yF89pUjInxpAWC8PEPI8pMO619RnQj27OQF5
qp4PVAhd/9kgf7pUknkg8rNQCbe+5A+lmMJ7EzTE0Ipjv0Ji
-----END CERTIFICATE-----
flag3
发现IV后3个16进制会变换,猜测使用CTR,OFB等密钥流一样的结构,直接疯狂请求然后xor后进行decode(源代码肯定都是utf8有效字符)
代码如下
import requests
import json
def xor(a,b):
c = []
for i,j in zip(a,b):
c.append(i^j)
return bytes(c)
# 替换为实际的工程师站服务器 IP
IP = "192.168.10.17"
# 成功登录工程师站服务器后,可通过浏览器开发者工具查找到
TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODUwMzc1NjAsInVzZXJuYW1lIjoiQm9iIn0.u40uXa8iFBO8kXdv5QxtWnVCZ_QYZ7KSzIHJl4-1A3I"
st = bytes.fromhex(open("1.py","r").read())
if __name__ == "__main__":
url = f"http://{IP}:5000/api/encrypt"
payload = json.dumps({
"plaintext": "1" * 11513
})
headers = {
'Authorization': TOKEN,
'Content-Type': 'application/json'
}
ivs = []
for _ in range(10000):
response = requests.request("POST", url, headers=headers, data=payload)
ct = bytes.fromhex(json.loads(response.text)["ciphertext"])
assert len(ct) == 11513
keys = xor(ct, b"1" * 11513)
mt = xor(keys, st)
try:
print(mt.decode())
except:
continue
flag4
检查go代码发现时chacha20poly结构
目标伪造一组password,使得其poly1305结果为指定的hash值(通过前端随便调用得到该值)
password约束为8-16有效字符,则判断为1组 poly1305,根据poly1305方程可以判断只有一个未知数,肯定能够得到一个固定的解。
sagemath代码如下
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
from Crypto.Util.number import *
P = (Integer(1) << 130) - 5
# def main() -> None:
# 32-byte key for ChaCha20-Poly1305.
key = bytes.fromhex(
"d0eb801593baacc3001025e6ce26e0cb855d26766970e82b0f9e3712ecd9ea15"
)
# 12-byte nonce. This demo intentionally reuses nonce as requested.
nonce = bytes.fromhex("bd17a47f94bf6ebd8ec876c7")
def pad0(msg):
while len(msg) % 16 != 0:
msg += b'\x00'
return msg
def clamp_r(r_bytes: bytes) -> int:
if len(r_bytes) != 16:
raise ValueError("r must be 16 bytes")
r = Integer(int.from_bytes(r_bytes, "little"))
r &= 0x0FFFFFFC0FFFFFFC0FFFFFFC0FFFFFFF
return r
G = GF(P)
PR.<mm> = PolynomialRing(G)
MM = 1 * (Integer(1) << 128)
r0 = clamp_r(pkey[:16])
s0 = Integer(int.from_bytes(pkey[16:], "little"))
print(r0,s0)
tag = bytes.fromhex("1784cd1dce8bae117ce7dd5f19a50266")
tag = int.from_bytes(tag, "little")
pkey = generate_poly1305_key(key, nonce)
for i in range(8,16):
for j in range(0,6):
msg1 = len("1"*i).to_bytes(8, "little") + len("").to_bytes(8, "little")
n = Integer(int.from_bytes(msg1 + b"\x01", "little"))
f = r0^2 * mm + r0 * n + s0 - (tag + j * MM)
rs = f.roots()
if rs == []:
continue
for rss in rs:
print(long_to_bytes(int(rss[0])))
得到如下数据
b"\x01\x00\x008MST%Yq'<2'Yxu"
得到一组数据,最终去填充01和小端转换得到口令
uxY'2<'qY%TSM8