internal TDelegate CreateDelegateForSyscall <TDelegate>() where TDelegate : class { // Get the address of the function var functionAddress = _ntDllAddress + (int)_ntDllFunctions.Find(function => function.Name == typeof(TDelegate).Name.Replace("Definition", "")).Offset; // Copy the first 8 bytes of the function var functionBytes = new byte[8]; Marshal.Copy(functionAddress, functionBytes, 0, 8); // Retrieve the syscall index from the bytes var syscallIndexBytes = Environment.Is64BitProcess ? functionBytes.Skip(4).Take(4) : functionBytes.Skip(1).Take(4); var syscallIndex = BitConverter.ToUInt32(syscallIndexBytes.ToArray(), 0); // Create the shellcode used to perform the syscall var shellcode = Environment.Is64BitProcess ? SyscallX64.GetShellcode(syscallIndex) : SyscallX86.GetShellcode(syscallIndex); // Store the shellcode in a buffer var shellcodeBuffer = MemoryTools.AllocateMemoryForBuffer(shellcode.Length); _shellcodeAddresses.Add(shellcodeBuffer); Marshal.Copy(shellcode, 0, shellcodeBuffer, shellcode.Length); // Create a delegate to perform the syscall return(Marshal.GetDelegateForFunctionPointer <TDelegate>(shellcodeBuffer)); }
// Since some items don't stack, it's possible to have several of the target item in your inventory. The state // of each one is tracked and returned when the split becomes relevant. public ItemState[] GetItemStates(int baseId, int category) { ItemId id = new ItemId(baseId, category); // It's assumed that an item ID will only be queried if it was present in the list of saved splits // (although it may not have been acquired yet). if (!tracker.TryGetValue(id, out List <IntPtr> list) || list.Count == 0) { return(new ItemState[0]); } ItemState[] states = new ItemState[list.Count]; for (int i = 0; i < list.Count; i++) { IntPtr address = list[i]; int rawId = ComputeRawId(address); int count = MemoryTools.ReadByte(Handle, address + countOffset); int mods = 0; int reinforcement = 0; if (rawId / 10000 > 0) { ComputeUpgrades(rawId, out mods, out reinforcement); } states[i] = new ItemState(mods, reinforcement, count); } return(states); }
internal Enumerations.MemoryProtectionType Invoke(SafeProcessHandle processHandle, IntPtr baseAddress, int protectionSize, Enumerations.MemoryProtectionType newProtectionType) { // Store the base address of the memory region to protect in a buffer var baseAddressBuffer = MemoryTools.StoreStructureInBuffer(baseAddress); // Store the protection size in a buffer var protectionSizeBuffer = MemoryTools.StoreStructureInBuffer(protectionSize); // Initialise a buffer to store the returned old protection of the memory region var oldProtectionBuffer = MemoryTools.AllocateMemoryForBuffer(sizeof(ulong)); // Perform the syscall var syscallResult = _ntProtectVirtualMemoryDelegate(processHandle, baseAddressBuffer, protectionSizeBuffer, newProtectionType, oldProtectionBuffer); if (syscallResult != Enumerations.NtStatus.Success) { ExceptionHandler.ThrowWin32Exception("Failed to protect memory in the target process", syscallResult); } // Marshal the returned old protection of the memory region from the buffer var oldProtection = (Enumerations.MemoryProtectionType)Marshal.PtrToStructure <ulong>(oldProtectionBuffer); MemoryTools.FreeMemoryForBuffer(baseAddressBuffer); MemoryTools.FreeMemoryForBuffer(protectionSizeBuffer); MemoryTools.FreeMemoryForBuffer(oldProtectionBuffer); return(oldProtection); }
public override void SetItems(List <ItemId> itemIds) { SetItems(itemIds, DefaultSlots, DefaultSlots); IntPtr address = Start + (DefaultSlots - 1) * Step; if (OpenSlots.Count == 0) { return; } // Since the item count above isn't accurate (just a default value large enough to reasonably accommodate // most scenarios), fake slots at the end of the list must be removed in order for the initial state to be // correct. for (int i = 0; i < DefaultSlots; i++) { int rawId = MemoryTools.ReadInt(Handle, address); address -= Step; if (rawId == -1) { OpenSlots.RemoveLast(); TotalSlots--; } else { break; } } }
internal IntPtr Invoke(SafeProcessHandle processHandle, int allocationSize, Enumerations.MemoryProtectionType protectionType) { // Initialise a buffer to store the returned address of the allocated memory region var memoryRegionAddressBuffer = MemoryTools.AllocateMemoryForBuffer(IntPtr.Size); // Store the size of the allocation in a buffer var allocationSizeBuffer = MemoryTools.StoreStructureInBuffer(allocationSize); // Perform the syscall const Enumerations.MemoryAllocationType allocationType = Enumerations.MemoryAllocationType.Commit | Enumerations.MemoryAllocationType.Reserve; var syscallResult = _ntAllocateVirtualMemoryDelegate(processHandle, memoryRegionAddressBuffer, 0, allocationSizeBuffer, allocationType, protectionType); if (syscallResult != Enumerations.NtStatus.Success) { ExceptionHandler.ThrowWin32Exception("Failed to allocate memory in the target process", syscallResult); } // Marshal the returned address of the memory region from the buffer var memoryRegionAddress = Marshal.PtrToStructure <IntPtr>(memoryRegionAddressBuffer); MemoryTools.FreeMemoryForBuffer(memoryRegionAddressBuffer); MemoryTools.FreeMemoryForBuffer(allocationSizeBuffer); return(memoryRegionAddress); }
internal SafeThreadHandle Invoke(SafeProcessHandle processHandle, IntPtr startAddress, IntPtr parameter) { // Initialise a buffer to store the returned thread handle var threadHandleBuffer = MemoryTools.AllocateMemoryForBuffer(IntPtr.Size); // Perform the syscall const Enumerations.ThreadAccessMask desiredAccess = Enumerations.ThreadAccessMask.SpecificRightsAll | Enumerations.ThreadAccessMask.StandardRightsAll; var syscallResult = _ntCreateThreadExDelegate(threadHandleBuffer, desiredAccess, IntPtr.Zero, processHandle, startAddress, parameter, Enumerations.ThreadCreationType.HideFromDebugger, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); if (syscallResult != Enumerations.NtStatus.Success) { ExceptionHandler.ThrowWin32Exception("Failed to create a thread in the target process", syscallResult); } // Marshal the returned thread handle from the buffer var threadHandle = new SafeThreadHandle(Marshal.PtrToStructure <IntPtr>(threadHandleBuffer), true); MemoryTools.FreeMemoryForBuffer(threadHandleBuffer); return(threadHandle); }
public override void Refresh() { int newTotal = MemoryTools.ReadInt(Handle, itemCount); if (newTotal != totalItems) { if (newTotal > totalItems) { // It's pretty common to acquire multiple items at once (such as an entire armor set). int pickupCount = newTotal - totalItems; AddItems(pickupCount); } // This means that items were lost (dropped, sold, etc.). else { // I'm fairly certain that only one item can be removed from the inventory at one time (via drop or // giving items to an NPC), but I'm not completely sure. A loop guarantees that the autosplitter's // internal inventory state remains correct. int dropCount = totalItems - newTotal; RemoveItems(dropCount); } totalItems = newTotal; } }
internal PortableExecutableParser(byte[] dllBytes) { _dllBuffer = MemoryTools.StoreBytesInBuffer(dllBytes); _peHeaders = new PeHeaders(); ReadHeaders(); }
internal PortableExecutableParser(string dllPath) { _dllBuffer = MemoryTools.StoreBytesInBuffer(File.ReadAllBytes(dllPath)); _peHeaders = new PeHeaders(); ReadHeaders(); }
public void Dispose() { foreach (var shellcodeAddress in _shellcodeAddresses) { // Free the memory allocated for the shellcode MemoryTools.FreeMemoryForBuffer(shellcodeAddress); } }
protected override int ComputeRawId(IntPtr address) { // Data is stored in the bottomless box a little differently than the main inventory. In the main // inventory, raw ID is stored as an integer (four bytes), with category as a single byte at a different // address. In contrast, the bottomless box stores both raw ID and category in a single integer, with three // bytes devoted to the ID and the fourth representing category. byte[] bytes = MemoryTools.ReadBytes(Handle, address, 4); return(TrimLastByte(bytes)); }
internal TStructure ReadVirtualMemory <TStructure>(IntPtr baseAddress) where TStructure : struct { // Read the bytes of the structure from the memory region var structureBytes = ReadVirtualMemory(baseAddress, Marshal.SizeOf <TStructure>()); // Marshal the bytes into a structure var structureBytesBuffer = MemoryTools.StoreBytesInBuffer(structureBytes); var structure = Marshal.PtrToStructure <TStructure>(structureBytesBuffer); MemoryTools.FreeMemoryForBuffer(structureBytesBuffer); return(structure); }
internal IntPtr Invoke(SafeProcessHandle processHandle, IntPtr baseAddress) { // Initialise a buffer to store the returned memory basic information structure in var memoryBasicInformationBuffer = MemoryTools.AllocateMemoryForBuffer(Marshal.SizeOf <Structures.MemoryBasicInformation>()); // Perform the syscall var syscallResult = _ntQueryVirtualMemoryDelegate(processHandle, baseAddress, Enumerations.MemoryInformationClass.BasicInformation, memoryBasicInformationBuffer, (ulong)Marshal.SizeOf <Structures.MemoryBasicInformation>(), IntPtr.Zero); if (syscallResult != Enumerations.NtStatus.Success) { ExceptionHandler.ThrowWin32Exception("Failed to query memory in the target process", syscallResult); } return(memoryBasicInformationBuffer); }
internal IntPtr Invoke(SafeProcessHandle processHandle, IntPtr baseAddress, int bytesToRead) { // Initialise a buffer to store the returned bytes read var bytesReadBuffer = MemoryTools.AllocateMemoryForBuffer(bytesToRead); // Perform the syscall var syscallResult = _ntReadVirtualMemoryDelegate(processHandle, baseAddress, bytesReadBuffer, (ulong)bytesToRead, IntPtr.Zero); if (syscallResult != Enumerations.NtStatus.Success) { ExceptionHandler.ThrowWin32Exception("Failed to read memory from the target process", syscallResult); } return(bytesReadBuffer); }
internal void WriteVirtualMemory <TStructure>(IntPtr baseAddress, TStructure structureToWrite) where TStructure : struct { // Store the structure in a buffer var structureBuffer = MemoryTools.StoreStructureInBuffer(structureToWrite); // Convert the structure into bytes var structureSize = Marshal.SizeOf <TStructure>(); var structureBytes = new byte[structureSize]; Marshal.Copy(structureBuffer, structureBytes, 0, structureSize); // Write the bytes of the structure into the target process WriteVirtualMemory(baseAddress, structureBytes); }
internal IntPtr Invoke(SafeProcessHandle processHandle, Enumerations.ProcessInformationClass processInformationClass) { // Initialise a buffer to store the returned process information structure var bufferSize = processInformationClass == Enumerations.ProcessInformationClass.BasicInformation ? Marshal.SizeOf <Structures.ProcessBasicInformation>() : sizeof(ulong); var processInformationBuffer = MemoryTools.AllocateMemoryForBuffer(bufferSize); // Perform the syscall var syscallResult = _ntQueryInformationProcessDelegate(processHandle, processInformationClass, processInformationBuffer, (ulong)bufferSize, IntPtr.Zero); if (syscallResult != Enumerations.NtStatus.Success) { ExceptionHandler.ThrowWin32Exception("Failed to query the information of the target process", syscallResult); } return(processInformationBuffer); }
internal bool Call() { var localDllBaseAddress = MemoryTools.StoreBytesInBuffer(_propertyWrapper.DllBytes); var peHeaders = _propertyWrapper.PeParser.GetHeaders(); // Build the import table of the DLL in the local process BuildImportTable(localDllBaseAddress); // Allocate memory for the DLL in the target process var dllSize = _propertyWrapper.TargetProcess.IsWow64 ? peHeaders.NtHeaders32.OptionalHeader.SizeOfImage : peHeaders.NtHeaders64.OptionalHeader.SizeOfImage; var remoteDllAddress = _propertyWrapper.MemoryManager.AllocateVirtualMemory((int)dllSize, Enumerations.MemoryProtectionType.ExecuteReadWrite); // Perform the needed relocations in the local process PerformRelocations(localDllBaseAddress, remoteDllAddress); // Map the sections of the DLL into the target process MapSections(localDllBaseAddress, remoteDllAddress); // Call any TLS callbacks CallTlsCallbacks(remoteDllAddress); // Call the entry point of the DLL var dllEntryPointAddress = _propertyWrapper.TargetProcess.IsWow64 ? (uint)remoteDllAddress + peHeaders.NtHeaders32.OptionalHeader.AddressOfEntryPoint : (ulong)remoteDllAddress + peHeaders.NtHeaders64.OptionalHeader.AddressOfEntryPoint; if (dllEntryPointAddress != 0) { CallEntryPoint(remoteDllAddress, (IntPtr)dllEntryPointAddress); } MemoryTools.FreeMemoryForBuffer(localDllBaseAddress); return(true); }
public override void Refresh() { IntPtr nextSlot = Start + (OpenSlots.Count > 0 ? OpenSlots.First.Value : TotalSlots) * Step; int value = MemoryTools.ReadInt(Handle, nextSlot); // Unlike the main inventory, there's no memory location that directly stores the number of items in the // bottomless box (or at least I wasn't able to find one). As such, item addition/removal must be detected // by checking slots directly. Also note that only one item can be put into the bottomless box at a time // (as opposed to picking up multiple items at once off the ground). if (value != -1) { AddItems(1); return; } LinkedListNode <int> slot = OpenSlots.First; // Since the bottomless box can't track count directly, all non-empty slots must be scanned each tick in // order to detect when one of them becomes empty. for (int i = 0; i < TotalSlots; i++) { if (slot != null && i == slot.Value) { slot = slot.Next; continue; } IntPtr address = Start + i * Step; value = MemoryTools.ReadInt(Handle, address); if (value == -1) { RemoveItems(1, address); return; } } }
internal List <Structures.LdrDataTableEntry32> GetWow64PebEntries() { var pebEntries = new List <Structures.LdrDataTableEntry32>(); // Query the target process for the base address of the WOW64 PEB var pebBaseAddressBuffer = (IntPtr)_syscallManager.InvokeSyscall <NtQueryInformationProcess>(Handle, Enumerations.ProcessInformationClass.Wow64Information); var pebBaseAddress = Marshal.PtrToStructure <ulong>(pebBaseAddressBuffer); // Read the WOW64 PEB of the target process var peb = _memoryManager.ReadVirtualMemory <Structures.Peb32>((IntPtr)pebBaseAddress); // Read the loader data of the WOW64 PEB var pebLoaderData = _memoryManager.ReadVirtualMemory <Structures.PebLdrData32>((IntPtr)peb.Ldr); var currentPebEntry = pebLoaderData.InLoadOrderModuleList.Flink; while (true) { if (currentPebEntry == pebLoaderData.InLoadOrderModuleList.Blink) { break; } // Read the current entry from the InLoadOrder doubly linked list var pebEntry = _memoryManager.ReadVirtualMemory <Structures.LdrDataTableEntry32>((IntPtr)currentPebEntry); pebEntries.Add(pebEntry); // Get the address of the next entry in the InLoadOrder doubly linked list currentPebEntry = pebEntry.InLoadOrderLinks.Flink; } MemoryTools.FreeMemoryForBuffer(pebBaseAddressBuffer); return(pebEntries); }
internal void WriteVirtualMemory(IntPtr baseAddress, byte[] bytesToWrite) { // Store the bytes to write in a buffer var bytesBuffer = MemoryTools.StoreBytesInBuffer(bytesToWrite); // Adjust the protection of the memory region to ensure it has write privileges var oldProtectionType = ProtectVirtualMemory(baseAddress, bytesToWrite.Length, Enumerations.MemoryProtectionType.ReadWrite); // Write the bytes into the memory region _syscallManager.InvokeSyscall <NtWriteVirtualMemory>(_processHandle, baseAddress, bytesBuffer, bytesToWrite.Length); // Restore the protection of the memory region ProtectVirtualMemory(baseAddress, bytesToWrite.Length, oldProtectionType); MemoryTools.FreeMemoryForBuffer(bytesBuffer); }
protected override ItemId ComputeItemId(IntPtr address) { // Data is stored in the bottomless box a little differently than the main inventory. In the main // inventory, raw ID is stored as an integer (four bytes), with category as a single byte at a different // address. In contrast, the bottomless box stores both raw ID and category in a single integer, with three // bytes devoted to the ID and the fourth representing category. byte[] bytes = MemoryTools.ReadBytes(Handle, address, 4); int category = bytes[3]; // This means that the slot is empty. if (category == byte.MaxValue) { return(null); } int rawId = TrimLastByte(bytes); return(ComputeItemId(rawId, Utilities.ToHex(category))); }
internal IntPtr Invoke(SafeThreadHandle threadHandle) { // Store a context structure in a buffer var context = new Structures.Context { ContextFlags = Enumerations.ContextFlags.Control }; var contextBuffer = MemoryTools.StoreStructureInBuffer(context); // Perform the syscall var syscallResult = _ntGetContextThreadDelegate(threadHandle, contextBuffer); if (syscallResult != Enumerations.NtStatus.Success) { ExceptionHandler.ThrowWin32Exception("Failed to get the context of a thread in the target process", syscallResult); } return(contextBuffer); }
internal SafeProcessHandle Invoke(int processId) { // Initialise a buffer to store the returned process handle var processHandleBuffer = MemoryTools.AllocateMemoryForBuffer(IntPtr.Size); // Store an empty object attributes structure in a buffer var objectAttributesBuffer = MemoryTools.StoreStructureInBuffer(new Structures.ObjectAttributes()); // Store a client id structure in a buffer var clientId = new Structures.ClientId { UniqueProcess = new IntPtr(processId), UniqueThread = IntPtr.Zero }; var clientIdBuffer = MemoryTools.StoreStructureInBuffer(clientId); // Perform the syscall var syscallResult = _ntOpenProcessDelegate(processHandleBuffer, Enumerations.ProcessAccessMask.AllAccess, objectAttributesBuffer, clientIdBuffer); if (syscallResult != Enumerations.NtStatus.Success) { ExceptionHandler.ThrowWin32Exception("Failed to open a handle to the target process", syscallResult); } // Marshal the returned process handle from the buffer var processHandle = new SafeProcessHandle(Marshal.PtrToStructure <IntPtr>(processHandleBuffer), true); MemoryTools.FreeMemoryForBuffer(processHandleBuffer); MemoryTools.FreeMemoryForBuffer(objectAttributesBuffer); MemoryTools.FreeMemoryForBuffer(clientIdBuffer); return(processHandle); }
internal byte[] ReadVirtualMemory(IntPtr baseAddress, int bytesToRead) { // Adjust the protection of the memory region to ensure it has read privileges var oldProtectionType = ProtectVirtualMemory(baseAddress, bytesToRead, Enumerations.MemoryProtectionType.ReadWrite); // Read the specified number of bytes from the memory region var bytesReadBuffer = (IntPtr)_syscallManager.InvokeSyscall <NtReadVirtualMemory>(_processHandle, baseAddress, bytesToRead); var bytesRead = new byte[bytesToRead]; Marshal.Copy(bytesReadBuffer, bytesRead, 0, bytesToRead); // Restore the protection of the memory region ProtectVirtualMemory(baseAddress, bytesToRead, oldProtectionType); MemoryTools.FreeMemoryForBuffer(bytesReadBuffer); return(bytesRead); }
internal void Invoke(SafeProcessHandle processHandle, IntPtr baseAddress) { // Store the base address of memory region to free in a buffer var baseAddressBuffer = MemoryTools.StoreStructureInBuffer(baseAddress); // Store the free size in a buffer var freeSizeBuffer = MemoryTools.StoreStructureInBuffer(0); // Perform the syscall var syscallResult = _ntFreeVirtualMemoryDelegate(processHandle, baseAddressBuffer, freeSizeBuffer, Enumerations.MemoryFreeType.Release); if (syscallResult != Enumerations.NtStatus.Success) { ExceptionHandler.ThrowWin32Exception("Failed to free memory in the target process", syscallResult); } MemoryTools.FreeMemoryForBuffer(baseAddressBuffer); MemoryTools.FreeMemoryForBuffer(freeSizeBuffer); }
public void Dispose() { MemoryTools.FreeMemoryForBuffer(_dllBuffer); }
internal bool Call() { // Get the address of the LoadLibraryW function var loadLibraryAddress = _propertyWrapper.TargetProcess.GetFunctionAddress("kernel32.dll", "LoadLibraryW"); // Write the DLL path into the target process var dllPathBuffer = _propertyWrapper.MemoryManager.AllocateVirtualMemory(_propertyWrapper.DllPath.Length, Enumerations.MemoryProtectionType.ExecuteReadWrite); var dllPathBytes = Encoding.Unicode.GetBytes(_propertyWrapper.DllPath + "\0"); _propertyWrapper.MemoryManager.WriteVirtualMemory(dllPathBuffer, dllPathBytes); // Open a handle to the first thread in the target process var threadHandle = (SafeThreadHandle)_propertyWrapper.SyscallManager.InvokeSyscall <NtOpenThread>(_propertyWrapper.TargetProcess.Process.Threads[0].Id); if (_propertyWrapper.TargetProcess.IsWow64) { // Suspend the thread if (PInvoke.Wow64SuspendThread(threadHandle) == -1) { ExceptionHandler.ThrowWin32Exception("Failed to suspend a thread in the target process"); } // Get the context of the thread var threadContextBuffer = MemoryTools.StoreStructureInBuffer(new Structures.Wow64Context { ContextFlags = Enumerations.ContextFlags.Control }); if (PInvoke.Wow64GetThreadContext(threadHandle, threadContextBuffer) == 0) { ExceptionHandler.ThrowWin32Exception("Failed to get the context of a thread in the target process"); } var threadContext = Marshal.PtrToStructure <Structures.Wow64Context>(threadContextBuffer); // Write the shellcode used to call LoadLibraryW from the thread into the target process var shellcode = ThreadHijackX86.GetShellcode((IntPtr)threadContext.Eip, dllPathBuffer, loadLibraryAddress); var shellcodeBuffer = _propertyWrapper.MemoryManager.AllocateVirtualMemory(shellcode.Length, Enumerations.MemoryProtectionType.ExecuteReadWrite); _propertyWrapper.MemoryManager.WriteVirtualMemory(shellcodeBuffer, shellcode); // Overwrite the instruction pointer of the thread with the shellcode buffer threadContext.Eip = (uint)shellcodeBuffer; // Set the context of the thread threadContextBuffer = MemoryTools.StoreStructureInBuffer(threadContext); if (PInvoke.Wow64SetThreadContext(threadHandle, threadContextBuffer) == 0) { ExceptionHandler.ThrowWin32Exception("Failed to set the context of a thread in the target process"); } MemoryTools.FreeMemoryForBuffer(threadContextBuffer); } else { // Suspend the thread _propertyWrapper.SyscallManager.InvokeSyscall <NtSuspendThread>(threadHandle); // Get the context of the thread var threadContextBuffer = (IntPtr)_propertyWrapper.SyscallManager.InvokeSyscall <NtGetContextThread>(threadHandle); var threadContext = Marshal.PtrToStructure <Structures.Context>(threadContextBuffer); // Write the shellcode used to call LoadLibraryW from the thread into the target process var shellcode = ThreadHijackX64.GetShellcode((IntPtr)threadContext.Rip, dllPathBuffer, loadLibraryAddress); var shellcodeBuffer = _propertyWrapper.MemoryManager.AllocateVirtualMemory(shellcode.Length, Enumerations.MemoryProtectionType.ExecuteReadWrite); _propertyWrapper.MemoryManager.WriteVirtualMemory(shellcodeBuffer, shellcode); // Overwrite the instruction pointer of the thread with the shellcode buffer threadContext.Rip = (ulong)shellcodeBuffer; // Set the context of the thread threadContextBuffer = MemoryTools.StoreStructureInBuffer(threadContext); _propertyWrapper.SyscallManager.InvokeSyscall <NtSetThreadContext>(threadHandle, threadContextBuffer); MemoryTools.FreeMemoryForBuffer(threadContextBuffer); } // Resume the thread _propertyWrapper.SyscallManager.InvokeSyscall <NtResumeThread>(threadHandle); PInvoke.SwitchToThisWindow(_propertyWrapper.TargetProcess.Process.MainWindowHandle, true); threadHandle.Dispose(); return(true); }
private int GetCategory(IntPtr address) { int value = MemoryTools.ReadByte(Handle, address - 0x1); return(Utilities.ToHex(value)); }
protected override int ComputeRawId(IntPtr address) { return(MemoryTools.ReadInt(Handle, address)); }
public override void SetItems(List <ItemId> itemIds) { totalItems = MemoryTools.ReadInt(Handle, itemCount); SetItems(itemIds, totalItems); }