Click here to Skip to main content
14,297,239 members
Rate this:
Please Sign up or sign in to vote.
Hello,

I´m stuck with my knowledge regarding receiving registry values remotely so I wanto to ask for help.

My situation is as follows:

I have a computer A and a computer B which are domain members. From computer A I want to read registry values from a remote PC, in this case computer B. To get general remote access to computer B I have to use my domain administrator account (I need to use my administrator account for every remote action).

My Windows Forms application will not be started with administrative credentials so I have to provide these credentials for every remote action. I tried this (the "Platform Version" key is an own created one which can be read successfully remotely via regedit.exe as administrator):
Dim RegKey As Microsoft.Win32.RegistryKey = Microsoft.Win32.RegistryKey.OpenRemoteBaseKey(Microsoft.Win32.RegistryHive.LocalMachine, computerB, Microsoft.Win32.RegistryView.Registry64).OpenSubKey("Software\Microsoft\Windows\CurrentVersion\Uninstall\Platform Version", False)
But this doesn´t work because of the missing possibility to provide credentials. Later I´ve read that it is not possible to get registry values remotely with different credentials because there is no chance to provide them as a parameter.

My next idea was to use WMI. For that there is the following Sub:
Private Sub CreateRemoteProcess(ByVal strComputer As String, ByVal strProcess As String)
    Dim MC As Management.ManagementClass = New Management.ManagementClass("Win32_Process")
    Dim MBO1 As Management.ManagementBaseObject = MC.GetMethodParameters("Create")
    Dim MS As Management.ManagementScope

    MBO1("CurrentDirectory") = Nothing
    MBO1("CommandLine") = strProcess

    MS = New Management.ManagementScope("\\" & strComputer & "\root\cimv2", New Management.ConnectionOptions With {.Username = AdminUsername, .SecurePassword = AdminPassword})

    MS.Connect()

    MC.Scope = MS
    Dim IMO As Management.InvokeMethodOptions = New Management.InvokeMethodOptions(Nothing, System.TimeSpan.MaxValue)
    Dim MBO2 As Management.ManagementBaseObject = MC.InvokeMethod("Create", MBO1, Nothing)
End Sub
This time I want to query the registry with reg.exe, pipe the output to a TXT file and copy this TXT file from computer B to computer A. I call the above Sub two times:
CreateRemoteProcess("computerB", "C:\Windows\System32\cmd.exe /c reg query ""HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Platform Version"" /v DisplayVersion > C:\Windows\Temp\PTVersion.txt")

CreateRemoteProcess("computerB", "C:\Windows\System32\cmd.exe /c copy C:\Windows\Temp\PTVersion.txt \\computerA\c$\unbackedup_data /y")
The first call will proceeded as expected and the file will be created but the second line doesn´t work and I don´t know why, there is no exception.

My next idea was to create a BAT file which I´m copying to computer B before calling the Sub:
IO.File.Copy("C:\Windows\Temp\PTVersion.bat", "\\computerB\Windows\Temp", True)

CreateRemoteProcess("computerB", "C:\Windows\System32\cmd.exe /c C:\Windows\Temp\PTVersion.bat")
The BAT file contains the reg.exe and copy command (the two calls above). If I call this the cmd.exe is appearing and disappearing in the Task Manager (in administrator context) but does only the reg.exe query and doesn´t copy the file to computer A.

My next idea was to use a process:
Dim psi As New ProcessStartInfo
With psi
    .FileName = "C:\Windows\System32\cmd.exe"
    .Arguments = "/c copy \\computerB\c$\windows\temp\PTVersion.txt C:\unbackedup_data /y"
    .UserName = AdminUsername
    .Password = AdminPassword
    .UseShellExecute = False
End With
Process.Start(psi)
But here I got an exception "The stub got wrong data" and don´t understand what this means. BTW this exception occurs as soon as I provide a username and a password for process "psi".

Does anyone have an idea why the file will not be copied to computer A? Or why I get the exception while using a process? My goal is to query the information without using external tools like psexec.

Thank you very much in advance!

Michael


UPDATE 01/28/2019

I added the property ".Verb" to the process and created an additional EXE file which will be started locally and queries the registry key remotely:
Dim psi As New ProcessStartInfo
With psi
    .FileName = Application.StartupPath & "\CC_Console.exe"
    .Domain = DirectoryServices.ActiveDirectory.Domain.GetCurrentDomain().Name
    .UserName = AdminUsername
    .Password = AdminPassword
    .UseShellExecute = False
    .LoadUserProfile = False
    .Verb = "runas"
End With
Process.Start(psi).WaitForExit()
This solution works now and I decided to use this way if there is no other chance. I tried to set ".FileName" to "cmd.exe" which is also working but it is not possible to hide the CMD window, even with ".WindowStyle = ProcessWindowStyle.Hidden" and/or ".CreateNoWindow = True".

