import ctypes.wintypes, sys, re
from keystone import *

# Function declaration to avoid calling getprocaddress every function call
_GetModuleHandleW = ctypes.windll.kernel32.GetModuleHandleW
_VirtualProtect = ctypes.windll.kernel32.VirtualProtect
_VirtualAlloc = ctypes.windll.kernel32.VirtualAlloc
_VirtualFree = ctypes.windll.kernel32.VirtualFree
_CreateThread = ctypes.windll.kernel32.CreateThread
_WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject
_CloseHandle = ctypes.windll.kernel32.CloseHandle
_FreeLibraryAndExitThread = ctypes.windll.kernel32.FreeLibraryAndExitThread
_CreateToolhelp32Snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot
_Module32First = ctypes.windll.kernel32.Module32First
_Module32Next = ctypes.windll.kernel32.Module32Next
# _EnumProcessModules = ctypes.windll.psapi.EnumProcessModules

_GetModuleHandleW.restype = ctypes.c_void_p
_VirtualAlloc.restype = ctypes.c_void_p

PAGE_EXECUTE_READWRITE = 0x40
MEM_RESERVE = 0x00002000
MEM_COMMIT = 0x00001000
MEM_FREE = 0x10000
MEM_RESERVE = 0x2000
MEM_DECOMMIT = 0x00004000
TH32CS_SNAPMODULE = 0x00000008

class LIST_ENTRY(ctypes.Structure):
    _fields_ = [("Flink", ctypes.c_void_p),
                ("Blink", ctypes.c_void_p)]


class UNICODE_STRING(ctypes.Structure):
    _fields_ = [("Length", ctypes.c_ushort),
                ("MaximumLength", ctypes.c_ushort),
                ("Buffer", ctypes.c_void_p)]


class LDR_DATA_TABLE_ENTRY(ctypes.Structure):
    _fields_ = [("InLoadOrderLinks", LIST_ENTRY),
                ("InMemoryOrderLinks", LIST_ENTRY),
                ("InInitializationOrderLinks", LIST_ENTRY),
                ("DllBase", ctypes.c_void_p),
                ("EntryPoint", ctypes.c_void_p),
                ("SizeOfImage", ctypes.c_uint),
                ("FullDllName", UNICODE_STRING),
                ("BaseDllName", UNICODE_STRING),]


class SECURITY_ATTRIBUTES(ctypes.Structure):
    _fields_ = [('nLength', ctypes.c_ulong),
                ('lpSecurityDescriptor', ctypes.c_void_p),
                ('bInheritHandle', ctypes.c_long)]

class IMAGE_DATA_DIRECTORY(ctypes.Structure):
    _fields_ = [
        ("VirtualAddress", ctypes.c_uint32),
        ("Size", ctypes.c_uint32),
    ]

class IMAGE_DOS_HEADER(ctypes.Structure):
    _fields_ = [
        ("e_magic", ctypes.c_uint16),
        ("e_cblp", ctypes.c_uint16),
        ("e_cp", ctypes.c_uint16),
        ("e_crlc", ctypes.c_uint16),
        ("e_cparhdr", ctypes.c_uint16),
        ("e_minalloc", ctypes.c_uint16),
        ("e_maxalloc", ctypes.c_uint16),
        ("e_ss", ctypes.c_uint16),
        ("e_sp", ctypes.c_uint16),
        ("e_csum", ctypes.c_uint16),
        ("e_ip", ctypes.c_uint16),
        ("e_cs", ctypes.c_uint16),
        ("e_lfarlc", ctypes.c_uint16),
        ("e_ovno", ctypes.c_uint16),
        ("e_res", ctypes.c_uint16 * 4),
        ("e_oemid", ctypes.c_uint16),
        ("e_oeminfo", ctypes.c_uint16),
        ("e_res2", ctypes.c_uint16 * 10),
        ("e_lfanew", ctypes.c_int32),
    ]
