2025 NepCTF nepsign

on 2025-08-01

这题比较简单,很多人都出了。

题目

#!/usr/bin/python3
# 导入必要的库
from gmssl import sm3  # SM3哈希算法库
from random import SystemRandom  # 系统随机数生成器
from ast import literal_eval  # 安全的字符串字面值求值
import os

# 从环境变量获取flag
# flag = os.environ["FLAG"]
flag = "123"


def SM3(data):
    """
    SM3哈希函数封装
    将输入数据转换为字节列表并计算SM3哈希值
    """
    d = [i for i in data]
    h = sm3.sm3_hash(d)
    return h


def SM3_n(data, n=1, bits=256):
    """
    执行n次SM3哈希运算
    Args:
        data: 输入数据
        n: 哈希轮数,默认为1
        bits: 输出位数,默认为256位
    Returns:
        截取指定位数的十六进制哈希值
    """
    for _ in range(n):
        data = bytes.fromhex(SM3(data))
    return data.hex()[: bits // 4]


class Nepsign:
    """
    Nepsign数字签名算法实现类
    基于SM3哈希函数的自定义签名方案
    """

    def __init__(self):
        self.n = 256  # 密钥长度参数
        self.hex_symbols = "0123456789abcdef"  # 十六进制字符集
        self.keygen()  # 初始化时生成密钥对

    def keygen(self):
        """
        生成公私钥对
        私钥:48个32字节的随机数
        公钥:对私钥进行255轮SM3哈希运算的结果
        """
        rng = SystemRandom()
        # 生成48个32字节的私钥
        self.sk = [rng.randbytes(32) for _ in range(48)]
        # 通过255轮哈希生成对应的公钥
        self.pk = [SM3_n(self.sk[_], 255, self.n) for _ in range(48)]
        return self.sk, self.pk

    def sign(self, msg, sk=None):
        """
        对消息进行数字签名
        Args:
            msg: 待签名的消息(字节形式)
            sk: 私钥(可选,默认使用实例的私钥)
        Returns:
            签名结果列表(48个元素)
        """
        sk = sk if sk else self.sk
        # 计算消息的SM3哈希值
        m = SM3(msg)
        # 将哈希值转换为256位二进制字符串
        m_bin = bin(int(m, 16))[2:].zfill(256)
        # 将二进制字符串按8位分组,转换为整数数组(32个字节)
        a = [int(m_bin[8 * i : 8 * i + 8], 2) for i in range(self.n // 8)]

        step = [0] * 48  # 存储哈希轮数
        qq = [0] * 48  # 存储签名结果

        # 对前32个私钥进行处理(基于消息字节值)
        for i in range(32):
            step[i] = a[i]  # 使用消息哈希的第i个字节作为哈希轮数
            qq[i] = SM3_n(sk[i], step[i])  # 对私钥进行step[i]轮哈希
            # if step[i] == 0, qq[i] = sk[i].hex()

        # 对后16个私钥进行处理(基于哈希值中十六进制字符的位置统计)
        sum = [0] * 16
        for i in range(16):
            sum[i] = 0
            # 统计哈希值前64位中第i个十六进制字符出现的位置总和
            for j in range(1, 65):
                if m[j - 1] == self.hex_symbols[i]:
                    sum[i] += j
            step[i + 32] = sum[i] % 255  # 对255取模作为哈希轮数
            qq[i + 32] = SM3_n(sk[i + 32], step[i + 32])
            # if step[i + 32] == 0, qq[i + 32] = sk[i + 32].hex()

        return [i for i in qq]

    def verify(self, msg, qq, pk=None):
        """
        验证数字签名
        Args:
            msg: 原始消息(字节形式)
            qq: 签名值列表
            pk: 公钥(可选,默认使用实例的公钥)
        Returns:
            验证结果(True/False)
        """
        # 将签名值从十六进制字符串转换为字节
        qq = [bytes.fromhex(i) for i in qq]
        pk = pk if pk else self.pk

        # 计算消息的SM3哈希值
        m = SM3(msg)
        # 将哈希值转换为256位二进制字符串
        m_bin = bin(int(m, 16))[2:].zfill(256)
        # 将二进制字符串按8位分组,转换为整数数组(32个字节)
        a = [int(m_bin[8 * i : 8 * i + 8], 2) for i in range(self.n // 8)]

        step = [0] * 48  # 存储哈希轮数
        pk_ = [0] * 48  # 存储重构的公钥

        # 验证前32个签名(基于消息字节值)
        for i in range(32):
            step[i] = a[i]  # 使用消息的第i个字节作为哈希轮数
            # 对签名进行(255-step[i])轮哈希,应该得到公钥
            pk_[i] = SM3_n(qq[i], 255 - step[i])

        # 验证后16个签名(基于哈希值中十六进制字符的位置统计)
        sum = [0] * 16
        for i in range(16):
            sum[i] = 0
            # 统计哈希值前64位中第i个十六进制字符出现的位置总和
            for j in range(1, 65):
                if m[j - 1] == self.hex_symbols[i]:
                    sum[i] += j
            step[i + 32] = sum[i] % 255  # 对255取模作为哈希轮数
            # 对签名进行(255-step[i+32])轮哈希,应该得到公钥
            pk_[i + 32] = SM3_n(qq[i + 32], 255 - step[i + 32])

        # 比较重构的公钥与原公钥是否一致
        return True if pk_ == pk else False


# 初始化签名系统
print("initializing...")
Sign = Nepsign()

# 主程序循环
while 1:
    match int(input("> ")):
        case 1:
            # 选项1:对用户输入的消息进行签名
            msg = bytes.fromhex(input("msg: "))
            # 禁止对目标消息进行签名(防止直接获取所需签名)
            if msg != b"happy for NepCTF 2025":
                print(Sign.sign(msg))
            else:
                print("You can't do that")
        case 2:
            # 选项2:验证签名,如果验证成功则输出flag
            qq = literal_eval(input("give me a qq: "))
            # 验证目标消息的签名
            if Sign.verify(b"happy for NepCTF 2025", qq):
                print(flag)

签名算法分析

这个Nepsign算法并不是Lamport签名,而是一个自定义的基于哈希链的签名方案。

算法原理:

  • 私钥:48个32字节的随机数
  • 公钥:每个私钥经过255轮SM3哈希的结果
  • 签名过程:根据消息哈希值确定每个私钥需要进行的哈希轮数,然后输出对应的哈希结果

具体来说:

  • 前32个私钥:哈希轮数 = 消息SM3哈希值对应字节的值 (0-255)
  • 后16个私钥:哈希轮数 = 消息哈希值中对应十六进制字符位置总和 % 255

验证时通过继续哈希到255轮来恢复公钥进行验证。

漏洞点

关键漏洞在于私钥泄露:当哈希轮数为0时,签名直接返回私钥本身。

  • 前32位:当消息哈希的某个字节值为0时,对应位置的签名就是私钥
  • 后16位:当某个十六进制字符的位置总和为255的倍数时,对应位置的签名就是私钥

通过构造特定消息使得哈希轮数为0,可以直接获取私钥,然后用这些私钥伪造任意消息的签名。

攻击思路

攻击分为两个阶段:

阶段1:寻找泄露私钥的消息

目标是找到48个消息,使得签名时对应的哈希轮数为0:

  1. 前32个私钥:寻找消息使得其SM3哈希值的第i个字节为0
  2. 后16个私钥:寻找消息使得其SM3哈希值中第i个十六进制字符的位置总和为255的倍数

阶段2:泄露私钥并伪造签名

  1. 用找到的消息获取对应位置的私钥
  2. 计算目标消息"happy for NepCTF 2025"的签名参数
  3. 用泄露的私钥计算对应的签名值
  4. 提交伪造的签名获取flag

攻击脚本

由于需要暴力搜索大量消息来找到合适的哈希值,Python版本速度较慢,因此使用Rust并行搜索。

use rayon::prelude::*;
use sm3::{Digest, Sm3};
use std::sync::{Arc, Mutex};

fn sm3_hash(data: &[u8]) -> String {
    let mut hasher = Sm3::new();
    hasher.update(data);
    let result = hasher.finalize();
    hex::encode(result)
}

fn analyze_message(i: u32) -> Vec<(usize, u32)> {
    // 将整数转换为字节(按Python的bytes(i)逻辑)
    let msg_bytes = if i <= 255 {
        vec![i as u8]
    } else {
        // 对于大于255的数,转换为大端字节序
        let mut bytes = Vec::new();
        let mut val = i;
        while val > 0 {
            bytes.insert(0, (val & 0xff) as u8);
            val >>= 8;
        }
        bytes
    };

    // 计算SM3哈希
    let hash_str = sm3_hash(&msg_bytes);

    // 将完整的哈希转换为256位二进制
    // 需要处理完整的64字符哈希字符串
    let mut m_bin = String::new();
    for chunk in hash_str.chars().collect::<Vec<char>>().chunks(2) {
        let hex_str: String = chunk.iter().collect();
        if let Ok(byte_val) = u8::from_str_radix(&hex_str, 16) {
            m_bin.push_str(&format!("{:08b}", byte_val));
        }
    }

    let mut zero_positions = Vec::new();

    // 检查前32个8位组是否为0
    for key in 0..32 {
        let start = key * 8;
        let end = start + 8;
        if start < m_bin.len() && end <= m_bin.len() {
            if let Ok(val) = u8::from_str_radix(&m_bin[start..end], 2) {
                if val == 0 {
                    zero_positions.push((key, i));
                }
            }
        }
    }

    zero_positions
}

fn find_leak_positions_parallel(max_range: u32, num_threads: usize) -> Vec<Option<u32>> {
    println!(
        "开始多线程搜索,范围: 0-{}, 线程数: {}",
        max_range, num_threads
    );

    let leak_pos = Arc::new(Mutex::new(vec![None; 32]));
    let found_count = Arc::new(Mutex::new(0));

    // 使用并行迭代器处理范围
    (0..max_range)
        .into_par_iter()
        .chunks(1000)
        .for_each(|chunk| {
            for i in chunk {
                // 检查是否已经找到所有位置
                {
                    let count = found_count.lock().unwrap();
                    if *count >= 32 {
                        break;
                    }
                }

                let zero_positions = analyze_message(i);

                if !zero_positions.is_empty() {
                    let mut leak_pos_guard = leak_pos.lock().unwrap();
                    let mut count_guard = found_count.lock().unwrap();

                    for (key, msg_id) in zero_positions {
                        if leak_pos_guard[key].is_none() {
                            leak_pos_guard[key] = Some(msg_id);
                            *count_guard += 1;
                            println!(
                                "发现位置 {} 的泄露消息: {} (十六进制: {:02x})",
                                key, msg_id, msg_id
                            );
                        }
                    }

                    if *count_guard >= 32 {
                        println!("已找到所有32个位置的泄露消息!");
                        break;
                    }
                }

                // 定期报告进度
                if i % 10000 == 0 {
                    let count = found_count.lock().unwrap();
                    println!("进度: 已检查 {} 个消息, 找到 {}/32 个位置", i, *count);
                }
            }
        });

    let final_result = leak_pos.lock().unwrap().clone();
    final_result
}

fn main() {
    println!("Nepsign 私钥泄露搜索工具 (Rust多线程版本)");
    println!("========================================");

    // 设置搜索范围和线程数
    let max_range = 100000; // 搜索前10万个消息
    let num_threads = num_cpus::get().max(4);

    println!("使用 {} 个线程", num_threads);
    rayon::ThreadPoolBuilder::new()
        .num_threads(num_threads)
        .build_global()
        .unwrap();

    let start_time = std::time::Instant::now();

    let leak_positions = find_leak_positions_parallel(max_range, num_threads);

    let elapsed = start_time.elapsed();

    println!("\n搜索完成!用时: {:?}", elapsed);
    println!("泄露位置结果:");
    println!("=============");

    let mut found_count = 0;
    for (i, pos) in leak_positions.iter().enumerate() {
        match pos {
            Some(msg_id) => {
                println!("位置 {:2}: 消息 {} (0x{:02x})", i, msg_id, msg_id);
                found_count += 1;
            }
            None => {
                println!("位置 {:2}: 未找到", i);
            }
        }
    }

    println!("\n统计信息:");
    println!("找到泄露位置: {}/32", found_count);
    println!(
        "搜索效率: {:.2} 消息/秒",
        max_range as f64 / elapsed.as_secs_f64()
    );

    // 输出Python脚本可用的格式
    println!("\nPython字典格式:");
    println!("leak_messages = {{");
    for (i, pos) in leak_positions.iter().enumerate() {
        if let Some(msg_id) = pos {
            // 确保输出的十六进制字符串是偶数长度
            let hex_str = format!("{:x}", msg_id);
            let padded_hex = if hex_str.len() % 2 == 1 {
                format!("0{}", hex_str)
            } else {
                hex_str
            };
            println!("    {}: \"{}\",  # 泄露sk[{}]", i, padded_hex, i);
        }
    }
    println!("}}");

    // 输出利用脚本
    println!("\n可直接用于exploit的消息列表:");
    let mut exploit_messages = Vec::new();
    for (i, pos) in leak_positions.iter().enumerate() {
        if let Some(msg_id) = pos {
            // 确保输出的十六进制字符串是偶数长度
            let hex_str = format!("{:x}", msg_id);
            let padded_hex = if hex_str.len() % 2 == 1 {
                format!("0{}", hex_str)
            } else {
                hex_str
            };
            exploit_messages.push(padded_hex);
        }
    }
    println!("messages = {:?}", exploit_messages);
}
use rayon::prelude::*;
use sm3::{Digest, Sm3};
use std::sync::{Arc, Mutex};

fn sm3_hash(data: &[u8]) -> String {
    let mut hasher = Sm3::new();
    hasher.update(data);
    let result = hasher.finalize();
    hex::encode(result)
}

fn analyze_hex_positions(i: u32) -> Vec<(usize, u32)> {
    // 将整数转换为字节(按Python的bytes(i)逻辑)
    let msg_bytes = if i <= 255 {
        vec![i as u8]
    } else {
        // 对于大于255的数,转换为大端字节序
        let mut bytes = Vec::new();
        let mut val = i;
        while val > 0 {
            bytes.insert(0, (val & 0xff) as u8);
            val >>= 8;
        }
        bytes
    };

    // 计算SM3哈希
    let hash_str = sm3_hash(&msg_bytes);

    // 十六进制字符集,对应server.py中的self.hex_symbols
    let hex_symbols = "0123456789abcdef";

    let mut zero_positions = Vec::new();

    // 对每个十六进制字符(0-15),检查位置总和是否为255的倍数
    for hex_idx in 0..16 {
        let target_char = hex_symbols.chars().nth(hex_idx).unwrap();
        let mut sum = 0;

        // 统计哈希值前64位中该字符出现的位置总和
        // j从1开始(对应Python中的range(1, 65))
        for j in 1..=64 {
            if let Some(hash_char) = hash_str.chars().nth(j - 1) {
                if hash_char == target_char {
                    sum += j;
                }
            }
        }

        // 检查sum % 255 == 0(对应step[hex_idx + 32] = 0)
        if sum % 255 == 0 {
            zero_positions.push((hex_idx + 32, i)); // 位置32-47对应后16个私钥
            println!(
                "消息 {} (0x{:x}): 字符 '{}' 位置和={}, 泄露sk[{}]",
                i,
                i,
                target_char,
                sum,
                hex_idx + 32
            );
        }
    }

    zero_positions
}

fn find_hex_leak_positions_parallel(max_range: u32, num_threads: usize) -> Vec<Option<u32>> {
    println!(
        "开始搜索后16个私钥的泄露消息,范围: 0-{}, 线程数: {}",
        max_range, num_threads
    );

    let leak_pos = Arc::new(Mutex::new(vec![None; 48])); // 0-47,但我们只关心32-47
    let found_count = Arc::new(Mutex::new(0));

    // 使用并行迭代器处理范围
    (0..max_range)
        .into_par_iter()
        .chunks(1000)
        .for_each(|chunk| {
            for i in chunk {
                // 检查是否已经找到所有后16个位置
                {
                    let count = found_count.lock().unwrap();
                    if *count >= 16 {
                        break;
                    }
                }

                let zero_positions = analyze_hex_positions(i);

                if !zero_positions.is_empty() {
                    let mut leak_pos_guard = leak_pos.lock().unwrap();
                    let mut count_guard = found_count.lock().unwrap();

                    for (key, msg_id) in zero_positions {
                        if key >= 32 && key < 48 && leak_pos_guard[key].is_none() {
                            leak_pos_guard[key] = Some(msg_id);
                            *count_guard += 1;
                            println!(
                                "发现位置 {} 的泄露消息: {} (十六进制: {:x})",
                                key, msg_id, msg_id
                            );
                        }
                    }

                    if *count_guard >= 16 {
                        println!("已找到所有16个后位置的泄露消息!");
                        break;
                    }
                }

                // 定期报告进度
                if i % 10000 == 0 {
                    let count = found_count.lock().unwrap();
                    println!("进度: 已检查 {} 个消息, 找到 {}/16 个后位置", i, *count);
                }
            }
        });

    let final_result = leak_pos.lock().unwrap().clone();
    final_result
}

fn main() {
    println!("Nepsign 后16个私钥泄露搜索工具");
    println!("===================================");

    // 设置搜索范围和线程数
    let max_range = 500000; // 搜索前50万个消息
    let num_threads = num_cpus::get().max(4);

    println!("使用 {} 个线程", num_threads);
    rayon::ThreadPoolBuilder::new()
        .num_threads(num_threads)
        .build_global()
        .unwrap();

    let start_time = std::time::Instant::now();

    let leak_positions = find_hex_leak_positions_parallel(max_range, num_threads);

    let elapsed = start_time.elapsed();

    println!("\n搜索完成!用时: {:?}", elapsed);
    println!("后16个位置的泄露结果:");
    println!("========================");

    let mut found_count = 0;
    for i in 32..48 {
        match leak_positions[i] {
            Some(msg_id) => {
                println!("位置 {:2}: 消息 {} (0x{:x})", i, msg_id, msg_id);
                found_count += 1;
            }
            None => {
                println!("位置 {:2}: 未找到", i);
            }
        }
    }

    println!("\n统计信息:");
    println!("找到后16个泄露位置: {}/16", found_count);
    println!(
        "搜索效率: {:.2} 消息/秒",
        max_range as f64 / elapsed.as_secs_f64()
    );

    // 输出Python脚本可用的格式(仅后16个位置)
    println!("\nPython字典格式(后16个私钥):");
    println!("hex_leak_messages = {{");
    for i in 32..48 {
        if let Some(msg_id) = leak_positions[i] {
            // 确保输出的十六进制字符串是偶数长度
            let hex_str = format!("{:x}", msg_id);
            let padded_hex = if hex_str.len() % 2 == 1 {
                format!("0{}", hex_str)
            } else {
                hex_str
            };
            println!("    {}: \"{}\",  # 泄露sk[{}]", i, padded_hex, i);
        }
    }
    println!("}}");
}

python的攻击脚本

from pwn import *
from gmssl import sm3

context.log_level = "debug"
# context.log_level = "info"

LOCAL = False
if LOCAL:
    p = process(["python3", "./server.py"])
else:
    p = remote("nepctf32-ncuk-bxts-ld1y-ivwodb1uj620.nepctf.com", 443, ssl=True)


def get_signature(msg_hex):
    """获取消息的签名"""
    p.sendline(b"1")
    p.recvuntil(b"msg: ")
    p.sendline(msg_hex.encode())
    # 接收完整的返回数据,直到下一个提示符
    result = p.recvuntil(b"> ").decode()
    # 提取签名列表部分(去除提示符)
    signature_line = result.split("\n")[0].strip()
    return eval(signature_line)


# 等待服务器初始化
p.recvuntil(b"initializing...")
# p.interactive()

# 使用Rust工具找到的泄露消息(更新版本,确保偶数长度)
leak_messages = {
    0: "9088",
    1: "61ce",
    2: "c3e6",
    3: "465b",
    4: "2f43",
    5: "4655",
    6: "2ef0",
    7: "2f28",
    8: "61be",
    9: "c37f",
    10: "15",
    11: "c36b",
    12: "2f4e",
    13: "c36e",
    14: "2f18",
    15: "4667",
    16: "6229",
    17: "61cf",
    18: "c35f",
    19: "c35e",
    20: "61ba",
    21: "4661",
    22: "c363",
    23: "c374",
    24: "c419",
    25: "15",
    26: "61ed",
    27: "2f40",
    28: "c355",
    29: "c363",
    30: "c378",
    31: "2f4f",
}

# 后16个私钥的泄露消息
hex_leak_messages = {
    32: "f235",
    33: "01e856",
    34: "01e85c",
    35: "f230",
    36: "01e84e",
    37: "00",
    38: "03d093",
    39: "01e85c",
    40: "01",
    41: "01e858",
    42: "016b49",
    43: "08",
    44: "0f",
    45: "f23a",
    46: "f236",
    47: "7927",
}

leaked_sk = {}

# 泄露前32个私钥(基于消息字节值)
for i, msg_hex in leak_messages.items():
    try:
        signature = get_signature(msg_hex)
        # 当step[i] = 0时,signature[i] = sk[i].hex()
        leaked_sk[i] = signature[i]
    except Exception as e:
        print(f"泄露sk[{i}]失败: {e}")

# 泄露后16个私钥(基于十六进制字符位置统计)
for i, msg_hex in hex_leak_messages.items():
    try:
        signature = get_signature(msg_hex)
        # 当step[i] = 0时,signature[i] = sk[i].hex()
        leaked_sk[i] = signature[i]
    except Exception as e:
        print(f"泄露sk[{i}]失败: {e}")

print(f"\n成功泄露了 {len(leaked_sk)}/48 个私钥!")
print(leaked_sk)

# 如果成功泄露了足够的私钥,尝试伪造目标消息的签名
if len(leaked_sk) >= 48:
    print("\n开始伪造目标消息签名...")

    # 目标消息: "happy for NepCTF 2025"
    target_msg = b"happy for NepCTF 2025"

    # 计算目标消息的SM3哈希
    def SM3(data):
        d = [i for i in data]
        h = sm3.sm3_hash(d)
        return h

    def SM3_n(data, n=1, bits=256):
        for _ in range(n):
            data = bytes.fromhex(SM3(data))
        return data.hex()[: bits // 4]

    # 重构签名算法
    m = SM3(target_msg)
    m_bin = bin(int(m, 16))[2:].zfill(256)
    a = [int(m_bin[8 * i : 8 * i + 8], 2) for i in range(32)]

    hex_symbols = "0123456789abcdef"

    forged_signature = []

    # 计算前32个签名
    for i in range(32):
        step_i = a[i]
        sk_i_hex = leaked_sk[i]
        sk_i = bytes.fromhex(sk_i_hex)
        signature_i = SM3_n(sk_i, step_i)
        forged_signature.append(signature_i)

    # 计算后16个签名
    for i in range(16):
        sum_i = 0
        # 统计哈希值前64位中第i个十六进制字符出现的位置总和
        for j in range(1, 65):
            if m[j - 1] == hex_symbols[i]:
                sum_i += j
        step_i = sum_i % 255
        sk_i_hex = leaked_sk[i + 32]
        sk_i = bytes.fromhex(sk_i_hex)
        signature_i = SM3_n(sk_i, step_i)
        forged_signature.append(signature_i)

    print("伪造的签名:", forged_signature)

    # 发送伪造的签名获取flag
    print("\n发送伪造签名获取flag...")
    p.sendline(b"2")
    p.recvuntil(b"give me a qq: ")
    p.sendline(str(forged_signature).encode())

    # 接收结果
    result = p.recvline().decode().strip()
    print("服务器响应:", result)

else:
    print(f"私钥泄露不完整,只获得了 {len(leaked_sk)}/48 个")
    p.interactive()
粤ICP备2025368514号-1