If someone knows how to hide the CMD window when calling cmd.exe in a process, it would be great to know how.


UPDATE 01/29/2019

I translated the code from here to VB.Net:
Imports BOOL = System.Int32

Public Class NetworkShare
    Private Declare Function WNetAddConnection2A Lib "mpr.dll" (ByRef refNetResource As NetResource, ByVal inPassword As String, ByVal inUsername As String, ByVal inFlags As Integer) As Integer
    Private Declare Function WNetCancelConnection2A Lib "mpr.dll" (ByVal inServer As String, ByVal inFlags As Integer, ByVal inForce As Integer)

    Private _Server As String
    Private _Share As String
    Private _DriveLetter As String = Nothing
    Private _Username As String = Nothing
    Private _Password As String = Nothing
    Private _Flags As Integer = 0
    Private _NetResource As NetResource = New NetResource
    Private _AllowDisconnectWhenInUse As BOOL = 0

    Public Sub New()
        MyBase.New()
    End Sub

    Public Sub New(ByVal inServer As String, ByVal inShare As String)
        MyBase.New()
        _Server = inServer
        _Share = inShare
    End Sub

    Public Sub New(ByVal inServer As String, ByVal inShare As String, ByVal inDriveLetter As String)
        MyBase.New()
        _Server = inServer
        _Share = inShare
        _DriveLetter = inDriveLetter
    End Sub

    Public Sub New(ByVal inServer As String, ByVal inShare As String, ByVal inUsername As String, ByVal inPassword As String)
        MyBase.New()
        _Server = inServer
        _Share = inShare
        _Username = inUsername
        _Password = inPassword
    End Sub

    Public Sub New(ByVal inServer As String, ByVal inShare As String, ByVal inDriveLetter As String, ByVal inUsername As String, ByVal inPassword As String)
        MyBase.New()
        _Server = inServer
        _Share = inShare
        _DriveLetter = inDriveLetter
        _Username = inUsername
        _Password = inPassword
    End Sub

    Public Property Server As String
        Get
            Return _Server
        End Get
        Set(value As String)
            _Server = value
        End Set
    End Property

    Public Property Share As String
        Get
            Return _Share
        End Get
        Set(value As String)
            _Share = value
        End Set
    End Property

    Public ReadOnly Property FullPath As String
        Get
            Return String.Format("\\{0}\{1}", _Server, _Share)
        End Get
    End Property

    Public Property DriveLetter As String
        Get
            Return _DriveLetter
        End Get
        Set(value As String)
            SetDriveLetter(value)
        End Set
    End Property

    Public Property Username As String
        Get
            If String.IsNullOrEmpty(_Username) Then
                Return Nothing
            Else
                Return _Username
            End If
        End Get
        Set(value As String)
            _Username = value
        End Set
    End Property

    Public Property Password As String
        Get
            If String.IsNullOrEmpty(_Password) Then
                Return Nothing
            Else
                Return _Password
            End If
        End Get
        Set(value As String)
            _Password = value
        End Set
    End Property

    Public Property Flags As Integer
        Get
            Return _Flags
        End Get
        Set(value As Integer)
            _Flags = value
        End Set
    End Property

    Public Property Resource As NetResource
        Get
            Return Me._NetResource
        End Get
        Set(value As NetResource)
            Me._NetResource = value
        End Set
    End Property

    Public Property AllowDisconnectWhenInUse As Boolean
        Get
            Return Convert.ToBoolean(_AllowDisconnectWhenInUse)
        End Get
        Set(value As Boolean)
            _AllowDisconnectWhenInUse = Convert.ToInt32(value)
        End Set
    End Property

    Public Function Connect() As Integer
        _NetResource.Scope = CType(Scope.RESOURCE_GLOBALNET, Integer)
        _NetResource.Type = CType(Type.RESOURCETYPE_DISK, Integer)
        _NetResource.DisplayType = CType(DisplayType.RESOURCEDISPLAYTYPE_SHARE, Integer)
        _NetResource.Usage = CType(Usage.RESOURCEUSAGE_CONNECTABLE, Integer)
        _NetResource.RemoteName = FullPath
        _NetResource.LocalName = DriveLetter

        Return WNetAddConnection2A(_NetResource, _Password, _Username, _Flags)
    End Function

    Public Function Disconnect() As Integer
        Dim retVal As Integer = 0
        If (_DriveLetter IsNot Nothing) Then
            retVal = WNetCancelConnection2A(_DriveLetter, _Flags, _AllowDisconnectWhenInUse)
            retVal = WNetCancelConnection2A(FullPath, _Flags, _AllowDisconnectWhenInUse)
        Else
            retVal = WNetCancelConnection2A(FullPath, _Flags, _AllowDisconnectWhenInUse)   '<-- here comes an exception
        End If

        Return retVal
    End Function

    Private Sub SetDriveLetter(ByVal inString As String)
        If inString.Length = 1 Then
            If Char.IsLetter(inString.ToCharArray()(0)) Then
                _DriveLetter = inString & ":"
            Else
                _DriveLetter = Nothing
            End If
        ElseIf inString.Length = 2 Then
            Dim drive As Char() = inString.ToCharArray()
            If (Char.IsLetter(drive(0)) AndAlso (drive(1) = Microsoft.VisualBasic.ChrW(58))) Then
                _DriveLetter = inString
            Else
                _DriveLetter = Nothing
            End If
        Else
            _DriveLetter = Nothing
        End If
    End Sub

    <Runtime.InteropServices.StructLayout(Runtime.InteropServices.LayoutKind.Sequential)> _
    Public Structure NetResource
        Public Scope As UInteger
        Public Type As UInteger
        Public DisplayType As UInteger
        Public Usage As UInteger
        Public LocalName As String
        Public RemoteName As String
        Public Comment As String
        Public Provider As String
    End Structure

    Public Enum Scope
        RESOURCE_CONNECTED = 1
        RESOURCE_GLOBALNET
        RESOURCE_REMEMBERED
        RESOURCE_RECENT
        RESOURCE_CONTEXT
    End Enum

    Public Enum Type As UInteger
        RESOURCETYPE_ANY
        RESOURCETYPE_DISK
        RESOURCETYPE_PRINT
        RESOURCETYPE_RESERVED = 8
        RESOURCETYPE_UNKNOWN = 4294967295
    End Enum

    Public Enum DisplayType
        RESOURCEDISPLAYTYPE_GENERIC
        RESOURCEDISPLAYTYPE_DOMAIN
        RESOURCEDISPLAYTYPE_SERVER
        RESOURCEDISPLAYTYPE_SHARE
        RESOURCEDISPLAYTYPE_FILE
        RESOURCEDISPLAYTYPE_GROUP
        RESOURCEDISPLAYTYPE_NETWORK
        RESOURCEDISPLAYTYPE_ROOT
        RESOURCEDISPLAYTYPE_SHAREADMIN
        RESOURCEDISPLAYTYPE_DIRECTORY
        RESOURCEDISPLAYTYPE_TREE
        RESOURCEDISPLAYTYPE_NDSCONTAINER
    End Enum

    Public Enum Usage As UInteger
        RESOURCEUSAGE_CONNECTABLE = 1
        RESOURCEUSAGE_CONTAINER = 2
        RESOURCEUSAGE_NOLOCALDEVICE = 4
        RESOURCEUSAGE_SIBLING = 8
        RESOURCEUSAGE_ATTACHED = 16
        RESOURCEUSAGE_ALL = 31
        RESOURCEUSAGE_RESERVED = 2147483648
    End Enum

    Public Enum ConnectionFlags As UInteger
        CONNECT_UPDATE_PROFILE = 1
        CONNECT_UPDATE_RECENT = 2
        CONNECT_TEMPORARY = 4
        CONNECT_INTERACTIVE = 8
        CONNECT_PROMPT = 16
        CONNECT_NEED_DRIVE = 32
        CONNECT_REFCOUNT = 64
        CONNECT_REDIRECT = 128
        CONNECT_LOCALDRIVE = 256
        CONNECT_CURRENT_MEDIA = 512
        CONNECT_DEFERRED = 1024
        CONNECT_COMMANDLINE = 2048
        CONNECT_CMD_SAVECRED = 4096
        CONNECT_CRED_RESET = 8192
        CONNECT_RESERVED = 4278190080
    End Enum
