在C#中调用LsaLogonUser函数返回ERROR_INVALID_PARAMETER(87)错误,请求排查问题并告知正确调用方法
在C#中调用LsaLogonUser函数返回ERROR_INVALID_PARAMETER(87)错误,请求排查问题并告知正确调用方法
我帮你排查了代码里的几个关键问题,这些正是导致ERROR_INVALID_PARAMETER(87)的核心原因,下面逐个说明并给出修正后的完整调用方案:
问题点分析
LSA_UNICODE_STRING创建逻辑完全错误
你在CreateLsaUnicodeString里误用了Marshal.StringToHGlobalAnsi(生成ANSI编码缓冲区),但LSA_UNICODE_STRING要求的是UTF-16(Unicode)编码的缓冲区,必须用Marshal.StringToHGlobalUni。同时,Length字段是字符串的字节数(每个Unicode字符占2字节,需乘以字符数),MaximumLength要预留足够空间容纳字符串和可选的终止符。AuthenticationInformationLength参数计算错误
你错误地将domain、username、password的字节数追加到Marshal.SizeOf(authInfo)上,这完全没必要。KERB_INTERACTIVE_LOGON结构体仅包含指向字符串缓冲区的指针,结构体本身的大小已经涵盖了所有必要内存,正确长度就是Marshal.SizeOf(authInfo)。资源泄漏风险
原代码中没有释放Marshal.StringToHGlobalAnsi分配的内存,会导致内存泄漏,需要用IDisposable包装类自动管理资源。
修正后的完整代码
using System; using System.Runtime.InteropServices; public static unsafe class LsaLogonHelper { // 假设CsWin32生成的类型已正确引入(NTSTATUS、LSA_STRING、LSA_UNICODE_STRING等) const string MICROSOFT_KERBEROS_NAME_A = "Kerberos"; const string MICROSOFT_NTLM_NAME_A = "NTLM"; // 本地账户建议用NTLM public static unsafe void CallLsaLogonUser() { const string logonProcessName = "MyLogonProcess", sourceName = "12345678", domain = ".", // 本地账户用".",域账户填实际域名 username = "testUser", password = "testPass123"; NTSTATUS ntStatus; // 必须启用SeTcbPrivilege权限,确保此函数实现正确 if (!EnableSeTcbPrivilege()) { Console.WriteLine("Failed to enable SeTcbPrivilege"); return; } // 创建LSA_STRING类型的登录进程名(自动释放内存) using var lsaLogonProcName = CreateLsaString(logonProcessName); // 注册登录进程 ntStatus = PInvoke.LsaRegisterLogonProcess( in lsaLogonProcName.Value, out var logonProcessHandle, out var securityMode); if (ntStatus != NTSTATUS.STATUS_SUCCESS) { int winErr = PInvoke.LsaNtStatusToWinError(ntStatus); Console.WriteLine($"LsaRegisterLogonProcess failed: {winErr}"); return; } // 创建Unicode编码的身份验证字符串(自动释放内存) using var domainLsaStr = CreateLsaUnicodeString(domain); using var usernameLsaStr = CreateLsaUnicodeString(username); using var passwordLsaStr = CreateLsaUnicodeString(password); // 构造Kerberos交互式登录信息 var kerbAuthInfo = new KERB_INTERACTIVE_LOGON { MessageType = KERB_LOGON_SUBMIT_TYPE.KerbInteractiveLogon, LogonDomainName = domainLsaStr.Value, UserName = usernameLsaStr.Value, Password = passwordLsaStr.Value }; uint authInfoLength = (uint)Marshal.SizeOf<KERB_INTERACTIVE_LOGON>(); // 查找身份验证包(本地账户建议用NTLM) using var authPackageName = CreateLsaString(MICROSOFT_KERBEROS_NAME_A); ntStatus = PInvoke.LsaLookupAuthenticationPackage( logonProcessHandle, in authPackageName.Value, out var authPackageId); if (ntStatus != NTSTATUS.STATUS_SUCCESS) { int winErr = PInvoke.LsaNtStatusToWinError(ntStatus); Console.WriteLine($"LsaLookupAuthenticationPackage failed: {winErr}"); return; } // 初始化TOKEN_SOURCE PInvoke.AllocateLocallyUniqueId(out var sourceLuid); TOKEN_SOURCE tokenSource = default; // 填充8字节的SourceName(自动补0) byte[] sourceBytes = System.Text.Encoding.ASCII.GetBytes(sourceName); Marshal.Copy(sourceBytes, 0, (IntPtr)tokenSource.SourceName, Math.Min(sourceBytes.Length, 8)); for (int i = sourceBytes.Length; i < 8; i++) ((byte*)&tokenSource.SourceName)[i] = 0; tokenSource.SourceIdentifier = sourceLuid; // 初始化LogonId(让系统生成新的登录ID) LUID logonId = default; // 调用LsaLogonUser ntStatus = PInvoke.LsaLogonUser( logonProcessHandle, in lsaLogonProcName.Value, SECURITY_LOGON_TYPE.Interactive, authPackageId, &kerbAuthInfo, authInfoLength, null, in tokenSource, out void* profileBuffer, out uint profileBufferLength, ref logonId, out var userToken, out var quotas, out int subStatus); if (ntStatus == NTSTATUS.STATUS_SUCCESS) { Console.WriteLine($"LOGON SUCCESS! LogonId: {logonId.HighPart}:{logonId.LowPart}"); // 释放ProfileBuffer(必须用LsaFree) if (profileBuffer != null) PInvoke.LsaFree(profileBuffer); // userToken是SafeFileHandle,会自动释放,也可手动Dispose } else { int winErr = PInvoke.LsaNtStatusToWinError(ntStatus); Console.WriteLine($"LsaLogonUser failed: {winErr}, SubStatus: {subStatus}"); if (userToken != null && !userToken.IsInvalid) userToken.Dispose(); } // 释放登录进程句柄 if (logonProcessHandle != null && !logonProcessHandle.IsInvalid) logonProcessHandle.Dispose(); } // 封装LSA_STRING的创建与自动释放 private sealed class LsaStringWrapper : IDisposable { public LSA_STRING Value { get; } private IntPtr _bufferPtr; public LsaStringWrapper(LSA_STRING value, IntPtr bufferPtr) { Value = value; _bufferPtr = bufferPtr; } public void Dispose() { if (_bufferPtr != IntPtr.Zero) { Marshal.FreeHGlobal(_bufferPtr); _bufferPtr = IntPtr.Zero; } } } // 封装LSA_UNICODE_STRING的创建与自动释放 private sealed class LsaUnicodeStringWrapper : IDisposable { public LSA_UNICODE_STRING Value { get; } private IntPtr _bufferPtr; public LsaUnicodeStringWrapper(LSA_UNICODE_STRING value, IntPtr bufferPtr) { Value = value; _bufferPtr = bufferPtr; } public void Dispose() { if (_bufferPtr != IntPtr.Zero) { Marshal.FreeHGlobal(_bufferPtr); _bufferPtr = IntPtr.Zero; } } } // 创建ANSI编码的LSA_STRING private static LsaStringWrapper CreateLsaString(string value) { if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value)); byte[] ansiBytes = System.Text.Encoding.ASCII.GetBytes(value); IntPtr buffer = Marshal.AllocHGlobal(ansiBytes.Length + 1); // 预留终止符空间 Marshal.Copy(ansiBytes, 0, buffer, ansiBytes.Length); Marshal.WriteByte(buffer, ansiBytes.Length, 0); return new LsaStringWrapper(new LSA_STRING { Buffer = (byte*)buffer, Length = (ushort)ansiBytes.Length, MaximumLength = (ushort)(ansiBytes.Length + 1) }, buffer); } // 创建Unicode编码的LSA_UNICODE_STRING private static LsaUnicodeStringWrapper CreateLsaUnicodeString(string value) { if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value)); char[] chars = value.ToCharArray(); int byteCount = chars.Length * 2; IntPtr buffer = Marshal.AllocHGlobal(byteCount + 2); // 预留Unicode终止符空间 Marshal.Copy(chars, 0, buffer, chars.Length); Marshal.WriteInt16(buffer, byteCount, 0); return new LsaUnicodeStringWrapper(new LSA_UNICODE_STRING { Buffer = (char*)buffer, Length = (ushort)byteCount, MaximumLength = (ushort)(byteCount + 2) }, buffer); } // 正确实现SeTcbPrivilege权限启用(示例框架) private static bool EnableSeTcbPrivilege() { // 完整实现步骤: // 1. 打开当前进程的令牌(TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY) // 2. 查询SeTcbPrivilege的LUID // 3. 构造TOKEN_PRIVILEGES结构体,设置权限为SE_PRIVILEGE_ENABLED // 4. 调用AdjustTokenPrivileges启用权限 // 5. 关闭令牌句柄 return true; // 替换为正确实现 } }
额外注意事项
- 身份验证包选择:如果是本地账户登录,建议使用
NTLM身份验证包,Kerberos通常用于域环境。 - SeTcbPrivilege权限:此权限是调用
LsaLogonUser的必要条件,必须确保EnableSeTcbPrivilege函数正确实现(打开当前进程令牌、调整权限)。 - 错误排查:如果仍报错,重点查看
SubStatus参数,它会返回更详细的错误原因,比如Kerberos的具体失败码。 - 资源释放:所有手动分配的内存和句柄都要正确释放,避免资源泄漏。
备注:内容来源于stack exchange,提问作者Inori
相关产品推荐
相关产品推荐

