• Status: Solved
  • Priority: Medium
  • Security: Public
  • Views: 2196
  • Last Modified:

How to read/copy strings from a listbox in another process

I am trying to read all strings in a listbox (owner-draw, does not store strings in the listbox, just pointers to them) that exists in another process.  I have the process id and the control id and thus can access the control, the difficulty is reading the strings since they are stored in the other memory space.  There seem to be two possibilities:

Option 1.  Allocate memory in the other process, use the Get_Text listbox function to copy each string into that memory, then use ReadProcessMemory to copy into memory I can see.  Code for this is as follows:

int CAPP20Dlg::Read_LB(int lb_id, CStringArray* pOutput)
{
      CListBox* pLB = (CListBox*) Get_Control(lb_id);  // gets the listbox from the control id- this works fine
      int i, count = pLB->GetCount();  // this works too- the correct numberof items shows up
      static char item[LB_READBUFFER_SIZE];
      unsigned long pid;
      HANDLE process;
      LPCVOID address;

      pOutput->RemoveAll();
      GetWindowThreadProcessId(pLB->m_hWnd, &pid);
      process=OpenProcess (PROCESS_VM_OPERATION|PROCESS_VM_READ|PROCESS_VM_WRITE|PROCESS_QUERY_INFORMATION, FALSE, pid);
      for(i=0; i<count; i++) {
            pLB->GetText(i, (LPTSTR) &address);
            ReadProcessMemory(process, address, item, LB_READBUFFER_SIZE, NULL);
            pOutput->Add((LPCTSTR) item);
      }
      CloseHandle(process);
      return count;
}

The problem with this code is that when executed in debug, although it does get the strings from the other process, it produces a run time checking error #2 saying the stack memory around address is corrupted.  I tried turning off the run time checking with #pragma runtime_checks( "s", off ), doing so gets rid of the error but the strings from the list box then do not get captured!  Interestingly this is even though with the error they were showing up correctly.  Unfortunately the error terminats execution after the function, so I can't just ignore it even if I wanted to.

Option 2: I suppose I could just copy the strings into the clipboard and transfer them that way.  Since I can use the mouse and highlight the text in the listbox, right click, select copy and successfully paste the text into notepad, I therefore thought it would be easy to do the same with the following code:

int CAPP20Dlg::Read_TableLB(int lb_id, CStringArray* pOutput)
{
      HWND      hWnd = Get_Dialog()->m_hWnd;
      HWND      hBut = Get_Control(lb_id)->m_hWnd;
      CListBox* pLB = (CListBox*) Get_Control(lb_id);

      int d, c = pLB->GetCount();
      for (d = 0; d < c; d++){
            pLB->SetCurSel(d);
            ::SendMessage(hBut, WM_COPY, 0, 0);
      }
      ... then the code to paste from the clipboard to a control and read it into the array
}

Unfortunately when I single step through and try to paste after the SendMessage call (by going to notepad and hitting paste, just to see if it captured anything) it does not capture any text!  I also tried using SetSelRange, but it didn't work any better.

I would prefer a solution to option 1 (i.e. how to get rid of the stack error and still get the data), but will award points for solutions to either or both of the options.  I.e. if someone solves option 1 and the same person or someone else solves option 2, I will award 250 points to each.  Option 1 is based on an article at http://www.codeproject.com/threads/int64_memsteal.asp if that helps- it dealt with a listview control though.
0
Nmcmurtray
Asked:
Nmcmurtray
  • 15
  • 7
  • 6
  • +1
2 Solutions
 
mahesh1402Commented:
I think you need Shared Memory block for this purpose.

Refer this article how Shared Memory can be used in Inter Process Communication..
http://www.codeguru.com/Cpp/W-P/system/sharedmemory/article.php/c2879/ <======

also class for shared memory
http://www.codeproject.com/threads/sm.asp

-MAHESH
0
 
NmcmurtrayAuthor Commented:
I probably should have been more explicit- the other process is not one over which I have control of the source code or can recompile.  Accordingly I can't just declare shared memory in both processes as the article suggests.
0
 
mahesh1402Commented:
Have you tried it by sending a LB_GETTEXT message ?

-MAHESH

0
Independent Software Vendors: We Want Your Opinion

We value your feedback.

