NSSCTF-4th

Reverse

checkit

先用jadx看,关键逻辑在native层的so文件

img

反编译libcheck.so文件,在Java_com_test_ezre_MainActivity_checkInput函数发现了主要函数实现exec

点进去看发现是vm的实现

img

主要逻辑

code 在 7→45 这一段每轮处理 两个字符 并把变换结果依次 PUSH 到栈里;然后 50→52 跳到比较循环,按从后到前 POP 出这 50 个值,逐个与 cmp_data[i] 比较,最终输出 “oh!You are right!”.

对第 i 个字符(从 0 开始):

  • 偶数位:v = 0x53 ^ (x - 5) ⇒ 反推 x = (v ^ 0x53) + 5
  • 奇数位:v = (x ^ 0x43) + 14 ⇒ 反推 x = (v - 14) ^ 0x43
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CMP_DATA = [
0x1A,0x1E,0x1D,0x0E,0x1C,0x13,0x25,0x0E,0x78,0x3B,0x31,0x3F,0x68,0x45,0x23,0x3D,
0x0F,0x45,0x37,0x3A,0x3A,0x70,0x07,0x81,0x1A,0x2A,0x3D,0x7E,0x7D,0x3C,0x09,0x82,
0x39,0x2A,0x0E,0x7E,0x09,0x32,0x19,0x81,0x0C,0x2A,0x68,0x45,0x09,0x43,0x3B,0x70,
0x4F,0x4C,0x00,0x00
]

def inv_even(v):
return ((v ^ 0x53) + 5) & 0xFF

def inv_odd(v):
return (((v - 14) & 0xFF) ^ 0x43) & 0xFF

def solve():
res = bytearray()
for i, v in enumerate(CMP_DATA[:50]):
res.append(inv_even(v) if i%2==0 else inv_odd(v))
s = bytes(res)
print(s.decode("latin1"))

if __name__ == "__main__":
solve()
NSSCTF{C0ngr@tulation!Y0N_s33m_7o_b3_gO0d_@t_vm!!}

CrackMe&F***Me

也是第一次遇到这种题捏,好好学习学习

先用010将文件末尾改为MEI魔数,**(**“MEI 魔术”是指 PyInstaller 打包后的 Windows 可执行文件头部的标记)

img

直接解包会出错,只能解包部分,主要的函数无法解包,这里可以推测可能是对压缩部分有魔改的加密,可以将没有解包的部分内容提取出来,最后单独处理

这里我给出修改后的pyinstxtractor.py,这个脚本可以解包并且将没有解包成功的部分初始内容提取出来

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
from __future__ import print_function
import os
import struct
import marshal
import zlib
import sys
from uuid import uuid4 as uniquename


class CTOCEntry:
def __init__(self, position, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name):
self.position = position
self.cmprsdDataSize = cmprsdDataSize
self.uncmprsdDataSize = uncmprsdDataSize
self.cmprsFlag = cmprsFlag
self.typeCmprsData = typeCmprsData
self.name = name


class PyInstArchive:
PYINST20_COOKIE_SIZE = 24
PYINST21_COOKIE_SIZE = 24 + 64
MAGIC = b'MEI\014\013\012\013\016'

def __init__(self, path):
self.filePath = path
self.pycMagic = b'\0' * 4
self.barePycList = []

def open(self):
try:
self.fPtr = open(self.filePath, 'rb')
self.fileSize = os.stat(self.filePath).st_size
except:
print('[!] Error: Could not open {0}'.format(self.filePath))
return False
return True

def close(self):
try:
self.fPtr.close()
except:
pass

def checkFile(self):
print('[+] Processing {0}'.format(self.filePath))
searchChunkSize = 8192
endPos = self.fileSize
self.cookiePos = -1

if endPos < len(self.MAGIC):
print('[!] Error : File is too short or truncated')
return False

while True:
startPos = endPos - searchChunkSize if endPos >= searchChunkSize else 0
chunkSize = endPos - startPos

