Wednesday 11 May 2011

UIAutomation, Unmanaged APIs and Powershell

Consider this scenario: you have an old, legacy program written in Visual Basic 6 (ThunderRT6 style controls). It is closed source and gets data back from an unknown location to display needed information in a list view format. Unfortunately, the output is somewhat lacking: no column sorting, no copying or parsing of the output, just viewing it.

How do you get this information back directly from the running program, so you can use it for your own needs?

The answer is via the .NET UI Automation interface. The assemblies in question are UIAutomationTypes and UIAutomationClient, among others, which you can add to your program's runspace via Add-Type using the -AssemblyName parameter.

The first thing you need to do is get the desktop:
$desktop=[Windows.Automation.AutomationElement]::RootElement

Once you have the desktop, you can find your program.

$myprogram=@($desktop.FindAll("Subtree",[Windows.Automation.Condition]::TrueCondition)|where-object {$_.Current.Name -match "My Program's Title"})[0]

Once you've verified your program's form is running and able to be found, you can find items within its control space:

$lview=@($myprogram.FindAll("Subtree",[Windows.Automation.Condition]::TrueCondition)|where-object {$_.Current.ClassName -match "ListView20"})[0]

The example above is returning classes matching ListView20. We are looking for the control ListView20WndClass in this case - an old VB6 list view control. Once we have it, $lview.Current.NativeWindowHandle gives us the hWnd to do more technical stuff...


Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;

namespace Win32 {
public class APIs {

[DllImport("user32.dll", EntryPoint="SendMessage", CharSet=CharSet.Auto)]
public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
public static extern IntPtr VirtualAllocEx(UInt32 hProcess,IntPtr lpAddress,IntPtr dwSize,UInt32 flAllocationType,UInt32 flProtect);
[DllImport("kernel32.dll")]
public static extern Boolean VirtualFreeEx(UInt32 hProcess,IntPtr lpAddress,IntPtr dwSize,UInt32 dwFreeType);
[DllImport("user32.dll")]
public static extern IntPtr GetWindowThreadProcessId(IntPtr hWnd,out int procId);
[DllImport("kernel32.dll")]
public static extern IntPtr OpenProcess(UInt32 dwDesiredAccess,Boolean bInheritHandle,UInt32 dwProcessId);
[DllImport("kernel32.dll")]
public static extern Boolean WriteProcessMemory(UInt32 hProcess,IntPtr lpBaseAddress,IntPtr lpBuffer,IntPtr nSize,out int lpNumberOfBytesWritten);
[DllImport("kernel32.dll")]
public static extern Boolean ReadProcessMemory(UInt32 hProcess,IntPtr lpBaseAddress,IntPtr lpBuffer,IntPtr nSize,out int lpNumberOfBytesRead);
[DllImport("kernel32.dll")]
public static extern Boolean CloseHandle(UInt32 hObject);
}

[StructLayoutAttribute(LayoutKind.Sequential)]
public struct LVITEM {
public uint mask;
public int iItem;
public int iSubItem;
public uint state;
public uint stateMask;
public IntPtr pszText;
public int cchTextMax;
public int iImage;
public IntPtr lParam;
}

}
"@

$lvpid=[Intptr]::Zero
$null=[Win32.APIs]::GetWindowThreadProcessId($lview.Current.NativeWindowHandle,[ref] $lvpid)


Here we have loaded functions from Kernel32 and User32 that will allow us to accomplish our goals. We get back the process id of the Window handle which allows us to write to the processes memory space.

Why would you want to do that, you ask? Well, since LVM_GETLISTTEXT, the message which allows you to return the text of a listview item, is above 1024 (the value is 0x102D specifically). Below this value (WM_USER), Windows performs automatic memory marshalling for you, however for custom controls such as this listview control, passing it a pointer to memory in Powershell's runspace with its managed code and shiny bells and whistles, would be a very bad thing (tm). Thats because this pointer is specific to Powershell's address space. The same pointer in the programs address space makes no sense and probably would overwrite critical program data.

Not a problem though, the function OpenProcess will allow you to allocate data in the other program's address space and copy data back from this memory.


$lvitem=New-Object Win32.LVITEM;
$lvphandle=[Win32.APIs]::OpenProcess(0x1f0fff,$False,$lvpid);
$lvstructmem=[Win32.APIs]::VirtualAllocEx($lvphandle.ToInt32(),[IntPtr]::Zero,[System.Runtime.InteropServices.Marshal]::SizeOf($lvitem),0x3000,0x04);
$lvstringmem=[Win32.APIs]::VirtualAllocEx($lvphandle.ToInt32(),[IntPtr]::Zero,1024,0x3000,0x04);
$numitems=[Win32.APIs]::SendMessage($lview.Current.NativeWindowHandle,0x1004,[IntPtr]::Zero,[IntPtr]::Zero)


Here we have created an LVITEM structure, described in the MSDN documentation for LVM_GETITEMTEXT, and allocated some memory in the other processes memory space for it and a character buffer large enough to house the string. We then get back the number of items in the list view (LVM_GETITEMCOUNT is 0x1004). This can be done without any marshalling - SendMessage just returns this value as an int.

We would then need to loop over the items in the listview starting from index zero.


$lvitem.pszText=$lvstringmem;
$lvitem.cchTextMax=1024;
$lvitem.iSubItem=0


Here we set the LVITEM structure to reference the memory in the other process. We then need to copy it to a local buffer and copy this buffer ultimately to the processes memory space:


$ptrtolocalbuff=[System.Runtime.InteropServices.Marshal]::AllocHGlobal(1024);
....
[System.Runtime.InteropServices.Marshal]::StructureToPtr($lvitem,$ptrtolocalbuff,$False);
$null=[Win32.APIs]::WriteProcessMemory($lvphandle.ToInt32(),$lvstructmem,$ptrtolocalbuff,[System.Runtime.InteropServices.Marshal]::SizeOf($lvitem),[ref] $outBytes)


Finally, we call SendMessage with the LVM_GETITEMTEXT message to get back the text of the subkey, and copy it back to our own memory space. We can then convert the buffer pointer to a string. In this case the VB6 program was using ANSI string encoding.


$bytesRet=[Win32.APIs]::SendMessage($lview.Current.NativeWindowHandle,0x102D,$lvitemnum,$lvstructmem)
$null=[Win32.APIs]::ReadProcessMemory($lvphandle.ToInt32(),$lvstringmem,$ptrtolocalbuff,1024,[ref] $outBytes);
$uname=[System.Runtime.InteropServices.Marshal]::PtrToStringAnsi($ptrtolocalbuff);


So there you have it. You would need to perform similar marshalling and calling of SendMessage for each field in the listview you wanted to get back as text.

From there you can do what you want with the data. Don't forget at the end however, to deallocate all the allocated memory:


$null=[Win32.Apis]::VirtualFreeEx($lvphandle.ToInt32(), $lvstringmem, 0, 0x8000)
$null=[Win32.Apis]::VirtualFreeEx($lvphandle.ToInt32(), $lvstructmem, 0, 0x8000)
$null=[Win32.APIs]::CloseHandle($lvphandle.ToInt32())
$null=[System.Runtime.InteropServices.Marshal]::FreeHGlobal($ptrtolocalbuff)

1 comment:

  1. Hello,

    I know this is a very old posting, but I was unable to get this to work for me. Is the line "$ptrtolocalbuff=[System.Runtime.InteropServices.Marshal]::AllocHGlobal(1024);
    ...." correct? Notice it has the 4 dots at the end.

    ReplyDelete