class IMAGE_OPTIONAL_HEADER(ctypes.Structure):
    _fields_ = [
        ("Magic", ctypes.c_uint16),
        ("MajorLinkerVersion", ctypes.c_uint8),
        ("MinorLinkerVersion", ctypes.c_uint8),
        ("SizeOfCode", ctypes.c_uint32),
        ("SizeOfInitializedData", ctypes.c_uint32),
        ("SizeOfUninitializedData", ctypes.c_uint32),
        ("AddressOfEntryPoint", ctypes.c_uint32),
        ("BaseOfCode", ctypes.c_uint32),
        ("ImageBase", ctypes.c_uint64),
        ("SectionAlignment", ctypes.c_uint32),
        ("FileAlignment", ctypes.c_uint32),
        ("MajorOperatingSystemVersion", ctypes.c_uint16),
        ("MinorOperatingSystemVersion", ctypes.c_uint16),
        ("MajorImageVersion", ctypes.c_uint16),
        ("MinorImageVersion", ctypes.c_uint16),
        ("MajorSubsystemVersion", ctypes.c_uint16),
        ("MinorSubsystemVersion", ctypes.c_uint16),
        ("Win32VersionValue", ctypes.c_uint32),
        ("SizeOfImage", ctypes.c_uint32),
        ("SizeOfHeaders", ctypes.c_uint32),
        ("CheckSum", ctypes.c_uint32),
        ("Subsystem", ctypes.c_uint16),
        ("DllCharacteristics", ctypes.c_uint16),
        ("SizeOfStackReserve", ctypes.c_uint64),
        ("SizeOfStackCommit", ctypes.c_uint64),
        ("SizeOfHeapReserve", ctypes.c_uint64),
        ("SizeOfHeapCommit", ctypes.c_uint64),
        ("LoaderFlags", ctypes.c_uint32),
        ("NumberOfRvaAndSizes", ctypes.c_uint32),
        ("DataDirectory", IMAGE_DATA_DIRECTORY * 16),
    ]
class IMAGE_EXPORT_DIRECTORY(ctypes.Structure):
    _fields_ = [
        ("Characteristics", ctypes.c_uint32),
        ("TimeDateStamp", ctypes.c_uint32),
        ("MajorVersion", ctypes.c_uint16),
        ("MinorVersion", ctypes.c_uint16),
        ("Name", ctypes.c_uint32),
        ("Base", ctypes.c_uint32),
        ("NumberOfFunctions", ctypes.c_uint32),
        ("NumberOfNames", ctypes.c_uint32),
        ("AddressOfFunctions", ctypes.c_uint32),
        ("AddressOfNames", ctypes.c_uint32),
        ("AddressOfNameOrdinals", ctypes.c_uint32)
    ]


class MODULEENTRY32(ctypes.Structure):
    _fields_ = [("dwSize", ctypes.wintypes.DWORD),
                ("th32ModuleID", ctypes.wintypes.DWORD),
                ("th32ProcessID", ctypes.wintypes.DWORD),
                ("th32ProcessID", ctypes.wintypes.DWORD),
                ("GlblcntUsage", ctypes.wintypes.DWORD),
                ("modBaseAddr", ctypes.wintypes.LPBYTE),
                ("modBaseSize", ctypes.wintypes.DWORD),
                ("hModule", ctypes.wintypes.HMODULE),
                ("szModule", ctypes.wintypes.CHAR * 256),
                ("szExePath", ctypes.wintypes.CHAR * 260)]
    
