NepCTF2025 - misc - WriteUp

碎碎念

这回也是第一次打nep(笑死了什么时候我能有打第二次的比赛),打之前看过去年的wp,感觉今年好像要难点? 反正一开始做的是挺牢的 但总的来说还是万变不离其宗了,这次本来只是想看看题的,打着打着又做了不少题,想歇着了)

NepBotEvent

知识点省流

变种键盘流量()

WP

题目说了是Keylogger,推断应该跟键盘流量类似

将里面的hex值提取出来,然后多次分析后发现41-42这两位就是对应的键盘hid数据,提取出来后用脚本转对应数据即可,要记得加上shift后会影响大小写

经过分析 每三行为一组数据,第一行为对应的hid数据,如果第二行相同位置的值为0则表示成功输入,如果为1则视为输入失败

image-20250728140235181

让ai搞个脚本过滤一下

def extract_chars(input_file, output_file):
    with open(input_file, 'r', encoding='utf-8') as infile, \
         open(output_file, 'w', encoding='utf-8') as outfile:
        for line in infile:
            if len(line) >= 42:
                outfile.write(line[40:42] + '\n')  # 索引从0开始,39是第40个字符
            else:
                outfile.write('\n')  # 行长度不足42,写空行

# 使用示例
extract_chars('input.txt', 'output1.txt')


def filter_by_next_line(input_file, output_file):
    with open(input_file, 'r', encoding='utf-8') as f:
        lines = [line.strip() for line in f]

    result = []
    for i in range(0, len(lines) - 1, 3):  # 每3行观察一次
        current_line = lines[i]
        next_line = lines[i + 1]
        if next_line == '00':
            result.append(current_line)

    with open(output_file, 'w', encoding='utf-8') as f:
        for item in result:
            f.write(item + '\n')

# 使用示例
filter_by_next_line('output1.txt', 'filtered.txt')

然后再映射即可

# 自定义 HID 映射表(十六进制小写字符串作为键)
hid_keymap = {
    "04": "a", "05": "b", "06": "c", "07": "d", "08": "e", "09": "f", "0a": "g", "0b": "h", "0c": "i",
    "0d": "j", "0e": "k", "0f": "l", "10": "m", "11": "n", "12": "o", "13": "p", "14": "q", "15": "r",
    "16": "s", "17": "t", "18": "u", "19": "v", "1a": "w", "1b": "x", "1c": "y", "1d": "z", "1e": "1",
    "1f": "2", "20": "3", "21": "4", "22": "5", "23": "6", "24": "7", "25": "8", "26": "9", "27": "0",
    "28": "<RET>", "29": "<ESC>", "2a": "<DEL>", "2b": "\t", "2c": "<SPACE>", "2d": "-", "2e": "=",
    "2f": "[", "30": "]", "31": "\\", "32": "<NON>", "33": ";", "34": "'", "35": "<GA>", "36": ",",
    "37": ".", "38": "/", "39": "<CAP>", "3a": "<F1>", "3b": "<F2>", "3c": "<F3>", "3d": "<F4>",
    "3e": "<F5>", "3f": "<F6>", "40": "<F7>", "41": "<F8>", "42": "<F9>", "43": "<F10>", "44": "<F11>",
    "45": "<F12>", "46": "<PRTSC>", "47": "<SCRLK>", "48": "<PAUSE>", "49": "<INS>", "4a": "<HOME>",
    "4b": "<PGUP>", "4c": "<DEL_FWD>", "4d": "<END>", "4e": "<PGDN>", "4f": "<RIGHT>", "50": "<LEFT>",
    "51": "<DOWN>", "52": "<UP>", "53": "<NUMLOCK>", "54": "/", "55": "*", "56": "-", "57": "+",
    "58": "<ENTER>", "59": "1", "5a": "2", "5b": "3", "5c": "4", "5d": "5", "5e": "6", "5f": "7",
    "60": "8", "61": "9", "62": "0", "63": ".", "64": "<NONUS_BACK>", "65": "<APP>", "66": "<POWER>",
    "67": "=", "68": "<F13>", "69": "<F14>", "6a": "<F15>", "6b": "<F16>", "6c": "<F17>", "6d": "<F18>",
    "6e": "<F19>", "6f": "<F20>", "70": "<F21>", "71": "<F22>", "72": "<F23>", "73": "<F24>","e1":"<leftshift>","e5":"<rightshift>",
}

