Misc

星辉骑士

把后缀.docx改为.zip后解压缩

word/media下找到flag.zip

伪加密修复后解压,垃圾邮件加密spammimic - decoded解密999.txt得到flag{0231265452-you-kn*w-spanmimic}

flag
H&NCTF{0231265452-you-kn*w-spanmimic}

谁动了黑线?

附件sheidongleheixian.csv的部分数据如下:

text
from_address,to_address,amount_sol,timestamp,tx_hash1P8KDHGQC0NM5MD9WEBAT,1IPDXFBEFC6863BQY66KQ,0.2,1680000662,FPAYmszHFmWAYano3HfRWc11HPSDI3AL3BBAPYOBJR1,170J437LF23601EIOTQXN,0.2,1680000616,FPAYmszHFVG7mJr2sqKitb1JTKE95594O0YYLA6DGFU,1WRB9XEAL5I4ZP2KCRD4G,0.2,1680003430,FPAYmt3ZCaGnD1EQFquhMF11W0LGK7HE2JSA1RHV585,1JV5TMA23YNYCFDQSJZ4Q,0.2,1680001147,FPAYmt1MVg8rvnH8Fx7XAJ

不难发现这是区块链交易数据,其中tx_hash还经过了base58编码,下面先对tx_hash列进行base58解码并将结果添加到一个新列decoded_tx_hash中,然后保存到文件sheidongleheixian_decoded.csv

python
import csvimport base58import os input_file_path = "sheidongleheixian.csv"file_name_without_ext, file_extension = os.path.splitext(input_file_path)output_file_path = f"{file_name_without_ext}_decoded{file_extension}" new_column_name = 'decoded_tx_hash' processed_rows = []header = []  with open(input_file_path, 'r', encoding='utf-8') as infile:    reader = csv.DictReader(infile)     header = reader.fieldnames     for row in reader:        tx_hash_val = row.get('tx_hash', '')        decoded_value = ''         decoded_bytes = base58.b58decode(tx_hash_val)        decoded_value = decoded_bytes.decode('utf-8')          row[new_column_name] = decoded_value        processed_rows.append(row) new_fieldnames = header + [new_column_name] with open(output_file_path, 'w', encoding='utf-8', newline='') as outfile:    writer = csv.DictWriter(outfile, fieldnames=new_fieldnames)    writer.writeheader()    writer.writerows(processed_rows)

得到的部分数据如下:

text
from_address,to_address,amount_sol,timestamp,tx_hash,decoded_tx_hash1P8KDHGQC0NM5MD9WEBAT,1IPDXFBEFC6863BQY66KQ,0.2,1680000662,FPAYmszHFmWAYano3HfRWc,tx000661YGG3QDKE11HPSDI3AL3BBAPYOBJR1,170J437LF23601EIOTQXN,0.2,1680000616,FPAYmszHFVG7mJr2sqKitb,tx000615QZN6T7KD1JTKE95594O0YYLA6DGFU,1WRB9XEAL5I4ZP2KCRD4G,0.2,1680003430,FPAYmt3ZCaGnD1EQFquhMF,tx003429JCPKOX0V

下面根据转出地址 (from_address) 和转入地址 (to_address) 来重建资金的流动路径:

