NSSCTF 4th reverse wp
checkit
java层没有内容,看so层

unsigned __int8 *__fastcall exec(__int64 p_code, unsigned __int8 *a2)
{
int v2; // r9d
int v3; // edx
int v4; // eax
int v5; // eax
int v6; // ecx
unsigned __int8 *result; // rax
char v8; // [rsp+0h] [rbp-E0h]
int n34; // [rsp+Ch] [rbp-D4h]
__memset_chk(Memory_allocation_error, 0, 200, 200);
*(a2 + 4) = 0;
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 ) // a=c
{
while ( 1 ) // a=cmp[i]
{
while ( 1 ) // pop
{
while ( 1 ) // push
{
while ( 1 ) // xor
{
while ( 1 ) // div
{
while ( 1 ) // *
{
while ( 1 ) // sub
{
while ( 1 ) // add
{
while ( 1 )
{
n34 = *(p_code + 4LL * *(a2 + 2));
if ( n34 != 34 )
break;
*a2 += a2[3];
++*(a2 + 2);
}
if ( n34 != 35 )
break;
*a2 -= a2[3];
++*(a2 + 2);
}
if ( n34 != 36 )
break;
*a2 *= a2[3];
++*(a2 + 2);
}
if ( n34 != 37 )
break;
if ( !a2[3] )
return __strlcpy_chk(Memory_allocation_error, "Error: Division by zero", 200, 200);
v8 = a2;
*a2 /= a2[3];
++*(a2 + 2);
}
if ( n34 != 38 )
break;
*a2 ^= a2[3];
++*(a2 + 2);
}
if ( n34 != 39 )
break;
if ( *(a2 + 3) >= 500 )
return __strlcpy_chk(Memory_allocation_error, "Error: Stack overflow", 200, 200);
v3 = *a2;
v4 = *(a2 + 3);
*(a2 + 3) = v4 + 1;
stack[v4] = v3;
++*(a2 + 2);
}
if ( n34 != 40 )
break;
if ( *(a2 + 3) <= 0 )
return __strlcpy_chk(Memory_allocation_error, "Error: Stack underflow", 200, 200);
v5 = *(a2 + 3) - 1;
*(a2 + 3) = v5;
*a2 = stack[v5];
++*(a2 + 2);
}
if ( n34 != 41 )
break;
if ( a2[1] >= 0x33uLL )
*a2 = 0;
else
*a2 = *(__emutls_get_address(&unk_45B0) + a2[1]);
++*(a2 + 2);
}
if ( n34 != 42 )
break;
*a2 = a2[3];
++*(a2 + 2);
}
if ( n34 != 43 )
break;
a2[1] = *a2;
++*(a2 + 2);
}
if ( n34 != 44 )
break;
a2[2] = *a2;
++*(a2 + 2);
}
if ( n34 != 45 )
break;
a2[3] = *a2;
++*(a2 + 2);
}
if ( n34 != 46 )
break;
*a2 = *(p_code + 4LL * (*(a2 + 2) + 1));
*(a2 + 2) += 2;
}
if ( n34 != 47 )
break;
*a2 = a2[1];
++*(a2 + 2);
}
if ( n34 != 48 )
break;
if ( a2[1] >= 0x32u )
{
result = a2;
a2[4] = 1;
return result;
}
*a2 = cmp_data[a2[1]];
++*(a2 + 2);
}
if ( n34 != 49 )
break;
++*a2;
++*(a2 + 2);
}
if ( n34 != 50 )
break;
if ( --a2[2] )
v6 = *(a2 + 2) - *(p_code + 4LL * (*(a2 + 2) + 1));
else
v6 = *(a2 + 2) + 2;
*(a2 + 2) = v6;
}
if ( n34 != 51 )
break;
++*(a2 + 4);
if ( a2[1] >= 0x32u )
{
result = a2;
a2[4] = 1;
return result;
}
if ( !a2[4] && *a2 != a2[3] )
a2[4] = 1;
--a2[1];
++*(a2 + 2);
}
if ( n34 != 52 )
break;
++*(a2 + 2);
}
if ( n34 != 255 )
return sub_18A0(Memory_allocation_error, 200, 200, "Unknown opcode: %d", n34, v2, v8);
if ( a2[4] || *(a2 + 4) != 50 )
return __strlcpy_chk(Memory_allocation_error, &unk_C61, 200, 200);
return __strlcpy_chk(Memory_allocation_error, "oh!You are right!", 200, 200);
}
vm代码