if chunkSize < len(self.MAGIC):
break

self.fPtr.seek(startPos, os.SEEK_SET)
data = self.fPtr.read(chunkSize)
offs = data.rfind(self.MAGIC)

if offs != -1:
self.cookiePos = startPos + offs
break

endPos = startPos + len(self.MAGIC) - 1
if startPos == 0:
break

if self.cookiePos == -1:
print('[!] Error : Missing cookie, unsupported pyinstaller version or not a pyinstaller archive')
return False

self.fPtr.seek(self.cookiePos + self.PYINST20_COOKIE_SIZE, os.SEEK_SET)
if b'python' in self.fPtr.read(64).lower():
print('[+] Pyinstaller version: 2.1+')
self.pyinstVer = 21
else:
self.pyinstVer = 20
print('[+] Pyinstaller version: 2.0')
return True

def getCArchiveInfo(self):
try:
if self.pyinstVer == 20:
self.fPtr.seek(self.cookiePos, os.SEEK_SET)
(magic, lengthofPackage, toc, tocLen, pyver) = struct.unpack('!8siiii', self.fPtr.read(self.PYINST20_COOKIE_SIZE))
else:
self.fPtr.seek(self.cookiePos, os.SEEK_SET)
(magic, lengthofPackage, toc, tocLen, pyver, pylibname) = struct.unpack('!8sIIii64s', self.fPtr.read(self.PYINST21_COOKIE_SIZE))
except:
print('[!] Error : The file is not a pyinstaller archive')
return False