class Process():
    def __init__(self):
        self.__exports = {}
        self.__ks = Ks(KS_ARCH_X86, KS_MODE_64)
        self.buffer = None

    def write_bytes(self, address:int, new_bytes:bytes|tuple):
        """
        Writes bytes at a given address such as b0 01 c3 (mov al,01;ret;)

        If the function succeeds then the return is True otherwise its None
        
        """
        old_protect = ctypes.wintypes.DWORD()


        _VirtualProtect(ctypes.c_void_p(address), len(new_bytes), 0x40, ctypes.byref(old_protect)) # Make sure we can write to the address even if we cant (will fail on things like the peb meaning you still cant write to the peb for example you cant write to any python.exe strings in the peb if python.exe is the main running program)
        ctypes.memmove(ctypes.c_void_p(address), new_bytes, len(new_bytes))
        _VirtualProtect(ctypes.c_void_p(address), len(new_bytes), old_protect.value, ctypes.byref(old_protect))
        return True
    

    def write_ctype(self, address:int, ctype:ctypes.Structure):
        """
        Writes a specified ctype type's value to a specified memory address. If the memory region is read-only, the function temporarily changes its protection to allow writing, then restores the original protection afterward.
        
        
        """
        return self.write_bytes(address, ctypes.string_at(ctypes.byref(ctype), ctypes.sizeof(ctype)))
    
    def write_string(self, address:int, value, encoding='UTF-8'):
        """
        Writes a ctypes.c_string value to a specified memory address. If the memory region is read-only, the function temporarily changes its protection to allow writing, then restores the original protection afterward.
        
        
        """
        return self.write_bytes(address, value.encode(encoding))
    
    def write_short(self, address:int, value):
        """
        Writes a ctypes.c_short value to a specified memory address. If the memory region is read-only, the function temporarily changes its protection to allow writing, then restores the original protection afterward.
        
        
        """
        return self.write_ctype(address, ctypes.c_short(value))

    def write_longlong(self, address:int, value):
        """
        Writes a ctypes.c_longlong value to a specified memory address. If the memory region is read-only, the function temporarily changes its protection to allow writing, then restores the original protection afterward.
        
        
        """
        return self.write_ctype(address, ctypes.c_longlong(value))

    def write_long(self, address:int, value):
        """
        Writes a ctypes.c_long value to a specified memory address. If the memory region is read-only, the function temporarily changes its protection to allow writing, then restores the original protection afterward.
        
        
        """
        return self.write_ctype(address, ctypes.c_long(value))

    def write_int(self, address:int, value):
        """
        Writes a ctypes.c_int value to a specified memory address. If the memory region is read-only, the function temporarily changes its protection to allow writing, then restores the original protection afterward.
        
        
        """
        return self.write_ctype(address, ctypes.c_int(value))

    def write_float(self, address:int, value):
        """
        Writes a ctypes.c_float value to a specified memory address. If the memory region is read-only, the function temporarily changes its protection to allow writing, then restores the original protection afterward.
        
        
        """
        return self.write_ctype(address, ctypes.c_float(value))

    def write_double(self, address:int, value):
        """
        Writes a ctypes.c_double value to a specified memory address. If the memory region is read-only, the function temporarily changes its protection to allow writing, then restores the original protection afterward.
        
        
        """
        return self.write_ctype(address, ctypes.c_double(value))

    def write_char(self, address:int, value):
        """
        Writes a ctypes.c_char value to a specified memory address. If the memory region is read-only, the function temporarily changes its protection to allow writing, then restores the original protection afterward.
        
        
        """
        return self.write_ctype(address, ctypes.c_char(value))

    def write_bool(self, address:int, value):
        """
        Writes a ctypes.c_bool value to a specified memory address. If the memory region is read-only, the function temporarily changes its protection to allow writing, then restores the original protection afterward.
        
        
        """
        return self.write_ctype(address, ctypes.c_bool(value))

    def read_bytes(self, address:int, length:int):
        """
        Reads bytes from address and returns it as raw bytes object

        """
        try:
            return (ctypes.c_char * length).from_address(address).raw
        except: # This isnt needed when using windows functions since they have their own error handling but since this is our own func we need error handling
            return None
        # return ctypes.cast(address, ctypes.POINTER(ctypes.c_char * length)).contents.raw # Very very very bad and i have no idea why so i guess ill stick to from_address instead (using this cast method causes tons of null pointer errors and or length errors)

    def read_ctype(self, address:int, ctype:ctypes.Structure):
        """
        Reads a ctype type such as a ctype.Structure and returns it as a structure object

        """
        # size = ctypes.sizeof(ctype)
        # raw = (ctypes.c_char * size).from_address(address).raw
        # return ctype.from_buffer_copy(raw)
        try:
            return ctypes.cast(address, ctypes.POINTER(ctype)).contents
        except:
            return None
        
    def read_string(self, address:int, length:int=50, encoding:str='UTF-8'):
        """
        Reads a string but cannot read a Utf-16 string so use read_bytes with a hard coded length or instead of Utf-16

        """
        buff = self.read_bytes(address, length)
        i = buff.find(b'\x00')
        if i != -1:
            buff = buff[:i]
        buff = buff.decode(encoding)
        return buff
    
    def read_unicode_string(self, address:int, length:int):
        """
        Reads a unicode string at a specified address given its length

        If the function succeeds the return value is a decoded unicode string
        
        """
        data = self.read_bytes(address, length)
        if len(data) % 2 != 0:
            data = data[:-1]
        return data.decode("utf-16-le", errors="ignore").rstrip("\x00")


    def read_short(self, address:int):
        """
        Reads a ctypes.c_short from the specified address within the process
        
        
        """
        return self.read_ctype(address, ctypes.c_short).value

    def read_longlong(self, address:int):
        """
        Reads a ctypes.c_longlong from the specified address within the process
        
        
        """
        return self.read_ctype(address, ctypes.c_longlong).value

    def read_long(self, address:int):
        """
        Reads a ctypes.c_long from the specified address within the process
        
        
        """
        return self.read_ctype(address, ctypes.c_long).value

    def read_int(self, address:int):
        """
        Reads a ctypes.c_int from the specified address within the process
        
        
        """
        return self.read_ctype(address, ctypes.c_int).value

    def read_float(self, address:int):
        """
        Reads a ctypes.c_float from the specified address within the process
        
        
        """
        return self.read_ctype(address, ctypes.c_float).value

    def read_double(self, address:int):
        """
        Reads a ctypes.c_double from the specified address within the process
        
        
        """
        return self.read_ctype(address, ctypes.c_double).value

    def read_char(self, address:int):
        """
        Reads a ctypes.c_char from the specified address within the process
        
        
        """
        return self.read_ctype(address, ctypes.c_char).value

    def read_bool(self, address:int):
        """
        Reads a ctypes.c_bool from the specified address within the process
        
        
        """
        return self.read_ctype(address, ctypes.c_bool).value



    def __parse_module(self, module:str):
        """
        Parses a modules headers to be able to get function addresses/names

        This is an internal function
        -

        If the function succeeds the return value is a json dictionary containing the module information

        """
        module_base = self.get_module_handle(module)

        # Get the rva needed for OptionalHeader
        coff_offset = self.read_ctype(module_base, IMAGE_DOS_HEADER).e_lfanew

        # Get the export directory rva
        rva = self.read_ctype(module_base + coff_offset + 0x18, IMAGE_OPTIONAL_HEADER).DataDirectory[0].VirtualAddress

        # read the export directory
        export_directory = self.read_ctype(module_base + rva, IMAGE_EXPORT_DIRECTORY)

        # Declare constants for reads
        address_of_names = module_base + export_directory.AddressOfNames
        address_of_ordinals = module_base + export_directory.AddressOfNameOrdinals
        address_of_functions = module_base + export_directory.AddressOfFunctions

        # Read all addresses/values in one go for each type to reduce calls to speed up code (original code would be reading each address + each ordinal + each function rva per iteration)
        # new code only reads the string of the function each iteration
        AddressArrayType = ctypes.c_uint32 * export_directory.NumberOfNames
        name_address_array = self.read_ctype(address_of_names, AddressArrayType)

        OrdinalsArrayType = ctypes.c_short * export_directory.NumberOfNames
        ordinals_array = self.read_ctype(address_of_ordinals, OrdinalsArrayType)

        FunctionsArrayType = ctypes.c_uint32 * export_directory.NumberOfFunctions
        functions_array = self.read_ctype(address_of_functions, FunctionsArrayType)

        exports = {}
        if len(name_address_array) == 64: # No exports (this is for some reason only needed when reading local addresses but works without this when using ReadProcessMemory)
            return
        for i in range(len(name_address_array)):
            try:
                export_name = self.read_string(module_base + name_address_array[i], 256)
            except: # If read_string fails this means the pe file has no exports listed
                return exports
            export_ordinal = ordinals_array[i]
            export_rva = functions_array[export_ordinal]

            exports[export_name] = {'ordinal': export_ordinal, 'rva': hex(export_rva)}

        self.__exports[module.lower()] = exports
        
        return self.__exports[module.lower()]
    
    def get_modules(self):
        """
        Gets a list of modules using CreateToolhelp32Snapshot and also gets some values such as its full path, base of dll and size

        If the function succeeds the return value is a json object containing each dll's full path name, the base address and the size

        """
        modules = {}
        snapshot = _CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, 0)

        if snapshot:
            module = MODULEENTRY32()
            module.dwSize = ctypes.sizeof(module)
            _Module32First(snapshot, ctypes.byref(module))
            
            while True:
                module_name = module.szModule.decode().lower()
                module_path = module.szExePath.decode()
                # module_name = module.szExePath.decode().split('\\')[-1]
                modules[module_name] = {"Full path": module_path, "lpBaseOfDll": ctypes.cast(module.modBaseAddr, ctypes.c_void_p).value, "SizeOfImage": module.modBaseSize}
                module = MODULEENTRY32()
                module.dwSize = ctypes.sizeof(MODULEENTRY32)

                if not _Module32Next(snapshot, ctypes.byref(module)):
                    break

            _CloseHandle(snapshot)
        
        return modules
    
    def get_module_from_name(self, module:str):
        """
        Gets a module using CreateToolhelp32Snapshot and also gets some values such as its full path, base of dll and size

        If the function succeeds the return value is a json object containing the dll's full path name, the base address and the size

        """

        return self.get_modules().get(module)



    
    def get_modules_from_peb(self):
        """
        Gets the module names loaded within the process by walking the InMemoryOrderModulesList in the Ldr in the peb
        
        Requires write access
        -

        If the function succeeds the return value is the list of the currently loaded modules and various info.
        """
        
        
        address = self.get_peb()
        address = self.read_longlong(address + 0x18)
        address = self.read_longlong(address + 0x10)
        first_address = address
        modules = {}

        # Use structures to read the memory instead of reading offsets (this makes it run a couple milliseconds faster and makes the code more mutable and readable)
        while True:
            # Read the structure contents
            module_info = self.read_ctype(address, LDR_DATA_TABLE_ENTRY)
            
            # Set the address to the next module in the LIST_ENTRY
            address = self.read_longlong(address)

            # If the module name length is 0 that means theirs not actually a valid module here so we skip to the next module
            if not module_info.BaseDllName.Length:
                pass

            # Read the fields to get the name and full path of the dll
            # module_name = self.read_bytes(module_info.BaseDllName.Buffer, module_info.BaseDllName.Length).decode('utf-16le') # Replaced with a dedicated read_unicode_string instead
            # module_path = self.read_bytes(module_info.FullDllName.Buffer, module_info.FullDllName.Length).decode('utf-16le') # Replaced with a dedicated read_unicode_string instead

            if module_info.BaseDllName.Buffer and module_info.BaseDllName.Length and module_info.FullDllName.Buffer and module_info.FullDllName.Length: # Apparently all of a sudden in this windows install sometimes we get a invalid buffer or invalid length so this wasnt needed before
                module_name = self.read_unicode_string(module_info.BaseDllName.Buffer, module_info.BaseDllName.Length)
                module_path = self.read_unicode_string(module_info.FullDllName.Buffer, module_info.FullDllName.Length)
            
            if address == first_address:
                break
            
            if not module_name in modules:
                modules[module_name] = {"Full path": module_path, "lpBaseOfDll": module_info.DllBase, "EntryPoint": module_info.EntryPoint, "SizeOfImage": module_info.SizeOfImage}

        return modules
    
    def get_all_exports(self):
        """
        Gets a list of all loaded modules and their exports

        If the function succeeds the return value is a json containing every found module and their exports name's rva's and ordinals 
        
        """
        modules = self.get_modules()
            
        for module in modules:
            self.__parse_module(module)
        
        return self.__exports
        
    def get_module_exports(self, module:str):
        """
        Gets a list of the specified modules exports

        If the function succeeds the return value is a json caontaining the module's export name's rva's and ordinals 
        
        """
        self.__parse_module(module)
        return self.__exports.get(module)

    def get_proc_address(self, module:str, function:str):
        """
        Gets the address of the specified function without calling GetProcAddress


        If the function suceeds the return value is the address of the function
        
        """
        module_base = self.get_module_handle(module)
        if self.__exports.get(module.lower()):
            return module_base + int(self.__exports.get(module.lower())[function]['rva'], 16)


        return module_base + int(self.__parse_module(module_base, module).get(function)['rva'], 16)
    
    def get_module_handle(self, module:str):
        """
        Gets the handle of a specified module

        If the function succeeds the return value is a nonzero
        
        """
        return _GetModuleHandleW(module)
    
    def get_peb(self):
        """
        Gets the address of the start of the peb


        If the function succeeds the return value is nonzero
        
        """
        peb_return = self.allocate(8) # i for some reason had this labeled as "addr" with 8 bytes and had it slapped in mov rbx, {hex(peb_return)} as mov rbx, {hex(addr)} this meant when this ran it overrided the original asm at offset 0x0 idk how i was so stupid to not realize this lmao
        
        GET_PEB = f"""
            mov rax, gs:[0x60];
            mov [{hex(peb_return)}], rax;
            ret;
        """

        self.execute_code(GET_PEB)

        peb = self.read_longlong(peb_return)

        return peb

    
    def allocate(self, amount:int):
        """
        Allocates the specified amount of bytes within the process

        If the function succeeds the return value is nonzero
        
        """
        # buffer = ctypes.create_string_buffer(amount)
        # self.buffer = buffer
        # return ctypes.addressof(buffer)
        self.buffer = _VirtualAlloc(None, ctypes.c_void_p(amount), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)
        return self.buffer
    
    def free(self, address:int):
        """
        Frees previously allocated memory at the specified address

        If the function succeeds, the return value is nonzero
        
        """
        return _VirtualFree(ctypes.c_void_p(address), 0, MEM_DECOMMIT)
    
    def execute_code(self, code:bytes|str):
        """
        Executes assembly in process given its machine code or bytes
        
        Once the code has executed and exited it will automatically be cleaned up

        Requires write access
        -

        """
        if not isinstance(code, (bytes, str)):
            raise TypeError("Invalid parameter, only valid type is a string or bytes")
        
        if isinstance(code, str):
            code = self.assemble(code)

        assembly_address = self.allocate(len(code))
        self.write_bytes(assembly_address, code)
        self.start_thread(assembly_address)

        self.free(assembly_address)
        return True
    

    def execute_code_async(self, code:bytes|str):
        """
        Executes assembly in a process given its machine code or bytes asynchronously
        
        Will not deallocate memory when it finishes its thread

        Requires write access
        -

        Return value
        -

        If the function succeeds the return value is a list containing the thread handle and the address where the code is being executed at

        """

        if not isinstance(code, (bytes, str)):
            raise TypeError("Invalid parameter, only valid type is a string or bytes")
        
        if isinstance(code, str):
            code = self.assemble(code)

        assembly_address = self.allocate(len(code))
        self.write_bytes(assembly_address, code, len(code))


        thread = self.start_thread(assembly_address, _async=True)

        return (thread, assembly_address)




    def start_thread(self, address:int, params:int=0, _async:bool=False):
        """
        Starts a thread at the given address

        If the function succeeds the return value is either True or a handle to the thread that is running if _async=True
        
        """
        NULL_SECURITY_ATTRIBUTES = ctypes.cast(0, ctypes.POINTER(SECURITY_ATTRIBUTES))
        hThread = _CreateThread(NULL_SECURITY_ATTRIBUTES, 0, ctypes.c_void_p(address), params, 0)
        if not _async:
            _WaitForSingleObject(hThread, -1)
            _CloseHandle(hThread)
        else:
            return hThread

        return True

    def assemble(self, code:str):
        """
        Assembles 64 bit machine code into opcodes

        If the function succeeds the return value is a bytes object
        
        """
        return self.__ks.asm(code, as_bytes=True)[0]
    
    def hide_module(self, module:str):
        """
        Hides the specified module in process by removing it's flink and blink in the peb
        
        Requires write access
        -

        If the function succeeds the return value is nonzero

        """
        #TODO: maybe replace all these "read_longlong" to read_ctype and just read the whole structure or something (most likely not gonna ever do that though)


        # This is put in a seperate function for nicer looking code
        def replace_flink_blink(_address):
            # Get current module's blink and flink
            flink = self.read_longlong(_address + 0x0)
            blink = self.read_longlong(_address + 0x8)


            # Go to next module and replace its blink
            previous_module = self.read_longlong(_address)
            self.write_longlong(previous_module + 0x8, blink)


            # Go to next module and replace its flink
            next_module = self.read_longlong(_address + 0x8)
            self.write_longlong(next_module + 0x0, flink)
            
        # Helper function for replacing multiple module list pointers
        def process_module_list(module_list, module, offset):
            modules = set()
            
            while True:
                unicode_string = module_list + offset

                
                length = self.read_short(unicode_string)
                max_length = self.read_short(unicode_string + 0x2)
                buffer_addr = self.read_longlong(unicode_string + 0x8)
                if buffer_addr and length: # Apparently all of a sudden in this windows install sometimes we get a invalid buffer or invalid length so this wasnt needed before
                    module_name = self.read_unicode_string(buffer_addr, length)

                if module_name in modules:
                    raise MemoryError("The specified module could not be found or isn't present within the process or cannot be overridden")

                modules.add(module_name)

                # Compare ignoring case (Windows module names are case-insensitive)
                if module_name.lower() == module.lower():
                    replace_flink_blink(module_list)
                    break

                # Move to the next module in the linked list
                module_list = self.read_longlong(module_list)

            return modules
            
            
            
        if not isinstance(module, str):
            raise TypeError("Wrong type, only allowed type is a string")
        


        address = self.read_longlong(self.get_peb() + 0x18)
        
        
        InLoadOrderModuleList = self.read_longlong(address + 0x10)
        InMemoryOrderModuleList = self.read_longlong(address + 0x20)
        InInitializationOrderModuleList = self.read_longlong(address + 0x30)
        
        process_module_list(InLoadOrderModuleList, module, 0x58)
        process_module_list(InMemoryOrderModuleList, module, 0x48)
        process_module_list(InInitializationOrderModuleList, module, 0x58 - 0x020)
        return True

    def hide_python(self):
        """
        Hides the currently loaded python dll in the peb

        If the function succeeds the return value is nonzero


        """
        one = self.hide_module('python{0}{1}.dll'.format(sys.version_info.major, sys.version_info.minor))
        two = self.hide_module('python{0}.dll'.format(sys.version_info.major))
        return (one, two) 
    
    def unload_module(self, module:str):
        """
        Unloads a module from process

        If the function succeeds the return value is nonzero
        
        """
        if not isinstance(module, str):
            raise TypeError("Wrong type, only allowed type is a string")
        
        _FreeLibraryAndExitThread(self.get_module_handle(module), 0)
        return True
    
    def pattern_scan_module(self, module:str, pattern:bytes, return_multiple:bool=False):
        """
        Scans a specified module for a match

        If the function succeeds the return value is the address at which the pattern was found
        
        """
        module_data = self.get_module_from_name(module)
        if not module_data:
            return
        
        module_base = module_data.get('lpBaseOfDll')
        if not module_base:
            return
        
        module_bytes = self.read_bytes(module_base, module_data.get('SizeOfImage'))
        if not module_bytes:
            return
        
        patt = re.compile(re.escape(pattern))
        if return_multiple:
            found = patt.finditer(module_bytes)
            found = [module_base + i.start() for i in found]
        else:
            found = patt.search(module_bytes)
            found = module_base + found.start()

        return found


    def pattern_scan_all_modules(self, pattern:bytes, return_multiple:bool=False):
        """
        Scans a specified module for a match

        If the function succeeds the return value is the address at which the pattern was found
        
        """
        modules = self.get_modules()
        found = []
        for i in modules:
            data = self.pattern_scan_module(i, pattern, return_multiple)
            found = data if data else None
            if not return_multiple:
                break

        return found