Compare commits

..

No commits in common. "main" and "test" have entirely different histories.

10 changed files with 557 additions and 754 deletions

View file

@ -1,7 +1,5 @@
# evbunpack # evbunpack
[![Windows Build](https://github.com/mos9527/evbunpack/actions/workflows/build-and-publish.yml/badge.svg)](https://github.com/mos9527/evbunpack/blob/main/.github/workflows/build-and-publish.yml) [![Releases](https://img.shields.io/github/downloads/mos9527/evbunpack/total.svg)](https://GitHub.com/mos9527/evbunpack/releases/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [Enigma Virtual Box](https://enigmaprotector.com/) unpacker
[Enigma Virtual Box](https://enigmaprotector.com/en/downloads/changelogenigmavb.html) unpacker
## Features ## Features
- Executable unpacking - Executable unpacking
@ -17,7 +15,6 @@
| Packer Version | Notes | Unpack with Flags | | Packer Version | Notes | Unpack with Flags |
| - | - | - | | - | - | - |
| 11.00 | Automatically tested in CI for x86/x64 binaries. | `-pe 10_70` |
| 10.70 | Automatically tested in CI for x86/x64 binaries. | `-pe 10_70` | | 10.70 | Automatically tested in CI for x86/x64 binaries. | `-pe 10_70` |
| 9.70 | Automatically tested in CI for x86/x64 binaries. | `-pe 9_70` | | 9.70 | Automatically tested in CI for x86/x64 binaries. | `-pe 9_70` |
| 7.80 | Automatically tested in CI for x86/x64 binaries | `-pe 7_80 --legacy-fs ` | | 7.80 | Automatically tested in CI for x86/x64 binaries | `-pe 7_80 --legacy-fs ` |

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -- #-*- coding: utf-8 --
__version__ = "0.2.6" __version__ = '0.2.2'
__author__ = "mos9527" __author__ = 'mos9527'

View file

@ -1,563 +1,386 @@
# -*- coding: utf-8 -*- #-*- coding: utf-8 -*-
# Copy # Copy
from itertools import dropwhile from itertools import dropwhile
import struct, os, array, sys, logging import struct,os,array,sys, logging
from argparse import ArgumentParser from argparse import ArgumentParser
from mmap import mmap, ACCESS_READ from mmap import mmap,ACCESS_READ
from io import BytesIO from io import BytesIO
from aplib import decompress from evbunpack.aplib import decompress
from evbunpack.const import * from evbunpack.const import *
from evbunpack import __version__ from evbunpack import __version__
logger = logging.getLogger('evbunpack')
logger = logging.getLogger("evbunpack") FOLDER_ALTNAMES = {
'%DEFAULT FOLDER%' : ''
}
FOLDER_ALTNAMES = {"%DEFAULT FOLDER%": ""} def write_bytes(fd,out_fd,size,chunk_sizes=None,chunk_process=None,default_chunksize=65536,desc='Extracting...'):
bytes_read = 0
def write_bytes(
fd,
out_fd,
size,
chunk_sizes=None,
chunk_process=None,
default_chunksize=65536,
desc="Extracting...",
):
bytes_read = 0
bytes_wrote = 0 bytes_wrote = 0
inital_offset = fd.tell() inital_offset = fd.tell()
stat_mxlen = 0
while bytes_read < size: while bytes_read < size:
stat = "%s: %.2f%% total=%8xh read=%8xh \r" % ( sys.stderr.write('%s: total=%8xh read=%8xh\r' % (desc,size,bytes_read))
desc, chunk_size = next(chunk_sizes) if chunk_sizes else default_chunksize
100 * bytes_read / size, size_to_read = min(chunk_size,size - (fd.tell() - inital_offset))
size,
bytes_read,
)
stat_mxlen = max(stat_mxlen, len(stat))
sys.stderr.write(stat)
chunk_size = next(chunk_sizes) if chunk_sizes else default_chunksize
size_to_read = min(chunk_size, size - (fd.tell() - inital_offset))
chunk = fd.read(size_to_read) chunk = fd.read(size_to_read)
bytes_read += len(chunk) bytes_read += len(chunk)
chunk = chunk if not chunk_process else chunk_process(chunk) chunk = chunk if not chunk_process else chunk_process(chunk)
bytes_wrote += out_fd.write(chunk) bytes_wrote += out_fd.write(chunk)
sys.stderr.write(" " * stat_mxlen + "\r") sys.stderr.write('\n')
return bytes_wrote return bytes_wrote
def get_size_by_struct(struct_): def get_size_by_struct(struct_):
fmt, desc = make_format_by_struct(struct_) fmt , desc = make_format_by_struct(struct_)
return struct.calcsize(fmt) return struct.calcsize(fmt)
def read_bytes_by_struct(src,struct_):
def read_bytes_by_struct(src, struct_):
return src.read(get_size_by_struct(struct_)) return src.read(get_size_by_struct(struct_))
def make_format_by_struct(struct, *args): def make_format_by_struct(struct, *args):
fmt, desc = zip(*filter(lambda p: isinstance(p, tuple), struct)) fmt, desc = zip(*filter(lambda p:isinstance(p, tuple),struct))
fmt = ("<" if type(struct[-1]) != str else struct[-1]) + ("".join(fmt)) % args fmt = ('<' if type(struct[-1]) != str else struct[-1]) + ("".join(fmt)) % args
return fmt, desc return fmt,desc
def pack(structure, *args):
fmt, desc = make_format_by_struct(structure)
return struct.pack(fmt, *args)
def pack(structure,*args):
fmt,desc = make_format_by_struct(structure)
return struct.pack(fmt,*args)
def unpack(structure, buffer, *args, **extra): def unpack(structure, buffer, *args, **extra):
"""Unpack buffer by structure given""" '''Unpack buffer by structure given'''
fmt, desc = make_format_by_struct(structure, *args) fmt,desc = make_format_by_struct(structure,*args)
unpacked = struct.unpack_from(fmt, buffer, 0) unpacked = struct.unpack_from(fmt, buffer, 0)
return {**{k: v for k, v in zip(desc, unpacked) if k}, **extra} return {**{k: v for k, v in zip(desc, unpacked) if k},**extra}
def read_named_node(src):
def read_named_node(src): blkFilename = bytearray()
blkFilename = bytearray()
p = src.read(2) p = src.read(2)
while p[0] != 0 or p[1] != 0: while (p[0]!=0 or p[1]!=0):
blkFilename.extend(p) blkFilename.extend(p)
p = src.read(2) p = src.read(2)
block = blkFilename + src.read(1) block = blkFilename + src.read(1)
return unpack(EVB_NODE_NAMED, block, len(blkFilename), offset=src.tell()) return unpack(EVB_NODE_NAMED, block, len(blkFilename),offset=src.tell())
def read_header_node(src):
return unpack(EVB_HEADER_NODE,read_bytes_by_struct(src,EVB_HEADER_NODE))
def read_header_node(src): def read_optional_legacy_pe_file_node(src):
return unpack(EVB_HEADER_NODE, read_bytes_by_struct(src, EVB_HEADER_NODE)) return unpack(EVB_NODE_OPTIONAL_PE_FILE, read_bytes_by_struct(src,EVB_NODE_OPTIONAL_PE_FILE))
def read_optional_legacy_pe_file_node(src):
return unpack(
EVB_NODE_OPTIONAL_PE_FILE, read_bytes_by_struct(src, EVB_NODE_OPTIONAL_PE_FILE)
)
def read_optional_file_node(src):
return unpack(
EVB_NODE_OPTIONAL_FILE, read_bytes_by_struct(src, EVB_NODE_OPTIONAL_FILE)
)
def read_optional_file_node(src):
return unpack(EVB_NODE_OPTIONAL_FILE, read_bytes_by_struct(src,EVB_NODE_OPTIONAL_FILE))
def read_chunk_block(src): def read_chunk_block(src):
return unpack(EVB_CHUNK_BLOCK, read_bytes_by_struct(src, EVB_CHUNK_BLOCK)) return unpack(EVB_CHUNK_BLOCK, read_bytes_by_struct(src,EVB_CHUNK_BLOCK))
def read_pack_header(src):
return unpack(EVB_PACK_HEADER, read_bytes_by_struct(src,EVB_PACK_HEADER))
def read_pack_header(src): def read_main_node(src):
return unpack(EVB_PACK_HEADER, read_bytes_by_struct(src, EVB_PACK_HEADER)) return unpack(EVB_NODE_MAIN, read_bytes_by_struct(src,EVB_NODE_MAIN))
def read_main_node(src):
return unpack(EVB_NODE_MAIN, read_bytes_by_struct(src, EVB_NODE_MAIN))
def pe_external_tree(fd): def pe_external_tree(fd):
# Before calling, make sure cursor is already at where # Before calling, make sure cursor is already at where
# the following bytes are b`EVB\x00` # the following bytes are b`EVB\x00`
# Both PE and external packages work with this method # Both PE and external packages work with this method
hdr = read_pack_header(fd) hdr = read_pack_header(fd)
assert hdr["signature"] == EVB_MAGIC, "Invalid signature" assert hdr['signature'] == EVB_MAGIC, "Invalid signature"
main_node = read_main_node(fd) main_node = read_main_node(fd)
abs_offset = ( abs_offset = fd.tell() + main_node['size'] - 12 # offset from the head of the stream
fd.tell() + main_node["size"] - 12 fd.seek(-1,1)
) # offset from the head of the stream
fd.seek(-1, 1)
yield main_node yield main_node
max_object_count = 0 max_object_count = 0
current_object_count = 0 current_object_count = 0
while True: while True:
try: try:
header_node = read_header_node(fd) header_node = read_header_node(fd)
named_node = read_named_node(fd) named_node = read_named_node(fd)
except struct.error: except struct.error:
return # Potential EOF exception return # Potential EOF exception
if named_node["type"] == NODE_TYPE_FILE: if named_node['type'] == NODE_TYPE_FILE:
optional_node = read_optional_file_node(fd) optional_node = read_optional_file_node(fd)
optional_node["offset"] = abs_offset optional_node['offset'] = abs_offset
abs_offset += optional_node["stored_size"] abs_offset += optional_node['stored_size']
current_object_count += 1 current_object_count += 1
elif named_node["type"] == NODE_TYPE_FOLDER: elif named_node['type'] == NODE_TYPE_FOLDER:
optional_node = {} optional_node = {}
fd.seek(25, 1) fd.seek(25,1)
max_object_count += header_node["objects_count"] max_object_count += header_node['objects_count']
current_object_count += 1 current_object_count += 1
else: else:
return # assuming finished return # assuming finished
named_node["name"] = named_node["name"].decode("utf-16-le") named_node['name'] = named_node['name'].decode('utf-16-le')
yield {**header_node, **named_node, **optional_node} yield {**header_node,**named_node,**optional_node}
if current_object_count > max_object_count and max_object_count > 0: if current_object_count > max_object_count and max_object_count > 0:
return return
def legacy_pe_tree(fd): def legacy_pe_tree(fd):
# Older executables has their file table and content placed together # Older executables has their file table and content placed together
# Courtesy of evb-extractor! # Courtesy of evb-extractor!
hdr = read_pack_header(fd) hdr = read_pack_header(fd)
assert hdr["signature"] == EVB_MAGIC, "Invalid signature" assert hdr['signature'] == EVB_MAGIC, "Invalid signature"
seek_origin = 0 seek_origin = 0
max_object_count = 0 max_object_count = 0
current_object_count = 0 current_object_count = 0
while True: while True:
seek_origin = fd.tell() seek_origin = fd.tell()
try: try:
header_node = read_header_node(fd) header_node = read_header_node(fd)
named_node = read_named_node(fd) named_node = read_named_node(fd)
except struct.error: except struct.error:
return # Potential EOF exception return # Potential EOF exception
if named_node["type"] == NODE_TYPE_FILE: if named_node['type'] == NODE_TYPE_FILE:
fd.seek( fd.seek(seek_origin + header_node['size'] + 4 - get_size_by_struct(EVB_NODE_OPTIONAL_PE_FILE))
seek_origin optional_node = read_optional_legacy_pe_file_node(fd)
+ header_node["size"] optional_node['offset'] = fd.tell()
+ 4 fd.seek(optional_node['stored_size'],1)
- get_size_by_struct(EVB_NODE_OPTIONAL_PE_FILE)
)
optional_node = read_optional_legacy_pe_file_node(fd)
optional_node["offset"] = fd.tell()
fd.seek(optional_node["stored_size"], 1)
current_object_count += 1 current_object_count += 1
elif named_node["type"] == NODE_TYPE_FOLDER: elif named_node['type'] == NODE_TYPE_FOLDER:
optional_node = {} optional_node = {}
fd.seek(seek_origin + header_node["size"] + 4) fd.seek(seek_origin + header_node['size'] + 4)
max_object_count += header_node["objects_count"] max_object_count += header_node['objects_count']
current_object_count += 1 current_object_count += 1
elif named_node["type"] == NODE_TYPE_MAIN: elif named_node['type'] == NODE_TYPE_MAIN:
optional_node = {} optional_node = {}
fd.seek(seek_origin + header_node["size"] + 4) fd.seek(seek_origin + header_node['size'] + 4)
else: else:
return # assuming finished return # assuming finished
named_node["name"] = named_node["name"].decode("utf-16-le") named_node['name'] = named_node['name'].decode('utf-16-le')
yield {**header_node, **named_node, **optional_node} yield {**header_node,**named_node,**optional_node}
if current_object_count > max_object_count and max_object_count > 0: if current_object_count > max_object_count and max_object_count > 0:
return return
def completed(generator): def completed(generator):
# Complete building the tree before we'd read the file # Complete building the tree before we'd read the file
for item in list(generator): for item in list(generator):
yield item yield item
def process_file_node(fd,path,node):
def process_file_node(fd, path, node): with open(path,'wb') as output:
with open(path, "wb") as output: rsize = node['original_size']
rsize = node["original_size"] ssize = node['stored_size']
ssize = node["stored_size"] offset = node['offset']
offset = node["offset"]
fd.seek(offset) fd.seek(offset)
if rsize != ssize: # Compression detected if rsize != ssize: # Compression detected
chunks_blk = read_chunk_block(fd) chunks_blk = read_chunk_block(fd)
blkChunkData = fd.read( blkChunkData = fd.read(chunks_blk['size'] - get_size_by_struct(EVB_CHUNK_BLOCK))
chunks_blk["size"] - get_size_by_struct(EVB_CHUNK_BLOCK) arrChunkData = (val for idx,val in enumerate(array.array('I',blkChunkData)) if idx % 3 == 0)
)
arrChunkData = (
val
for idx, val in enumerate(array.array("I", blkChunkData))
if idx % 3 == 0
)
# Chunk data comes in 12-bytes rotation: Chunk size (4bytes), Total size (4bytes), Padding (4bytes) # Chunk data comes in 12-bytes rotation: Chunk size (4bytes), Total size (4bytes), Padding (4bytes)
# But with the last Chunk size, it does not come with Total size or Padding... # But with the last Chunk size, it does not come with Total size or Padding...
# Thus filtering only every 3rd elements works. Which should always give us Chunk size # Thus filtering only every 3rd elements works. Which should always give us Chunk size
# Even if the last 8 bytes is missing # Even if the last 8 bytes is missing
wsize = write_bytes( wsize = write_bytes(
fd, fd,output,
output, size=ssize - chunks_blk['size'],
size=ssize - chunks_blk["size"],
chunk_sizes=arrChunkData, chunk_sizes=arrChunkData,
chunk_process=decompress, chunk_process=decompress,
desc="Decompressing File [offset=0x%x, offsetBlk=0x%x]" desc='Decompressing File [offset=0x%x, offsetBlk=0x%x]' % (fd.tell(),chunks_blk['size'])
% (fd.tell(), chunks_blk["size"]),
) )
assert wsize == rsize, "Incorrect size" assert wsize == rsize,"Incorrect size"
else: else:
write_bytes( write_bytes(
fd, fd,output,
output,
size=ssize, size=ssize,
desc="Writing File [size=0x%x, offset=0x%x]" % (ssize, offset), desc='Writing File [size=0x%x, offset=0x%x]' % (ssize,offset)
) )
def restore_pe(input_file : str, output_file : str, pe_variant : str):
def restore_pe(input_file: str, output_file: str, pe_variant: str):
warnings_issued = 0 warnings_issued = 0
from pefile import PE, OPTIONAL_HEADER_MAGIC_PE_PLUS from pefile import PE,OPTIONAL_HEADER_MAGIC_PE_PLUS
logger.debug('Loading PE...')
logger.debug("Loading PE...") pe = PE(input_file,fast_load=True)
pe = PE(input_file, fast_load=True)
arch_64 = pe.PE_TYPE == OPTIONAL_HEADER_MAGIC_PE_PLUS arch_64 = pe.PE_TYPE == OPTIONAL_HEADER_MAGIC_PE_PLUS
logger.debug("PE loaded. Arch=%s" % ("x64" if arch_64 else "x86")) logger.debug('PE loaded. Arch=%s' % ('x64' if arch_64 else 'x86'))
pe.__data__ = bytearray( pe.__data__ = bytearray(pe.__data__) # This allows us to apply slicing on the PE data
pe.__data__ # Helpers
) # This allows us to apply slicing on the PE data find_section = lambda name:next(filter(lambda x:name in x.Name,pe.sections))
# Helpers find_data_directory = lambda name:next(filter(lambda x:name in x.name,pe.OPTIONAL_HEADER.DATA_DIRECTORY))
find_section = lambda name: next( search_pattern_in_sections = lambda pattern:next(dropwhile(lambda x: x[1] == -1, ((section, pe.__data__.find(pattern,section.PointerToRawData, section.PointerToRawData + section.SizeOfRawData)) for section in pe.sections)))
filter(lambda x: name in x.Name, pe.sections), None
)
find_data_directory = lambda name: next(
filter(lambda x: name in x.name, pe.OPTIONAL_HEADER.DATA_DIRECTORY)
)
search_pattern_in_sections = lambda pattern: next(
dropwhile(
lambda x: x[1] == -1,
(
(
section,
pe.__data__.find(
pattern,
section.PointerToRawData,
section.PointerToRawData + section.SizeOfRawData,
),
)
for section in pe.sections
),
)
)
# Data # Data
logger.info("Unpacking with variant: %s" % pe_variant) logger.info('Unpacking with variant: %s' % pe_variant)
enigma1 = find_section(b".enigma1") enigma1 = pe.__data__[find_section(b'.enigma1').PointerToRawData:]
assert (
enigma1
), "Cannot find .enigma1 section. The file is likely not packed with Enigma Virtual Box, or has obfuscated section names."
enigma1 = pe.__data__[enigma1.PointerToRawData :]
hdr = unpack(EVB_ENIGMA1_HEADER.get_struct(arch_64, pe_variant), enigma1) hdr = unpack(EVB_ENIGMA1_HEADER.get_struct(arch_64, pe_variant), enigma1)
# Restore section with built-in offsets. All these ADDRESSes are VAs # Restore section with built-in offsets. All these ADDRESSes are VAs
find_data_directory("IMPORT").VirtualAddress = hdr["IMPORT_ADDRESS"] find_data_directory('IMPORT').VirtualAddress = hdr['IMPORT_ADDRESS']
find_data_directory("IMPORT").Size = hdr["IMPORT_SIZE"] find_data_directory('IMPORT').Size = hdr['IMPORT_SIZE']
find_data_directory("RELOC").VirtualAddress = hdr["RELOC_ADDRESS"] find_data_directory('RELOC').VirtualAddress = hdr['RELOC_ADDRESS']
find_data_directory("RELOC").Size = hdr["RELOC_SIZE"] find_data_directory('RELOC').Size = hdr['RELOC_SIZE']
logger.debug( logger.debug('Import -> VA=0x%x Size=0x%x' % (hdr['IMPORT_ADDRESS'],hdr['IMPORT_SIZE']))
"Import -> VA=0x%x Size=0x%x" % (hdr["IMPORT_ADDRESS"], hdr["IMPORT_SIZE"]) logger.debug('Reloc -> VA=0x%x Size=0x%x' % (hdr['RELOC_ADDRESS'],hdr['RELOC_SIZE']))
) if hdr['RELOC_SIZE'] == 0 or hdr['IMPORT_SIZE'] == 0:
logger.debug(
"Reloc -> VA=0x%x Size=0x%x" % (hdr["RELOC_ADDRESS"], hdr["RELOC_SIZE"])
)
if hdr["RELOC_SIZE"] == 0 or hdr["IMPORT_SIZE"] == 0:
warnings_issued += 1 warnings_issued += 1
logger.warning( logger.warning('Import/Reloc table size is zero. This may indicate that the header is incorrectly parsed.')
"Import/Reloc table size is zero. This may indicate that the header is incorrectly parsed." logger.debug('Rebuilding Exception directory...')
)
logger.debug("Rebuilding Exception directory...")
# Rebuild the exception directory # Rebuild the exception directory
exception_dir = find_data_directory("EXCEPTION") exception_dir = find_data_directory('EXCEPTION')
exception_raw_ptr = pe.get_offset_from_rva(exception_dir.VirtualAddress) exception_raw_ptr = pe.get_offset_from_rva(exception_dir.VirtualAddress)
exception_data = pe.__data__[ exception_data = pe.__data__[exception_raw_ptr:exception_raw_ptr + exception_dir.Size]
exception_raw_ptr : exception_raw_ptr + exception_dir.Size
]
exception_struct = PE64_EXCEPTION if arch_64 else PE_EXCEPTION exception_struct = PE64_EXCEPTION if arch_64 else PE_EXCEPTION
exception_end = 0 exception_end = 0
for i in range(0, exception_dir.Size, get_size_by_struct(exception_struct)): for i in range(0,exception_dir.Size,get_size_by_struct(exception_struct)):
block = unpack(exception_struct, exception_data[i:]) block = unpack(exception_struct,exception_data[i:])
block["section"] = pe.get_section_by_rva(block["BEGIN_ADDRESS"]) block['section'] = pe.get_section_by_rva(block['BEGIN_ADDRESS'])
exception_end = i exception_end = i
if b".enigma" in block["section"].Name: if b'.enigma' in block['section'].Name:
break break
exception_data = exception_data[:exception_end] exception_data = exception_data[:exception_end]
# Destory .enigma* sections # Destory .enigma* sections
pe.__data__ = ( pe.__data__ = pe.__data__[:find_section(b'.enigma1').PointerToRawData] + pe.__data__[find_section(b'.enigma2').PointerToRawData + find_section(b'.enigma2').SizeOfRawData:]
pe.__data__[: find_section(b".enigma1").PointerToRawData]
+ pe.__data__[
find_section(b".enigma2").PointerToRawData
+ find_section(b".enigma2").SizeOfRawData :
]
)
# If original program has a overlay, this will perserve it. Otherwise it's okay to remove them anyway. # If original program has a overlay, this will perserve it. Otherwise it's okay to remove them anyway.
assert pe.sections.pop().Name == b".enigma2" assert pe.sections.pop().Name == b'.enigma2'
assert pe.sections.pop().Name == b".enigma1" assert pe.sections.pop().Name == b'.enigma1'
pe.FILE_HEADER.NumberOfSections -= 2 pe.FILE_HEADER.NumberOfSections -= 2
# NOTE: .enigma1 contains the VFS, as well as some Optional PE Header info as descrbied above # NOTE: .enigma1 contains the VFS, as well as some Optional PE Header info as descrbied above
# NOTE: .enigma2 is a aplib compressed loader DLL. You can decompress it with aplib provided in this repo # NOTE: .enigma2 is a aplib compressed loader DLL. You can decompress it with aplib provided in this repo
# Append the exception section and assign the pointers # Append the exception section and assign the pointers
if exception_data: if (exception_data):
# Reassign the RVA & sizes # Reassign the RVA & sizes
logger.debug("Rebuilt Exception directory. Size=0x%x" % len(exception_data)) logger.debug('Rebuilt Exception directory. Size=0x%x' % len(exception_data))
# Find where this could be placed at...since EVB clears the original exception directory listings # Find where this could be placed at...since EVB clears the original exception directory listings
# PEs with overlays won't work at all if EVB packed them. # PEs with overlays won't work at all if EVB packed them.
# We must remove the sections and do NOT append anything new # We must remove the sections and do NOT append anything new
try: try:
section, offset = search_pattern_in_sections(b"\x00" * len(exception_data)) section, offset = search_pattern_in_sections(b'\x00' * len(exception_data))
logger.debug( logger.debug('Found suitable section to place Exception Directory. Name=%s RVA=0x%x' % (section.Name.decode(),offset - section.PointerToRawData))
"Found suitable section to place Exception Directory. Name=%s RVA=0x%x" pe.__data__[offset:offset+len(exception_data)] = exception_data
% (section.Name.decode(), offset - section.PointerToRawData) section.SizeOfRawData = max(section.SizeOfRawData,len(exception_data))
)
pe.__data__[offset : offset + len(exception_data)] = exception_data
section.SizeOfRawData = max(section.SizeOfRawData, len(exception_data))
exception_dir.VirtualAddress = pe.get_rva_from_offset(offset) exception_dir.VirtualAddress = pe.get_rva_from_offset(offset)
exception_dir.Size = len(exception_data) exception_dir.Size = len(exception_data)
except StopIteration as e: except StopIteration as e:
logger.warning( logger.warning('Cannot place Exception Directory. It\'s highly likely that the unpacked PE won\'t work.')
"Cannot place Exception Directory. It's highly likely that the unpacked PE won't work."
)
warnings_issued += 1 warnings_issued += 1
exception_dir.VirtualAddress = 0 exception_dir.VirtualAddress = 0
exception_dir.Size = 0 exception_dir.Size = 0
else: else:
logger.debug("Original program does not contain Exception Directory.") logger.debug('Original program does not contain Exception Directory.')
exception_dir.VirtualAddress = 0 exception_dir.VirtualAddress = 0
exception_dir.Size = 0 exception_dir.Size = 0
# Serach for TLS in memory map since it's copied to the header # Serach for TLS in memory map since it's copied to the header
tls_dir = find_data_directory("TLS") tls_dir = find_data_directory('TLS')
try: try:
tls_data = hdr["TLS"] tls_data = hdr['TLS']
section, offset = search_pattern_in_sections(tls_data[:12]) section, offset = search_pattern_in_sections(tls_data[:12])
logger.debug( logger.debug('TLS Directory found. Offset=0x%x Section=%s' % (offset,section.Name.decode()))
"TLS Directory found. Offset=0x%x Section=%s"
% (offset, section.Name.decode())
)
tls_dir.VirtualAddress = pe.get_rva_from_offset(offset) tls_dir.VirtualAddress = pe.get_rva_from_offset(offset)
tls_dir.Size = 40 if arch_64 else 24 tls_dir.Size = 40 if arch_64 else 24
except StopIteration as e: except StopIteration as e:
logger.debug( logger.debug('TLS Directory not found. Original program probably does not have TLS data.')
"TLS Directory not found. Original program probably does not have TLS data."
)
tls_dir.VirtualAddress = 0 tls_dir.VirtualAddress = 0
tls_dir.Size = 0 tls_dir.Size = 0
# Write to new file # Write to new file
new_file_data = pe.write() new_file_data = pe.write()
with open(output_file, "wb+") as f: with open(output_file,'wb+') as f:
write_bytes(BytesIO(new_file_data), f, len(new_file_data), desc="Saving PE") write_bytes(BytesIO(new_file_data),f,len(new_file_data),desc='Saving PE')
logger.info("Unpacked PE saved: %s" % output_file) logger.info('Unpacked PE saved: %s' % output_file)
if warnings_issued: if warnings_issued:
logger.warning( logger.warning('There were %d warning(s) issued during the restoration process.' % warnings_issued)
"There were %d warning(s) issued during the restoration process." logger.warning('Please try using other pe_variant or check the original PE for any issues. Current variant: %s' % pe_variant)
% warnings_issued
)
logger.warning(
"Please try using other pe_variant or check the original PE for any issues. Current variant: %s"
% pe_variant
)
def search_for_magic(fd,size,magic):
def search_for_magic(fd, size, magic): CHUNKSIZE = 16 * 2**20 # 16MB
with mmap(fd.fileno(), offset=0, length=size, access=ACCESS_READ) as mm: for i in range(0,size,CHUNKSIZE):
result = mm.find(magic) with mmap(fd.fileno(),offset=i,length=min(CHUNKSIZE,size - i),access=ACCESS_READ) as mm:
if result >= 0: result = mm.find(magic)
logger.debug("Found magic at %xh" % result) if result >= 0:
return result logger.debug('Found magic at %xh' % result)
return result
return -1 return -1
def unpack_files(file : str, out_dir : str, legacy_fs : bool, fs_listing_only : bool):
def unpack_files(file: str, out_dir: str, legacy_fs: bool, fs_listing_only: bool):
size = os.stat(file).st_size size = os.stat(file).st_size
with open(file, "rb") as fd: with open(file,'rb') as fd:
magic = search_for_magic(fd, size, EVB_MAGIC) magic = search_for_magic(fd,size,EVB_MAGIC)
assert ( assert magic >= 0, "EVB filesystem magic not found. Cannot proceed."
magic >= 0
), "EVB filesystem magic not found. It's highly likely that this file is not produced by Enigma Virtual Box."
fd.seek(magic) fd.seek(magic)
if legacy_fs: if legacy_fs:
nodes = completed(legacy_pe_tree(fd)) nodes = completed(legacy_pe_tree(fd))
else: else:
nodes = completed(pe_external_tree(fd)) nodes = completed(pe_external_tree(fd))
depths = dict() depths = dict()
def write_line(s): def write_line(s):
sys.stderr.write(s + "\n") sys.stderr.write(s + '\n')
def get_prefix(level): def get_prefix(level):
prefix = "└───" if depths[level] else "├───" prefix = '└───' if depths[level] else '├───'
for _ in range(level, 0, -1): for _ in range(level,0,-1):
if _ != 0: if _ != 0: prefix = '' if not depths[_ - 1] else ' ' +prefix
prefix = "" if not depths[_ - 1] else " " + prefix
return prefix return prefix
def traverse_next_node(node,pfx=out_dir,depth=0):
def traverse_next_node(node, pfx=out_dir, depth=0): if node['type'] == NODE_TYPE_FOLDER: node['name'] = FOLDER_ALTNAMES.get(node['name'],node['name'])
if node["type"] == NODE_TYPE_FOLDER: assert ('\\' not in node['name']) and ('/' not in node['name']) and (':' not in node['name']), f'Invalid character in node name: {node["name"]}'
node["name"] = FOLDER_ALTNAMES.get(node["name"], node["name"]) assert node['name'] != '..' and node['name'] != '.', 'node name cannot be either . or ..'
assert ( path = os.path.normpath(os.path.join(pfx,node['name'])).replace('\\','/')
("\\" not in node["name"]) write_line(' ' + get_prefix(depth) + ' ' + path)
and ("/" not in node["name"]) if node['type'] == NODE_TYPE_FILE and not fs_listing_only: process_file_node(fd,path,node)
and (":" not in node["name"]) elif node['type'] == NODE_TYPE_FOLDER:
), f'Invalid character in node name: {node["name"]}'
assert (
node["name"] != ".." and node["name"] != "."
), "node name cannot be either . or .."
path = os.path.normpath(os.path.join(pfx, node["name"])).replace("\\", "/")
write_line(" " + get_prefix(depth) + " " + path)
if node["type"] == NODE_TYPE_FILE and not fs_listing_only:
process_file_node(fd, path, node)
elif node["type"] == NODE_TYPE_FOLDER:
if not os.path.isdir(path) and not fs_listing_only: if not os.path.isdir(path) and not fs_listing_only:
os.makedirs(path) os.makedirs(path)
for _ in range(0, node["objects_count"]): for _ in range(0,node['objects_count']):
last = _ == node["objects_count"] - 1 last = _ == node['objects_count'] - 1
depths[depth + 1] = last depths[depth + 1] = last
traverse_next_node(next(nodes), pfx=path, depth=depth + 1) traverse_next_node(next(nodes),pfx=path,depth=depth + 1)
try: try:
main_node = next(nodes) main_node = next(nodes)
write_line("Filesystem:") write_line('Filesystem:')
for _ in range(main_node["objects_count"]): for _ in range(main_node['objects_count']):
last = _ == main_node["objects_count"] - 1 last = _ == main_node['objects_count'] - 1
depths[0] = last depths[0] = last
traverse_next_node(next(nodes)) traverse_next_node(next(nodes))
except StopIteration: except StopIteration:
logger.error("The filetable appears to be corrupted.") logger.error('The filetable appears to be corrupted. Cannot proceed any further.')
logger.error("Toggling `--legacy-fs` may remedy the issue.") logger.error('Please try toggling the --legacy-fs flag to solve this issue.')
return return
logger.info("Extraction complete")
def main(
in_file: str,
out_dir: str = ".",
out_pe: str = "",
ignore_fs: bool = False,
ignore_pe: bool = False,
legacy_fs: bool = False,
pe_variant: str = "10_70",
fs_listing_only: bool = False,
):
logger.info("Enigma Virtual Box Unpacker v%s" % __version__)
logger.debug("File: %s" % in_file)
os.makedirs(out_dir, exist_ok=True)
if legacy_fs:
logger.warning("Legacy mode for filesystem extraction enabled")
if pe_variant:
logger.warning("Legacy mode for PE restoration enabled")
if fs_listing_only:
logger.warning("Listing virtual filesystem only")
if ignore_fs:
logger.warning("Skipping virtual FS extraction")
else:
logger.info("Extracting virtual filesystem")
try:
unpack_files(in_file, out_dir, legacy_fs, fs_listing_only)
except AssertionError as e: except AssertionError as e:
logger.error("While extracting VFS: %s" % e) logger.error('While extracting package %s' % e)
except Exception as e: return
logger.error("Unhandled exception occured while extracting VFS: %s" % e) logger.info('Extraction complete')
if ignore_pe:
logger.warning("Skipping executable restoration") def main(in_file : str, out_dir : str = '.', out_pe : str = '', ignore_fs: bool = False, ignore_pe: bool = False, legacy_fs: bool = False, pe_variant: str = '10_70', fs_listing_only: bool = False):
logger.info('Enigma Virtual Box Unpacker v%s' % __version__)
logger.debug('File: %s' % in_file)
os.makedirs(out_dir,exist_ok=True)
if legacy_fs:
logger.warning('Legacy mode for filesystem extraction enabled')
if pe_variant:
logger.warning('Legacy mode for PE restoration enabled')
if fs_listing_only:
logger.warning('Listing virtual filesystem only')
if ignore_fs:
logger.warning('Skipping virtual FS extraction')
else: else:
logger.info("Restoring executable") logger.info('Extracting virtual filesystem')
try:
unpack_files(in_file,out_dir,legacy_fs,fs_listing_only)
except Exception as e:
logger.error('Unhandled exception occured while extracting virtual filesystem: %s' % e)
raise e
if ignore_pe:
logger.warning('Skipping executable restoration')
else:
logger.info('Restoring executable')
if not out_pe: if not out_pe:
out_pe = os.path.join(out_dir, os.path.basename(in_file)) out_pe = os.path.join(out_dir, os.path.basename(in_file))
logger.info("Using default executable save path: %s" % out_pe) logger.info('Using default executable save path: %s' % out_pe)
try: try:
restore_pe(in_file, out_pe, pe_variant) restore_pe(in_file,out_pe,pe_variant)
except AssertionError as e:
logger.error("While restoring executable: %s" % e)
except Exception as e: except Exception as e:
logger.error( logger.error('Unhandled exception occured while restoring executable: %s' % e)
"Unhandled exception occured while restoring executable: %s" % e
)
def __main__(): def __main__():
parser = ArgumentParser(description="Enigma Virtual Box Unpacker") parser = ArgumentParser(description='Enigma Virtual Box Unpacker')
parser.add_argument( parser.add_argument('--log-level',help='Set log level',default='INFO',choices=['DEBUG','INFO','WARNING','ERROR','CRITICAL'])
"--log-level", group = parser.add_argument_group('Flags')
help="Set log level", group.add_argument('-l', '--list',help='Don\'t extract the files and print the table of content to stderr only',action='store_true')
default="INFO", group.add_argument('--ignore-fs',help='Don\'t extract virtual filesystem',action='store_true')
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], group.add_argument('--ignore-pe',help='Don\'t restore the executable',action='store_true')
) group.add_argument('--legacy-fs',help='Use legacy mode for filesystem extraction',action='store_true')
group = parser.add_argument_group("Flags") group.add_argument('-pe','--pe-variant',help='Unpacker variant to use when unpacking EXEs. default=%(default)s', default='9_70', choices=EVB_ENIGMA1_HEADER.get_options())
group.add_argument( group = parser.add_argument_group('Overrides')
"-l", group.add_argument('--out-pe', help='(If the executable is to be recovered) Where the unpacked EXE is saved. Leave as-is to save it in the output folder.',default='')
"--list", group = parser.add_argument_group('Input')
help="Don't extract the files and print the table of content to stderr only", group.add_argument('file', help='File to be unpacked')
action="store_true", group.add_argument('output', help='Output folder')
) args = parser.parse_args()
group.add_argument( logging.basicConfig(level=args.log_level, format='%(levelname)s: %(message)s')
"--ignore-fs", help="Don't extract virtual filesystem", action="store_true" sys.exit(main(args.file,args.output,args.out_pe,args.ignore_fs,args.ignore_pe,args.legacy_fs,args.pe_variant,args.list))
)
group.add_argument(
"--ignore-pe", help="Don't restore the executable", action="store_true"
)
group.add_argument(
"--legacy-fs",
help="Use legacy mode for filesystem extraction",
action="store_true",
)
group.add_argument(
"-pe",
"--pe-variant",
help="Unpacker variant to use when unpacking EXEs. default=%(default)s",
default="9_70",
choices=EVB_ENIGMA1_HEADER.get_options(),
)
group = parser.add_argument_group("Overrides")
group.add_argument(
"--out-pe",
help="(If the executable is to be recovered) Where the unpacked EXE is saved. Leave as-is to save it in the output folder.",
default="",
)
group = parser.add_argument_group("Input")
group.add_argument("file", help="File to be unpacked")
group.add_argument("output", help="Output folder")
args = parser.parse_args()
logging.basicConfig(level=args.log_level, format="%(levelname)s: %(message)s")
sys.exit(
main(
args.file,
args.output,
args.out_pe,
args.ignore_fs,
args.ignore_pe,
args.legacy_fs,
args.pe_variant,
args.list,
)
)
if __name__ == "__main__": if __name__ == "__main__":
__main__() __main__()

172
evbunpack/aplib.py Normal file
View file

@ -0,0 +1,172 @@
#-*- coding: utf-8 --
"""A pure Python module for decompressing aPLib compressed data
Adapted from the original C source code from http://ibsensoftware.com/files/aPLib-1.1.1.zip
Approximately 20 times faster than other Python implementations.
Compatible with both Python 2 and 3.
"""
import struct
from binascii import crc32
from io import BytesIO
__all__ = ['APLib', 'decompress']
__version__ = '0.6'
__author__ = 'Sandor Nemes'
class APLib(object):
__slots__ = 'source', 'destination', 'tag', 'bitcount', 'strict'
def __init__(self, source, strict=True):
self.source = BytesIO(source)
self.destination = bytearray()
self.tag = 0
self.bitcount = 0
self.strict = bool(strict)
def getbit(self):
# check if tag is empty
self.bitcount -= 1
if self.bitcount < 0:
# load next tag
self.tag = ord(self.source.read(1))
self.bitcount = 7
# shift bit out of tag
bit = self.tag >> 7 & 1
self.tag <<= 1
return bit
def getgamma(self):
result = 1
# input gamma2-encoded bits
while True:
result = (result << 1) + self.getbit()
if not self.getbit():
break
return result
def depack(self):
r0 = -1
lwm = 0
done = False
try:
# first byte verbatim
self.destination += self.source.read(1)
# main decompression loop
while not done:
if self.getbit():
if self.getbit():
if self.getbit():
offs = 0
for _ in range(4):
offs = (offs << 1) + self.getbit()
if offs:
self.destination.append(self.destination[-offs])
else:
self.destination.append(0)
lwm = 0
else:
offs = ord(self.source.read(1))
length = 2 + (offs & 1)
offs >>= 1
if offs:
for _ in range(length):
self.destination.append(self.destination[-offs])
else:
done = True
r0 = offs
lwm = 1
else:
offs = self.getgamma()
if lwm == 0 and offs == 2:
offs = r0
length = self.getgamma()
for _ in range(length):
self.destination.append(self.destination[-offs])
else:
if lwm == 0:
offs -= 3
else:
offs -= 2
offs <<= 8
offs += ord(self.source.read(1))
length = self.getgamma()
if offs >= 32000:
length += 1
if offs >= 1280:
length += 1
if offs < 128:
length += 2
for _ in range(length):
self.destination.append(self.destination[-offs])
r0 = offs
lwm = 1
else:
self.destination += self.source.read(1)
lwm = 0
except (TypeError, IndexError):
if self.strict:
raise RuntimeError('aPLib decompression error')
return bytes(self.destination)
def pack(self):
raise NotImplementedError
def decompress(data, strict=True):
packed_size = None
packed_crc = None
orig_size = None
orig_crc = None
if data.startswith(b'AP32') and len(data) >= 24:
# data has an aPLib header
header_size, packed_size, packed_crc, orig_size, orig_crc = struct.unpack_from('=IIIII', data, 4)
data = data[header_size : header_size + packed_size]
if strict:
if packed_size is not None and packed_size != len(data):
raise RuntimeError('Packed data size is incorrect')
if packed_crc is not None and packed_crc != crc32(data):
raise RuntimeError('Packed data checksum is incorrect')
result = APLib(data, strict=strict).depack()
if strict:
if orig_size is not None and orig_size != len(result):
raise RuntimeError('Unpacked data size is incorrect')
if orig_crc is not None and orig_crc != crc32(result):
raise RuntimeError('Unpacked data checksum is incorrect')
return result
def main():
# self-test
data = b'T\x00he quick\xecb\x0erown\xcef\xaex\x80jumps\xed\xe4veur`t?lazy\xead\xfeg\xc0\x00'
assert decompress(data) == b'The quick brown fox jumps over the lazy dog'
if __name__ == '__main__':
main()

View file

@ -1,212 +1,188 @@
# -*- coding: utf-8 -- #-*- coding: utf-8 --
EVB_MAGIC = b"EVB\x00" EVB_MAGIC = b'EVB\x00'
PE_EXCEPTION = [ PE_EXCEPTION = [
("I", "BEGIN_ADDRESS"), ('I','BEGIN_ADDRESS'),
("I", "END_ADDRESS"), ('I','END_ADDRESS'),
("I", "HANDLER_PTR"), ('I','HANDLER_PTR'),
("I", "HANDLER_DATA"), ('I','HANDLER_DATA'),
("I", "PROLOG_ADDRESS"), ('I','PROLOG_ADDRESS'),
] ]
PE64_EXCEPTION = [ PE64_EXCEPTION = [
("I", "BEGIN_ADDRESS"), ('I','BEGIN_ADDRESS'),
("I", "END_ADDRESS"), ('I','END_ADDRESS'),
("I", "UNWIND_INFO"), ('I','UNWIND_INFO'),
] ]
class EVB_ENIGMA1_HEADER: class EVB_ENIGMA1_HEADER:
x64 = { x64 = {
"10_70": [ '10_70':[
("32s", "TLS"), ('32s', 'TLS'),
# Padding Begin # Padding Begin
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), ('Q', ''),
("Q", ""), # Padding End
("Q", ""), ('Q', 'UNK_1'),
("Q", ""), ('Q', ''), ('Q', ''),
# Padding End ('Q', ''),
("Q", "UNK_1"), ('I','IMPORT_ADDRESS'),
("Q", ""), ('I','IMPORT_SIZE'),
("Q", ""), ('I','RELOC_ADDRESS'),
("Q", ""), ('I','RELOC_SIZE'),
("I", "IMPORT_ADDRESS"), ('I','TLS_ADDRESS'),
("I", "IMPORT_SIZE"), ('I','TLS_SIZE'),
("I", "RELOC_ADDRESS"),
("I", "RELOC_SIZE"),
("I", "TLS_ADDRESS"),
("I", "TLS_SIZE"),
], ],
"9_70": [ '9_70':[
("32s", "TLS"), ('32s', 'TLS'),
# Padding Begin # Padding Begin
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), ('Q', ''),
("Q", ""), ('I', ''),
("Q", ""), # Padding End
("Q", ""), ('Q', 'UNK_1'),
("I", ""), ('Q', ''),
# Padding End ('I','IMPORT_ADDRESS'),
("Q", "UNK_1"), ('I','IMPORT_SIZE'),
("Q", ""), ('I','RELOC_ADDRESS'),
("I", "IMPORT_ADDRESS"), ('I','RELOC_SIZE'),
("I", "IMPORT_SIZE"), ('I','TLS_ADDRESS'),
("I", "RELOC_ADDRESS"), ('I','TLS_SIZE'),
("I", "RELOC_SIZE"), ],
("I", "TLS_ADDRESS"), '7_80':[
("I", "TLS_SIZE"), ('32s', 'TLS'),
],
"7_80": [
("32s", "TLS"),
# Padding Begin # Padding Begin
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), ('Q', ''),
("Q", ""), # Padding End
("Q", ""), ('Q', 'UNK_1'),
("Q", ""), ('Q', ''),
# Padding End ('I','IMPORT_ADDRESS'),
("Q", "UNK_1"), ('I','IMPORT_SIZE'),
("Q", ""), ('I','RELOC_ADDRESS'),
("I", "IMPORT_ADDRESS"), ('I','RELOC_SIZE'),
("I", "IMPORT_SIZE"), ('I','TLS_ADDRESS'),
("I", "RELOC_ADDRESS"), ('I','TLS_SIZE'),
("I", "RELOC_SIZE"), ]
("I", "TLS_ADDRESS"),
("I", "TLS_SIZE"),
],
} }
x86 = { x86 = {
"10_70": [ '10_70':[
("16s", "TLS"), ('16s', 'TLS'),
# Padding Begin # Padding Begin
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), # Padding End
("Q", ""), ('Q', 'UNK_1'), ('Q', 'UNK_2'),
("Q", ""), ('I', 'UNK_3'),
# Padding End ('I','IMPORT_ADDRESS'),
("Q", "UNK_1"), ('I','IMPORT_SIZE'),
("Q", "UNK_2"), ('I','RELOC_ADDRESS'),
("I", "UNK_3"), ('I','RELOC_SIZE'),
("I", "IMPORT_ADDRESS"), ('I','TLS_ADDRESS'),
("I", "IMPORT_SIZE"), ('I','TLS_SIZE'),
("I", "RELOC_ADDRESS"),
("I", "RELOC_SIZE"),
("I", "TLS_ADDRESS"),
("I", "TLS_SIZE"),
], ],
"9_70": [ '9_70' : [
("16s", "TLS"), ('16s', 'TLS'),
# Padding Begin # Padding Begin
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), # Padding End
("Q", ""), ('Q', 'UNK_1'), ('Q', 'UNK_2'),
("Q", ""), ('I','IMPORT_ADDRESS'),
# Padding End ('I','IMPORT_SIZE'),
("Q", "UNK_1"), ('I','RELOC_ADDRESS'),
("Q", "UNK_2"), ('I','RELOC_SIZE'),
("I", "IMPORT_ADDRESS"), ('I','TLS_ADDRESS'),
("I", "IMPORT_SIZE"), ('I','TLS_SIZE'),
("I", "RELOC_ADDRESS"), ],
("I", "RELOC_SIZE"), '7_80' : [
("I", "TLS_ADDRESS"), ('16s', 'TLS'),
("I", "TLS_SIZE"),
],
"7_80": [
("16s", "TLS"),
# Padding Begin # Padding Begin
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), ('Q', ''), ('Q', ''),
("Q", ""), # Padding End
("Q", ""), ('Q', 'UNK_1'),
("Q", ""), ('I', 'UNK_3'),
# Padding End ('I','IMPORT_ADDRESS'),
("Q", "UNK_1"), ('I','IMPORT_SIZE'),
("I", "UNK_3"), ('I','RELOC_ADDRESS'),
("I", "IMPORT_ADDRESS"), ('I','RELOC_SIZE'),
("I", "IMPORT_SIZE"), ('I','TLS_ADDRESS'),
("I", "RELOC_ADDRESS"), ('I','TLS_SIZE'),
("I", "RELOC_SIZE"), ]
("I", "TLS_ADDRESS"),
("I", "TLS_SIZE"),
],
} }
@staticmethod @staticmethod
def get_options(): def get_options():
return list(EVB_ENIGMA1_HEADER.x86.keys()) return list(EVB_ENIGMA1_HEADER.x86.keys())
@staticmethod @staticmethod
def get_struct(arch_64: bool, key: str): def get_struct(arch_64 : bool, key : str):
if arch_64: if arch_64:
return EVB_ENIGMA1_HEADER.x64[key] return EVB_ENIGMA1_HEADER.x64[key]
else: else:
return EVB_ENIGMA1_HEADER.x86[key] return EVB_ENIGMA1_HEADER.x86[key]
EVB_PACK_HEADER = [ EVB_PACK_HEADER = [
("4s", "signature"), # Would always be ('EVB\x00') if valid ('4s', 'signature'), # Would always be ('EVB\x00') if valid
("60s", ""), ('60s',''),
] ]
EVB_CHUNK_BLOCK = [ EVB_CHUNK_BLOCK = [
("I", "size"), ('I','size'),
("I", ""), ('I',''),
] ]
EVB_HEADER_NODE = [ EVB_HEADER_NODE = [
("I", "size"), ('I','size'),
("8s", ""), ('8s',''),
("I", "objects_count"), ('I','objects_count'),
] ]
EVB_NODE_MAIN = [ EVB_NODE_MAIN = [
("I", "size"), ('I', 'size'),
("8s", ""), ('8s', ''),
("I", "objects_count"), ('I', 'objects_count'),
] ]
EVB_NODE_NAMED = [ EVB_NODE_NAMED = [
("%ds", "name"), # args[0] - filename buffer length ('%ds', 'name'), # args[0] - filename buffer length
("B", "type"), ('B', 'type'),
] ]
EVB_NODE_OPTIONAL_FILE = [ EVB_NODE_OPTIONAL_FILE = [
("2s", ""), ('2s', ''),
("I", "original_size"), ('I', 'original_size'),
("4s", ""), ('4s',''),
("8s", "filetime1"), ('8s','filetime1'),
("8s", "filetime2"), ('8s','filetime2'),
("8s", "filetime3"), ('8s','filetime3'),
("15s", ""), ('15s',''),
("I", "stored_size"), ('I', 'stored_size'),
] ]
EVB_NODE_OPTIONAL_PE_FILE = [ EVB_NODE_OPTIONAL_PE_FILE = [
("2s", ""), ('2s', ''),
("I", "original_size"), ('I', 'original_size'),
("4s", ""), ('4s',''),
("8s", "filetime1"), ('8s','filetime1'),
("8s", "filetime2"), ('8s','filetime2'),
("8s", "filetime3"), ('8s','filetime3'),
("7s", ""), ('7s',''),
("I", "stored_size"), ('I', 'stored_size'),
("4s", ""), ('4s',''),
] ]
NODE_TYPE_MAIN = 0 NODE_TYPE_MAIN = 0
NODE_TYPE_FILE = 2 NODE_TYPE_FILE = 2
NODE_TYPE_FOLDER = 3 NODE_TYPE_FOLDER = 3