self.pymaj, self.pymin = (pyver//100, pyver%100) if pyver >= 100 else (pyver//10, pyver%10)
print('[+] Python version: {0}.{1}'.format(self.pymaj, self.pymin))

tailBytes = self.fileSize - self.cookiePos - (self.PYINST20_COOKIE_SIZE if self.pyinstVer == 20 else self.PYINST21_COOKIE_SIZE)
self.overlaySize = lengthofPackage + tailBytes
self.overlayPos = self.fileSize - self.overlaySize
self.tableOfContentsPos = self.overlayPos + toc
self.tableOfContentsSize = tocLen
print('[+] Length of package: {0} bytes'.format(lengthofPackage))
return True

def parseTOC(self):
self.fPtr.seek(self.tableOfContentsPos, os.SEEK_SET)
self.tocList = []
parsedLen = 0

while parsedLen < self.tableOfContentsSize:
(entrySize,) = struct.unpack('!i', self.fPtr.read(4))
nameLen = struct.calcsize('!iIIIBc')
(entryPos, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name) = \
struct.unpack('!IIIBc{0}s'.format(entrySize - nameLen), self.fPtr.read(entrySize - 4))

try:
name = name.decode("utf-8").rstrip("\0")
except UnicodeDecodeError:
name = str(uniquename())

if name.startswith("/"):
name = name.lstrip("/")
if len(name) == 0:
name = str(uniquename())

self.tocList.append(CTOCEntry(self.overlayPos + entryPos, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name))
parsedLen += entrySize

print('[+] Found {0} files in CArchive'.format(len(self.tocList)))

def _writeRawData(self, filepath, data):
nm = filepath.replace('\\', os.path.sep).replace('/', os.path.sep).replace('..', '__')
nmDir = os.path.dirname(nm)
if nmDir != '' and not os.path.exists(nmDir):
os.makedirs(nmDir)
with open(nm, 'wb') as f:
f.write(data)

def _writePyc(self, filename, data):
with open(filename, 'wb') as pycFile:
pycFile.write(self.pycMagic)
if self.pymaj >= 3 and self.pymin >= 7:
pycFile.write(b'\0' * 4)
pycFile.write(b'\0' * 8)
else:
pycFile.write(b'\0' * 4)
if self.pymaj >= 3 and self.pymin >= 3:
pycFile.write(b'\0' * 4)
pycFile.write(data)

def _fixBarePycs(self):
for pycFile in self.barePycList:
with open(pycFile, 'r+b') as f:
f.write(self.pycMagic)

def extractFiles(self):
print('[+] Beginning extraction...please standby')
extractionDir = os.path.join(os.getcwd(), os.path.basename(self.filePath) + '_extracted')
if not os.path.exists(extractionDir):
os.mkdir(extractionDir)
os.chdir(extractionDir)

for entry in self.tocList:
self.fPtr.seek(entry.position, os.SEEK_SET)
data = self.fPtr.read(entry.cmprsdDataSize)

if entry.cmprsFlag == 1:
try:
data = zlib.decompress(data)
assert len(data) == entry.uncmprsdDataSize
except Exception as e:
print('[!] Error decompressing {0}, saving raw. Reason: {1}'.format(entry.name, e))
self._writeRawData(entry.name + ".raw", data)
continue

if entry.typeCmprsData in (b'd', b'o'):
continue

basePath = os.path.dirname(entry.name)
if basePath != '' and not os.path.exists(basePath):
os.makedirs(basePath)

try:
if entry.typeCmprsData == b's':
print('[+] Possible entry point: {0}.pyc'.format(entry.name))
if self.pycMagic == b'\0' * 4:
self.barePycList.append(entry.name + '.pyc')
self._writePyc(entry.name + '.pyc', data)
elif entry.typeCmprsData in (b'M', b'm'):
if data[2:4] == b'\r\n':
if self.pycMagic == b'\0' * 4:
self.pycMagic = data[0:4]
self._writeRawData(entry.name + '.pyc', data)
else:
if self.pycMagic == b'\0' * 4:
self.barePycList.append(entry.name + '.pyc')
self._writePyc(entry.name + '.pyc', data)
else:
self._writeRawData(entry.name, data)
if entry.typeCmprsData in (b'z', b'Z'):
self._extractPyz(entry.name)
except Exception as e:
print('[!] Failed to write {0}, saving raw. Reason: {1}'.format(entry.name, e))
self._writeRawData(entry.name + ".raw", data)

self._fixBarePycs()

def _extractPyz(self, name):
dirName = name + '_extracted'
if not os.path.exists(dirName):
os.mkdir(dirName)

with open(name, 'rb') as f:
pyzMagic = f.read(4)
if pyzMagic != b'PYZ\0':
print('[!] Not a valid PYZ, saving raw')
self._writeRawData(name + ".raw", pyzMagic + f.read())
return

pyzPycMagic = f.read(4)
if self.pycMagic == b'\0' * 4:
self.pycMagic = pyzPycMagic
elif self.pycMagic != pyzPycMagic:
self.pycMagic = pyzPycMagic
print('[!] Warning: pyc magic changed inside PYZ')

if self.pymaj != sys.version_info.major or self.pymin != sys.version_info.minor:
print('[!] Python version mismatch, skipping PYZ extraction')
self._writeRawData(name + ".raw", f.read())
return

try:
(tocPosition,) = struct.unpack('!i', f.read(4))
f.seek(tocPosition, os.SEEK_SET)
toc = marshal.load(f)
except:
print('[!] Unmarshalling failed, saving raw PYZ')
f.seek(0)
self._writeRawData(name + ".raw", f.read())
return

if isinstance(toc, list):
toc = dict(toc)

for key in toc.keys():
ispkg, pos, length = toc[key]
f.seek(pos, os.SEEK_SET)
fileName = key
try:
fileName = fileName.decode('utf-8')
except:
pass
fileName = fileName.replace('..', '__').replace('.', os.path.sep)
if ispkg == 1:
filePath = os.path.join(dirName, fileName, '__init__.pyc')
else:
filePath = os.path.join(dirName, fileName + '.pyc')
fileDir = os.path.dirname(filePath)
if not os.path.exists(fileDir):
os.makedirs(fileDir)
try:
data = f.read(length)
data = zlib.decompress(data)
self._writePyc(filePath, data)
except:
print('[!] Error decompressing {0}, saving raw'.format(filePath))
self._writeRawData(filePath + ".raw", data)


def main():
if len(sys.argv) < 2:
print('[+] Usage: python exp.py <filename>')
return

arch = PyInstArchive(sys.argv[1])
if not arch.open():
return
if not arch.checkFile():
arch.close()
return
if not arch.getCArchiveInfo():
arch.close()
return
arch.parseTOC()
arch.extractFiles()
arch.close()
print('[+] Extraction finished. Check the "_extracted" folder for files.')


if __name__ == '__main__':
main()

提取出来的.raw文件就是没有解包成功部分的初始内容

img

用ida看exe文件的加密方式,sub_1400011F0函数里就是魔改的地方

解密过程:检查开头“swdd”,去除头, 每 16 字节用 key[j] = 0xAA 异或 ,剩余的逐字节xor, 最后zlib解压

img

脚本提取先前提取出为解包的CreackMe&&FxxMe.raw文件处理得到pyc文件

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
import zlib

key_16 = bytes([0xAA]*16)

def decrypt_raw(data: bytes) -> bytes:
# 检查是否有 'swdd' 头
if data.startswith(b'swdd'):
enc = data[4:]
else:
enc = data

dec = bytearray()
length = len(enc)
i = 0


while i + 16 <= length:
block = enc[i:i+16]
dec.extend(bytes([block[j] ^ key_16[j] for j in range(16)]))
i += 16

while i < length:
dec.append(enc[i] ^ 0xAA)
i += 1

return zlib.decompress(dec)

if __name__ == "__main__":
raw_file = "CreackMe&&FxxMe.raw"
pyc_file = "CreackMe&&FxxMe.pyc"

with open(raw_file, "rb") as f:
raw_data = f.read()

decrypted_data = decrypt_raw(raw_data)

pyc_header = b'\x55\x0d\x0d\x0a' + b'\x00'*12

with open(pyc_file, "wb") as f:
f.write(pyc_header)
f.write(decrypted_data)

print(f"[+] 解密完成,生成 {pyc_file}")

反编译得到主要加密内容

img

rc4加密

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
def rc4(data: bytes, key: bytes) -> bytes:
S = list(range(256))
j = 0
key_length = len(key)
for i in range(256):
j = (j + S[i] + key[i % key_length]) % 256
S[i], S[j] = S[j], S[i]
i = j = 0
out = bytearray()
for byte in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) % 256]
out.append(byte ^ K)
return bytes(out)
target_hex = 'd29b81e136efc517c2967b863f584baf4b82f710f8869f5a56185cb22a9a25fc'
ciphertext = bytes.fromhex(target_hex)

