Get elevated excel process - c#

A few days ago, I had to run Excel with current user rights using ProcessStartInfo in order to user some addins that require admin or current user rights.
new Process{
StartInfo = new ProcessStartInfo{
Verb = "runas",
FileName = #"path/to/app.exe",
Arguments = "-officeexcel",
Domain = domain,
UserName = login,
Password = pa ss,
UseShellExecute = false,
}
}.Start();
Thus, i can't use anymore :
Marshal.GetActiveObject("Excel.Application")
to get excel instance. So i tried to use late binding with AccessibleObjectFromWindow() :
int hwnd = (int)FindWindow("XLMAIN", null);
if (hwnd != 0)
{
int hwndChild = 0;
EnumChildCallback cb = new EnumChildCallback(EnumChildProc);
EnumChildWindows(hwnd, cb, ref hwndChild);
if (hwndChild != 0)
{
const uint OBJID_NATIVEOM = 0xFFFFFFF0;
Guid IID_IDispatch = new Guid("{00020400-0000-0000-C000-000000000046}");
ExcelWindow ptr;
int hr = AccessibleObjectFromWindow(hwndChild, OBJID_NATIVEOM, IID_IDispatch.ToByteArray(), out ptr);
if (hr >= 0)
{
using (UILanguageHelper fix = new UILanguageHelper())
{
return (Excel.Application)ptr.GetType().InvokeMember("Application", BindingFlags.GetProperty, null, ptr, null);
}
}
}
}
return null;
It worked but the Excel Application I get has zero property and I can't invoke any methods !
I need to run some macros after getting elevated Excel instance !
What did i do wrong ?

You cannot interact with an application launched in elevated mode from a non-elevated one for security reasons.
In your case, I think the only way to achieve what you ask is to elevate your app (or another small app dedicated to this task) first, then, your elevated app, will, on its turn, run excel and perform the tasks you require. Excel will run in elevated mode.
The design pattern in this way is to run your application with the minimum rights required. And then elevate, specifying in the command line a "token" (whatsoever you can use to connect to your application via IPC/sockets).

Related

Get all MSACCESS application instances using C# Interop

I need to get a list of all currently opened MSACCESS instances in the system (windows) to be able to close any of them from within my app. I have no problems with EXCEL and WINWORD but can't hook up with Access.
I use Office 2016 and I see that MSACCESS creates separate procss for each opened database file. So I think I have to get application instances from window handles. I've tried to adapt this code: How to iterate through instance of Excel c#
I'm able to get all MSACCESS processes but the Excel or Word code isn't working for MSACCESS. The Code line:
if (buf.ToString() == "EXCEL7")
Always gives me the MsoCommandBarDock value.
Any thoughts on how I can achieve this?
Based on the answer for Excel, the Access version is similar:
const uint OBJID_NATIVEOM = 0xFFFFFFF0;
var procs = new List<Process>(Process.GetProcessesByName("MSACCESS.EXE"));
foreach (var p in procs)
{
var mainHandle = (int)p.MainWindowHandle;
if (mainHandle > 0)
{
var IID_IDispatch = new Guid("{00020400-0000-0000-C000-000000000046}");
Microsoft.Office.Interop.Access.Application app = null;
int res = AccessibleObjectFromWindow(mainHandle, OBJID_NATIVEOM, IID_IDispatch.ToByteArray(), ref app);
if (res >= 0)
{
Debug.Assert(app.hWndAccessApp == mainHandle);
Console.WriteLine(app.Name);
}
}
}
I tested it with Access 2016 on Windows 10, en-us locale.
The major difference is that the window hierarchy of access is not as convoluted as the one of Excel, therefore you can omit the iteration of child windows.
Disclaimer: This relies on the internal structure of a closed-source Windows application. Microsoft as its vendor discourages this kind of tricks for obvious reasons: they may ship and update or release a new version at any time where the inner structure (the window hierarchy) has changed, breaking code that relies on this. Also, MS Access used to have a single document view mode, which may present you with two versions of window hierarchy in the same release. Don't do this in commercial products / productive software.
According to the answer from Cee McSharpface, in 2021 (Microsoft Access for Microsoft 365 MSO (16.0.14326.20504) 64-bit and Windows 10 20H2) I had to adapt the solution as follows:
[DllImport("oleacc.dll")]
private static extern int AccessibleObjectFromWindow(
int hwnd, uint dwObjectID, byte[] riid,
ref Microsoft.Office.Interop.Access.Application ptr);
const uint OBJID_NATIVEOM = 0xFFFFFFF0;
var procs = new List<Process>(Process.GetProcessesByName("MSACCESS"));
foreach (var p in procs)
{
var mainHandle = (int)p.MainWindowHandle;
if (mainHandle > 0)
{
var IID_IDispatch = new Guid("{00020400-0000-0000-C000-000000000046}");
Microsoft.Office.Interop.Access.Application app = null;
int res = AccessibleObjectFromWindow(mainHandle, OBJID_NATIVEOM, IID_IDispatch.ToByteArray(), ref app);
if (res >= 0)
{
Debug.Assert(app.hWndAccessApp() == mainHandle);
Console.WriteLine(app.Name);
}
}
}
Please notice the following changes:
GetProcessesByName uses "MSACESS" instead of "MSACCESS.EXE", according to this documentation:
The process name is a friendly name for the process, such as Outlook,
that does not include the .exe extension or the path
AccessibleObjectFromWindow uses a ref Microsoft.Office.Interop.Access.Application ptr because there is no Window object like in the Excel interop.
There are many ways of doing this inlcuding retrieve COM Objects from ROT (running object table). Since your need is "just" to be able to close apps, following code should work fine.
using System.Diagnostics;
using System.Linq;
Process.GetProcessesByName("MSACCESS").All(x => x.CloseMainWindow());
This sends a close message to all Access main windows, which is similar to user closing the app.