View file

@ -1,6 +1,6 @@
import setuptools, evbunpack import setuptools,evbunpack
with open("README.md", "r", encoding="utf-8") as fh: with open("README.md", "r",encoding='utf-8') as fh:
long_description = fh.read() long_description = fh.read()
setuptools.setup( setuptools.setup(
@ -18,7 +18,9 @@ setuptools.setup(
"License :: OSI Approved :: Apache Software License", "License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
], ],
install_requires=["pefile", "aplib"], install_requires=[
"pefile"
],
entry_points={"console_scripts": ["evbunpack=evbunpack.__main__:__main__"]}, entry_points={"console_scripts": ["evbunpack=evbunpack.__main__:__main__"]},
python_requires=">=3.0", python_requires='>=3.0',
) )

View file

@ -1,106 +1,36 @@
import os, logging import os, logging
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
os.chdir(os.path.join(os.path.dirname(__file__), "tests")) os.chdir(os.path.join(os.path.dirname(__file__),'tests'))
TEMP_OUTPUT_FILENAME = "_unpacked.exe"
TEMP_OUTPUT_FILENAME = '_unpacked.exe'
def unpack_exec(pe_file, **kw): def unpack_exec(pe_file, **kw):
from evbunpack.__main__ import main from evbunpack.__main__ import main
main(pe_file, 'output', os.path.join('.',TEMP_OUTPUT_FILENAME), **kw)
main(pe_file, "output", os.path.join(".", TEMP_OUTPUT_FILENAME), **kw) return_code = os.system(TEMP_OUTPUT_FILENAME)
return_code = os.system(TEMP_OUTPUT_FILENAME)
if return_code != 0: if return_code != 0:
logging.error("Failed to execute unpacked file. Exit code: %d", return_code) logging.error('Failed to execute unpacked file. Exit code: %d', return_code)
return return_code return return_code
def test_unpack11_00_x64():
assert (
unpack_exec(
"x64_PackerTestApp_packed_20240826.exe", legacy_fs=False, pe_variant="10_70"
)
== 0
)
def test_unpack10_80_x64(): def test_unpack10_80_x64():
assert ( assert unpack_exec('x64_PackerTestApp_packed_20240613.exe', legacy_fs = False, pe_variant = '10_70') == 0
unpack_exec(
"x64_PackerTestApp_packed_20240613.exe", legacy_fs=False, pe_variant="10_70"
)
== 0
)
def test_unpack10_70_x64(): def test_unpack10_70_x64():
assert ( assert unpack_exec('x64_PackerTestApp_packed_20240522.exe', legacy_fs = False, pe_variant = '10_70') == 0
unpack_exec(
"x64_PackerTestApp_packed_20240522.exe", legacy_fs=False, pe_variant="10_70"
)
== 0
)
def test_unpack9_70_x64(): def test_unpack9_70_x64():
assert ( assert unpack_exec('x64_PackerTestApp_packed_20210329.exe', legacy_fs = False, pe_variant = '9_70') == 0
unpack_exec(
"x64_PackerTestApp_packed_20210329.exe", legacy_fs=False, pe_variant="9_70"
)
== 0
)
def test_unpack7_80_x64(): def test_unpack7_80_x64():
assert ( assert unpack_exec('x64_PackerTestApp_packed_20170713.exe', legacy_fs = True, pe_variant = '7_80') == 0
unpack_exec(
"x64_PackerTestApp_packed_20170713.exe", legacy_fs=True, pe_variant="7_80"
)
== 0
)
def test_unpack11_00_x86():
assert (
unpack_exec(
"x86_PackerTestApp_packed_20240826.exe", legacy_fs=False, pe_variant="10_70"
)
== 0
)
def test_unpack10_80_x86(): def test_unpack10_80_x86():
assert ( assert unpack_exec('x86_PackerTestApp_packed_20240613.exe', legacy_fs = False, pe_variant = '10_70') == 0
unpack_exec(
"x86_PackerTestApp_packed_20240613.exe", legacy_fs=False, pe_variant="10_70"
)
== 0
)
def test_unpack10_70_x86(): def test_unpack10_70_x86():
assert ( assert unpack_exec('x86_PackerTestApp_packed_20240522.exe', legacy_fs = False, pe_variant = '10_70') == 0
unpack_exec(
"x86_PackerTestApp_packed_20240522.exe", legacy_fs=False, pe_variant="10_70"
)
== 0
)
def test_unpack9_70_x86(): def test_unpack9_70_x86():
assert ( assert unpack_exec('x86_PackerTestApp_packed_20210329.exe', legacy_fs = False, pe_variant = '9_70') == 0
unpack_exec(
"x86_PackerTestApp_packed_20210329.exe", legacy_fs=False, pe_variant="9_70"
)
== 0
)
def test_unpack7_80_x86(): def test_unpack7_80_x86():
assert ( assert unpack_exec('x86_PackerTestApp_packed_20170713.exe', legacy_fs = True, pe_variant = '7_80') == 0
unpack_exec(
"x86_PackerTestApp_packed_20170713.exe", legacy_fs=True, pe_variant="7_80"
)
== 0
)

