NSSCTF 4th reverse wp

checkit

java层没有内容,看so层

image

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代码

image

只需要分析opcode了

https://imconfident11.github.io/2025/08/25/NSSCTF2025/

这份wp提到了同构vm

image

分析出来是分奇偶加密

#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

image

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

image

image

进行修改

image

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

image

字符串提示

image

进去有自定义的解密

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文件

image

成功解压

用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")

image

image

posted @ 2025-09-01 22:16  zzz222666  阅读(61)  评论(0)    收藏  举报