Delete a directory where someone has opened a file

I am trying to programmatically delete and replace the contents of an application, "App A", using an "installer" program, which is just a custom WPF .exe app, we'll call "App B". (My question concerns code in "App B".)
GUI Setup (not particularly important)
App B has a GUI where a user can pick computer names to copy App A onto. A file picker is there the admin uses to fill in the source directory path on the local machine by clicking "App A.exe". There are also textboxes for a user name and password, so the admin can enter their credentials for the target file server where App A will be served - the code impersonates the user with these to prevent permission issues. A "Copy" button starts the routine.
Killing App A, File Processes, and Doing File Deletion
The Copy routine starts by killing the "App A.exe" process on all computers in the domain, as well as explorer.exe, in case they had App A's explorer folder open. Obviously this would be done afterhours, but someone may still have left things open and locked their machine before going home. And that's really the base of the problem I'm looking to solve.
Prior to copying over the updated files, we want to delete the entire old directory. In order to delete the directory (and its subdirectories), each file within them has to be deleted. But say they had a file open from App A's folder. The code finds any locking process on any file prior to deleting it (using code from Eric J.'s answer at How do I find out which process is locking a file using .NET? ), it kills that process on whatever computer it is running on. If local, it just uses:
public static void localProcessKill(string processName)
{
foreach (Process p in Process.GetProcessesByName(processName))
{
p.Kill();
}
}
If remote, it uses WMI:
public static void remoteProcessKill(string computerName, string fullUserName, string pword, string processName)
{
var connectoptions = new ConnectionOptions();
connectoptions.Username = fullUserName; // #"YourDomainName\UserName";
connectoptions.Password = pword;
ManagementScope scope = new ManagementScope(#"\\" + computerName + #"\root\cimv2", connectoptions);
// WMI query
var query = new SelectQuery("select * from Win32_process where name = '" + processName + "'");
using (var searcher = new ManagementObjectSearcher(scope, query))
{
foreach (ManagementObject process in searcher.Get())
{
process.InvokeMethod("Terminate", null);
process.Dispose();
}
}
}
Then it can delete the file. All is well.
Directory Deletion Failure
In my code below, it is doing the recursive deletion of the files, and does it fine, up until the Directory.Delete(), where it will say The process cannot access the file '\\\\SERVER\\C$\\APP_A_DIR' because it is being used by another process, because I am attempting to delete the directory while I had a file still open from it (even though the code was actually able to delete the physical file-the instance is still open).
public void DeleteDirectory(string target_dir)
{
string[] files = Directory.GetFiles(target_dir);
string[] dirs = Directory.GetDirectories(target_dir);
List<Process> lstProcs = new List<Process>();
foreach (string file in files)
{
File.SetAttributes(file, FileAttributes.Normal);
lstProcs = ProcessHandler.WhoIsLocking(file);
if (lstProcs.Count == 0)
File.Delete(file);
else // deal with the file lock
{
foreach (Process p in lstProcs)
{
if (p.MachineName == ".")
ProcessHandler.localProcessKill(p.ProcessName);
else
ProcessHandler.remoteProcessKill(p.MachineName, txtUserName.Text, txtPassword.Password, p.ProcessName);
}
File.Delete(file);
}
}
foreach (string dir in dirs)
{
DeleteDirectory(dir);
}
//ProcessStartInfo psi = new ProcessStartInfo();
//psi.Arguments = "/C choice /C Y /N /D Y /T 1 & Del " + target_dir;
//psi.WindowStyle = ProcessWindowStyle.Hidden;
//psi.CreateNoWindow = true;
//psi.FileName = "cmd.exe";
//Process.Start(psi);
//ProcessStartInfo psi = new ProcessStartInfo();
//psi.Arguments = "/C RMDIR /S /Q " + target_dir;
//psi.WindowStyle = ProcessWindowStyle.Hidden;
//psi.CreateNoWindow = true;
//psi.FileName = "cmd.exe";
//Process.Start(psi);
// This is where the failure occurs
//FileSystem.DeleteDirectory(target_dir, DeleteDirectoryOption.DeleteAllContents);
Directory.Delete(target_dir, false);
}
I've left things I've tried commented out in the code above. While I can kill processes attached to the files and delete them, is there a way to kill processes attached to folders, in order to delete them?
Everything online I saw tries to solve this using a loop-check with a delay. This will not work here. I need to kill the file that was opened-which I do-but also ensure the handle is released from the folder so it can also be deleted, at the end. Is there a way to do this?
Another option I considered that will not work:
I thought I might just freeze the "installation" (copying) process by marking that network folder for deletion in the registry and schedule a programmatic reboot of the file server, then re-run afterwards. How to delete Thumbs.db (it is being used by another process) gives this code by which to do this:
[DllImport("kernel32.dll")]
public static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, int dwFlags);
public const int MOVEFILE_DELAY_UNTIL_REBOOT = 0x4;
//Usage:
MoveFileEx(fileName, null, MOVEFILE_DELAY_UNTIL_REBOOT);
But it has in the documentation that If MOVEFILE_DELAY_UNTIL_REBOOT is used, "the file cannot exist on a remote share, because delayed operations are performed before the network is available." And that was assuming it might have allowed a folder path, instead of a file name. (Reference: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365240(v=vs.85).aspx ).
So there are 2 scenarios I wanted to handle - both are where the folder is prevented from being deleted:
1) A user has a file open on their local machine from the application's folder on the file server.
2) An admin has a file open from the application's folder, which they will see while remoted (RDP'ed) into the server.
I've settled on a way forward. If I run into this issue, I figure about all I can do is to either:
1) Freeze the "installation" (copying) process by simply scheduling a programmatic reboot of the file server in the IOException block if I really want to blow away the folder (not ideal and probably overkill, but others running across this same issue may be inspired by this option). The installer will need to be run again to copy the files after the server reboots.
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, out SafeTokenHandle phToken);
LogonUser(userName, domainName, password,
LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT,
out safeTokenHandle);
try
{
using (WindowsIdentity newId = new WindowsIdentity(safeTokenHandle.DangerousGetHandle()))
{
using (WindowsImpersonationContext impersonatedUser = newId.Impersonate())
{
foreach (Computer pc in selectedList) // selectedList is an ObservableCollection<Computer>
{
string newDir = "//" + pc.Name + txtExtension.Text; // the textbox has /C$/APP_A_DIR in it
if (Directory.Exists(newDir))
{
DeleteDirectory(newDir); // <-- this is where the exception happens
}
}
}
}
}
catch (IOException ex)
{
string msg = "There was a file left open, thereby preventing a full deletion of the previous folder, though all contents have been removed. Do you wish to proceed with installation, or reboot the server and begin again, in order to remove and replace the installation directory?";
MessageBoxResult result = MessageBox.Show(msg, "Reboot File Server?", MessageBoxButton.OKCancel);
if (result == MessageBoxResult.OK)
{
var psi = new ProcessStartInfo("shutdown","/s /t 0");
psi.CreateNoWindow = true;
psi.UseShellExecute = false;
Process.Start(psi);
}
else
{
MessageBox.Show("Copying files...");
FileSystem.CopyDirectory(sourcePath, newDir);
MessageBox.Show("Completed!");
}
}
Reference: How to shut down the computer from C#
OR
2) Ignore it altogether and perform my copy, anyway. The files actually do delete, and I found there's really no problem with having a folder I can't delete, as long as I can write to it, which I can. So this is the one I ultimately picked.
So again, in the IOException catch block:
catch (IOException ex)
{
if (ex.Message.Contains("The process cannot access the file") &&
ex.Message.Contains("because it is being used by another process") )
{
MessageBox.Show("Copying files...");
FileSystem.CopyDirectory(sourcePath, newDir);
MessageBox.Show("Completed!");
}
else
{
string err = "Issue when performing file copy: " + ex.Message;
MessageBox.Show(err);
}
}
Code above leaves out my model for Computer, which just has a Name node in it, and the rest of my Impersonation class, which is based on my own rendition of several different (but similar) code blocks of how they say to do it. If anyone needs that, here are a couple of links to some good answers:
Need Impersonation when accessing shared network drive
copy files with authentication in c#
Related: Cannot delete directory with Directory.Delete(path, true)

