Sunday, June 27, 2010

Crossing the process boundary with .NET

 

Every so often my post Hacking my way across the process boundary gets some attention. Mostly in the form of requests for a .NET version of this technique. Now out of laziness more than anything else I have not actually taken the time or effort to do the conversion until now. So for those that need to access ListView or TreeView data from another process here is a simple example of one possible way to do it. Since I used C# for the example, I could very well have used unsafe code blocks to do some of the work, however I decided to avoid this making this example applicable to VB.NET developers as well. If you would like to see a version using unsafe code blocks, drop me a note and I will get round to it. To keep the sample short I have removed anything but the most rudimentary error checking. For an explanation of this code, please refer to the original post sighted above.

 

using System;
using System.Runtime.InteropServices;
using System.Text;
public class CrossProcessMemory
{
const int LVM_GETITEM = 0x1005;
const int LVM_SETITEM = 0x1006;
const int LVIF_TEXT = 0x0001;
const uint PROCESS_ALL_ACCESS = (uint)(0x000F0000L | 0x00100000L | 0xFFF);
const uint MEM_COMMIT = 0x1000;
const uint MEM_RELEASE = 0x8000;
const uint PAGE_READWRITE = 0x04;

[DllImport("user32.dll")]
static extern bool SendMessage(IntPtr hWnd, Int32 msg, Int32 wParam, IntPtr lParam);

[DllImport("user32")]
static extern IntPtr GetWindowThreadProcessId( IntPtr hWnd, out int lpwdProcessID );

[DllImport("kernel32")]
static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle,
int dwProcessId);

[DllImport("kernel32")]
static extern IntPtr VirtualAllocEx( IntPtr hProcess, IntPtr lpAddress,
int dwSize, uint flAllocationType, uint flProtect);

[DllImport("kernel32")]
static extern bool VirtualFreeEx( IntPtr hProcess, IntPtr lpAddress, int dwSize,
uint dwFreeType );

[DllImport("kernel32")]
static extern bool WriteProcessMemory( IntPtr hProcess, IntPtr lpBaseAddress,
ref LV_ITEM buffer, int dwSize, IntPtr lpNumberOfBytesWritten );

[DllImport("kernel32")]
static extern bool ReadProcessMemory( IntPtr hProcess, IntPtr lpBaseAddress,
IntPtr lpBuffer, int dwSize, IntPtr lpNumberOfBytesRead );

[DllImport("kernel32")]
static extern bool CloseHandle( IntPtr hObject );

[StructLayout(LayoutKind.Sequential)]
public struct LV_ITEM
{
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 static string ReadListViewItem( IntPtr hWnd, int item )
{
const int dwBufferSize = 1024;

int dwProcessID;
LV_ITEM lvItem;
string retval;
bool bSuccess;
IntPtr hProcess = IntPtr.Zero;
IntPtr lpRemoteBuffer = IntPtr.Zero;
IntPtr lpLocalBuffer = IntPtr.Zero;
IntPtr threadId = IntPtr.Zero;

try
{
lvItem = new LV_ITEM();
lpLocalBuffer = Marshal.AllocHGlobal(dwBufferSize);
// Get the process id owning the window
threadId = GetWindowThreadProcessId( hWnd, out dwProcessID );
if ( (threadId == IntPtr.Zero) || (dwProcessID == 0) )
throw new ArgumentException( "hWnd" );

// Open the process with all access
hProcess = OpenProcess( PROCESS_ALL_ACCESS, false, dwProcessID );
if ( hProcess == IntPtr.Zero )
throw new ApplicationException( "Failed to access process" );

// Allocate a buffer in the remote process
lpRemoteBuffer = VirtualAllocEx( hProcess, IntPtr.Zero, dwBufferSize, MEM_COMMIT,
PAGE_READWRITE );
if ( lpRemoteBuffer == IntPtr.Zero )
throw new SystemException( "Failed to allocate memory in remote process" );

// Fill in the LVITEM struct, this is in your own process
// Set the pszText member to somewhere in the remote buffer,
// For the example I used the address imediately following the LVITEM stuct
lvItem.mask = LVIF_TEXT;
lvItem.iItem = item;
lvItem.pszText = (IntPtr)(lpRemoteBuffer.ToInt32() + Marshal.SizeOf(typeof(LV_ITEM)));
lvItem.cchTextMax = 50;

// Copy the local LVITEM to the remote buffer
bSuccess = WriteProcessMemory( hProcess, lpRemoteBuffer, ref lvItem,
Marshal.SizeOf(typeof(LV_ITEM)), IntPtr.Zero );
if ( !bSuccess )
throw new SystemException( "Failed to write to process memory" );

// Send the message to the remote window with the address of the remote buffer
SendMessage( hWnd, LVM_GETITEM, 0, lpRemoteBuffer);

// Read the struct back from the remote process into local buffer
bSuccess = ReadProcessMemory( hProcess, lpRemoteBuffer, lpLocalBuffer, dwBufferSize,
IntPtr.Zero );
if ( !bSuccess )
throw new SystemException( "Failed to read from process memory" );

// At this point the lpLocalBuffer contains the returned LV_ITEM structure
// the next line extracts the text from the buffer into a managed string
retval = Marshal.PtrToStringAnsi((IntPtr)(lpLocalBuffer.ToInt32() +
Marshal.SizeOf(typeof(LV_ITEM))));
}
finally
{
if ( lpLocalBuffer != IntPtr.Zero )
Marshal.FreeHGlobal( lpLocalBuffer );
if ( lpRemoteBuffer != IntPtr.Zero )
VirtualFreeEx( hProcess, lpRemoteBuffer, 0, MEM_RELEASE );
if ( hProcess != IntPtr.Zero )
CloseHandle( hProcess );
}
return retval;
}
}