# 加载 USB HID 数据文件
input_file = "filtered.txt"  # 替换为你的实际路径
with open(input_file, "r") as f:
    lines = f.read().splitlines()

# 解析数据并还原按键
keystrokes = []
for line in lines:
    hex_code = line.lower()
    key = hid_keymap.get(hex_code, '')
    keystrokes.append(key)

# 输出完整的按键还原结果(包括控制符号)
reconstructed_text = ''.join(keystrokes)
print(reconstructed_text)

image-20250728140109614

SpeedMino

知识点省流

俄罗斯高手 脚本小子

WP

俄罗斯方块,题目说玩到2600分给flag

试了几次有点费劲,但是发现只要一直翻转方块就不会落地

所以下个连点脚本让他慢慢攒分

import keyboard
import time

print("立即开始连点 X 键,按 Ctrl+C 或 ESC 停止。")

try:
    while True:
        keyboard.press_and_release('x')
        time.sleep(0.1)
        if keyboard.is_pressed('esc'):
            print("已停止")
            break
except KeyboardInterrupt:
    print("\n用户终止")
x

跑了3小时就出了

image-20250726021132802

客服小美

知识点省流

取证分析+cs流量解密

WP

内存取证和流量取证

用户名很简单,用ufs打开内存镜像找到盘里的用户即可——JohnDoe

image-20250728015910317

ip和端口可以直接在流量包中找到 确定的过程是在桌面上发现了一个exe文件,导出来丢给沙箱会发现是个cs程序,而流量包也符合cs的特征,所以这段流量就是被控机再给攻击者的木马地址进行通信,dst就是木马ip和端口

image-20250728020021124

敏感信息则需要解密cs流量,常规的解公钥爆破私钥的方法尝试后无果,n分解不了

搜索一下找到了这个文章 https://www.secrss.com/articles/35756

用vol3提取恶意程序的进程转储,然后用里面提到的cs-extract脚本直接提取hmac key和aes key后

image-20250728140932920

image-20250728141001477

https://goodlunatic.github.io/posts/f002407/#%E9%A2%98%E7%9B%AE%E5%90%8D%E7%A7%B0-cscs 我直接用lunatic师傅的cscs这题的脚本解的数据(用文章的那个解析脚本会报错不知道为什么 key是没问题的)

import hmac
import binascii
import base64
import hexdump
from Crypto.Cipher import AES

def decrypt(encrypted_data, iv_bytes, signature, shared_key, hmac_key):
    if hmac.new(hmac_key, encrypted_data, digestmod="sha256").digest()[:16] != signature:
        print("message authentication failed")
        return
    cipher = AES.new(shared_key, AES.MODE_CBC, iv_bytes)
    return cipher.decrypt(encrypted_data)