python
import pandas as pdfrom collections import defaultdict def find_transaction_chains(csv_path):    """    从交易数据中还原出交易链。    """    try:        # 1. 加载数据        df = pd.read_csv(csv_path)        print(f"成功加载 {len(df)} 条交易记录。")    except FileNotFoundError:        print(f"错误: 文件未找到,请检查路径 '{csv_path}' 是否正确。")        return []     # 2. 按时间戳排序,这是构建链条的基础    df = df.sort_values(by='timestamp').reset_index(drop=True)     # 3. 建立一个从 'from_address' 到其发起交易的快速查找字典    transactions_by_sender = defaultdict(list)    all_transactions = df.to_dict('records')     for tx in all_transactions:        transactions_by_sender[tx['from_address']].append(tx)     # 4. 遍历和构建链条    chains = []    visited_hashes = set()     for tx in all_transactions:        # 如果当前交易已经被包含在某个链中,则跳过        if tx['tx_hash'] in visited_hashes:            continue         # 发现了一个新链条的起点        current_chain = []        current_tx = tx         # 循环追踪链条,直到中断        while current_tx is not None:            # 将当前交易加入链条,并标记为已访问            current_chain.append(current_tx)            visited_hashes.add(current_tx['tx_hash'])             # 寻找下一个环节:当前交易的接收方是否是下一笔交易的发送方?            next_sender = current_tx['to_address']                        next_tx_candidate = None            # 检查这个接收方是否发起过任何交易            if next_sender in transactions_by_sender:                # 遍历该发送方的所有交易,找到第一个尚未被访问的交易                for potential_next_tx in transactions_by_sender[next_sender]:                    if potential_next_tx['tx_hash'] not in visited_hashes:                        next_tx_candidate = potential_next_tx                        break # 找到了,跳出内层循环                        # 更新current_tx,如果没找到下一个环节,它会变为None,循环终止            current_tx = next_tx_candidate         # 将构建完成的链条存入结果列表        chains.append(current_chain)            return chains def print_chains_summary(chains):    """打印找到的交易链的摘要信息"""    print("\n" + "="*50)    print(f"分析完成!共找到 {len(chains)} 条独立的交易链。")    print("="*50 + "\n")     # 按链条长度从长到短排序    chains.sort(key=len, reverse=True)     for i, chain in enumerate(chains):        print(f"--- 链条 {i+1} (长度: {len(chain)}) ---")                # 打印链条的起点和终点        start_addr = chain[0]['from_address']        end_addr = chain[-1]['to_address']                print(f"  起点地址: {start_addr}")        print(f"  终点地址: {end_addr}")         path_str = chain[0]['from_address']        for tx in chain:            path_str += f" --({tx['decoded_tx_hash']})--> {tx['to_address']}"        print(f"  路径: {path_str}")   if __name__ == "__main__":    FILE_PATH = "sheidongleheixian_decoded.csv"    found_chains = find_transaction_chains(FILE_PATH)    if found_chains:        print_chains_summary(found_chains)

将运行结果保存到output.txt中,得到部分数据如下:

text
成功加载 7030 条交易记录。==================================================分析完成!共找到 3125 条独立的交易链。==================================================--- 链条 1 (长度: 6) ---  起点地址: 14B43RN0FRLN2MVPS9D1C  终点地址: 14QIWXQXAET9C1H8OR09U  路径: 14B43RN0FRLN2MVPS9D1C --(tx0000017XIY8MI0)--> 1V6BOKA7ZFVIHYE0NTU2K --(tx000006R6WLJTGV)--> 1NEC9LG6OX02GUPGS9EIS --(tx0000314J815ZDZ)--> 16AXD9WB9Q5DDBTIIQLZK --(tx0001566NBB4FG1)--> 1AYRO89L2T28WOTEU9O1M --(tx000781Z43PIY3K)--> 1Q1PQ0EL8HF0PY55KYKDN --(tx003906Z6HA9JZA)--> 14QIWXQXAET9C1H8OR09U--- 链条 2 (长度: 6) ---  起点地址: 14B43RN0FRLN2MVPS9D1C  终点地址: 14QIWXQXAET9C1H8OR09U  路径: 14B43RN0FRLN2MVPS9D1C --(tx000002litt7H6R)--> 1QLHG0KSCA7WIMUZOPPO7 --(tx000011F6JVVRGF)--> 146WVE6IQ95L936UO8Y8W --(tx00005602RR07WL)--> 1RDRUCP47T8X597CIT2A3 --(tx000281HKCM3YA1)--> 1LWW3QK5EXYZ39J2NS1C1 --(tx001406Z25GXL9L)--> 1T4T9LOYM46CQV5M8U9T5 --(tx004531CCLW6SRL)--> 14QIWXQXAET9C1H8OR09U

