NepCTF2025 - misc - WriteUp
碎碎念
这回也是第一次打nep(笑死了什么时候我能有打第二次的比赛),打之前看过去年的wp,感觉今年好像要难点? 反正一开始做的是挺牢的 但总的来说还是万变不离其宗了,这次本来只是想看看题的,打着打着又做了不少题,想歇着了)
NepBotEvent
知识点省流
变种键盘流量()
WP
题目说了是Keylogger,推断应该跟键盘流量类似
将里面的hex值提取出来,然后多次分析后发现41-42这两位就是对应的键盘hid数据,提取出来后用脚本转对应数据即可,要记得加上shift后会影响大小写
经过分析 每三行为一组数据,第一行为对应的hid数据,如果第二行相同位置的值为0则表示成功输入,如果为1则视为输入失败
让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)
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小时就出了
客服小美
知识点省流
取证分析+cs流量解密
WP
内存取证和流量取证
用户名很简单,用ufs打开内存镜像找到盘里的用户即可——JohnDoe
ip和端口可以直接在流量包中找到 确定的过程是在桌面上发现了一个exe文件,导出来丢给沙箱会发现是个cs程序,而流量包也符合cs的特征,所以这段流量就是被控机再给攻击者的木马地址进行通信,dst就是木马ip和端口
敏感信息则需要解密cs流量,常规的解公钥爆破私钥的方法尝试后无果,n分解不了
搜索一下找到了这个文章 https://www.secrss.com/articles/35756
用vol3提取恶意程序的进程转储,然后用里面提到的cs-extract脚本直接提取hmac key和aes key后
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)
最后得到了敏感信息
MoewBle喵泡
知识点省流
玩游戏就对了+轻逆向(熟悉游戏的也可以直接猜到指令)
WP
玩游戏玩完可以得到几乎全部flag 但少了一段
找到后我反过来爆搜flag片段发现flag明文存储在了文件中
简单搜索后翻到了一个解析unity游戏数据的工具 https://assetripper.github.io/AssetRipper/articles/Downloads.html
提取后爆搜发现了所有的面板提示 但是少了一个 根据提示应该要进gm
翻了翻里面的文件,找到了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 得到最后缺少的部分
easyshock
知识点省流
不懂 说是故障注入 但是爆破
WP
不是特别懂 但可以让ai梭哈
根据题目信息以及ai对附件的分析 好像是要得到一个准确的shocktime 让寄存器进行一个随机翻转
不是很懂 但可以进行爆破,稍微把范围开大点,然后让他慢慢爆破并且记录 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)
嗯爆还是可以爆出东西的
再来个提交脚本 挑个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了