只需要分析opcode了
https://imconfident11.github.io/2025/08/25/NSSCTF2025/
这份wp提到了同构vm

分析出来是分奇偶加密
#include<stdio.h>
int main(){
int cmp[]={0x1A,0x1E,0x1D,0xE,0x1C,0x13,0x25,0xE,0x78,0x3B,0x31,0x3F,0x68,0x45,0x23,0x3D,0xF,0x45,0x37,0x3A,0x3A,0x70,0x7,0x81,0x1A,0x2A,0x3D,0x7E,0x7D,0x3C,0x9,0x82,0x39,0x2A,0xE,0x7E,0x9,0x32,0x19,0x81,0xC,0x2A,0x68,0x45,0x9,0x43,0x3B,0x70,0x4F,0x4C};
for(int i=0;i<50;i++){
if(i%2){
printf("%c",(cmp[i] - 14) ^ 67);
}else{
printf("%c",(cmp[i] ^ 83) + 5);
}
}
}
CrackMe&F***Me

首先用pyinstaller无法解包,所以通过与其他文件比较发现文件尾部有修改


进行修改

关键的pyc文件没有解压出来

字符串提示

进去有自定义的解密
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 # For pyinstaller 2.0
PYINST21_COOKIE_SIZE = 24 + 64 # For pyinstaller 2.1+
MAGIC = b'MEI\014\013\012\013\016' # Magic number which identifies pyinstaller
def __init__(self, path):
self.filePath = path
self.pycMagic = b'\0' * 4
self.barePycList = [] # List of pyc's whose headers have to be fixed
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 # pyinstaller 2.1+
else:
self.pyinstVer = 20 # pyinstaller 2.0
print('[+] Pyinstaller version: 2.0')
return True
def getCArchiveInfo(self):
try:
if self.pyinstVer == 20:
self.fPtr.seek(self.cookiePos, os.SEEK_SET)
# Read CArchive cookie
(magic, lengthofPackage, toc, tocLen, pyver) = \
struct.unpack('!8siiii', self.fPtr.read(self.PYINST20_COOKIE_SIZE))
elif self.pyinstVer == 21:
self.fPtr.seek(self.cookiePos, os.SEEK_SET)
# Read CArchive cookie
(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))
# Additional data after the cookie
tailBytes = self.fileSize - self.cookiePos - (self.PYINST20_COOKIE_SIZE if self.pyinstVer == 20 else self.PYINST21_COOKIE_SIZE)
# Overlay is the data appended at the end of the PE
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):
# Go to the table of contents
self.fPtr.seek(self.tableOfContentsPos, os.SEEK_SET)
self.tocList = []
parsedLen = 0
# Parse table of contents
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:
newName = str(uniquename())
print('[!] Warning: File name {0} contains invalid bytes. Using random name {1}'.format(name, newName))
name = newName
# Prevent writing outside the extraction directory
if name.startswith("/"):
name = name.lstrip("/")
if len(name) == 0:
name = str(uniquename())
print('[!] Warning: Found an unamed file in CArchive. Using random name {0}'.format(name))
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 _decrypt_swdd_data(self, encrypted_data):
"""
解密PyInstaller的swdd格式数据
"""
if not encrypted_data.startswith(b'swdd'):
return encrypted_data
print('[+] 检测到swdd加密数据,进行解密...')
# 移除4字节的swdd头部
data_without_header = encrypted_data[4:]
# 使用XOR 0xAA进行解密
decrypted_data = bytes(byte ^ 0xAA for byte in data_without_header)
return decrypted_data
def _decompress_with_retry(self, compressed_data, filename=""):
"""
尝试多种方式解压数据
"""
# 首先尝试标准解压
try:
return zlib.decompress(compressed_data)
except zlib.error as e:
pass
# 尝试不同的窗口大小
window_sizes = [15, -15, 31, -31, 47, -47]
for window_bits in window_sizes:
try:
decompressor = zlib.decompressobj(window_bits)
result = decompressor.decompress(compressed_data)
result += decompressor.flush()
print(f'[+] 使用窗口大小 {window_bits} 成功解压 {filename}')
return result
except zlib.error:
continue
# 尝试去掉可能的头部
for skip_bytes in [2, 4, 6, 8, 12, 16]:
try:
if len(compressed_data) > skip_bytes:
result = zlib.decompress(compressed_data[skip_bytes:])
print(f'[+] 跳过 {skip_bytes} 字节后成功解压 {filename}')
return result
except zlib.error:
continue
return None
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): # Check if path exists, create if not
os.makedirs(nmDir)
with open(nm, 'wb') as f:
f.write(data)
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)
# 添加swdd解密处理
if data.startswith(b'swdd'):
print(f'[+] 解密: {entry.name}')
data = self._decrypt_swdd_data(data)
if entry.cmprsFlag == 1:
try:
data = zlib.decompress(data)
except zlib.error:
print(f'[!] Error : Failed to decompress {entry.name}, 尝试其他方法...')
# 尝试其他解压方法
data = self._decompress_with_retry(data, entry.name)
if data is None:
print(f'[!] Error : 所有解压方法都失败 {entry.name}')
continue
# 检查解压后的大小是否匹配
if len(data) != entry.uncmprsdDataSize:
print(f'[!] Warning: 解压后大小不匹配 {entry.name} (预期: {entry.uncmprsdDataSize}, 实际: {len(data)})')
if entry.typeCmprsData == b'd' or entry.typeCmprsData == b'o':
# d -> ARCHIVE_ITEM_DEPENDENCY
# o -> ARCHIVE_ITEM_RUNTIME_OPTION
# These are runtime options, not files
continue
basePath = os.path.dirname(entry.name)
if basePath != '':
# Check if path exists, create if not
if not os.path.exists(basePath):
os.makedirs(basePath)
if entry.typeCmprsData == b's':
# s -> ARCHIVE_ITEM_PYSOURCE
# Entry point are expected to be python scripts
print('[+] Possible entry point: {0}.pyc'.format(entry.name))
if self.pycMagic == b'\0' * 4:
# if we don't have the pyc header yet, fix them in a later pass
self.barePycList.append(entry.name + '.pyc')
self._writePyc(entry.name + '.pyc', data)
elif entry.typeCmprsData == b'M' or entry.typeCmprsData == b'm':
# M -> ARCHIVE_ITEM_PYPACKAGE
# m -> ARCHIVE_ITEM_PYMODULE
# packages and modules are pyc files with their header intact
# From PyInstaller 5.3 and above pyc headers are no longer stored
# https://github.com/pyinstaller/pyinstaller/commit/a97fdf
if data[2:4] == b'\r\n':
# < pyinstaller 5.3
if self.pycMagic == b'\0' * 4:
self.pycMagic = data[0:4]
self._writeRawData(entry.name + '.pyc', data)
else:
# >= pyinstaller 5.3
if self.pycMagic == b'\0' * 4:
# if we don't have the pyc header yet, fix them in a later pass
self.barePycList.append(entry.name + '.pyc')
self._writePyc(entry.name + '.pyc', data)
else:
self._writeRawData(entry.name, data)
if entry.typeCmprsData == b'z' or entry.typeCmprsData == b'Z':
self._extractPyz(entry.name)
# Fix bare pyc's if any
self._fixBarePycs()
def _fixBarePycs(self):
for pycFile in self.barePycList:
with open(pycFile, 'r+b') as pycFile:
# Overwrite the first four bytes
pycFile.write(self.pycMagic)
def _writePyc(self, filename, data):
with open(filename, 'wb') as pycFile:
pycFile.write(self.pycMagic) # pyc magic
if self.pymaj >= 3 and self.pymin >= 7: # PEP 552 -- Deterministic pycs
pycFile.write(b'\0' * 4) # Bitfield
pycFile.write(b'\0' * 8) # (Timestamp + size) || hash
else:
pycFile.write(b'\0' * 4) # Timestamp
if self.pymaj >= 3 and self.pymin >= 3:
pycFile.write(b'\0' * 4) # Size parameter added in Python 3.3
pycFile.write(data)
def _extractPyz(self, name):
dirName = name + '_extracted'
# Create a directory for the contents of the pyz
if not os.path.exists(dirName):
os.mkdir(dirName)
with open(name, 'rb') as f:
pyzMagic = f.read(4)
assert pyzMagic == b'PYZ\0' # Sanity Check
pyzPycMagic = f.read(4) # Python magic value
if self.pycMagic == b'\0' * 4:
self.pycMagic = pyzPycMagic
elif self.pycMagic != pyzPycMagic:
self.pycMagic = pyzPycMagic
print('[!] Warning: pyc magic of files inside PYZ archive are different from those in CArchive')
# Skip PYZ extraction if not running under the same python version
if self.pymaj != sys.version_info.major or self.pymin != sys.version_info.minor:
print('[!] Warning: This script is running in a different Python version than the one used to build the executable.')
print('[!] Please run this script in Python {0}.{1} to prevent extraction errors during unmarshalling'.format(self.pymaj, self.pymin))
print('[!] Skipping pyz extraction')
return
(tocPosition, ) = struct.unpack('!i', f.read(4))
f.seek(tocPosition, os.SEEK_SET)
try:
toc = marshal.load(f)
except:
print('[!] Unmarshalling FAILED. Cannot extract {0}. Extracting remaining files.'.format(name))
return
print('[+] Found {0} files in PYZ archive'.format(len(toc)))
# From pyinstaller 3.1+ toc is a list of tuples
if type(toc) == list:
toc = dict(toc)
for key in toc.keys():
(ispkg, pos, length) = toc[key]
f.seek(pos, os.SEEK_SET)
fileName = key
try:
# for Python > 3.3 some keys are bytes object some are str object
fileName = fileName.decode('utf-8')
except:
pass
# Prevent writing outside dirName
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)
# 添加swdd解密处理
if data.startswith(b'swdd'):
data = self._decrypt_swdd_data(data)
data = zlib.decompress(data)
except:
print('[!] Error: Failed to decompress {0}, probably encrypted. Extracting as is.'.format(filePath))
open(filePath + '.encrypted', 'wb').write(data)
else:
self._writePyc(filePath, data)
def main():
if len(sys.argv) < 2:
print('[+] Usage: pyinstxtractor.py <filename>')
else:
arch = PyInstArchive(sys.argv[1])
if arch.open():
if arch.checkFile():
if arch.getCArchiveInfo():
arch.parseTOC()
arch.extractFiles()
arch.close()
print('[+] Successfully extracted pyinstaller archive: {0}'.format(sys.argv[1]))
print('')
print('You can now use a python decompiler on the pyc files within the extracted directory')
return
arch.close()
if __name__ == '__main__':
main()
ai给出用pyinstaller.py魔改py文件

成功解压
用uncompyle6反编译pyc文件
import binascii
def arc_cipher(data: bytes, key: bytes) -> bytes:
"""��ʵ�� RC4 ����/����"""
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]
else:
i = 0
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)
else:
return bytes(out)
target_hex = "d29b81e136efc517c2967b863f584baf4b82f710f8869f5a56185cb22a9a25fc"
target_bytes = bytes.fromhex(target_hex)
key = b'NSSCTF'
user_input = input("Enter input: ").encode()
encrypted = arc_cipher(user_input, key)
if encrypted == target_bytes:
print(f"Your are right, flag is NSSCTF{{{user_input.decode()}}}")
else:
print("Wrong input")



浙公网安备 33010602011771号