Writing to handle 3...? (Accessing cmd.exe handles 1,2,3... programmatically)

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Post Reply
Message
Author
lockedscope
Posts: 31
Joined: 12 Jun 2020 10:38

Writing to handle 3...? (Accessing cmd.exe handles 1,2,3... programmatically)

#1 Post by lockedscope » 18 Jul 2020 08:31

I tried to write handle 3 with following. There is no way to write directly to handle 3 so i create a file to with the hope of writing to handle 3 but it does not work!
It works fine for handle 1 and handle 2.

Code: Select all

using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

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();

        }
        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)
        {
            if (handle.Trim() == "3")
            {
                WriteNewHandle(message);
                return;
            }
            IntPtr stdHandle = GetStdHandle(GetHandle(handle));
            SafeFileHandle safeFileHandle = new SafeFileHandle(stdHandle, true);
            FileStream fileStream = new FileStream(safeFileHandle, FileAccess.Write);
            Encoding encoding = Encoding.ASCII;
            StreamWriter writer = new StreamWriter(fileStream, encoding);
            writer.WriteLine(message);
            writer.Flush();

        }
        static void Main(string[] args)
        {
            // https://stackoverflow.com/questions/5711291/get-the-handle-and-write-to-the-console-that-launched-our-process
            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]);
            }
        }
    }
}

Last edited by lockedscope on 19 Jul 2020 15:35, edited 2 times in total.

penpen
Expert
Posts: 2009
Joined: 23 Jun 2013 06:15
Location: Germany

Re: Writing to handle 3...?

#2 Post by penpen » 18 Jul 2020 14:46

I heavily doubt Microsoft supported that detail with a function for easy access:
Most of the console is unaccessable from outside, because MS thought that such internals are things, no other program except "cmd.exe" should fiddle with.


penpen

lockedscope
Posts: 31
Joined: 12 Jun 2020 10:38

Re: Writing to handle 3...?

#3 Post by lockedscope » 18 Jul 2020 14:48

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

*/

Post Reply