View file

@ -1,97 +0,0 @@
<?xml version="1.0" encoding="windows-1252"?>
<>
<InputFile>C:\Users\mos9527\evbunpack\tests\x64_PackerTestApp.exe</InputFile>
<OutputFile>C:\Users\mos9527\evbunpack\tests\x64_PackerTestApp_packed_20240826.exe</OutputFile>
<Files>
<Enabled>True</Enabled>
<DeleteExtractedOnExit>False</DeleteExtractedOnExit>
<CompressFiles>False</CompressFiles>
<Files>
<File>
<Type>3</Type>
<Name>%DEFAULT FOLDER%</Name>
<Action>0</Action>
<OverwriteDateTime>False</OverwriteDateTime>
<OverwriteAttributes>False</OverwriteAttributes>
<HideFromDialogs>0</HideFromDialogs>
<Files>
<File>
<Type>2</Type>
<Name>README.txt</Name>
<File>C:\Users\mos9527\evbunpack\tests\README_packed.txt</File>
<ActiveX>False</ActiveX>
<ActiveXInstall>False</ActiveXInstall>
<Action>0</Action>
<OverwriteDateTime>False</OverwriteDateTime>
<OverwriteAttributes>False</OverwriteAttributes>
<PassCommandLine>False</PassCommandLine>
<HideFromDialogs>0</HideFromDialogs>
</File>
</Files>
</File>
</Files>
</Files>
<Registries>
<Enabled>False</Enabled>
<Registries>
<Registry>
<Type>1</Type>
<Virtual>True</Virtual>
<Name>Classes</Name>
<ValueType>0</ValueType>
<Value/>
<Registries/>
</Registry>
<Registry>
<Type>1</Type>
<Virtual>True</Virtual>
<Name>User</Name>
<ValueType>0</ValueType>
<Value/>
<Registries/>
</Registry>
<Registry>
<Type>1</Type>
<Virtual>True</Virtual>
<Name>Machine</Name>
<ValueType>0</ValueType>
<Value/>
<Registries/>
</Registry>
<Registry>
<Type>1</Type>
<Virtual>True</Virtual>
<Name>Users</Name>
<ValueType>0</ValueType>
<Value/>
<Registries/>
</Registry>
<Registry>
<Type>1</Type>
<Virtual>True</Virtual>
<Name>Config</Name>
<ValueType>0</ValueType>
<Value/>
<Registries/>
</Registry>
</Registries>
</Registries>
<Packaging>
<Enabled>False</Enabled>
</Packaging>
<Options>
<ShareVirtualSystem>False</ShareVirtualSystem>
<MapExecutableWithTemporaryFile>True</MapExecutableWithTemporaryFile>
<TemporaryFileMask/>
<AllowRunningOfVirtualExeFiles>True</AllowRunningOfVirtualExeFiles>
<ProcessesOfAnyPlatforms>False</ProcessesOfAnyPlatforms>
</Options>
<Storage>
<Files>
<Enabled>False</Enabled>
<Folder>%DEFAULT FOLDER%\</Folder>
<RandomFileNames>False</RandomFileNames>
<EncryptContent>False</EncryptContent>
</Files>
</Storage>
</>