if __name__ == "__main__":
    SHARED_KEY = binascii.unhexlify("9fe14473479a283821241e2af78017e8")
    HMAC_KEY = binascii.unhexlify("1e3d54f1b9f0e106773a59b7c379a89d")
    encrypt_datas = ["00000040efeda3e57f7d7fd589d11640ea0f9a4fe6bc91332723ffc5f43f78b37c21cc7485c44d6c8eb6af74fc7044046059c76519e493e351c9f631d6785d5c07eae9e3","000001602a99f7cc51face35199e8b1a4a5616e0301591b6f1f48b1d000149cb83d6a81e9659849a52c4f50a8629b0dfb7c036df406b44d449e40fe18df3594721e1f5849662271c1ea18b18c8eb58af5ee2c3a784852dd1c4a5c699f9518d2e2fc70d756cd68361ac794eed4eae6b062be6c31651caf93954f2a89b10e25b1fd9757ec17ee8b97038c4babb73c4f21688f5d235797844c2c9c288fac3fd2bd9cf5373956389b7e5232e35b6f268f9d67ba54f3e7e1606d4cb4020d5f480c6e5f4409b8d87e0443ae0bcfe93d286291ba6bfd0c7f37593581d90bb4ab7cfb065b4421a727f120fb491c2dc01797e38996dfc123fb120c5ed312577cc917d8a435b73c25b6d29ef0bad595100256c9aa5571e5c0ce0a8ea2c173ca1fae577fa924506b75b86522052f019d6843d74dc6fbdf2219b77e020a049c4e77df3658c80bcb703f8f878ff2f70c5c69d0cf6f4efb5a755ba854dfa5777a23989286770da6e0444d0","000000d0c72ef8b74a7d8acc332695b62448280f9a4eaa12457de4adcad279b0563f2d4cb0707f7e2853c45acf28a365d905cf8ca421d557bd7655cbd50aafbdbe5f3f570c9c3d876d0c21b661ca5c46e09f987f7e1263f6d33c34db28a2fd342fe48e5801d1a97fb88e00f0c648ec889f6b72d71edd2eed5affd32bc8d51e27fcc148d16823c1bc235b0e16d9d477bd0b4582941db373e171cce78b10c869eb987baf3fd9f879b236be6f3af43b7742f6241dfe02ab696c96f1779d0003d6b2720d1c93890e75fcce939f1c8e0922ce5044bc3a","00000100f24f15cd6f33c36e70ca228d10babfac1cf6bfbb9b6923a7828c9ed30b76d3ce1cb3d8f97c358bf90004e771ac646b1b996fd248ac8f0b460e0a36950dffcde04f3bae831982b528393f3a3c771310ba0c0bb7418ba5e8734a6bd37bc8a51cc0683c0904e0f404180e4c4c34720a3e5d6767c435f1746e6b93a13a2ecdc8074089e684b90748fc1a7e24e66bd637673437d9e24a37ce6f584b478e2f0485f3c05414dd4c35eb9ecfed8d4fbdab54db4233258f4fea6ed515a1030feeb184db94a4841236b491d2f7379e10f52d50ae573cd6f4504aa9750da273fa65c2a9eaf9b9bb014cafc53a9e9f0042bfcd5d24fc1b29173fd3308ff08d30b2a7d42132d4"]
    for encrypt_data in encrypt_datas:
        # encrypt_data = base64.b64decode(encrypt_data)
        encrypt_data = bytes.fromhex(encrypt_data)
        encrypt_data_length = int.from_bytes(encrypt_data[:4], byteorder='big', signed=False)
        encrypt_data_l = encrypt_data[4:]
        data1 = encrypt_data_l[:encrypt_data_length-16]
        signature = encrypt_data_l[encrypt_data_length-16:]
        iv_bytes = b"abcdefghijklmnop"

        dec = decrypt(data1, iv_bytes, signature, SHARED_KEY, HMAC_KEY)
        print(f"{'='*80}")
        print("[+] counter: {}".format(int.from_bytes(dec[:4], byteorder='big', signed=False)))
        print("[+] 任务返回长度: {}".format(int.from_bytes(dec[4:8], byteorder='big', signed=False)))
        print("[+] 任务输出类型: {}".format(int.from_bytes(dec[8:12], byteorder='big', signed=False)))
        output = dec[12:int.from_bytes(dec[4:8], byteorder='big', signed=False)].decode('gbk',errors='ignore')
        print(hexdump.hexdump(dec))
        print(output)

最后得到了敏感信息

3a596282a4dadb852c1a8760d1eaa15

MoewBle喵泡

知识点省流

玩游戏就对了+轻逆向(熟悉游戏的也可以直接猜到指令)

WP

玩游戏玩完可以得到几乎全部flag 但少了一段

a47c5319576ae3cefd105e4d116f4a8

找到后我反过来爆搜flag片段发现flag明文存储在了文件中