Take our survey and automatically be enter to win anyone of the following:
Yeti Cooler, Amazon eGift Card, and Movie eGift Card!

 
mahesh1402Commented:
Another Suggestion is to try that with WM_COPYDATA... Refer following :

Inter Process Communication using WM_COPYDATA
http://www.codersource.net/mfc_ipc_wm_copydata.aspx

http://www.codeproject.com/threads/ipc_wmcopy.asp
http://www.codeguru.com/Cpp/W-P/system/processesmodules/article.php/c5777/

-MAHESH
0
 
NmcmurtrayAuthor Commented:
If LB_GETTEXT had worked, I never would have asked the question!  :)  Unfortunately, LB_GETTEXT doesn't work across processes nor does it work when the listbox is an ownerdraw listbox that does not store the strings internally (ie does not have HASSTRINGS set).

WM_COPYDATA is a good thought, but I have control over the receiving program, not the sending program, so I can't get it to nicely package and pass the data to me.

Any thoughts on why option 1 is throwing the error?  It does successfully pass the data back- but the error terminates the program...
0
 
mahesh1402Commented:
whats exact error string you are getting ?

-MAHESH
0
 
mahesh1402Commented:
Also have a loot at this discussion thread which talks abt stack and IPC..may helpful to you

http://blogs.msdn.com/oldnewthing/archive/2006/01/17/513779.aspx <==

-MAHESH
0
 
mahesh1402Commented:

Will you try allocating memory to 'item' by VirtualAllocEx() function before using....

after using free it using VirtualFreeEx().

-MAHESH
0
 
jkrCommented:
Your 1st approach seems to be the way to go for me. What is your 'LB_READBUFFER_SIZE' and how many bytes are copied actually? Check that with

          DWORD dwRead;
          ReadProcessMemory(process, address, item, LB_READBUFFER_SIZE, &dwRead);

Chances are that increasing the buffer size to a page size will solve the issue.
0
 
NmcmurtrayAuthor Commented:
The exact error string is: Run-Time Check Failure #2 - Stack around the variable 'address' was corrupted.  

I tried modifying it as follows with VirtualAllocEX():

int CApp20Dlg::Read_LB(int lb_id, CStringArray* pOutput)
{
      CListBox* pLB = (CListBox*) Get_Control(lb_id);
      int i, count = pLB->GetCount();
      static char item[LB_READBUFFER_SIZE];
      unsigned long pid;
      HANDLE process;
      char*      pr_item;

      pOutput->RemoveAll();
      GetWindowThreadProcessId(pLB->m_hWnd, &pid);
      process=OpenProcess(PROCESS_VM_OPERATION|PROCESS_VM_READ|PROCESS_VM_WRITE|PROCESS_QUERY_INFORMATION, FALSE, pid);
      
      pr_item=(char*)VirtualAllocEx(process, NULL, LB_READBUFFER_SIZE, MEM_COMMIT, PAGE_READWRITE);
      
      
      for(i=0; i<count; i++) {
            pLB->GetText(i, (LPTSTR) &pr_item);            
            ReadProcessMemory(process, pr_item, item, LB_READBUFFER_SIZE, NULL);
            pOutput->Add((LPCTSTR) item);
            
      }
      VirtualFreeEx(process, pr_item, 0, MEM_RELEASE);
      CloseHandle(process);
      return count;
}

Same error, but now its at 'pr_item' rather than 'address'.

Interestingly, the error is tied to the statement pLB->GetText(i, (LPTSTR) &pr_item); When I comment this out, the error is not produced, though neither of course are the list box strings.  If I put the line back in but change it to pLB->GetText(i, (LPTSTR) pr_item); which makes it a direct reference to the allocated memory, it does not cause the error, but also does not get the strings.

I'm sure I'm missing something obvious, but I just can't see it.
   
0
 
NmcmurtrayAuthor Commented:
The buiffer size is 1024 char.  Tried moving it up to 65536 but same error...
0
 
NmcmurtrayAuthor Commented:
Also tried setting the buffer size to 8096 bytes.  The full 8096 bytes were returned in dwRead.  Same error...
0
 
jkrCommented:
The problem could be

          pLB->GetText(i, (LPTSTR) &address);
          ReadProcessMemory(process, address, item, LB_READBUFFER_SIZE, NULL);
          pOutput->Add((LPCTSTR) item);

