osthing's blog
GitHub RSS
4 min read

CryptoCUP2026 wp

又是被队友带飞的一天,也太强

ctf crypto writeup match

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