a38d7604aec8ce0ccd1bc0a3667e20e

简单搜索后翻到了一个解析unity游戏数据的工具 https://assetripper.github.io/AssetRipper/articles/Downloads.html

提取后爆搜发现了所有的面板提示 但是少了一个 根据提示应该要进gm

782bf3cfaa5d4da9a4fb05ab74be3e2

翻了翻里面的文件,找到了gm相关的代码 GmManager.cs 让ai分析一下可以知道切gm的方式是 上上下下左右左右ba

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq.Expressions;
using TMPro;
using UnityEngine;

public sealed class GmManager : MonoBehaviour
{
	public enum KonamiKey
	{
		Up = 0,
		Down = 1,
		Left = 2,
		Right = 3,
		A = 4,
		B = 5
	}

	private readonly struct GmResultLogger
	{
		private readonly TextMeshProUGUI resultText;

		public GmResultLogger(TextMeshProUGUI resultText)
		{
			this.resultText = null;
		}

		public void Log(string message)
		{
		}

		public void Info(string message)
		{
		}

		public void Error(string message)
		{
		}

		public void Warning(string message)
		{
		}
	}

	private class GmCommandRunner
	{
		public enum TokenType
		{
			Ident = 0,
			Expr = 1,
			Int = 2,
			Float = 3,
			String = 4,
			Bool = 5,
			Vector2 = 6,
			Vector3 = 7,
			List = 8
		}

		public interface IToken
		{
			TokenType Type { get; }

			string Text { get; }

			object Value { get; }
		}

		public interface IToken<T> : IToken
		{
			new T Value { get; }
		}

		public readonly struct IdentToken : IToken<string>, IToken
		{
			public TokenType Type => default(TokenType);

			public string Text { get; }

			public string Value => null;

			object IToken.Value => null;

			public IdentToken(string text)
			{
				Text = null;
			}
		}

		public struct ExprToken : IToken<Expression>, IToken
		{
			private Func<string> getValueText;

			private Action<object> setValue;

			public TokenType Type => default(TokenType);

			public string Text => null;

			public Expression Value { get; }

			object IToken.Value => null;

			public ExprToken(Expression member)
			{
				Value = null;
				getValueText = null;
				setValue = null;
			}

			public string GetText()
			{
				return null;
			}

			public void SetValue(IToken token)
			{
			}
		}

		public readonly struct IntToken : IToken<int>, IToken, IToken<float>
		{
			public TokenType Type => default(TokenType);

			public string Text { get; }

			public int Value { get; }

			float IToken<float>.Value => 0f;

			object IToken.Value => null;

			public IntToken(int value)
			{
				Text = null;
				Value = 0;
			}
		}

		public readonly struct FloatToken : IToken<float>, IToken
		{
			public TokenType Type => default(TokenType);

			public string Text { get; }

			public float Value { get; }

			object IToken.Value => null;

			public FloatToken(float value)
			{
				Text = null;
				Value = 0f;
			}
		}

		public readonly struct StringToken : IToken<string>, IToken
		{
			public TokenType Type => default(TokenType);

			public string Text { get; }

			public string Value => null;

			object IToken.Value => null;

			public StringToken(string text)
			{
				Text = null;
			}
		}

		public readonly struct BoolToken : IToken<bool>, IToken
		{
			public TokenType Type => default(TokenType);

			public string Text { get; }

			public bool Value { get; }

			object IToken.Value => null;

			public BoolToken(bool value)
			{
				Text = null;
				Value = false;
			}
		}

		public readonly struct Vector2Token : IToken<Vector2>, IToken
		{
			public TokenType Type => default(TokenType);

			public string Text { get; }

			public Vector2 Value { get; }

			object IToken.Value => null;

			public Vector2Token(Vector2 value)
			{
				Text = null;
				Value = default(Vector2);
			}
		}

		public readonly struct Vector3Token : IToken<Vector3>, IToken
		{
			public TokenType Type => default(TokenType);

			public string Text { get; }

			public Vector3 Value { get; }

			object IToken.Value => null;

			public Vector3Token(Vector3 value)
			{
				Text = null;
				Value = default(Vector3);
			}
		}