as 'GetText()' tries to write the string contents to where 'address' is stored. Have you tried to

    LPCVOID address [1024];

(Yes, that's an ungly hack and for testing purposes only)
0
 
DanRollinsCommented:
There is a fantastic article about this here:
     MSDN Home > MSJ > September 1997
     http://www.microsoft.com/msj/0997/win320997.aspx

Although it is about getting data from a ListView,  it mentions a specific peculiarity of the LB_GETTEXT message.  The included source code should provide enough details to show how to marshal any data across the process boundary.
    LV2Clip
    http://www.microsoft.com/msj/0997/win32textfigs.htm#fig1

BTW, just to avoid skipping some possible issues:  

1) Are you 100% certain that it is a listbox and not a ListView control (most modern apps use the latter) you can check with Spy++.

2) Have you tried sending other messages that pass data without a buffer -- such as LB_GETCOUNT ?

-- Dan
0
 
NmcmurtrayAuthor Commented:
Didn't work- wouldn't even compile in fact.

It seems the problem is entirely dependent on whether GetText is called with &address or just address.  &address gets the data but produces the run-time check failure. Using just address avoids the run-time check failure but does not get the data.

Any thoughts on option 2, using the clipboard?  Nowhere near as elegant but at this point I'm happy with anything that works...
0
 
NmcmurtrayAuthor Commented:
Dan,

I will check the article.  On the other points, it is definitely a listbox according to Spy++ and it does return the correct numberof items when called without a buffer using pLB->GetCount().  Will try sending LB_GETCOUNT to see it it matches up.

Nate
0
 
NmcmurtrayAuthor Commented:
Dan,