Start a Process from a C# Application impersonating a specific User

I have a .NET c# application,
the business flow (short and to the point) is:
Users make a call to my app which authenticates them by windows authentication.
If the user is a "special user" (business logic part, E.g. some account admin), I impersonate to a "Master Account" in the active directory which has read / write permissions to a shared folder.
I then create folders and files with the impersonated user context --> This works.
But when I try to start a process (bcp.exe for those who care), I can't get it to work!
After many failing attempts , getting many error messages such as "access denied",
and trying to use almost all of the Process.ProcessStartInfo() attributes which should assist me to run a process as a different user, I decided to Post this as a question.
I've read many blogs suggesting the only way to do this is to use the win32 dll and call CreateProcessAsUser() method, but it's just to damn complicated, and I couldn't find any working sample of it.
bottom line question:
How can I start a Process (Process.Start) from a c# app while in impersonation context as the impersonated user?
My code:
private void ExecuteCommand(string backupSource, string backupFilename, string formatFilename)
{
// This works --> Here I'm under impersonated user context
// with read write permissions to the shared folder
if (!Directory.Exists(OutputPath))
{
Directory.CreateDirectory(OutputPath);
}
using (Process p = new Process())
{
p.StartInfo = GetProcessStartInfo(backupSource, backupFilename, formatFilename);
//Here I'm currently getting ""Access Denied" exception"
p.Start();
...
}
}
private ProcessStartInfo GetProcessStartInfo(string backupSource, string backupFilename, string formatFilename)
{
var result = new ProcessStartInfo();
result.UseShellExecute = false;
result.RedirectStandardOutput = true;
result.RedirectStandardError = true;
var file = Path.Combine(PathToExecutable, "bcp.exe");
// #"C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\110\Tools\Binn\bcp.exe";
result.FileName = file;
result.WorkingDirectory = Path.GetDirectoryName(file);
result.LoadUserProfile = true;
result.Domain = "IMPERSONATED USER DOMAIN";
result.UserName = "IMPERSONATED USER NAME";
var ssPwd = new SecureString();
string password = "IMPERSONATED USER PASSWORD";
for (int x = 0; x < password.Length; x++)
{
ssPwd.AppendChar(password[x]);
}
result.Password = ssPwd;
var backupFullFilename = GetFullFileName(backupFilename);
StringBuilder arguments = new StringBuilder(backupSource);
var formatFullFilename = GetFullFileName(formatFilename);
FormatArguments(arguments, backupFullFilename, formatFullFilename);
var argumentsString = arguments.ToString();
result.Arguments = argumentsString;
return result;
}
Edit #1:
I was able to resolve the "Access is denied" exception, by adding the impersonating user to the administrators group on the machine which the application that starts the process runs on.
Now, I'm having a different issue, no exception but seems like the process isn't starting, or exiting right on start, I'm getting exit code 1073741502.
I've read I must use the native win32 api CreateProcessAsUser() instead of System.Diagnostics.Process.Start() but I'm not sure if that's true.
Ideas?
Assistance would be appreciated.