		public readonly struct ListToken : IToken<IList>, IToken
		{
			public TokenType Type => default(TokenType);

			public string Text { get; }

			public IList Value { get; }

			object IToken.Value => null;

			public ListToken(IList value)
			{
				Text = null;
				Value = null;
			}
		}

		private readonly GmResultLogger logger;

		private readonly Dictionary<string, Action<GmResultLogger, IToken[]>> commandHandlers;

		public GmCommandRunner(GmResultLogger logger)
		{
		}

		public void RegisterCommand(string command, Action<GmResultLogger, IToken[]> handler)
		{
		}

		private void HelpHandler(GmResultLogger logger, IToken[] args)
		{
		}

		private void DebugHandler(GmResultLogger logger, IToken[] args)
		{
		}

		private void SetHandler(GmResultLogger logger, IToken[] args)
		{
		}

		private void EnableSuperHandler(GmResultLogger logger, IToken[] args)
		{
		}

		private void GetFlagHandler(GmResultLogger logger, IToken[] args)
		{
		}

		private void InitHandlers()
		{
		}

		public void Run(string command)
		{
		}

		public static void SplitTokens(string command, out IToken[] tokens)
		{
			tokens = null;
		}
	}

	public bool IsUseGm;

	private readonly KonamiKey[] sequence;

	private int currentIndex;

	private float lastInputTime;

	public float sequenceTimeout;

	public GameObject gmPanel;

	public TMP_InputField gmCommandText;

	public TextMeshProUGUI resultText;

	private GmCommandRunner runner;

	private void Update()
	{
	}

	public void ExitGmMode()
	{
	}

	private void CheckEnterGmMode()
	{
	}

	private bool CheckKeyDown(KonamiKey targetKey, KeyCode keyCode)
	{
		return false;
	}

	private void OnKonamiCodeActivated()
	{
	}

	private void RunGmCommand(string command)
	{
	}
}

经过尝试后 发现是要在游戏过程中的设置页面里操作即可打开gm面板 help后发现有getflag的方法

输入getflag 7 得到最后缺少的部分

image-20250728141101622

easyshock

知识点省流

不懂 说是故障注入 但是爆破

WP

不是特别懂 但可以让ai梭哈

根据题目信息以及ai对附件的分析 好像是要得到一个准确的shocktime 让寄存器进行一个随机翻转

image-20250728021108508

不是很懂 但可以进行爆破,稍微把范围开大点,然后让他慢慢爆破并且记录 shock成功的次数,以防万一存在偶然性

import os
import random
from Crypto.Cipher import ARC4
from unicorn import *
from unicorn.x86_const import *
from capstone import *
from capstone.x86_const import *

# ==== 基础参数 ====
FLAG = b'test{dummy_flag}'
CODE_COUNT = 0
TARGET_COUNT = 0

CODE_ADDRESS    = 0x110000
DATA_ADDRESS    = 0x220000
OUTPUT_ADDRESS  = 0x330000
KEY_ADDRESS     = 0x440000
STACK_ADDRESS   = 0x880000

with open("prog", "rb") as f:
    CODE = f.read()

ENTRY_POINT = CODE_ADDRESS
ENTRY_POINT_END = CODE_ADDRESS + 0x296

# ==== 模拟核心 ====
def flipBit(uc: Uc, reg_name: str):
    randBit = random.randint(0, 7)
    reg_num = eval("UC_X86_REG_" + reg_name.upper())
    reg_value = uc.reg_read(reg_num)
    reg_value ^= 1 << randBit
    uc.reg_write(reg_num, reg_value)

def shock(uc: Uc):
    rip = uc.reg_read(UC_X86_REG_RIP)
    code = uc.mem_read(rip, 16)
    md = Cs(CS_ARCH_X86, CS_MODE_64)
    md.detail = True
    dis = next(md.disasm(code, 0), None)
    if dis:
        for reg_list in dis.regs_access():
            for reg in reg_list:
                flipBit(uc, md.reg_name(reg))