Took a look at the article.  I think its basically the same idea for ListViews as in the article I mentioned at the top of the thread.  Essentially it is the method I'm trying to adapt to listboxes, problem is the run time checking error that pops up.  The article doesn't, unfortunately, help much for that though.  :(

Nate
0
 
DanRollinsCommented:
I decided to test the validity of that info in that article.  As it says, you do NOT need to marshal out-of-process data in the LB_GETTEXT message.  It's actually as simple as:

int CD28Dlg::Read_LB( CStringArray* pOutput)
{
      HWND hWndDlg=  ::FindWindow(0,"D27");
      HWND hWndLB=   ::FindWindowEx(hWndDlg,0, "ListBox", 0);

      pOutput->RemoveAll();

      char sLocalBuf[1000];  // arbitrary, but must be larger than the text

      int nCount= ::SendMessage( hWndLB, LB_GETCOUNT, 0, 0);
      for( int j=0; j<nCount; j++) {
            int nRet= ::SendMessage( hWndLB, LB_GETTEXT, j, (LPARAM)sLocalBuf );
            sLocalBuf[nRet]= '\0';
            pOutput->Add( sLocalBuf );
      }
      return nCount;
}

Try that and see if you get any runtime errors -- it works fine for me.

If that works, then you might consider playing with CListBox* but, it is possible that that is the root of the problem.  In the above code I used only API calls (no MFC except for accumulating the CStringArray).

-- Dan
0
 
NmcmurtrayAuthor Commented:
Dan,

I gave it a shot, but the basic problem it runs into is that LB_GETTEXT is returning the address of the strings rather than the strings themselves.  Accordingly it gets back meaningless text.  If the Listbox in question was not an owner draw list box, or was an owner draw listbox wth the Has Strings characteristic enabled, it would probably work.  Unfortunately, as mentioned throughout the above, this isn't one of those- so the Listbox only stores pointers itself not the strings directly.  

I'm going to try using this method in conjunction with a call to ReadProcessMemory, to see if that then returns the correct string.  If so, then I will accept your solution with full points.

Back shortly,

Nate
0
 
NmcmurtrayAuthor Commented:
Dan,

No luck.

Nate
0
 
NmcmurtrayAuthor Commented:
Just to recap, since its been a lot of attempts: the following is the closest thing to a working function:

int CApp20Dlg::Read_TableLB(int lb_id, CStringArray* pOutput)
{
      CListBox* pLB = (CListBox*) Get_Control(lb_id);
      int i, count = pLB->GetCount();
      static char item[LB_READBUFFER_SIZE];
      unsigned long pid;
      HANDLE process;
      LPVOID      pr_item;

      pOutput->RemoveAll();
      GetWindowThreadProcessId(pLB->m_hWnd, &pid);
      process=OpenProcess(PROCESS_VM_OPERATION|PROCESS_VM_READ|PROCESS_VM_WRITE|PROCESS_QUERY_INFORMATION, FALSE, pid);
      pr_item=(char*)VirtualAllocEx(process, NULL, sizeof(pr_item), MEM_COMMIT, PAGE_READWRITE);
      
      for(i=0; i<count; i++) {
            pLB->GetText(i, (LPTSTR) &pr_item);
            ReadProcessMemory(process, pr_item, item, LB_READBUFFER_SIZE, NULL);
            pOutput->Add((LPCTSTR) item);
      }

      VirtualFreeEx(process, pr_item, 0, MEM_RELEASE);
      
      CloseHandle(process);
      return count;
}

This works in that it successfully captures all strings in the other process listbox (which is an owner-draw listbox that does not have HASSTRINGS enabled- ie the strings are not stored in the listbox just pointers to them).  It fails though because on exit it generates the following error: Run-Time Check Failure #2 - Stack around the variable 'pr_item' was corrupted.  Using pragmas to turn off run-time checking causes it not to successfully capture the strings.

I think the problem lies in the pointer syntax.  What is intended to happen is the following:

1.  Allocate storage in the other process for a pointer that will point to the string data for each item.  pr_item is thus a pointer to this pointer

2.  for each item in the listbox, call Get_Text with pr_item pointing to the address in the other process memory where the address of the string for the item is to be stored.  Thus Get_Text is going to store the address of the string someplace in other process memory, the specific place should be the address in other process memory that pr_item is pointing to.  This is necessary because Get_Text (whether called through MFC or by SendMessage) cannot cross process boundaries.

3.  then call ReadProcessMemory to read the string pointed to by the address in other memory pointed to by pr_item.  Its confusing, but basically [pr_item] points to [memory in the other process representing a pointer] that points to [the string itself].  

That's the intent of the code above, and I think it comes close but that one of the pointer references is incorrect which is causing the stack corruption.  My understanding of pointer syntax is nowhere near good enough to figure out where the problem in the code is, but I suspect its the call to GetText(i, (LPTSTR) &pr_item), since as written it returns data with the error, but if called instead as GetText(i, (LPTSTR) pr_item) the error disappears but so does the data.

Hope this helps to clarify/focus the solution being sought.  Alternatively, I know its sloppy but I really don't care about keeping the clipboard pristine- if anyone has a method for option 2 discussed above (using the clipboard to transfer the text, since this does work manually- i.e. highlighting with the mouse then right-clicking, selecting copy, then pasting) at this point I'd be happy with that instead.

Nate

Nate
0
 
DanRollinsCommented:
Something about this makes no sense:
Even if the ListBox is Owner-Drawn without LBS_HASSTRINGS, the LB_GETTEXT function should return the string data itself (filling a buffer with that data) and *not* a pointer to it.

In the above code, you are allocating a buffer the size of a pointer to a pr_item (i.e., 4 bytes) and asking the control to put the string at that address.  Since this is one of the few Win32API functions that do not provide a means to set a buffer limit on such transactions, the control is dumping all of the string text in the remote buffer... and thus overwriting whatever is at byte 5,6,7, etc above that memory location.

The first try would be to assume that LB_GETTEXT works normally, and allocate a large remote buffer (rather than a 4-byte one).

Alas, when I try that on a ListBox in a separate process, the LB_GETTEXT always returns 0xffff -- that is, LB_ERR (though LB_GETTEXTLEN always returns correct values).  That indicates to me that the "built-in marshalling" as described in that article is getting mixed up -- perhaps because a message from the external process is passing in a pointer to a local buffer (local to the remote process, that is).  Not that LB_ERR is returned when I pass avariety of invalid addresses (so it is not actually saying that my index is wrong, only that the request failed).
=-=-=-=-=-=-=-=-=-=-=

If that doesn't work (possibly because the ListBox has been subclassed by the remote process to work in an abnormal way), then you could try a two-step operation: Read the pointer, then read the data at that address.  (note: in this case, I have no clue as to why that last code could possibly work "successfully").  It would look something like:

      LPVOID pr_item;
      pr_item=(char*)VirtualAllocEx( hProcess, NULL, 4, MEM_COMMIT, PAGE_READWRITE);

      int nRet= ::SendMessage( hWndLB, LB_GETTEXT, j, (LPARAM)pr_item );
      ... if nRet != LB_ERR...
      char *p= 0;
      ReadProcessMemory( hProcess, pr_item, &p, 4, NULL); // populate p
      ReadProcessMemory( hProcess, p, sLocalBuff, 1000, NULL); // get data at addr pointed to by p

I think this is a longshot because LB_GETTEXT should *not* return a pointer to the data but rather the data itself.

Another variation of that:  
In Owner-Drawn ListBoxes without LBS_HASSTRINGS, the handler typically saves the string data and other related stuff in the ItemData value:  That is typically a pointer to a private structure... the structure may, in turn contain, say, an HICON, some flags, and a pointer to a string of text.  It might be possible to hack that structure by examining the ItemData itself.

A more complicated alternative (though ultimately, possibly the only one that might work):
Write a Window's Hook that will run in the address space of the target process.  Intercept the LB_GETTEXT call and use, for instance, WM_COPYDATA to pass it to your controlling (data-collecting) app.  Among other things, that gives you the freedom to subclass the ListBox and add your own program code to take whatever action is needed to grab the data.

=-=-=-=-=-=-=-=-=-=-=
If the external app that you are accessing is a common one, or one that I can download from somewhere, then I can try some other stuff.  But as for now, I can't replicate the results you are getting, so I'm stuck.

-- Dan
0
 
DanRollinsCommented:
I found this:

     http://msdn.microsoft.com/library/en-us/shellcc/platform/commctls/listboxes/listboxreference/listboxmessages/lb_addstring.asp

>> If you create the list box with an owner-drawn style but without the
>> LBS_HASSTRINGS style, the buffer pointed to by the lParam parameter will
>> receive the value associated with the item (the item data). " 

So, you are getting back the itemdata.

To verify, try:
        int nRet= ::SendMessage( hWndLB, LB_GETITEMDATA, j, 0);
and verify that nRet is the same value as pr_item after the LB_GETTEXT message

Now, the itemdata can be anything -- whatever the guy who wrote the owner-draw handler decided.  For instance, it could be an interger INDEX into an ARRAY of structures, or it could be a pointer to a structure, or it could be a pointer to a string or a wstring.  Since you are getting valid data after the ReadProcessMemory, then we can start by assuming the latter.

I still don't know why you would be getting stack corruption.  I'd go back the basics:  One gets  stack corruption when one writes say, 20 bytes to a 19-byte buffer.  All I can offer is to try a simple hard-coded test:

      char sLocalBuf[5000];  // arbitrary, but must be larger tha the text
      for( int j=0; j< nCount; j++) {
            int nLen= ::SendMessage( hWndLB, LB_GETTEXTLEN, j, 0 ); // eyeball  when debugging
            int nRet= ::SendMessage( hWndLB, LB_GETITEMDATA, j, 0 );
            LPVOID p= (LPVOID)nRet;
            ReadProcessMemory( hProcess, p, sLocalBuf, 100, NULL);
            // here, you MUST eyeball the text in sLocalBuf... it might be a Lstring, or anything!
            pOutput->Add( sLocalBuf );
      }

Because we only ever transfer 100 bytes into a 5000-byte buffer, we can't be corrupting the stack.

-- Dan
0
 
NmcmurtrayAuthor Commented:
Dan and Mahesh,

I definitely appreciate your help here.  In the end, partly because of Dan's post, I realized that as I thought in my clarification comment it really was a question of pointer syntax and that it probably was arising because I was going about it in a more convoluted way than necessary- why use GET_LBTEXT to get the pointer to the string/item data as char then try to turn the char into a pointer when there is a function for getting the pointer directly- GetItemDataPtr?  I switched to using that and it works perfectly.  The new code is the following:

int CApp20Dlg::Read_LB(int lb_id, CStringArray* pOutput)
{
      CListBox* pLB = (CListBox*) Get_Control(lb_id);
      int i, count = pLB->GetCount();
      static char item[LB_READBUFFER_SIZE];
      unsigned long pid;
      HANDLE process;
      LPVOID      pr_item;

      pOutput->RemoveAll();
      GetWindowThreadProcessId(pLB->m_hWnd, &pid);
      process=OpenProcess(PROCESS_VM_OPERATION|PROCESS_VM_READ|PROCESS_VM_WRITE|PROCESS_QUERY_INFORMATION, FALSE, pid);
      
      for(i=0; i<count; i++) {
            pr_item = pLB->GetItemDataPtr(i);
            ReadProcessMemory(process, pr_item, item, LB_READBUFFER_SIZE, NULL);
            pOutput->Add((CString) item);
      }
      
      CloseHandle(process);
      return count;
}

Anyway, thanks for all the help thinking this through.  I'm going to split the points 250 and 250 since you both put a bunch of thought into this.

Thanks again,

Nate
0
 
DanRollinsCommented:
For what it's worth, CListBox::GetItemDataPtr(j) is just ...

    return (LPVOID)::SendMessage(m_hWnd, LB_GETITEMDATA, j, 0);

which is identical to what I posted:

     int nRet= ::SendMessage( hWndLB, LB_GETITEMDATA, j, 0 );
      LPVOID p= (LPVOID)nRet;

What's troubling is that the other technique (of using LB_GETTEXT) should also have worked the same way.  In otherwords, it is still a mystery why that last piece of code that you showed caused problems.

I'd also like to ask this:

    Why did you award a grade of "B" ?

I spent several hours trying to replicate your problem and digging into obscure references to help you, and eventually providing the exactly correct answer.   Then you indicate (by issuing a B) that I should have worked a little harder...  It just seems strange.

-- Dan
0
 
NmcmurtrayAuthor Commented:
Dan,

Sorry, first time I've asked a question on here so I'm not all that familar with the grading scale.  I called it a "B" since it was a specific problem but the answer wasn't really a clean "here's the problem: your use of the pointer in LB_GETTEXT shouldn't be &pr_item it should be [whatever] and then the code worked" but more of a "well, it ought to work as writen, maybe try someting else to debug" and then from that it became clear we could just get it to work that way.  I take your point though, its more a presentation issue than anything else- if you had written "Nate- don't bother with LB_GETTEXT and trying to change the pointer to make it work, that's what LB_GETITEMDATA is for, just use that directly" no question I would have given it an A.

Happy to switch it to an A though since as I said it reflected my perception and lack of familiarity with the system here.  How do I go about that?
0
 
DanRollinsCommented:
With many problems, there are false starts.  I'd not worked on a ListBox problem in years (everybody uses CListCtrl these days) and I'd never tried to do this with an OwnerDraw in a different process.  Also, (shame on me) I did not re-read all the way to the *very bottom* of the API documentation (I stopped at the description of lParam)
     http://msdn.microsoft.com/library/en-us/shellcc/platform/commctls/listboxes/listboxreference/listboxmessages/lb_addstring.asp
nor did I put it together with the more specific info in
    http://msdn.microsoft.com/library/en-us/shellcc/platform/commctls/listboxes/listboxreference/listboxmessages/lb_addstring.asp

The point is, I stuck with you until it was solved.

To revise a grade:
Post a 0-pt question to Community Support (click "Support" atop this page.) In the question text, be sure to include the URL of this question.
Thanks!

-- Dan
0
 
NmcmurtrayAuthor Commented:
Sure, will do.  Thanks again for helping on this.  Much appreciated.
0
 
mahesh1402Commented:
Nmcmurtray ,

Just for Referene There is a nice one must read article which is somewhat closer to this..

'Stealing Program's Memory'
http://www.codeproject.com/threads/int64_memsteal.asp <=== Grabs items of ListView from another Processes.

-MAHESH

0
 
NmcmurtrayAuthor Commented:
Mahesh,

Look at the very last line of my very first post!  

Its the same cite to the same article- that's where I got the initial approach from.  I agree though- its a great article.

Nate
0

Featured Post

Vote for the Most Valuable Expert

It’s time to recognize experts that go above and beyond with helpful solutions and engagement on site. Choose from the top experts in the Hall of Fame or on the right rail of your favorite topic page. Look for the blue “Nominate” button on their profile to vote.

  • 15
  • 7
  • 6
  • +1
Tackle projects and never again get stuck behind a technical roadblock.
Join Now