15 comments:

  1. Hi Chris,
    Using your code i am able to get text from various listviews, but not from a ownerdrawfixed (and unicode) syslistview32 of an external app. can you provide some insight for the same.

    regards..

    p.s. - you got some great biceps ;)

    ReplyDelete
  2. Thanks a lot for providing the C# conversion.
    Just tried this code on Windows 7 (64 bit) and it didnt return any value. Works ok on 32 bit windows.

    I tried changing the last line to something like this
    retval = Marshal.PtrToStringAnsi((IntPtr)(lpLocalBuffer.ToInt64() + Marshal.SizeOf(typeof(LV_ITEM))));

    But still no change. The retValue is still String.Empty.

    Kindly reflect on this and any other changes required to make it work on 64 bit windows .

    Help will be appreciated.
    Thanks

    ReplyDelete
    Replies
    1. I just tested it on a Windows 7 machine accessing the ListView control from another application (.NET) and it worked fine. Are you certain that you are using the correct handle and that the control you are trying to access is a ListView control?

      Delete
    2. Sorry, I forgot to mention, it is 64 Bit Windows 7.

      Delete
    3. Chris: it doesn't work if the calling (stealer) app is 32bit and the target (hosting the listview) app is 64bit.

      Also I couldn't make it work in the 64bit-64bit scenario also.

      If both are 32-bits, it works.

      Delete
  3. Hey may you please update or explain How to read cells inside SysListView32

    ReplyDelete
    Replies
    1. There should not be much difference when reading the sub items, you just need to set the iSubItem member of the LV_ITEM structure to the relevant sub-item index.

      http://msdn.microsoft.com/en-us/library/windows/desktop/bb774760(v=vs.85).aspx

      Delete
    2. Thanks for Reply Chris.

      i tested what you said, with referring to other lots of code found on internet.

      ui automation
      http://pastebin.com/6x7rXMiW

      with other P/Invoke and Marshaling
      http://pastebin.com/61RjXZuK


      now both code run perfectly where Name property have same value. but if name property is null both code returns null/empty, but cell still contains a value which we cant see in UiSpy or other tools.

      Works on

      Identification
      ClassName: ""
      ControlType: "ControlType.Text"
      Culture: "(null)"
      AutomationId: ""
      LocalizedControlType: "text"
      Name: "May 03 12:49:17 2012"


      and returns null/empty on

      Identification
      ClassName: ""
      ControlType: "ControlType.Text"
      Culture: "(null)"
      AutomationId: ""
      LocalizedControlType: "text"
      Name: ""


      see here, Natalia is referring to same, where name field contains same as value in a cell.
      http://stackoverflow.com/questions/8547537/datagrid-contents-with-ui-automation-and-net


      Hope you will understand the problem.

      Delete
  4. i'm having a difficult time figuring out how you would get the other columns of a listview, i'd really appreciate any feedback you can give me on this. For me this seems to just pull the first column's data from a listview

    ReplyDelete
    Replies
    1. To access the other columns you need to set the iSubItem member in the LV_ITEM structure. For example, to read the second column you would add the following

      lvItem.iSubItem = 1;

      Delete
  5. I am having a really hard time trying to convert this to get the data from a listbox. I'd really appreciate any help you can provide.

    ReplyDelete
    Replies
    1. @Carolyn, what have you tried and what problems are you facing. Can you post some simple code somewhere that I could take a look at?

      Delete
    2. Snippet is below. Does this process also work for an OwnerDrawn listbox.

      IntPtr hProc;
      IntPtr lpExternText;
      IntPtr lpText = IntPtr.Zero;

      int processID = 0;

      IntPtr NumberOfBytesWritten = IntPtr.Zero;

      int nrItems = (int)SendMessage(hwnd, LB_GETCOUNT, 0,IntPtr.Zero);

      GetWindowThreadProcessId(hwnd, out processID);
      hProc = OpenProcess((uint)(ProcessAccessTypes.PROCESS_ALL_ACCESS ), false, processID);

      lpExternText = VirtualAllocEx(hProc, IntPtr.Zero, 4096, (uint)(VirtualAllocExTypes.MEM_RESERVE | VirtualAllocExTypes.MEM_COMMIT), (uint)AccessProtectionFlags.PAGE_READWRITE);

      int i = 1;
      WriteProcessMemory(hProc, lpExternText, Marshal.AllocHGlobal(lpText), 512, NumberOfBytesWritten);

      int lenght = (int)SendMessage(hwnd, LB_GETTEXTLEN, i, IntPtr.Zero);
      IntPtr iFile = SendMessage(hwnd, LB_GETTEXT, i, lpExternText);

      byte[] lpBuffer = new byte[lenght];
      int nSize = (int)lenght;

      IntPtr lpNumberOfBytesWritten = IntPtr.Zero;
      ReadProcessMemory(hProc, lpExternText, Marshal.UnsafeAddrOfPinnedArrayElement(lpBuffer, 0), nSize, lpNumberOfBytesWritten);

      string LB_text = BitConverter.ToString(lpBuffer, 0);

      Delete
  6. Can I use this to delete a certain row from a another process, and can you provide a working sample for noob like me to study,

    thanks, much appreciated

    ReplyDelete
  7. @Carolyn, @Rojalde, you have caught me at a bad time. I have been at a conference and this week I am moving to a new house. I hope to be in better shape late next week and I will provide you both with samples. Sorry for the delay.

    ReplyDelete