I have following .Net(C#) code finally. Hope someone could make benefit from it.
Actually my purpose is to understand how handles are written without experimenting with chained redirections but writing to them directly from a command/application.
- Instead of Process Explorer handle from SysInternals could be used but it does not identify handle as Input/Output, we could only guess if it is a handle1,2,3... with \Device\ConDrv names.
handle64 -p cmd.exe -a -nobanner | findstr "File"
44: File \Device\ConDrv
48: File \Device\ConDrv
50: File \Device\ConDrv
54: File \Device\ConDrv
58: File \Device\ConDrv
- Execute following to observe handle 3 as it becomes permanent
echo 1 2>&3 3>&1 - After it executes handle 3 is permanent with stderr, i.e. 0x104
And in handle64 output, a new file handle appears;
104: File \Device\ConDrv
So, we could use this address to write to this handle(handle 3) in our program.
- MyStdHandles.exe [Handle] [Message] [[AnotherHandle] [Message]]
- Handle; HandleNum:1 or 2, HandleAddr: 0x...
- MyStdHandles.exe 2 bbb 0x104 ccc 3>&1 | findstr /A:4E /N "^" -> findstr could get `ccc` but not `bbb` value so 0x104 is handle 3.
- MyStdHandles.exe 2 bbb 0x104 ccc 2>&1 | findstr /A:4E /N "^" -> findstr could not get `ccc` but `bbb` value because handle 3 writes to stderr, (IMPORTANT)redirecting 2>&1 does not redirect handle 3(stderr) but only handle2 itself. So, just having stderr in handle3 does not get it redirected when handle2(stderr) redirected.
- MyStdHandles.exe 2 bbb 0x104 ccc 3>&1 2>&1 | findstr /A:4E /N "^" - findstr could get both `bbb` and `ccc`because handles1,2,3 write to stdout and get piped.
- MyStdHandles.exe 2 bbb 0x104 ccc 3>&2 2>&1 | findstr /A:4E /N "^" - findstr could get `bbb` and not `ccc` because only handle1,2 write to stdout and get piped.
- MyStdHandles.exe 2 bbb 0x104 ccc 2>&1 3>&2 | findstr /A:4E /N "^" - findstr could get `bbb, ccc` because handles1,2,3 write to stdout and get piped
- I determined that when cmd runs as administrator, Handle64 could show full name of handles(Input/Output) and also read/write state.
handle64 -p cmd.exe -a -nobanner | findstr "File"
44: File (---) \Device\ConDrv\Connect
4C: File (---) \Device\ConDrv\Reference
54: File (---) \Device\ConDrv\Input
58: File (---) \Device\ConDrv\Output
5C: File (---) \Device\ConDrv\Output
84: File (RW-) D:\CaptureCmdOutput
98: File (---) \Device\CNG
FC: File (R-D) C:\Windows\System32\en-US\cmd.exe.mui
118: Key HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
120: File (---) \Device\NamedPipe\
- Alternatively, this code could be used to dump handles.
https://github.com/apriorit/handles
- Always close streams explicitly otherwise path exceptions raised, might be handles are closed/opened in redirection steps.
writer.Flush();
writer.Close();
Code: Select all
using Microsoft.Win32.SafeHandles;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace MyStdHandles
{
class Program
{
[DllImport("kernel32.dll",
EntryPoint = "GetStdHandle",
SetLastError = true,
CharSet = CharSet.Auto,
CallingConvention = CallingConvention.StdCall)]
private static extern IntPtr GetStdHandle(int nStdHandle);
private const int STD_INPUT_HANDLE = -10;
private const int STD_OUTPUT_HANDLE = -11;
private const int STD_ERROR_HANDLE = -12;
private const int STD_HANDLE3 = -13;
[DllImport("kernel32.dll", CharSet = CharSet.Auto)] //, EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall
private static extern IntPtr CreateFileW(
string lpFileName,
UInt32 dwDesiredAccess,
UInt32 dwShareMode,
IntPtr lpSecurityAttributes,
UInt32 dwCreationDisposition,
UInt32 dwFlagsAndAttributes,
IntPtr hTemplateFile
);
const UInt32 GENERIC_WRITE = 0x40000000;
const UInt32 GENERIC_READ = 0x80000000;
const UInt32 OPEN_EXISTING = 0x00000003;
const int FILE_SHARE_WRITE = 2;
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool SetStdHandle(int nStdHandle, IntPtr hHandle);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
public const uint CREATE_NEW = 1;
static void WriteNewHandle(string message)
{
var outFile = CreateFileW("CONOUT$", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_WRITE, IntPtr.Zero, OPEN_EXISTING, /*FILE_ATTRIBUTE_NORMAL*/0, IntPtr.Zero);
var safeHandle = new SafeFileHandle(outFile, true);
SetStdHandle(STD_OUTPUT_HANDLE, outFile);
var fs = new FileStream(safeHandle, FileAccess.Write);
var writer = new StreamWriter(fs) { AutoFlush = true };
//Console.SetOut(writer);
writer.WriteLine(message);
writer.Flush();
writer.Close();
}
static void WriteByHandleAddr(string handle, string message)
{
IntPtr handleAddr = (IntPtr)Convert.ToInt32(handle, 16);
var safeFileHandle = new SafeFileHandle(handleAddr, true);
FileStream fileStream = new FileStream(safeFileHandle, FileAccess.Write);
Encoding encoding = Encoding.ASCII;
var writer = new StreamWriter(fileStream, encoding){ AutoFlush = true };
writer.WriteLine(message);
writer.Flush();
writer.Close();
}
static int GetHandle(string handle)
{
int stdHandle = STD_OUTPUT_HANDLE;
switch (handle)
{
case "1": stdHandle = STD_OUTPUT_HANDLE; break;
case "2": stdHandle = STD_ERROR_HANDLE; break;
//case "3": stdHandle = STD_HANDLE3; break; // It does not work
}
return stdHandle;
}
static void WriteHandle(string handle, string message)
{
// Following does not work
//if (handle.Trim() == "3")
//{
// WriteNewHandle(message);
// return;
//}
if (handle.Trim().StartsWith("0x"))
{
WriteByHandleAddr(handle, message);
return;
}
// Based on https://stackoverflow.com/questions/5711291/get-the-handle-and-write-to-the-console-that-launched-our-process
IntPtr stdHandle = GetStdHandle(GetHandle(handle));
SafeFileHandle safeFileHandle = new SafeFileHandle(stdHandle, true);
Console.WriteLine($"Console.WriteLine(Handle1) - Handle: {handle} Address:0x{safeFileHandle.DangerousGetHandle().ToInt64():X} Handle Addr:0x{stdHandle.ToInt64():X}");
FileStream fileStream = new FileStream(safeFileHandle, FileAccess.Write);
Encoding encoding = Encoding.ASCII;
var writer = new StreamWriter(fileStream, encoding){ AutoFlush = true };
writer.WriteLine(message);
writer.Flush();
writer.Close();
}
static void Main(string[] args)
{
// Check with Process Explorer, cmd.exe, file handles windows below the screen.
// Observe the handle values column, it is the handle ptr or safehandle value. Get the safe handle value use it as handle argument 0x54 etc. or use 1 or 2 for standard handles.
// When cmd.exe starts, there are 1 input(handle1: stdin) and 2 output(handle2:stdout,handle3:stderr) handles.
// Sample;
// Check handle values;
// MyStdHandles.exe 1 a
// Handle: 1 Address:0x54 Handle Addr:0x54
// MyStdHandles.exe 2 b
// Handle: 2 Address:0x58 Handle Addr:0x58
// - echo 1 2>&3 3>&1 - After it executes handle 3 is permanent, i.e. 0x104
// - MyStdHandles.exe 2 bbb 0x104 ccc 3>&1 | findstr /A:4E /N "^" -> findstr could get `ccc` but not `bbb` value so 0x104 is handle 3.
// - MyStdHandles.exe 2 bbb 0x104 ccc 2>&1 | findstr /A:4E /N "^" -> findstr could not get `ccc` but `bbb` value because handle 3 writes to stderr, (IMPORTANT)redirecting 2>&1 does not redirect handle 3(stderr) but only handle2 itself. So, just having stderr in handle3 does not get it redirected when handle2(stderr) redirected.
// - MyStdHandles.exe 2 bbb 0x104 ccc 3>&2 2>&1 | findstr /A:4E /N "^" - findstr could get `bbb` and not `ccc` because only handle1,2 write to stdout and get piped.
// - MyStdHandles.exe 2 bbb 0x104 ccc 2>&1 3>&2 | findstr /A:4E /N "^" - findstr could get `bbb, ccc` because handles1,2,3 write to stdout and get piped
if (args.Length < 2)
{
Console.WriteLine("[arg1: Handle Number] [arg2: message1] [arg3: Handle Number] [arg4: message2]\n");
Console.WriteLine("Test with this;\n MyStdHandles.exe 1 normal 2 error 2>&1 | findstr /A:4E /N \"^\"");
return;
}
WriteHandle(args[0], args[1]);
if (args.Length >= 4)
{
WriteHandle(args[2], args[3]);
}
}
}
}
/*
# 3>&2 2>&1
- 3>&2
- 2 need to be duplicated to 3 and 3 is empty.
- dup2(2,3), 3=stderr(2 is stderr)
- 2>&1
- 1 need to be duplicated to 2 but 2 is not empty.
- dup(2)->4(currently empty one), 4=stderr
- close(2), 2=nothing
- dup2(1,2), 2=stdout(1 is stdout)
- Finally, 1=2=stdout, 3=4=stderr
- So, 1=2 writes to stdout but 3 does not write and will not be piped.
- Restoration
- 3=2; 2=3, 2=stderr, 3=nothing
- 4=2; 2=4, 2=stderr, 4=nothing
- So, 1=stdout, 2=stderr, 3=nothing(or stderr)
# 3>&2 2>&1, when 3 is initially stderr.
- 3>&2
- 2 need to be duplicated to 3 but 3 is not empty.
- dup(3)->4(currently empty one), 4=stderr
- close(3), 3=nothing
- dup2(2,3), 3=stderr(2 is stderr)
- 2>&1
- 1 need to be duplicated to 2 but 2 is not empty.
- dup(2)->5(currently empty one), 5=stderr
- close(2), 2=nothing
- dup2(1,2), 2=stdout(1 is stdout)
- Finally, 1=2=stdout, 3=4=5=stderr
- So, 1=2 writes to stdout but 3 does not write and will not be piped.
- Restoration
- 4=3, 3=4, 3=stderr, 4=nothing
- 5=2, 2=5, 2=stderr, 5=nothing
- So, 1=stdout, 2,3=stderr
# 2>&1 3>&2
- 2>&1
- 1 need to be duplicated to 2 but 2 is not empty.
- dup(2)->3(currently empty one), 3=stderr
- close(2), 2=nothing
- dup2(1,2), 2=stdout(1 is stdout)
- 3>&2
- 2 need to be duplicated to 3 but 3 is not empty.
- dup(3)->4(currently empty one), 4=stderr
- close(3), 3=nothing
- dup2(2,3), 3=stdout(2 is stdout)
- Finally, 1=2=3=stdout, 4=stderr
- So, only 1=2=3 writes to stdout and they will be piped.
# 2>&1 3>&2, when 3 is initialy stderr
- 2>&1
- 1 need to be duplicated to 2 but 2 is not empty.
- dup(2)->4(currently empty one), 4=stderr
- close(2), 2=nothing
- dup2(1,2), 2=stdout(1 is stdout)
- 3>&2
- 2 need to be duplicated to 3 but 3 is not empty.
- dup(3)->5(currently empty one), 5=stderr
- close(3), 3=nothing
- dup2(2,3), 3=stdout(2 is stdout)
- Finally, 1=2=3=stdout, 4=5=stderr
- So, only 1=2=3 writes to stdout and they will be piped.
- Restoration;
- 4=2, 2=4, 2=stderr, 4=nothing
- 5=3, 3=5, 3=stderr, 5=nothing
# 2>&1
- 2>&1
- 1 need to be duplicated to 2 but 2 is not empty.
- dup(2)->3(currently empty one), 3=stderr
- close(2), 2=nothing
- dup2(1,2), 2=stdout(1 is stdout)
- Finally, 1=2=stdout, 3=stderr
*/