C# console application stdin/stdout redirection

I have an interesting (read: frustrating) problem kicking off a console application from a C# WPF app and redirecting its stdin and stdout.
It is mostly up and working but I seem to end up not getting some data from stdout as soon as I start redirecting stdin.
I'll clarify with an example. If I don't set hStdInput in the STARTUPINFO structure, when I start the child process I receive the following:
MongoDB shell version: 2.2.0
connecting to: test
local:PRIMARY>
Once I set hStdInput however, I just get this:
MongoDB shell version: 2.2.0
connecting to: test
I know that the BackgroundWorker processing stdout is still functioning because if I send something to the process on stdin, it responds accordingly.
use TestDB
switched to db TestDB
So, this is how I create the process:
_processInfo = new ProcessInfo();
bool ok = false;
SECURITY_ATTRIBUTES sattr = new SECURITY_ATTRIBUTES();
sattr.bInheritHandle = 1;
unsafe
{
sattr.lpSecurityDescriptor = null;
}
sattr.nLength = Marshal.SizeOf(sattr);
IntPtr hWrite;
ok = CreatePipe(out _hReadStdOut, out hWrite, ref sattr, 0);
ok = SetHandleInformation(_hReadStdOut, HANDLE_FLAGS.INHERIT, 0);
IntPtr hRead;
ok = CreatePipe(out hRead, out _hWriteStdIn, ref sattr, 0);
ok = SetHandleInformation(_hWriteStdIn, HANDLE_FLAGS.INHERIT, 0);
var startInfo = new StartupInfo
{
dwFlags = 0x0001 | 0x0100,
wShowWindow = 0,
hStdOutput = hWrite,
hStdError = hWrite,
hStdInput = hRead // If this is IntPtr.Zero, I get everything from stdout
};
SECURITY_ATTRIBUTES pSec = new SECURITY_ATTRIBUTES();
pSec.nLength = Marshal.SizeOf(pSec);
SECURITY_ATTRIBUTES tSec = new SECURITY_ATTRIBUTES();
tSec.nLength = Marshal.SizeOf(tSec);
unsafe
{
ok = CreateProcess(
null,
pathToExeAndArgs,
ref pSec,
ref tSec,
true,
0,
IntPtr.Zero,
null,
ref startInfo,
out _processInfo);
}
I have a BackgroundWorker processing stdout on DoWork which reads the pipe like so:
success = ReadFile(
_hReadStdOut,
bufPtr,
1024,
&read,
IntPtr.Zero);
I'm not using the .Net Process class because it didn't obtain data from stdout until the console application sent a newline, so I didn't get the prompt back in that case either.
Any help with this greatly appreciated.
Cheers.
I suspect the following explains what you have observed:
When you don't define hStdInput the child process uses the standard input device attached to the console. The child process detects that standard input is an interactive console device and writes a prompt.
When you do define hStdInput the child process detects that the standard input is a pipe and so neglects to write a prompt. After all, what's the point of prompting a non-interactive input device?
The child process will use GetFileType(GetStdHandle(STD_INPUT_HANDLE)) to detect what type of device is attached to the standard input. A value of FILE_TYPE_CHAR indicates a console. When you attach a pipe to the standard input then the standard input file type will be FILE_TYPE_PIPE.
My conclusion is that everything is working as designed and intended.