观察到第2条链有一个decoded_tx_hash不对劲,tx000002litt7H6R里面包含小写字母,其他的decoded_tx_hash都是大写字母(当然tx是除外的)

并且里面的litt看起来像是little的前半部分,在sheidongleheixian_decoded.csv中搜索le发现了tx000014le_dWAHC

那么接下来只要找出带小写字母的然后把它们拼起来就可以了

python
import pandas as pd file_path = "sheidongleheixian_decoded.csv"df = pd.read_csv(file_path)pattern = r'^tx.*[a-z]'filtered_df = df[df['decoded_tx_hash'].str.contains(pattern, regex=True)]print(filtered_df['decoded_tx_hash'])

得到的输出如下:

text
286     tx001890mr!!ZIU4848     tx000002litt7H6R2298    tx000075og_i43J22795    tx000377s_AoOCHG3801    tx000014le_dWAHCName: decoded_tx_hash, dtype: object
flag
H&NCTF{little_dog_is_Aomr!!}

Forensics

ez_game

对附件的磁盘镜像challenge.vhd进行分析,找到关键信息readme.txtkey.jpghhhhhhhh.zip

其中readme.txt的内容如下:

text
这次是个简单的取证小游戏1.我藏了一个电脑,它的密码是很简单的弱密码,但是你找的到吗2.图片没有隐写,但是它凭什么可以成为key,好难猜啊3.如果你找的了flag,注意flag的内容全部大写

首先第1条的“藏了一个电脑”指的是容器hhhh里的CentOS 7镜像(这个要靠第2条才能解出来),吐槽一下这里很简单的弱密码指的是系统的密码,不过都拿到镜像文件了这个提示就没啥用

第2条想表达的是,key.jpg就是VeraCrypt容器hhhh的密码,解开并挂载之后就能对里面的系统镜像进行分析了

第3条就是字面意思

HNCTF2025-1

在系统的/root/test目录下发现了文件hhhh.txt,内容如下:

text
‌‌‌‌‍‌·1234567890‌‌‌‌‍‬‬‌‌‌‌‌‍‬‬‍‌‌‌‌‍‬‍‬‌‌‌‌‍‍‌-=

这里存在零宽字符隐写

HNCTF2025-2

得到提示shift,按住shift键把键盘上面那一排从左到右按一遍就行,得到:

text
~!@#$%^&*()_+

这个就是加密压缩包hhhh.zip的解压密码,这个压缩包被删除了,就是$RQCSJAK.zip

解压得到flag.drawio,用官网提供的在线工具draw.io打开查看

HNCTF2025-3

flag
H&NCTF{YOU_R_SSSO_COOL}

OSINT

Chasing Freedom 1

查询图片exif得知是5月3日

图片上的渔船有名字闽平渔65599

船位查询查询发现该渔船位置

HNCTF2025-4

flag
H&NCTF{0503-丁鼻垄}

Chasing Freedom 2

查询图片exif得知是5月4日

识图搜索图中的黑白灯塔

HNCTF2025-5

flag
H&NCTF{0504-东庠岛灯塔}

Chasing Freedom 3

查询图片exif得知是5月34日

图片下方的水桶写着岚庠渡

搜索发现这篇文章等一刻轮渡,过一天属于岛屿的生活 - 东庠岛生活画册_码头_海面_渔船

得到两个信息:位于流水码头,船的名字是岚庠渡x号

由于不知道具体是几号,从1号开始试,最终试出来是3号

flag
H&NCTF{0504-流水码头-岚庠渡3号}

Crypto

哈基coke