End Class
The value from the registry was get but unfortunately I got an "MarshalDirectiveException" exception: "PInvoke restriction: cannot return variants."

The Program.cs is:
Module Module1

    Sub Main()
        Dim ServerName As String = "computer"

        Dim share As NetworkShare = New NetworkShare(ServerName, "C$", "admin", "password")

        share.Connect()

        Dim ProductName As String = String.Empty
        Dim key As Microsoft.Win32.RegistryKey = Microsoft.Win32.RegistryKey.OpenRemoteBaseKey(Microsoft.Win32.RegistryHive.LocalMachine, ServerName)
        If key IsNot Nothing Then
            key = key.OpenSubKey("SOFTWARE\Microsoft\Windows NT\CurrentVersion")
            If key IsNot Nothing Then
                ProductName = key.GetValue("ProductName").ToString()
            End If
        End If

        Console.WriteLine("The device " & ServerName & " is running " & ProductName & ".")

        share.Disconnect()
    End Sub

End Module


What I have tried:

I explained my troubleshooting steps in the question.
Posted
Updated 29-Jan-19 11:09am
v3
Comments
Richard Deeming 28-Jan-19 7:45am
   
Storing your domain admin credentials inside your application sounds like a bad idea. Apart from the issues around changing or expiring your password, there's a very real danger that a non-admin user with access to your application could decompile it and gain domain admin access using your credentials.