key = b'NSSCTF'

plaintext = rc4(ciphertext, key)

try:
flag_content = plaintext.decode('utf-8')
except UnicodeDecodeError:
flag_content = plaintext.hex()

print(f"NSSCTF{{{flag_content}}}")

NSSCTF{4984aa7eeb8c7fa0709832e364e03989}

Mobile

我是谁?!

看主函数得到flag的逻辑,这里用来验证账号是否有权限,如果有权限就通过 PR.showCND() 方法调用 stringFromJNI()

img

stringFromJNI()flag 的来源

主动调用 PR pr = new PR(); pr.stringFromJNI() 就能直接拿到 flag。

img

这里直接写hook脚本

1
2
3
4
5
6
7
8
9
10
Java.perform(function () {
try {
var PR = Java.use("com.example.nss_4th.ad.PR");
var pr = PR.$new(); // 创建一个 PR 对象
var flag = pr.stringFromJNI(); // 调用 JNI 方法
console.log("[*] FLAG = " + flag);
} catch (e) {
console.log("[!] 调用失败: " + e);
}
});

img

NSSCTF{Y3s!NSS_hAs_r34ched_iTs_4th_5nNiv4rsar9}


NSSCTF-4th
https://j1nxem-o.github.io/2025/08/26/NSSCTF-4th/
作者
J1NXEM
发布于
2025年8月26日
更新于
2025年8月26日
许可协议