mirror of
https://github.com/naksyn/PythonMemoryModule
synced 2026-06-06 16:24:25 +00:00
command line support (partial) via PEB stomping
This update include support to passing command line parameters to unmanaged exe via PEB stomping. This technique is not working with every executable since it depends on which functions are used to pass arguments. Generally, to get a universally working technique would be required to hook GetCommandlineA GetCommandlineW __getmainargs and __wgetmainargs since PEB stomping won't cover all cases, more details here: https://blog-30cm-tw.translate.goog/2020/08/windows-c-mainargc-argv.html?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=it&_x_tr_pto=wapp However, during my testing I found that mimikatz and several go binaries are working just by doing PEB stomping. On the other hand, cmdline passing via PEB stomping alone to mingw and VS compiled binaries won't likely work.
This commit is contained in:
@@ -0,0 +1,453 @@
|
||||
import ctypes
|
||||
import windows
|
||||
import windows.hooks as hooks
|
||||
import windows.utils as utils
|
||||
|
||||
from windows.generated_def.winstructs import *
|
||||
from windows.utils import transform_ctypes_fields
|
||||
import windows.remotectypes as rctypes
|
||||
|
||||
IMAGE_ORDINAL_FLAG32 = 0x80000000
|
||||
IMAGE_ORDINAL_FLAG64 = 0x8000000000000000
|
||||
|
||||
|
||||
def get_structure_transformer_for_target(target, targetbitness=None):
|
||||
current_bitness = windows.current_process.bitness
|
||||
if target is None:
|
||||
ctypes_structure_transformer = lambda x:x
|
||||
create_structure_at = lambda structcls, addr: structcls.from_address(addr)
|
||||
return ctypes_structure_transformer, create_structure_at
|
||||
|
||||
if targetbitness is None:
|
||||
targetbitness = target.bitness
|
||||
|
||||
if targetbitness == 32 and current_bitness == 64:
|
||||
ctypes_structure_transformer = rctypes.transform_type_to_remote32bits
|
||||
elif targetbitness == 64 and current_bitness == 32:
|
||||
ctypes_structure_transformer = rctypes.transform_type_to_remote64bits
|
||||
elif targetbitness == current_bitness:
|
||||
ctypes_structure_transformer = rctypes.transform_type_to_remote
|
||||
else:
|
||||
raise NotImplementedError("Parsing {0} PE from {1} Process".format(targetedbitness, proc_bitness))
|
||||
|
||||
def create_structure_at(structcls, addr): # Il reste une closure sur 'target' ici !!
|
||||
return ctypes_structure_transformer(structcls)(addr, target)
|
||||
return ctypes_structure_transformer, create_structure_at
|
||||
|
||||
def get_pe_bitness(baseaddr, target):
|
||||
# We can force bitness as the field we access are bitness-independant
|
||||
pe = GetPEFile(baseaddr, target, force_bitness=32)
|
||||
machine = pe.get_NT_HEADER().FileHeader.Machine
|
||||
if machine == 0x14c:
|
||||
return 32
|
||||
elif machine == 0x8664:
|
||||
return 64
|
||||
else:
|
||||
raise ValueError("Unknow PE target machine <0x{0:x}>".format(machine))
|
||||
|
||||
|
||||
## == PEPARSE V2 ==
|
||||
|
||||
|
||||
import collections
|
||||
CtypesStructureTransformers = collections.namedtuple("CtypesStructureTransformers", ["ctypes_structure_transformer", "create_structure_at"])
|
||||
|
||||
def GetPEFile(baseaddr, target=None, force_bitness=None):
|
||||
"""Returns a :class:`PEFile` to explore a PE loaded at `baseaddr` in process `target`.
|
||||
|
||||
:rtype: :class:`PEFile`
|
||||
|
||||
.. note::
|
||||
|
||||
If target is ``None`` it refers to the current process
|
||||
"""
|
||||
proc_bitness = windows.current_process.bitness
|
||||
|
||||
if force_bitness is None:
|
||||
targetedbitness = get_pe_bitness(baseaddr, target)
|
||||
else:
|
||||
targetedbitness = force_bitness
|
||||
|
||||
transformers = get_structure_transformer_for_target(target, targetedbitness)
|
||||
#ctypes_structure_transformer, create_structure_at = transformers
|
||||
transfor_funcs = CtypesStructureTransformers(*transformers) # TODO: rename
|
||||
return PEFile(target, baseaddr, targetedbitness, transfor_funcs)
|
||||
|
||||
|
||||
class THUNK_DATA(ctypes.Union):
|
||||
_fields_ = [
|
||||
("Ordinal", PVOID),
|
||||
("AddressOfData", PVOID)
|
||||
]
|
||||
|
||||
# Special case for .NET PE32 rewrite as 64b
|
||||
# We may have a PE in a 64b process with a 32b IAT
|
||||
class THUNK_DATA_32(ctypes.Union):
|
||||
_fields_ = [
|
||||
("Ordinal", DWORD),
|
||||
("AddressOfData", DWORD)
|
||||
]
|
||||
|
||||
class IMPORT_BY_NAME(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("Hint", WORD),
|
||||
("Name", BYTE)
|
||||
]
|
||||
|
||||
|
||||
def get_string(target, addr):
|
||||
if target is None:
|
||||
return ctypes.c_char_p(addr).value.decode("latin1")
|
||||
return target.read_string(addr)
|
||||
|
||||
|
||||
class PESection(IMAGE_SECTION_HEADER):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if self.target is None:
|
||||
name = get_string(self.target, ctypes.addressof(self.Name))[:8]
|
||||
else:
|
||||
name = get_string(self.target, self._base_addr)[:8]
|
||||
# Decode as UTF-8 as the MS doc say ?
|
||||
return name
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
return self.baseaddr + self.VirtualAddress
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self.VirtualSize
|
||||
|
||||
def __repr__(self):
|
||||
return "<PESection \"{0}\">".format(self.name)
|
||||
|
||||
|
||||
@classmethod
|
||||
def create(cls, pefile, addr):
|
||||
self = pefile.transformers.create_structure_at(cls, addr)
|
||||
self.baseaddr = pefile.baseaddr
|
||||
self.target = pefile.target
|
||||
return self
|
||||
|
||||
|
||||
class IATPtr(PVOID):
|
||||
@classmethod
|
||||
def from_iatentry(cls, iat_entry):
|
||||
self = cls.from_address(iat_entry.addr)
|
||||
self.addr = iat_entry.addr
|
||||
self.nonhookvalue = iat_entry.nonhookvalue
|
||||
return self
|
||||
|
||||
class IATEntry(ctypes.Structure):
|
||||
"""Represent an entry in the IAT of a module
|
||||
Can be used to get resolved value and setup hook
|
||||
"""
|
||||
_fields_ = [
|
||||
("value", PVOID)]
|
||||
|
||||
@classmethod
|
||||
def create(cls, addr, ord, name, target, transformers):
|
||||
self = transformers.create_structure_at(cls, addr)
|
||||
self.addr = addr
|
||||
self.ord = ord
|
||||
self.name = name
|
||||
self.hook = None
|
||||
self.nonhookvalue = self.value
|
||||
self.target = target
|
||||
return self
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0} "{1}" ordinal {2}>'.format(self.__class__.__name__, self.name, self.ord)
|
||||
|
||||
def set_hook(self, callback, types=None):
|
||||
"""Setup a hook on the entry and return it.
|
||||
You MUST keep a reference to the hook while the hook is enabled.
|
||||
|
||||
:param callback: the hook
|
||||
|
||||
.. note::
|
||||
|
||||
see :ref:`hook_protocol`
|
||||
|
||||
:rtype: :class:`windows.hooks.IATHook`
|
||||
|
||||
.. warning::
|
||||
|
||||
This works only for PEFile with the current process as target.
|
||||
"""
|
||||
if self.target is not None:
|
||||
raise NotImplementedError("Setting hook in remote process (use python code injection)")
|
||||
|
||||
hook = hooks.IATHook(self, callback, types)
|
||||
import weakref
|
||||
self.whook = weakref.ref(hook, self.on_destroy)
|
||||
self.hook = hook
|
||||
hook.enable()
|
||||
return hook
|
||||
|
||||
def on_destroy(self, *args):
|
||||
# We cannot know if the hook was enabled here..
|
||||
print("DESTROY: {0} -> ".format(args, self.enabled))
|
||||
# print(args[0]())
|
||||
|
||||
def remove_hook(self):
|
||||
"""Remove the hook on the entry"""
|
||||
if self.hook is None:
|
||||
return False
|
||||
self.hook.disable()
|
||||
self.hook = None
|
||||
return True
|
||||
|
||||
# def __del__(self):
|
||||
# print(self.hook)
|
||||
# if self.hook:
|
||||
# print("LOL BYE {0}".format(self.hook))
|
||||
|
||||
|
||||
class IMAGE_IMPORT_DESCRIPTOR(IMAGE_IMPORT_DESCRIPTOR): # TODO: use explicite name winstructs.IMAGE_IMPORT_DESCRIPTOR
|
||||
def get_INT(self, pe):
|
||||
THUNK_DATA_TYPE = THUNK_DATA
|
||||
if not self.OriginalFirstThunk:
|
||||
return None
|
||||
# We may have 32bits PE mapped in 32bits process (thanks to .NET PE)
|
||||
if self.target is None and pe.bitness != windows.current_process.bitness:
|
||||
assert windows.current_process.bitness == 64 and pe.bitness == 32, "Mapped 64b PE in current process 32b not handled"
|
||||
THUNK_DATA_TYPE = THUNK_DATA_32
|
||||
int_addr = self.OriginalFirstThunk + self.baseaddr
|
||||
int_entry = self.transformers.create_structure_at(THUNK_DATA_TYPE, int_addr)
|
||||
res = []
|
||||
while int_entry.Ordinal:
|
||||
if int_entry.Ordinal & self.IMAGE_ORDINAL_FLAG:
|
||||
res += [(int_entry.Ordinal & 0x7fffffff, None)]
|
||||
else:
|
||||
import_by_name = self.transformers.create_structure_at(IMPORT_BY_NAME, self.baseaddr + int_entry.AddressOfData)
|
||||
name_address = self.baseaddr + int_entry.AddressOfData + type(import_by_name).Name.offset
|
||||
name = get_string(self.target, name_address)
|
||||
res.append((import_by_name.Hint, name))
|
||||
int_addr += ctypes.sizeof(type(int_entry))
|
||||
int_entry = self.transformers.create_structure_at(THUNK_DATA_TYPE, int_addr)
|
||||
return res
|
||||
|
||||
def get_IAT(self, pe):
|
||||
THUNK_DATA_TYPE = THUNK_DATA
|
||||
if self.target is None and pe.bitness != windows.current_process.bitness:
|
||||
assert windows.current_process.bitness == 64 and pe.bitness == 32, "Mapped 64b PE in current process 32b not handled"
|
||||
THUNK_DATA_TYPE = THUNK_DATA_32
|
||||
iat_addr = self.FirstThunk + self.baseaddr
|
||||
iat_entry = self.transformers.create_structure_at(THUNK_DATA_TYPE, iat_addr)
|
||||
res = []
|
||||
while iat_entry.Ordinal:
|
||||
res.append(IATEntry.create(iat_addr, -1, "??", self.target, self.transformers))
|
||||
iat_addr += ctypes.sizeof(type(iat_entry))
|
||||
iat_entry = self.transformers.create_structure_at(THUNK_DATA_TYPE, iat_addr)
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def create(cls, pefile, addr):
|
||||
self = pefile.transformers.create_structure_at(cls, addr)
|
||||
self.baseaddr = pefile.baseaddr
|
||||
self.transformers = pefile.transformers
|
||||
self.IMAGE_ORDINAL_FLAG = pefile.IMAGE_ORDINAL_FLAG
|
||||
self.target = pefile.target
|
||||
return self
|
||||
|
||||
class IMAGE_EXPORT_DIRECTORY(IMAGE_EXPORT_DIRECTORY): # TODO: use explicite name winstructs._IMAGE_EXPORT_DIRECTORY
|
||||
def get_exports(self):
|
||||
NameOrdinals = self.transformers.create_structure_at((WORD * self.NumberOfNames), self.AddressOfNameOrdinals + self.baseaddr)
|
||||
NameOrdinals = list(NameOrdinals)
|
||||
Functions = self.transformers.create_structure_at((DWORD * self.NumberOfFunctions), self.AddressOfFunctions + self.baseaddr)
|
||||
Names = self.transformers.create_structure_at((DWORD * self.NumberOfNames), self.AddressOfNames + self.baseaddr)
|
||||
res = []
|
||||
for nb, func in enumerate(Functions):
|
||||
func += self.baseaddr
|
||||
if nb in NameOrdinals:
|
||||
name = get_string(self.target, Names[NameOrdinals.index(nb)] + self.baseaddr)
|
||||
# Export name should be ascii
|
||||
# Decode from ascii or return bytes ?
|
||||
# https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#export-address-table
|
||||
else:
|
||||
name = None
|
||||
res.append((nb, func, name))
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def create(cls, pefile, addr):
|
||||
self = pefile.transformers.create_structure_at(cls, addr)
|
||||
self.transformers = pefile.transformers
|
||||
self.target = pefile.target
|
||||
self.baseaddr = pefile.baseaddr
|
||||
return self
|
||||
|
||||
|
||||
class IMAGE_DOS_HEADER(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("e_magic", CHAR * 2),
|
||||
("e_cblp", WORD),
|
||||
("e_cp", WORD),
|
||||
("e_crlc", WORD),
|
||||
("e_cparhdr", WORD),
|
||||
("e_minalloc", WORD),
|
||||
("e_maxalloc", WORD),
|
||||
("e_ss", WORD),
|
||||
("e_sp", WORD),
|
||||
("e_csum", WORD),
|
||||
("e_ip", WORD),
|
||||
("e_cs", WORD),
|
||||
("e_lfarlc", WORD),
|
||||
("e_ovno", WORD),
|
||||
("e_res", WORD * 4),
|
||||
("e_oemid", WORD),
|
||||
("e_oeminfo", WORD),
|
||||
("e_res2", WORD * 10),
|
||||
("e_lfanew", DWORD),
|
||||
]
|
||||
|
||||
class PEFile(object):
|
||||
"""Represent a PE loaded in a process (current or remote)"""
|
||||
|
||||
def __init__(self, target, baseaddr, targetedbitness, transformers):
|
||||
self.target = target
|
||||
self.baseaddr = baseaddr
|
||||
self.bitness = targetedbitness
|
||||
self.transformers = transformers
|
||||
|
||||
if targetedbitness == 32:
|
||||
self.IMAGE_ORDINAL_FLAG = IMAGE_ORDINAL_FLAG32
|
||||
else:
|
||||
self.IMAGE_ORDINAL_FLAG = IMAGE_ORDINAL_FLAG64
|
||||
|
||||
def get_DOS_HEADER(self):
|
||||
return self.transformers.create_structure_at(IMAGE_DOS_HEADER, self.baseaddr)
|
||||
|
||||
def get_NT_HEADER(self):
|
||||
offset = self.get_DOS_HEADER().e_lfanew
|
||||
if self.bitness == 32:
|
||||
return self.transformers.create_structure_at(IMAGE_NT_HEADERS32, self.baseaddr + offset)
|
||||
return self.transformers.create_structure_at(IMAGE_NT_HEADERS64, self.baseaddr + offset)
|
||||
|
||||
|
||||
STANDARD_OPTIONAL_HEADER_TYPE_PER_MAGIC = {
|
||||
IMAGE_NT_OPTIONAL_HDR32_MAGIC: IMAGE_OPTIONAL_HEADER32,
|
||||
IMAGE_NT_OPTIONAL_HDR64_MAGIC: IMAGE_OPTIONAL_HEADER64,
|
||||
}
|
||||
|
||||
STANDARD_OPTIONAL_HEADER_SIZE_PER_MAGIC = (
|
||||
(IMAGE_NT_OPTIONAL_HDR32_MAGIC, ctypes.sizeof(IMAGE_OPTIONAL_HEADER32)),
|
||||
(IMAGE_NT_OPTIONAL_HDR64_MAGIC, ctypes.sizeof(IMAGE_OPTIONAL_HEADER64)),
|
||||
)
|
||||
|
||||
def get_OptionalHeader(self):
|
||||
# We can have a 32bits PE with a 64 bits OptionalHeader
|
||||
# Ex : PE32 .NET that allow to be loaded in 64b process
|
||||
# See: https://github.com/dotnet/runtime/blob/8bbe33819464216becffb7cf8b7ea8dd3bab5836/src/coreclr/src/vm/peimagelayout.cpp#L599
|
||||
# In this case the OptionalHeader is transformed in 64bits & OptionalHeader.Magic is changed accordingly
|
||||
# So we cannot just rely on get_NT_HEADER() to give us the correct OptionalHeader type. some re-check are required
|
||||
default_opth = self.get_NT_HEADER().OptionalHeader
|
||||
# Cannot juste compare types with type(default_opth) as it may be a remoteType
|
||||
current_opth_infos = (default_opth.Magic, ctypes.sizeof(default_opth))
|
||||
if current_opth_infos in self.STANDARD_OPTIONAL_HEADER_SIZE_PER_MAGIC:
|
||||
# The default OptionalHeader structure match what we expect based on the magic (most of the cases)
|
||||
return default_opth
|
||||
# Mismatch -> PE32 remapped as 64b (with OptionalHeader rewrite)
|
||||
# Remap the correct OptionalHeader
|
||||
opt_header_real_type = self.STANDARD_OPTIONAL_HEADER_TYPE_PER_MAGIC[default_opth.Magic]
|
||||
opt_header_addr = default_opth._base_addr if self.target else ctypes.addressof(default_opth)
|
||||
return self.transformers.create_structure_at(opt_header_real_type, opt_header_addr)
|
||||
|
||||
def get_DataDirectory(self):
|
||||
return self.get_OptionalHeader().DataDirectory
|
||||
|
||||
|
||||
def get_IMPORT_DESCRIPTORS(self):
|
||||
import_datadir = self.get_DataDirectory()[IMAGE_DIRECTORY_ENTRY_IMPORT]
|
||||
if import_datadir.VirtualAddress == 0:
|
||||
return []
|
||||
import_descriptor_addr = self.baseaddr + import_datadir.VirtualAddress
|
||||
current_import_descriptor = IMAGE_IMPORT_DESCRIPTOR.create(self, import_descriptor_addr)
|
||||
res = []
|
||||
while current_import_descriptor.FirstThunk:
|
||||
res.append(current_import_descriptor)
|
||||
import_descriptor_addr += ctypes.sizeof(IMAGE_IMPORT_DESCRIPTOR)
|
||||
current_import_descriptor = IMAGE_IMPORT_DESCRIPTOR.create(self, import_descriptor_addr)
|
||||
return res
|
||||
|
||||
def get_EXPORT_DIRECTORY(self):
|
||||
export_directory_rva = self.get_DataDirectory()[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
|
||||
if export_directory_rva == 0:
|
||||
return None
|
||||
export_directory_addr = self.baseaddr + export_directory_rva
|
||||
exp_dir = IMAGE_EXPORT_DIRECTORY.create(self, export_directory_addr)
|
||||
return exp_dir
|
||||
|
||||
@utils.fixedpropety
|
||||
def sections(self):
|
||||
nt_header = self.get_NT_HEADER()
|
||||
nb_section = nt_header.FileHeader.NumberOfSections
|
||||
SizeOfOptionalHeader = self.get_NT_HEADER().FileHeader.SizeOfOptionalHeader
|
||||
if self.target is None:
|
||||
opt_header_addr = ctypes.addressof(self.get_NT_HEADER().OptionalHeader)
|
||||
else:
|
||||
opt_header_addr = self.get_NT_HEADER().OptionalHeader._base_addr
|
||||
base_section = opt_header_addr + SizeOfOptionalHeader
|
||||
#pe_section_type = IMAGE_SECTION_HEADER
|
||||
return [PESection.create(self, base_section + (sizeof(IMAGE_SECTION_HEADER) * i)) for i in range(nb_section)]
|
||||
#sections_array = self.transformers.create_structure_at((self.PESection * nb_section), base_section)
|
||||
#return list(sections_array)
|
||||
|
||||
@utils.fixedpropety
|
||||
def exports(self):
|
||||
"""The exports of the PE in a dict. Keys are ordinal (:class:`int`) and name (:class:`str`).
|
||||
The values are the addresses of the exports.
|
||||
|
||||
:type: {(:class:`int` or :class:`str`) : :class:`int`}"""
|
||||
res = {}
|
||||
exp_dir = self.get_EXPORT_DIRECTORY()
|
||||
export_datadir = self.get_DataDirectory()[IMAGE_DIRECTORY_ENTRY_EXPORT]
|
||||
export_start = self.baseaddr + export_datadir.VirtualAddress
|
||||
export_end = export_start + export_datadir.Size
|
||||
if exp_dir is None:
|
||||
return res
|
||||
raw_exports = exp_dir.get_exports()
|
||||
for id, rva_addr, rva_name in raw_exports:
|
||||
if export_start <= rva_addr < export_end:
|
||||
# Export proxy...
|
||||
# Contains the string to another Dll.Function
|
||||
rva_addr = get_string(self.target, rva_addr) # Put the string proxy instead
|
||||
|
||||
res[id] = rva_addr
|
||||
if rva_name is not None:
|
||||
res[rva_name] = rva_addr
|
||||
return res
|
||||
|
||||
@utils.fixedpropety
|
||||
def export_name(self):
|
||||
"""The Name attribute of the ``EXPORT_DIRECTORY``"""
|
||||
exp_dir = self.get_EXPORT_DIRECTORY()
|
||||
if exp_dir is None:
|
||||
return None
|
||||
if not exp_dir.Name:
|
||||
return None
|
||||
return get_string(self.target, self.baseaddr + exp_dir.Name)
|
||||
|
||||
# TODO: get imports by parsing other modules exports if no INT
|
||||
@utils.fixedpropety
|
||||
def imports(self):
|
||||
"""The imports of the PE in a dict.
|
||||
Keys are the names of DLL to import from and values are :class:`list`
|
||||
of :class:`IATEntry`
|
||||
|
||||
:type: {:class:`str` : [:class:`IATEntry`]}"""
|
||||
res = {}
|
||||
for import_descriptor in self.get_IMPORT_DESCRIPTORS():
|
||||
INT = import_descriptor.get_INT(self)
|
||||
IAT = import_descriptor.get_IAT(self)
|
||||
if INT is not None:
|
||||
for iat_entry, (ord, name) in zip(IAT, INT):
|
||||
# str(name.decode()) -> python2 and python3 compatible for str result
|
||||
iat_entry.ord = ord
|
||||
iat_entry.name = str(name) if name else ""
|
||||
name = get_string(self.target, self.baseaddr + import_descriptor.Name)
|
||||
res.setdefault(name.lower(), []).extend(IAT)
|
||||
return res
|
||||
Reference in New Issue
Block a user