Why not create a second application which has to be run elevated, and launch that from your main application?
Michael____ 28-Jan-19 13:01pm
   
The application have two forms. At every start of the application the first form asks the user for the admin credentials. This information will be stored in public variables of the second form. After closing the application the credential information is gone. The credentials will not be saved to a file.

Regarding your suggestion: I tried this already and wanted to start the second application locally as a process but I get the exception "The stub got wrong data". The credentials itself (e.g. wrong password) cannot be the cause because I can do a WMI query with the same one and this works.
pdoxtader 28-Jan-19 16:01pm
   
Michael,
Entering your domain administrator credentials into a .NET application and walking away from it is serious security problem. I don't know if you are aware of this, but strings are never destroyed in .net applications, and they are visible to anyone who has a clue. For instance, Process Explorer from Sysinternals is free and doesn't need to be installed. You just download it and run it. It works and looks very much like the windows Task Manager - except you have a few nice added tools for more computer savvy people (and network admins) - like the ability to see all strings in any running application. Including your administrator password. Think about it. Just because you didn't know about this doesn't mean that the user sitting at your workstation doesn't either.

The right way to do this would be to build a windows service instead, and run it with the lowest level of permissions you possibly can and still get what you need. Have a look here on codeproject for tcp/ip libraries (I have two you can have a look at), and build yourself a network enterprise application that does what you need and transfers any files or data to your machine.
Dave Kreskowiak 28-Jan-19 16:33pm
   
CreateRemoteProcess("computerB", "C:\Windows\System32\cmd.exe /c copy C:\Windows\Temp\PTVersion.txt \\computerA\c$\unbackedup_data /y")

The first call will proceeded as expected and the file will be created but the second line doesn´t work and I don´t know why, there is no exception.


This doesn't work because the remote process you created will not have admin creditials to get past the C$ (Admin share!) of ComputerA to copy the file back.

WMI is the way to go if you have to enter credentials, but you never said what problem you ran into using it.
Michael____ 29-Jan-19 2:44am
   
Thank you very much for the explanation! I know Process Explorer but I didn´t know that you can see strings with it. This makes me thoughtful...

The idea with the service is good and I´m able to install services but unfortunately I´m not allowed to do that because I use the application in a company environment and cannot install/configure what I want on customer´s computers.

1 solution

Rate this:
Please Sign up or sign in to vote.

Solution 1

See my comment above about the WMI way to do this. You never mentioned a problem with this method.

But, there is a way to get the .NET Registry classes to work remotely AND with provided credentials. See Rhyous » Blog Archive » How to authenticate and access the registry remotely using C#[^].
   
v2
Comments
Michael____ 29-Jan-19 2:56am
   
Hi Dave, thanks for the link, it sounds interesting and could be a solution in further projects. I converted it to VB.Net and it works (it gets the value and returns it) but I get an exception in class "NetworkShare.cs" line 166. It tells me "MarshalDirectiveException: PInvoke restriction: cannot return variants.". Do you have an idea how to solve it?

BTW: I cannot mention a problem which I do not get.
Dave Kreskowiak 29-Jan-19 8:47am
   
Considering I have no idea what your converted code looks like, I have no idea.
Michael____ 29-Jan-19 17:11pm
   
Sorry, I forgot to mention that the line 166 was the line in the C# project from your suggested link. I´ve added the translated code to the question above.
Dave Kreskowiak 29-Jan-19 19:29pm
   
Your second DLL import function header doesn't have a return type specified.

Private Declare Function WNetCancelConnection2A Lib "mpr.dll" (ByVal inServer As String, ByVal inFlags As Integer, ByVal inForce As Integer) As ??????????

Looking at the original C# code, it should be an Integer.
Michael____ 30-Jan-19 12:51pm
   
WHAT THE ???

I added the return type and the exception is gone. The application is working now.

Thank you very much for your help! Your suggestion is accepted as the solution.
Dave Kreskowiak 30-Jan-19 13:31pm
   
Sub's never have return types. Function's always should have them specified.
Michael____ 30-Jan-19 15:22pm
   
Yes, I know. I simply forgot it and Visual Studio doesn´t show a hint regarding this. But thanks anyway!

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)




CodeProject, 503-250 Ferrand Drive Toronto Ontario, M3C 3G8 Canada +1 416-849-8900 x 100