拿到这个问题,我习惯性地会从VCL内核开始分析。
找到TRichEdit声明的单元,分析TRichEdit保存为RTF流的代码。
(分析VCL内核代码方便了解Windows标准API的封装和使用)
打开声明TRichEdit的ComCtrls.pas单元。
搜索"TRichEditStrings"
(保存流使用TRichEdit.Lines.SaveToStream方法,TRichEditStrings为TRichEdit.Line的类型)
TRichEditStrings = class(TStrings)
private
RichEdit:
TCustomRichEdit;
FPlainText:
Boolean;
FConverter:
TConversion;
procedure
EnableChange(const Value: Boolean);
protected
function
Get(Index: Integer): string; override;
function
GetCount: Integer; override;
procedure
Put(Index: Integer; const S: string); override;
procedure
SetUpdateState(Updating: Boolean); override;
procedure
SetTextStr(const Value: string); override;
public
destructor
Destroy; override;
procedure
Clear; override;
procedure
AddStrings(Strings: TStrings); override;
procedure
Delete(Index: Integer); override;
procedure
Insert(Index: Integer; const S: string); override;
procedure
LoadFromFile(const FileName: string); override;
procedure
LoadFromStream(Stream: TStream); override;
procedure
SaveToFile(const FileName: string); override;
procedure
SaveToStream(Stream: TStream); override;
property
PlainText: Boolean read FPlainText write FPlainText;
end;
寻找到SaveToStream的方法
procedure TRichEditStrings.SaveToStream(Stream: TStream);
var
EditStream: TEditStream;
TextType: Longint;
StreamInfo: TRichEditStreamInfo;
Converter: TConversion;
begin
if FConverter <>
nil then Converter := FConverter
else Converter :=
RichEdit.DefaultConverter.Create;
StreamInfo.Stream := Stream;
StreamInfo.Converter := Converter;
try
with
EditStream do
begin
dwCookie := LongInt(Pointer(@StreamInfo));
pfnCallBack := @StreamSave;
dwError := 0;
end;
if PlainText
then TextType := SF_TEXT
else
TextType := SF_RTF;
SendMessage(RichEdit.Handle, EM_STREAMOUT, TextType,
Longint(@EditStream));
if
EditStream.dwError <> 0 then
raise EOutOfResources.Create(sRichEditSaveFail);
finally
if
FConverter = nil then Converter.Free;
end;
end;
看关键的一句:“SendMessage(RichEdit.Handle,
EM_STREAMOUT, TextType, Longint(@EditStream));”
这下明白了,获取RTF关键的是向Richedit发送EM_STREAMOUT消息。
关于EM_STREAMOUT消息想了解更多可以查阅MSDN:
EM_STREAMOUT
wParam = (WPARAM) (UINT) uFormat;
lParam = (LPARAM) (EDITSTREAM FAR *) lpStream;
进程间的内存地址是相对的。
A进程$00450000内存地址值为34,那么B进程$00450000内存地址就不一定是34了。
在发送EM_STREAMOUT消息时,lParam参数表示的地址就是相对于目标进程的。
跨进程访问内存主要用到如下API函数:
GetWindowThreadProcessId -- 根据窗体句柄获得其所在的线程、进程ID
OpenProcess -- 打开进程并返回访问句柄
VirtualAllocEx -- 分配进程虚拟内存空间,返回所分配的内存地址。
VirtualFreeEx -- 释放进程虚拟内存空间
ReadProcessMemory、WriteProcessMemory -- 读写进程内存数据。
和以往不一样,这个消息用到了回调函数“pfnCallBack :=
@StreamSave;”
函数也是存放在内存中的数据(一些机器指令),访问函数同样会碰到进程间不能直接访问内存的问题。也就是说:需要将函数数据写入到目标进程中,才能被正常调用。
【如何获得函数的数据?】
function MyFunction(A, B: Integer): Integer;
begin
Result := A + B;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Edit1.Text :=
IntToStr(Integer(@MyFunction));
end;
函数地址容易得到。
调试如上代码,点击按钮获得函数地址,打开CPU查看器(Ctrl+Alt+C),定位函数地址。 这样将看到如图:
http://p.blog.csdn.net/images/p_blog_csdn_net/zswang/CPU%E6%B1%87%E7%BC%96.JPG
03C2 --------add eax,edx
7105 --------jon +$05
E81f43FBFF --call @IntOver
C3 --------- ret
前面就是十六进制数据,后面就是该数据表示的机器指令。
这些就是函数数据,将它写入到目标进程就可以调用了!!!
两个进程载入相同的DLL那么DLL的函数地址则是相同的,也就是说API函数SendMessage在A、B两个进程的地址一致。有了这点,利用系统API函数SendMessage发送WM_COPYDATA消息就可以交互数据了。
当然,如果指令里有相对地址的访问也得克隆才成,比如上面的"E81f43FBFF --call
@IntOver"就用到了相对地址。囧
可以将编译条件中“溢出、范围、IO"检查都关掉,减少相对地址的访问。
跨进程之前,先在本进程实验通过再说:
【第一个实验:正常调用回调函数】
function MySendMessage(hWnd: HWND; Msg: UINT; wParam: WPARAM;
lParam: LPARAM): LRESULT; stdcall;
begin
Result := SendMessage(hWnd, Msg, wParam,
lParam);
end;
type
TMySendMessage = function (hWnd: HWND; Msg:
UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var vMySendMessage: TMySendMessage = MySendMessage;
function EditStreamCallBack(dwCookie: Longint; pbBuff:
PByte;
cb: Longint; var pcb: Longint): Longint;
stdcall;
var
vCopyDataStruct: TCopyDataStruct;
begin
pcb := cb;
vCopyDataStruct.dwData := 0;
vCopyDataStruct.cbData := cb;
vCopyDataStruct.lpData := pbBuff;
vMySendMessage(dwCookie, WM_COPYDATA, 0,
Integer(@vCopyDataStruct));
Result := ERROR_SUCCESS;
end;
procedure TForm1.Button3Click(Sender: TObject);
var
vEditStream: TEditStream;
begin
vEditStream.dwCookie := Handle;
vEditStream.dwError := 0;
vEditStream.pfnCallback :=
EditStreamCallBack;
FMemoryStream := TMemoryStream.Create;
SendMessage(RichEdit1.Handle, EM_STREAMOUT,
SF_RTF, Integer(@vEditStream));
FMemoryStream.Position := 0;
Memo1.Lines.LoadFromStream(FMemoryStream);
FMemoryStream.Free;
end;
“EditStreamCallBack”就是要复制到目标进程的函数数据。
调试通过后,获得其十六进制数据。
{
55
} PUSH EBP
{
8BEC
} MOV EBP,ESP
{
83C4F4
} ADD ESP,$F4
{
8B4510
} MOV EAX,DWORD PTR [EBP+$10]
{
8B5514
} MOV EDX,DWORD PTR [EBP+$14]
{
8902
} MOV DWORD PTR [EDX],EAX
{
33D2
} XOR EDX,EDX
{
8955F4
} MOV DWORD PTR [EBP-$0C],EDX
{
8945F8
} MOV DWORD PTR [EBP-$08],EAX
{
8B450C
} MOV EAX,DWORD PTR [EBP+$0C]
{
8945FC
} MOV DWORD PTR [EBP-$04],EAX
{
8D45F4
} LEA EAX,DWORD PTR [EBP-$0C]
{
50
} PUSH EAX
{
6A00
} PUSH $00
{
6A4A
} PUSH $4A
{
8B4508
} MOV EAX,DWORD PTR [EBP+$08]
{
50
} PUSH EAX
{ FF15B88C4500} CALL DWORD PTR [$00458CB8]
{
33C0
} XOR EAX,EAX
{
8BE5
} MOV ESP,EBP
{
5D
} POP EBP
{
C21000
} RET $0010
其中{ FF15B88C4500} CALL DWORD PTR [$00458CB8]里的[$00458CB8]
里是相对地址,不同的调试环境会不一样。我们这里[$00458CB8]里存放的就是系统API函数SendMessage的地址。
【第二个实验:拼装函数数据】
const
EditStreamCallBackBytes =
#$55 +
//
PUSH EBP
#$8B#$EC +
//
MOV EBP,ESP
#$83#$C4#$F4 +
//
ADD ESP,$F4
#$8B#$45#$10 +
//
MOV EAX,DWORD PTR [EBP+$10]
#$8B#$55#$14 +
//
MOV EDX,DWORD PTR [EBP+$14]
#$89#$02 +
//
MOV DWORD PTR [EDX],EAX
#$33#$D2 +
//
XOR EDX,EDX
#$89#$55#$F4 +
//
MOV DWORD PTR [EBP-$0C],EDX
#$89#$45#$F8 +
//
MOV DWORD PTR [EBP-$08],EAX
#$8B#$45#$0C +
//
MOV EAX,DWORD PTR [EBP+$0C]
#$89#$45#$FC +
//
MOV DWORD PTR [EBP-$04],EAX
#$8D#$45#$F4 +
//
LEA EAX,DWORD PTR [EBP-$0C]
#$50 +
//
PUSH EAX
#$6A#$00 +
//
PUSH $00
#$6A#$4A +
//
PUSH $4A
#$8B#$45#$08 +
//
MOV EAX,DWORD PTR [EBP+$08]
#$50 +
//