text
import numpy as npimport cv2import osdef arnold_decode(arnold_image, shuffle_times, a, b):    """ Arnold shuffle for rgb image    Args:        arnold_image: input arnold encoded rgb image        shuffle_times: how many times to shuffle        a: element a of the arnold transform matrix        b: element b of the arnold transform matrix    Returns:        Arnold decode image    """    # 1: 创建新图像    decode_image = np.zeros(shape=arnold_image.shape)    # 2: 计算N    h, w = arnold_image.shape[0], arnold_image.shape[1]    N = h  # 或N=w    # 3: 遍历像素坐标变换    for time in range(shuffle_times):        for ori_x in range(h):            for ori_y in range(w):                # 按照公式坐标变换                new_x = ((a * b + 1) * ori_x + (-b) * ori_y) % N                new_y = ((-a) * ori_x + ori_y) % N                decode_image[new_x, new_y, :] = arnold_image[ori_x, ori_y, :]        # 更新坐标        arnold_image = np.copy(decode_image)    return decode_image# 检查文件路径是否存在image_path = 'en_flag.png'if not os.path.exists(image_path):    print(f"文件 {image_path} 不存在")    exit()encoded_image = cv2.imread(image_path)if encoded_image is None:    print(f"无法读取文件 {image_path}")    exit()decoded_image = arnold_decode(encoded_image,6,9,1)output_path = "coke.png"cv2.imwrite(output_path, decoded_image)print("处理完成")

猫变换

HNCTF2025-6

flag
H&NCTF{haji_coke_you_win}

Reverse

签到re

Gemini一把梭

代码分析:

  1. main 函数:

    • 程序使用硬编码的密钥 "MySecretKey123!"
    • 调用 sub_11B9 函数,基于该密钥生成一个32位的加密密钥 v5
    • 读取用户输入 s
    • 调用 sub_1452 函数,使用 v5 对用户输入 s 进行加密。
    • 将加密后的结果与 byte_4080 的内容逐字节比较。如果完全相同,则输出 “right”。
  2. sub_11B9 函数:

    • 计算 "MySecretKey123!" 的 SHA256 哈希值。
    • 取哈希值的前4个字节,记为 d0, d1, d2, d3
    • 通过位运算生成最终的4字节加密密钥 k = [k0, k1, k2, k3]
      • k0 = d0 | 1
      • k1 = d1 & 0xFE
      • k2 = d2 & 0xFE
      • k3 = d3 | 1
    • 这个过程确保了 k0k3 是奇数,而 k1k2 是偶数。
  3. sub_13ACsub_1452 函数:

    • sub_1452 是主加密函数。它将输入数据填充到4字节的倍数,然后以4字节为一块进行处理。
    • 每一块4字节的明文被分成两个2字节的块。
    • sub_13AC 是核心的加密算法。它对每个2字节的块进行操作。其运算本质上是一个模256的2x2矩阵乘法: 其中 (p0, p1) 是2字节的明文,(c0, c1) 是加密后的2字节密文,k 是从 sub_11B9 得到的4字节密钥。

解题思路:

要得到 flag,我们需要执行以下步骤:

  1. 重新生成加密密钥: 完全按照 sub_11B9 的逻辑,从 "MySecretKey123!" 生成4字节的密钥矩阵 K
  2. 求逆矩阵: 为了解密,我们需要找到加密矩阵 K 在模256下的逆矩阵 K_inv。解密操作就是 P = K_inv * C mod 256
    • 求矩阵的行列式: det(K) = (k0*k3 - k1*k2) mod 256
    • 求行列式的模逆元: det(K)^-1 mod 256
    • 计算逆矩阵: K_inv = det(K)^-1 * [[k3, -k1], [-k2, k0]] mod 256
  3. 解密数据:
    • byte_4080 中提取密文数据(从第5个字节开始)。
    • 将密文数据以4字节为单位进行分割。
    • 将每个4字节的块再分成两个2字节的块。
    • 使用求得的逆矩阵 K_inv 对每个2字节的密文块进行解密。
    • 将解密后的数据拼接起来。
  4. 提取Flag: byte_4080 的前4个字节 (00 00 00 25) 指明了原始明文的长度是37字节。从解密后的数据中截取前37个字节,即为最终的 flag。