def hook_code(uc: Uc, address, size, user_data):
    global CODE_COUNT
    if CODE_COUNT >= 200000:
        uc.emu_stop()
    if CODE_COUNT == TARGET_COUNT:
        shock(uc)
    CODE_COUNT += 1

def emulate(cipher, key, target):
    global CODE_COUNT, TARGET_COUNT
    CODE_COUNT = 0
    TARGET_COUNT = target

    mu = Uc(UC_ARCH_X86, UC_MODE_64)
    mu.mem_map(CODE_ADDRESS, 0x1000)
    mu.mem_map(DATA_ADDRESS, 0x1000)
    mu.mem_map(OUTPUT_ADDRESS, 0x1000)
    mu.mem_map(KEY_ADDRESS, 0x1000)
    mu.mem_map(STACK_ADDRESS, 0x2000)

    mu.mem_write(CODE_ADDRESS, CODE)
    mu.mem_write(DATA_ADDRESS, cipher)
    mu.mem_write(KEY_ADDRESS, key)

    mu.reg_write(UC_X86_REG_RIP, ENTRY_POINT)
    mu.reg_write(UC_X86_REG_RDI, DATA_ADDRESS)
    mu.reg_write(UC_X86_REG_RSI, KEY_ADDRESS)
    mu.reg_write(UC_X86_REG_RDX, OUTPUT_ADDRESS)
    mu.reg_write(UC_X86_REG_RSP, STACK_ADDRESS + 0x1000)

    mu.hook_add(UC_HOOK_CODE, hook_code)

    try:
        mu.emu_start(ENTRY_POINT, ENTRY_POINT_END)
        result = mu.mem_read(OUTPUT_ADDRESS, 512)
        return result
    except:
        return b''

# ==== 构造密文 ====
def generate_cipher():
    text = open("text.txt", "rb").read() + FLAG + b'\n'
    key = os.urandom(16)
    cipher = ARC4.ARC4Cipher(key).encrypt(text)
    return cipher, key

# ==== 本地爆破 ====
if __name__ == '__main__':
    cipher, key = generate_cipher()
    successes = []

    for shocktime in range(1, 100000):
        if shocktime % 2000 == 0:
            print(f"[*] Testing shocktime = {shocktime}")
        found = False
        for _ in range(10):  # 每个点尝试 10 次
            result = emulate(cipher, key, shocktime)
            if FLAG in result:
                print(f"[!!] HIT shocktime = {shocktime}")
                successes.append(shocktime)
                found = True
                break
        # 不停止,继续下一个 shocktime

    print("\n=== 爆破完成 ===")
    print("成功的 shocktime 列表:", successes)

嗯爆还是可以爆出东西的

image-20250728023747941

再来个提交脚本 挑个shocktime试试

import socket
import ssl

# 靶场地址和端口
HOST = 'nepctf31-qayq-xg28-m9l3-cvbc6vzim342.nepctf.com'
PORT = 443

# 要发送的 shocktime
SHOCKTIME = 49202

# 建立 SSL 连接并发送 shocktime
with socket.create_connection((HOST, PORT), timeout=10) as raw_sock:
    ctx = ssl.create_default_context()
    conn = ctx.wrap_socket(raw_sock, server_hostname=HOST)

    # 读取直到提示出现
    prompt = b''
    while b'time to shock:' not in prompt:
        chunk = conn.recv(1024)
        if not chunk:
            break
        prompt += chunk
    print(prompt.decode(errors='ignore'), end='')

    # 发送 shocktime
    conn.sendall(f"{SHOCKTIME}\n".encode())

    # 读取并打印服务器响应
    response = b''
    conn.settimeout(5)
    try:
        while True:
            data = conn.recv(4096)
            if not data:
                break
            response += data
    except socket.timeout:
        pass

    print(response.decode(errors='ignore'))

丢给厨子处理一下得到的数据就得到flag了

image-20250728023945970

qwq