C# STARTUPINFO Flags to show UI for a process started from a Service in XP

I'm launching a process from a windows service in XP, I'm just launching the process not trying to interact with it. The process starts but the UI does not show. I believe I need to set some flags in STARTUPINFO to make process visible, and hoping someone could show how and what flags to set.
sPath = #"C:\Windows\notepad.exe";
string Message = string.Empty;
// Variables
PROCESS_INFORMATION processInfo = new PROCESS_INFORMATION();
STARTUPINFO startInfo = new STARTUPINFO();
Boolean bResult = false;
IntPtr hToken = IntPtr.Zero;
try
{
// Logon user
bResult = LogonUser(
"Test",
"VirtualXP-23639",
"test",
LogonType.LOGON32_LOGON_INTERACTIVE,
LogonProvider.LOGON32_PROVIDER_DEFAULT,
out hToken
);
if (!bResult) { throw new Exception("Logon error #" + Marshal.GetLastWin32Error()); }
// Create process
startInfo.cb = Marshal.SizeOf(startInfo);
startInfo.lpDesktop = "winsta0\\default";
bResult = CreateProcessAsUser(
hToken,
null,
sPath,
IntPtr.Zero,
IntPtr.Zero,
false,
0,
IntPtr.Zero,
null,
ref startInfo,
out processInfo
);
if (!bResult)
{
Message = "Failed to Create Process on Desktop/Console. Code=" + Marshal.GetLastWin32Error().ToString();
Logging.LogError(Ascension.CM.Common.Enums.ApplicationModuleEnums.Service, Message, "Ascension.CM.ServiceWorker.ProcessLauncher.XpLaunchDesktopProcess", null);
}
}
finally
{
// Close all handles
CloseHandle(hToken);
CloseHandle(processInfo.hProcess);
CloseHandle(processInfo.hThread);
}
}
You'll at least need to allow the service to interact with the desktop, so in services.msc, click on your serivce an go to properties, then logon and select allow to interact with desktop..
I would suggest that you use the Process class in the .net framework.
Process.Start("notepad.exe")
This should have your desired effect.
Thanks guys, but I've found a solution.
I ended up using WTSQueryUserToken to get the current logged in user and then used DuplicateTokenEx to get a token that I used with CreateProcessAsUser to start the process.
For XP use session id 0 and for win7 use WTSGetActiveConsoleSessionId to get the current session Id.
This works fine with out having to use the "Allow to interact with Desktop" property.
Thanks

Categories