python
import hashlib def generate_key(secret: str) -> list[int]:    """    根据 sub_11B9 的逻辑从密钥字符串生成加密密钥。    """    sha256 = hashlib.sha256(secret.encode()).digest()    d = sha256[:4]        # k = [k0, k1, k2, k3]    key = [0] * 4    key[0] = d[0] | 1    key[1] = d[1] & 0xFE    key[2] = d[2] & 0xFE    key[3] = d[3] | 1        return key def get_inverse_matrix(key: list[int]) -> list[int]:    """    计算加密矩阵在模256下的逆矩阵。    """    k0, k1, k2, k3 = key        # 计算行列式 det(K) = (k0*k3 - k1*k2) mod 256    det = (k0 * k3 - k1 * k2) % 256        # 计算行列式的模256乘法逆元    # 在 Python 3.8+ 中,可以直接使用 pow(det, -1, 256)    try:        det_inv = pow(det, -1, 256)    except ValueError:        raise ValueError(f"Determinant {det} has no modular inverse for modulus 256")     # 计算逆矩阵 K_inv = det_inv * [[k3, -k1], [-k2, k0]] mod 256    inv_matrix = [0] * 4    inv_matrix[0] = (det_inv * k3) % 256    inv_matrix[1] = (det_inv * -k1) % 256    inv_matrix[2] = (det_inv * -k2) % 256    inv_matrix[3] = (det_inv * k0) % 256        return inv_matrix def decrypt_block(cipher_block: bytes, inv_matrix: list[int]) -> bytes:    """    使用逆矩阵解密一个2字节的数据块。    P = K_inv * C mod 256    """    c0, c1 = cipher_block    m0, m1, m2, m3 = inv_matrix        p0 = (m0 * c0 + m1 * c1) % 256    p1 = (m2 * c0 + m3 * c1) % 256        return bytes([p0, p1]) def solve():    """    主函数,执行完整的解密流程并输出Flag。    """    # .data:0000000000004080    byte_4080 = bytes([        0x00, 0x00, 0x00, 0x25, 0x0C, 0xE2, 0x70, 0x89, 0x98, 0xB2, 0xBB, 0xE4,        0x94, 0xA0, 0x95, 0xAC, 0x38, 0x92, 0x22, 0xF8, 0x0E, 0x7B, 0x76, 0x1A,        0x66, 0xC8, 0x03, 0x05, 0x2E, 0x7D, 0xA1, 0x04, 0x3D, 0xC0, 0x62, 0xFE,        0x66, 0x67, 0x02, 0x87, 0x81, 0xF4, 0x00, 0x00    ])     # 1. 生成密钥    secret = "MySecretKey123!"    key = generate_key(secret)    print(f"🔑 生成的加密密钥 (k0,k1,k2,k3): {key}")     # 2. 计算逆矩阵    inv_matrix = get_inverse_matrix(key)    print(f"🔢 逆矩阵 (mod 256): {inv_matrix}")        # 3. 提取密文和明文长度    original_len = int.from_bytes(byte_4080[:4], 'big')    ciphertext = byte_4080[4:]        decrypted_padded = bytearray()        # 4. 循环解密    # 将密文按4字节分块    for i in range(0, len(ciphertext), 4):        chunk = ciphertext[i : i+4]        if len(chunk) < 4:            # 处理可能的填充不完整情况(虽然在此题中不会发生)            continue                    # 将4字节块分成两个2字节块并解密        decrypted_padded.extend(decrypt_block(chunk[0:2], inv_matrix))        decrypted_padded.extend(decrypt_block(chunk[2:4], inv_matrix))     # 5. 截取原始长度,得到Flag    flag = decrypted_padded[:original_len].decode('utf-8')        print("\n" + "="*40)    print(f"🚩 解密得到的 Flag: {flag}")    print("="*40) if __name__ == "__main__":    solve() 

运行结果

text
🔑 生成的加密密钥 (k0,k1,k2,k3): [81, 22, 52, 251]🔢 逆矩阵 (mod 256): [217, 238, 4, 171]========================================🚩 解密得到的 Flag: H&NCTF{840584fb08a26f01c471054628e451========================================
flag
H&NCTF{840584fb08a26f01c471054628e451}