You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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; // 替换为正确实现
    }
}

额外注意事项

  1. 身份验证包选择:如果是本地账户登录,建议使用NTLM身份验证包,Kerberos通常用于域环境。
  2. SeTcbPrivilege权限:此权限是调用LsaLogonUser的必要条件,必须确保EnableSeTcbPrivilege函数正确实现(打开当前进程令牌、调整权限)。
  3. 错误排查:如果仍报错,重点查看SubStatus参数,它会返回更详细的错误原因,比如Kerberos的具体失败码。
  4. 资源释放:所有手动分配的内存和句柄都要正确释放,避免资源泄漏。

备注:内容来源于stack exchange,提问作者Inori

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 10:19:33